better-supabase 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (536) hide show
  1. package/README.md +299 -41
  2. package/dist/access-sql-B0dF-SoF.js +181 -0
  3. package/dist/actor-CQTlc1Hh.d.ts +43 -0
  4. package/dist/adapter-Bc1o3zyy.js +1106 -0
  5. package/dist/agents-CVHGd7It.d.ts +94 -0
  6. package/dist/ai-cache-CBWs1W0k.js +80 -0
  7. package/dist/ai-cache-DaDu_G5L.d.ts +64 -0
  8. package/dist/ai-chat-7amvXjmQ.d.ts +590 -0
  9. package/dist/ai-chat-B24UI962.js +743 -0
  10. package/dist/ai-files-CSPNswUY.js +319 -0
  11. package/dist/ai-sdk/agents/index.d.ts +74 -0
  12. package/dist/ai-sdk/agents/index.js +159 -0
  13. package/dist/ai-sdk/batches/index.d.ts +74 -0
  14. package/dist/ai-sdk/batches/index.js +180 -0
  15. package/dist/ai-sdk/cache/index.d.ts +38 -0
  16. package/dist/ai-sdk/cache/index.js +127 -0
  17. package/dist/ai-sdk/chat/index.d.ts +2 -0
  18. package/dist/ai-sdk/chat/index.js +246 -0
  19. package/dist/ai-sdk/embeddings/index.d.ts +3 -0
  20. package/dist/ai-sdk/embeddings/index.js +2 -0
  21. package/dist/ai-sdk/files/index.d.ts +61 -0
  22. package/dist/ai-sdk/files/index.js +111 -0
  23. package/dist/ai-sdk/index.d.ts +145 -0
  24. package/dist/ai-sdk/index.js +174 -0
  25. package/dist/ai-sdk/mcp/index.d.ts +102 -0
  26. package/dist/ai-sdk/mcp/index.js +362 -0
  27. package/dist/ai-sdk/memory/index.d.ts +67 -0
  28. package/dist/ai-sdk/memory/index.js +175 -0
  29. package/dist/ai-sdk/react/index.d.ts +2 -0
  30. package/dist/ai-sdk/react/index.js +3 -0
  31. package/dist/ai-sdk/react/server.d.ts +6 -0
  32. package/dist/ai-sdk/react/server.js +7 -0
  33. package/dist/ai-sdk/workflow/index.d.ts +249 -0
  34. package/dist/ai-sdk/workflow/index.js +575 -0
  35. package/dist/ai-sdk/workflow/react/index.d.ts +20 -0
  36. package/dist/ai-sdk/workflow/react/index.js +66 -0
  37. package/dist/ai-sdk/workflow/react/server.d.ts +10 -0
  38. package/dist/ai-sdk/workflow/react/server.js +12 -0
  39. package/dist/ai-tasks-Cd1HMFvB.d.ts +131 -0
  40. package/dist/announcements-CiyOs-xP.js +107 -0
  41. package/dist/announcements-ZRGnScWI.d.ts +72 -0
  42. package/dist/api-keys-rCGtKaZA.d.ts +156 -0
  43. package/dist/assistant-DwdpDH1q.d.ts +147 -0
  44. package/dist/astro/index.d.ts +86 -0
  45. package/dist/astro/index.js +93 -0
  46. package/dist/attributes-5dYul0u0.js +13 -0
  47. package/dist/auth-link-BjFPqwLz.d.ts +97 -0
  48. package/dist/auth-link-DrDX9KOT.js +114 -0
  49. package/dist/authorization-BFCqamMs.d.ts +172 -0
  50. package/dist/before-route-CnR0C0Jr.js +30 -0
  51. package/dist/bind-Bu_ZNa3W.d.ts +62 -0
  52. package/dist/bind-l4kAvYSv.js +97 -0
  53. package/dist/block-events-CrT8r7Fe.js +76 -0
  54. package/dist/block-fields-GFkxl4dP.js +49 -0
  55. package/dist/block-fields-SJ6fEf3o.d.ts +31 -0
  56. package/dist/block-helpers-CKtbFcRv.d.ts +37 -0
  57. package/dist/block-helpers-DWHxHsEQ.js +190 -0
  58. package/dist/block-hooks-BmjMyXTa.js +114 -0
  59. package/dist/block-hooks-plbZ0AbJ.d.ts +107 -0
  60. package/dist/{kit-transport-DJlHQUBU.js → block-transport-BLr-ZgLc.js} +11 -7
  61. package/dist/block-transport-DvaoPuZ9.d.ts +48 -0
  62. package/dist/blocks/agents/index.d.ts +3 -0
  63. package/dist/blocks/agents/index.js +123 -0
  64. package/dist/blocks/ai-cache/index.d.ts +3 -0
  65. package/dist/blocks/ai-cache/index.js +3 -0
  66. package/dist/blocks/ai-chat/index.d.ts +31 -0
  67. package/dist/blocks/ai-chat/index.js +5 -0
  68. package/dist/blocks/ai-chat/react/index.d.ts +77 -0
  69. package/dist/blocks/ai-chat/react/index.js +387 -0
  70. package/dist/blocks/ai-chat/react/server.d.ts +16 -0
  71. package/dist/blocks/ai-chat/react/server.js +14 -0
  72. package/dist/blocks/ai-files/index.d.ts +14 -0
  73. package/dist/blocks/ai-files/index.js +3 -0
  74. package/dist/blocks/ai-providers/index.d.ts +7 -0
  75. package/dist/blocks/ai-providers/index.js +211 -0
  76. package/dist/blocks/ai-tasks/index.d.ts +3 -0
  77. package/dist/blocks/ai-tasks/index.js +203 -0
  78. package/dist/blocks/announcements/index.d.ts +3 -0
  79. package/dist/blocks/announcements/index.js +3 -0
  80. package/dist/blocks/announcements/react/index.d.ts +31 -0
  81. package/dist/blocks/announcements/react/index.js +86 -0
  82. package/dist/blocks/announcements/react/server.d.ts +6 -0
  83. package/dist/blocks/announcements/react/server.js +7 -0
  84. package/dist/blocks/api-keys/index.d.ts +35 -0
  85. package/dist/blocks/api-keys/index.js +334 -0
  86. package/dist/blocks/attachments/index.d.ts +210 -0
  87. package/dist/blocks/attachments/index.js +222 -0
  88. package/dist/blocks/audit/index.d.ts +366 -0
  89. package/dist/blocks/audit/index.js +714 -0
  90. package/dist/blocks/billing/index.d.ts +265 -0
  91. package/dist/blocks/billing/index.js +400 -0
  92. package/dist/blocks/comments/index.d.ts +200 -0
  93. package/dist/blocks/comments/index.js +207 -0
  94. package/dist/blocks/connectors/index.d.ts +3 -0
  95. package/dist/blocks/connectors/index.js +164 -0
  96. package/dist/blocks/data-lifecycle/index.d.ts +251 -0
  97. package/dist/blocks/data-lifecycle/index.js +353 -0
  98. package/dist/blocks/entitlements/index.d.ts +14 -0
  99. package/dist/blocks/entitlements/index.js +25 -0
  100. package/dist/blocks/flags/index.d.ts +226 -0
  101. package/dist/blocks/flags/index.js +282 -0
  102. package/dist/blocks/inbox/index.d.ts +6 -0
  103. package/dist/blocks/inbox/index.js +499 -0
  104. package/dist/blocks/inbox/react/index.d.ts +2 -0
  105. package/dist/blocks/inbox/react/index.js +225 -0
  106. package/dist/blocks/inbox/react/server.d.ts +10 -0
  107. package/dist/blocks/inbox/react/server.js +12 -0
  108. package/dist/blocks/index.d.ts +65 -0
  109. package/dist/blocks/index.js +77 -0
  110. package/dist/blocks/jobs/index.d.ts +297 -0
  111. package/dist/blocks/jobs/index.js +1051 -0
  112. package/dist/blocks/knowledge/index.d.ts +3 -0
  113. package/dist/blocks/knowledge/index.js +3 -0
  114. package/dist/blocks/memory/index.d.ts +8 -0
  115. package/dist/blocks/memory/index.js +289 -0
  116. package/dist/blocks/notifications/index.d.ts +297 -0
  117. package/dist/blocks/notifications/index.js +425 -0
  118. package/dist/blocks/notifications/react/index.d.ts +44 -0
  119. package/dist/blocks/notifications/react/index.js +67 -0
  120. package/dist/blocks/notifications/react/server.d.ts +6 -0
  121. package/dist/blocks/notifications/react/server.js +7 -0
  122. package/dist/blocks/onboarding/index.d.ts +3 -0
  123. package/dist/blocks/onboarding/index.js +64 -0
  124. package/dist/blocks/onboarding/react/index.d.ts +28 -0
  125. package/dist/blocks/onboarding/react/index.js +78 -0
  126. package/dist/blocks/onboarding/react/server.d.ts +7 -0
  127. package/dist/blocks/onboarding/react/server.js +7 -0
  128. package/dist/blocks/organizations/index.d.ts +241 -0
  129. package/dist/blocks/organizations/index.js +400 -0
  130. package/dist/blocks/outbox/index.d.ts +3 -0
  131. package/dist/blocks/outbox/index.js +266 -0
  132. package/dist/blocks/profiles/index.d.ts +40 -0
  133. package/dist/blocks/profiles/index.js +28 -0
  134. package/dist/blocks/push/index.d.ts +185 -0
  135. package/dist/blocks/push/index.js +258 -0
  136. package/dist/blocks/settings/index.d.ts +89 -0
  137. package/dist/blocks/settings/index.js +104 -0
  138. package/dist/blocks/sso/index.d.ts +198 -0
  139. package/dist/blocks/sso/index.js +1297 -0
  140. package/dist/blocks/usage/index.d.ts +4 -0
  141. package/dist/blocks/usage/index.js +233 -0
  142. package/dist/blocks/waitlist/index.d.ts +117 -0
  143. package/dist/blocks/waitlist/index.js +132 -0
  144. package/dist/blocks/webhooks/index.d.ts +304 -0
  145. package/dist/{webhooks → blocks/webhooks}/index.js +468 -148
  146. package/dist/blocks/workflow-builder/index.d.ts +21 -0
  147. package/dist/blocks/workflow-builder/index.js +5 -0
  148. package/dist/blocks/workflow-builder/react/index.d.ts +47 -0
  149. package/dist/blocks/workflow-builder/react/index.js +119 -0
  150. package/dist/blocks/workflow-builder/react/server.d.ts +8 -0
  151. package/dist/blocks/workflow-builder/react/server.js +11 -0
  152. package/dist/blocks/workflows/index.d.ts +3 -0
  153. package/dist/blocks/workflows/index.js +3 -0
  154. package/dist/blocks/workflows/react/index.d.ts +47 -0
  155. package/dist/blocks/workflows/react/index.js +137 -0
  156. package/dist/blocks/workflows/react/server.d.ts +8 -0
  157. package/dist/blocks/workflows/react/server.js +11 -0
  158. package/dist/{index-SaTsEfGP.d.ts → bucket-Cl46E3jS.d.ts} +134 -91
  159. package/dist/{simplify-_0oUKqCd.js → build-BXIzE6Xv.js} +222 -124
  160. package/dist/{channel-FQW-CnGR.d.ts → channel-_2Mx1Oyu.d.ts} +2 -2
  161. package/dist/channels-CkgQ_X60.js +114 -0
  162. package/dist/chat-sdk/index.d.ts +303 -0
  163. package/dist/chat-sdk/index.js +733 -0
  164. package/dist/chat-sdk/react/index.d.ts +2 -0
  165. package/dist/chat-sdk/react/index.js +3 -0
  166. package/dist/chat-sdk/react/server.d.ts +3 -0
  167. package/dist/chat-sdk/react/server.js +2 -0
  168. package/dist/claims-DDBQT6BK.js +61 -0
  169. package/dist/{claims-BAf6c7is.js → claims-PqzCxaTD.js} +39 -2
  170. package/dist/cli/bin.js +1 -1
  171. package/dist/cli/{codemod-DqV7612m.js → codemod-Bl_tQpHn.js} +105 -2
  172. package/dist/cli/config-B9HkL8nF.js +2 -0
  173. package/dist/cli/config-DU7EJImt.js +49 -0
  174. package/dist/cli/config-NiPxAmUF.js +1077 -0
  175. package/dist/cli/{dist-xn4wlboc.js → dist-DRAVaE6w.js} +111 -1
  176. package/dist/cli/{doctor-L7Z8mYTx.js → doctor-B4JOm_ox.js} +992 -316
  177. package/dist/cli/{env-DmKv_G4A.js → env-CJLbUVPB.js} +1 -1
  178. package/dist/cli/{env-jIFUADLY.js → env-qXgyMV6r.js} +2 -2
  179. package/dist/cli/fresh-import-BeYf9I3x.js +40 -0
  180. package/dist/cli/gen-BGLb_zou.js +3100 -0
  181. package/dist/cli/gen-DAH8FM2W.js +2 -0
  182. package/dist/cli/index.d.ts +44 -5
  183. package/dist/cli/index.js +7 -7
  184. package/dist/cli/{init-Saf5-R9T.js → init-BIIwclPg.js} +242 -27
  185. package/dist/cli/{introspect--H7oGmcd.js → introspect-DNUTuNvb.js} +5 -5
  186. package/dist/cli/{io-k41_z-jT.js → io-DI1mTdSA.js} +12 -3
  187. package/dist/cli/{keys-ByKHnvP9.js → keys-BGCnnb-s.js} +8 -6
  188. package/dist/cli/{ohash-D7bG_EOb.js → ohash-BjQY1cBb.js} +1 -1
  189. package/dist/cli/{openapi-Cp5Fyta-.js → openapi-BlRJ18s3.js} +2 -2
  190. package/dist/cli/{project-B_GeXyHh.js → project-DZRaBaY7.js} +11 -6
  191. package/dist/cli/read-sets-CETOSFx4.js +223 -0
  192. package/dist/cli/{run-BKR2W_79.js → run-DNWxHynX.js} +33 -23
  193. package/dist/cli/{seed-CUlqWh-8.js → seed-DQzqT7LP.js} +8 -6
  194. package/dist/cli/{skills-LJJIhLFR.js → skills-QQs4q68J.js} +3 -2
  195. package/dist/cli/sql-D0GUtUPp.js +536 -0
  196. package/dist/cli/{config-CPVpXP5I.js → supabase-toml-B-bIukQF.js} +72 -942
  197. package/dist/cli/typegen-version-DeFbtzYI.js +8 -0
  198. package/dist/cli/{version-BDJuO0UR.js → version-hr88OZG0.js} +1 -1
  199. package/dist/client/index.d.ts +3 -2
  200. package/dist/client/index.js +11 -71
  201. package/dist/client/native/index.d.ts +228 -0
  202. package/dist/client/native/index.js +361 -0
  203. package/dist/client-CI0Z2zWX.js +277 -0
  204. package/dist/cloud-event-BnmD5fb9.js +46 -0
  205. package/dist/cloud-event-C1JCc1bH.d.ts +50 -0
  206. package/dist/{compiler-BXH_aZ7N.d.ts → compiler-DY_wkj4R.d.ts} +2 -2
  207. package/dist/compose-BIBNgAnq.js +53 -0
  208. package/dist/composite-Bt1YK3Ke.d.ts +146 -0
  209. package/dist/composite-DhE66P5u.js +130 -0
  210. package/dist/config/index.d.ts +6 -4
  211. package/dist/config/index.js +5 -5
  212. package/dist/{config-T_ejwyd3.d.ts → config-D44Y7PAv.d.ts} +406 -205
  213. package/dist/{config-DreERlKo.js → config-Dn7F5k9A.js} +62 -14
  214. package/dist/connector-Dv8kK0MO.d.ts +107 -0
  215. package/dist/connectors-hkLCwyEz.d.ts +120 -0
  216. package/dist/context-BCrWe7ow.d.ts +237 -0
  217. package/dist/credentials/index.d.ts +31 -0
  218. package/dist/credentials/index.js +168 -0
  219. package/dist/cron-DVPUg5k3.js +145 -0
  220. package/dist/csv-CPAzEmB6.js +25 -0
  221. package/dist/db-exception-CnWcYPxL.js +15 -0
  222. package/dist/{define-cNKzqwL_.js → define-B-TVjC25.js} +784 -227
  223. package/dist/{define-Bp_BZa5h.d.ts → define-CJJkkr1-.d.ts} +353 -61
  224. package/dist/edge/index.d.ts +69 -11
  225. package/dist/edge/index.js +191 -75
  226. package/dist/elysia/index.d.ts +59 -0
  227. package/dist/elysia/index.js +62 -0
  228. package/dist/embeddings-4ZvFzjsU.js +127 -0
  229. package/dist/embeddings-C9KGQnXt.d.ts +68 -0
  230. package/dist/{refresh-BTrQ_ZTp.d.ts → ended-jLfvw8h-.d.ts} +20 -2
  231. package/dist/{entitlements-DUxU4oHe.d.ts → entitlements-DHrY2UDZ.d.ts} +16 -7
  232. package/dist/entitlements-DRl6ZJUm.js +24 -0
  233. package/dist/envelope-DqnYGRw7.js +47 -0
  234. package/dist/{errors-DtwXa6b8.js → errors-B11Duf73.js} +82 -13
  235. package/dist/errors-DLjJHPLK.js +44 -0
  236. package/dist/escape-like-DHfx6jRr.js +7 -0
  237. package/dist/escape-like-ImTK0ps_.d.ts +15 -0
  238. package/dist/eve/index.d.ts +220 -0
  239. package/dist/eve/index.js +504 -0
  240. package/dist/eve-gT2Fpjut.d.ts +151 -0
  241. package/dist/events/index.d.ts +4 -3
  242. package/dist/events/index.js +9 -50
  243. package/dist/events-DWDhx77e.d.ts +549 -0
  244. package/dist/executor-Bq28caBo.js +718 -0
  245. package/dist/executor-C-R4pucm.js +83 -0
  246. package/dist/executor-CaWigTyR.d.ts +148 -0
  247. package/dist/executor-D14tGopJ.d.ts +36 -0
  248. package/dist/{executor-Cghdstwu.d.ts → executor-DbvExCFC.d.ts} +7 -1
  249. package/dist/{sql-Coh6n4cC.js → executor-De1Of3m3.js} +210 -11
  250. package/dist/executor-DvsBbNBS.d.ts +261 -0
  251. package/dist/expo/index.d.ts +79 -0
  252. package/dist/expo/index.js +129 -0
  253. package/dist/expo-sqlite/index.d.ts +58 -0
  254. package/dist/expo-sqlite/index.js +104 -0
  255. package/dist/field-errors-BFOa7WhB.d.ts +99 -0
  256. package/dist/field-errors-VF1yb8yI.js +13 -0
  257. package/dist/framework-CONDyPYx.js +51 -0
  258. package/dist/framework-DuVb4Xa8.d.ts +25 -0
  259. package/dist/gateway-C1ssRHnf.d.ts +130 -0
  260. package/dist/graph-BGyLLFLX.js +265 -0
  261. package/dist/h3/index.d.ts +59 -0
  262. package/dist/h3/index.js +55 -0
  263. package/dist/h3/v1/index.d.ts +63 -0
  264. package/dist/h3/v1/index.js +49 -0
  265. package/dist/history-BZih6SFP.js +64 -0
  266. package/dist/hono/index.d.ts +56 -7
  267. package/dist/hono/index.js +97 -45
  268. package/dist/{react-C_s7kiNz.js → hooks-1BTCJsiw.js} +104 -133
  269. package/dist/http-Cv4T29xh.js +40 -0
  270. package/dist/http-DdYadv-o.js +126 -0
  271. package/dist/image-url-pIVlO6R1.js +41 -0
  272. package/dist/impersonation-DcqaTBHx.js +148 -0
  273. package/dist/inbox-i7E1ppo6.d.ts +384 -0
  274. package/dist/index-ACiNPN4Z.d.ts +74 -0
  275. package/dist/{index-DgILJRXj.d.ts → index-Bepi-Hgg.d.ts} +91 -28
  276. package/dist/index-BkWT_uBO.d.ts +51 -0
  277. package/dist/index-BpFYMsi0.d.ts +53 -0
  278. package/dist/{index-C7f8cOBM.d.ts → index-Bxih3l0u.d.ts} +2 -2
  279. package/dist/index-CaxrMD1A.d.ts +1 -0
  280. package/dist/index-ClmjK_o-.d.ts +55 -0
  281. package/dist/index-CtuakVRc.d.ts +267 -0
  282. package/dist/index-D48jVS-N.d.ts +246 -0
  283. package/dist/index-DDrYaUcq.d.ts +138 -0
  284. package/dist/index-DQh4VCc9.d.ts +98 -0
  285. package/dist/{live-D9W2M5aO.d.ts → index-DunNRdnT.d.ts} +136 -38
  286. package/dist/index.d.ts +65 -18
  287. package/dist/index.js +42 -14
  288. package/dist/invalidate-BBqhpxyB.d.ts +19 -0
  289. package/dist/json-CEnpTHlZ.js +16 -0
  290. package/dist/json-schema-CvER4kgT.js +502 -0
  291. package/dist/keys-4MnkWTTh.js +35 -0
  292. package/dist/kit-B5tid-q5.d.ts +60 -0
  293. package/dist/kit-Fm-Fv7q5.js +78 -0
  294. package/dist/knowledge-BXBbT0Tr.d.ts +176 -0
  295. package/dist/knowledge-CDmiMLew.js +255 -0
  296. package/dist/lint/index.d.ts +1 -1
  297. package/dist/lint/index.js +37 -0
  298. package/dist/list/index.d.ts +3 -2
  299. package/dist/list/index.js +1 -444
  300. package/dist/{index-fag8VJZ2.d.ts → list-query-BRt-zDF7.d.ts} +20 -12
  301. package/dist/list-query-gl808ac1.js +459 -0
  302. package/dist/{live-CdJBOc4f.js → live-CTAZQkwl.js} +99 -14
  303. package/dist/logger-DWVYUmwi.d.ts +18 -0
  304. package/dist/mcp/index.d.ts +38 -11
  305. package/dist/mcp/index.js +82 -46
  306. package/dist/mcp/sdk/index.d.ts +105 -0
  307. package/dist/mcp/sdk/index.js +126 -0
  308. package/dist/message-CaKlqdsX.js +219 -0
  309. package/dist/message-CfDoR0Y6.d.ts +94 -0
  310. package/dist/messages-B9-5887d.js +715 -0
  311. package/dist/messages-DYP0sUTe.d.ts +59 -0
  312. package/dist/{mfa-CusDgPaD.d.ts → mfa-Daak8ukG.d.ts} +8 -2
  313. package/dist/modules-DpPwzC7E.d.ts +192 -0
  314. package/dist/modules-dxrVK3ID.js +228 -0
  315. package/dist/nestjs/index.d.ts +68 -0
  316. package/dist/nestjs/index.js +164 -0
  317. package/dist/next/client/index.d.ts +35 -0
  318. package/dist/next/client/index.js +36 -0
  319. package/dist/next/image/index.js +3 -24
  320. package/dist/next/index.d.ts +150 -51
  321. package/dist/next/index.js +261 -107
  322. package/dist/node/index.d.ts +4 -0
  323. package/dist/node/index.js +210 -0
  324. package/dist/node-headers-AO06VhqH.d.ts +24 -0
  325. package/dist/node-headers-C71zNQHe.js +55 -0
  326. package/dist/nuxt/index.d.ts +60 -0
  327. package/dist/nuxt/index.js +64 -0
  328. package/dist/onboarding-BZGHYJks.d.ts +62 -0
  329. package/dist/openapi/index.d.ts +1 -1
  330. package/dist/openapi/index.js +3 -3
  331. package/dist/orpc/index.d.ts +70 -11
  332. package/dist/orpc/index.js +73 -24
  333. package/dist/otel/index.d.ts +7 -7
  334. package/dist/otel/index.js +87 -57
  335. package/dist/outbox-Cq7pPYvl.d.ts +161 -0
  336. package/dist/{plugin-o144f322.d.ts → plugin-FGY8UhYm.d.ts} +30 -10
  337. package/dist/plugin-tfXW1-qO.js +45 -0
  338. package/dist/plugins/actor/index.d.ts +2 -2
  339. package/dist/plugins/actor/index.js +3 -3
  340. package/dist/plugins/rules/index.d.ts +11 -4
  341. package/dist/plugins/rules/index.js +40 -13
  342. package/dist/plugins/soft-delete/index.d.ts +5 -4
  343. package/dist/plugins/soft-delete/index.js +10 -5
  344. package/dist/plugins/tenant/index.d.ts +2 -2
  345. package/dist/plugins/tenant/index.js +18 -11
  346. package/dist/plugins/timestamps/index.d.ts +5 -3
  347. package/dist/plugins/timestamps/index.js +6 -4
  348. package/dist/plugins/validation/index.d.ts +2 -2
  349. package/dist/plugins/validation/index.js +10 -6
  350. package/dist/{pool-C9voM4Kw.d.ts → pool-CAu2nLrL.d.ts} +2 -2
  351. package/dist/postgres/index.d.ts +4 -4
  352. package/dist/postgres/index.js +1 -2
  353. package/dist/{postgrest-CruYKAvV.d.ts → postgrest-BLVdEnfe.d.ts} +13 -3
  354. package/dist/powersync/index.d.ts +43 -0
  355. package/dist/powersync/index.js +199 -0
  356. package/dist/powersync/react/index.d.ts +87 -0
  357. package/dist/powersync/react/index.js +108 -0
  358. package/dist/{problem-BW1KhzSG.js → problem-BreOl8b8.js} +14 -3
  359. package/dist/{problem-Blxe8S31.d.ts → problem-DD8LZFjc.d.ts} +21 -3
  360. package/dist/provider-BH5UWZJ6.d.ts +113 -0
  361. package/dist/provider-Di3sRutW.js +57 -0
  362. package/dist/query/index.d.ts +4 -2
  363. package/dist/query/index.js +4 -3
  364. package/dist/{query-CCbo44lG.js → query-BQ-NaZon.js} +141 -24
  365. package/dist/{queue-BYUCjr59.d.ts → queue-CLJurhdy.d.ts} +258 -83
  366. package/dist/react/index.d.ts +11 -7
  367. package/dist/react/index.js +7 -3
  368. package/dist/react/native/index.d.ts +116 -0
  369. package/dist/react/native/index.js +130 -0
  370. package/dist/react/server.d.ts +28 -10
  371. package/dist/react/server.js +22 -5
  372. package/dist/react/session.d.ts +2 -2
  373. package/dist/react-4ZqkP8da.js +79 -0
  374. package/dist/react-F80mF4-t.js +409 -0
  375. package/dist/react-router/index.d.ts +83 -0
  376. package/dist/react-router/index.js +89 -0
  377. package/dist/react-topic-seAWjL3_.js +38 -0
  378. package/dist/{read-set-DQFt1N_Y.js → read-set-DuKWC9Mj.js} +67 -25
  379. package/dist/realtime/index.d.ts +4 -5
  380. package/dist/realtime/index.js +144 -82
  381. package/dist/refusal-CeJWxmkH.js +61 -0
  382. package/dist/refusal-D1rgbzNq.d.ts +15 -0
  383. package/dist/replicas-CwrYp5Zj.js +94 -0
  384. package/dist/{view-Bq64ki8-.js → resolve-D_IcnvFd.js} +239 -289
  385. package/dist/{resolve-C15TJq77.d.ts → resolve-KW8nCeL6.d.ts} +29 -45
  386. package/dist/{resource-CRxxwkQq.js → resource-BsvzCMlX.js} +4 -4
  387. package/dist/{resource-D8iqWMr-.d.ts → resource-D6Uc7Sk7.d.ts} +5 -5
  388. package/dist/respond-BUgsXNgk.js +91 -0
  389. package/dist/respond-DoBijysA.d.ts +93 -0
  390. package/dist/result-D7y2fT87.d.ts +282 -0
  391. package/dist/{result-BKiliPY7.js → result-DzUbqgdW.js} +27 -2
  392. package/dist/run-state-CPo-1X2B.d.ts +12 -0
  393. package/dist/run-state-CgBKzYMW.js +13 -0
  394. package/dist/search-CUWCRYKL.js +9 -0
  395. package/dist/server/index.d.ts +143 -21
  396. package/dist/server/index.js +46 -14
  397. package/dist/{respond-D2OQl62-.d.ts → server-CeVeeXwz.d.ts} +225 -80
  398. package/dist/session-ChiXDZdZ.d.ts +72 -0
  399. package/dist/shared-CBxosKK3.js +75 -0
  400. package/dist/{shared-D1dRUrAg.js → shared-CxWmcyqB.js} +2 -1
  401. package/dist/shared-DDSjEGDR.d.ts +11 -0
  402. package/dist/sign-in-BCV5S2WZ.d.ts +49 -0
  403. package/dist/sign-in-Cb1mwv6X.js +86 -0
  404. package/dist/{signers-Cc6r98SW.d.ts → signers-4y62_VIi.d.ts} +13 -9
  405. package/dist/simplify-DDO0j0LI.js +83 -0
  406. package/dist/solid/index.d.ts +131 -0
  407. package/dist/solid/index.js +344 -0
  408. package/dist/solid-start/index.d.ts +75 -0
  409. package/dist/solid-start/index.js +58 -0
  410. package/dist/{spec-pins-C3oc-Tdp.js → spec-pins--j6mTok4.js} +8 -1
  411. package/dist/{spec-pins-CqorWDnO.d.ts → spec-pins-C89s6DyW.d.ts} +7 -0
  412. package/dist/sql/index.d.ts +330 -194
  413. package/dist/sql/index.js +34901 -6489
  414. package/dist/ssr/index.d.ts +6 -5
  415. package/dist/ssr/index.js +3 -3
  416. package/dist/{standard-D7r6dMUu.js → standard-BK0cqJ7y.js} +4 -3
  417. package/dist/{standard-J4YgMOLm.d.ts → standard-CYsm7xpT.d.ts} +1 -1
  418. package/dist/stats-D_AD9GGu.js +1123 -0
  419. package/dist/storage/index.d.ts +5 -5
  420. package/dist/storage/index.js +1027 -368
  421. package/dist/store-BdH5ruDx.js +66 -0
  422. package/dist/store-CGjgWu5n.d.ts +68 -0
  423. package/dist/streams/index.d.ts +76 -0
  424. package/dist/streams/index.js +77 -0
  425. package/dist/streams/redis/index.d.ts +52 -0
  426. package/dist/streams/redis/index.js +185 -0
  427. package/dist/stripe-BMj5LtsT.js +38 -0
  428. package/dist/stripe-D04h-QBT.d.ts +136 -0
  429. package/dist/{support-Dk6vdmEw.js → support-BivNqSpK.js} +35 -12
  430. package/dist/{support-view-Da5peKFg.d.ts → support-view-33Oi07TT.d.ts} +1 -1
  431. package/dist/svelte/index.d.ts +114 -0
  432. package/dist/svelte/index.js +344 -0
  433. package/dist/sveltekit/index.d.ts +116 -0
  434. package/dist/sveltekit/index.js +106 -0
  435. package/dist/tags-B99_U9Pp.js +39 -0
  436. package/dist/tags-Cx-LnIQP.d.ts +30 -0
  437. package/dist/tanstack-db/index.d.ts +71 -0
  438. package/dist/tanstack-db/index.js +72 -0
  439. package/dist/tanstack-start/index.d.ts +75 -0
  440. package/dist/tanstack-start/index.js +95 -0
  441. package/dist/tee-CoEppCER.js +101 -0
  442. package/dist/{template-TxrGI5jR.js → template--jDkl3UP.js} +34 -13
  443. package/dist/template-CwO38Do8.d.ts +6 -0
  444. package/dist/temporal-BJGitm-h.js +79 -0
  445. package/dist/{temporal-required-CAt4Dc9Z.js → temporal-required-BVvW7SYJ.js} +3 -2
  446. package/dist/tenant-I1TEZ_BX.js +27 -0
  447. package/dist/tenant-icPeuUBN.d.ts +17 -0
  448. package/dist/testing/index.d.ts +343 -37
  449. package/dist/testing/index.js +1186 -79
  450. package/dist/timing-CyTQPubY.js +92 -0
  451. package/dist/ttl-Hr34gRu8.js +12 -0
  452. package/dist/{types-Dnf_wRDz.d.ts → types-6BdhkmZe.d.ts} +111 -45
  453. package/dist/types-BI-0Sl2v.d.ts +359 -0
  454. package/dist/types-C05bxW-W.d.ts +220 -0
  455. package/dist/types-DGs5L0VN.d.ts +192 -0
  456. package/dist/types-TaiEeBGA.d.ts +156 -0
  457. package/dist/usage-CrvrL2vw.d.ts +177 -0
  458. package/dist/user-change-XzvgafU4.js +130 -0
  459. package/dist/vault-CSO7ss5C.d.ts +50 -0
  460. package/dist/vercel-connect/index.d.ts +47 -0
  461. package/dist/vercel-connect/index.js +119 -0
  462. package/dist/{verify-Ce49ETPz.js → verify-DL41hbvY.js} +85 -13
  463. package/dist/verify-DPuWngD4.d.ts +172 -0
  464. package/dist/{version-DM_Roffb.js → version-Bvv6aLjZ.js} +1 -1
  465. package/dist/view-BeFcRV6G.js +99 -0
  466. package/dist/{view-BEf0diC5.d.ts → view-DbxLNu8L.d.ts} +35 -32
  467. package/dist/vue/index.d.ts +141 -0
  468. package/dist/vue/index.js +362 -0
  469. package/dist/watch-DI34G86j.d.ts +60 -0
  470. package/dist/watch-Xuxrvxq-.js +122 -0
  471. package/dist/{wire-D2dT86vi.js → wire-DtZQ4sdE.js} +15 -5
  472. package/dist/workflow-builder-DJ1oHn8g.js +398 -0
  473. package/dist/workflow-sdk/builder/index.d.ts +85 -0
  474. package/dist/workflow-sdk/builder/index.js +250 -0
  475. package/dist/workflow-sdk/index.d.ts +14 -0
  476. package/dist/workflow-sdk/index.js +3 -0
  477. package/dist/workflow-sdk/world/index.d.ts +107 -0
  478. package/dist/workflow-sdk/world/index.js +702 -0
  479. package/dist/workflow-sdk-B3yLD6Br.d.ts +98 -0
  480. package/dist/workflow-sdk-C2WJ_G2O.js +173 -0
  481. package/dist/workflow-sdk-world-D-prir6O.js +1397 -0
  482. package/dist/workflows-B0j9WKPu.d.ts +185 -0
  483. package/dist/workflows-wu1WWBn7.js +216 -0
  484. package/package.json +470 -29
  485. package/schemas/ai-message-v1.json +258 -0
  486. package/schemas/config-v1.json +1074 -140
  487. package/schemas/doctor-report-v1.json +21 -1
  488. package/schemas/snapshot-v2.json +8 -0
  489. package/skills/better-supabase/SKILL.md +41 -12
  490. package/skills/better-supabase/references/frontend.md +96 -0
  491. package/skills/better-supabase/references/plugins.md +10 -6
  492. package/skills/better-supabase/references/troubleshooting.md +2 -1
  493. package/skills/better-supabase-api/SKILL.md +76 -22
  494. package/skills/better-supabase-api/references/adapters.md +207 -0
  495. package/skills/better-supabase-api/references/ai.md +125 -0
  496. package/skills/better-supabase-api/references/expo.md +65 -0
  497. package/skills/better-supabase-api/references/powersync.md +35 -0
  498. package/skills/better-supabase-api/references/streams-credentials.md +105 -0
  499. package/skills/better-supabase-auth/SKILL.md +62 -18
  500. package/skills/better-supabase-auth/references/authorization-providers.md +51 -0
  501. package/skills/better-supabase-testing/SKILL.md +3 -1
  502. package/dist/cli/config-dtoekM0v.js +0 -2
  503. package/dist/cli/gen-BJc9vdyu.js +0 -2
  504. package/dist/cli/gen-C2osRcr5.js +0 -2204
  505. package/dist/cli/read-sets-BfMQcKy8.js +0 -289
  506. package/dist/cli/sql-DzPbfngx.js +0 -348
  507. package/dist/entitlements-CTEBm7IB.js +0 -20
  508. package/dist/events-aIO7vsIk.d.ts +0 -230
  509. package/dist/executor-BunPOO-k.d.ts +0 -402
  510. package/dist/executor-XycESQPH.js +0 -94
  511. package/dist/impersonation-VfaGc4Q_.js +0 -41
  512. package/dist/index-BGdgBPdf.d.ts +0 -161
  513. package/dist/index-DOESKG27.d.ts +0 -100
  514. package/dist/index-DmXSxbLi.d.ts +0 -62
  515. package/dist/index-Dxn2a5cw.d.ts +0 -50
  516. package/dist/jobs/index.d.ts +0 -226
  517. package/dist/jobs/index.js +0 -1007
  518. package/dist/json-schema-m1fiwKgN.js +0 -208
  519. package/dist/kit-events-CTFDfSwb.js +0 -60
  520. package/dist/kit-transport-B82k1djG.d.ts +0 -33
  521. package/dist/kits-CGLLrNPd.js +0 -157
  522. package/dist/notifications/index.d.ts +0 -162
  523. package/dist/notifications/index.js +0 -264
  524. package/dist/orgs/index.d.ts +0 -132
  525. package/dist/orgs/index.js +0 -245
  526. package/dist/permdock-sql-C2Inc6Xc.d.ts +0 -29
  527. package/dist/permdock-sql-DgOfeixi.js +0 -51
  528. package/dist/plugin-DwARMuQp.js +0 -59
  529. package/dist/server-Csx2sRjg.js +0 -655
  530. package/dist/stats-Bqob3WqT.js +0 -592
  531. package/dist/support-DT-OhSxD.d.ts +0 -88
  532. package/dist/template-mbvlysOo.d.ts +0 -6
  533. package/dist/temporal-CVlIV2DZ.js +0 -40
  534. package/dist/user-change-CHFEUro1.js +0 -49
  535. package/dist/webhooks/index.d.ts +0 -285
  536. package/skills/better-supabase-auth/references/permdock.md +0 -100
@@ -1,19 +1,20 @@
1
1
  import { a as list, r as defineCliCommand } from "./command-CK14fP91.js";
2
2
  import { n as CliError } from "./errors-4v4DTtda.js";
3
- import { r as writeIfChanged } from "./io-k41_z-jT.js";
3
+ import { i as writeIfChanged, t as display } from "./io-DI1mTdSA.js";
4
4
  import { i as plain, r as painter } from "./style-C0NFdJRL.js";
5
- import { t as VERSION } from "./version-BDJuO0UR.js";
6
- import { c as diffEngine, d as pgFunctionHooks, f as readSupabaseToml, l as httpHooks, m as tomlGet, p as schemaPaths, s as stdinDatabaseUrl, u as migrationCommand } from "./config-CPVpXP5I.js";
7
- import { a as parseHookMarker, i as parseGrantsMarker, o as permdockSource, s as readPermdock, t as compiledReadSets } from "./read-sets-BfMQcKy8.js";
5
+ import { t as VERSION } from "./version-hr88OZG0.js";
6
+ import { s as providerLabel, t as compiledReadSets } from "./read-sets-CETOSFx4.js";
7
+ import { s as stdinDatabaseUrl } from "./config-NiPxAmUF.js";
8
+ import { c as migrationCommand, d as schemaPaths, i as findRepoRoot, l as pgFunctionHooks, n as declarativeSchemasDir, p as tomlGet, r as diffEngine, s as httpHooks, u as readSupabaseToml } from "./supabase-toml-B-bIukQF.js";
8
9
  import { t as byCodePoint } from "./compare-BmIWVEui.js";
9
- import { C as catalogOf, D as policyObject, E as lineOf, O as qualified, S as entitlementsKit, T as exposedSchemas, c as openSource, d as snapshotFile, h as CACHE_DIR, k as tableObject, o as loadSnapshot, r as renderFiles, s as managementTarget, w as exposed, x as PERMDOCK_RULES } from "./gen-C2osRcr5.js";
10
+ import { A as exposed, C as MetadataRejectedError, D as accessModule, E as AUTHORIZATION_RULES, F as qualified, I as tableObject, M as lineOf, N as permissionCatalogKeys, O as entitlementsModule, P as policyObject, S as configFor, _ as METADATA_GAPS_TEXT, a as renderFiles, c as loadSnapshot, j as exposedSchemas, k as catalogOf, l as managementTarget, p as snapshotFile, r as readMetadataSnapshot, u as openSource, v as CACHE_DIR } from "./gen-BGLb_zou.js";
10
11
  import { n as withSpinner } from "./prompts-BNcNfAve.js";
11
12
  import { t as connect } from "./db-DOBPt660.js";
12
13
  import { dirname, resolve } from "node:path";
13
14
  import { existsSync } from "node:fs";
14
15
  import { glob, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
15
- import { SQL_MODULES, contractSignature, customContracts, kitDeprecations, kitFileVersion, kitLayout, migrationOptionUses, moduleVersion, renderKit, sameKitFile } from "better-supabase/sql";
16
16
  import { DEFAULT_ACTIVE_TENANT, tenantClaimPaths } from "better-supabase/config";
17
+ import { SQL_MODULES, contractSignature, customContracts, migrationOptionUses, moduleDeprecations, moduleEventTriggers, moduleFileVersion, moduleLayout, moduleVersion, renderModules, sameModuleFile, sharedRolesProblems } from "better-supabase/sql";
17
18
  import { createHash } from "node:crypto";
18
19
  import { defineBucket, parseSize } from "better-supabase/storage";
19
20
  //#region src/cli/doctor/advisors.ts
@@ -46,6 +47,17 @@ function managementAdvisors(options) {
46
47
  }
47
48
  };
48
49
  }
50
+ /** The `[api] schemas` list of a `config.toml`, when it names any. */
51
+ function apiSchemas(document) {
52
+ const value = document ? tomlGet(document, ["api", "schemas"]) : void 0;
53
+ if (!Array.isArray(value)) return [];
54
+ return value.filter((schema) => typeof schema === "string" && schema.trim() !== "");
55
+ }
56
+ const literal$1 = (value) => `'${value.replaceAll("'", "''")}'`;
57
+ /** The read-only transaction splinter runs in, with `pgrst.db_schemas` set first. */
58
+ function splinterScript(sql, schemas = []) {
59
+ return `begin read only;\n${schemas.length > 0 ? `set local pgrst.db_schemas = ${literal$1(schemas.join(","))};\n` : ""}${sql}\n;\nrollback;`;
60
+ }
49
61
  const sha256 = (text) => createHash("sha256").update(text).digest("hex");
50
62
  /** Downloads (once) and verifies the pinned `splinter.sql`. */
51
63
  async function splinterSql(options) {
@@ -75,7 +87,7 @@ function splinterAdvisors(queryable, describe, options) {
75
87
  let all;
76
88
  const run = async () => {
77
89
  const sql = await splinterSql(options);
78
- return lintRows(await queryable.query(`begin read only;\n${sql}\n;\nrollback;`)).filter(isLint);
90
+ return lintRows(await queryable.query(splinterScript(sql, options.schemas))).filter(isLint);
79
91
  };
80
92
  return {
81
93
  describe: `${describe} (splinter ${SPLINTER_COMMIT.slice(0, 7)})`,
@@ -87,6 +99,153 @@ function splinterAdvisors(queryable, describe, options) {
87
99
  };
88
100
  }
89
101
  //#endregion
102
+ //#region src/cli/doctor/advisor-rules.ts
103
+ /** One request per advisor category, shared by the advisor rules and the rules that defer to them. */
104
+ const fetched = /* @__PURE__ */ new WeakMap();
105
+ function lintsOf(advisors, category) {
106
+ let byCategory = fetched.get(advisors);
107
+ if (!byCategory) {
108
+ byCategory = /* @__PURE__ */ new Map();
109
+ fetched.set(advisors, byCategory);
110
+ }
111
+ let lints = byCategory.get(category);
112
+ if (!lints) {
113
+ lints = advisors.lints(category);
114
+ byCategory.set(category, lints);
115
+ }
116
+ return lints;
117
+ }
118
+ const ADVISOR_SEVERITY = {
119
+ ERROR: "error",
120
+ WARN: "warning",
121
+ INFO: "info"
122
+ };
123
+ function lintObject(lint) {
124
+ const { schema, name, type } = lint.metadata ?? {};
125
+ if (typeof schema !== "string" || typeof name !== "string") return void 0;
126
+ if (type === "table" || type === "view") return {
127
+ kind: "table",
128
+ schema,
129
+ name
130
+ };
131
+ if (type === "function") return {
132
+ kind: "function",
133
+ schema,
134
+ name
135
+ };
136
+ }
137
+ function lintFinding(lint) {
138
+ const object = lintObject(lint);
139
+ return {
140
+ severity: ADVISOR_SEVERITY[lint.level] ?? "warning",
141
+ title: lint.title,
142
+ message: `${lint.detail.replaceAll("\\`", "`")} [${lint.name}]`,
143
+ target: object ? `${object.schema}.${object.name}` : lint.cache_key,
144
+ help: lint.remediation,
145
+ ...object ? { object } : {}
146
+ };
147
+ }
148
+ /** A rule that reports one category of Supabase's advisors (splinter) as findings. */
149
+ function advisorRule(code, category, title, description) {
150
+ return {
151
+ code,
152
+ severity: "warning",
153
+ title,
154
+ description,
155
+ async check(context) {
156
+ const { advisors } = context;
157
+ if (!advisors) return [];
158
+ if ("skipped" in advisors) return [{
159
+ severity: "info",
160
+ message: `Skipped the ${category} advisor: ${advisors.skipped}`
161
+ }];
162
+ try {
163
+ return (await lintsOf(advisors, category)).map(lintFinding);
164
+ } catch (cause) {
165
+ return [{ message: `The ${category} advisor could not run (${advisors.describe}): ${cause instanceof Error ? cause.message : String(cause)}` }];
166
+ }
167
+ }
168
+ };
169
+ }
170
+ /** The performance advisor rule the duplicate rules defer to. */
171
+ const PERFORMANCE_ADVISOR = "BS200";
172
+ /**
173
+ * The tables splinter reports `lint` for (`schema.table`), when the
174
+ * performance advisor runs in this run and its lints loaded. A rule that
175
+ * repeats the lint (BS207, BS216) drops its findings for these tables; with
176
+ * `undefined` (no advisors, a saved snapshot, an advisor error, or BS200 not
177
+ * in the run) it reports them itself.
178
+ */
179
+ async function splinterTables(context, lint) {
180
+ const { advisors } = context;
181
+ if (!advisors || "skipped" in advisors) return void 0;
182
+ if (!context.codes?.includes(PERFORMANCE_ADVISOR)) return void 0;
183
+ let lints;
184
+ try {
185
+ lints = await lintsOf(advisors, "performance");
186
+ } catch {
187
+ return;
188
+ }
189
+ return new Set(lints.flatMap((entry) => {
190
+ if (entry.name !== lint) return [];
191
+ const object = lintObject(entry);
192
+ return object ? [`${object.schema}.${object.name}`] : [];
193
+ }));
194
+ }
195
+ //#endregion
196
+ //#region src/cli/doctor/cookies.ts
197
+ const TOKENS_ONLY = /\bencode\s*:\s*["'`]tokens-only["'`]/;
198
+ /** Imports that create the browser client, which writes the session cookie in the browser. */
199
+ const BROWSER_SIDE = /from\s+["']better-supabase\/client["']|\bcreateBrowserClient\s*\(/;
200
+ /** Imports that read or refresh the session cookie on the server. */
201
+ const SERVER_SIDE = /from\s+["']better-supabase\/(?:server|next|ssr|hono|orpc|edge|expo)["']|\bcreateServerClient\s*\(/;
202
+ function cookieSide(file) {
203
+ const browser = BROWSER_SIDE.test(file.text);
204
+ if (browser === SERVER_SIDE.test(file.text)) return void 0;
205
+ return browser ? "browser" : "server";
206
+ }
207
+ /**
208
+ * Files on one side of the session cookie that set `encode: 'tokens-only'`
209
+ * while files on the other side don't (BS412).
210
+ */
211
+ function encodingDrift(context) {
212
+ const sides = context.sources.flatMap((file) => {
213
+ const side = cookieSide(file);
214
+ return side ? [{
215
+ file,
216
+ side,
217
+ tokensOnly: TOKENS_ONLY.test(file.text)
218
+ }] : [];
219
+ });
220
+ const tokensOnly = sides.filter((entry) => entry.tokensOnly);
221
+ if (tokensOnly.length === 0) return [];
222
+ const findings = [];
223
+ for (const side of ["browser", "server"]) {
224
+ const set = tokensOnly.find((entry) => entry.side === side);
225
+ if (!set) continue;
226
+ const other = side === "browser" ? "server" : "browser";
227
+ const unset = sides.filter((entry) => entry.side === other && !entry.tokensOnly);
228
+ if (unset.length === 0) continue;
229
+ const line = lineOf(set.file.text, TOKENS_ONLY) ?? 1;
230
+ findings.push({
231
+ message: `${set.file.path} writes the session cookie with \`encode: 'tokens-only'\`, but the ${other} side (${unset.map((entry) => entry.file.path).join(", ")}) uses the default \`user-and-tokens\`. Set the same \`encode\` on both sides.`,
232
+ target: `${set.file.path}:encode`,
233
+ location: {
234
+ file: set.file.path,
235
+ line
236
+ }
237
+ });
238
+ }
239
+ return findings;
240
+ }
241
+ const COOKIE_RULES = [{
242
+ code: "BS412",
243
+ severity: "info",
244
+ title: "Session cookie encoding differs between server and browser",
245
+ description: "`@supabase/ssr` needs the same `cookies.encode` on the server and in the browser client. With `tokens-only` on one side only, the other side writes the user object back into the cookie, or reads a session without one and `session.user` throws.",
246
+ check: encodingDrift
247
+ }];
248
+ //#endregion
90
249
  //#region src/cli/doctor/live.ts
91
250
  /** Statements slower than this (ms, mean) with more calls than `SLOW_CALLS` are BS209. */
92
251
  const SLOW_MEAN_MS = 50;
@@ -316,81 +475,61 @@ const LIVE_RULES = [
316
475
  //#region src/cli/doctor/hooks.ts
317
476
  /** Claims past this size make every request carry a large cookie and header. */
318
477
  const HOOK_CLAIMS_LIMIT = 2048;
319
- /** PermDock's default `supabase.hook.budget`, over `memberships` plus `attrs`. */
320
- const PERMDOCK_CLAIMS_LIMIT = 1024;
321
478
  /**
322
- * The BS405 limits. With PermDock, `doctor.claimsLimit` (else the manifest's
323
- * budget) is its budget for `memberships` plus `attrs` and the whole token
324
- * keeps 2048; without it, `doctor.claimsLimit` limits the whole token.
479
+ * The BS405 limits. With a provider hook budget, `doctor.claimsLimit` (else
480
+ * the budget's bytes) limits its claims and the whole token keeps 2048;
481
+ * without one, `doctor.claimsLimit` limits the whole token.
325
482
  */
326
483
  function claimsLimits(context) {
327
484
  const configured = context.config.doctor.claimsLimit;
328
- return context.permdock ? {
485
+ const budget = providerHook(context)?.budget;
486
+ return budget ? {
329
487
  token: HOOK_CLAIMS_LIMIT,
330
- budget: configured ?? context.permdock.manifest?.budget ?? PERMDOCK_CLAIMS_LIMIT
331
- } : {
332
- token: configured ?? HOOK_CLAIMS_LIMIT,
333
- budget: void 0
334
- };
488
+ budget: {
489
+ bytes: configured ?? budget.bytes,
490
+ claims: budget.claims,
491
+ ...budget.truncatedClaim ? { truncatedClaim: budget.truncatedClaim } : {}
492
+ }
493
+ } : { token: configured ?? HOOK_CLAIMS_LIMIT };
335
494
  }
336
- /** The kit function that fills PermDock's `memberships` claim. */
337
- const KIT_MEMBERSHIPS = /better_supabase\s*\.\s*membership_claims\b/i;
338
- const PERMDOCK_CALL = /\bpermdock\w*\s*\(|permdock\s*\./i;
339
- /** Only PermDock's generated hook sets this claim, so it marks that hook's body. */
340
- const PERMDOCK_HOOK = /'\{\s*(?:claims\s*,\s*)?memberships_truncated\s*\}'/i;
495
+ /** The module function that fills the `memberships` claim. */
496
+ const MODULE_MEMBERSHIPS = /better_supabase\s*\.\s*membership_claims\b/i;
497
+ const providerHook = (context) => context.config.authorization?.tokenHook;
498
+ const splitName = (qualified) => {
499
+ const dot = qualified.indexOf(".");
500
+ return {
501
+ schema: qualified.slice(0, dot),
502
+ name: qualified.slice(dot + 1)
503
+ };
504
+ };
341
505
  const escapeRegExp$1 = (text) => text.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
342
506
  /**
343
- * The claims PermDock's hook writes that a second writer would contradict.
344
- * The tenant claim is the manifest's when there is one, since that is the
345
- * claim PermDock's hook really writes.
507
+ * The claims the provider's hook writes that a second writer would
508
+ * contradict, with the tenant claim it writes to.
346
509
  */
347
- function ownedClaims(context) {
348
- const manifest = context.permdock?.manifest;
349
- const tenant = manifest?.rls?.tenantClaim ?? manifest?.tenantClaim ?? context.config.claims.tenant;
350
- return [.../* @__PURE__ */ new Set([
351
- "roles",
352
- "user_role",
353
- "memberships",
354
- tenant,
355
- context.config.claims.tenant
356
- ])];
510
+ function ownedClaims(hook) {
511
+ return [.../* @__PURE__ */ new Set([...hook.ownedClaims, ...hook.tenantClaim ? [hook.tenantClaim] : []])];
357
512
  }
358
513
  const quotedName = (schema, name) => `(?:"?${escapeRegExp$1(schema)}"?\\s*\\.\\s*)?"?${escapeRegExp$1(name)}"?`;
359
514
  const createsFunction = (fn) => new RegExp(`create\\s+(?:or\\s+replace\\s+)?function\\s+${quotedName(fn.schema, fn.name)}\\s*\\(`, "i");
360
- /** The SQL file that starts with PermDock's hook marker and creates `fn`. */
515
+ /** The SQL file that has the provider's hook marker and creates `fn`. */
361
516
  function markerFileFor(context, fn) {
517
+ const marker = providerHook(context)?.markers?.hook;
518
+ if (marker === void 0) return void 0;
362
519
  const creates = createsFunction(fn);
363
- return (context.sqlFiles ?? []).find((file) => parseHookMarker(file.text) && creates.test(file.text));
520
+ return (context.sqlFiles ?? []).find((file) => file.text.includes(marker) && creates.test(file.text));
364
521
  }
365
- /** Whether `fn` is the hook PermDock generated: by its manifest, its marker file or its body. */
366
- function isPermdockHook(context, fn) {
367
- const hook = context.permdock?.manifest?.hook;
368
- if (hook && hook.schema === fn.schema && hook.function === fn.name) return true;
369
- if (markerFileFor(context, fn)) return true;
370
- return fn.source !== void 0 && PERMDOCK_HOOK.test(fn.source);
522
+ /** Whether `fn` is the provider's hook: by its name or its marker file. */
523
+ function isProviderHook(context, fn) {
524
+ const hook = providerHook(context);
525
+ if (!hook) return false;
526
+ if (hook.function === `${fn.schema}.${fn.name}`) return true;
527
+ return markerFileFor(context, fn) !== void 0;
371
528
  }
372
- /**
373
- * Extra claims PermDock's hook writes from `supabase.hook.claims`, by claim
374
- * name, with the function that fills each: from the manifest, else from the
375
- * hook marker's `claims=` list.
376
- */
377
- function registeredClaims(context) {
378
- const claims = /* @__PURE__ */ new Map();
379
- for (const claim of context.permdock?.manifest?.claims ?? []) if (claim.source !== "permdock") claims.set(claim.name, claim.source);
380
- if (claims.size > 0) return claims;
381
- const owned = /* @__PURE__ */ new Set([
382
- ...ownedClaims(context),
383
- "memberships_truncated",
384
- "attrs",
385
- "authz_ver"
386
- ]);
387
- for (const file of context.sqlFiles ?? []) for (const name of parseHookMarker(file.text)?.claims ?? []) if (!owned.has(name)) claims.set(name, void 0);
388
- return claims;
389
- }
390
- /** Whether the hook body calls PermDock's generated hook (a wrapper around it). */
391
- function wrapsPermdockHook(context, source) {
392
- const hook = context.permdock?.manifest?.hook;
393
- return hook !== void 0 && new RegExp(`${quotedName(hook.schema, hook.function)}\\s*\\(`, "i").test(source);
529
+ /** Whether the hook body calls the provider's hook (a wrapper around it). */
530
+ function wrapsProviderHook(hook, source) {
531
+ const { schema, name } = splitName(hook.function);
532
+ return new RegExp(`${quotedName(schema, name)}\\s*\\(`, "i").test(source);
394
533
  }
395
534
  /** Which of `claims` the hook body writes through `jsonb_set` or `jsonb_build_object`. */
396
535
  function writtenClaims(source, claims) {
@@ -399,7 +538,7 @@ function writtenClaims(source, claims) {
399
538
  return new RegExp(`jsonb_set\\s*\\([^;]*?'\\{\\s*(?:claims\\s*,\\s*)?${name}\\s*\\}'`, "i").test(source) || new RegExp(`jsonb_build_object\\s*\\([^;]*?'${name}'\\s*,`, "i").test(source);
400
539
  });
401
540
  }
402
- const API_ROLES$1 = ["authenticated", "anon"];
541
+ const API_ROLES$2 = ["authenticated", "anon"];
403
542
  /** Hooks enabled in `config.toml`, matched with their introspected functions. */
404
543
  function configuredHooks(context) {
405
544
  if (!context.configToml) return [];
@@ -430,7 +569,7 @@ function grantProblems(fn) {
430
569
  problems.push("supabase_auth_admin may not execute it");
431
570
  fix.push(`grant execute on function ${signatureOf(fn)} to supabase_auth_admin;`);
432
571
  }
433
- const exposedTo = API_ROLES$1.filter((role) => fn.execute.includes(role));
572
+ const exposedTo = API_ROLES$2.filter((role) => fn.execute.includes(role));
434
573
  if (exposedTo.length > 0 || fn.publicExecute) {
435
574
  const roles = [...exposedTo, ...fn.publicExecute ? ["public"] : []];
436
575
  problems.push(`${roles.join(", ")} may execute it, so any client can call it through the API`);
@@ -443,18 +582,17 @@ function grantProblems(fn) {
443
582
  }
444
583
  const MIGRATIONS = /(?:^|\/)supabase\/migrations\//;
445
584
  /**
446
- * The file that grants `fn` to `supabase_auth_admin`: a `-- permdock:grants
447
- * v1` migration, or under pg-delta, which carries grants, the declarative
585
+ * The file that grants `fn` to `supabase_auth_admin`: a migration with the
586
+ * provider's grants marker, or under pg-delta, which carries grants, the declarative
448
587
  * schema file that defines `fn`. `supabase db diff` drops grants, so the
449
588
  * legacy engine only counts the migration.
450
589
  */
451
590
  function grantsSourceFor(context, fn, engine) {
452
591
  const grants = new RegExp(`grant\\s+execute\\s+on\\s+function\\s+${quotedName(fn.schema, fn.name)}\\s*\\([^)]*\\)\\s*to\\s+[^;]*\\bsupabase_auth_admin\\b`, "i");
453
592
  const creates = createsFunction(fn);
454
- return (context.sqlFiles ?? []).find((file) => grants.test(file.text) && (parseGrantsMarker(file.text) !== void 0 || engine === "pg-delta" && !MIGRATIONS.test(file.path) && creates.test(file.text)))?.path;
593
+ const marker = providerHook(context)?.markers?.grants;
594
+ return (context.sqlFiles ?? []).find((file) => grants.test(file.text) && (marker !== void 0 && file.text.includes(marker) || engine === "pg-delta" && !MIGRATIONS.test(file.path) && creates.test(file.text)))?.path;
455
595
  }
456
- const PERMDOCK_GRANTS_COMMAND = "permdock supabase hook generate --grants-out supabase/migrations/<timestamp>_permdock_hook_grants.sql";
457
- const permdockSchemaCommand = (file) => `permdock supabase hook generate --out ${file ?? "<the schema file that defines it>"}`;
458
596
  /** Configured hook functions whose grants are wrong (BS404, `doctor --fix-grants`). */
459
597
  function hookGrantProblems(context) {
460
598
  const engine = diffEngine(context.configToml);
@@ -462,7 +600,8 @@ function hookGrantProblems(context) {
462
600
  const { problems, fix } = grantProblems(fn);
463
601
  if (problems.length === 0) return [];
464
602
  const file = grantsSourceFor(context, fn, engine);
465
- const hookFile = markerFileFor(context, fn)?.path;
603
+ const provider = context.config.authorization;
604
+ const command = provider?.tokenHook?.grantsCommand;
466
605
  return [{
467
606
  hook: config.hook,
468
607
  fn,
@@ -470,9 +609,10 @@ function hookGrantProblems(context) {
470
609
  fix: file ? {
471
610
  kind: "migration",
472
611
  file
473
- } : isPermdockHook(context, fn) ? {
474
- kind: "permdock",
475
- ...hookFile ? { file: hookFile } : {}
612
+ } : provider && isProviderHook(context, fn) ? {
613
+ kind: "provider",
614
+ name: providerLabel(provider),
615
+ ...command ? { command } : {}
476
616
  } : {
477
617
  kind: "sql",
478
618
  sql: fix
@@ -484,13 +624,13 @@ function hookGrantProblems(context) {
484
624
  function grantFixText(fix, engine) {
485
625
  switch (fix.kind) {
486
626
  case "sql": return engine === "pg-delta" ? `Add to the schema file that defines the function, then run \`supabase db schema declarative sync\` (\`doctor --fix-grants\` prints every block):\n${fix.sql.join("\n")}` : `Append to the migration \`supabase db diff\` wrote (\`doctor --fix-grants\` prints every block):\n${fix.sql.join("\n")}`;
487
- case "permdock": return engine === "pg-delta" ? `It is PermDock's hook, so let PermDock write the grants into the schema file that defines it: \`${permdockSchemaCommand(fix.file)}\`, without \`--grants-out\` so pg-delta carries them, then run \`supabase db schema declarative sync\`.` : `It is PermDock's hook, so let PermDock write the grants: \`${PERMDOCK_GRANTS_COMMAND}\`.`;
627
+ case "provider": return fix.command ? `It is the hook of ${fix.name}, so let the provider write the grants: \`${fix.command}\`.` : `It is the hook of ${fix.name}, so let the provider write the grants.`;
488
628
  case "migration": return engine === "pg-delta" ? `${fix.file} grants it, so the database is behind: run \`supabase db schema declarative sync\`, then apply the migrations (\`supabase migration up\`).` : `${fix.file} grants it, so the database is behind the migrations: apply them (\`supabase migration up\`).`;
489
629
  default: return fix;
490
630
  }
491
631
  }
492
632
  /** `doctor --fix-grants`: the SQL block for every BS404 problem, as one migration snippet. */
493
- function hookGrantBlock(problems, engine = "migra") {
633
+ function hookGrantBlock(problems, engine = "pg-delta") {
494
634
  if (problems.length === 0) return "-- Every configured Auth hook function has its grants.";
495
635
  const lines = ["-- Auth hook grants (better-supabase doctor --fix-grants).", engine === "pg-delta" ? "-- Add this to the schema file that defines the function, then run `supabase db schema declarative sync`." : "-- `supabase db diff` does not carry function grants; append this to its migration."];
496
636
  for (const { fn, hook, fix } of problems) {
@@ -499,8 +639,8 @@ function hookGrantBlock(problems, engine = "migra") {
499
639
  case "sql":
500
640
  lines.push("", `-- ${name}`, ...fix.sql);
501
641
  break;
502
- case "permdock":
503
- lines.push("", engine === "pg-delta" ? `-- ${name} is PermDock's hook: ${permdockSchemaCommand(fix.file)} (without --grants-out), then supabase db schema declarative sync` : `-- ${name} is PermDock's hook: ${PERMDOCK_GRANTS_COMMAND}`);
642
+ case "provider":
643
+ lines.push("", `-- ${name} is the hook of ${fix.name}${fix.command ? `: ${fix.command}` : ""}`);
504
644
  break;
505
645
  case "migration":
506
646
  lines.push("", engine === "pg-delta" ? `-- ${name}: ${fix.file} grants it; run \`supabase db schema declarative sync\`, then \`supabase migration up\`.` : `-- ${name}: ${fix.file} grants it; run \`supabase migration up\`.`);
@@ -563,18 +703,19 @@ async function hookClaims(db, fn, userId) {
563
703
  return isRecord(claims) ? claims : {};
564
704
  });
565
705
  }
566
- async function hookClaimsSize(db, fn, userId) {
706
+ async function hookClaimsSize(db, fn, userId, budget) {
707
+ const budgetSql = budget && budget.claims.length > 0 ? budget.claims.map((claim) => `coalesce(octet_length((c -> ${literal(claim)})::text), 0)`).join(" + ") : "0";
708
+ const truncatedSql = budget?.truncatedClaim ? `c ->> ${literal(budget.truncatedClaim)} = 'true'` : "false";
567
709
  return withHookEvent(db, userId, async () => {
568
710
  const [row] = await db.query(`select octet_length(c::text) as bytes,
569
711
  octet_length((c -> 'memberships')::text) as memberships,
570
- octet_length((c -> 'attrs')::text) as attrs,
571
- c ->> 'memberships_truncated' = 'true' as truncated
712
+ ${budgetSql} as budget,
713
+ ${truncatedSql} as truncated
572
714
  from (select ${hookCall(fn)} as c) h`);
573
- const memberships = Number(row?.memberships ?? 0);
574
715
  return {
575
716
  bytes: Number(row?.bytes ?? 0),
576
- memberships,
577
- budget: memberships + Number(row?.attrs ?? 0),
717
+ memberships: Number(row?.memberships ?? 0),
718
+ budget: Number(row?.budget ?? 0),
578
719
  truncated: row?.truncated === true
579
720
  };
580
721
  });
@@ -632,7 +773,7 @@ const HOOK_RULES = [
632
773
  code: "BS405",
633
774
  severity: "warning",
634
775
  title: "Custom access token hook shape",
635
- description: `Auth runs the custom access token hook on every sign-in and refresh. It should be \`stable\` with \`set search_path = ''\`, and the claims it returns end up in every request's cookie and Authorization header. With \`--as <user id>\` doctor calls it for that user in a transaction that is rolled back. It warns when the whole token's claims pass ${HOOK_CLAIMS_LIMIT} bytes (\`doctor.claimsLimit\` without PermDock). With a \`permdock.config.ts\` it also warns when \`memberships\` plus \`attrs\` pass PermDock's budget, ${PERMDOCK_CLAIMS_LIMIT} bytes or \`doctor.claimsLimit\`, measured with \`octet_length\` as PermDock's hook measures it.`,
776
+ description: `Auth runs the custom access token hook on every sign-in and refresh. It should be \`stable\` with \`set search_path = ''\`, and the claims it returns end up in every request's cookie and Authorization header. With \`--as <user id>\` doctor calls it for that user in a transaction that is rolled back. It warns when the whole token's claims pass ${HOOK_CLAIMS_LIMIT} bytes (\`doctor.claimsLimit\` without a provider budget). When the authorization provider's hook has a \`budget\`, it also warns when the budget's claims pass its bytes (or \`doctor.claimsLimit\`), measured with \`octet_length\` of each claim.`,
636
777
  async check(context) {
637
778
  const findings = [];
638
779
  const limits = claimsLimits(context);
@@ -663,7 +804,7 @@ const HOOK_RULES = [
663
804
  continue;
664
805
  }
665
806
  try {
666
- const size = await hookClaimsSize(db, fn, context.hookUser);
807
+ const size = await hookClaimsSize(db, fn, context.hookUser, limits.budget);
667
808
  if (size === void 0) {
668
809
  findings.push({
669
810
  severity: "info",
@@ -672,8 +813,8 @@ const HOOK_RULES = [
672
813
  });
673
814
  continue;
674
815
  }
675
- if (limits.budget !== void 0 && size.budget > limits.budget) findings.push({
676
- message: `${signatureOf(fn)} returns ${size.budget} bytes of memberships and attrs for ${context.hookUser}, over PermDock's budget of ${limits.budget} (memberships ${size.memberships}). Raise \`supabase.hook.budget\` and \`doctor.claimsLimit\` together, or move attrs out of the token.`,
816
+ if (limits.budget !== void 0 && size.budget > limits.budget.bytes) findings.push({
817
+ message: `${signatureOf(fn)} returns ${size.budget} bytes of ${limits.budget.claims.join(" and ")} for ${context.hookUser}, over the hook's budget of ${limits.budget.bytes}. Raise the provider's budget and \`doctor.claimsLimit\` together, or move claims out of the token.`,
677
818
  target: `${signatureOf(fn)}:budget`,
678
819
  object
679
820
  });
@@ -684,7 +825,7 @@ const HOOK_RULES = [
684
825
  });
685
826
  if (size.truncated) findings.push({
686
827
  severity: "info",
687
- message: `${signatureOf(fn)} sets memberships_truncated for ${context.hookUser}: the token lists only some memberships (${size.memberships} bytes). Server checks for this user need a database lookup (PermDock's claimsFirst falls back to one).`,
828
+ message: `${signatureOf(fn)} sets ${limits.budget?.truncatedClaim ?? "its truncated claim"} for ${context.hookUser}: the token lists only some entries (memberships ${size.memberships} bytes). Server checks for this user need a database lookup.`,
688
829
  target: `${signatureOf(fn)}:truncated`,
689
830
  object
690
831
  });
@@ -704,42 +845,29 @@ const HOOK_RULES = [
704
845
  code: "BS407",
705
846
  severity: "error",
706
847
  title: "Two authorization hooks",
707
- description: "A `permdock.config.ts` or `permdock.manifest.json` (or a hook that calls PermDock's functions) means PermDock writes `user_role`, `roles`, `memberships` and the tenant claim (`claims.tenant`) into the token. A custom access token hook that also calls `better_supabase.membership_claims`, or writes one of those claims itself, gives them a second source that drifts from PermDock's. Generate the hook with `permdock supabase hook generate` and drop the extra writes. Functions registered in PermDock's `supabase.hook.claims` (such as `better_supabase.feature_claims` for `features`) are PermDock's hook's own sources and are not reported; a hook that wraps PermDock's and writes one of those claims again is.",
848
+ description: "When the authorization provider's hook (`authorization.tokenHook`) owns claims such as `memberships`, `roles` or the tenant claim, a custom access token hook that also calls `better_supabase.membership_claims`, or writes one of those claims itself, gives them a second source that drifts from the provider's. Use the provider's hook and drop the extra writes. Claims the provider's hook fills from other functions (`registeredClaims`, such as `features` from `better_supabase.feature_claims`) are not reported; a hook that wraps the provider's and writes one of those claims again is.",
708
849
  check: (context) => {
709
- const project = context.permdock;
850
+ const provider = context.config.authorization;
851
+ const hook = provider?.tokenHook;
852
+ if (!provider || !hook) return [];
853
+ const label = providerLabel(provider);
854
+ const registered = new Map((hook.registeredClaims ?? []).map((claim) => [claim.name, claim.function]));
855
+ const owned = ownedClaims(hook);
710
856
  const findings = [];
711
- if (project) {
712
- findings.push(...project.problems.filter((problem) => problem.startsWith(project.manifestPath)).map((problem) => ({
713
- severity: "info",
714
- message: `Could not read PermDock's manifest: ${problem}`,
715
- target: project.manifestPath
716
- })));
717
- if (project.config && !project.manifest && !project.problems.some((problem) => problem.startsWith(project.manifestPath))) findings.push({
718
- severity: "info",
719
- message: `${project.config} is present but ${project.manifestPath} is not, so doctor and the SQL kit can't see PermDock's hook, helpers and membership sources. Run \`permdock supabase inspect --out\`, and \`permdock supabase inspect --check\` in CI.`,
720
- target: project.manifestPath
721
- });
722
- }
723
- const registered = registeredClaims(context);
724
857
  for (const { config, extras } of configuredHooks(context)) {
725
858
  if (config.hook !== "custom_access_token" || !extras) continue;
726
859
  for (const fn of extras.functions) {
727
- if (fn.source === void 0 || isPermdockHook(context, fn)) continue;
728
- const kit = KIT_MEMBERSHIPS.test(fn.source);
729
- const wrapper = wrapsPermdockHook(context, fn.source);
730
- const written = writtenClaims(fn.source, [...ownedClaims(context), ...wrapper ? registered.keys() : []]);
731
- if (!kit && written.length === 0) continue;
732
- const permdock = project ? permdockSource(project) : PERMDOCK_CALL.test(fn.source) ? "the hook body" : void 0;
733
- if (!permdock) continue;
860
+ if (fn.source === void 0 || isProviderHook(context, fn)) continue;
861
+ const module = owned.includes("memberships") && MODULE_MEMBERSHIPS.test(fn.source);
862
+ const wrapper = wrapsProviderHook(hook, fn.source);
863
+ const written = writtenClaims(fn.source, [...owned, ...wrapper ? registered.keys() : []]);
864
+ if (!module && written.length === 0) continue;
734
865
  const location = hookLocation(context, config.hook);
735
- const what = [...kit ? ["calls better_supabase.membership_claims"] : [], ...written.length > 0 ? [`writes ${written.join(", ")}`] : []].join(" and ");
866
+ const what = [...module ? ["calls better_supabase.membership_claims"] : [], ...written.length > 0 ? [`writes ${written.join(", ")}`] : []].join(" and ");
736
867
  const extra = written.filter((claim) => registered.has(claim));
737
- const sources = extra.map((claim) => {
738
- const source = registered.get(claim);
739
- return source ? `${claim} from ${source}` : claim;
740
- }).join(", ");
868
+ const sources = extra.map((claim) => `${claim} from ${registered.get(claim)}`).join(", ");
741
869
  findings.push({
742
- message: `${signatureOf(fn)} ${what}, and ${permdock} says PermDock owns those claims${extra.length > 0 ? ` (its hook already writes ${sources} through supabase.hook.claims)` : ""}. Keep one source: run \`permdock supabase hook generate\` and remove these writes from your hook.`,
870
+ message: `${signatureOf(fn)} ${what}, but the hook of ${label} (${hook.function}) owns those claims${extra.length > 0 ? ` (it already writes ${sources})` : ""}. Keep one source: use the provider's hook and remove these writes from yours.`,
743
871
  target: signatureOf(fn),
744
872
  object: {
745
873
  kind: "function",
@@ -757,7 +885,7 @@ const HOOK_RULES = [
757
885
  code: "BS410",
758
886
  severity: "warning",
759
887
  title: "HTTP auth hooks",
760
- description: "Auth calls an `http://` or `https://` hook with a request signed by the Standard Webhooks secret in `secrets` (`v1,whsec_<base64>`, several joined with `|`). Doctor can't read the endpoint's code, so it reports each HTTP hook and checks what it can: the secret's format, that it comes from `env()` rather than the committed file, and that a non-local endpoint uses https. Verify the request in the endpoint with `authHook` from `better-supabase/webhooks`.",
888
+ description: "Auth calls an `http://` or `https://` hook with a request signed by the Standard Webhooks secret in `secrets` (`v1,whsec_<base64>`, several joined with `|`). Doctor can't read the endpoint's code, so it reports each HTTP hook and checks what it can: the secret's format, that it comes from `env()` rather than the committed file, and that a non-local endpoint uses https. Verify the request in the endpoint with `authHook` from `better-supabase/blocks/webhooks`.",
761
889
  check: (context) => {
762
890
  const toml = context.configToml;
763
891
  if (!toml) return [];
@@ -767,7 +895,7 @@ const HOOK_RULES = [
767
895
  const target = `[auth.hook.${hook}]`;
768
896
  const findings = [{
769
897
  severity: "info",
770
- message: `${target} calls ${uri.origin}${uri.pathname} over HTTP. Doctor can't check its code; verify the signature there with authHook from better-supabase/webhooks.`,
898
+ message: `${target} calls ${uri.origin}${uri.pathname} over HTTP. Doctor can't check its code; verify the signature there with authHook from better-supabase/blocks/webhooks.`,
771
899
  target,
772
900
  ...at
773
901
  }];
@@ -808,7 +936,62 @@ const HOOK_RULES = [
808
936
  }
809
937
  ];
810
938
  //#endregion
811
- //#region src/cli/doctor/kits.ts
939
+ //#region src/cli/doctor/metadata.ts
940
+ /**
941
+ * Rules that read what a `GeneratorMetadata` document leaves out (policies,
942
+ * grants, indexes, triggers, buckets, the realtime publication, foreign key
943
+ * actions, multi-column constraints, role settings, hook functions). On a
944
+ * snapshot built from such a document they would report what is missing
945
+ * from the document, not from the database, so they skip with an info finding.
946
+ */
947
+ const EXTRAS_RULES = /* @__PURE__ */ new Set([
948
+ "BS103",
949
+ "BS106",
950
+ "BS107",
951
+ "BS108",
952
+ "BS109",
953
+ "BS110",
954
+ "BS111",
955
+ "BS112",
956
+ "BS113",
957
+ "BS114",
958
+ "BS204",
959
+ "BS205",
960
+ "BS206",
961
+ "BS207",
962
+ "BS210",
963
+ "BS211",
964
+ "BS213",
965
+ "BS215",
966
+ "BS216",
967
+ "BS217",
968
+ "BS218",
969
+ "BS219",
970
+ "BS301",
971
+ "BS302",
972
+ "BS303",
973
+ "BS305",
974
+ "BS306",
975
+ "BS310",
976
+ "BS315",
977
+ "BS404",
978
+ "BS405",
979
+ "BS406",
980
+ "BS407"
981
+ ]);
982
+ /** The info finding for a rule in `EXTRAS_RULES` that did not run, or `undefined` when it can. */
983
+ function skippedForMetadata(snapshot, rule, help) {
984
+ if (!snapshot.extras.fromMetadata || !EXTRAS_RULES.has(rule.code)) return void 0;
985
+ return {
986
+ code: rule.code,
987
+ severity: "info",
988
+ title: rule.title,
989
+ help,
990
+ message: `Skipped: the schema came from a GeneratorMetadata document (--metadata), which has no ${METADATA_GAPS_TEXT}. Run doctor against the database to run this check.`
991
+ };
992
+ }
993
+ //#endregion
994
+ //#region src/cli/doctor/modules.ts
812
995
  /** The non-empty string at a dotted path, as the tenant() plugin reads it. */
813
996
  function claimAt(claims, path) {
814
997
  let value = claims;
@@ -816,7 +999,7 @@ function claimAt(claims, path) {
816
999
  return typeof value === "string" && value.length > 0 ? value : void 0;
817
1000
  }
818
1001
  function contractChecks(context) {
819
- return customContracts(context.config.sql.kit, kitLayout(context.config)).flatMap((contract) => contract.functions.map((fn) => ({
1002
+ return customContracts(context.config.sql.moduleNames, moduleLayout(context.config, context.config.sql.testsDir, [], void 0, void 0, permissionCatalogKeys(context))).flatMap((contract) => contract.functions.map((fn) => ({
820
1003
  module: contract.module,
821
1004
  schema: contract.schema,
822
1005
  name: fn.name,
@@ -845,10 +1028,10 @@ from (
845
1028
  limit 1
846
1029
  ) as hook
847
1030
  ) h`;
848
- /** `rate-limit` in `sql.kit` without PostgREST's pre-request hook reaching `check_request()`. */
1031
+ /** `rate-limit` in `sql.modules` without PostgREST's pre-request hook reaching `check_request()`. */
849
1032
  async function rateLimitHook(context) {
850
1033
  const db = context.database;
851
- if (!context.config.sql.kit.includes("rate-limit") || !db || "skipped" in db) return [];
1034
+ if (!context.config.sql.moduleNames.includes("rate-limit") || !db || "skipped" in db) return [];
852
1035
  let rows;
853
1036
  try {
854
1037
  rows = await db.query(PRE_REQUEST_HOOK);
@@ -862,6 +1045,57 @@ async function rateLimitHook(context) {
862
1045
  target: "authenticator pgrst.db_pre_request"
863
1046
  }];
864
1047
  }
1048
+ const DANGLING_AUDIT = `select a.target::oid::text as oid
1049
+ from better_supabase.audited_tables a
1050
+ where pg_catalog.to_regclass('better_supabase.audited_tables') is not null
1051
+ and not exists (select 1 from pg_catalog.pg_class c where c.oid = a.target::oid)
1052
+ order by 1`;
1053
+ /** `better_supabase.audited_tables` rows whose table no longer exists. */
1054
+ async function danglingAuditRegistrations(context) {
1055
+ const db = context.database;
1056
+ if (!context.config.sql.moduleNames.includes("audit") || !db || "skipped" in db) return [];
1057
+ let rows;
1058
+ try {
1059
+ rows = await db.query(DANGLING_AUDIT);
1060
+ } catch {
1061
+ return [];
1062
+ }
1063
+ return rows.map((row) => ({
1064
+ message: `better_supabase.audited_tables registers the table with oid ${row.oid}, which no longer exists. It was dropped before the audit module's sql_drop event trigger was installed. Run \`better-supabase sql sync\` and apply the migration \`better-supabase sql data\` writes, which deletes such rows, or run \`delete from better_supabase.audited_tables where not exists (select 1 from pg_catalog.pg_class c where c.oid = target::oid)\`.`,
1065
+ target: `better_supabase.audited_tables ${row.oid}`
1066
+ }));
1067
+ }
1068
+ const EVENT_TRIGGER_FIX = "Event triggers belong to no schema, so a schema diff limited to some schemas (`supabase db schema declarative sync -s ...`) leaves them out of the migration. Run `better-supabase sql sync`, then `better-supabase sql data`: the module's data migration creates it.";
1069
+ /** Event triggers the `sql.modules` create that the database, or every migration, lacks. */
1070
+ async function missingEventTriggers(context) {
1071
+ const expected = moduleEventTriggers(context.config.sql.moduleNames);
1072
+ if (expected.length === 0) return [];
1073
+ const db = context.database;
1074
+ if (db && !("skipped" in db)) {
1075
+ let rows;
1076
+ try {
1077
+ rows = await db.query("select evtname as name from pg_catalog.pg_event_trigger");
1078
+ } catch {
1079
+ return [];
1080
+ }
1081
+ const present = new Set(rows.map((row) => row.name));
1082
+ return expected.filter((trigger) => !present.has(trigger.name)).map((trigger) => ({
1083
+ message: `The database has no event trigger ${trigger.name}, which the ${trigger.module} module creates. ${EVENT_TRIGGER_FIX}`,
1084
+ target: trigger.name
1085
+ }));
1086
+ }
1087
+ const toml = context.configToml;
1088
+ if (toml === void 0) return [];
1089
+ const migrations = (context.sqlFiles ?? []).filter((file) => file.path.startsWith(`${toml.dir}/migrations/`));
1090
+ if (migrations.length === 0) return [];
1091
+ return expected.filter((trigger) => {
1092
+ const pattern = new RegExp(`create\\s+event\\s+trigger\\s+${ident(trigger.name)}\\s`, "i");
1093
+ return !migrations.some((file) => pattern.test(file.text));
1094
+ }).map((trigger) => ({
1095
+ message: `No migration creates the event trigger ${trigger.name}, which the ${trigger.module} module creates, so a database built from the migrations lacks it. ${EVENT_TRIGGER_FIX}`,
1096
+ target: trigger.name
1097
+ }));
1098
+ }
865
1099
  /** Custom-mode modules whose contract functions the database or SQL files don't have. */
866
1100
  async function missingContracts(context) {
867
1101
  const checks = contractChecks(context);
@@ -869,7 +1103,7 @@ async function missingContracts(context) {
869
1103
  const db = context.database;
870
1104
  const live = db && !("skipped" in db) ? db : void 0;
871
1105
  if (!live) return checks.filter((check) => !declared(context, check)).map((check) => ({
872
- message: `kits.${check.module} is in custom mode, but no SQL file declares ${check.schema}.${check.name}(${check.args}). Write it, or switch the module to adopt or managed.`,
1106
+ message: `sql.modules.${check.module} is in custom mode, but no SQL file declares ${check.schema}.${check.name}(${check.args}). Write it, or switch the module to adopt or managed.`,
873
1107
  target: `${check.schema}.${check.name}`,
874
1108
  object: {
875
1109
  kind: "function",
@@ -888,8 +1122,8 @@ async function missingContracts(context) {
888
1122
  } catch (cause) {
889
1123
  return [{
890
1124
  severity: "info",
891
- message: `Reading the custom kit contract functions failed: ${errorText(cause)}`,
892
- target: "kits"
1125
+ message: `Reading the custom module contract functions failed: ${errorText(cause)}`,
1126
+ target: "modules"
893
1127
  }];
894
1128
  }
895
1129
  return checks.flatMap((check) => {
@@ -901,7 +1135,7 @@ async function missingContracts(context) {
901
1135
  name: check.name
902
1136
  };
903
1137
  if (found.length === 0) return [{
904
- message: `kits.${check.module} is in custom mode, but ${target}(${check.args}) doesn't exist. Write it, or switch the module to adopt or managed.`,
1138
+ message: `sql.modules.${check.module} is in custom mode, but ${target}(${check.args}) doesn't exist. Write it, or switch the module to adopt or managed.`,
905
1139
  target,
906
1140
  object
907
1141
  }];
@@ -921,8 +1155,8 @@ async function missingContracts(context) {
921
1155
  }
922
1156
  /** With the tenant claim as the active tenant, the hook must put it in the token. */
923
1157
  async function missingTenantClaim(context) {
924
- if (!context.config.sql.kit.includes("tenant")) return [];
925
- if ((context.config.kits.access?.activeTenant ?? DEFAULT_ACTIVE_TENANT) !== "claim") return [];
1158
+ if (!context.config.sql.moduleNames.includes("tenant")) return [];
1159
+ if ((context.config.sql.modules.access?.activeTenant ?? DEFAULT_ACTIVE_TENANT) !== "claim") return [];
926
1160
  const userId = context.hookUser;
927
1161
  const db = context.database;
928
1162
  if (!userId || !db || "skipped" in db || !db.session) return [];
@@ -941,7 +1175,7 @@ async function missingTenantClaim(context) {
941
1175
  if (claims === void 0) continue;
942
1176
  if (tenantClaimPaths(claim).some((path) => claimAt(claims, path) !== void 0)) continue;
943
1177
  findings.push({
944
- message: `${signatureOf(fn)} returns no \`${claim}\` claim for ${userId} (top level or app_metadata), and kits.access.activeTenant is 'claim', so current_tenant_id() is null and the tenant() plugin rejects their requests. Write the claim in the hook, or set kits.access.activeTenant to 'resolver' (with ServerOptions.tenant) or a profile column.`,
1178
+ message: `${signatureOf(fn)} returns no \`${claim}\` claim for ${userId} (top level or app_metadata), and sql.modules.access.activeTenant is 'claim', so current_tenant_id() is null and the tenant() plugin rejects their requests. Write the claim in the hook, or set sql.modules.access.activeTenant to 'resolver' (with ServerOptions.tenant) or a profile column.`,
945
1179
  target,
946
1180
  object: {
947
1181
  kind: "function",
@@ -989,12 +1223,12 @@ function lineOfUse(entry, text, policyTable) {
989
1223
  offset += statement.length + 1;
990
1224
  }
991
1225
  }
992
- /** SQL files and policies that still use a symbol a kit module deprecated or removed. */
1226
+ /** SQL files and policies that still use a symbol a SQL module deprecated or removed. */
993
1227
  function deprecatedSymbols(context) {
994
1228
  const configured = new Set(Object.values(context.config.claims));
995
- const entries = kitDeprecations().filter((entry) => context.config.sql.kit.includes(entry.module) && !(entry.kind === "claim" && configured.has(entry.symbol)) && !((entry.kind === "table" || entry.kind === "column") && (context.config.kits[entry.module]?.mode ?? "managed") !== "managed"));
1229
+ const entries = moduleDeprecations().filter((entry) => context.config.sql.moduleNames.includes(entry.module) && !(entry.kind === "claim" && configured.has(entry.symbol)) && !((entry.kind === "table" || entry.kind === "column") && (context.config.sql.modules[entry.module]?.mode ?? "managed") !== "managed"));
996
1230
  if (entries.length === 0) return [];
997
- const files = (context.sqlFiles ?? []).filter((file) => !file.path.startsWith("supabase/migrations/") && kitFileVersion(file.text) === void 0);
1231
+ const files = (context.sqlFiles ?? []).filter((file) => !/(^|\/)supabase\/migrations\//.test(file.path) && moduleFileVersion(file.text) === void 0);
998
1232
  const findings = [];
999
1233
  for (const entry of entries) {
1000
1234
  const status = entry.removed === void 0 ? `deprecated since ${entry.since}` : `removed in ${entry.removed}`;
@@ -1022,7 +1256,7 @@ function deprecatedSymbols(context) {
1022
1256
  }
1023
1257
  return findings;
1024
1258
  }
1025
- /** Kit triggers and the equivalent triggers they replace. */
1259
+ /** Module triggers and the equivalent triggers they replace. */
1026
1260
  const EQUIVALENT_TRIGGERS = [{
1027
1261
  trigger: "bs_updated_at",
1028
1262
  call: "track_updated_at",
@@ -1032,7 +1266,7 @@ const EQUIVALENT_TRIGGERS = [{
1032
1266
  call: "audit",
1033
1267
  pattern: /audit/i
1034
1268
  }];
1035
- /** Tables where a kit trigger and an older trigger do the same work. */
1269
+ /** Tables where a module trigger and an older trigger do the same work. */
1036
1270
  function duplicateTriggers(context) {
1037
1271
  return catalogOf(context).tables.flatMap((table) => EQUIVALENT_TRIGGERS.flatMap(({ trigger, call, pattern }) => {
1038
1272
  if (!table.triggers.some((entry) => entry.name === trigger)) return [];
@@ -1043,72 +1277,418 @@ function duplicateTriggers(context) {
1043
1277
  }));
1044
1278
  }));
1045
1279
  }
1046
- /** `better_supabase` and every `kits.*.schema` that `[api] schemas` serves through the Data API. */
1047
- function exposedKitSchemas(context) {
1048
- if (context.config.sql.kit.length === 0) return [];
1049
- const kitSchemas = /* @__PURE__ */ new Set(["better_supabase"]);
1050
- for (const module of Object.values(context.config.kits)) if (module?.schema !== void 0) kitSchemas.add(module.schema);
1051
- return exposedSchemas(context).filter((schema) => kitSchemas.has(schema)).map((schema) => ({
1052
- message: `The Data API serves the kit schema ${schema}, so its tables and internal helpers are reachable over REST and RPC. Remove it from [api] schemas in supabase/config.toml (and the dashboard's exposed schemas), and call the kit functions through a wrapper in an exposed schema.`,
1280
+ /** `exempt` globs (`public.*_archive`): `*` matches any run of characters. */
1281
+ const globs = (value) => (Array.isArray(value) ? value : []).filter((entry) => typeof entry === "string").map((glob) => new RegExp(`^${glob.split("*").map(escapeRegExp).join(".*")}$`, "i"));
1282
+ /** The module schemas and the app tables SQL modules adopted, which the audit trigger skips. */
1283
+ function moduleOwned(context) {
1284
+ const schemas = /* @__PURE__ */ new Set(["better_supabase"]);
1285
+ const tables = /* @__PURE__ */ new Set();
1286
+ for (const module of Object.values(context.config.sql.modules)) {
1287
+ if (module?.schema !== void 0) schemas.add(module.schema);
1288
+ for (const table of Object.values(module?.tables ?? {})) {
1289
+ if (table === null) continue;
1290
+ tables.add(table.includes(".") ? table : `${module?.schema ?? "better_supabase"}.${table}`);
1291
+ }
1292
+ }
1293
+ return {
1294
+ schemas,
1295
+ tables
1296
+ };
1297
+ }
1298
+ /** Tables in `config.schemas` without the `bs_audit` trigger, when `audit` is in `sql.modules`. */
1299
+ function unauditedTables(context) {
1300
+ if (!context.config.sql.moduleNames.includes("audit")) return [];
1301
+ const exempt = globs(context.config.sql.modules["audit"]?.options?.["exempt"]);
1302
+ const owned = moduleOwned(context);
1303
+ return catalogOf(context).tables.filter((table) => table.kind === "table" && context.config.schemas.includes(table.schema) && !owned.schemas.has(table.schema) && !owned.tables.has(qualified(table)) && !exempt.some((pattern) => pattern.test(qualified(table))) && !table.triggers.some((trigger) => trigger.name === "bs_audit" || trigger.function.split(".").at(-1) === "audit_row_change")).map((table) => ({
1304
+ message: `${qualified(table)} has no audit trigger, so its inserts, updates and deletes are not in the audit log. Run \`select better_supabase.audit('${qualified(table)}')\` in a schema file, or list it in sql.modules.audit.options.exempt.`,
1305
+ target: qualified(table),
1306
+ object: tableObject(table)
1307
+ }));
1308
+ }
1309
+ /** RLS tables in `config.schemas` without a restrictive `session_active()` policy, when `sessions` is in `sql.modules`. */
1310
+ function sessionlessTables(context) {
1311
+ if (!context.config.sql.moduleNames.includes("sessions")) return [];
1312
+ const exclude = globs(context.config.sql.modules["sessions"]?.options?.["exclude"]);
1313
+ const owned = moduleOwned(context);
1314
+ return catalogOf(context).tables.filter((table) => table.kind === "table" && table.rls && context.config.schemas.includes(table.schema) && !owned.schemas.has(table.schema) && !owned.tables.has(qualified(table)) && !exclude.some((pattern) => pattern.test(qualified(table))) && !table.policies.some((policy) => !policy.permissive && `${policy.using ?? ""} ${policy.check ?? ""}`.includes("session_active()"))).map((table) => ({
1315
+ message: `${qualified(table)} has no restrictive session_active() policy, so a signed-out or revoked session keeps its access until the token expires. Set sql.modules.sessions.options.policies to true and run \`better-supabase sql sync\`, add the policy by hand, or list the table in sql.modules.sessions.options.exclude.`,
1316
+ target: qualified(table),
1317
+ object: tableObject(table)
1318
+ }));
1319
+ }
1320
+ /** `better_supabase` and every `modules.*.schema` that `[api] schemas` serves through the Data API. */
1321
+ function exposedModuleSchemas(context) {
1322
+ if (context.config.sql.moduleNames.length === 0) return [];
1323
+ const moduleSchemas = /* @__PURE__ */ new Set(["better_supabase"]);
1324
+ for (const module of Object.values(context.config.sql.modules)) if (module?.schema !== void 0) moduleSchemas.add(module.schema);
1325
+ return exposedSchemas(context).filter((schema) => moduleSchemas.has(schema)).map((schema) => ({
1326
+ message: `The Data API serves the module schema ${schema}, so its tables and internal helpers are reachable over REST and RPC. Remove it from [api] schemas in supabase/config.toml (and the dashboard's exposed schemas), set sql.modules.<module>.api to an exposed schema such as "api" so \`sql add\` writes security invoker entry points there, and call them with rpcTransport(supabase, { schema: "api" }).`,
1053
1327
  target: schema
1054
1328
  }));
1055
1329
  }
1330
+ /**
1331
+ * supautils' `reserved_roles` on Supabase. Nobody but a superuser may alter,
1332
+ * drop or change the memberships of these.
1333
+ */
1334
+ const RESERVED_ROLES = /* @__PURE__ */ new Set([
1335
+ "supabase_admin",
1336
+ "supabase_auth_admin",
1337
+ "supabase_storage_admin",
1338
+ "supabase_functions_admin",
1339
+ "supabase_read_only_user",
1340
+ "supabase_realtime_admin",
1341
+ "supabase_replication_admin",
1342
+ "supabase_etl_admin",
1343
+ "dashboard_user",
1344
+ "pgbouncer"
1345
+ ]);
1346
+ /** Reserved roles whose settings (`alter role ... set`) supautils allows changing. */
1347
+ const SETTINGS_ONLY_ROLES = /* @__PURE__ */ new Set([
1348
+ "authenticator",
1349
+ "authenticated",
1350
+ "anon",
1351
+ "service_role"
1352
+ ]);
1353
+ /** Comments and string literals blanked, newlines kept so offsets map to lines. */
1354
+ const sqlCode = (text) => text.replaceAll(/\/\*[\s\S]*?\*\//g, (comment) => comment.replaceAll(/[^\n]/g, " ")).replaceAll(/--[^\n]*/g, "").replaceAll(/'(?:[^']|'')*'/g, "''");
1355
+ const roleNames = (list) => list.split(",").map((name) => name.trim().replaceAll("\"", "").toLowerCase()).filter((name) => name !== "");
1356
+ /** Why a statement changes a reserved role, or undefined when it doesn't. */
1357
+ function reservedRoleChange(statement) {
1358
+ const alter = /^\s*(alter|drop)\s+(?:role|user)\s+(?:if\s+exists\s+)?("[^"]+"|\w+)\s*(\w*)/i.exec(statement);
1359
+ if (alter) {
1360
+ const role = roleNames(alter[2])[0];
1361
+ const verb = alter[1].toLowerCase();
1362
+ const next = alter[3].toLowerCase();
1363
+ if (RESERVED_ROLES.has(role)) return `${verb}s the reserved role ${role}`;
1364
+ if (SETTINGS_ONLY_ROLES.has(role) && (verb === "drop" || next !== "set" && next !== "reset" && next !== "in")) return `${verb}s the reserved role ${role}; only \`alter role ${role} set ...\` is allowed`;
1365
+ return;
1366
+ }
1367
+ const membership = /^\s*(grant|revoke)\s+(?![\s\S]*\bon\b)([\w",\s]+?)\s+(?:to|from)\s+([\w",\s]+?)(?:\s+with\s+\w+\s+option)?\s*$/i.exec(statement);
1368
+ if (!membership) return void 0;
1369
+ const role = [...roleNames(membership[2]), ...roleNames(membership[3])].find((name) => RESERVED_ROLES.has(name));
1370
+ return role === void 0 ? void 0 : `${membership[1].toLowerCase()}s a membership of the reserved role ${role}`;
1371
+ }
1372
+ /** Where a statement ends, including statements inside `do` blocks and function bodies. */
1373
+ const STATEMENT_BOUNDARY = /;|\$\w*\$|\b(?:begin|then|else|loop)\b/gi;
1374
+ /** Statements in schema files and migrations that supautils rejects on Supabase. */
1375
+ function reservedRoles(context) {
1376
+ return (context.sqlFiles ?? []).flatMap((file) => {
1377
+ const code = sqlCode(file.text);
1378
+ const findings = [];
1379
+ let start = 0;
1380
+ for (const end of [...code.matchAll(STATEMENT_BOUNDARY), void 0]) {
1381
+ const until = end?.index ?? code.length;
1382
+ const statement = code.slice(start, until);
1383
+ const reason = reservedRoleChange(statement);
1384
+ if (reason !== void 0) {
1385
+ const at = start + statement.search(/\S/);
1386
+ const line = code.slice(0, at).split("\n").length;
1387
+ findings.push({
1388
+ message: `${file.path}:${String(line)} ${reason}. supautils rejects it on Supabase, so the migration fails on a hosted project; grant privileges to your own role instead.`,
1389
+ target: `${file.path}:${String(line)}`,
1390
+ location: {
1391
+ file: file.path,
1392
+ line
1393
+ }
1394
+ });
1395
+ }
1396
+ start = until + (end?.[0].length ?? 0);
1397
+ }
1398
+ return findings;
1399
+ });
1400
+ }
1056
1401
  function migrationOptions(context) {
1057
- const kit = new Set(context.config.sql.kit);
1058
- return migrationOptionUses(context.config.kits).filter((use) => kit.has(use.module)).map((use) => ({
1402
+ const module = new Set(context.config.sql.moduleNames);
1403
+ return migrationOptionUses(context.config.sql.modules).filter((use) => module.has(use.module)).map((use) => ({
1059
1404
  message: `${use.message} It exists to adopt an existing schema; remove it once your data matches the managed default.`,
1060
- target: `kits.${use.module}.options.${use.option}`
1405
+ target: `sql.modules.${use.module}.options.${use.option}`
1406
+ }));
1407
+ }
1408
+ /** A roles table shared by tenant and platform roles without `where` on both sides. */
1409
+ function sharedRoleTables(context) {
1410
+ if (context.config.sql.moduleNames.length === 0) return [];
1411
+ const access = accessModule(context);
1412
+ if (access.kind !== "provider") return [];
1413
+ let problems;
1414
+ try {
1415
+ problems = sharedRolesProblems(context.config.sql.moduleNames, moduleLayout(context.config, context.config.sql.testsDir, [], entitlementsModule(context), access.access, permissionCatalogKeys(context)));
1416
+ } catch {
1417
+ return [];
1418
+ }
1419
+ return problems.map(({ setting, message }) => ({
1420
+ message: `${message} Without it, ${setting.startsWith("sql.modules.tenant.") ? "an organization invitation, accept or role change can resolve a platform role" : "a platform invitation can resolve a tenant role"}.`,
1421
+ target: setting
1061
1422
  }));
1062
1423
  }
1063
- const KIT_RULES = [
1424
+ const MODULE_RULES = [
1064
1425
  {
1065
1426
  code: "BS307",
1066
1427
  severity: "error",
1067
- title: "Custom kit module without its contract",
1068
- description: "A module in `kits` uses `mode: 'custom'`, so the app writes its contract functions. One is missing or has another signature, and the modules and TypeScript APIs that call it fail at run time.",
1428
+ title: "Custom SQL module without its contract",
1429
+ description: "A module in `sql.modules` uses `mode: 'custom'`, so the app writes its contract functions. One is missing or has another signature, and the modules and TypeScript APIs that call it fail at run time.",
1069
1430
  check: missingContracts
1070
1431
  },
1071
1432
  {
1072
1433
  code: "BS308",
1073
1434
  severity: "warning",
1074
1435
  title: "Tenant claim the hook does not write",
1075
- description: "With the `tenant` module and `kits.access.activeTenant: 'claim'`, `current_tenant_id()` and the `tenant()` plugin read the tenant from the `claims.tenant` claim, at the top level or in `app_metadata`. With `--as <user id>` doctor calls the custom access token hook for that user and warns when the claims it returns have neither. The default source, `'resolver'`, takes the tenant from the request instead.",
1436
+ description: "With the `tenant` module and `sql.modules.access.activeTenant: 'claim'`, `current_tenant_id()` and the `tenant()` plugin read the tenant from the `claims.tenant` claim, at the top level or in `app_metadata`. With `--as <user id>` doctor calls the custom access token hook for that user and warns when the claims it returns have neither. The default source, `'resolver'`, takes the tenant from the request instead.",
1076
1437
  check: missingTenantClaim
1077
1438
  },
1078
1439
  {
1079
1440
  code: "BS309",
1080
1441
  severity: "warning",
1081
- title: "Deprecated SQL kit symbol",
1082
- description: "A schema file or policy uses a function, table, column or claim that a kit module deprecated or removed. Deprecated symbols keep a wrapper for at least one minor release; removed ones fail at run time.",
1442
+ title: "Deprecated SQL module symbol",
1443
+ description: "A schema file or policy uses a function, table, column or claim that a SQL module deprecated or removed. Deprecated symbols keep a wrapper for at least one minor release; removed ones fail at run time.",
1083
1444
  check: deprecatedSymbols
1084
1445
  },
1085
1446
  {
1086
1447
  code: "BS310",
1087
1448
  severity: "warning",
1088
- title: "Duplicate kit trigger",
1089
- description: "A table has a kit trigger (`bs_updated_at`, `bs_audit`) and another trigger that does the same work, so both run on every write.",
1449
+ title: "Duplicate module trigger",
1450
+ description: "A table has a module trigger (`bs_updated_at`, `bs_audit`) and another trigger that does the same work, so both run on every write.",
1090
1451
  check: duplicateTriggers
1091
1452
  },
1092
1453
  {
1093
1454
  code: "BS313",
1094
1455
  severity: "warning",
1095
1456
  title: "Rate limits not wired to PostgREST",
1096
- description: "The `rate-limit` module is in `sql.kit`, but on the live database `pgrst.db_pre_request` for the `authenticator` role is unset or names a function that doesn't call `better_supabase.check_request()`, so Data API requests are never counted.",
1457
+ description: "The `rate-limit` module is in `sql.modules`, but on the live database `pgrst.db_pre_request` for the `authenticator` role is unset or names a function that doesn't call `better_supabase.check_request()`, so Data API requests are never counted.",
1097
1458
  check: rateLimitHook
1098
1459
  },
1099
1460
  {
1100
1461
  code: "BS312",
1101
1462
  severity: "error",
1102
- title: "Kit schema exposed through the Data API",
1103
- description: "`[api] schemas` in `config.toml` lists `better_supabase` or a `kits.*.schema`. The kit schemas hold internal tables and helpers that are granted to `authenticated` for policies, so exposing them makes those callable over REST and RPC.",
1104
- check: exposedKitSchemas
1463
+ title: "Module schema exposed through the Data API",
1464
+ description: "`[api] schemas` in `config.toml` lists `better_supabase` or a `modules.*.schema`. The module schemas hold internal tables and helpers that are granted to `authenticated` for policies, so exposing them makes those callable over REST and RPC.",
1465
+ check: exposedModuleSchemas
1105
1466
  },
1106
1467
  {
1107
1468
  code: "BS314",
1108
1469
  severity: "warning",
1109
- title: "Migration-only kit option",
1110
- description: "A module in adopt mode sets an option that only exists to match an existing schema: plain invitation tokens, webhook secrets in a column, non-text webhook ids, or a custom outbox source. Remove it once the data matches the managed default.",
1470
+ title: "Migration-only module option",
1471
+ description: "A module in adopt mode sets an option that only exists to match an existing schema: plain invitation tokens, webhook secrets in a column, non-text webhook ids, a custom outbox source, or audit values mapped to an adopted log's. Remove it once the data matches the managed default.",
1111
1472
  check: migrationOptions
1473
+ },
1474
+ {
1475
+ code: "BS315",
1476
+ severity: "warning",
1477
+ title: "Table without an audit trigger",
1478
+ description: "The `audit` module is in `sql.modules`, and a table in `schemas` has no `bs_audit` trigger (or another trigger that calls `audit_row_change()`), so its changes are not in the audit log. The module's own schemas and the tables SQL modules adopt are skipped; `sql.modules.audit.options.exempt` lists more, as `schema.table` globs such as `public.*_archive`.",
1479
+ check: unauditedTables
1480
+ },
1481
+ {
1482
+ code: "BS319",
1483
+ severity: "error",
1484
+ title: "SQL alters a reserved role",
1485
+ description: "A schema file, SQL module or migration alters, drops or changes the memberships of a role supautils reserves on Supabase (`supabase_admin`, `supabase_auth_admin`, `supabase_storage_admin`, `pgbouncer` and the other platform roles). The statement fails on a hosted project. `authenticator`, `authenticated`, `anon` and `service_role` accept `alter role ... set` (for example `pgrst.db_pre_request` or `statement_timeout`) and nothing else.",
1486
+ check: reservedRoles
1487
+ },
1488
+ {
1489
+ code: "BS320",
1490
+ severity: "warning",
1491
+ title: "Table without the session policy",
1492
+ description: "The `sessions` module is in `sql.modules`, and a table with RLS in `schemas` has no restrictive policy that calls `better_supabase.session_active()`, so a token whose session was signed out, or whose user was banned or deleted, keeps reaching it until it expires. `sql.modules.sessions.options.policies` writes the policy on every table; `options.exclude` lists the tables to skip, as `schema.table` globs.",
1493
+ check: sessionlessTables
1494
+ },
1495
+ {
1496
+ code: "BS322",
1497
+ severity: "warning",
1498
+ title: "Audit registration of a dropped table",
1499
+ description: "The `audit` module is in `sql.modules`, and `better_supabase.audited_tables` on the live database has a row whose table no longer exists. A table dropped before the module's `sql_drop` event trigger was installed keeps its row; the module's data migration deletes such rows.",
1500
+ check: danglingAuditRegistrations
1501
+ },
1502
+ {
1503
+ code: "BS323",
1504
+ severity: "warning",
1505
+ title: "Module event trigger missing",
1506
+ description: "A module in `sql.modules` creates an event trigger (`bs_audit_forget_dropped` for `audit`, `bs_ensure_rls` for `ensure-rls`) that the live database lacks, or, without a database, that no migration creates. Event triggers belong to no schema, so a schema diff limited to some schemas leaves them out; the module's data migration creates them.",
1507
+ check: missingEventTriggers
1508
+ },
1509
+ {
1510
+ code: "BS324",
1511
+ severity: "error",
1512
+ title: "Shared roles table without a role condition",
1513
+ description: "Under the `provider` model, the tenant module's `roleThrough` and the invitations module's `platformRoles.through` name the same roles table, and one of them has no `where` condition on `{row}`. That side then resolves a role id or key of the other kind: an organization invitation, accept or `update_member_role` can grant a platform role, or a platform invitation a tenant role. `sql sync` refuses the config; set `where` on both, such as `{row}.scope = 'organization'` and `{row}.scope = 'system'`.",
1514
+ check: sharedRoleTables
1515
+ },
1516
+ {
1517
+ code: "BS325",
1518
+ severity: "warning",
1519
+ title: "Audit log readable only as the service role",
1520
+ description: "The `audit` module defaults `eventRoles` to `service_role` and `readPolicy` to false, so `audit.list()` under a user session is 403. Set `options.readPolicy: true` and include `authenticated` in `options.eventRoles` when the app lists the log as the signed-in user.",
1521
+ check: auditUserSession
1522
+ },
1523
+ {
1524
+ code: "BS326",
1525
+ severity: "warning",
1526
+ title: "Admin Auth call without a secret key in env files",
1527
+ description: "`deleteAccount` and `bs.admin()` need `SUPABASE_SECRET_KEY`. The env files doctor reads do not set it, so those calls fail at run time.",
1528
+ check: missingSecretKey
1529
+ },
1530
+ {
1531
+ code: "BS327",
1532
+ severity: "warning",
1533
+ title: "aal2 required without MFA enabled",
1534
+ description: "The app calls `requireAal('aal2')` (or otherwise requires aal2), but `[auth.mfa.totp]` in `config.toml` does not enable enroll and verify, so no user can satisfy the check.",
1535
+ check: aal2WithoutMfa
1536
+ }
1537
+ ];
1538
+ function auditUserSession(context) {
1539
+ if (!context.config.sql.moduleNames.includes("audit")) return [];
1540
+ const options = context.config.sql.modules["audit"]?.options ?? {};
1541
+ const roles = Array.isArray(options["eventRoles"]) ? options["eventRoles"].filter((role) => typeof role === "string") : ["service_role"];
1542
+ if (options["readPolicy"] === true && roles.includes("authenticated")) return [];
1543
+ if (!context.sources.some((file) => /\baudit\.(list|listAll)\s*\(/.test(file.text))) return [];
1544
+ return [{
1545
+ message: "sql.modules.audit leaves the log readable only as the service role (`eventRoles` defaults to service_role, `readPolicy` defaults to false), but the app calls audit.list() or audit.listAll() as a user. Set options.readPolicy to true and include authenticated in options.eventRoles.",
1546
+ target: "sql.modules.audit"
1547
+ }];
1548
+ }
1549
+ function missingSecretKey(context) {
1550
+ if (!context.sources.some((file) => /\bdeleteAccount\b|\.admin\s*\(|supabaseAdmin\b/.test(file.text))) return [];
1551
+ if (context.envFiles.some((file) => /^\s*(?:SUPABASE_SECRET_KEY|SUPABASE_SERVICE_ROLE_KEY)\s*=/m.test(file.text))) return [];
1552
+ return [{
1553
+ message: "The app calls deleteAccount or bs.admin(), which need SUPABASE_SECRET_KEY, but no env file doctor reads sets it. Add it to .env.local (and the hosted environments), listed without a value in .env.example.",
1554
+ target: "SUPABASE_SECRET_KEY"
1555
+ }];
1556
+ }
1557
+ function aal2WithoutMfa(context) {
1558
+ const toml = context.configToml;
1559
+ if (!toml) return [];
1560
+ if (!context.sources.some((file) => /requireAal\s*\(\s*['"]aal2['"]/.test(file.text))) return [];
1561
+ const enroll = tomlGet(toml.document, [
1562
+ "auth",
1563
+ "mfa",
1564
+ "totp",
1565
+ "enroll_enabled"
1566
+ ]);
1567
+ const verify = tomlGet(toml.document, [
1568
+ "auth",
1569
+ "mfa",
1570
+ "totp",
1571
+ "verify_enabled"
1572
+ ]);
1573
+ if (enroll === true && verify === true) return [];
1574
+ return [{
1575
+ message: "The app requires aal2, but [auth.mfa.totp] in config.toml does not set enroll_enabled and verify_enabled to true, so no user can satisfy the check.",
1576
+ target: toml.path,
1577
+ location: {
1578
+ file: toml.path,
1579
+ line: lineOf(toml.text, /^\[auth\.mfa\.totp\]/) ?? 1
1580
+ }
1581
+ }];
1582
+ }
1583
+ //#endregion
1584
+ //#region src/cli/doctor/pgdelta.ts
1585
+ /** Comments blanked out, so a commented-out statement never matches and line numbers stay. */
1586
+ function withoutComments(text) {
1587
+ return text.replaceAll(/\/\*[\s\S]*?\*\//g, (comment) => comment.replaceAll(/[^\n]/g, " ")).replaceAll(/--[^\n]*/g, (comment) => " ".repeat(comment.length));
1588
+ }
1589
+ const lineAt = (text, index) => text.slice(0, index).split("\n").length;
1590
+ /**
1591
+ * The declarative schema files pg-delta loads, outside the SQL module files
1592
+ * better-supabase writes, when the project diffs with pg-delta.
1593
+ */
1594
+ function pgdeltaSchemaFiles(context) {
1595
+ const toml = context.configToml;
1596
+ if (toml === void 0 || diffEngine(toml) !== "pg-delta") return [];
1597
+ const schemas = `${toml.dir}/schemas/`;
1598
+ return (context.sqlFiles ?? []).filter((file) => file.path.startsWith(schemas) && !/^-- @bs-module /m.test(file.text));
1599
+ }
1600
+ const BULK_GRANT = /\bgrant\b[^;]*?\bon\s+all\s+(tables|routines|functions|procedures|sequences)\s+in\s+schema\s+("?[\w$]+"?(?:\s*,\s*"?[\w$]+"?)*)\s+to\s+([^;]*)/gi;
1601
+ /** Grantees a bulk grant must not reach: the Data API roles and `public`. */
1602
+ const API_ROLES$1 = /\b(anon|authenticated|public)\b/i;
1603
+ /** `do` blocks; group 2 is the body. */
1604
+ const DO_BLOCK = /\bdo\s+(\$[\w]*\$)([\s\S]*?)\1/gi;
1605
+ const CATALOG = /\b(information_schema\.\w+|(?:pg_catalog\.)?pg_(?:class|tables|views|proc|namespace|attribute|trigger|constraint|index|type|policies|matviews))\b/i;
1606
+ const EXECUTES = /\b(execute|perform)\b/i;
1607
+ function bulkGrants(context) {
1608
+ return pgdeltaSchemaFiles(context).flatMap((file) => {
1609
+ const text = withoutComments(file.text);
1610
+ return [...text.matchAll(BULK_GRANT)].filter((match) => API_ROLES$1.test(match[3])).map((match) => ({
1611
+ message: `${file.path} grants on all ${match[1].toLowerCase()} in schema ${match[2]} at once. pg-delta gives a bulk grant no dependency position, so it can run after the per-object revokes and open functions and tables to anon and authenticated again. Grant each object by name next to its definition, or set Supabase default privileges (alter default privileges in schema ... grant ...) for objects created later.`,
1612
+ target: file.path,
1613
+ location: {
1614
+ file: file.path,
1615
+ line: lineAt(text, match.index)
1616
+ }
1617
+ }));
1618
+ });
1619
+ }
1620
+ function catalogLoops(context) {
1621
+ return pgdeltaSchemaFiles(context).flatMap((file) => {
1622
+ const text = withoutComments(file.text);
1623
+ return [...text.matchAll(DO_BLOCK)].flatMap((match) => {
1624
+ const body = match[2];
1625
+ const catalog = CATALOG.exec(body);
1626
+ if (!catalog || !EXECUTES.test(body)) return [];
1627
+ return [{
1628
+ message: `${file.path} has a do block that reads ${catalog[1]} to create objects. pg-delta runs a do block with no dependency position, before the tables it looks for exist, so it creates nothing and the diff never sees its objects. Write the statements it would generate (one trigger, grant or policy per table) as static SQL, or call a per-table function such as better_supabase.audit('<table>') or track_updated_at('<table>') once per table.`,
1629
+ target: file.path,
1630
+ location: {
1631
+ file: file.path,
1632
+ line: lineAt(text, match.index)
1633
+ }
1634
+ }];
1635
+ });
1636
+ });
1637
+ }
1638
+ const CREATE_EXTENSION = /\bcreate\s+extension\s+(?:if\s+not\s+exists\s+)?"?([\w-]+)"?/gi;
1639
+ /**
1640
+ * Extensions a pg-delta schema file creates, SQL module files included, that
1641
+ * no migration creates. pg-delta can leave an extension that owns its own
1642
+ * schema (`pgmq`) out of the generated migration.
1643
+ */
1644
+ function missingExtensions(context) {
1645
+ const toml = context.configToml;
1646
+ if (toml === void 0 || diffEngine(toml) !== "pg-delta") return [];
1647
+ const files = context.sqlFiles ?? [];
1648
+ const migrations = files.filter((file) => file.path.startsWith(`${toml.dir}/migrations/`));
1649
+ if (migrations.length === 0) return [];
1650
+ const created = new Set(migrations.flatMap((file) => [...withoutComments(file.text).matchAll(CREATE_EXTENSION)].map((match) => match[1].toLowerCase())));
1651
+ const seen = /* @__PURE__ */ new Set();
1652
+ return files.filter((file) => file.path.startsWith(`${toml.dir}/schemas/`)).flatMap((file) => {
1653
+ const text = withoutComments(file.text);
1654
+ return [...text.matchAll(CREATE_EXTENSION)].flatMap((match) => {
1655
+ const name = match[1].toLowerCase();
1656
+ if (created.has(name) || seen.has(name)) return [];
1657
+ seen.add(name);
1658
+ const module = /^-- @bs-module /m.test(file.text);
1659
+ return [{
1660
+ message: `${file.path} creates the extension ${name}, but no migration creates it. pg-delta can leave an extension that owns its own schema out of the generated migration, and a database built from the migrations then lacks it. ${module ? "Run `better-supabase sql sync`: it writes the extension into a migration that runs before the schema migration." : `Add \`create extension if not exists ${name};\` to a migration that runs before the one that uses it.`}`,
1661
+ target: file.path,
1662
+ location: {
1663
+ file: file.path,
1664
+ line: lineAt(text, match.index)
1665
+ }
1666
+ }];
1667
+ });
1668
+ });
1669
+ }
1670
+ /** Statements in declarative schema files that pg-delta can't order. */
1671
+ const PGDELTA_RULES = [
1672
+ {
1673
+ code: "BS317",
1674
+ severity: "warning",
1675
+ title: "Bulk grant in a pg-delta schema file",
1676
+ description: "A `grant ... on all tables|routines|sequences in schema ... to anon|authenticated|public` statement in `supabase/schemas` under pg-delta. pg-delta can't place it after the objects it covers or before the revokes that narrow them, so it can re-open functions and tables to `anon` and `authenticated`.",
1677
+ check: bulkGrants
1678
+ },
1679
+ {
1680
+ code: "BS318",
1681
+ severity: "warning",
1682
+ title: "Catalog loop in a pg-delta schema file",
1683
+ description: "A `do` block in `supabase/schemas` under pg-delta that reads the catalog (`information_schema`, `pg_class`, ...) to create objects. pg-delta runs it before the tables exist, so it creates nothing.",
1684
+ check: catalogLoops
1685
+ },
1686
+ {
1687
+ code: "BS321",
1688
+ severity: "warning",
1689
+ title: "Extension missing from the migrations under pg-delta",
1690
+ description: "A `create extension` in `supabase/schemas` (a SQL module's file included) under pg-delta that no migration repeats. pg-delta can leave an extension that owns its own schema, such as `pgmq`, out of the generated migration.",
1691
+ check: missingExtensions
1112
1692
  }
1113
1693
  ];
1114
1694
  //#endregion
@@ -1292,7 +1872,7 @@ function grantColumns(context) {
1292
1872
  if (existing) return existing;
1293
1873
  const created = {
1294
1874
  helpers: /* @__PURE__ */ new Set(),
1295
- permdock: false
1875
+ provider: false
1296
1876
  };
1297
1877
  byColumn.set(column, created);
1298
1878
  return created;
@@ -1306,20 +1886,22 @@ function grantColumns(context) {
1306
1886
  if (qualifiedColumn.test(body) || read.length === 1 && bare.test(body)) entry(qualified(table), column.name).helpers.add(helper);
1307
1887
  }
1308
1888
  }
1309
- for (const deciding of context.permdock?.manifest?.decidingColumns ?? []) {
1889
+ for (const deciding of context.config.authorization?.decidingColumns ?? []) {
1310
1890
  const dot = deciding.lastIndexOf(".");
1311
- if (dot > 0) entry(deciding.slice(0, dot), deciding.slice(dot + 1)).permdock = true;
1891
+ if (dot > 0) entry(deciding.slice(0, dot), deciding.slice(dot + 1)).provider = true;
1312
1892
  }
1313
1893
  return columns;
1314
1894
  }
1315
1895
  /** Whether `role` may run `command` on rows of `table` at all: no RLS, or a permissive policy for it. */
1316
1896
  const policyAllows = (table, command, role) => !table.rls || table.policies.some((policy) => policy.permissive && (policy.command === "all" || policy.command === command) && appliesTo(policy, role));
1897
+ /** Whether `grant` reaches `role`: granted to it, or to PUBLIC, which every role inherits. */
1898
+ const grantsTo = (grant, role) => grant.role === role || grant.role === "PUBLIC";
1317
1899
  /** The grant columns `role` may write with `command`, through a table-level or a column-level grant. */
1318
1900
  function writableColumns(table, columns, command, role) {
1319
1901
  if (!policyAllows(table, command, role)) return [];
1320
1902
  const privilege = command.toUpperCase();
1321
- if (table.grants.some((grant) => grant.role === role && grant.privileges.includes(privilege))) return [...columns];
1322
- return columns.filter((column) => (table.columnGrants ?? []).some((grant) => grant.role === role && grant.column === column && grant.privileges.includes(privilege)));
1903
+ if (table.grants.some((grant) => grantsTo(grant, role) && grant.privileges.includes(privilege))) return [...columns];
1904
+ return columns.filter((column) => (table.columnGrants ?? []).some((grant) => grantsTo(grant, role) && grant.column === column && grant.privileges.includes(privilege)));
1323
1905
  }
1324
1906
  /** PostgREST's default for `db-hoisted-tx-settings`. */
1325
1907
  const DEFAULT_HOISTED = [
@@ -1389,24 +1971,28 @@ const RLS_RULES = [
1389
1971
  code: "BS207",
1390
1972
  severity: "warning",
1391
1973
  title: "Several permissive policies for one command and role",
1392
- description: "Postgres evaluates every permissive policy that applies and ORs them, so each extra policy adds its cost to every row. Merge them into one policy per command and role. This replaces splinter's `multiple_permissive_policies` lint for the same table.",
1393
- check: (context) => rlsTables(context).flatMap((table) => {
1394
- const overlaps = permissiveOverlaps(table);
1395
- if (overlaps.length === 0) return [];
1396
- const first = table.policies.find((policy) => policy.name === overlaps[0].policies[0]);
1397
- const list = overlaps.map((overlap) => `${overlap.command} for ${overlap.role}: ${overlap.policies.join(", ")}`).join("; ");
1398
- return [{
1399
- message: `${qualified(table)} has several permissive policies per command and role (${list}). Postgres runs all of them for every row; merge each group into one policy.`,
1400
- target: qualified(table),
1401
- object: policyObject(table, first)
1402
- }];
1403
- })
1974
+ description: "Postgres evaluates every permissive policy that applies and ORs them, so each extra policy adds its cost to every row. Merge them into one policy per command and role. When the performance advisor (BS200) runs, splinter's `multiple_permissive_policies` reports a table it covers and this rule skips it; on a saved snapshot this rule reports every table.",
1975
+ check: async (context) => {
1976
+ const splinter = await splinterTables(context, "multiple_permissive_policies");
1977
+ return rlsTables(context).flatMap((table) => {
1978
+ if (splinter?.has(qualified(table))) return [];
1979
+ const overlaps = permissiveOverlaps(table);
1980
+ if (overlaps.length === 0) return [];
1981
+ const first = table.policies.find((policy) => policy.name === overlaps[0].policies[0]);
1982
+ const list = overlaps.map((overlap) => `${overlap.command} for ${overlap.role}: ${overlap.policies.join(", ")}`).join("; ");
1983
+ return [{
1984
+ message: `${qualified(table)} has several permissive policies per command and role (${list}). Postgres runs all of them for every row; merge each group into one policy.`,
1985
+ target: qualified(table),
1986
+ object: policyObject(table, first)
1987
+ }];
1988
+ });
1989
+ }
1404
1990
  },
1405
1991
  {
1406
1992
  code: "BS213",
1407
1993
  severity: "warning",
1408
1994
  title: "API roles can write the columns that grant access",
1409
- description: "RLS helpers decide access from columns such as `memberships.user_id`, `memberships.role` or `contacts.customer_id`, and PermDock's manifest lists them as `decidingColumns`. When `anon` or `authenticated` may insert or update one of them, and a policy lets them write the row, a user can grant themselves access. A table-level grant counts even after a column-level revoke.",
1995
+ description: "RLS helpers decide access from columns such as `memberships.user_id`, `memberships.role` or `contacts.customer_id`, and an authorization provider lists them as `decidingColumns`. When `anon` or `authenticated` may insert or update one of them, and a policy lets them write the row, a user can grant themselves access. A table-level grant counts even after a column-level revoke.",
1410
1996
  check: (context) => {
1411
1997
  const tables = new Map(catalogOf(context).tables.map((table) => [qualified(table), table]));
1412
1998
  return [...grantColumns(context)].flatMap(([name, byColumn]) => {
@@ -1421,18 +2007,24 @@ const RLS_RULES = [
1421
2007
  const writable = writableColumns(table, columns, command, role);
1422
2008
  if (writable.length === 0) continue;
1423
2009
  writes.push(`${role} may ${command} ${writable.join(", ")}`);
1424
- if (table.grants.some((grant) => grant.role === role && grant.privileges.includes(command.toUpperCase()))) tableLevel.push(command);
1425
- else fix.push(`revoke ${command} (${writable.join(", ")}) on ${name} from ${role};`);
2010
+ const privilege = command.toUpperCase();
2011
+ if (table.grants.some((grant) => grantsTo(grant, role) && grant.privileges.includes(privilege))) {
2012
+ tableLevel.push(command);
2013
+ continue;
2014
+ }
2015
+ const fromPublic = (table.columnGrants ?? []).some((grant) => grant.role === "PUBLIC" && writable.includes(grant.column) && grant.privileges.includes(privilege));
2016
+ fix.push(`revoke ${command} (${writable.join(", ")}) on ${name} from ${fromPublic ? `${role}, public` : role};`);
1426
2017
  }
1427
2018
  if (tableLevel.length === 0) continue;
1428
- fix.push(`revoke ${tableLevel.join(", ")} on ${name} from ${role};`);
2019
+ const fromPublic = table.grants.some((grant) => grant.role === "PUBLIC" && tableLevel.some((command) => grant.privileges.includes(command.toUpperCase())));
2020
+ fix.push(`revoke ${tableLevel.join(", ")} on ${name} from ${fromPublic ? `${role}, public` : role};`);
1429
2021
  const rest = table.columns.map((column) => column.name).filter((column) => !byColumn.has(column));
1430
2022
  if (rest.length > 0) fix.push(`grant ${tableLevel.map((command) => `${command} (${rest.join(", ")})`).join(", ")} on ${name} to ${role};`);
1431
2023
  }
1432
2024
  if (writes.length === 0) return [];
1433
2025
  const helpers = [...new Set(columns.flatMap((column) => [...byColumn.get(column).helpers]))].sort();
1434
- const permdock = columns.filter((column) => byColumn.get(column).permdock);
1435
- const readers = [...helpers.length > 0 ? [`${helpers.join(", ")} read${helpers.length === 1 ? "s" : ""} them to decide access`] : [], ...permdock.length > 0 ? [`PermDock's manifest lists ${permdock.join(", ")} as deciding columns, which PD028 protects`] : []].join(", and ");
2026
+ const deciding = columns.filter((column) => byColumn.get(column).provider);
2027
+ const readers = [...helpers.length > 0 ? [`${helpers.join(", ")} read${helpers.length === 1 ? "s" : ""} them to decide access`] : [], ...deciding.length > 0 ? [`the authorization provider lists ${deciding.join(", ")} as deciding columns`] : []].join(", and ");
1436
2028
  return [{
1437
2029
  message: `${name}: ${writes.join("; ")}. ${readers}, so a user who writes them can grant themselves access. Revoke the columns and grant the rest:\n${fix.join("\n")}`,
1438
2030
  target: name,
@@ -1474,7 +2066,7 @@ const RLS_RULES = [
1474
2066
  //#region src/cli/doctor/policies.ts
1475
2067
  const AUTH_ROLE = /\bauth"?\s*\.\s*"?role"?\s*\(\s*\)/i;
1476
2068
  /** A check of who is calling: the JWT, the user id, or the session role. */
1477
- const CALLER_CHECK = /\bauth"?\s*\.\s*"?(?:uid|jwt)"?\s*\(|request\.jwt|\b(?:current_user|session_user|current_role)\b|has_org_role|member_org_ids/i;
2069
+ const CALLER_CHECK = /\bauth"?\s*\.\s*"?(?:uid|jwt)"?\s*\(|request\.jwt|\b(?:current_user|session_user|current_role)\b|has_organization_role|member_organization_ids/i;
1478
2070
  const USER_METADATA = /\buser_metadata\b/i;
1479
2071
  /** Privileges an API role never needs on a table. */
1480
2072
  const NEEDLESS = [
@@ -1704,15 +2296,18 @@ const SCHEMA_DESIGN_RULES = [
1704
2296
  code: "BS216",
1705
2297
  severity: "info",
1706
2298
  title: "Foreign key without an index",
1707
- description: "Deletes and updates on the referenced table scan the referencing table for each row, and joins on the key can't use an index. Add an index that starts with the key's columns. This replaces splinter's `unindexed_foreign_keys` for the same table and also runs on a saved snapshot.",
1708
- check: (context) => exposed(context).flatMap((table) => {
1709
- if (table.kind !== "table") return [];
1710
- return unindexedForeignKeys(table).map((key) => ({
1711
- message: `${qualified(table)}.${key.name} (${key.columns.join(", ")}) has no index that starts with its columns, so deletes on ${key.refSchema}.${key.refTable} scan ${qualified(table)}. Run \`create index on ${qualified(table)} (${key.columns.join(", ")});\`.`,
1712
- target: `${qualified(table)}.${key.name}`,
1713
- object: tableObject(table)
1714
- }));
1715
- })
2299
+ description: "Deletes and updates on the referenced table scan the referencing table for each row, and joins on the key can't use an index. Add an index that starts with the key's columns. When the performance advisor (BS200) runs, splinter's `unindexed_foreign_keys` reports a table it covers and this rule skips it; on a saved snapshot this rule reports every table.",
2300
+ check: async (context) => {
2301
+ const splinter = await splinterTables(context, "unindexed_foreign_keys");
2302
+ return exposed(context).flatMap((table) => {
2303
+ if (table.kind !== "table" || splinter?.has(qualified(table))) return [];
2304
+ return unindexedForeignKeys(table).map((key) => ({
2305
+ message: `${qualified(table)}.${key.name} (${key.columns.join(", ")}) has no index that starts with its columns, so deletes on ${key.refSchema}.${key.refTable} scan ${qualified(table)}. Run \`create index on ${qualified(table)} (${key.columns.join(", ")});\`.`,
2306
+ target: `${qualified(table)}.${key.name}`,
2307
+ object: tableObject(table)
2308
+ }));
2309
+ });
2310
+ }
1716
2311
  },
1717
2312
  {
1718
2313
  code: "BS217",
@@ -1819,6 +2414,27 @@ const SCHEMA_DESIGN_RULES = [
1819
2414
  }];
1820
2415
  }));
1821
2416
  }
2417
+ },
2418
+ {
2419
+ code: "BS222",
2420
+ severity: "warning",
2421
+ title: "Column that can exceed a JavaScript number",
2422
+ description: "`int8` and `numeric` decode as `number` by default, which matches `supabase gen types` but loses precision past 2^53 (or 15 to 17 significant digits). An `int8` identity or sequence keeps growing toward that limit, and `numeric` usually holds exact amounts. Set `codecs.int8` to `bigint` or `string`, or `codecs.numeric` to `string`.",
2423
+ check: (context) => {
2424
+ const { int8, numeric } = context.config.codecs;
2425
+ if (int8 !== "number" && numeric !== "number") return [];
2426
+ return exposed(context).flatMap((table) => {
2427
+ if (configFor(context.config.tables, table.schema, table.name)?.exclude) return [];
2428
+ const columns = table.columns.filter((column) => column.udt === "int8" ? int8 === "number" && !column.isArray && (column.identity !== null || (column.default?.includes("nextval(") ?? false)) : column.udt === "numeric" && numeric === "number");
2429
+ if (columns.length === 0) return [];
2430
+ const codecs = [...columns.some((column) => column.udt === "int8") ? ["`int8: \"bigint\"`"] : [], ...columns.some((column) => column.udt === "numeric") ? ["`numeric: \"string\"`"] : []];
2431
+ return [{
2432
+ message: `${qualified(table)} ${columns.map((column) => `${column.name} (${column.udt})`).join(", ")} decode as number and lose precision past 2^53. Set ${codecs.join(" and ")} in \`codecs\`.`,
2433
+ target: qualified(table),
2434
+ object: tableObject(table)
2435
+ }];
2436
+ });
2437
+ }
1822
2438
  }
1823
2439
  ];
1824
2440
  //#endregion
@@ -1827,29 +2443,40 @@ const DOCS_URL = "https://bettersupabase.com/docs/cli/doctor";
1827
2443
  const writes = (policy) => policy.command === "all" || policy.command === "insert" || policy.command === "update" || policy.command === "delete";
1828
2444
  const publicRoles = (policy) => policy.roles.length === 0 || policy.roles.some((role) => role === "public" || role === "anon");
1829
2445
  const isTrue = (expression) => expression !== null && /^\(*\s*true\s*\)*$/i.test(expression.trim());
1830
- /** The `sql.kit` files as this release renders them, next to what is on disk. */
1831
- async function kitFiles(context) {
1832
- if (context.config.sql.kit.length === 0) return [];
2446
+ /** The `sql.modules` files as this release renders them, next to what is on disk. */
2447
+ async function moduleFiles(context) {
2448
+ if (context.config.sql.moduleNames.length === 0) return [];
2449
+ const access = accessModule(context);
2450
+ if (access.kind === "invalid") return [];
1833
2451
  const readSets = context.readSets;
1834
2452
  const skipped = readSets !== void 0 && "skipped" in readSets;
1835
- const files = renderKit(context.config.sql.kit, kitLayout(context.config, context.config.sql.testsDir, skipped ? [] : readSets, entitlementsKit(context))).filter((file) => !(skipped && file.module === "read-sets"));
2453
+ let rendered;
2454
+ try {
2455
+ rendered = renderModules(context.config.sql.moduleNames, {
2456
+ ...moduleLayout(context.config, context.config.sql.testsDir, skipped ? [] : readSets, entitlementsModule(context), access.kind === "provider" ? access.access : void 0, permissionCatalogKeys(context)),
2457
+ schemasDir: declarativeSchemasDir(context.configToml)
2458
+ });
2459
+ } catch {
2460
+ return [];
2461
+ }
2462
+ const files = rendered.filter((file) => !(skipped && file.module === "read-sets"));
1836
2463
  return Promise.all(files.map(async (file) => {
1837
2464
  const current = await readFile(resolve(context.config.root, file.path), "utf8").catch(() => void 0);
1838
- const version = kitFileVersion(file.contents)?.version ?? 1;
2465
+ const version = moduleFileVersion(file.contents)?.version ?? 1;
1839
2466
  return {
1840
2467
  file,
1841
2468
  current,
1842
- behind: current !== void 0 && (kitFileVersion(current)?.version ?? 1) < version
2469
+ behind: current !== void 0 && (moduleFileVersion(current)?.version ?? 1) < version
1843
2470
  };
1844
2471
  }));
1845
2472
  }
1846
- async function kitVersionsBehind(context) {
2473
+ async function moduleVersionsBehind(context) {
1847
2474
  const findings = [];
1848
2475
  const upgrade = "Run `better-supabase sql upgrade`";
1849
- for (const { file, current, behind } of await kitFiles(context)) {
2476
+ for (const { file, current, behind } of await moduleFiles(context)) {
1850
2477
  if (!behind || current === void 0) continue;
1851
2478
  findings.push({
1852
- message: `${file.path} has ${file.module} version ${String(kitFileVersion(current)?.version ?? 1)}; this release ships version ${String(kitFileVersion(file.contents)?.version ?? 1)}. ${upgrade}, then \`${migrationCommand(context.configToml)}\`.`,
2479
+ message: `${file.path} has ${file.module} version ${String(moduleFileVersion(current)?.version ?? 1)}; this release ships version ${String(moduleFileVersion(file.contents)?.version ?? 1)}. ${upgrade}, then \`${migrationCommand(context.configToml)}\`.`,
1853
2480
  target: file.path,
1854
2481
  location: {
1855
2482
  file: file.path,
@@ -1858,21 +2485,21 @@ async function kitVersionsBehind(context) {
1858
2485
  });
1859
2486
  }
1860
2487
  const db = context.database;
1861
- if (!db || "skipped" in db || context.config.sql.kit.length === 0) return findings;
2488
+ if (!db || "skipped" in db || context.config.sql.moduleNames.length === 0) return findings;
1862
2489
  let rows;
1863
2490
  try {
1864
- rows = await db.query("select name, version from better_supabase.kit_modules");
2491
+ rows = await db.query("select name, version from better_supabase.modules");
1865
2492
  } catch {
1866
2493
  return findings;
1867
2494
  }
1868
2495
  for (const row of rows) {
1869
2496
  const module = SQL_MODULES[row.name];
1870
- if (!module || !context.config.sql.kit.includes(row.name)) continue;
2497
+ if (!module || !context.config.sql.moduleNames.includes(row.name)) continue;
1871
2498
  const version = moduleVersion(module);
1872
2499
  if (row.version >= version) continue;
1873
2500
  findings.push({
1874
2501
  message: `The database has ${row.name} version ${String(row.version)}; this release ships version ${String(version)}. ${upgrade} and apply the migrations it prints.`,
1875
- target: `better_supabase.kit_modules.${row.name}`
2502
+ target: `better_supabase.modules.${row.name}`
1876
2503
  });
1877
2504
  }
1878
2505
  return findings;
@@ -1907,70 +2534,6 @@ function envEntries(file) {
1907
2534
  }];
1908
2535
  });
1909
2536
  }
1910
- const ADVISOR_SEVERITY = {
1911
- ERROR: "error",
1912
- WARN: "warning",
1913
- INFO: "info"
1914
- };
1915
- function lintObject(lint) {
1916
- const { schema, name, type } = lint.metadata ?? {};
1917
- if (typeof schema !== "string" || typeof name !== "string") return void 0;
1918
- if (type === "table" || type === "view") return {
1919
- kind: "table",
1920
- schema,
1921
- name
1922
- };
1923
- if (type === "function") return {
1924
- kind: "function",
1925
- schema,
1926
- name
1927
- };
1928
- }
1929
- function lintFinding(lint) {
1930
- const object = lintObject(lint);
1931
- return {
1932
- severity: ADVISOR_SEVERITY[lint.level] ?? "warning",
1933
- title: lint.title,
1934
- message: `${lint.detail.replaceAll("\\`", "`")} [${lint.name}]`,
1935
- target: object ? `${object.schema}.${object.name}` : lint.cache_key,
1936
- help: lint.remediation,
1937
- ...object ? { object } : {}
1938
- };
1939
- }
1940
- function advisorRule(code, category, title, description, covered = () => false) {
1941
- return {
1942
- code,
1943
- severity: "warning",
1944
- title,
1945
- description,
1946
- async check(context) {
1947
- const { advisors } = context;
1948
- if (!advisors) return [];
1949
- if ("skipped" in advisors) return [{
1950
- severity: "info",
1951
- message: `Skipped the ${category} advisor: ${advisors.skipped}`
1952
- }];
1953
- try {
1954
- return (await advisors.lints(category)).filter((lint) => !covered(lint, context)).map(lintFinding);
1955
- } catch (cause) {
1956
- return [{ message: `The ${category} advisor could not run (${advisors.describe}): ${cause instanceof Error ? cause.message : String(cause)}` }];
1957
- }
1958
- }
1959
- };
1960
- }
1961
- /**
1962
- * splinter lints doctor's own rules report for the same table:
1963
- * `multiple_permissive_policies` (BS207) and `unindexed_foreign_keys` (BS216).
1964
- */
1965
- function coveredByOwnRules(lint, context) {
1966
- const code = lint.name === "multiple_permissive_policies" ? "BS207" : lint.name === "unindexed_foreign_keys" ? "BS216" : void 0;
1967
- if (!code || !context.codes?.includes(code)) return false;
1968
- const object = lintObject(lint);
1969
- if (!object) return false;
1970
- const table = exposed(context).find((candidate) => candidate.schema === object.schema && candidate.name === object.name);
1971
- if (!table) return false;
1972
- return code === "BS207" ? permissiveOverlaps(table).length > 0 : unindexedForeignKeys(table).length > 0;
1973
- }
1974
2537
  /**
1975
2538
  * `db.x.aggregate(`, an `_sum:`/`_avg:`/`_min:`/`_max:` include or a list
1976
2539
  * with `facetCounts: true`.
@@ -1978,11 +2541,12 @@ function coveredByOwnRules(lint, context) {
1978
2541
  const AGGREGATE_USE = /\.aggregate\(|\b_(?:sum|avg|min|max)\s*:\s*\{|\bfacetCounts\s*:\s*true\b/;
1979
2542
  /**
1980
2543
  * A policy that reads memberships or the tenant claim: `is_member(...)`,
1981
- * `current_tenant_id()`, `memberships`, PermDock's `permitted_*_ids()`.
2544
+ * `current_tenant_id()`, `memberships`. The authorization provider's
2545
+ * functions (`authorization.requires`) count too.
1982
2546
  */
1983
- const TENANT_HELPER = /member|tenant|current_org|org_id|permitted_\w+_ids/i;
2547
+ const TENANT_HELPER = /member|tenant|current_org|org_id/i;
1984
2548
  /** A foreign key to the tenant table itself, whose own policies are usually read-only. */
1985
- const TENANT_COLUMN = /^(?:org|organization|tenant|team|workspace|account)_id$/;
2549
+ const TENANT_COLUMN = /^(?:organization|organization|tenant|team|workspace|account)_id$/;
1986
2550
  const COMMANDS = [
1987
2551
  "select",
1988
2552
  "insert",
@@ -2008,20 +2572,34 @@ const RULES = [
2008
2572
  code: "BS106",
2009
2573
  severity: "error",
2010
2574
  title: "Table not granted to the Data API",
2011
- description: "Supabase no longer grants new tables to anon and authenticated. Without a grant, every Data API request fails with 42501 before RLS runs. Tables in `expose` need the privileges listed there; other tables need `select` for authenticated.",
2575
+ description: "Supabase no longer grants new tables to anon and authenticated. Without a grant, every Data API request fails with 42501 before RLS runs. Tables in `expose` need the privileges listed there; other tables need `select` for authenticated. Tables marked `serviceRole` in `tables` must have no grants for either role.",
2012
2576
  check: (context) => {
2013
2577
  const auto = context.configToml ? tomlGet(context.configToml.document, ["api", "auto_expose_new_tables"]) : void 0;
2014
2578
  const note = auto === false || auto === "false" ? " config.toml sets [api] auto_expose_new_tables = false, so new tables start without grants." : "";
2015
2579
  return exposed(context).flatMap((table) => {
2016
- if (context.config.tables[table.name]?.exclude) return [];
2580
+ const options = configFor(context.config.tables, table.schema, table.name);
2581
+ if (options?.exclude) return [];
2582
+ const granted = (role) => new Set(table.grants.filter((grant) => grant.role === role || grant.role === "PUBLIC").flatMap((grant) => grant.privileges.map((privilege) => privilege.toLowerCase())));
2583
+ if (options?.serviceRole) {
2584
+ const fromPublic = table.grants.some((grant) => grant.role === "PUBLIC" && grant.privileges.length > 0);
2585
+ return ["anon", "authenticated"].flatMap((role) => {
2586
+ const have = [...granted(role)];
2587
+ if (have.length === 0) return [];
2588
+ const from = fromPublic ? `${role}, public` : role;
2589
+ return [{
2590
+ message: `${qualified(table)} has \`serviceRole: true\` in \`tables\`, but ${role} has ${have.join(", ")} on it, so the Data API reaches it. Remove it from \`expose\` and run \`revoke all on table ${qualified(table)} from ${from};\`.`,
2591
+ target: `${qualified(table)}:${role}`,
2592
+ object: tableObject(table)
2593
+ }];
2594
+ });
2595
+ }
2017
2596
  const wanted = context.config.expose[qualified(table)] ?? context.config.expose[table.name] ?? {
2018
2597
  anon: [],
2019
2598
  authenticated: ["select"]
2020
2599
  };
2021
- const granted = (role) => new Set(table.grants.filter((grant) => grant.role === role || grant.role === "PUBLIC").flatMap((grant) => grant.privileges.map((privilege) => privilege.toLowerCase())));
2022
2600
  return ["anon", "authenticated"].flatMap((role) => {
2023
2601
  const have = granted(role);
2024
- const missing = wanted[role].filter((privilege) => !have.has(privilege));
2602
+ const missing = wanted[role].filter((privilege) => !privilege.includes("(") && !have.has(privilege));
2025
2603
  if (missing.length === 0) return [];
2026
2604
  return [{
2027
2605
  message: `${role} has no ${missing.join(", ")} on ${qualified(table)}, so the Data API answers 42501. Add it to \`expose\` and run \`better-supabase sql add grants\`, or run \`grant ${missing.join(", ")} on table ${qualified(table)} to ${role};\`.${note}`,
@@ -2039,20 +2617,27 @@ const RULES = [
2039
2617
  description: "A tenant-scoped table with policies for some commands but not all four denies the rest silently: an update or delete then affects 0 rows without an error. Add the missing policies, or a restrictive one, so the intent is explicit, and test it with `expectTenantIsolation`.",
2040
2618
  check: (context) => {
2041
2619
  const column = context.config.plugins.tenant?.column;
2620
+ const providerFunctions = [...new Set((context.config.authorization?.requires ?? []).flatMap((entry) => [entry.function, entry.function.slice(entry.function.indexOf(".") + 1)]))];
2042
2621
  const tables = exposed(context);
2043
2622
  const roots = new Set(tables.flatMap((table) => table.foreignKeys.filter((key) => key.columns.length === 1 && (column === void 0 ? TENANT_COLUMN.test(key.columns[0]) : key.columns[0] === column)).map((key) => `${key.refSchema}.${key.refTable}`)));
2044
2623
  return tables.flatMap((table) => {
2045
2624
  if (table.kind !== "table" || !table.rls) return [];
2046
2625
  if (roots.has(qualified(table))) return [];
2047
- if (context.config.tables[table.name]?.exclude) return [];
2048
- const granting = table.policies.filter((policy) => policy.permissive && policy.roles.some((role) => ["authenticated", "public"].includes(role)));
2626
+ const options = configFor(context.config.tables, table.schema, table.name);
2627
+ if (options?.exclude || options?.serviceRole) return [];
2628
+ const forAuthenticated = (policy) => policy.roles.some((role) => ["authenticated", "public"].includes(role));
2629
+ const granting = table.policies.filter((policy) => policy.permissive && forAuthenticated(policy));
2049
2630
  if (granting.length === 0) return [];
2050
- if (!(column !== void 0 && table.columns.some((entry) => entry.name === column) || granting.some((policy) => TENANT_HELPER.test([
2051
- policy.using ?? "",
2052
- policy.check ?? "",
2053
- ...policy.functions ?? []
2054
- ].join(" "))))) return [];
2055
- const covered = new Set(granting.flatMap((policy) => policy.command === "all" ? COMMANDS : [policy.command]));
2631
+ if (!(column !== void 0 && table.columns.some((entry) => entry.name === column) || granting.some((policy) => {
2632
+ const text = [
2633
+ policy.using ?? "",
2634
+ policy.check ?? "",
2635
+ ...policy.functions ?? []
2636
+ ].join(" ");
2637
+ return TENANT_HELPER.test(text) || providerFunctions.some((fn) => text.includes(fn));
2638
+ }))) return [];
2639
+ const denying = table.policies.filter((policy) => !policy.permissive && forAuthenticated(policy) && /^\s*false\s*$/i.test(policy.using ?? ""));
2640
+ const covered = new Set([...granting, ...denying].flatMap((policy) => policy.command === "all" ? COMMANDS : [policy.command]));
2056
2641
  const missing = COMMANDS.filter((command) => !covered.has(command));
2057
2642
  if (missing.length === 0) return [];
2058
2643
  return [{
@@ -2067,14 +2652,14 @@ const RULES = [
2067
2652
  code: "BS108",
2068
2653
  severity: "error",
2069
2654
  title: "Policy reads auth.mfa_factors directly",
2070
- description: "`authenticated` has no access to `auth.mfa_factors`, so a policy that reads it fails every request with 42501. Check the factor in a `security definer` function such as `better_supabase.mfa_satisfied()` from the `mfa` kit module.",
2655
+ description: "`authenticated` has no access to `auth.mfa_factors`, so a policy that reads it fails every request with 42501. Check the factor in a `security definer` function such as `better_supabase.mfa_satisfied()` from the `mfa` SQL module.",
2071
2656
  check: (context) => exposed(context).flatMap((table) => table.policies.filter((policy) => MFA_FACTORS.test(`${policy.using ?? ""} ${policy.check ?? ""}`)).map((policy) => ({
2072
2657
  message: `Policy "${policy.name}" on ${qualified(table)} reads auth.mfa_factors, which authenticated can't select, so every request fails with 42501. Use \`(select better_supabase.mfa_satisfied())\` from \`better-supabase sql add mfa\` instead.`,
2073
2658
  target: `${qualified(table)}.${policy.name}`,
2074
2659
  object: policyObject(table, policy)
2075
2660
  })))
2076
2661
  },
2077
- advisorRule("BS200", "performance", "Supabase performance advisor", "Findings from the Performance Advisor (splinter): unindexed foreign keys, auth calls re-evaluated per row in policies, multiple permissive policies, unused and duplicate indexes and more.", coveredByOwnRules),
2662
+ advisorRule("BS200", "performance", "Supabase performance advisor", "Findings from the Performance Advisor (splinter): unindexed foreign keys, auth calls re-evaluated per row in policies, multiple permissive policies, unused and duplicate indexes and more."),
2078
2663
  {
2079
2664
  code: "BS204",
2080
2665
  severity: "warning",
@@ -2145,7 +2730,9 @@ const RULES = [
2145
2730
  path: bucket.path,
2146
2731
  ...bucket.public === void 0 ? {} : { public: bucket.public },
2147
2732
  ...bucket.fileSizeLimit === void 0 ? {} : { fileSizeLimit: bucket.fileSizeLimit },
2148
- ...bucket.allowedMimeTypes === void 0 ? {} : { allowedMimeTypes: bucket.allowedMimeTypes }
2733
+ ...bucket.allowedMimeTypes === void 0 ? {} : { allowedMimeTypes: bucket.allowedMimeTypes },
2734
+ ...bucket.versioning === void 0 ? {} : { versioning: bucket.versioning },
2735
+ ...bucket.lifecycle === void 0 ? {} : { lifecycle: bucket.lifecycle }
2149
2736
  });
2150
2737
  const actual = catalogOf(context).buckets.find((entry) => entry.id === id);
2151
2738
  const inDatabase = defined.drift(actual).map((drift) => ({
@@ -2202,13 +2789,13 @@ const RULES = [
2202
2789
  {
2203
2790
  code: "BS304",
2204
2791
  severity: "warning",
2205
- title: "SQL kit files are out of date",
2206
- description: "A module listed in `sql.kit` differs from the version in this release. Same check as `sql sync --check`.",
2792
+ title: "SQL module files are out of date",
2793
+ description: "A module listed in `sql.modules` differs from the version in this release. Same check as `sql sync --check`.",
2207
2794
  check: async (context) => {
2208
2795
  const stale = [];
2209
- for (const { file, current, behind } of await kitFiles(context)) {
2210
- if (behind || sameKitFile(current, file.contents)) continue;
2211
- const edited = current === void 0 ? "" : ` If you edited it, move the change to \`kits.${file.module}\` in better-supabase.config.ts or to the module's SQL hooks: sync overwrites the file.`;
2796
+ for (const { file, current, behind } of await moduleFiles(context)) {
2797
+ if (behind || sameModuleFile(current, file.contents)) continue;
2798
+ const edited = current === void 0 ? "" : ` If you edited it, move the change to \`sql.modules.${file.module}\` in better-supabase.config.ts or to the module's SQL hooks: sync overwrites the file.`;
2212
2799
  const next = file.kind === "data" ? "Run `better-supabase sql sync`, which also writes the rows into a migration, since a schema diff skips them." : `Run \`better-supabase sql sync\`, then \`${migrationCommand(context.configToml)}\`.`;
2213
2800
  stale.push({
2214
2801
  message: `${file.path} (${file.module}) is ${current === void 0 ? "missing" : "out of date"}. ${next}${edited}`,
@@ -2222,6 +2809,26 @@ const RULES = [
2222
2809
  return stale;
2223
2810
  }
2224
2811
  },
2812
+ {
2813
+ code: "BS316",
2814
+ severity: "warning",
2815
+ title: "Legacy migra diff engine",
2816
+ description: "`supabase/config.toml` has no `[experimental.pgdelta] enabled = true`, so the Supabase CLI diffs `supabase/schemas` with migra, which drops grants, comments and security-invoker views and needs the stack stopped. New projects from `supabase init` use pg-delta.",
2817
+ check: (context) => {
2818
+ const toml = context.configToml;
2819
+ if (toml === void 0 || diffEngine(toml) === "pg-delta") return [];
2820
+ const schemas = `${toml.dir}/schemas/`;
2821
+ if (!(context.config.sql.moduleNames.length > 0 || (context.sqlFiles ?? []).some((file) => file.path.startsWith(schemas)))) return [];
2822
+ return [{
2823
+ message: `${toml.path} uses the legacy migra engine. Add \`[experimental.pgdelta]\` with \`enabled = true\`, remove \`[db.migrations] schema_paths\`, and create migrations with \`supabase db schema declarative sync\` instead of \`supabase db diff\`.`,
2824
+ target: toml.path,
2825
+ location: {
2826
+ file: toml.path,
2827
+ line: 1
2828
+ }
2829
+ }];
2830
+ }
2831
+ },
2225
2832
  {
2226
2833
  code: "BS305",
2227
2834
  severity: "warning",
@@ -2261,9 +2868,9 @@ const RULES = [
2261
2868
  {
2262
2869
  code: "BS311",
2263
2870
  severity: "warning",
2264
- title: "SQL kit module behind its current version",
2265
- description: "A module file or `better_supabase.kit_modules` records an older module version than this release ships. Same check as `sql upgrade --check`.",
2266
- check: kitVersionsBehind
2871
+ title: "SQL module behind its current version",
2872
+ description: "A module file or `better_supabase.modules` records an older module version than this release ships. Same check as `sql upgrade --check`.",
2873
+ check: moduleVersionsBehind
2267
2874
  },
2268
2875
  {
2269
2876
  code: "BS401",
@@ -2370,9 +2977,11 @@ const RULES = [
2370
2977
  ...POLICY_RULES,
2371
2978
  ...SCHEMA_DESIGN_RULES,
2372
2979
  ...HOOK_RULES,
2373
- ...PERMDOCK_RULES,
2980
+ ...COOKIE_RULES,
2981
+ ...AUTHORIZATION_RULES,
2374
2982
  ...LIVE_RULES,
2375
- ...KIT_RULES
2983
+ ...MODULE_RULES,
2984
+ ...PGDELTA_RULES
2376
2985
  ].sort((a, b) => byCodePoint(a.code, b.code));
2377
2986
  const RULE_CODES = RULES.map((rule) => rule.code);
2378
2987
  async function runRules(context, rules = RULES) {
@@ -2381,13 +2990,21 @@ async function runRules(context, rules = RULES) {
2381
2990
  ...context,
2382
2991
  codes: rules.map((rule) => rule.code)
2383
2992
  };
2384
- for (const rule of rules) for (const input of await rule.check(scoped)) findings.push({
2385
- code: rule.code,
2386
- severity: rule.severity,
2387
- title: rule.title,
2388
- help: `${DOCS_URL}#${rule.code.toLowerCase()}`,
2389
- ...input
2390
- });
2993
+ for (const rule of rules) {
2994
+ const help = `${DOCS_URL}#${rule.code.toLowerCase()}`;
2995
+ const skipped = skippedForMetadata(context.snapshot, rule, help);
2996
+ if (skipped) {
2997
+ findings.push(skipped);
2998
+ continue;
2999
+ }
3000
+ for (const input of await rule.check(scoped)) findings.push({
3001
+ code: rule.code,
3002
+ severity: rule.severity,
3003
+ title: rule.title,
3004
+ help,
3005
+ ...input
3006
+ });
3007
+ }
2391
3008
  return findings;
2392
3009
  }
2393
3010
  //#endregion
@@ -2529,6 +3146,11 @@ const ARGS = {
2529
3146
  description: "Check a saved snapshot instead of the database",
2530
3147
  valueHint: "file"
2531
3148
  },
3149
+ metadata: {
3150
+ type: "string",
3151
+ description: "Check a GeneratorMetadata document (- for stdin); checks it lacks data for are skipped",
3152
+ valueHint: "path|-"
3153
+ },
2532
3154
  "db-url-stdin": {
2533
3155
  type: "boolean",
2534
3156
  description: "Read the connection string from stdin instead of $DATABASE_URL or the config"
@@ -2565,6 +3187,7 @@ const ARGS = {
2565
3187
  const ENV_FILES = [
2566
3188
  ".env",
2567
3189
  ".env.local",
3190
+ ".env.example",
2568
3191
  ".env.development",
2569
3192
  ".env.development.local",
2570
3193
  ".env.production",
@@ -2584,8 +3207,9 @@ async function sqlFiles(root, toml) {
2584
3207
  text: await readFile(resolve(root, path), "utf8")
2585
3208
  });
2586
3209
  const schemas = await Promise.all((await schemaPaths(root, toml)).files.map(read));
2587
- const migrationsDir = resolve(root, "supabase/migrations");
2588
- const migrations = existsSync(migrationsDir) ? (await readdir(migrationsDir)).filter((name) => name.endsWith(".sql")).sort().reverse().map((name) => `supabase/migrations/${name}`) : [];
3210
+ const migrationsPath = `${toml?.dir ?? "supabase"}/migrations`;
3211
+ const migrationsDir = resolve(root, migrationsPath);
3212
+ const migrations = existsSync(migrationsDir) ? (await readdir(migrationsDir)).filter((name) => name.endsWith(".sql")).sort().reverse().map((name) => `${migrationsPath}/${name}`) : [];
2589
3213
  return [...schemas, ...await Promise.all(migrations.map(read))];
2590
3214
  }
2591
3215
  /** Files matching `doctor.sources`, skipping dependencies and build output. */
@@ -2605,6 +3229,17 @@ const escape = (name) => name.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
2605
3229
  function locate(files, object) {
2606
3230
  const name = `"?${escape(object.name)}"?`;
2607
3231
  const qualifiedName = `(?:"?${escape(object.schema)}"?\\.)?${name}`;
3232
+ if (object.kind === "policy" && object.table !== void 0) {
3233
+ const onTable = new RegExp(`create\\s+policy\\s+${name}\\s+on\\s+(?:only\\s+)?(?:"?${escape(object.schema)}"?\\.)?"?${escape(object.table)}"?(?:\\s|;|$)`, "i");
3234
+ for (const file of files) {
3235
+ const match = onTable.exec(file.text);
3236
+ if (match) return {
3237
+ file: file.path,
3238
+ line: file.text.slice(0, match.index).split("\n").length
3239
+ };
3240
+ }
3241
+ return;
3242
+ }
2608
3243
  const pattern = object.kind === "table" ? new RegExp(`create\\s+(?:unlogged\\s+)?table\\s+(?:if\\s+not\\s+exists\\s+)?${qualifiedName}(?:\\s|\\(|$)`, "i") : object.kind === "function" ? new RegExp(`create\\s+(?:or\\s+replace\\s+)?function\\s+${qualifiedName}\\s*\\(`, "i") : new RegExp(`create\\s+policy\\s+${name}`, "i");
2609
3244
  for (const file of files) {
2610
3245
  const line = lineOf(file.text, pattern);
@@ -2676,7 +3311,10 @@ function openLive(config, env, source, pg) {
2676
3311
  advisors: {
2677
3312
  describe: "database (splinter)",
2678
3313
  async lints(category) {
2679
- splinter ??= open().then((db) => splinterAdvisors(db.queryable, db.describe, { cacheDir }));
3314
+ splinter ??= Promise.all([open(), readSupabaseToml(config.root).catch(() => void 0)]).then(([db, toml]) => splinterAdvisors(db.queryable, db.describe, {
3315
+ cacheDir,
3316
+ schemas: apiSchemas(toml?.document)
3317
+ }));
2680
3318
  return (await splinter).lints(category);
2681
3319
  }
2682
3320
  },
@@ -2755,10 +3393,9 @@ async function runDoctor(config, args, env, options = {}) {
2755
3393
  ...options.advisors === void 0 ? {} : { advisors: options.advisors },
2756
3394
  ...options.database === void 0 ? {} : { database: options.database }
2757
3395
  };
2758
- const [snapshot, envFiles, permdock, [configToml, sql], gitignore, sources, readSets] = await Promise.all([
3396
+ const [snapshot, envFiles, [configToml, sql], gitignore, sources, readSets] = await Promise.all([
2759
3397
  options.snapshot ?? loadSnapshot(config, env, source, pg, opened.open),
2760
3398
  Promise.all(ENV_FILES.map((path) => readText(config.root, path))).then((files) => files.filter((file) => file !== void 0)),
2761
- readPermdock(config.root, config.permdock),
2762
3399
  readSupabaseToml(config.root).then(async (toml) => [toml, await sqlFiles(config.root, toml)]),
2763
3400
  readText(config.root, ".gitignore"),
2764
3401
  sourceFiles(config),
@@ -2770,7 +3407,6 @@ async function runDoctor(config, args, env, options = {}) {
2770
3407
  const context = {
2771
3408
  config,
2772
3409
  snapshot,
2773
- ...permdock ? { permdock } : {},
2774
3410
  sqlFiles: sql,
2775
3411
  configToml,
2776
3412
  envFiles,
@@ -2808,11 +3444,20 @@ async function runDoctor(config, args, env, options = {}) {
2808
3444
  } : finding;
2809
3445
  });
2810
3446
  const out = args.out;
2811
- const report = formatReport(findings, {
3447
+ const fromRepo = format === "sarif" || format === "github";
3448
+ const repoRoot = findRepoRoot(config.root);
3449
+ const repoPath = (file) => display(repoRoot, resolve(config.root, file));
3450
+ const report = formatReport(fromRepo ? findings.map((finding) => finding.location ? {
3451
+ ...finding,
3452
+ location: {
3453
+ ...finding.location,
3454
+ file: repoPath(finding.location.file)
3455
+ }
3456
+ } : finding) : findings, {
2812
3457
  format,
2813
3458
  rules,
2814
3459
  version: VERSION,
2815
- fallbackFile: context.configToml?.path ?? "package.json",
3460
+ fallbackFile: repoPath(context.configToml?.path ?? "package.json"),
2816
3461
  ...options.paint && !out ? { paint: options.paint } : {}
2817
3462
  });
2818
3463
  const errors = findings.filter((finding) => finding.severity === "error").length;
@@ -2850,7 +3495,38 @@ const doctorCommand = defineCliCommand({
2850
3495
  "ignore",
2851
3496
  "explain"
2852
3497
  ],
2853
- run: async (args, { config, env, io, signal }) => {
3498
+ run: async (args, { config, cwd, env, io, signal }) => {
3499
+ if (args.metadata !== void 0) {
3500
+ const conflicting = [
3501
+ ...args.snapshot === void 0 ? [] : ["--snapshot"],
3502
+ ...args["db-url-stdin"] === true ? ["--db-url-stdin"] : [],
3503
+ ...args["project-ref"] === void 0 ? [] : ["--project-ref"],
3504
+ ...args.stats === true ? ["--stats"] : [],
3505
+ ...args.explain === void 0 ? [] : ["--explain"],
3506
+ ...args.as === void 0 ? [] : ["--as"]
3507
+ ];
3508
+ if (conflicting.length > 0) return {
3509
+ code: 2,
3510
+ error: `--metadata checks the document, not a database, so it can't be combined with ${conflicting.join(", ")}`
3511
+ };
3512
+ let snapshot;
3513
+ try {
3514
+ snapshot = await readMetadataSnapshot(args.metadata, cwd, io);
3515
+ } catch (cause) {
3516
+ if (cause instanceof MetadataRejectedError) return {
3517
+ code: 65,
3518
+ error: cause.message
3519
+ };
3520
+ throw cause;
3521
+ }
3522
+ const skipped = `reading a GeneratorMetadata document; set $DATABASE_URL, or pass --db-url-stdin or --project-ref, to check a database.`;
3523
+ return runDoctor(config, args, env, {
3524
+ snapshot,
3525
+ advisors: { skipped },
3526
+ database: { skipped },
3527
+ paint: painter(io.color)
3528
+ });
3529
+ }
2854
3530
  const dbUrl = await stdinDatabaseUrl(args["db-url-stdin"], io);
2855
3531
  return withSpinner(args.snapshot === void 0 ? io.prompts : void 0, "Checking the database", () => runDoctor(config, args, env, {
2856
3532
  paint: painter(io.color),