better-supabase 0.13.0 → 0.15.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 (1581) hide show
  1. package/README.md +67 -345
  2. package/dist/access-sql-B-MkVARa.js +207 -0
  3. package/dist/actor-go7p9-CJ.js +150 -0
  4. package/dist/adapter-B0b6MXYS.js +2299 -0
  5. package/dist/agents-5n8DWRfP.d.ts +117 -0
  6. package/dist/ai-cache-Cu8j6aQQ.js +87 -0
  7. package/dist/ai-cache-DyHohbFP.d.ts +82 -0
  8. package/dist/ai-chat-D1GuRnke.d.ts +619 -0
  9. package/dist/ai-files-CgFCRsnU.js +339 -0
  10. package/dist/ai-schedules-CAquecT_.d.ts +166 -0
  11. package/dist/ai-sdk/agents/index.d.ts +6 -6
  12. package/dist/ai-sdk/agents/index.js +4 -4
  13. package/dist/ai-sdk/batches/index.d.ts +5 -5
  14. package/dist/ai-sdk/batches/index.js +4 -4
  15. package/dist/ai-sdk/cache/index.d.ts +3 -3
  16. package/dist/ai-sdk/cache/index.js +2 -2
  17. package/dist/ai-sdk/chat/index.d.ts +1 -1
  18. package/dist/ai-sdk/chat/index.js +5 -5
  19. package/dist/ai-sdk/embeddings/index.d.ts +2 -2
  20. package/dist/ai-sdk/embeddings/index.js +1 -1
  21. package/dist/ai-sdk/files/index.d.ts +2 -2
  22. package/dist/ai-sdk/files/index.js +5 -5
  23. package/dist/ai-sdk/index.d.ts +30 -9
  24. package/dist/ai-sdk/index.js +42 -4
  25. package/dist/ai-sdk/mcp/index.d.ts +4 -4
  26. package/dist/ai-sdk/mcp/index.js +7 -7
  27. package/dist/ai-sdk/memory/index.d.ts +3 -3
  28. package/dist/ai-sdk/memory/index.js +2 -2
  29. package/dist/ai-sdk/workflow/index.d.ts +4 -4
  30. package/dist/ai-sdk/workflow/index.js +7 -7
  31. package/dist/anchors-7F34QyHa.d.ts +40 -0
  32. package/dist/api-DxAPqMrE.d.ts +495 -0
  33. package/dist/api-keys-D5k63K6y.d.ts +213 -0
  34. package/dist/app-tables-CF2DD-Mu.d.ts +28 -0
  35. package/dist/app-tables-D29Rahtg.js +16350 -0
  36. package/dist/approvals-Crutd4-o.d.ts +129 -0
  37. package/dist/assistant-CD_h8Vxx.d.ts +157 -0
  38. package/dist/astro/index.d.ts +19 -16
  39. package/dist/astro/index.js +8 -8
  40. package/dist/attachments-aJYgdYtB.js +369 -0
  41. package/dist/attributes-Bzf51_nc.js +47 -0
  42. package/dist/auth-module-BEFESDC4.js +44 -0
  43. package/dist/auth-reply-tlXB2FQ9.js +31 -0
  44. package/dist/authorization-HZdJCaVm.d.ts +3595 -0
  45. package/dist/authorization-hooks-Cs1vP_ej.d.ts +217 -0
  46. package/dist/authorizer-B6X09otb.d.ts +118 -0
  47. package/dist/authorizer-BGhXaDmo.js +172 -0
  48. package/dist/automation-D41DXyvT.js +75 -0
  49. package/dist/automations-BYsz43AG.js +314 -0
  50. package/dist/automations-Bc2ue55V.js +26 -0
  51. package/dist/automations-CD1XlL2W.js +33 -0
  52. package/dist/automations-CIwRq7zm.js +30 -0
  53. package/dist/availability-vguUvEK2.d.ts +226 -0
  54. package/dist/better-result/index.d.ts +20 -30
  55. package/dist/better-result/index.js +2 -2
  56. package/dist/bind-CrMBwrPN.d.ts +104 -0
  57. package/dist/block-authors-BDV3y1SP.js +14 -0
  58. package/dist/block-authors-DzXLUiIq.d.ts +12 -0
  59. package/dist/block-env-B_uEM2xz.d.ts +32 -0
  60. package/dist/block-event-listener-Bu6G0TFH.js +16 -0
  61. package/dist/block-events-pBXPzwnW.js +83 -0
  62. package/dist/block-fields-BE7pInWZ.d.ts +58 -0
  63. package/dist/block-fields-DE_FgpOU.js +139 -0
  64. package/dist/block-helpers-CRpAHrp_.d.ts +62 -0
  65. package/dist/block-helpers-CtBdjgO4.js +353 -0
  66. package/dist/block-hooks-BfxX3682.js +130 -0
  67. package/dist/block-hooks-DZN2w69m.d.ts +117 -0
  68. package/dist/block-method-C9hWdHom.js +21 -0
  69. package/dist/block-method-CiVa2G-J.d.ts +14 -0
  70. package/dist/block-subjects-BhDKK_j7.js +55 -0
  71. package/dist/block-subjects-CphkQPxp.d.ts +6 -0
  72. package/dist/block-tenant-DHp7subc.js +14 -0
  73. package/dist/block-transport-DWZSJrwN.d.ts +69 -0
  74. package/dist/blocks/access-requests/index.d.ts +8 -8
  75. package/dist/blocks/access-requests/index.js +6 -7
  76. package/dist/blocks/activities/index.d.ts +215 -0
  77. package/dist/blocks/activities/index.js +414 -0
  78. package/dist/blocks/agents/index.d.ts +4 -4
  79. package/dist/blocks/agents/index.js +5 -6
  80. package/dist/blocks/ai-cache/index.d.ts +3 -3
  81. package/dist/blocks/ai-cache/index.js +3 -3
  82. package/dist/blocks/ai-chat/index.d.ts +119 -6
  83. package/dist/blocks/ai-chat/index.js +1067 -5
  84. package/dist/blocks/ai-files/index.d.ts +5 -5
  85. package/dist/blocks/ai-files/index.js +3 -3
  86. package/dist/blocks/ai-keys/index.d.ts +22 -0
  87. package/dist/blocks/ai-keys/index.js +236 -0
  88. package/dist/blocks/ai-schedules/index.d.ts +10 -0
  89. package/dist/blocks/ai-schedules/index.js +258 -0
  90. package/dist/blocks/announcements/index.d.ts +134 -5
  91. package/dist/blocks/announcements/index.js +179 -4
  92. package/dist/blocks/api-keys/index.d.ts +69 -8
  93. package/dist/blocks/api-keys/index.js +417 -7
  94. package/dist/blocks/approvals/index.d.ts +4 -4
  95. package/dist/blocks/approvals/index.js +25 -24
  96. package/dist/blocks/assets/index.d.ts +281 -0
  97. package/dist/blocks/assets/index.js +288 -0
  98. package/dist/blocks/assignment/index.d.ts +124 -0
  99. package/dist/blocks/assignment/index.js +141 -0
  100. package/dist/blocks/attachments/index.d.ts +130 -6
  101. package/dist/blocks/attachments/index.js +328 -7
  102. package/dist/blocks/audit/index.d.ts +476 -7
  103. package/dist/blocks/audit/index.js +863 -7
  104. package/dist/blocks/automations/index.d.ts +524 -0
  105. package/dist/blocks/automations/index.js +553 -0
  106. package/dist/blocks/billable-time/index.d.ts +201 -0
  107. package/dist/blocks/billable-time/index.js +260 -0
  108. package/dist/blocks/billing/index.d.ts +146 -16
  109. package/dist/blocks/billing/index.js +231 -41
  110. package/dist/blocks/bookings/index.d.ts +54 -5
  111. package/dist/blocks/bookings/index.js +423 -6
  112. package/dist/blocks/bulk/index.d.ts +6 -6
  113. package/dist/blocks/bulk/index.js +4 -4
  114. package/dist/blocks/calendar/index.d.ts +186 -5
  115. package/dist/blocks/calendar/index.js +1180 -4
  116. package/dist/blocks/catalog/index.d.ts +134 -28
  117. package/dist/blocks/catalog/index.js +183 -54
  118. package/dist/blocks/channels/index.d.ts +113 -0
  119. package/dist/blocks/channels/index.js +189 -0
  120. package/dist/blocks/collections/index.d.ts +90 -17
  121. package/dist/blocks/collections/index.js +85 -10
  122. package/dist/blocks/comments/index.d.ts +263 -45
  123. package/dist/blocks/comments/index.js +360 -16
  124. package/dist/blocks/compliance/index.d.ts +325 -0
  125. package/dist/blocks/compliance/index.js +410 -0
  126. package/dist/blocks/connectors/index.d.ts +4 -4
  127. package/dist/blocks/connectors/index.js +8 -9
  128. package/dist/blocks/contacts/index.d.ts +182 -0
  129. package/dist/blocks/contacts/index.js +358 -0
  130. package/dist/blocks/content/index.d.ts +188 -0
  131. package/dist/blocks/content/index.js +219 -0
  132. package/dist/blocks/contracts/index.d.ts +239 -0
  133. package/dist/blocks/contracts/index.js +361 -0
  134. package/dist/blocks/courses/index.d.ts +341 -0
  135. package/dist/blocks/courses/index.js +445 -0
  136. package/dist/blocks/created-by/index.d.ts +27 -0
  137. package/dist/blocks/created-by/index.js +14 -0
  138. package/dist/blocks/custom-fields/index.d.ts +44 -13
  139. package/dist/blocks/custom-fields/index.js +25 -20
  140. package/dist/blocks/data-lifecycle/index.d.ts +9 -9
  141. package/dist/blocks/data-lifecycle/index.js +7 -8
  142. package/dist/blocks/deals/index.d.ts +20 -40
  143. package/dist/blocks/deals/index.js +156 -66
  144. package/dist/blocks/dispatch/index.d.ts +325 -0
  145. package/dist/blocks/dispatch/index.js +406 -0
  146. package/dist/blocks/documents/index.d.ts +8 -5
  147. package/dist/blocks/documents/index.js +66 -7
  148. package/dist/blocks/domains/index.d.ts +7 -7
  149. package/dist/blocks/domains/index.js +6 -7
  150. package/dist/blocks/donations/index.d.ts +350 -0
  151. package/dist/blocks/donations/index.js +610 -0
  152. package/dist/blocks/drive/index.d.ts +56 -11
  153. package/dist/blocks/drive/index.js +62 -11
  154. package/dist/blocks/employees/index.d.ts +41 -23
  155. package/dist/blocks/employees/index.js +125 -33
  156. package/dist/blocks/enrichment/index.d.ts +176 -0
  157. package/dist/blocks/enrichment/index.js +251 -0
  158. package/dist/blocks/enrollments/index.d.ts +182 -0
  159. package/dist/blocks/enrollments/index.js +173 -0
  160. package/dist/blocks/entitlements/index.d.ts +48 -8
  161. package/dist/blocks/entitlements/index.js +62 -4
  162. package/dist/blocks/expenses/index.d.ts +214 -0
  163. package/dist/blocks/expenses/index.js +279 -0
  164. package/dist/blocks/field-history/index.d.ts +63 -0
  165. package/dist/blocks/field-history/index.js +46 -0
  166. package/dist/blocks/flags/index.d.ts +17 -13
  167. package/dist/blocks/flags/index.js +7 -8
  168. package/dist/blocks/forecasts/index.d.ts +119 -0
  169. package/dist/blocks/forecasts/index.js +129 -0
  170. package/dist/blocks/forms/index.d.ts +208 -140
  171. package/dist/blocks/forms/index.js +488 -149
  172. package/dist/blocks/goals/index.d.ts +82 -0
  173. package/dist/blocks/goals/index.js +72 -0
  174. package/dist/blocks/guests/index.d.ts +198 -0
  175. package/dist/blocks/guests/index.js +224 -0
  176. package/dist/blocks/hiring/index.d.ts +20 -10
  177. package/dist/blocks/hiring/index.js +10 -10
  178. package/dist/blocks/identities/index.d.ts +7 -7
  179. package/dist/blocks/identities/index.js +4 -5
  180. package/dist/blocks/imports/index.d.ts +8 -8
  181. package/dist/blocks/imports/index.js +6 -6
  182. package/dist/blocks/inbox/index.d.ts +138 -6
  183. package/dist/blocks/inbox/index.js +383 -85
  184. package/dist/blocks/incidents/index.d.ts +337 -0
  185. package/dist/blocks/incidents/index.js +477 -0
  186. package/dist/blocks/index.d.ts +145 -11
  187. package/dist/blocks/index.js +184 -187
  188. package/dist/blocks/inventory/index.d.ts +10 -10
  189. package/dist/blocks/inventory/index.js +6 -7
  190. package/dist/blocks/invoices/index.d.ts +11 -11
  191. package/dist/blocks/invoices/index.js +8 -9
  192. package/dist/blocks/knowledge/index.d.ts +3 -3
  193. package/dist/blocks/knowledge/index.js +4 -4
  194. package/dist/blocks/lead-scoring/index.d.ts +50 -0
  195. package/dist/blocks/lead-scoring/index.js +87 -0
  196. package/dist/blocks/leases/index.d.ts +221 -0
  197. package/dist/blocks/leases/index.js +244 -0
  198. package/dist/blocks/leave/index.d.ts +8 -8
  199. package/dist/blocks/leave/index.js +5 -6
  200. package/dist/blocks/ledger/index.d.ts +279 -0
  201. package/dist/blocks/ledger/index.js +229 -0
  202. package/dist/blocks/mail-sync/index.d.ts +37 -0
  203. package/dist/blocks/mail-sync/index.js +906 -0
  204. package/dist/blocks/memberships/index.d.ts +67 -0
  205. package/dist/blocks/memberships/index.js +14 -0
  206. package/dist/blocks/memory/index.d.ts +4 -4
  207. package/dist/blocks/memory/index.js +8 -9
  208. package/dist/blocks/messaging/index.d.ts +186 -0
  209. package/dist/blocks/messaging/index.js +619 -0
  210. package/dist/blocks/metrics/index.d.ts +7 -7
  211. package/dist/blocks/metrics/index.js +6 -7
  212. package/dist/blocks/mfa/index.d.ts +145 -3
  213. package/dist/blocks/mfa/index.js +211 -2
  214. package/dist/blocks/moderation/index.d.ts +203 -0
  215. package/dist/blocks/moderation/index.js +199 -0
  216. package/dist/blocks/notifications/index.d.ts +67 -15
  217. package/dist/blocks/notifications/index.js +64 -16
  218. package/dist/blocks/numbering/index.d.ts +45 -0
  219. package/dist/blocks/numbering/index.js +49 -0
  220. package/dist/blocks/oauth-server/index.d.ts +6 -6
  221. package/dist/blocks/oauth-server/index.js +6 -7
  222. package/dist/blocks/onboarding/index.d.ts +112 -4
  223. package/dist/blocks/onboarding/index.js +107 -4
  224. package/dist/blocks/orders/index.d.ts +152 -13
  225. package/dist/blocks/orders/index.js +188 -18
  226. package/dist/blocks/organizations/index.d.ts +452 -6
  227. package/dist/blocks/organizations/index.js +689 -5
  228. package/dist/blocks/outbox/index.d.ts +4 -4
  229. package/dist/blocks/outbox/index.js +20 -8
  230. package/dist/blocks/passes/index.d.ts +306 -0
  231. package/dist/blocks/passes/index.js +411 -0
  232. package/dist/blocks/permissions/index.d.ts +92 -0
  233. package/dist/blocks/permissions/index.js +127 -0
  234. package/dist/blocks/pipelines/index.d.ts +15 -15
  235. package/dist/blocks/pipelines/index.js +12 -13
  236. package/dist/blocks/posts/index.d.ts +190 -0
  237. package/dist/blocks/posts/index.js +230 -0
  238. package/dist/blocks/profiles/index.d.ts +7 -7
  239. package/dist/blocks/profiles/index.js +5 -6
  240. package/dist/blocks/promotions/index.d.ts +9 -9
  241. package/dist/blocks/promotions/index.js +7 -8
  242. package/dist/blocks/push/index.d.ts +9 -9
  243. package/dist/blocks/push/index.js +4 -5
  244. package/dist/blocks/queue/index.d.ts +562 -0
  245. package/dist/blocks/queue/index.js +1407 -0
  246. package/dist/blocks/quotes/index.d.ts +29 -14
  247. package/dist/blocks/quotes/index.js +14 -14
  248. package/dist/blocks/rank/index.d.ts +7 -7
  249. package/dist/blocks/rank/index.js +4 -4
  250. package/dist/blocks/reactions/index.d.ts +154 -5
  251. package/dist/blocks/reactions/index.js +168 -4
  252. package/dist/blocks/relations/index.d.ts +59 -42
  253. package/dist/blocks/relations/index.js +117 -59
  254. package/dist/blocks/reminders/index.d.ts +21 -17
  255. package/dist/blocks/reminders/index.js +26 -27
  256. package/dist/blocks/reviews/index.d.ts +8 -8
  257. package/dist/blocks/reviews/index.js +6 -7
  258. package/dist/blocks/saved-views/index.d.ts +311 -4
  259. package/dist/blocks/saved-views/index.js +437 -2
  260. package/dist/blocks/scores/index.d.ts +48 -0
  261. package/dist/blocks/scores/index.js +40 -0
  262. package/dist/blocks/search/index.d.ts +168 -5
  263. package/dist/blocks/search/index.js +277 -6
  264. package/dist/blocks/security-events/index.d.ts +440 -0
  265. package/dist/blocks/security-events/index.js +885 -0
  266. package/dist/blocks/segments/index.d.ts +130 -0
  267. package/dist/blocks/segments/index.js +135 -0
  268. package/dist/blocks/sequences/index.d.ts +259 -0
  269. package/dist/blocks/sequences/index.js +376 -0
  270. package/dist/blocks/service-accounts/index.d.ts +132 -0
  271. package/dist/blocks/service-accounts/index.js +109 -0
  272. package/dist/blocks/sessions/index.d.ts +138 -3
  273. package/dist/blocks/sessions/index.js +173 -2
  274. package/dist/blocks/settings/index.d.ts +181 -5
  275. package/dist/blocks/settings/index.js +226 -4
  276. package/dist/blocks/share-links/index.d.ts +46 -19
  277. package/dist/blocks/share-links/index.js +74 -23
  278. package/dist/blocks/shipments/index.d.ts +236 -0
  279. package/dist/blocks/shipments/index.js +548 -0
  280. package/dist/blocks/signatures/index.d.ts +151 -0
  281. package/dist/blocks/signatures/index.js +149 -0
  282. package/dist/blocks/signing-keys/index.d.ts +4 -4
  283. package/dist/blocks/signing-keys/index.js +4 -165
  284. package/dist/blocks/sla/index.d.ts +37 -27
  285. package/dist/blocks/sla/index.js +48 -13
  286. package/dist/blocks/sso/index.d.ts +7 -7
  287. package/dist/blocks/sso/index.js +4 -5
  288. package/dist/blocks/state-machine/index.d.ts +14 -14
  289. package/dist/blocks/state-machine/index.js +26 -26
  290. package/dist/blocks/subjects/index.d.ts +45 -0
  291. package/dist/blocks/subjects/index.js +203 -0
  292. package/dist/blocks/tags/index.d.ts +170 -5
  293. package/dist/blocks/tags/index.js +265 -5
  294. package/dist/blocks/tasks/index.d.ts +346 -0
  295. package/dist/blocks/tasks/index.js +626 -0
  296. package/dist/blocks/teams/index.d.ts +7 -7
  297. package/dist/blocks/teams/index.js +4 -4
  298. package/dist/blocks/templates/index.d.ts +18 -18
  299. package/dist/blocks/templates/index.js +11 -12
  300. package/dist/blocks/time-tracking/index.d.ts +40 -32
  301. package/dist/blocks/time-tracking/index.js +40 -40
  302. package/dist/blocks/timeline/index.d.ts +116 -0
  303. package/dist/blocks/timeline/index.js +164 -0
  304. package/dist/blocks/trash/index.d.ts +47 -9
  305. package/dist/blocks/trash/index.js +9 -8
  306. package/dist/blocks/undo/index.d.ts +48 -5
  307. package/dist/blocks/undo/index.js +60 -4
  308. package/dist/blocks/updated-at/index.d.ts +20 -0
  309. package/dist/blocks/updated-at/index.js +14 -0
  310. package/dist/blocks/usage/index.d.ts +40 -6
  311. package/dist/blocks/usage/index.js +301 -4
  312. package/dist/blocks/vercel-marketplace/index.d.ts +7 -7
  313. package/dist/blocks/vercel-marketplace/index.js +8 -9
  314. package/dist/blocks/versions/index.d.ts +38 -19
  315. package/dist/blocks/versions/index.js +66 -19
  316. package/dist/blocks/waitlist/index.d.ts +9 -9
  317. package/dist/blocks/waitlist/index.js +5 -6
  318. package/dist/blocks/webhooks/index.d.ts +21 -21
  319. package/dist/blocks/webhooks/index.js +18 -19
  320. package/dist/blocks/workflow-editor/index.d.ts +99 -0
  321. package/dist/blocks/workflow-editor/index.js +733 -0
  322. package/dist/blocks/workflows/index.d.ts +72 -6
  323. package/dist/blocks/workflows/index.js +379 -4
  324. package/dist/bookings-D4dvGXiR.d.ts +290 -0
  325. package/dist/bucket-CiNi8sIw.d.ts +666 -0
  326. package/dist/build-BFC8nFiL.js +797 -0
  327. package/dist/build-QitPVzzO.js +2073 -0
  328. package/dist/carrier-KzeYXVrW.d.ts +169 -0
  329. package/dist/chat-sdk/index.d.ts +43 -11
  330. package/dist/chat-sdk/index.js +51 -24
  331. package/dist/claims-DTy6R6my.js +63 -0
  332. package/dist/cli/add-BgsUjCan.js +1252 -0
  333. package/dist/cli/add-DCI5JKjA.js +99 -0
  334. package/dist/cli/add-DWjlVwvP.js +1 -0
  335. package/dist/cli/args-UEwg0l-N.js +27 -0
  336. package/dist/cli/backfill-nxPbcsns.js +293 -0
  337. package/dist/cli/bin.js +1 -1
  338. package/dist/cli/blocks-BZHI8mRC.js +156 -0
  339. package/dist/cli/blocks-Cr4HGePW.js +1 -0
  340. package/dist/cli/cache-CXgR_Ahi.js +607 -0
  341. package/dist/cli/codemod-DSN7iAdg.js +2343 -0
  342. package/dist/cli/codemod-block-renames-BuSaTE_G.js +239 -0
  343. package/dist/cli/command-CQ61x6gN.js +941 -0
  344. package/dist/cli/completion-2Y3D_eNu.js +211 -0
  345. package/dist/cli/completion-BwI0_Ap5.js +30 -0
  346. package/dist/cli/completion-Dlu5AdUW.js +1 -0
  347. package/dist/cli/config-9t5ec_N7.js +1642 -0
  348. package/dist/cli/config-CBqkQjjN.js +1 -0
  349. package/dist/cli/config-CPux9Wa7.js +47 -0
  350. package/dist/cli/config-edit-CrWfk8s3.js +621 -0
  351. package/dist/cli/config-group-C8tjut39.js +391 -0
  352. package/dist/cli/content-mapper-DNs7lTu7.js +182 -0
  353. package/dist/cli/db-DrBI_HNv.js +223 -0
  354. package/dist/cli/doctor-DDrEM6S1.js +4561 -0
  355. package/dist/cli/env-BBl5L8Bf.js +143 -0
  356. package/dist/cli/env-CAU1nknN.js +1 -0
  357. package/dist/cli/files-CX71yBm8.js +1010 -0
  358. package/dist/cli/gen-MtijJQ9r.js +387 -0
  359. package/dist/cli/in-stack-diff-DQTW53Vf.js +257 -0
  360. package/dist/cli/index.d.ts +51 -5
  361. package/dist/cli/index.js +8 -8
  362. package/dist/cli/init-D-aDv9r5.js +440 -0
  363. package/dist/cli/keys-DGEpMbeB.js +106 -0
  364. package/dist/cli/live-iEeH15ck.js +341 -0
  365. package/dist/cli/mcp-BAPyaQ62.js +249 -0
  366. package/dist/cli/metadata-DVx9Zgbj.js +150 -0
  367. package/dist/cli/migration-safety-D7PhzFxm.js +928 -0
  368. package/dist/cli/module-migration-aPCckev6.js +734 -0
  369. package/dist/cli/moved-CsfDQcyk.js +254 -0
  370. package/dist/cli/orphans-BAboqyVM.js +28 -0
  371. package/dist/cli/parse-B9Iv_-Pf.js +182 -0
  372. package/dist/cli/preset-BwmIfFnh.js +101 -0
  373. package/dist/cli/print-BeWO7OJY.js +67 -0
  374. package/dist/cli/remove-B46Dmd2M.js +238 -0
  375. package/dist/cli/render-ucJmUyuM.js +2170 -0
  376. package/dist/cli/run-BXx6D1aR.js +63 -0
  377. package/dist/cli/run-CvYVAztY.js +1427 -0
  378. package/dist/cli/run-DYhLBcFW.js +702 -0
  379. package/dist/cli/seed-Cz8PcXeT.js +188 -0
  380. package/dist/cli/seed-D6XijwbR.js +1 -0
  381. package/dist/cli/shared-LZroULBe.js +151 -0
  382. package/dist/cli/skills-toUan_OV.js +121 -0
  383. package/dist/cli/snapshot-C8MVPyO8.js +729 -0
  384. package/dist/cli/source-DANaJJVc.js +320 -0
  385. package/dist/cli/source-args-IpJRJO5t.js +59 -0
  386. package/dist/cli/spec-DGVNMSSB.js +7530 -0
  387. package/dist/cli/spec-g8_G5K7o.js +1 -0
  388. package/dist/cli/stack-CWEHdO0b.js +135 -0
  389. package/dist/cli/sync-btj01bcM.js +319 -0
  390. package/dist/cli/upgrade-B596d4t8.js +403 -0
  391. package/dist/cli/upgrade-Dw3TMQzn.js +41 -0
  392. package/dist/cli/version-Du4CgONY.js +4 -0
  393. package/dist/client/index.d.ts +8 -7
  394. package/dist/client/index.js +11 -7
  395. package/dist/client/native/index.d.ts +6 -6
  396. package/dist/client/native/index.js +3 -3
  397. package/dist/client-BAEbEA7t.js +287 -0
  398. package/dist/cloud-event-CzZTJkXv.d.ts +64 -0
  399. package/dist/codecs-CMWlhHZ0.d.ts +222 -0
  400. package/dist/codecs-TylXqU_I.js +358 -0
  401. package/dist/commerce/index.d.ts +4 -3
  402. package/dist/commerce/index.js +6 -6
  403. package/dist/compiler-D3UmE7ao.d.ts +14 -0
  404. package/dist/compose-DKW0Tr3e.js +53 -0
  405. package/dist/composite-BARuBEDE.js +130 -0
  406. package/dist/composite-Dsxj7T5K.d.ts +82 -0
  407. package/dist/config/index.d.ts +6 -7
  408. package/dist/config/index.js +6 -6
  409. package/dist/config-CcehOvbz.js +1698 -0
  410. package/dist/config-DWGgUQCh.js +1108 -0
  411. package/dist/connectors-Fud4amxp.d.ts +147 -0
  412. package/dist/context-CEuTKrkz.d.ts +4129 -0
  413. package/dist/context-DBicfUNz.js +41 -0
  414. package/dist/controller-BNZKd6bJ.js +73 -0
  415. package/dist/controller-BrP0gn47.d.ts +52 -0
  416. package/dist/conversion-BAbucuXq.d.ts +86 -0
  417. package/dist/conversion-Dq1S7xzE.js +77 -0
  418. package/dist/create/index.d.ts +34 -23
  419. package/dist/create/index.js +35 -27
  420. package/dist/create/lite/index.d.ts +2 -2
  421. package/dist/create/supabase/index.d.ts +2 -2
  422. package/dist/create/supabase/index.js +2 -2
  423. package/dist/create-blocks-Bu4H5TXw.js +352 -0
  424. package/dist/credentials/index.d.ts +5 -5
  425. package/dist/credentials/index.js +6 -7
  426. package/dist/cron-CgHTKwYG.js +148 -0
  427. package/dist/cron-route-UU9i5i9J.js +24 -0
  428. package/dist/data-engine-BWoz9lED.d.ts +273 -0
  429. package/dist/data-engine-D1BkmBoR.js +173 -0
  430. package/dist/data-hooks-tgF1Ct0F.js +280 -0
  431. package/dist/db-blocks-DJGbjIPz.js +62 -0
  432. package/dist/db-exception-Lngc1W49.js +31 -0
  433. package/dist/deal-forecast-Bv52QMyX.js +39 -0
  434. package/dist/deal-forecast-LwO-Z4_C.d.ts +23 -0
  435. package/dist/define-DFBnQyYg.d.ts +1255 -0
  436. package/dist/define-DlUeKHce.js +2011 -0
  437. package/dist/define-block-BGqOO8B3.js +64 -0
  438. package/dist/define-block-BVcLQ3DF.d.ts +1657 -0
  439. package/dist/delegation--93jRsVO.js +146 -0
  440. package/dist/documents-Cup1rpjF.d.ts +152 -0
  441. package/dist/edge/index.d.ts +23 -23
  442. package/dist/edge/index.js +14 -14
  443. package/dist/elysia/index.d.ts +1 -1
  444. package/dist/elysia/index.js +1 -1
  445. package/dist/embeddings-kFbRvuic.d.ts +78 -0
  446. package/dist/embeddings-vVzseUw0.js +128 -0
  447. package/dist/ended-gUsMZVNg.d.ts +26 -0
  448. package/dist/entitlements-CJpYQXeE.d.ts +54 -0
  449. package/dist/entitlements-NZAE_vLj.js +45 -0
  450. package/dist/entity-naming-Dtyq5sKE.js +131 -0
  451. package/dist/entity-schema-BDv6w-OQ.js +39 -0
  452. package/dist/envelope-Bzu0JnY2.js +90 -0
  453. package/dist/errors-BVFgt8BZ.js +328 -0
  454. package/dist/errors-CyxMxzlu.js +44 -0
  455. package/dist/escape-like-PReMbEEU.d.ts +30 -0
  456. package/dist/eve/index.d.ts +5 -5
  457. package/dist/eve/index.js +4 -4
  458. package/dist/eve-BFSwTu1z.d.ts +157 -0
  459. package/dist/event-calendar-DCufbihU.d.ts +65 -0
  460. package/dist/events/index.d.ts +4 -4
  461. package/dist/events/index.js +2 -1
  462. package/dist/events-vcqenSYU.d.ts +852 -0
  463. package/dist/executor-3qcMhg6R.js +207 -0
  464. package/dist/executor-BQ7dYw4E.js +843 -0
  465. package/dist/executor-CVIn6JHa.d.ts +174 -0
  466. package/dist/executor-D3gB-9si.js +486 -0
  467. package/dist/executor-_2ekNofh.d.ts +104 -0
  468. package/dist/executor-hGjrop3A.d.ts +37 -0
  469. package/dist/expo/index.d.ts +12 -11
  470. package/dist/expo/index.js +12 -11
  471. package/dist/extension-B-lajjex.js +58 -0
  472. package/dist/extension-B0EjCYhO.js +61 -0
  473. package/dist/extension-DeAd_MqI.js +121 -0
  474. package/dist/extension-DzCc0IOd.js +103 -0
  475. package/dist/extension-shared-Djm8mGHD.js +60 -0
  476. package/dist/field-errors-fsbFktYF.d.ts +216 -0
  477. package/dist/field-formats-CAFNkKSN.d.ts +38 -0
  478. package/dist/fields-B0zaJAO3.js +107 -0
  479. package/dist/fields-C3q2ypnO.d.ts +78 -0
  480. package/dist/filter-evaluate-CPxeae5X.d.ts +37 -0
  481. package/dist/filter-evaluate-w1XOIUOL.js +217 -0
  482. package/dist/filter-query-BXLs72G9.d.ts +133 -0
  483. package/dist/filter-query-DMoKo82_.js +566 -0
  484. package/dist/flags-BOdA_ETQ.js +279 -0
  485. package/dist/flags-C01PLMJ-.d.ts +246 -0
  486. package/dist/flags-sdk/index.d.ts +1 -1
  487. package/dist/flags-sdk/index.js +2 -2
  488. package/dist/form-definition-BTpM_tz1.js +363 -0
  489. package/dist/form-definition-BjEtH8BT.d.ts +119 -0
  490. package/dist/format-u-65igKI.d.ts +79 -0
  491. package/dist/framework-BKyTeS-j.d.ts +47 -0
  492. package/dist/framework-DDdkR7FX.js +58 -0
  493. package/dist/function-contract-DxFNOmsm.d.ts +27 -0
  494. package/dist/gateway-TD1pRfTP.d.ts +115 -0
  495. package/dist/guest-actions-1T1DeS5Y.d.ts +5 -0
  496. package/dist/guest-actions-B7zdhfnh.js +9 -0
  497. package/dist/h3/index.d.ts +2 -4
  498. package/dist/h3/index.js +2 -4
  499. package/dist/h3/v1/index.d.ts +2 -3
  500. package/dist/h3/v1/index.js +2 -3
  501. package/dist/hono/index.d.ts +27 -21
  502. package/dist/hono/index.js +22 -20
  503. package/dist/http-7-UvHjQe.js +28 -0
  504. package/dist/impersonation-DM3oWNoD.js +62 -0
  505. package/dist/inbox-NDKwSk_H.d.ts +321 -0
  506. package/dist/index-B938YqY-.d.ts +188 -0
  507. package/dist/index-BUuVq1co.d.ts +158 -0
  508. package/dist/index-BnH4Gomw.d.ts +633 -0
  509. package/dist/index-BxYOB1Oc.d.ts +149 -0
  510. package/dist/index-C-qpk8EQ.d.ts +262 -0
  511. package/dist/index-CG9rA8H8.d.ts +407 -0
  512. package/dist/index-CguFm716.d.ts +518 -0
  513. package/dist/index-DLq69k3F.d.ts +588 -0
  514. package/dist/index-Do2ChN_y.d.ts +287 -0
  515. package/dist/index.d.ts +39 -92
  516. package/dist/index.js +95 -16
  517. package/dist/instances-BHjCZoF8.js +277 -0
  518. package/dist/invalidate-DMp4eqfF.d.ts +21 -0
  519. package/dist/json-schema-Ca6gYbEP.js +888 -0
  520. package/dist/jwks-cache-DEsKI0hJ.js +49 -0
  521. package/dist/kit-5l-o8RxL.js +133 -0
  522. package/dist/kit-DOwKpSkA.d.ts +94 -0
  523. package/dist/knowledge-BGbtxMP9.js +300 -0
  524. package/dist/knowledge-CtL-Vfgy.d.ts +217 -0
  525. package/dist/lint/index.d.ts +94 -11
  526. package/dist/lint/index.js +268 -26
  527. package/dist/list/index.d.ts +4 -4
  528. package/dist/list/index.js +3 -3
  529. package/dist/list-query-BDVutLFr.d.ts +222 -0
  530. package/dist/list-query-BKy2zBaT.js +559 -0
  531. package/dist/live-C21DgbsD.js +275 -0
  532. package/dist/mapper/bin.js +380 -0
  533. package/dist/mcp/index.d.ts +8 -8
  534. package/dist/mcp/index.js +11 -11
  535. package/dist/mcp/sdk/index.d.ts +10 -10
  536. package/dist/mcp/sdk/index.js +11 -11
  537. package/dist/messages-D4XWuRTj.js +492 -0
  538. package/dist/messaging-C_Pr9tGx.d.ts +297 -0
  539. package/dist/methods-B3o7BFOW.js +11 -0
  540. package/dist/mfa-oIOYTpvc.d.ts +36 -0
  541. package/dist/model-hwpvole9.d.ts +340 -0
  542. package/dist/model-messages-CKG0zwdF.js +206 -0
  543. package/dist/money-codec-BzddguAQ.js +41 -0
  544. package/dist/my-work-kinds-BU3SlA-6.js +11 -0
  545. package/dist/nestjs/index.d.ts +3 -3
  546. package/dist/nestjs/index.js +2 -2
  547. package/dist/next/index.d.ts +296 -57
  548. package/dist/next/index.js +310 -54
  549. package/dist/node/index.d.ts +2 -2
  550. package/dist/node/index.js +6 -8
  551. package/dist/operations-CSc_IsHO.d.ts +6 -0
  552. package/dist/options-0qvUckqt.js +9 -0
  553. package/dist/options-1GgseJq5.js +10 -0
  554. package/dist/options-21s0dpnX.js +9 -0
  555. package/dist/options-2EMA_x6_.js +16 -0
  556. package/dist/options-63AKQNv0.js +5 -0
  557. package/dist/options-9gvPo1BC.js +8 -0
  558. package/dist/options-B-cxGmRA.js +5 -0
  559. package/dist/options-B0EJULm5.js +5 -0
  560. package/dist/options-B2swWnJW.js +9 -0
  561. package/dist/options-B5VUf4IT.js +8 -0
  562. package/dist/options-BMGClqoa.js +11 -0
  563. package/dist/options-BPDiwSa-.js +5 -0
  564. package/dist/options-BQIYfx9F.js +9 -0
  565. package/dist/options-BVg58avP.js +5 -0
  566. package/dist/options-BYOtaF7w.js +5 -0
  567. package/dist/options-BZOliZvb.js +9 -0
  568. package/dist/options-BbnPv6p7.js +12 -0
  569. package/dist/options-BfLmA375.js +5 -0
  570. package/dist/options-BjtrC7o6.js +8 -0
  571. package/dist/options-BlncUmIc.js +9 -0
  572. package/dist/options-BlzSfWxC.d.ts +10 -0
  573. package/dist/options-BnZS7IK-.js +9 -0
  574. package/dist/options-Bpl-WFPk.js +15 -0
  575. package/dist/options-Br1Xi2Hw.js +9 -0
  576. package/dist/options-Bt0bYMLl.js +24 -0
  577. package/dist/options-BvtnxHfK.js +24 -0
  578. package/dist/options-C-dQHH5-.js +8 -0
  579. package/dist/options-C3RgtOLo.js +5 -0
  580. package/dist/options-CE93HOYi.d.ts +7 -0
  581. package/dist/options-CF7nRWXS.js +5 -0
  582. package/dist/options-CM6-18FQ.d.ts +13 -0
  583. package/dist/options-CPK2nQER.js +5 -0
  584. package/dist/options-CRz8Ht_4.js +9 -0
  585. package/dist/options-CSCXNYJO.d.ts +8 -0
  586. package/dist/options-CW9wTqOk.js +11 -0
  587. package/dist/options-CYV4iZiQ.js +5 -0
  588. package/dist/options-Cl9Ag2Pt.js +8 -0
  589. package/dist/options-Cp0HyUEk.js +9 -0
  590. package/dist/options-Cqq2jUww.js +10 -0
  591. package/dist/options-CsGl5BLH.d.ts +52 -0
  592. package/dist/options-CtUUU71w.js +20 -0
  593. package/dist/options-CvD9vNvx.js +9 -0
  594. package/dist/options-CyAhRKKm.d.ts +14 -0
  595. package/dist/options-Cyu0hgmN.js +9 -0
  596. package/dist/options-D0G06Zm9.js +8 -0
  597. package/dist/options-D0bzZx9a.d.ts +10 -0
  598. package/dist/options-D1hSuf50.d.ts +12 -0
  599. package/dist/options-D6as77gU.d.ts +16 -0
  600. package/dist/options-DChZoVfs.d.ts +11 -0
  601. package/dist/options-DINdgVih.d.ts +9 -0
  602. package/dist/options-DJ-aYPVN.js +5 -0
  603. package/dist/options-DPgdAXO5.js +11 -0
  604. package/dist/options-DWML05Cp.js +5 -0
  605. package/dist/options-DWVYM8P0.d.ts +17 -0
  606. package/dist/options-DWbA9buW.js +9 -0
  607. package/dist/options-DXnBN2E7.js +8 -0
  608. package/dist/options-D_bBVCXg.js +5 -0
  609. package/dist/options-DcOx8_3X.d.ts +19 -0
  610. package/dist/options-DeNejBtq.js +8 -0
  611. package/dist/options-DgQo80Xh.js +5 -0
  612. package/dist/options-DlohRAYd.d.ts +13 -0
  613. package/dist/options-DplE18Ko.d.ts +12 -0
  614. package/dist/options-DrSkIBCD.js +9 -0
  615. package/dist/options-E7KcmHQv.js +8 -0
  616. package/dist/options-HLwOuF2H.js +10 -0
  617. package/dist/options-IHPg8ZoA.js +5 -0
  618. package/dist/options-KV84gAxl.js +10 -0
  619. package/dist/options-Li1Dyu88.d.ts +10 -0
  620. package/dist/options-Q1Bzgqpr.js +5 -0
  621. package/dist/options-_F-enoBs.d.ts +26 -0
  622. package/dist/options-c5WCsky5.js +21 -0
  623. package/dist/options-nFzfgtA6.js +5 -0
  624. package/dist/options-o2kfYU2x.js +8 -0
  625. package/dist/options-pOVWwbAK.d.ts +10 -0
  626. package/dist/options-t_O5ldT6.js +5 -0
  627. package/dist/options-xD4_FRUq.js +10 -0
  628. package/dist/options-y_-AkVCs.d.ts +15 -0
  629. package/dist/options-zkyhM3qi.js +8 -0
  630. package/dist/orpc/index.d.ts +76 -20
  631. package/dist/orpc/index.js +174 -9
  632. package/dist/otel/index.d.ts +3 -3
  633. package/dist/otel/index.js +7 -7
  634. package/dist/outbox-HvGAJuBe.d.ts +199 -0
  635. package/dist/payments-BXvabJOw.js +16 -0
  636. package/dist/payments-BYcN34Fp.d.ts +114 -0
  637. package/dist/permissions-controller-BMsqIE5T.js +34 -0
  638. package/dist/plugin-AKwwe2gF.js +48 -0
  639. package/dist/pool-CrtT4dg-.d.ts +123 -0
  640. package/dist/pool-errors-JUBcGrre.js +27 -0
  641. package/dist/ports-DOhtuw3c.js +152 -0
  642. package/dist/ports-sx226JUL.d.ts +89 -0
  643. package/dist/postgres/index.d.ts +5 -5
  644. package/dist/postgres/index.js +3 -3
  645. package/dist/postgrest-executor-BFW4oWte.js +1064 -0
  646. package/dist/postgrest-z-T0l23T.d.ts +53 -0
  647. package/dist/powersync/index.d.ts +106 -7
  648. package/dist/powersync/index.js +169 -23
  649. package/dist/preset/index.d.ts +4 -4
  650. package/dist/preset/index.js +4 -4
  651. package/dist/preset-CsHnIBJF.js +875 -0
  652. package/dist/problem-Bf0cJ7tk.js +162 -0
  653. package/dist/problem-DNwN8iGz.d.ts +79 -0
  654. package/dist/provider-B6g7rBZX.js +57 -0
  655. package/dist/provider-CqwEOLCA.d.ts +112 -0
  656. package/dist/push-token-D3Vcf0Pf.d.ts +24 -0
  657. package/dist/queries-BWYYS9um.js +239 -0
  658. package/dist/query/index.d.ts +4 -4
  659. package/dist/query/index.js +357 -3
  660. package/dist/rank-result-BBJIK-k_.js +18 -0
  661. package/dist/rank-result-Bwe7igy-.d.ts +14 -0
  662. package/dist/rate-limit-BWKGlhTM.js +49 -0
  663. package/dist/rate-limit-Bztf32cl.d.ts +50 -0
  664. package/dist/rate-limit-o44SYkVY.js +327 -0
  665. package/dist/react/index.d.ts +10 -9
  666. package/dist/react/index.js +5 -5
  667. package/dist/react/native/index.d.ts +1 -1
  668. package/dist/react/native/index.js +2 -3
  669. package/dist/react/server.d.ts +72 -13
  670. package/dist/react/server.js +71 -9
  671. package/dist/react/session.d.ts +3 -3
  672. package/dist/react-BuDYI62C.js +979 -0
  673. package/dist/react-router/index.d.ts +19 -13
  674. package/dist/react-router/index.js +10 -10
  675. package/dist/read-set-BnKnofIj.js +199 -0
  676. package/dist/realtime/index.d.ts +7 -7
  677. package/dist/realtime/index.js +36 -4
  678. package/dist/record-bindings-DkBJpC5A.js +26 -0
  679. package/dist/record-form-BYs9xv_N.d.ts +82 -0
  680. package/dist/record-type-DRbnlj5q.js +221 -0
  681. package/dist/recordings-BBSJt_YV.d.ts +119 -0
  682. package/dist/records/index.d.ts +83 -0
  683. package/dist/records/index.js +314 -0
  684. package/dist/refresh-DT_ICaK-.d.ts +54 -0
  685. package/dist/refusal-ByBZcm0V.js +64 -0
  686. package/dist/refusal-CSntB_x8.d.ts +32 -0
  687. package/dist/replicas-Dyoh3CEg.js +94 -0
  688. package/dist/resolve-B0mW2m7f.js +932 -0
  689. package/dist/resolve-CXX6DHv3.d.ts +288 -0
  690. package/dist/resource-D8pzB5Qf.d.ts +123 -0
  691. package/dist/resource-DV2GC2dB.js +520 -0
  692. package/dist/resource-DvgXqZB_.js +105 -0
  693. package/dist/resource-execute-CzHuDsil.d.ts +15 -0
  694. package/dist/resource-types-DnKiH4G2.d.ts +157 -0
  695. package/dist/resources-B9cQxhjw.d.ts +7 -0
  696. package/dist/resources-C02l_YBJ.js +64 -0
  697. package/dist/respond-C3CcOZlA.js +100 -0
  698. package/dist/respond-Dn1_YhC1.d.ts +106 -0
  699. package/dist/result-DZeA5KS6.d.ts +351 -0
  700. package/dist/result-ntFBW2p2.js +146 -0
  701. package/dist/rows-BDLZStp4.js +80 -0
  702. package/dist/rows-Bm6sskji.js +39 -0
  703. package/dist/rrule-79K2c3Nr.js +290 -0
  704. package/dist/run-state-BMCD_1Lt.d.ts +13 -0
  705. package/dist/sales-lines-Bk4mbLH1.d.ts +37 -0
  706. package/dist/sales-lines-D-sf9PIB.js +43 -0
  707. package/dist/sales-shared-DXGFJP4g.js +22 -0
  708. package/dist/schemas-l2aF78zQ.js +216 -0
  709. package/dist/scores-B2BiNuVJ.d.ts +174 -0
  710. package/dist/scores-CRp3vvQU.js +177 -0
  711. package/dist/server/index.d.ts +103 -42
  712. package/dist/server/index.js +98 -30
  713. package/dist/server-types-CFtehAsr.d.ts +946 -0
  714. package/dist/session-CH0SKgIz.d.ts +99 -0
  715. package/dist/shared-C47zAh2A.js +17 -0
  716. package/dist/sign-in-BAjR49fq.d.ts +24 -0
  717. package/dist/sign-in-BsIaNx2C.js +392 -0
  718. package/dist/sign-in-DNCdrk05.js +608 -0
  719. package/dist/signatures-CNcoq392.js +145 -0
  720. package/dist/signatures-Dkx9OE8j.d.ts +43 -0
  721. package/dist/signers-BAEzvTJ0.d.ts +390 -0
  722. package/dist/signing-keys-BNDUxpQL.d.ts +119 -0
  723. package/dist/signing-keys-wGKh4vtt.js +164 -0
  724. package/dist/solid/index.d.ts +67 -15
  725. package/dist/solid/index.js +93 -11
  726. package/dist/solid-start/index.d.ts +17 -14
  727. package/dist/solid-start/index.js +8 -8
  728. package/dist/spec/index.d.ts +691 -9
  729. package/dist/spec/index.js +2487 -5
  730. package/dist/spec-DTWkHzYZ.js +124 -0
  731. package/dist/spec-pins-C1Vm8OBl.js +61 -0
  732. package/dist/spec-pins-DIx95afw.d.ts +125 -0
  733. package/dist/sql/index.d.ts +5 -5
  734. package/dist/sql/index.js +5 -5
  735. package/dist/sql-BPbbPzRJ.js +421 -0
  736. package/dist/sql-ClHIdgKS.js +126996 -0
  737. package/dist/sqlite/index.d.ts +126 -0
  738. package/dist/sqlite/index.js +197 -0
  739. package/dist/sso-B0Txid2-.d.ts +161 -0
  740. package/dist/sso-d2S4x_f9.js +217 -0
  741. package/dist/ssr/index.d.ts +5 -4
  742. package/dist/ssr/index.js +3 -3
  743. package/dist/standard-BA3mLh6w.js +25 -0
  744. package/dist/standard-DBV730sF.d.ts +7 -0
  745. package/dist/stats-BHl1-zj9.js +83 -0
  746. package/dist/statuses-DMMDwXpZ.js +29 -0
  747. package/dist/statuses-zRcc2TeZ.js +28 -0
  748. package/dist/storage/index.d.ts +3 -3
  749. package/dist/storage/index.js +11 -11
  750. package/dist/store-CpheI-lN.d.ts +83 -0
  751. package/dist/streams/index.d.ts +3 -3
  752. package/dist/streams/index.js +2 -3
  753. package/dist/streams/redis/index.d.ts +2 -2
  754. package/dist/streams/redis/index.js +3 -3
  755. package/dist/stripe/index.d.ts +1 -1
  756. package/dist/stripe-X22LjuH-.d.ts +156 -0
  757. package/dist/subject-readers-B-pQ9Mmo.js +49 -0
  758. package/dist/subject-readers-BNSzkqw_.d.ts +21 -0
  759. package/dist/subjects-BpIEN1MS.d.ts +90 -0
  760. package/dist/subjects-DvmQeF_F.d.ts +79 -0
  761. package/dist/summaries-8jQTELIX.d.ts +69 -0
  762. package/dist/support-view-BYOgwXsJ.d.ts +17 -0
  763. package/dist/svelte/index.d.ts +67 -12
  764. package/dist/svelte/index.js +108 -11
  765. package/dist/sveltekit/index.d.ts +19 -16
  766. package/dist/sveltekit/index.js +14 -14
  767. package/dist/table-schema-BlF6k62Q.d.ts +81 -0
  768. package/dist/table-schema-CD8cJz_M.js +293 -0
  769. package/dist/tags-BjHflcHF.d.ts +30 -0
  770. package/dist/tags-CIZTqtlY.js +39 -0
  771. package/dist/tanstack-db/index.d.ts +50 -6
  772. package/dist/tanstack-db/index.js +44 -3
  773. package/dist/tanstack-start/index.d.ts +15 -12
  774. package/dist/tanstack-start/index.js +9 -9
  775. package/dist/tax-BkCNWWMc.js +140 -0
  776. package/dist/tax-DPShHual.d.ts +170 -0
  777. package/dist/tee-V97_v1ZS.js +101 -0
  778. package/dist/template-Drgg3v2J.js +126 -0
  779. package/dist/temporal-required-Dy0GGD8E.js +20 -0
  780. package/dist/tenant-BgIiMQpo.js +27 -0
  781. package/dist/tenant-source-bqWpSr86.js +8 -0
  782. package/dist/tenant-yYg0E2-n.d.ts +17 -0
  783. package/dist/testing/index.d.ts +183 -58
  784. package/dist/testing/index.js +405 -97
  785. package/dist/timeline--VU3A1e4.js +50 -0
  786. package/dist/timeline-CtG0sehj.d.ts +151 -0
  787. package/dist/timing-dypS0nxW.js +95 -0
  788. package/dist/topic-CJrQZQaa.d.ts +415 -0
  789. package/dist/topic-ikV_pLTX.js +643 -0
  790. package/dist/types-BBQLbui9.d.ts +659 -0
  791. package/dist/types-CAQBmpDx.d.ts +217 -0
  792. package/dist/types-DFdg_4yN.d.ts +912 -0
  793. package/dist/types-Dp3UCr1e.d.ts +194 -0
  794. package/dist/types-Dr9wZgNy.d.ts +251 -0
  795. package/dist/types-EO2crSX_.d.ts +177 -0
  796. package/dist/types-SFRomQF7.d.ts +624 -0
  797. package/dist/undo-B1jwnVyN.d.ts +46 -0
  798. package/dist/undo-BjCChCmH.js +44 -0
  799. package/dist/upload-claim-B1ZU8ydB.js +33 -0
  800. package/dist/usage-D7QkrwzV.d.ts +232 -0
  801. package/dist/user-change-DgWC6RRC.js +135 -0
  802. package/dist/variants-Cy--Ub-a.js +53 -0
  803. package/dist/vault-O-YlAyHb.d.ts +62 -0
  804. package/dist/vercel-connect/index.d.ts +1 -1
  805. package/dist/vercel-connect/index.js +3 -3
  806. package/dist/verify-CMou8dWZ.d.ts +146 -0
  807. package/dist/verify-ZLFUdlqE.js +130 -0
  808. package/dist/view-C8Ve14ja.js +247 -0
  809. package/dist/view-DyFdMCqg.d.ts +97 -0
  810. package/dist/vue/index.d.ts +67 -20
  811. package/dist/vue/index.js +102 -14
  812. package/dist/watch-topic-BcGfvZOE.d.ts +31 -0
  813. package/dist/wiring-CDNuQoJJ.js +13 -0
  814. package/dist/workflow-sdk/builder/index.d.ts +12 -12
  815. package/dist/workflow-sdk/builder/index.js +16 -17
  816. package/dist/workflow-sdk/index.d.ts +18 -15
  817. package/dist/workflow-sdk/index.js +2 -2
  818. package/dist/workflow-sdk/world/index.d.ts +6 -6
  819. package/dist/workflow-sdk/world/index.js +12 -12
  820. package/dist/workflow-sdk-BFHu4sci.d.ts +163 -0
  821. package/dist/workflow-sdk-BVlFEB5Q.js +242 -0
  822. package/dist/workflow-sdk-Bw6_u0ar.js +2446 -0
  823. package/dist/workflows-B0ZPAQu0.d.ts +255 -0
  824. package/dist/yjs/index.d.ts +28 -4
  825. package/dist/yjs/index.js +52 -10
  826. package/package.json +201 -540
  827. package/schemas/config-v1.json +2720 -1693
  828. package/schemas/design-v1.json +147 -5
  829. package/schemas/doctor-report-v1.json +14 -0
  830. package/schemas/snapshot-v2.json +25 -0
  831. package/skills/better-supabase/SKILL.md +71 -28
  832. package/skills/better-supabase/references/block-extensions.md +64 -0
  833. package/skills/better-supabase/references/frontend.md +29 -26
  834. package/skills/better-supabase/references/troubleshooting.md +19 -19
  835. package/skills/better-supabase/references/ui-recipes.md +12837 -8039
  836. package/skills/better-supabase-api/SKILL.md +49 -38
  837. package/skills/better-supabase-api/references/adapters.md +42 -39
  838. package/skills/better-supabase-api/references/ai.md +4 -4
  839. package/skills/better-supabase-api/references/expo.md +8 -8
  840. package/skills/better-supabase-api/references/powersync.md +25 -11
  841. package/skills/better-supabase-api/references/streams-credentials.md +3 -3
  842. package/skills/better-supabase-auth/SKILL.md +17 -17
  843. package/skills/better-supabase-auth/references/authorization-providers.md +8 -6
  844. package/skills/better-supabase-testing/SKILL.md +8 -8
  845. package/dist/access-sql-BpOdJvfj.js +0 -205
  846. package/dist/actor-C9pwyCVz.js +0 -150
  847. package/dist/adapter-D8dW6p2A.js +0 -2190
  848. package/dist/agents-CuxuXVWv.d.ts +0 -117
  849. package/dist/ai-cache-BXMXRhkx.d.ts +0 -82
  850. package/dist/ai-cache-Byy5q6EZ.js +0 -87
  851. package/dist/ai-chat-B00sIOjY.d.ts +0 -619
  852. package/dist/ai-files-DTbIJcxQ.js +0 -339
  853. package/dist/ai-tasks-DN-AV7qB.d.ts +0 -166
  854. package/dist/announcements-DqiLxtKK.js +0 -128
  855. package/dist/api-CmcTYuFT.js +0 -685
  856. package/dist/api-DwLhJS1j.d.ts +0 -444
  857. package/dist/api-keys-Bade6Qtl.d.ts +0 -213
  858. package/dist/api-keys-CO24Ffil.js +0 -318
  859. package/dist/app-tables-0YZ9IskE.js +0 -831
  860. package/dist/app-tables-CbWUI7h_.d.ts +0 -35
  861. package/dist/approvals-C-4MOWdV.d.ts +0 -127
  862. package/dist/arazzo/index.d.ts +0 -277
  863. package/dist/arazzo/index.js +0 -630
  864. package/dist/assistant-CcP_kpu7.d.ts +0 -157
  865. package/dist/asyncapi/index.d.ts +0 -109
  866. package/dist/asyncapi/index.js +0 -572
  867. package/dist/attachments-CPkGJVfk.js +0 -270
  868. package/dist/attributes-BOFl922R.js +0 -44
  869. package/dist/audit-log-5aTnl7W2.js +0 -788
  870. package/dist/auth-module-CwCIY97x.js +0 -10
  871. package/dist/auth-reply-D02x5NSv.js +0 -31
  872. package/dist/authorization-CB69cvnA.d.ts +0 -1504
  873. package/dist/authorization-hooks-HXVZXxUk.d.ts +0 -217
  874. package/dist/authorizer-BVR2vPGA.js +0 -125
  875. package/dist/authorizer-DFP3gHv8.d.ts +0 -82
  876. package/dist/bind-DcXLU1ul.d.ts +0 -76
  877. package/dist/block-env-YfPmSNa1.js +0 -32
  878. package/dist/block-env-kFcaaJLM.d.ts +0 -10
  879. package/dist/block-events-BURaqwyp.js +0 -96
  880. package/dist/block-fields-Cwd4c8m-.js +0 -139
  881. package/dist/block-fields-DnXkMney.d.ts +0 -58
  882. package/dist/block-helpers-DwE32_gT.js +0 -272
  883. package/dist/block-helpers-DyA5BAW4.d.ts +0 -43
  884. package/dist/block-hooks-BadAniNI.d.ts +0 -117
  885. package/dist/block-hooks-C1r-R2r0.js +0 -130
  886. package/dist/block-transport-DBPhgF3W.d.ts +0 -69
  887. package/dist/block-transport-Ds0C2mUf.js +0 -84
  888. package/dist/blocks/ai-chat/react/index.d.ts +0 -52
  889. package/dist/blocks/ai-chat/react/index.js +0 -95
  890. package/dist/blocks/ai-chat/react/server.d.ts +0 -25
  891. package/dist/blocks/ai-chat/react/server.js +0 -23
  892. package/dist/blocks/ai-chat/solid/index.d.ts +0 -34
  893. package/dist/blocks/ai-chat/solid/index.js +0 -41
  894. package/dist/blocks/ai-chat/svelte/index.d.ts +0 -34
  895. package/dist/blocks/ai-chat/svelte/index.js +0 -41
  896. package/dist/blocks/ai-chat/vue/index.d.ts +0 -34
  897. package/dist/blocks/ai-chat/vue/index.js +0 -41
  898. package/dist/blocks/ai-providers/index.d.ts +0 -22
  899. package/dist/blocks/ai-providers/index.js +0 -237
  900. package/dist/blocks/ai-tasks/index.d.ts +0 -10
  901. package/dist/blocks/ai-tasks/index.js +0 -253
  902. package/dist/blocks/announcements/react/index.d.ts +0 -28
  903. package/dist/blocks/announcements/react/index.js +0 -38
  904. package/dist/blocks/announcements/react/server.d.ts +0 -9
  905. package/dist/blocks/announcements/react/server.js +0 -10
  906. package/dist/blocks/announcements/solid/index.d.ts +0 -13
  907. package/dist/blocks/announcements/solid/index.js +0 -14
  908. package/dist/blocks/announcements/svelte/index.d.ts +0 -13
  909. package/dist/blocks/announcements/svelte/index.js +0 -14
  910. package/dist/blocks/announcements/vue/index.d.ts +0 -13
  911. package/dist/blocks/announcements/vue/index.js +0 -14
  912. package/dist/blocks/api-keys/react/index.d.ts +0 -32
  913. package/dist/blocks/api-keys/react/index.js +0 -43
  914. package/dist/blocks/api-keys/react/server.d.ts +0 -7
  915. package/dist/blocks/api-keys/react/server.js +0 -7
  916. package/dist/blocks/api-keys/solid/index.d.ts +0 -13
  917. package/dist/blocks/api-keys/solid/index.js +0 -14
  918. package/dist/blocks/api-keys/svelte/index.d.ts +0 -13
  919. package/dist/blocks/api-keys/svelte/index.js +0 -14
  920. package/dist/blocks/api-keys/vue/index.d.ts +0 -13
  921. package/dist/blocks/api-keys/vue/index.js +0 -14
  922. package/dist/blocks/audit/react/index.d.ts +0 -31
  923. package/dist/blocks/audit/react/index.js +0 -46
  924. package/dist/blocks/audit/react/server.d.ts +0 -7
  925. package/dist/blocks/audit/react/server.js +0 -7
  926. package/dist/blocks/audit/solid/index.d.ts +0 -13
  927. package/dist/blocks/audit/solid/index.js +0 -14
  928. package/dist/blocks/audit/svelte/index.d.ts +0 -13
  929. package/dist/blocks/audit/svelte/index.js +0 -14
  930. package/dist/blocks/audit/vue/index.d.ts +0 -13
  931. package/dist/blocks/audit/vue/index.js +0 -14
  932. package/dist/blocks/billing/react/index.d.ts +0 -44
  933. package/dist/blocks/billing/react/index.js +0 -40
  934. package/dist/blocks/billing/react/server.d.ts +0 -10
  935. package/dist/blocks/billing/react/server.js +0 -10
  936. package/dist/blocks/billing/solid/index.d.ts +0 -15
  937. package/dist/blocks/billing/solid/index.js +0 -14
  938. package/dist/blocks/billing/svelte/index.d.ts +0 -15
  939. package/dist/blocks/billing/svelte/index.js +0 -14
  940. package/dist/blocks/billing/vue/index.d.ts +0 -15
  941. package/dist/blocks/billing/vue/index.js +0 -14
  942. package/dist/blocks/bookings/react/index.d.ts +0 -35
  943. package/dist/blocks/bookings/react/index.js +0 -63
  944. package/dist/blocks/bookings/react/server.d.ts +0 -9
  945. package/dist/blocks/bookings/react/server.js +0 -10
  946. package/dist/blocks/bookings/solid/index.d.ts +0 -13
  947. package/dist/blocks/bookings/solid/index.js +0 -14
  948. package/dist/blocks/bookings/svelte/index.d.ts +0 -13
  949. package/dist/blocks/bookings/svelte/index.js +0 -14
  950. package/dist/blocks/bookings/vue/index.d.ts +0 -13
  951. package/dist/blocks/bookings/vue/index.js +0 -14
  952. package/dist/blocks/calendar/react/index.d.ts +0 -26
  953. package/dist/blocks/calendar/react/index.js +0 -58
  954. package/dist/blocks/calendar/react/server.d.ts +0 -9
  955. package/dist/blocks/calendar/react/server.js +0 -10
  956. package/dist/blocks/calendar/solid/index.d.ts +0 -13
  957. package/dist/blocks/calendar/solid/index.js +0 -14
  958. package/dist/blocks/calendar/svelte/index.d.ts +0 -13
  959. package/dist/blocks/calendar/svelte/index.js +0 -14
  960. package/dist/blocks/calendar/vue/index.d.ts +0 -13
  961. package/dist/blocks/calendar/vue/index.js +0 -14
  962. package/dist/blocks/comments/react/index.d.ts +0 -34
  963. package/dist/blocks/comments/react/index.js +0 -49
  964. package/dist/blocks/comments/react/server.d.ts +0 -7
  965. package/dist/blocks/comments/react/server.js +0 -7
  966. package/dist/blocks/comments/solid/index.d.ts +0 -13
  967. package/dist/blocks/comments/solid/index.js +0 -14
  968. package/dist/blocks/comments/svelte/index.d.ts +0 -13
  969. package/dist/blocks/comments/svelte/index.js +0 -14
  970. package/dist/blocks/comments/vue/index.d.ts +0 -13
  971. package/dist/blocks/comments/vue/index.js +0 -14
  972. package/dist/blocks/engagements/index.d.ts +0 -134
  973. package/dist/blocks/engagements/index.js +0 -143
  974. package/dist/blocks/entitlements/react/index.d.ts +0 -27
  975. package/dist/blocks/entitlements/react/index.js +0 -35
  976. package/dist/blocks/entitlements/react/server.d.ts +0 -9
  977. package/dist/blocks/entitlements/react/server.js +0 -10
  978. package/dist/blocks/entitlements/solid/index.d.ts +0 -13
  979. package/dist/blocks/entitlements/solid/index.js +0 -14
  980. package/dist/blocks/entitlements/svelte/index.d.ts +0 -13
  981. package/dist/blocks/entitlements/svelte/index.js +0 -14
  982. package/dist/blocks/entitlements/vue/index.d.ts +0 -13
  983. package/dist/blocks/entitlements/vue/index.js +0 -14
  984. package/dist/blocks/forms/react/index.d.ts +0 -19
  985. package/dist/blocks/forms/react/index.js +0 -28
  986. package/dist/blocks/forms/react/server.d.ts +0 -9
  987. package/dist/blocks/forms/react/server.js +0 -10
  988. package/dist/blocks/forms/solid/index.d.ts +0 -13
  989. package/dist/blocks/forms/solid/index.js +0 -14
  990. package/dist/blocks/forms/svelte/index.d.ts +0 -13
  991. package/dist/blocks/forms/svelte/index.js +0 -14
  992. package/dist/blocks/forms/vue/index.d.ts +0 -13
  993. package/dist/blocks/forms/vue/index.js +0 -14
  994. package/dist/blocks/inbox/react/index.d.ts +0 -3
  995. package/dist/blocks/inbox/react/index.js +0 -81
  996. package/dist/blocks/inbox/react/server.d.ts +0 -17
  997. package/dist/blocks/inbox/react/server.js +0 -18
  998. package/dist/blocks/inbox/solid/index.d.ts +0 -27
  999. package/dist/blocks/inbox/solid/index.js +0 -32
  1000. package/dist/blocks/inbox/svelte/index.d.ts +0 -27
  1001. package/dist/blocks/inbox/svelte/index.js +0 -32
  1002. package/dist/blocks/inbox/vue/index.d.ts +0 -27
  1003. package/dist/blocks/inbox/vue/index.js +0 -32
  1004. package/dist/blocks/jobs/index.d.ts +0 -523
  1005. package/dist/blocks/jobs/index.js +0 -1346
  1006. package/dist/blocks/mfa/react/index.d.ts +0 -30
  1007. package/dist/blocks/mfa/react/index.js +0 -45
  1008. package/dist/blocks/mfa/react/server.d.ts +0 -9
  1009. package/dist/blocks/mfa/react/server.js +0 -12
  1010. package/dist/blocks/mfa/solid/index.d.ts +0 -20
  1011. package/dist/blocks/mfa/solid/index.js +0 -23
  1012. package/dist/blocks/mfa/svelte/index.d.ts +0 -20
  1013. package/dist/blocks/mfa/svelte/index.js +0 -23
  1014. package/dist/blocks/mfa/vue/index.d.ts +0 -20
  1015. package/dist/blocks/mfa/vue/index.js +0 -23
  1016. package/dist/blocks/notifications/react/index.d.ts +0 -19
  1017. package/dist/blocks/notifications/react/index.js +0 -41
  1018. package/dist/blocks/notifications/react/server.d.ts +0 -10
  1019. package/dist/blocks/notifications/react/server.js +0 -10
  1020. package/dist/blocks/notifications/solid/index.d.ts +0 -13
  1021. package/dist/blocks/notifications/solid/index.js +0 -14
  1022. package/dist/blocks/notifications/svelte/index.d.ts +0 -13
  1023. package/dist/blocks/notifications/svelte/index.js +0 -14
  1024. package/dist/blocks/notifications/vue/index.d.ts +0 -13
  1025. package/dist/blocks/notifications/vue/index.js +0 -14
  1026. package/dist/blocks/number-sequences/index.d.ts +0 -48
  1027. package/dist/blocks/number-sequences/index.js +0 -52
  1028. package/dist/blocks/onboarding/react/index.d.ts +0 -24
  1029. package/dist/blocks/onboarding/react/index.js +0 -38
  1030. package/dist/blocks/onboarding/react/server.d.ts +0 -10
  1031. package/dist/blocks/onboarding/react/server.js +0 -10
  1032. package/dist/blocks/onboarding/solid/index.d.ts +0 -13
  1033. package/dist/blocks/onboarding/solid/index.js +0 -14
  1034. package/dist/blocks/onboarding/svelte/index.d.ts +0 -13
  1035. package/dist/blocks/onboarding/svelte/index.js +0 -14
  1036. package/dist/blocks/onboarding/vue/index.d.ts +0 -13
  1037. package/dist/blocks/onboarding/vue/index.js +0 -14
  1038. package/dist/blocks/organizations/react/index.d.ts +0 -127
  1039. package/dist/blocks/organizations/react/index.js +0 -113
  1040. package/dist/blocks/organizations/react/server.d.ts +0 -20
  1041. package/dist/blocks/organizations/react/server.js +0 -24
  1042. package/dist/blocks/organizations/solid/index.d.ts +0 -34
  1043. package/dist/blocks/organizations/solid/index.js +0 -32
  1044. package/dist/blocks/organizations/svelte/index.d.ts +0 -34
  1045. package/dist/blocks/organizations/svelte/index.js +0 -32
  1046. package/dist/blocks/organizations/vue/index.d.ts +0 -34
  1047. package/dist/blocks/organizations/vue/index.js +0 -32
  1048. package/dist/blocks/parties/index.d.ts +0 -190
  1049. package/dist/blocks/parties/index.js +0 -261
  1050. package/dist/blocks/reactions/react/index.d.ts +0 -33
  1051. package/dist/blocks/reactions/react/index.js +0 -50
  1052. package/dist/blocks/reactions/react/server.d.ts +0 -9
  1053. package/dist/blocks/reactions/react/server.js +0 -10
  1054. package/dist/blocks/reactions/solid/index.d.ts +0 -13
  1055. package/dist/blocks/reactions/solid/index.js +0 -14
  1056. package/dist/blocks/reactions/svelte/index.d.ts +0 -13
  1057. package/dist/blocks/reactions/svelte/index.js +0 -14
  1058. package/dist/blocks/reactions/vue/index.d.ts +0 -13
  1059. package/dist/blocks/reactions/vue/index.js +0 -14
  1060. package/dist/blocks/rules/index.d.ts +0 -171
  1061. package/dist/blocks/rules/index.js +0 -162
  1062. package/dist/blocks/rules/react/index.d.ts +0 -38
  1063. package/dist/blocks/rules/react/index.js +0 -59
  1064. package/dist/blocks/rules/react/server.d.ts +0 -6
  1065. package/dist/blocks/rules/react/server.js +0 -7
  1066. package/dist/blocks/rules/solid/index.d.ts +0 -13
  1067. package/dist/blocks/rules/solid/index.js +0 -14
  1068. package/dist/blocks/rules/svelte/index.d.ts +0 -13
  1069. package/dist/blocks/rules/svelte/index.js +0 -14
  1070. package/dist/blocks/rules/vue/index.d.ts +0 -13
  1071. package/dist/blocks/rules/vue/index.js +0 -14
  1072. package/dist/blocks/saved-views/react/index.d.ts +0 -40
  1073. package/dist/blocks/saved-views/react/index.js +0 -60
  1074. package/dist/blocks/saved-views/react/server.d.ts +0 -10
  1075. package/dist/blocks/saved-views/react/server.js +0 -10
  1076. package/dist/blocks/saved-views/solid/index.d.ts +0 -13
  1077. package/dist/blocks/saved-views/solid/index.js +0 -14
  1078. package/dist/blocks/saved-views/svelte/index.d.ts +0 -13
  1079. package/dist/blocks/saved-views/svelte/index.js +0 -14
  1080. package/dist/blocks/saved-views/vue/index.d.ts +0 -13
  1081. package/dist/blocks/saved-views/vue/index.js +0 -14
  1082. package/dist/blocks/search/react/index.d.ts +0 -39
  1083. package/dist/blocks/search/react/index.js +0 -49
  1084. package/dist/blocks/search/react/server.d.ts +0 -10
  1085. package/dist/blocks/search/react/server.js +0 -11
  1086. package/dist/blocks/search/solid/index.d.ts +0 -13
  1087. package/dist/blocks/search/solid/index.js +0 -14
  1088. package/dist/blocks/search/svelte/index.d.ts +0 -13
  1089. package/dist/blocks/search/svelte/index.js +0 -14
  1090. package/dist/blocks/search/vue/index.d.ts +0 -13
  1091. package/dist/blocks/search/vue/index.js +0 -14
  1092. package/dist/blocks/service-principals/index.d.ts +0 -132
  1093. package/dist/blocks/service-principals/index.js +0 -110
  1094. package/dist/blocks/sessions/react/index.d.ts +0 -36
  1095. package/dist/blocks/sessions/react/index.js +0 -53
  1096. package/dist/blocks/sessions/react/server.d.ts +0 -15
  1097. package/dist/blocks/sessions/react/server.js +0 -17
  1098. package/dist/blocks/sessions/solid/index.d.ts +0 -20
  1099. package/dist/blocks/sessions/solid/index.js +0 -23
  1100. package/dist/blocks/sessions/svelte/index.d.ts +0 -20
  1101. package/dist/blocks/sessions/svelte/index.js +0 -23
  1102. package/dist/blocks/sessions/vue/index.d.ts +0 -20
  1103. package/dist/blocks/sessions/vue/index.js +0 -23
  1104. package/dist/blocks/settings/react/index.d.ts +0 -31
  1105. package/dist/blocks/settings/react/index.js +0 -37
  1106. package/dist/blocks/settings/react/server.d.ts +0 -10
  1107. package/dist/blocks/settings/react/server.js +0 -10
  1108. package/dist/blocks/settings/solid/index.d.ts +0 -13
  1109. package/dist/blocks/settings/solid/index.js +0 -14
  1110. package/dist/blocks/settings/svelte/index.d.ts +0 -13
  1111. package/dist/blocks/settings/svelte/index.js +0 -14
  1112. package/dist/blocks/settings/vue/index.d.ts +0 -13
  1113. package/dist/blocks/settings/vue/index.js +0 -14
  1114. package/dist/blocks/shared-signals/index.d.ts +0 -440
  1115. package/dist/blocks/shared-signals/index.js +0 -886
  1116. package/dist/blocks/tags/react/index.d.ts +0 -36
  1117. package/dist/blocks/tags/react/index.js +0 -52
  1118. package/dist/blocks/tags/react/server.d.ts +0 -7
  1119. package/dist/blocks/tags/react/server.js +0 -7
  1120. package/dist/blocks/tags/solid/index.d.ts +0 -13
  1121. package/dist/blocks/tags/solid/index.js +0 -14
  1122. package/dist/blocks/tags/svelte/index.d.ts +0 -13
  1123. package/dist/blocks/tags/svelte/index.js +0 -14
  1124. package/dist/blocks/tags/vue/index.d.ts +0 -13
  1125. package/dist/blocks/tags/vue/index.js +0 -14
  1126. package/dist/blocks/targets/index.d.ts +0 -82
  1127. package/dist/blocks/targets/index.js +0 -73
  1128. package/dist/blocks/undo/react/index.d.ts +0 -17
  1129. package/dist/blocks/undo/react/index.js +0 -24
  1130. package/dist/blocks/undo/react/server.d.ts +0 -10
  1131. package/dist/blocks/undo/react/server.js +0 -10
  1132. package/dist/blocks/undo/solid/index.d.ts +0 -13
  1133. package/dist/blocks/undo/solid/index.js +0 -14
  1134. package/dist/blocks/undo/svelte/index.d.ts +0 -13
  1135. package/dist/blocks/undo/svelte/index.js +0 -14
  1136. package/dist/blocks/undo/vue/index.d.ts +0 -13
  1137. package/dist/blocks/undo/vue/index.js +0 -14
  1138. package/dist/blocks/usage/react/index.d.ts +0 -29
  1139. package/dist/blocks/usage/react/index.js +0 -38
  1140. package/dist/blocks/usage/react/server.d.ts +0 -7
  1141. package/dist/blocks/usage/react/server.js +0 -7
  1142. package/dist/blocks/usage/solid/index.d.ts +0 -13
  1143. package/dist/blocks/usage/solid/index.js +0 -14
  1144. package/dist/blocks/usage/svelte/index.d.ts +0 -13
  1145. package/dist/blocks/usage/svelte/index.js +0 -14
  1146. package/dist/blocks/usage/vue/index.d.ts +0 -13
  1147. package/dist/blocks/usage/vue/index.js +0 -14
  1148. package/dist/blocks/work-items/index.d.ts +0 -210
  1149. package/dist/blocks/work-items/index.js +0 -304
  1150. package/dist/blocks/workflow-builder/index.d.ts +0 -43
  1151. package/dist/blocks/workflow-builder/index.js +0 -33
  1152. package/dist/blocks/workflow-builder/react/index.d.ts +0 -32
  1153. package/dist/blocks/workflow-builder/react/index.js +0 -75
  1154. package/dist/blocks/workflow-builder/react/server.d.ts +0 -14
  1155. package/dist/blocks/workflow-builder/react/server.js +0 -17
  1156. package/dist/blocks/workflow-builder/solid/index.d.ts +0 -20
  1157. package/dist/blocks/workflow-builder/solid/index.js +0 -23
  1158. package/dist/blocks/workflow-builder/svelte/index.d.ts +0 -20
  1159. package/dist/blocks/workflow-builder/svelte/index.js +0 -23
  1160. package/dist/blocks/workflow-builder/vue/index.d.ts +0 -20
  1161. package/dist/blocks/workflow-builder/vue/index.js +0 -23
  1162. package/dist/blocks/workflows/react/index.d.ts +0 -39
  1163. package/dist/blocks/workflows/react/index.js +0 -65
  1164. package/dist/blocks/workflows/react/server.d.ts +0 -14
  1165. package/dist/blocks/workflows/react/server.js +0 -17
  1166. package/dist/blocks/workflows/solid/index.d.ts +0 -20
  1167. package/dist/blocks/workflows/solid/index.js +0 -23
  1168. package/dist/blocks/workflows/svelte/index.d.ts +0 -20
  1169. package/dist/blocks/workflows/svelte/index.js +0 -23
  1170. package/dist/blocks/workflows/vue/index.d.ts +0 -20
  1171. package/dist/blocks/workflows/vue/index.js +0 -23
  1172. package/dist/blocks-CR3vCYhj.js +0 -88
  1173. package/dist/bookings-DJflcGqs.js +0 -184
  1174. package/dist/bucket-DBay-ICp.d.ts +0 -667
  1175. package/dist/build-DxzJf79X.js +0 -1839
  1176. package/dist/build-UqJeGtsH.js +0 -703
  1177. package/dist/calendar-DvbYFkrh.js +0 -801
  1178. package/dist/chat-sdk/react/index.d.ts +0 -3
  1179. package/dist/chat-sdk/react/index.js +0 -2
  1180. package/dist/chat-sdk/react/server.d.ts +0 -4
  1181. package/dist/chat-sdk/react/server.js +0 -1
  1182. package/dist/claims-T8TWZ53E.js +0 -63
  1183. package/dist/cli/add-BRjL7LuG.js +0 -1
  1184. package/dist/cli/add-Cf0uxfZ2.js +0 -672
  1185. package/dist/cli/add-DPfdDRq7.js +0 -75
  1186. package/dist/cli/adopt-B28iUeGg.js +0 -1
  1187. package/dist/cli/adopt-C0VugVMu.js +0 -421
  1188. package/dist/cli/apply-BlxtZPxe.js +0 -329
  1189. package/dist/cli/blocks-CHQ7K8Yz.js +0 -1
  1190. package/dist/cli/blocks-D3tsq-mR.js +0 -124
  1191. package/dist/cli/cache-DdgzhoaL.js +0 -516
  1192. package/dist/cli/catalog-DwTUdJaQ.js +0 -188
  1193. package/dist/cli/codemod-Dz4R-tVY.js +0 -1127
  1194. package/dist/cli/command-33pu13LT.js +0 -963
  1195. package/dist/cli/completion-6M6Hr8TL.js +0 -211
  1196. package/dist/cli/completion-Bqu8yq0q.js +0 -1
  1197. package/dist/cli/completion-Ddr3yqd3.js +0 -30
  1198. package/dist/cli/config-2qlSTi2o.js +0 -1503
  1199. package/dist/cli/config-B31tIvAl.js +0 -42
  1200. package/dist/cli/config-DVksOjSr.js +0 -1
  1201. package/dist/cli/config-edit-D46H0UHp.js +0 -613
  1202. package/dist/cli/db-D8iId4R8.js +0 -473
  1203. package/dist/cli/design-edit-B2aBiarJ.js +0 -205
  1204. package/dist/cli/doctor-pIek4vIO.js +0 -3897
  1205. package/dist/cli/emit-CJKf47Bi.js +0 -1078
  1206. package/dist/cli/env-BrLZfqsL.js +0 -143
  1207. package/dist/cli/env-cbCEa79W.js +0 -1
  1208. package/dist/cli/files-C5JfVnj5.js +0 -972
  1209. package/dist/cli/gen-DquZzY5a.js +0 -307
  1210. package/dist/cli/init-Dd-UdrE0.js +0 -397
  1211. package/dist/cli/introspect-IMQCji2l.js +0 -51
  1212. package/dist/cli/keys-iJs_1WT8.js +0 -106
  1213. package/dist/cli/live-BfAO5YRX.js +0 -344
  1214. package/dist/cli/mcp-D8Az0FNn.js +0 -240
  1215. package/dist/cli/metadata-CTStQPfV.js +0 -144
  1216. package/dist/cli/migration-safety-Dg1lxomi.js +0 -305
  1217. package/dist/cli/module-migration-CbX-jRhV.js +0 -606
  1218. package/dist/cli/moved-CdNtCf4t.js +0 -161
  1219. package/dist/cli/names-D6LrQnOv.js +0 -6
  1220. package/dist/cli/parse-CRZLku1b.js +0 -182
  1221. package/dist/cli/preset-DEvwDc7U.js +0 -129
  1222. package/dist/cli/run-BXhJoghZ.js +0 -1602
  1223. package/dist/cli/run-DdxNRTr0.js +0 -63
  1224. package/dist/cli/run-dTu_PD4I.js +0 -711
  1225. package/dist/cli/scaffold-Bx3bcy2R.js +0 -901
  1226. package/dist/cli/scaffold-mbNAQE19.js +0 -1
  1227. package/dist/cli/seed-CLseQdXQ.js +0 -188
  1228. package/dist/cli/seed-DSHSUsYy.js +0 -1
  1229. package/dist/cli/shared-CAdIkTR7.js +0 -151
  1230. package/dist/cli/skills-Bz4INB8f.js +0 -121
  1231. package/dist/cli/snapshot-GfdB9Cjl.js +0 -350
  1232. package/dist/cli/source-args-Dy3j1-H-.js +0 -59
  1233. package/dist/cli/spec-BfEK0_04.js +0 -1
  1234. package/dist/cli/spec-DXTdodlO.js +0 -7534
  1235. package/dist/cli/sql-BUhpAvrW.js +0 -2
  1236. package/dist/cli/sql-CLbWk3Yb.js +0 -332
  1237. package/dist/cli/stack-_F_JhllX.js +0 -135
  1238. package/dist/cli/sync-BBzNO-v9.js +0 -91
  1239. package/dist/cli/typegen-F9GEjpvD.js +0 -502
  1240. package/dist/cli/typegen-version-BNBPI4Eu.js +0 -9
  1241. package/dist/cli/version-DiwcIKOL.js +0 -4
  1242. package/dist/cli/write-DZKk5rSF.js +0 -201
  1243. package/dist/client--tyFmxDT.js +0 -52
  1244. package/dist/client-0qFNkNhT.js +0 -1060
  1245. package/dist/client-2z1aHG57.d.ts +0 -475
  1246. package/dist/client-5IVhjmtl.d.ts +0 -74
  1247. package/dist/client-B3WBilR2.d.ts +0 -69
  1248. package/dist/client-B4JPQgf-.d.ts +0 -165
  1249. package/dist/client-BAYh7P2h.js +0 -211
  1250. package/dist/client-BKGVUgo7.js +0 -380
  1251. package/dist/client-BNaOcfeH.js +0 -58
  1252. package/dist/client-BOuaQA0h.js +0 -173
  1253. package/dist/client-BP-8vn9G.d.ts +0 -37
  1254. package/dist/client-BU5Yd0yO.d.ts +0 -103
  1255. package/dist/client-Bb_Pj9wL.js +0 -137
  1256. package/dist/client-BkH-Vh3s.d.ts +0 -112
  1257. package/dist/client-BsfsRHae.d.ts +0 -136
  1258. package/dist/client-Bta7yl1C.d.ts +0 -451
  1259. package/dist/client-BwKXlohx.js +0 -121
  1260. package/dist/client-C1zOmnS8.js +0 -89
  1261. package/dist/client-CMqO1gCn.d.ts +0 -64
  1262. package/dist/client-CPirbPPi.js +0 -85
  1263. package/dist/client-CQPB-VJx.d.ts +0 -110
  1264. package/dist/client-CXbP_p02.d.ts +0 -156
  1265. package/dist/client-C_S9avzV.d.ts +0 -420
  1266. package/dist/client-Cb1g45Hc.js +0 -99
  1267. package/dist/client-CebKlRU7.js +0 -157
  1268. package/dist/client-Ch-dDfTB.js +0 -437
  1269. package/dist/client-CuFD4o56.d.ts +0 -111
  1270. package/dist/client-D2y_CfhU.d.ts +0 -137
  1271. package/dist/client-DAcVF-YJ.js +0 -134
  1272. package/dist/client-DCuE1rFm.js +0 -441
  1273. package/dist/client-DD02cst6.d.ts +0 -245
  1274. package/dist/client-DD3EcytK.js +0 -65
  1275. package/dist/client-DEE57s9m.d.ts +0 -132
  1276. package/dist/client-DScMvaty.js +0 -38
  1277. package/dist/client-DVRFa59w.d.ts +0 -309
  1278. package/dist/client-DXb1Nmr3.js +0 -111
  1279. package/dist/client-DkI0oyR7.js +0 -76
  1280. package/dist/client-DkQcRKhm.js +0 -67
  1281. package/dist/client-DoXBX3vS.d.ts +0 -116
  1282. package/dist/client-Dsuy9wfA.d.ts +0 -59
  1283. package/dist/client-EQvEzUQg.d.ts +0 -276
  1284. package/dist/client-G7NGXZKe.d.ts +0 -43
  1285. package/dist/client-OhTiAol1.js +0 -213
  1286. package/dist/client-Wt19DaEb.js +0 -291
  1287. package/dist/client-Y-kaHabc.d.ts +0 -46
  1288. package/dist/client-YASsOslj.js +0 -321
  1289. package/dist/client-Y_HnZGF4.js +0 -51
  1290. package/dist/client-_mL998Gl.js +0 -129
  1291. package/dist/client-i_u1e0Kf.d.ts +0 -144
  1292. package/dist/client-kRvHKAKS.js +0 -60
  1293. package/dist/client-nieeGXdg.d.ts +0 -178
  1294. package/dist/client-pxXoQOdC.js +0 -40
  1295. package/dist/client-ysVOUgxM.d.ts +0 -54
  1296. package/dist/cloud-event-CidL9jiw.d.ts +0 -64
  1297. package/dist/codecs-Du_zE1xu.js +0 -358
  1298. package/dist/codecs-WHpM3Byy.d.ts +0 -221
  1299. package/dist/codegen-DGtOsqz9.d.ts +0 -1809
  1300. package/dist/comments-Ceqo9bwE.js +0 -230
  1301. package/dist/compiler-DCotQWcs.d.ts +0 -14
  1302. package/dist/compose-VBQL7zIO.js +0 -53
  1303. package/dist/composite-BgBzdYP-.d.ts +0 -82
  1304. package/dist/composite-BgSdL5tk.js +0 -130
  1305. package/dist/config-V6HMvce5.js +0 -3930
  1306. package/dist/config-xUf0dj0t.js +0 -562
  1307. package/dist/connector-CKcTPJRV.d.ts +0 -107
  1308. package/dist/connectors-CgWuPQks.d.ts +0 -147
  1309. package/dist/context-B8_p3MXo.js +0 -11
  1310. package/dist/context-C-bHOtoD.js +0 -5
  1311. package/dist/context-CCz6ZgyI.js +0 -19
  1312. package/dist/context-CMxVnPMY.js +0 -9
  1313. package/dist/context-CrojkPNC.js +0 -41
  1314. package/dist/context-DvBEhsXX.d.ts +0 -403
  1315. package/dist/controller-lWRcKqCl.d.ts +0 -52
  1316. package/dist/cron-CI1kP-t5.js +0 -148
  1317. package/dist/cron-route-BbVtwXZz.js +0 -24
  1318. package/dist/db-exception-CnWcYPxL.js +0 -15
  1319. package/dist/define-D0LSXmUP.d.ts +0 -1117
  1320. package/dist/define-DwZ_LQVO.js +0 -2011
  1321. package/dist/define-block-B6Gjs57G.js +0 -52
  1322. package/dist/define-block-MyM4lH42.d.ts +0 -281
  1323. package/dist/delegation-DeAJehLk.js +0 -146
  1324. package/dist/documents-BijGGIjr.d.ts +0 -99
  1325. package/dist/embeddings-DRGIc8ti.js +0 -128
  1326. package/dist/embeddings-Deusi99x.d.ts +0 -78
  1327. package/dist/ended-D_Twxl8v.d.ts +0 -76
  1328. package/dist/entitlements-CMGsO4gL.d.ts +0 -54
  1329. package/dist/entitlements-_tw0V3BY.js +0 -45
  1330. package/dist/envelope-BDShQQ7-.js +0 -90
  1331. package/dist/errors-Cq72lONT.js +0 -44
  1332. package/dist/errors-D7u24ZJH.js +0 -321
  1333. package/dist/escape-like-DuKupGUV.d.ts +0 -15
  1334. package/dist/eve-dWNqpd9Z.d.ts +0 -157
  1335. package/dist/event-calendar-E6AI-dgg.d.ts +0 -217
  1336. package/dist/events-DhGgwqQA.d.ts +0 -842
  1337. package/dist/executor-C29668fO.d.ts +0 -281
  1338. package/dist/executor-C5uyGttm.js +0 -207
  1339. package/dist/executor-Cy1nk4MZ.d.ts +0 -104
  1340. package/dist/executor-DK2F9Zis.js +0 -746
  1341. package/dist/executor-DXfgDsKd.d.ts +0 -37
  1342. package/dist/executor-D_viYkhf.d.ts +0 -179
  1343. package/dist/executor-QtVn0axs.js +0 -147
  1344. package/dist/expo-sqlite/index.d.ts +0 -61
  1345. package/dist/expo-sqlite/index.js +0 -110
  1346. package/dist/field-errors-CKEqR38q.d.ts +0 -119
  1347. package/dist/field-errors-DZVRWRkf.js +0 -13
  1348. package/dist/field-formats-BX2FqLle.d.ts +0 -39
  1349. package/dist/filter-evaluate-CRDXv0Dl.js +0 -217
  1350. package/dist/filter-evaluate-CSWhzYWN.d.ts +0 -37
  1351. package/dist/filter-query-C7qgRYYv.d.ts +0 -133
  1352. package/dist/filter-query-D4ZSwbcJ.js +0 -524
  1353. package/dist/flags-C--SBMT9.js +0 -279
  1354. package/dist/flags-CJFDNAvL.d.ts +0 -246
  1355. package/dist/form-definition-BjeElHsB.js +0 -311
  1356. package/dist/form-definition-DcVnFzl4.d.ts +0 -95
  1357. package/dist/format-AYHceC2m.d.ts +0 -79
  1358. package/dist/framework-BpEnOfAo.js +0 -57
  1359. package/dist/framework-CMeAuJts.d.ts +0 -41
  1360. package/dist/function-contract-4vfi93Y5.d.ts +0 -27
  1361. package/dist/gateway-Dar9C0uP.d.ts +0 -115
  1362. package/dist/graph-B4eubUQG.js +0 -365
  1363. package/dist/groups-CxonH3u5.js +0 -16
  1364. package/dist/http-AkLjqOuB.js +0 -28
  1365. package/dist/impersonation-DFfDVJHk.js +0 -62
  1366. package/dist/inbox-CLsIrPXs.d.ts +0 -417
  1367. package/dist/index-B5XcM3VY.d.ts +0 -230
  1368. package/dist/index-B5dVVZtW.d.ts +0 -142
  1369. package/dist/index-Brm8I8iZ.d.ts +0 -232
  1370. package/dist/index-C7-2ZS9f.d.ts +0 -127
  1371. package/dist/index-CE0p3st5.d.ts +0 -227
  1372. package/dist/index-CcpGT2GI.d.ts +0 -151
  1373. package/dist/index-CrAKHNJF.d.ts +0 -287
  1374. package/dist/index-DLjs7j_z.d.ts +0 -61
  1375. package/dist/index-DdZcWH6q.d.ts +0 -820
  1376. package/dist/index-Dh5ZFCtu.d.ts +0 -61
  1377. package/dist/index-PADpaDku.d.ts +0 -447
  1378. package/dist/index-eoAU_QaY.d.ts +0 -357
  1379. package/dist/invalidate-BLiBFbLy.d.ts +0 -19
  1380. package/dist/json-schema-Bu4YCRbZ.js +0 -551
  1381. package/dist/jwks-cache-DbgNmSgV.js +0 -49
  1382. package/dist/kit-BeXTrlZn.js +0 -127
  1383. package/dist/kit-LeQGMzLH.d.ts +0 -94
  1384. package/dist/knowledge-Bx_qehoZ.js +0 -272
  1385. package/dist/knowledge-CYdNJasZ.d.ts +0 -202
  1386. package/dist/list-query-CuS1w2Af.js +0 -559
  1387. package/dist/list-query-Dx5cAqG6.d.ts +0 -223
  1388. package/dist/live-CZtRMh62.js +0 -246
  1389. package/dist/messages-Bd5-5ZjP.js +0 -492
  1390. package/dist/methods-7u6vSgz_.js +0 -11
  1391. package/dist/mfa-Dt8AdWht.d.ts +0 -36
  1392. package/dist/model-Btl8X9cs.d.ts +0 -326
  1393. package/dist/model-messages-BWyXXA4s.js +0 -206
  1394. package/dist/money-codec-BdnIiFFX.js +0 -41
  1395. package/dist/onboarding-DjXV4rgO.js +0 -68
  1396. package/dist/openapi/index.d.ts +0 -4
  1397. package/dist/openapi/index.js +0 -36
  1398. package/dist/operations-Cc27HkTG.d.ts +0 -6
  1399. package/dist/options-B7EJaOX-.js +0 -5
  1400. package/dist/options-BAW2ZlZ8.js +0 -8
  1401. package/dist/options-Blg8lAkz.js +0 -9
  1402. package/dist/options-Bn4cu0gt.js +0 -5
  1403. package/dist/options-BnoJOwXK.js +0 -8
  1404. package/dist/options-C1HR4UZd.js +0 -13
  1405. package/dist/options-C2HesT4A.js +0 -9
  1406. package/dist/options-C3EzJATk.js +0 -10
  1407. package/dist/options-C3hZ1fbC.js +0 -10
  1408. package/dist/options-CAzVf-1T.js +0 -8
  1409. package/dist/options-CGKSUFUq.js +0 -5
  1410. package/dist/options-CGjGE8xS.js +0 -12
  1411. package/dist/options-CIf3xni7.d.ts +0 -52
  1412. package/dist/options-CUqQ9pm7.js +0 -9
  1413. package/dist/options-ChD15QxJ.js +0 -9
  1414. package/dist/options-CyD7OObW.js +0 -17
  1415. package/dist/options-D1blDihM.js +0 -9
  1416. package/dist/options-D2Gd6VfS.js +0 -8
  1417. package/dist/options-D7f2VKzj.js +0 -25
  1418. package/dist/options-DXqcJyOP.js +0 -10
  1419. package/dist/options-Dh4wu9Ks.js +0 -15
  1420. package/dist/options-Dmg1Da9P.js +0 -25
  1421. package/dist/options-DsUgamiy.js +0 -5
  1422. package/dist/options-HSv8R6h5.js +0 -13
  1423. package/dist/options-I8zVkB_y.js +0 -8
  1424. package/dist/options-MHhjOoWz.js +0 -5
  1425. package/dist/options-MLxMkd5B.js +0 -11
  1426. package/dist/options-OJ5zmgr8.js +0 -10
  1427. package/dist/options-QKP9FFcK.js +0 -8
  1428. package/dist/options-XgfTctbI.js +0 -5
  1429. package/dist/options-_9arpsX9.js +0 -11
  1430. package/dist/options-kLKILdIu.js +0 -10
  1431. package/dist/options-n22NK3PL.js +0 -8
  1432. package/dist/options-qU8vgxhO.js +0 -18
  1433. package/dist/options-vXMBEWSH.js +0 -5
  1434. package/dist/options-wFnK3qWl.js +0 -9
  1435. package/dist/options-xVAy3UsV.js +0 -9
  1436. package/dist/organizations-R98kfFwF.js +0 -401
  1437. package/dist/outbox-Bsk-HJnq.d.ts +0 -199
  1438. package/dist/overlay/index.d.ts +0 -249
  1439. package/dist/overlay/index.js +0 -561
  1440. package/dist/payments-6uuergov.js +0 -16
  1441. package/dist/plugin-D9bYbYli.js +0 -45
  1442. package/dist/plugin-DouW0Kvg.d.ts +0 -203
  1443. package/dist/plugins/actor/index.d.ts +0 -19
  1444. package/dist/plugins/actor/index.js +0 -57
  1445. package/dist/plugins/rules/index.d.ts +0 -96
  1446. package/dist/plugins/rules/index.js +0 -235
  1447. package/dist/plugins/soft-delete/index.d.ts +0 -39
  1448. package/dist/plugins/soft-delete/index.js +0 -90
  1449. package/dist/plugins/tenant/index.d.ts +0 -60
  1450. package/dist/plugins/tenant/index.js +0 -119
  1451. package/dist/plugins/timestamps/index.d.ts +0 -11
  1452. package/dist/plugins/timestamps/index.js +0 -47
  1453. package/dist/plugins/validation/index.d.ts +0 -24
  1454. package/dist/plugins/validation/index.js +0 -83
  1455. package/dist/pool-Bu8zXGzI.d.ts +0 -123
  1456. package/dist/pool-errors-DOZ3oDeS.js +0 -27
  1457. package/dist/ports-BltcWsHf.d.ts +0 -312
  1458. package/dist/ports-CM3aG3zn.js +0 -137
  1459. package/dist/ports-n3ZWSS_I.d.ts +0 -89
  1460. package/dist/postgrest-DGjrW4sK.d.ts +0 -53
  1461. package/dist/powersync/react/index.d.ts +0 -93
  1462. package/dist/powersync/react/index.js +0 -120
  1463. package/dist/preset-HNFqCWaD.js +0 -838
  1464. package/dist/problem-D0KDiYPk.js +0 -162
  1465. package/dist/problem-D_DEBfMn.d.ts +0 -79
  1466. package/dist/provider-BhIkKBOh.js +0 -57
  1467. package/dist/provider-Dmsbl8ug.d.ts +0 -112
  1468. package/dist/push-token-8lvtOEv4.d.ts +0 -24
  1469. package/dist/query-DgPYlU82.js +0 -311
  1470. package/dist/rank-result-C6vsw27N.js +0 -18
  1471. package/dist/rank-result-DHflCNwo.d.ts +0 -14
  1472. package/dist/rate-limit-B7N1ZGl6.d.ts +0 -50
  1473. package/dist/rate-limit-CXpPm4_l.js +0 -49
  1474. package/dist/rate-limit-CpTD_WHm.js +0 -328
  1475. package/dist/react-controller-CKSdtJ10.js +0 -43
  1476. package/dist/react-env-CodKZgPk.js +0 -16
  1477. package/dist/react-uvu_KcVY.js +0 -599
  1478. package/dist/read-set-DP6fm21F.js +0 -316
  1479. package/dist/refusal-BEyNVFy7.d.ts +0 -32
  1480. package/dist/refusal-Ce2p81VW.js +0 -64
  1481. package/dist/replicas-BvIL4_jL.js +0 -94
  1482. package/dist/resolve-D57oDWNr.d.ts +0 -289
  1483. package/dist/resolve-sy3_Gz0i.js +0 -892
  1484. package/dist/resource-BRRU-A7j.js +0 -91
  1485. package/dist/resource-BVvIiuX_.js +0 -469
  1486. package/dist/resource-DxQOPkZ5.d.ts +0 -115
  1487. package/dist/resource-execute-BbjuiXwD.d.ts +0 -15
  1488. package/dist/resource-types-NXau9LJE.d.ts +0 -153
  1489. package/dist/resources-CIi7sw1T.js +0 -64
  1490. package/dist/resources-CZ60Vqwq.d.ts +0 -7
  1491. package/dist/respond-DjEmE_DN.js +0 -100
  1492. package/dist/respond-QW-f1yio.d.ts +0 -101
  1493. package/dist/result-DIT_qp4G.js +0 -146
  1494. package/dist/result-mijqfoJN.d.ts +0 -346
  1495. package/dist/rrule-CesB9Rxd.js +0 -290
  1496. package/dist/rules-D75V7dAE.js +0 -286
  1497. package/dist/run-state-CPo-1X2B.d.ts +0 -12
  1498. package/dist/sales-lines-9HsX89aO.d.ts +0 -28
  1499. package/dist/sales-lines-DgXeOEar.js +0 -41
  1500. package/dist/sales-shared-CjXsy0NJ.js +0 -22
  1501. package/dist/sales-shared-DAAGwQwN.d.ts +0 -8
  1502. package/dist/search-9hoZ8rN1.js +0 -106
  1503. package/dist/server-types-D_cQ6OxZ.d.ts +0 -809
  1504. package/dist/session-DNRYXIOU.d.ts +0 -97
  1505. package/dist/settings-DzQtMwfl.js +0 -93
  1506. package/dist/shared-C80IHf4B.js +0 -60
  1507. package/dist/shared-D11SWNLx.js +0 -17
  1508. package/dist/sign-in-B_QJ2u62.js +0 -386
  1509. package/dist/signatures-CzqfLlfV.d.ts +0 -43
  1510. package/dist/signatures-D8IoN3Rw.js +0 -145
  1511. package/dist/signers-BuVQKDfK.d.ts +0 -390
  1512. package/dist/signing-keys-i2Rsll6L.d.ts +0 -119
  1513. package/dist/solid-controller-7TxGaJNy.js +0 -33
  1514. package/dist/solid-controller-BnF3ovwV.d.ts +0 -11
  1515. package/dist/spec-pins-CZ2CL8YZ.d.ts +0 -120
  1516. package/dist/spec-pins-D3Tm6HJi.js +0 -60
  1517. package/dist/sql-CQIikLnQ.js +0 -421
  1518. package/dist/sql-frpIs51D.js +0 -89308
  1519. package/dist/sso-C86TPpZr.js +0 -217
  1520. package/dist/sso-D_am-gBz.d.ts +0 -161
  1521. package/dist/standard-16AIDny5.d.ts +0 -7
  1522. package/dist/standard-l10kE0yS.js +0 -25
  1523. package/dist/stats-Bt1RyT-F.js +0 -1147
  1524. package/dist/store-DKqbuNvw.d.ts +0 -83
  1525. package/dist/stripe-BSEwKCni.d.ts +0 -140
  1526. package/dist/subject-readers-BJzh7WF7.js +0 -49
  1527. package/dist/subject-readers-Bbq1i0Ol.d.ts +0 -21
  1528. package/dist/subjects-DQ2SU4cA.d.ts +0 -77
  1529. package/dist/support-CWVTM2AB.js +0 -534
  1530. package/dist/support-view-BOJJSZKv.d.ts +0 -17
  1531. package/dist/svelte-controller-BmRhHvFz.js +0 -50
  1532. package/dist/svelte-controller-DXO7gsmS.d.ts +0 -13
  1533. package/dist/table-schema-xqSXrjCN.js +0 -284
  1534. package/dist/tags-C_ym1yDM.js +0 -39
  1535. package/dist/tags-Co-QIHeK.js +0 -84
  1536. package/dist/tags-tbiuHHMV.d.ts +0 -30
  1537. package/dist/tanstack-db/supabase/index.d.ts +0 -66
  1538. package/dist/tanstack-db/supabase/index.js +0 -271
  1539. package/dist/tax-BHSCb5Rw.d.ts +0 -280
  1540. package/dist/tax-keSd-q_1.js +0 -140
  1541. package/dist/tee-1fsH3bEz.js +0 -101
  1542. package/dist/template-DXsGm_R0.js +0 -126
  1543. package/dist/temporal-required-Dt4mcPsi.js +0 -20
  1544. package/dist/tenant-DnMqeNIM.js +0 -27
  1545. package/dist/tenant-l3H2ouv7.d.ts +0 -17
  1546. package/dist/tenant-naming-Bsj3Uvap.js +0 -27
  1547. package/dist/timing-C5NzaEgy.js +0 -95
  1548. package/dist/topic-CZvLXFND.d.ts +0 -415
  1549. package/dist/topic-Cr01EHZy.js +0 -643
  1550. package/dist/types-BTK75521.js +0 -8
  1551. package/dist/types-Bzm0OgJk.d.ts +0 -177
  1552. package/dist/types-CB5MvzuE.d.ts +0 -624
  1553. package/dist/types-DYXhFhmQ.d.ts +0 -251
  1554. package/dist/types-DmshMaXz.d.ts +0 -421
  1555. package/dist/types-EUrisY2U.d.ts +0 -217
  1556. package/dist/types-vVsLvOUJ.d.ts +0 -700
  1557. package/dist/undo-CClrTbm9.d.ts +0 -46
  1558. package/dist/undo-HHonvNeg.js +0 -44
  1559. package/dist/upload-claim-DCuklVyh.js +0 -33
  1560. package/dist/usage-BFwuH7GK.d.ts +0 -232
  1561. package/dist/usage-CJmmCGzd.js +0 -264
  1562. package/dist/user-change-CSR_prhd.js +0 -133
  1563. package/dist/variants-Bat9zU8l.js +0 -53
  1564. package/dist/vault-BQThkbws.d.ts +0 -62
  1565. package/dist/verify-BCmBaHlN.js +0 -130
  1566. package/dist/verify-BTAEsJbF.d.ts +0 -146
  1567. package/dist/view-CKzCp8FW.d.ts +0 -90
  1568. package/dist/view-CfUd5vI9.js +0 -108
  1569. package/dist/vue-controller-DCOtUOA_.d.ts +0 -11
  1570. package/dist/vue-controller-EzyTjqca.js +0 -32
  1571. package/dist/watch-CYeMXFtj.d.ts +0 -92
  1572. package/dist/watch-Cnd6XDnU.js +0 -139
  1573. package/dist/watch-topic-CloXeDTO.d.ts +0 -31
  1574. package/dist/wire-BJhepFbv.js +0 -66
  1575. package/dist/workflow-builder-DzZNlI45.js +0 -699
  1576. package/dist/workflow-sdk-BXwl1THQ.d.ts +0 -163
  1577. package/dist/workflow-sdk-CPGiFE1B.js +0 -242
  1578. package/dist/workflow-sdk-world-BOEsvWmx.js +0 -2123
  1579. package/dist/workflows-BHyYAR6c.d.ts +0 -258
  1580. package/dist/workflows-otOMN33L.js +0 -285
  1581. package/skills/better-supabase/references/plugins.md +0 -71
@@ -0,0 +1,4561 @@
1
+ import { a as list, p as commandMeta, r as defineCliCommand } from "./command-CQ61x6gN.js";
2
+ import { i as plain } from "./style-Bm2X5JrN.js";
3
+ import { t as VERSION } from "./version-Du4CgONY.js";
4
+ import { n as CliError, r as errorText } from "./errors-BTe6A15O.js";
5
+ import { i as isLocalDatabaseUrl, r as databaseHost } from "./agent-CbuPvI2x.js";
6
+ import { a as sameText, t as display } from "./io-DH1kcmmP.js";
7
+ import { t as CACHE_DIR } from "./cache-CXgR_Ahi.js";
8
+ import { t as byCodePoint } from "./compare-BmIWVEui.js";
9
+ import { C as satisfies, S as peerVersions, c as isSqliteLiteDriver, n as managementTarget, o as snapshotFile, r as openSource, s as liteDriverOf, t as loadSnapshot, u as MetadataRejectedError } from "./snapshot-C8MVPyO8.js";
10
+ import { d as configFor, r as renderFiles, u as notNullTag } from "./render-ucJmUyuM.js";
11
+ import { t as isRecord } from "./guards-CUlDNYZs.js";
12
+ import { i as request } from "./source-DANaJJVc.js";
13
+ import { A as providerLabel, S as moduleLayoutFor, b as blockDeclaredTables, i as migrationHolds, o as migrationModules, r as latestMigration, t as MODULES_SUFFIX, x as migrationsDir, y as compiledReadSets } from "./module-migration-aPCckev6.js";
14
+ import { l as stdinDatabaseUrl, n as databaseUrl } from "./config-9t5ec_N7.js";
15
+ import { a as findRepoRoot, c as httpHooks, d as readSupabaseToml, f as schemaPaths, h as tomlGet, i as diffEngine, l as migrationCommand, n as declarativeSchemasDir, p as sqlWorkflow, u as pgFunctionHooks } from "./supabase-toml-CncmQbO0.js";
16
+ import { t as PROCESS_ENV } from "./env-DMgjB_4y.js";
17
+ import { n as readStackStatus } from "./stack-status-B_Ipd_-z.js";
18
+ import { t as connect } from "./db-CQCLx7cx.js";
19
+ import { a as hasContentMapper, c as supportsContentMappers, i as contentMapperSetup$1, n as TSCONFIG_SNIPPET, o as needsRunExternalCode, t as RUN_EXTERNAL_CODE } from "./content-mapper-DNs7lTu7.js";
20
+ import { c as insideRoot, i as changeLines, r as applyPlan } from "./files-CX71yBm8.js";
21
+ import { S as splinterTables, _ as withoutStrings, a as METADATA_GAPS_TEXT, b as appTablesOf, c as accessModule, d as RLS_RULES, f as appliesTo, g as policyHelpers, h as functionObject, l as entitlementsModule, m as functionBody, p as escape$2, s as AUTHORIZATION_RULES, u as COMMANDS$2, v as BLOCK_PREFIX, x as advisorRule, y as blockTargets } from "./run-CvYVAztY.js";
22
+ import { a as permissionCatalogKeys, i as lineOf, l as tableObject, n as exposed, o as policyObject, r as exposedSchemas, s as qualified, t as catalogOf, u as workflowOf } from "./shared-LZroULBe.js";
23
+ import { o as wiringFor, r as configInstances, t as MIGRATION_SAFETY_RULES } from "./migration-safety-D7PhzFxm.js";
24
+ import { t as orphanedBlockFiles } from "./orphans-BAboqyVM.js";
25
+ import { i as reservedRoles, n as ident$1, r as literal$1, t as LIVE_RULES } from "./live-iEeH15ck.js";
26
+ import { a as sourceFlags, n as SOURCE_ARGS } from "./source-args-IpJRJO5t.js";
27
+ import { i as PLUGIN_SUBPATH_RENAMES, n as BLOCK_SUBPATH_RENAMES, o as SPEC_SUBPATH_RENAMES, t as BLOCK_EXPORT_RENAMES } from "./codemod-block-renames-BuSaTE_G.js";
28
+ import { n as PLATFORM_SCHEMAS } from "./in-stack-diff-DQTW53Vf.js";
29
+ import { i as staleVariables, r as stackEnvValues, t as PREFIX_NAMES } from "./stack-env-6hS-WEmh.js";
30
+ import { a as readSharedMemory, n as listStacks, t as branchName } from "./stacks-BYlQ96WS.js";
31
+ import { i as refuseWithMetadata, r as readMetadataSnapshot } from "./metadata-DVx9Zgbj.js";
32
+ import { existsSync } from "node:fs";
33
+ import { dirname, resolve } from "node:path";
34
+ import { createHash } from "node:crypto";
35
+ import { glob, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
36
+ import { DEFAULT_ACTIVE_TENANT, tenantClaimPaths } from "better-supabase/config";
37
+ import { SECRET_COLUMNS, SQL_MODULES, TENANT_SCOPE_POLICY, adoptedTables, blockFileState, blockSqlVersion, contractSignature, customContracts, dataExportPlan, excludedColumns, expiredFlagsQuery, hasColumnAccess, migrationOptionUses, moduleDeprecations, moduleEventTriggers, moduleFileVersion, moduleLayout, notNullCheckName, renderBlocks, sameBlockFile, sameTenantColumn, sameTenantTrigger, scopePolicyName, sharedRolesProblems, subjectColumns, widenedCommands } from "better-supabase/sql";
38
+ import { defineBucket, parseSize } from "better-supabase/storage";
39
+ //#region src/cli/doctor/env.ts
40
+ const PUBLIC_PREFIXES = [
41
+ "NEXT_PUBLIC_",
42
+ "VITE_",
43
+ "PUBLIC_",
44
+ "EXPO_PUBLIC_",
45
+ "NUXT_PUBLIC_"
46
+ ];
47
+ function envEntries(file) {
48
+ return file.text.split("\n").flatMap((raw, index) => {
49
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(raw);
50
+ if (!match) return [];
51
+ const value = match[2].trim().replace(/^(['"])(.*)\1$/, "$2");
52
+ return [{
53
+ key: match[1],
54
+ value,
55
+ line: index + 1
56
+ }];
57
+ });
58
+ }
59
+ const PREFIXES = [...new Set(Object.values(PREFIX_NAMES).filter((prefix) => prefix !== ""))];
60
+ /**
61
+ * The names `stackEnvValues` writes, with or without a browser prefix; not the
62
+ * CLI's own `SUPABASE_BIN` or `SUPABASE_ACCESS_TOKEN`.
63
+ */
64
+ const STACK_VARIABLE = /(?:^|_)SUPABASE_(?:URL|PUBLISHABLE_KEY|SECRET_KEY|JWKS_URL|DB_URL|JWT_SECRET)$/;
65
+ const exportsStackVariable = (shell) => Object.entries(shell).some(([name, value]) => value !== void 0 && value !== "" && STACK_VARIABLE.test(name));
66
+ /**
67
+ * Reads the running stack only when the shell exports a `SUPABASE_*` variable,
68
+ * so a doctor run without one never starts `supabase status`.
69
+ */
70
+ async function loadStackEnvCheck(root, env, signal) {
71
+ const shell = env[PROCESS_ENV] ?? {};
72
+ if (!exportsStackVariable(shell)) return { skipped: "No SUPABASE_* variable is exported." };
73
+ try {
74
+ const result = await readStackStatus(root, env, { signal });
75
+ if (!result.ok) return { skipped: "The local stack is not running." };
76
+ const values = { ...stackEnvValues(result.status, "") };
77
+ if (values["SUPABASE_URL"] === void 0) return { skipped: "supabase status has no API_URL." };
78
+ for (const prefix of PREFIXES) {
79
+ values[`${prefix}SUPABASE_URL`] = values["SUPABASE_URL"];
80
+ if (values["SUPABASE_PUBLISHABLE_KEY"] !== void 0) values[`${prefix}SUPABASE_PUBLISHABLE_KEY`] = values["SUPABASE_PUBLISHABLE_KEY"];
81
+ }
82
+ return {
83
+ shell,
84
+ values
85
+ };
86
+ } catch (cause) {
87
+ return { skipped: errorText(cause) };
88
+ }
89
+ }
90
+ /** Checks on env files and the variables the shell exports (BS501 to BS503). */
91
+ const ENV_RULES = [
92
+ {
93
+ code: "BS501",
94
+ severity: "error",
95
+ title: "Secret in a browser variable",
96
+ description: "Variables with a public prefix are bundled into client code. A secret key there gives every visitor full access.",
97
+ check: (context) => context.envFiles.flatMap((file) => envEntries(file).filter((entry) => PUBLIC_PREFIXES.some((prefix) => entry.key.startsWith(prefix)) && (entry.value.startsWith("sb_secret_") || /SECRET|SERVICE_ROLE/.test(entry.key))).map((entry) => ({
98
+ message: `${entry.key} in ${file.path} is exposed to the browser but holds a secret. Rename it without the public prefix and rotate the key.`,
99
+ target: `${file.path}:${entry.key}`,
100
+ location: {
101
+ file: file.path,
102
+ line: entry.line
103
+ }
104
+ })))
105
+ },
106
+ {
107
+ code: "BS502",
108
+ severity: "warning",
109
+ title: "Env file with secrets is not ignored by git",
110
+ description: "Env files holding a secret key or database password should never be committed.",
111
+ check: (context) => {
112
+ const patterns = context.gitignore.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
113
+ const ignored = (path) => patterns.some((pattern) => {
114
+ const source = pattern.replace(/^\//, "").replaceAll(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
115
+ return new RegExp(`^${source}$`).test(path) || new RegExp(`(^|/)${source}$`).test(path);
116
+ });
117
+ return context.envFiles.flatMap((file) => {
118
+ const secret = envEntries(file).find((entry) => entry.value.startsWith("sb_secret_") || /SECRET_KEY|SERVICE_ROLE|DB_URL|DATABASE_URL/.test(entry.key));
119
+ if (!secret || ignored(file.path) || file.path.endsWith(".example")) return [];
120
+ return [{
121
+ message: `${file.path} contains ${secret.key} but isn't in .gitignore.`,
122
+ target: file.path,
123
+ location: {
124
+ file: file.path,
125
+ line: secret.line
126
+ }
127
+ }];
128
+ });
129
+ }
130
+ },
131
+ {
132
+ code: "BS503",
133
+ severity: "warning",
134
+ title: "Exported Supabase variable differs from the running stack",
135
+ description: "A SUPABASE_* variable exported in the shell has another value than the local stack, so the app talks to a stale stack (often another worktree's port). Values are redacted: host and port for URLs, a SHA-256 fingerprint for keys. Unset the variable, or start the app with `better-supabase dev run -- <cmd>`, which overrides it.",
136
+ check: ({ stackEnv }) => stackEnv === void 0 || "skipped" in stackEnv ? [] : staleVariables(stackEnv.shell, stackEnv.values).map((stale) => ({
137
+ target: stale.variable,
138
+ message: `${stale.message} Unset it or start the app with better-supabase dev run -- <cmd>.`
139
+ }))
140
+ }
141
+ ];
142
+ //#endregion
143
+ //#region src/cli/doctor/adoption.ts
144
+ const tagText = (notNull) => notNull === true ? "@notNull" : `@notNull(${notNull.join(", ")})`;
145
+ const sameNotNull = (a, b) => a === true || b === true ? a === b : a !== void 0 && a.length === b.length && a.every((name) => b.includes(name));
146
+ /**
147
+ * Checks for adopted schemas: config the SQL could carry, and references and
148
+ * exports the modules leave open.
149
+ */
150
+ const ADOPTION_RULES = [
151
+ {
152
+ code: "BS338",
153
+ severity: "info",
154
+ title: "gen.functions.notNull entry a comment tag could replace",
155
+ description: "`gen` reads `@notNull` and `@notNull(a, b)` from a function's comment. A `gen.functions.<name>.notNull` entry for a function in the read schemas can move to the comment, next to the SQL it describes. A config entry wins over the tag.",
156
+ check: (context) => {
157
+ const { config } = context;
158
+ return catalogOf(context).functions.flatMap((fn) => {
159
+ const notNull = configFor(config.functions, fn.schema, fn.name)?.notNull;
160
+ if (notNull === void 0 || !config.schemas.includes(fn.schema)) return [];
161
+ const name = `${fn.schema}.${fn.name}`;
162
+ const tag = notNullTag(fn.comment);
163
+ const wanted = tagText(notNull);
164
+ let message;
165
+ if (sameNotNull(tag, notNull)) message = `functions.${fn.name}.notNull matches the ${wanted} tag on ${name}(${fn.signature}). Remove the config entry.`;
166
+ else if (tag === void 0) message = `functions.${fn.name}.notNull can move to the function: add \`${wanted}\` to the comment on ${name}(${fn.signature}) (\`comment on function ${name}(${fn.signature}) is '${wanted}';\`), then remove the config entry.`;
167
+ else message = `functions.${fn.name}.notNull (${wanted}) differs from the ${tagText(tag)} tag on ${name}(${fn.signature}), and the config entry wins. Make them agree, then remove the config entry.`;
168
+ return [{
169
+ message,
170
+ target: `${name}(${fn.signature})`
171
+ }];
172
+ });
173
+ }
174
+ },
175
+ {
176
+ code: "BS339",
177
+ severity: "warning",
178
+ title: "expose excludes a column the table doesn't have",
179
+ description: "`upgrade` turns `select(*, -col)` in `expose` into the table's declared columns minus the excluded ones. An excluded column the table no longer has excludes nothing, and a renamed secret column is then granted under its new name. Remove the exclusion, or exclude the new name.",
180
+ check: (context) => {
181
+ const tables = new Map(catalogOf(context).tables.map((table) => [qualified(table), table]));
182
+ return Object.entries(context.config.expose).flatMap(([key, roles]) => {
183
+ const table = tables.get(key.includes(".") ? key : `public.${key}`);
184
+ if (table === void 0) return [];
185
+ const columns = new Set(table.columns.map((column) => column.name));
186
+ return [...new Set([
187
+ ...roles.anon,
188
+ ...roles.authenticated,
189
+ ...roles.serviceRole
190
+ ].flatMap((privilege) => excludedColumns(privilege)).filter((column) => !columns.has(column)))].map((column) => ({
191
+ message: `expose.${key} excludes "${column}", which ${qualified(table)} doesn't have, so it excludes nothing. Remove "-${column}", or exclude the column's new name.`,
192
+ target: `${qualified(table)}.${column}`
193
+ }));
194
+ });
195
+ }
196
+ },
197
+ {
198
+ code: "BS340",
199
+ severity: "info",
200
+ title: "Tenant foreign key without a same-tenant trigger",
201
+ description: "A foreign key checks that the referenced row exists, not that it belongs to the row's tenant. Doctor lists each single-column key between two tables that both have the tenant column, when the key doesn't include that column and the table has no `bsb_same_tenant_<column>` trigger. List the reference in `blocks.memberships.options.sameTenant`, or set `sameTenant: { auto: true }` to derive every one from the schema files.",
202
+ check: (context) => {
203
+ if (!context.config.blockNames.includes("memberships")) return [];
204
+ const tenant = sameTenantColumn(moduleLayout(context.config));
205
+ const { tables } = catalogOf(context);
206
+ const tenantTables = new Set(tables.filter((table) => table.columns.some((column) => column.name === tenant)).map(qualified));
207
+ return tables.filter((table) => context.config.schemas.includes(table.schema) && tenantTables.has(qualified(table))).flatMap((table) => {
208
+ const triggers = new Set(table.triggers.map((trigger) => trigger.name));
209
+ return table.foreignKeys.flatMap((key) => {
210
+ const [column] = key.columns;
211
+ if (column === void 0 || key.columns.length !== 1 || column === tenant || !tenantTables.has(`${key.refSchema}.${key.refTable}`) || triggers.has(sameTenantTrigger(column))) return [];
212
+ return [{
213
+ message: `${qualified(table)}.${column} references ${key.refSchema}.${key.refTable}, and both have ${tenant}, but no trigger checks that the referenced row has the same ${tenant}. Add it to blocks.memberships.options.sameTenant, or set sameTenant: { auto: true }.`,
214
+ target: `${qualified(table)}.${column}`
215
+ }];
216
+ });
217
+ });
218
+ }
219
+ },
220
+ {
221
+ code: "BS342",
222
+ severity: "error",
223
+ title: "Enum column of the app's that a module writes as text",
224
+ description: "A module writes a text parameter or computed text value into a column of the app's table that the snapshot declares as an enum; string literals are fine. Postgres has no implicit cast from text to an enum, so the module's functions fail when they run. Columns the module casts to their declared type, such as the tags `kind` and the numbering `entity`, may be enums.",
225
+ check: (context) => {
226
+ const { tables } = catalogOf(context);
227
+ const ownTables = Object.fromEntries(tables.map((table) => [qualified(table), "database"]));
228
+ return adoptedTables(context.config.blocks, ownTables).flatMap((adopted) => {
229
+ const table = tables.find((entry) => entry.schema === adopted.schema && entry.name === adopted.table);
230
+ if (table === void 0) return [];
231
+ return adopted.textColumns.flatMap((logical) => {
232
+ const name = adopted.columns[logical];
233
+ const column = table.columns.find((entry) => entry.name === name);
234
+ if (column?.isEnum !== true || column.isArray) return [];
235
+ const target = `${qualified(table)}.${column.name}`;
236
+ return [{
237
+ message: `blocks.${adopted.module} writes text into ${target} (${adopted.logical}.${logical}), which is the enum ${column.typeSchema}.${column.udt}. Postgres does not cast text to an enum, so the module's functions fail. Change the column to text with a check constraint on its values.`,
238
+ target
239
+ }];
240
+ });
241
+ });
242
+ }
243
+ },
244
+ {
245
+ code: "BS344",
246
+ severity: "warning",
247
+ title: "App webhook endpoints without a format",
248
+ description: "A `webhooks-out` endpoints table of the app's that maps no `format` column sends every endpoint in `blocks.webhooks-out.options.defaultFormat`. Its default changed from the 0.8 body (`standard`) to `cloudevents` in 0.9, so receivers built for the old body break without any change to the config. `upgrade` refuses this config; doctor reports it for configs it loads without that check. Set `defaultFormat` explicitly, or add a format column and map it in `columns.endpoints.format`.",
249
+ check: (context) => {
250
+ const block = context.config.blocks["webhooks-out"];
251
+ if (block === void 0 || block.options?.["defaultFormat"] !== void 0 || typeof block.columns?.["endpoints"]?.["format"] === "string") return [];
252
+ const adopted = adoptedTables(context.config.blocks, appTablesOf(context)).find((entry) => entry.module === "webhooks-out" && entry.logical === "endpoints");
253
+ if (adopted === void 0) return [];
254
+ const table = catalogOf(context).tables.find((entry) => entry.schema === adopted.schema && entry.name === adopted.table);
255
+ return [{
256
+ message: `${`${adopted.schema}.${adopted.table}`} ${table?.columns.some((column) => column.name === "format") ? "has a format column the config doesn't map" : "has no format column"}, so every endpoint gets blocks.webhooks-out.options.defaultFormat, which defaults to "cloudevents" since 0.9 ("standard" before). Set defaultFormat to the body your receivers expect, or map columns.endpoints.format.`,
257
+ target: "blocks.webhooks-out.options.defaultFormat"
258
+ }];
259
+ }
260
+ },
261
+ {
262
+ code: "BS341",
263
+ severity: "warning",
264
+ title: "Data export includes a column that looks like a secret",
265
+ description: "A data export writes every column of each table it reads, except the ones in `omit`. Doctor reports columns of the exported tables, including the ones `autoTables` finds, whose name matches `token`, `secret`, `_hash`, `password`, `api_key` or `license_key` and that no `omit` and no `omitPattern` leaves out. Add the column to the table's `omit`, keep the table out of exports with `export: false` or `autoTables.exclude`, or set `blocks.data-lifecycle.options.omitPattern`.",
266
+ check: (context) => {
267
+ const plan = dataExportPlan(moduleLayout(context.config), context.config.blockNames);
268
+ if (plan === void 0) return [];
269
+ const secret = new RegExp(SECRET_COLUMNS, "i");
270
+ const omitted = patternOf(plan.omitPattern);
271
+ const tables = catalogOf(context).tables;
272
+ const byName = new Map(tables.map((table) => [qualified(table), table]));
273
+ const exported = [...plan.tables.map((entry) => ({
274
+ table: byName.get(entry.name),
275
+ omit: entry.omit
276
+ })), ...autoExported(plan, tables)];
277
+ const findings = /* @__PURE__ */ new Map();
278
+ for (const { table, omit } of exported) {
279
+ if (table === void 0) continue;
280
+ for (const column of table.columns) {
281
+ const target = `${qualified(table)}.${column.name}`;
282
+ if (!secret.test(column.name) || omit.includes(column.name) || omitted?.test(column.name) === true || findings.has(target)) continue;
283
+ findings.set(target, {
284
+ message: `Data exports of ${qualified(table)} include ${column.name}, which looks like a secret. Add it to the table's omit, keep the table out of exports, or set blocks.data-lifecycle.options.omitPattern.`,
285
+ target
286
+ });
287
+ }
288
+ }
289
+ return [...findings.values()];
290
+ }
291
+ }
292
+ ];
293
+ const NO_OMIT = [];
294
+ /** The tables `autoTables` adds to exports, which omit nothing. */
295
+ function autoExported(plan, tables) {
296
+ const { auto } = plan;
297
+ if (auto === void 0) return [];
298
+ return tables.filter((table) => auto.schemas.includes(table.schema) && !auto.excluded(qualified(table)) && table.columns.some((column) => auto.columns.includes(column.name))).map((table) => ({
299
+ table,
300
+ omit: NO_OMIT
301
+ }));
302
+ }
303
+ /** `omitPattern` as a JavaScript regex; undefined when off or not valid there. */
304
+ function patternOf(pattern) {
305
+ if (pattern === void 0) return;
306
+ try {
307
+ return new RegExp(pattern, "i");
308
+ } catch {
309
+ return;
310
+ }
311
+ }
312
+ //#endregion
313
+ //#region src/cli/doctor/app-table-scope.ts
314
+ const SCHEMA = "public";
315
+ const COMMANDS$1 = [
316
+ "select",
317
+ "insert",
318
+ "update",
319
+ "delete"
320
+ ];
321
+ /**
322
+ * The app tables whose table the app's own schema creates, so their entries
323
+ * enrich it. Without `sqlFiles`, none.
324
+ */
325
+ function enrichedAppTables(context) {
326
+ if (context.sqlFiles === void 0) return /* @__PURE__ */ new Set();
327
+ const created = blockDeclaredTables(context.sqlFiles, context.config.schemas);
328
+ return new Set(Object.keys(context.config.appTables).filter((name) => !created.has(`${SCHEMA}.${name}`)));
329
+ }
330
+ /** The app tables in `tenant` or `user` scope. */
331
+ function scopedAppTables(context) {
332
+ const enriched = enrichedAppTables(context);
333
+ return Object.entries(context.config.appTables).flatMap(([name, config]) => {
334
+ const scope = config.scope === void 0 || config.scope === "inherit" ? context.config.sql.scope ?? "tenant" : config.scope;
335
+ if (scope === "none") return [];
336
+ return [{
337
+ name,
338
+ scope,
339
+ column: scope === "user" ? "user_id" : context.config.extensions.tenant?.column ?? "organization_id",
340
+ enriched: enriched.has(name)
341
+ }];
342
+ });
343
+ }
344
+ const WRITES = [
345
+ "INSERT",
346
+ "UPDATE",
347
+ "DELETE"
348
+ ];
349
+ const ROW_PRIVILEGES = /* @__PURE__ */ new Set(["SELECT", ...WRITES]);
350
+ /**
351
+ * The grants an enriched app table holds beyond what its scope grants, for anon
352
+ * and authenticated. Truncate, references and trigger are BS111's.
353
+ */
354
+ function broaderGrants(context, name) {
355
+ const config = context.config.appTables[name];
356
+ const table = catalogOf(context).tables.find((entry) => entry.schema === SCHEMA && entry.name === name);
357
+ if (config === void 0 || table === void 0 || context.config.expose[`${SCHEMA}.${name}`] !== void 0 || context.config.expose[name] !== void 0) return {
358
+ grants: [],
359
+ columns: false
360
+ };
361
+ const scope = config.scope === void 0 || config.scope === "inherit" ? context.config.sql.scope ?? "tenant" : config.scope;
362
+ const columns = hasColumnAccess(config);
363
+ const allowed = /* @__PURE__ */ new Set([...columns ? [] : ["SELECT"], ...scope === "none" ? [] : columns ? ["DELETE"] : WRITES]);
364
+ return {
365
+ grants: table.grants.flatMap((grant) => grant.role === "anon" || grant.role === "authenticated" ? grant.privileges.filter((privilege) => ROW_PRIVILEGES.has(privilege) && (grant.role === "anon" || !allowed.has(privilege))).map((privilege) => `${privilege.toLowerCase()} to ${grant.role}`) : []),
366
+ columns
367
+ };
368
+ }
369
+ /** The `<table>_<column>_not_null` check an enriched table's scope column gets. */
370
+ const scopeCheckName = (table) => notNullCheckName(`${ident$1(SCHEMA)}.${ident$1(table.name)}`, ident$1(table.column));
371
+ const APP_TABLE_SCOPE_RULES = [{
372
+ code: "BS237",
373
+ severity: "warning",
374
+ title: "App table keeps permissive policies or grants of its own",
375
+ description: "An app table in `tenant` or `user` scope has permissive policies next to the ones its scope renders, usually the app's own policies on a table that a `tables.<name>` entry enriches. Postgres ORs permissive policies, so each one widens what the scope allows. In `tenant` scope the restrictive `bsb_tenant_scope` policy still keeps rows inside the member's tenants, so this is a hint (info) to check the roles and permissions those policies skip. In `user` scope, or without `bsb_tenant_scope`, nothing narrows them: a signed-in user reaches every row they allow. Drop the app's policies, or make them restrictive. Enriching never revokes the app's grants either, so the rule also lists the grants anon and authenticated hold on an enriched table beyond what its scope grants: a hint (info) while row level security filters the rows, and a warning when the entry restricts columns (`access` or `encrypted`), since a table-level grant reaches every column.",
376
+ check: (context) => {
377
+ const { tables } = catalogOf(context);
378
+ const grants = [...enrichedAppTables(context)].flatMap((name) => {
379
+ const broader = broaderGrants(context, name);
380
+ if (broader.grants.length === 0) return [];
381
+ const label = `${SCHEMA}.${name}`;
382
+ return [{
383
+ message: broader.columns ? `${label} keeps the app's table-level grants (${broader.grants.join(", ")}), which reach the columns tables.${name} restricts. Revoke them in the app's schema; the column grants stay.` : `${label} keeps the app's grants beyond its scope (${broader.grants.join(", ")}). Row level security still filters the rows; revoke them in the app's schema if nothing needs them.`,
384
+ ...broader.columns ? {} : { severity: "info" },
385
+ target: label,
386
+ object: {
387
+ kind: "table",
388
+ schema: SCHEMA,
389
+ name
390
+ }
391
+ }];
392
+ });
393
+ return [...scopedAppTables(context).flatMap((app) => {
394
+ const table = tables.find((entry) => entry.schema === SCHEMA && entry.name === app.name);
395
+ if (table === void 0) return [];
396
+ const own = new Set(COMMANDS$1.map((command) => scopePolicyName({
397
+ name: app.name,
398
+ enrich: app.enriched
399
+ }, command)));
400
+ const extra = table.policies.filter((policy) => policy.permissive && !own.has(policy.name));
401
+ if (extra.length === 0) return [];
402
+ const narrowed = app.scope === "tenant" && table.policies.some((policy) => !policy.permissive && policy.name === TENANT_SCOPE_POLICY);
403
+ const names = extra.map((policy) => policy.name).join(", ");
404
+ const label = `${SCHEMA}.${app.name}`;
405
+ return [{
406
+ message: narrowed ? `${label} has permissive policies of its own (${names}). ${TENANT_SCOPE_POLICY} keeps rows inside the member's tenants, but within a tenant these widen what tables.${app.name} allows. Check that they need no role or permission.` : `${label} has permissive policies of its own (${names}), and nothing narrows them to the ${app.scope === "user" ? "signed-in user's rows" : "member's tenants"}. Drop them, or make them restrictive.`,
407
+ ...narrowed ? { severity: "info" } : {},
408
+ target: label,
409
+ object: {
410
+ kind: "table",
411
+ schema: SCHEMA,
412
+ name: app.name
413
+ }
414
+ }];
415
+ }), ...grants];
416
+ }
417
+ }, {
418
+ code: "BS238",
419
+ severity: "warning",
420
+ title: "Rows of an enriched app table without an owner",
421
+ description: "A `tables.<name>` entry enriched a table the app already has, and some of its rows have no value in the scope column (`organization_id`, or `user_id` in `user` scope). The scope's policies match no member or user for them, so nobody but the service role reads or writes those rows, and updates to them fail the scope column's `not valid` check. Fill the column with `better-supabase db backfill`, then `doctor --fix` validates the check (BS236). Needs a database; saved snapshots skip it.",
422
+ async check(context) {
423
+ const db = context.database;
424
+ if (!db || "skipped" in db) return [];
425
+ const { tables } = catalogOf(context);
426
+ const findings = [];
427
+ for (const app of scopedAppTables(context)) {
428
+ const table = tables.find((entry) => entry.schema === SCHEMA && entry.name === app.name);
429
+ if (!app.enriched || !table?.columns.some((column) => column.name === app.column)) continue;
430
+ const label = `${SCHEMA}.${app.name}`;
431
+ const object = {
432
+ kind: "table",
433
+ schema: SCHEMA,
434
+ name: app.name
435
+ };
436
+ let missing;
437
+ try {
438
+ const [row] = await db.query(`select count(*) as missing from ${ident$1(SCHEMA)}.${ident$1(app.name)} where ${ident$1(app.column)} is null`);
439
+ missing = Number(row?.missing ?? 0);
440
+ } catch (cause) {
441
+ findings.push({
442
+ message: `Could not count the rows of ${label} without ${app.column}: ${errorText(cause)}`,
443
+ target: `${label}.${app.column}`,
444
+ object
445
+ });
446
+ continue;
447
+ }
448
+ if (missing === 0) continue;
449
+ findings.push({
450
+ message: `${label} has ${missing} ${missing === 1 ? "row" : "rows"} without ${app.column}, which no ${app.scope === "user" ? "user" : "member"} reads or writes, and updates to them fail ${scopeCheckName(app)}. Fill it with \`better-supabase db backfill ${app.name}.${app.column} --from <table.column> via <column>\` (or --value), then run \`better-supabase doctor --fix\`.`,
451
+ target: `${label}.${app.column}`,
452
+ object
453
+ });
454
+ }
455
+ return findings;
456
+ }
457
+ }];
458
+ //#endregion
459
+ //#region src/cli/doctor/cookies.ts
460
+ const TOKENS_ONLY = /\bencode\s*:\s*["'`]tokens-only["'`]/;
461
+ /**
462
+ * Imports that create the browser client, which writes the session cookie in
463
+ * the browser.
464
+ */
465
+ const BROWSER_SIDE = /from\s+["']better-supabase\/client["']|\bcreateBrowserClient\s*\(/;
466
+ /** Imports that read or refresh the session cookie on the server. */
467
+ const SERVER_SIDE = /from\s+["']better-supabase\/(?:server|next|ssr|hono|orpc|edge|expo)["']|\bcreateServerClient\s*\(/;
468
+ function cookieSide(file) {
469
+ const browser = BROWSER_SIDE.test(file.text);
470
+ if (browser === SERVER_SIDE.test(file.text)) return;
471
+ return browser ? "browser" : "server";
472
+ }
473
+ /**
474
+ * Files on one side of the session cookie that set `encode: 'tokens-only'`
475
+ * while files on the other side don't (BS412).
476
+ */
477
+ function encodingDrift(context) {
478
+ const sides = context.sources.flatMap((file) => {
479
+ const side = cookieSide(file);
480
+ return side ? [{
481
+ file,
482
+ side,
483
+ tokensOnly: TOKENS_ONLY.test(file.text)
484
+ }] : [];
485
+ });
486
+ const tokensOnly = sides.filter((entry) => entry.tokensOnly);
487
+ if (tokensOnly.length === 0) return [];
488
+ const findings = [];
489
+ for (const side of ["browser", "server"]) {
490
+ const set = tokensOnly.find((entry) => entry.side === side);
491
+ if (!set) continue;
492
+ const other = side === "browser" ? "server" : "browser";
493
+ const unset = sides.filter((entry) => entry.side === other && !entry.tokensOnly);
494
+ if (unset.length === 0) continue;
495
+ const line = lineOf(set.file.text, TOKENS_ONLY) ?? 1;
496
+ findings.push({
497
+ 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.`,
498
+ target: `${set.file.path}:encode`,
499
+ location: {
500
+ file: set.file.path,
501
+ line
502
+ }
503
+ });
504
+ }
505
+ return findings;
506
+ }
507
+ const COOKIE_RULES = [{
508
+ code: "BS412",
509
+ severity: "info",
510
+ title: "Session cookie encoding differs between server and browser",
511
+ 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.",
512
+ check: encodingDrift
513
+ }];
514
+ //#endregion
515
+ //#region src/cli/doctor/dependencies.ts
516
+ function contentMapperSetup(context) {
517
+ const setup = context.contentMapper;
518
+ if (context.config.gen.contentMapper !== true || setup === void 0) return [];
519
+ const findings = [];
520
+ if (!hasContentMapper(setup.tsconfig)) findings.push({
521
+ message: `gen.contentMapper is on, but tsconfig.json doesn't list the content mapper, so schema.bsb has no types. Add ${TSCONFIG_SNIPPET} next to compilerOptions, or run \`better-supabase init --content-mapper\`.`,
522
+ target: "tsconfig.json"
523
+ });
524
+ if (setup.typescript === void 0 || !supportsContentMappers(setup.typescript)) findings.push({
525
+ message: `gen.contentMapper needs TypeScript 7.1 or later, but the project resolves ${setup.typescript === void 0 ? "no typescript" : `typescript@${setup.typescript}`}. Install typescript@7.1, or turn gen.contentMapper off and run \`better-supabase gen\` to write the files again.`,
526
+ target: "typescript"
527
+ });
528
+ if (setup.typecheck !== void 0 && needsRunExternalCode(setup.typecheck)) findings.push({
529
+ message: `The typecheck script runs tsc without ${RUN_EXTERNAL_CODE}, so tsc doesn't load the content mapper and every import of schema.bsb fails. Add ${RUN_EXTERNAL_CODE} to it.`,
530
+ target: "package.json"
531
+ });
532
+ return findings;
533
+ }
534
+ const DEPENDENCY_RULES = [{
535
+ code: "BS601",
536
+ severity: "warning",
537
+ title: "Peer outside its range",
538
+ description: "An optional peer of better-supabase is installed at a version outside the range better-supabase declares. With strict peer checks the install fails, and the subpath that needs the peer can break at runtime.",
539
+ check: (context) => (context.peers ?? []).flatMap((peer) => peer.installed !== void 0 && satisfies(peer.installed, peer.range) === false ? [{
540
+ message: `${peer.name}@${peer.installed} is installed, but better-supabase needs ${peer.range}. Install a version in that range, or remove ${peer.name} if nothing imports it.`,
541
+ target: peer.name
542
+ }] : [])
543
+ }, {
544
+ code: "BS602",
545
+ severity: "error",
546
+ title: "Content mapper not set up",
547
+ description: "gen.contentMapper is on, but tsc can't load the content mapper: tsconfig.json has no contentMappers entry for better-supabase, the project resolves TypeScript older than 7.1, or the typecheck script runs tsc without --runExternalCode. Imports of schema.bsb then have no types.",
548
+ check: contentMapperSetup
549
+ }];
550
+ //#endregion
551
+ //#region src/cli/doctor/flags.ts
552
+ /** Flags past their `expires_at` extra column that aren't archived. */
553
+ async function expiredFlags(context) {
554
+ const db = context.database;
555
+ if (!db || "skipped" in db) return [];
556
+ let query;
557
+ try {
558
+ query = expiredFlagsQuery(moduleLayout(context.config), context.config.blockNames);
559
+ } catch {
560
+ return [];
561
+ }
562
+ if (!query) return [];
563
+ let rows;
564
+ try {
565
+ rows = await db.query(query);
566
+ } catch {
567
+ return [];
568
+ }
569
+ return rows.map((row) => ({
570
+ message: `Flag "${row.key}" expired on ${row.expires_at}${row.owner ? ` (owner: ${row.owner})` : ""}. Remove its checks from the code, then archive it with \`flags.upsert("${row.key}", { archived: true })\`, or move \`expires_at\` when it stays.`,
571
+ target: `flags:${row.key}`
572
+ }));
573
+ }
574
+ const FLAG_RULES = [{
575
+ code: "BS336",
576
+ severity: "warning",
577
+ title: "Flag past its expiry",
578
+ description: "The flags table has an `expires_at` column (`blocks['flags'].options.extraColumns`), and a flag on the live database that isn't archived has an `expires_at` in the past. A release flag that outlives its rollout keeps a dead branch in the code. The finding names the `owner` column when the table has one.",
579
+ check: expiredFlags
580
+ }];
581
+ //#endregion
582
+ //#region src/cli/doctor/hooks.ts
583
+ /** Claims past this size make every request carry a large cookie and header. */
584
+ const HOOK_CLAIMS_LIMIT = 2048;
585
+ /**
586
+ * The BS405 limits. With a provider hook budget, `doctor.claimsLimit` (else the
587
+ * budget's bytes) limits its claims and the whole token keeps 2048; without
588
+ * one, `doctor.claimsLimit` limits the whole token.
589
+ */
590
+ function claimsLimits(context) {
591
+ const configured = context.config.doctor.claimsLimit;
592
+ const budget = providerHook(context)?.budget;
593
+ return budget ? {
594
+ token: HOOK_CLAIMS_LIMIT,
595
+ budget: {
596
+ bytes: configured ?? budget.bytes,
597
+ claims: budget.claims,
598
+ ...budget.truncatedClaim ? { truncatedClaim: budget.truncatedClaim } : {}
599
+ }
600
+ } : { token: configured ?? HOOK_CLAIMS_LIMIT };
601
+ }
602
+ /** The module function that fills the `memberships` claim. */
603
+ const MODULE_MEMBERSHIPS = /better_supabase\s*\.\s*membership_claims\b/i;
604
+ const providerHook = (context) => context.config.authorization?.tokenHook;
605
+ const splitName = (qualified) => {
606
+ const dot = qualified.indexOf(".");
607
+ return {
608
+ schema: qualified.slice(0, dot),
609
+ name: qualified.slice(dot + 1)
610
+ };
611
+ };
612
+ const escapeRegExp$2 = (text) => text.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
613
+ /**
614
+ * The claims the provider's hook writes that a second writer would contradict,
615
+ * with the tenant claim it writes to.
616
+ */
617
+ function ownedClaims(hook) {
618
+ return [.../* @__PURE__ */ new Set([...hook.ownedClaims, ...hook.tenantClaim ? [hook.tenantClaim] : []])];
619
+ }
620
+ const quotedName = (schema, name) => `(?:"?${escapeRegExp$2(schema)}"?\\s*\\.\\s*)?"?${escapeRegExp$2(name)}"?`;
621
+ const createsFunction = (fn) => new RegExp(`create\\s+(?:or\\s+replace\\s+)?function\\s+${quotedName(fn.schema, fn.name)}\\s*\\(`, "i");
622
+ /** The SQL file that has the provider's hook marker and creates `fn`. */
623
+ function markerFileFor(context, fn) {
624
+ const marker = providerHook(context)?.markers?.hook;
625
+ if (marker === void 0) return;
626
+ const creates = createsFunction(fn);
627
+ return (context.sqlFiles ?? []).find((file) => file.text.includes(marker) && creates.test(file.text));
628
+ }
629
+ /** Whether `fn` is the provider's hook: by its name or its marker file. */
630
+ function isProviderHook(context, fn) {
631
+ const hook = providerHook(context);
632
+ if (!hook) return false;
633
+ if (hook.function === `${fn.schema}.${fn.name}`) return true;
634
+ return markerFileFor(context, fn) !== void 0;
635
+ }
636
+ /** Whether the hook body calls the provider's hook (a wrapper around it). */
637
+ function wrapsProviderHook(hook, source) {
638
+ const { schema, name } = splitName(hook.function);
639
+ return new RegExp(`${quotedName(schema, name)}\\s*\\(`, "i").test(source);
640
+ }
641
+ /**
642
+ * Which of `claims` the hook body writes through `jsonb_set` or
643
+ * `jsonb_build_object`.
644
+ */
645
+ function writtenClaims(source, claims) {
646
+ return claims.filter((claim) => {
647
+ const name = escapeRegExp$2(claim);
648
+ 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);
649
+ });
650
+ }
651
+ const API_ROLES$1 = ["authenticated", "anon"];
652
+ /** Hooks enabled in `config.toml`, matched with their introspected functions. */
653
+ function configuredHooks(context) {
654
+ if (!context.configToml) return [];
655
+ const introspected = context.snapshot.extras.hooks ?? [];
656
+ return pgFunctionHooks(context.configToml.document).map((config) => ({
657
+ config,
658
+ extras: introspected.find((hook) => hook.hook === config.hook && hook.schema === config.schema && hook.name === config.name)
659
+ }));
660
+ }
661
+ function hookLocation(context, hook) {
662
+ if (!context.configToml) return;
663
+ const line = lineOf(context.configToml.text, new RegExp(`^\\s*\\[auth\\.hook\\.${hook}\\]`));
664
+ return line ? {
665
+ file: context.configToml.path,
666
+ line
667
+ } : void 0;
668
+ }
669
+ const signatureOf = (fn) => `${fn.schema}.${fn.name}(${fn.signature})`;
670
+ /** What is wrong with who may call a hook function, and the SQL that fixes it. */
671
+ function grantProblems(fn) {
672
+ const problems = [];
673
+ const fix = [];
674
+ if (!fn.schemaUsage.includes("supabase_auth_admin")) {
675
+ problems.push(`supabase_auth_admin has no usage on schema ${fn.schema}`);
676
+ fix.push(`grant usage on schema ${fn.schema} to supabase_auth_admin;`);
677
+ }
678
+ if (!fn.execute.includes("supabase_auth_admin")) {
679
+ problems.push("supabase_auth_admin may not execute it");
680
+ fix.push(`grant execute on function ${signatureOf(fn)} to supabase_auth_admin;`);
681
+ }
682
+ const exposedTo = API_ROLES$1.filter((role) => fn.execute.includes(role));
683
+ if (exposedTo.length > 0 || fn.publicExecute) {
684
+ const roles = [...exposedTo, ...fn.publicExecute ? ["public"] : []];
685
+ problems.push(`${roles.join(", ")} may execute it, so any client can call it through the API`);
686
+ fix.push(`revoke execute on function ${signatureOf(fn)} from authenticated, anon, public;`);
687
+ }
688
+ return {
689
+ problems,
690
+ fix
691
+ };
692
+ }
693
+ const MIGRATIONS = /(?:^|\/)supabase\/migrations\//;
694
+ /**
695
+ * The file that grants `fn` to `supabase_auth_admin`: a migration with the
696
+ * provider's grants marker; under pg-delta, which carries grants, the
697
+ * declarative schema file that defines `fn`; and without declarative schemas,
698
+ * the migration that defines it. `supabase db diff` drops grants, so migra only
699
+ * counts the marked migration.
700
+ */
701
+ function grantsSourceFor(context, fn, workflow) {
702
+ 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");
703
+ const creates = createsFunction(fn);
704
+ const marker = providerHook(context)?.markers?.grants;
705
+ return (context.sqlFiles ?? []).find((file) => grants.test(file.text) && (marker !== void 0 && file.text.includes(marker) || workflow === "pg-delta" && !MIGRATIONS.test(file.path) && creates.test(file.text) || workflow === "migrations" && MIGRATIONS.test(file.path) && creates.test(file.text)))?.path;
706
+ }
707
+ /**
708
+ * Configured hook functions whose grants are wrong (BS404, `doctor
709
+ * --fix-grants`).
710
+ */
711
+ function hookGrantProblems(context) {
712
+ const workflow = workflowOf(context);
713
+ return configuredHooks(context).flatMap(({ config, extras }) => (extras?.functions ?? []).flatMap((fn) => {
714
+ const { problems, fix } = grantProblems(fn);
715
+ if (problems.length === 0) return [];
716
+ const file = grantsSourceFor(context, fn, workflow);
717
+ const provider = context.config.authorization;
718
+ const command = provider?.tokenHook?.grantsCommand;
719
+ return [{
720
+ hook: config.hook,
721
+ fn,
722
+ problems,
723
+ fix: file ? {
724
+ kind: "migration",
725
+ file
726
+ } : provider && isProviderHook(context, fn) ? {
727
+ kind: "provider",
728
+ name: providerLabel(provider),
729
+ ...command ? { command } : {}
730
+ } : {
731
+ kind: "sql",
732
+ sql: fix
733
+ }
734
+ }];
735
+ }));
736
+ }
737
+ /** The sentence that tells the reader how to apply `fix`. */
738
+ function grantFixText(fix, workflow) {
739
+ switch (fix.kind) {
740
+ case "sql": return `${grantFixPlace(workflow)} (\`doctor --fix-grants\` prints every block):\n${fix.sql.join("\n")}`;
741
+ 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.`;
742
+ case "migration": return workflow === "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\`).`;
743
+ default: return fix;
744
+ }
745
+ }
746
+ /** Where the grants go under `workflow`. */
747
+ function grantFixPlace(workflow) {
748
+ switch (workflow) {
749
+ case "pg-delta": return "Add to the schema file that defines the function, then run `supabase db schema declarative sync`";
750
+ case "migra": return "Append to the migration `supabase db diff` wrote";
751
+ case "migrations": return "Add to a new migration (`supabase migration new auth_hook_grants`), then apply it (`supabase migration up`)";
752
+ default: return workflow;
753
+ }
754
+ }
755
+ /**
756
+ * `doctor --fix-grants`: the SQL block for every BS404 problem, as one
757
+ * migration snippet.
758
+ */
759
+ function hookGrantBlock(problems, workflow = "pg-delta") {
760
+ if (problems.length === 0) return "-- Every configured Auth hook function has its grants.";
761
+ const lines = ["-- Auth hook grants (better-supabase doctor --fix-grants).", workflow === "migra" ? "-- `supabase db diff` does not carry function grants; append this to its migration." : `-- ${grantFixPlace(workflow)}.`];
762
+ for (const { fn, hook, fix } of problems) {
763
+ const name = `${signatureOf(fn)} ([auth.hook.${hook}])`;
764
+ switch (fix.kind) {
765
+ case "sql":
766
+ lines.push("", `-- ${name}`, ...fix.sql);
767
+ break;
768
+ case "provider":
769
+ lines.push("", `-- ${name} is the hook of ${fix.name}${fix.command ? `: ${fix.command}` : ""}`);
770
+ break;
771
+ case "migration":
772
+ lines.push("", workflow === "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\`.`);
773
+ break;
774
+ default: return fix;
775
+ }
776
+ }
777
+ return lines.join("\n");
778
+ }
779
+ const emptySearchPath = (fn) => {
780
+ const value = fn.settings["search_path"];
781
+ return value === "" || value === "\"\"" || value === "''";
782
+ };
783
+ /**
784
+ * Auth's issuer shape (`{url}/auth/v1`); doctor has no project URL to put in
785
+ * it.
786
+ */
787
+ const DOCTOR_ISSUER = "https://doctor.invalid/auth/v1";
788
+ /**
789
+ * Runs the hook as Auth does for `userId` (as `supabase_auth_admin` when the
790
+ * connecting role may switch to it), in a transaction that is rolled back.
791
+ * Undefined when `auth.users` has no such user.
792
+ */
793
+ async function withHookEvent(db, userId, run) {
794
+ await db.query("begin");
795
+ try {
796
+ await db.query(`set local statement_timeout = '10s'`);
797
+ const [user] = await db.query(`select set_config('better_supabase.hook_event', jsonb_build_object(
798
+ 'user_id', u.id,
799
+ 'authentication_method', 'password',
800
+ 'claims', jsonb_build_object(
801
+ 'aud', 'authenticated', 'role', 'authenticated', 'sub', u.id,
802
+ 'email', coalesce(u.email, ''), 'phone', coalesce(u.phone, ''),
803
+ 'app_metadata', coalesce(u.raw_app_meta_data, '{}'::jsonb),
804
+ 'user_metadata', coalesce(u.raw_user_meta_data, '{}'::jsonb),
805
+ 'aal', 'aal1', 'session_id', gen_random_uuid(),
806
+ 'is_anonymous', coalesce(u.is_anonymous, false),
807
+ 'amr', jsonb_build_array(jsonb_build_object(
808
+ 'method', 'password', 'timestamp', extract(epoch from now())::int8
809
+ )),
810
+ 'iss', ${literal$1(DOCTOR_ISSUER)},
811
+ 'iat', extract(epoch from now())::int8,
812
+ 'exp', extract(epoch from now())::int8 + 3600
813
+ )
814
+ )::text, true) as event
815
+ from auth.users u where u.id = ${literal$1(userId)}::uuid`);
816
+ if (!user?.event) return;
817
+ const [role] = await db.query(`select case when exists(select 1 from pg_roles where rolname = 'supabase_auth_admin')
818
+ then pg_has_role('supabase_auth_admin', 'member') else false end as member`);
819
+ if (role?.member) await db.query("set local role supabase_auth_admin");
820
+ return await run();
821
+ } finally {
822
+ await db.query("rollback");
823
+ }
824
+ }
825
+ const hookCall = (fn) => `${ident$1(fn.schema)}.${ident$1(fn.name)}(current_setting('better_supabase.hook_event')::jsonb) -> 'claims'`;
826
+ /** The claims the hook returns for `userId`, or undefined without that user. */
827
+ async function hookClaims(db, fn, userId) {
828
+ return withHookEvent(db, userId, async () => {
829
+ const [row] = await db.query(`select ${hookCall(fn)} as claims`);
830
+ const claims = typeof row?.claims === "string" ? JSON.parse(row.claims) : row?.claims;
831
+ return isRecord(claims) ? claims : {};
832
+ });
833
+ }
834
+ async function hookClaimsSize(db, fn, userId, budget) {
835
+ const budgetSql = budget && budget.claims.length > 0 ? budget.claims.map((claim) => `coalesce(octet_length((c -> ${literal$1(claim)})::text), 0)`).join(" + ") : "0";
836
+ const truncatedSql = budget?.truncatedClaim ? `c ->> ${literal$1(budget.truncatedClaim)} = 'true'` : "false";
837
+ return withHookEvent(db, userId, async () => {
838
+ const [row] = await db.query(`select octet_length(c::text) as bytes,
839
+ octet_length((c -> 'memberships')::text) as memberships,
840
+ ${budgetSql} as budget,
841
+ ${truncatedSql} as truncated
842
+ from (select ${hookCall(fn)} as c) h`);
843
+ return {
844
+ bytes: Number(row?.bytes ?? 0),
845
+ memberships: Number(row?.memberships ?? 0),
846
+ budget: Number(row?.budget ?? 0),
847
+ truncated: row?.truncated === true
848
+ };
849
+ });
850
+ }
851
+ const LOCAL_HOSTS = /* @__PURE__ */ new Set([
852
+ "localhost",
853
+ "127.0.0.1",
854
+ "[::1]",
855
+ "host.docker.internal"
856
+ ]);
857
+ /** The `secrets = ...` line of `[auth.hook.<hook>]` reads an `env()` value. */
858
+ function secretsFromEnv(text, hook) {
859
+ const start = text.search(new RegExp(`^\\s*\\[auth\\.hook\\.${hook}\\]`, "m"));
860
+ if (start < 0) return false;
861
+ const rest = text.slice(start).split("\n").slice(1);
862
+ const end = rest.findIndex((line) => /^\s*\[/.test(line));
863
+ const section = (end === -1 ? rest : rest.slice(0, end)).join("\n");
864
+ return /^\s*secrets\s*=\s*["']env\(/m.test(section);
865
+ }
866
+ /**
867
+ * What is wrong with one Standard Webhooks secret (`v1,whsec_<base64>`), the
868
+ * format Auth signs HTTP hook requests with. Never quotes the secret.
869
+ */
870
+ function secretProblem(secret) {
871
+ const match = /^v1,whsec_([A-Za-z0-9+/]+={0,2})$/.exec(secret.trim());
872
+ if (!match) return "is not in the `v1,whsec_<base64>` format";
873
+ const bytes = atob(match[1]).length;
874
+ return bytes < 24 || bytes > 64 ? `decodes to ${bytes} bytes; Standard Webhooks secrets are 24 to 64` : void 0;
875
+ }
876
+ const HOOK_RULES = [
877
+ {
878
+ code: "BS404",
879
+ severity: "error",
880
+ title: "Auth hook function grants",
881
+ description: "Auth calls Postgres hook functions as `supabase_auth_admin`, which needs usage on the schema and execute on the function. Nobody else should be able to call them: a client calling the custom access token hook through the API sees what it adds for any user id.",
882
+ check: (context) => [...configuredHooks(context).flatMap(({ config, extras }) => {
883
+ if (!extras || extras.functions.length > 0) return [];
884
+ const location = hookLocation(context, config.hook);
885
+ return [{
886
+ message: `[auth.hook.${config.hook}] calls ${config.schema}.${config.name}, which does not exist. Sign-ins fail until it does.`,
887
+ target: `[auth.hook.${config.hook}]`,
888
+ ...location ? { location } : {}
889
+ }];
890
+ }), ...hookGrantProblems(context).map(({ hook, fn, problems, fix }) => ({
891
+ message: `${signatureOf(fn)} ([auth.hook.${hook}]): ${problems.join("; ")}. ${grantFixText(fix, workflowOf(context))}`,
892
+ target: signatureOf(fn),
893
+ object: {
894
+ kind: "function",
895
+ schema: fn.schema,
896
+ name: fn.name
897
+ }
898
+ }))]
899
+ },
900
+ {
901
+ code: "BS405",
902
+ severity: "warning",
903
+ title: "Custom access token hook shape",
904
+ 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 (\`tooling.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 \`tooling.doctor.claimsLimit\`), measured with \`octet_length\` of each claim.`,
905
+ async check(context) {
906
+ const findings = [];
907
+ const limits = claimsLimits(context);
908
+ for (const { config, extras } of configuredHooks(context)) {
909
+ if (config.hook !== "custom_access_token" || !extras) continue;
910
+ for (const fn of extras.functions) {
911
+ const object = {
912
+ kind: "function",
913
+ schema: fn.schema,
914
+ name: fn.name
915
+ };
916
+ const problems = [];
917
+ if (fn.volatility !== "stable") problems.push(`it is ${fn.volatility}; declare it \`stable\``);
918
+ if (!emptySearchPath(fn)) problems.push("it has no `set search_path = ''`; add it and qualify every table");
919
+ if (problems.length > 0) findings.push({
920
+ message: `${signatureOf(fn)}: ${problems.join("; ")}.`,
921
+ target: signatureOf(fn),
922
+ object
923
+ });
924
+ if (!context.hookUser) continue;
925
+ const db = context.database;
926
+ if (!db || "skipped" in db || !db.session) {
927
+ findings.push({
928
+ severity: "info",
929
+ message: `Measuring the hook's claims needs a direct database connection (local stack, $DATABASE_URL, $SUPABASE_DB_URL or --db-url-stdin)${db && "skipped" in db ? `: ${db.skipped}` : "."}`,
930
+ target: `${signatureOf(fn)}:claims`
931
+ });
932
+ continue;
933
+ }
934
+ try {
935
+ const size = await hookClaimsSize(db, fn, context.hookUser, limits.budget);
936
+ if (size === void 0) {
937
+ findings.push({
938
+ severity: "info",
939
+ message: `--as ${context.hookUser}: no such user in auth.users, so the hook was not called.`,
940
+ target: `${signatureOf(fn)}:claims`
941
+ });
942
+ continue;
943
+ }
944
+ if (limits.budget !== void 0 && size.budget > limits.budget.bytes) findings.push({
945
+ 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 \`tooling.doctor.claimsLimit\` together, or move claims out of the token.`,
946
+ target: `${signatureOf(fn)}:budget`,
947
+ object
948
+ });
949
+ if (size.bytes > limits.token) findings.push({
950
+ message: `${signatureOf(fn)} returns ${size.bytes} bytes of claims for ${context.hookUser} (limit ${limits.token}, memberships ${size.memberships}). Every request carries them twice (cookie and header); keep ids and roles in the token and look up the rest.`,
951
+ target: `${signatureOf(fn)}:claims`,
952
+ object
953
+ });
954
+ if (size.truncated) findings.push({
955
+ severity: "info",
956
+ 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.`,
957
+ target: `${signatureOf(fn)}:truncated`,
958
+ object
959
+ });
960
+ } catch (cause) {
961
+ findings.push({
962
+ message: `Calling ${signatureOf(fn)} for ${context.hookUser} failed: ${errorText(cause)}`,
963
+ target: `${signatureOf(fn)}:claims`,
964
+ object
965
+ });
966
+ }
967
+ }
968
+ }
969
+ return findings;
970
+ }
971
+ },
972
+ {
973
+ code: "BS407",
974
+ severity: "error",
975
+ title: "Two authorization hooks",
976
+ 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.",
977
+ check: (context) => {
978
+ const provider = context.config.authorization;
979
+ const hook = provider?.tokenHook;
980
+ if (!provider || !hook) return [];
981
+ const label = providerLabel(provider);
982
+ const registered = new Map((hook.registeredClaims ?? []).map((claim) => [claim.name, claim.function]));
983
+ const owned = ownedClaims(hook);
984
+ const findings = [];
985
+ for (const { config, extras } of configuredHooks(context)) {
986
+ if (config.hook !== "custom_access_token" || !extras) continue;
987
+ for (const fn of extras.functions) {
988
+ if (fn.source === void 0 || isProviderHook(context, fn)) continue;
989
+ const module = owned.includes("memberships") && MODULE_MEMBERSHIPS.test(fn.source);
990
+ const wrapper = wrapsProviderHook(hook, fn.source);
991
+ const written = writtenClaims(fn.source, [...owned, ...wrapper ? registered.keys() : []]);
992
+ if (!module && written.length === 0) continue;
993
+ const location = hookLocation(context, config.hook);
994
+ const what = [...module ? ["calls better_supabase.membership_claims"] : [], ...written.length > 0 ? [`writes ${written.join(", ")}`] : []].join(" and ");
995
+ const extra = written.filter((claim) => registered.has(claim));
996
+ const sources = extra.map((claim) => `${claim} from ${registered.get(claim)}`).join(", ");
997
+ findings.push({
998
+ 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.`,
999
+ target: signatureOf(fn),
1000
+ object: {
1001
+ kind: "function",
1002
+ schema: fn.schema,
1003
+ name: fn.name
1004
+ },
1005
+ ...location ? { location } : {}
1006
+ });
1007
+ }
1008
+ }
1009
+ return findings;
1010
+ }
1011
+ },
1012
+ {
1013
+ code: "BS410",
1014
+ severity: "warning",
1015
+ title: "HTTP auth hooks",
1016
+ 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`.",
1017
+ check: (context) => {
1018
+ const toml = context.configToml;
1019
+ if (!toml) return [];
1020
+ return httpHooks(toml.document).flatMap(({ hook, uri, secrets }) => {
1021
+ const location = hookLocation(context, hook);
1022
+ const at = location ? { location } : {};
1023
+ const target = `[auth.hook.${hook}]`;
1024
+ const findings = [{
1025
+ severity: "info",
1026
+ 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.`,
1027
+ target,
1028
+ ...at
1029
+ }];
1030
+ if (uri.protocol === "http:" && !LOCAL_HOSTS.has(uri.hostname)) findings.push({
1031
+ message: `${target} sends claims and its signature to ${uri.host} over plain http. Use https.`,
1032
+ target,
1033
+ ...at
1034
+ });
1035
+ const fromEnv = secretsFromEnv(toml.text, hook);
1036
+ if (secrets === void 0 || secrets.trim() === "") {
1037
+ findings.push({
1038
+ severity: "error",
1039
+ message: `${target} has no \`secrets\`, so Auth can't sign its requests. Set \`secrets = "env(AUTH_HOOK_SECRET)"\` with a \`v1,whsec_<base64>\` value.`,
1040
+ target,
1041
+ ...at
1042
+ });
1043
+ return findings;
1044
+ }
1045
+ if (!fromEnv) findings.push({
1046
+ message: `${target} has its secret written in ${toml.path}, which is committed. Move it to an env file and read it with \`secrets = "env(AUTH_HOOK_SECRET)"\`.`,
1047
+ target,
1048
+ ...at
1049
+ });
1050
+ if (fromEnv && secrets.trim().startsWith("env(")) return findings;
1051
+ const problems = secrets.split("|").flatMap((secret, index) => {
1052
+ const problem = secretProblem(secret);
1053
+ return problem ? [`secret ${index + 1} ${problem}`] : [];
1054
+ });
1055
+ if (problems.length > 0) findings.push({
1056
+ severity: "error",
1057
+ message: `${target}: ${problems.join("; ")}. Auth rejects the config; generate one with \`openssl rand -base64 32\` and prefix it with \`v1,whsec_\`.`,
1058
+ target,
1059
+ ...at
1060
+ });
1061
+ return findings;
1062
+ });
1063
+ }
1064
+ }
1065
+ ];
1066
+ //#endregion
1067
+ //#region src/cli/doctor/lite.ts
1068
+ const RPC_CALL = /(?:\.|\$)rpc\s*\(|\brpcTransport\s*\(/;
1069
+ const MISSING_FEATURES = [{
1070
+ feature: "Realtime",
1071
+ pattern: /from\s+["']better-supabase\/realtime["']|\.channel\s*\(|\.removeChannel\s*\(/
1072
+ }, {
1073
+ feature: "Edge Functions",
1074
+ pattern: /\.functions\.invoke\s*\(|\bfunctions\(\)\.invoke\s*\(/
1075
+ }];
1076
+ /**
1077
+ * The `[db] driver` of a Supabase Lite project, or `undefined` for any other
1078
+ * project.
1079
+ */
1080
+ function liteDriver(context) {
1081
+ try {
1082
+ return liteDriverOf(context.configToml);
1083
+ } catch {
1084
+ return;
1085
+ }
1086
+ }
1087
+ function rpcOnSqlite(context) {
1088
+ const driver = liteDriver(context);
1089
+ if (driver === void 0 || !isSqliteLiteDriver(driver)) return [];
1090
+ return context.sources.flatMap((file) => {
1091
+ const line = lineOf(file.text, RPC_CALL);
1092
+ if (line === void 0) return [];
1093
+ return [{
1094
+ message: `${file.path} calls a Postgres function, but Supabase Lite's ${driver} driver has no rpc(), so the call returns an unsupported error. Switch [db] driver to "pglite" or "postgres" in supabase/config.toml, or replace the function with table queries.`,
1095
+ target: `${file.path}:rpc`,
1096
+ location: {
1097
+ file: file.path,
1098
+ line
1099
+ }
1100
+ }];
1101
+ });
1102
+ }
1103
+ function missingLiteFeature(context) {
1104
+ if (liteDriver(context) === void 0) return [];
1105
+ return context.sources.flatMap((file) => MISSING_FEATURES.flatMap(({ feature, pattern }) => {
1106
+ const line = lineOf(file.text, pattern);
1107
+ if (line === void 0) return [];
1108
+ return [{
1109
+ message: `${file.path} uses ${feature}, which Supabase Lite does not implement on any driver. Run the full Supabase stack for this feature, or move the project off Lite with \`supabase lite upgrade\`.`,
1110
+ target: `${file.path}:${feature}`,
1111
+ location: {
1112
+ file: file.path,
1113
+ line
1114
+ }
1115
+ }];
1116
+ }));
1117
+ }
1118
+ const LITE_RULES = [{
1119
+ code: "BS328",
1120
+ severity: "warning",
1121
+ title: "rpc() on a Supabase Lite SQLite driver",
1122
+ description: "`[db] driver` in `config.toml` is `sqlite-postgres` or `sqlite`, which have no `rpc()`, but the app calls a Postgres function. Those calls return an `unsupported` error.",
1123
+ check: rpcOnSqlite
1124
+ }, {
1125
+ code: "BS329",
1126
+ severity: "warning",
1127
+ title: "Feature Supabase Lite does not implement",
1128
+ description: "`config.toml` sets `[db] driver`, so the project runs on Supabase Lite, but the app uses Realtime or Edge Functions, which Lite does not implement.",
1129
+ check: missingLiteFeature
1130
+ }];
1131
+ //#endregion
1132
+ //#region src/cli/doctor/metadata.ts
1133
+ /**
1134
+ * Rules that read what a `GeneratorMetadata` document leaves out (policies,
1135
+ * grants, indexes, triggers, buckets, the realtime publication, foreign key
1136
+ * actions, multi-column constraints, role settings, hook functions). On a
1137
+ * snapshot built from such a document they would report what is missing from
1138
+ * the document, not from the database, so they skip with an info finding.
1139
+ */
1140
+ const EXTRAS_RULES = /* @__PURE__ */ new Set([
1141
+ "BS103",
1142
+ "BS106",
1143
+ "BS107",
1144
+ "BS108",
1145
+ "BS109",
1146
+ "BS110",
1147
+ "BS111",
1148
+ "BS112",
1149
+ "BS113",
1150
+ "BS114",
1151
+ "BS115",
1152
+ "BS116",
1153
+ "BS117",
1154
+ "BS204",
1155
+ "BS205",
1156
+ "BS206",
1157
+ "BS207",
1158
+ "BS210",
1159
+ "BS211",
1160
+ "BS224",
1161
+ "BS213",
1162
+ "BS215",
1163
+ "BS216",
1164
+ "BS217",
1165
+ "BS218",
1166
+ "BS219",
1167
+ "BS301",
1168
+ "BS302",
1169
+ "BS303",
1170
+ "BS305",
1171
+ "BS306",
1172
+ "BS337",
1173
+ "BS338",
1174
+ "BS340",
1175
+ "BS310",
1176
+ "BS315",
1177
+ "BS404",
1178
+ "BS405",
1179
+ "BS406",
1180
+ "BS407"
1181
+ ]);
1182
+ /**
1183
+ * The info finding for a rule in `EXTRAS_RULES` that did not run, or
1184
+ * `undefined` when it can.
1185
+ */
1186
+ function skippedForMetadata(snapshot, rule, help) {
1187
+ if (!snapshot.extras.fromMetadata || !EXTRAS_RULES.has(rule.code)) return;
1188
+ return {
1189
+ code: rule.code,
1190
+ severity: "info",
1191
+ title: rule.title,
1192
+ help,
1193
+ 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.`
1194
+ };
1195
+ }
1196
+ //#endregion
1197
+ //#region src/cli/doctor/module-files.ts
1198
+ /** The `blocks` files as this release renders them, next to what is on disk. */
1199
+ async function moduleFiles(context) {
1200
+ if (context.config.blockNames.length === 0 && Object.keys(context.config.appTables).length === 0) return [];
1201
+ const access = accessModule(context);
1202
+ if (access.kind === "invalid") return [];
1203
+ const readSets = context.readSets;
1204
+ const skipped = readSets !== void 0 && "skipped" in readSets;
1205
+ let rendered;
1206
+ try {
1207
+ rendered = renderBlocks(context.config.blockNames, await moduleLayoutFor(context.config, {
1208
+ names: context.config.blockNames,
1209
+ testsDir: context.config.sql.testsDir,
1210
+ readSets: skipped ? [] : readSets,
1211
+ entitlementsProvider: entitlementsModule(context),
1212
+ accessProvider: access.kind === "provider" ? access.access : void 0,
1213
+ permissionCatalog: permissionCatalogKeys(context)
1214
+ }));
1215
+ } catch {
1216
+ return [];
1217
+ }
1218
+ const files = rendered.filter((file) => !(skipped && file.module === "read-sets"));
1219
+ if (workflowOf(context) === "migrations") {
1220
+ const latest = await latestMigration(context.config, MODULES_SUFFIX);
1221
+ const versions = migrationModules(latest?.text ?? "");
1222
+ const path = latest?.path ?? `${migrationsDir(context.config)}/<stamp>_better_supabase_modules.sql`;
1223
+ return Promise.all(files.map(async (file) => {
1224
+ if (file.kind === "test") return fileState(context, file);
1225
+ const installed = versions.get(file.instance ?? file.module);
1226
+ return {
1227
+ file,
1228
+ path,
1229
+ installed,
1230
+ current: latest === void 0 ? void 0 : migrationHolds(latest.text, file.contents) ? file.contents : latest.text,
1231
+ behind: file.kind === "schema" && installed !== void 0 && installed < (moduleFileVersion(file.contents)?.version ?? 1)
1232
+ };
1233
+ }));
1234
+ }
1235
+ return Promise.all(files.map((file) => fileState(context, file)));
1236
+ }
1237
+ async function fileState(context, file) {
1238
+ const current = await readFile(resolve(context.config.root, file.path), "utf-8").catch(() => void 0);
1239
+ const installed = current === void 0 ? void 0 : moduleFileVersion(current)?.version ?? 1;
1240
+ return {
1241
+ file,
1242
+ path: file.path,
1243
+ current,
1244
+ installed,
1245
+ behind: installed !== void 0 && installed < (moduleFileVersion(file.contents)?.version ?? 1)
1246
+ };
1247
+ }
1248
+ //#endregion
1249
+ //#region src/cli/doctor/app-table-policies.ts
1250
+ /**
1251
+ * Block policies (`bsb_*`, permissive) on the app's tables that grant a command
1252
+ * more than the app's own permissive policies do. Postgres ORs permissive
1253
+ * policies, so a block policy whose condition no app policy repeats can open
1254
+ * rows the app's policies keep closed. A command the app has no policy for is
1255
+ * the block's to grant.
1256
+ */
1257
+ function widenedAppTablePolicies(context) {
1258
+ const tables = adoptedTables(context.config.blocks, appTablesOf(context));
1259
+ if (tables.length === 0) return [];
1260
+ const modules = /* @__PURE__ */ new Map();
1261
+ for (const table of tables) {
1262
+ const target = `${table.schema}.${table.table}`;
1263
+ modules.set(target, (modules.get(target) ?? /* @__PURE__ */ new Set()).add(table.module));
1264
+ }
1265
+ return catalogOf(context).tables.flatMap((table) => {
1266
+ const target = qualified(table);
1267
+ const owners = modules.get(target);
1268
+ if (owners === void 0) return [];
1269
+ const permissive = table.policies.filter((policy) => policy.permissive);
1270
+ const own = permissive.filter((policy) => !policy.name.startsWith(BLOCK_PREFIX));
1271
+ const blocks = [...owners].map((module) => `blocks.${module}`).join(" or ");
1272
+ return permissive.filter((policy) => policy.name.startsWith(BLOCK_PREFIX)).flatMap((policy) => {
1273
+ const widened = widenedCommands(policy, own);
1274
+ if (widened.length === 0) return [];
1275
+ const reason = `no policy of the app's for ${widened.join(", ")} reaches its roles with a condition it repeats (alone or with more "and" operands), so it can open rows the app's policies keep closed`;
1276
+ return [{
1277
+ message: `${blocks} adds policy ${policy.name} on ${target}, and ${reason}. Postgres ORs permissive policies; doctor compares conditions as text and can't prove that one implies another. If the block's condition is what the app means, repeat it in the app's policy; otherwise narrow the permission key it checks, or drop ${policy.name} in a schema file that runs after the block's.`,
1278
+ target: `${target}.${policy.name}`,
1279
+ object: {
1280
+ kind: "policy",
1281
+ schema: table.schema,
1282
+ table: table.name,
1283
+ name: policy.name
1284
+ }
1285
+ }];
1286
+ });
1287
+ });
1288
+ }
1289
+ const KEPT_TRIGGER = /^-- (\S+) keeps updated_at current with its own trigger (\S+)\.$/gm;
1290
+ /**
1291
+ * App tables whose own trigger keeps `updated_at` current, so the block skipped
1292
+ * its `bsb_updated_at` trigger there.
1293
+ */
1294
+ async function keptUpdatedAtTriggers(context) {
1295
+ return (await moduleFiles(context)).flatMap(({ file }) => file.kind === "schema" ? [...file.contents.matchAll(KEPT_TRIGGER)].map(([, target = "", trigger = ""]) => {
1296
+ const dot = target.indexOf(".");
1297
+ return {
1298
+ message: `blocks.${file.instance ?? file.module} runs over ${target}, which keeps updated_at current with its own trigger ${trigger}, so the block adds no bsb_updated_at trigger there. Keep ${trigger} firing on update; if you drop it, run \`better-supabase upgrade\` to add the block's.`,
1299
+ target,
1300
+ object: {
1301
+ kind: "table",
1302
+ schema: target.slice(0, dot),
1303
+ name: target.slice(dot + 1)
1304
+ }
1305
+ };
1306
+ }) : []);
1307
+ }
1308
+ /**
1309
+ * Whether a deprecated table or column name belongs to the app: the app's
1310
+ * tables keep the app's names, as custom blocks' tables do.
1311
+ */
1312
+ function keepsAppsName(context) {
1313
+ const adopted = context.config.blockNames.some((name) => moduleDeprecations().some((entry) => entry.module === name && (entry.kind === "table" || entry.kind === "column"))) ? adoptedTables(context.config.blocks, appTablesOf(context)) : [];
1314
+ return (entry) => {
1315
+ if (entry.kind !== "table" && entry.kind !== "column") return false;
1316
+ if ((context.config.blocks[entry.module]?.mode ?? "managed") !== "managed") return true;
1317
+ const name = entry.kind === "table" ? entry.symbol.slice(entry.symbol.lastIndexOf(".") + 1) : entry.symbol.slice(0, entry.symbol.indexOf("."));
1318
+ return adopted.some((table) => table.module === entry.module && (name === table.logical || name === table.table || name === SQL_MODULES[table.module]?.names?.tables[table.logical]?.name));
1319
+ };
1320
+ }
1321
+ //#endregion
1322
+ //#region src/cli/doctor/attachment-subjects.ts
1323
+ /**
1324
+ * The attachments subjects as the block resolves them, with the per-subject
1325
+ * keys each one has.
1326
+ */
1327
+ function attachmentSubjects(context) {
1328
+ const registry = context.config.subjects;
1329
+ const raw = context.config.blocks["attachments"]?.on;
1330
+ return (raw === void 0 ? Object.keys(registry).map((type) => [type, {}]) : Array.isArray(raw) ? raw.filter((type) => typeof type === "string").map((type) => [type, {}]) : isRecord(raw) ? Object.entries(raw).flatMap(([type, entry]) => isRecord(entry) ? [[type, entry]] : []) : []).flatMap(([type, entry]) => {
1331
+ const registered = registry[type];
1332
+ const table = entry["table"] ?? registered?.table;
1333
+ const tenant = entry["tenant"] ?? registered?.tenant ?? "organization_id";
1334
+ if (typeof table !== "string" || typeof tenant !== "string") return [];
1335
+ const own = isRecord(entry["permissions"]) ? Object.keys(entry["permissions"]) : [];
1336
+ const fromRegistry = Object.keys(registered?.permissions ?? {}).filter((key) => key !== "read");
1337
+ const subjectRead = typeof entry["permission"] === "string" || registered?.permissions?.["read"] !== void 0 ? ["read"] : [];
1338
+ return [{
1339
+ type,
1340
+ table,
1341
+ tenant,
1342
+ keys: /* @__PURE__ */ new Set([
1343
+ ...own,
1344
+ ...fromRegistry,
1345
+ ...subjectRead
1346
+ ])
1347
+ }];
1348
+ });
1349
+ }
1350
+ /**
1351
+ * Whether `expression` only checks that the caller is a member of the row's
1352
+ * tenant: `current_tenant_id()`, a `*_ids_with` or `*member_*ids` function in
1353
+ * any schema (the provider's own helpers included), `can('tenant', ...)` or
1354
+ * `member_can`, joined by `and`, with a soft-delete `is null` allowed. A
1355
+ * permission call passes only with one of `keys`.
1356
+ */
1357
+ function tenantOnly(expression, column, keys) {
1358
+ const col = `(?:"?\\w+"?\\.)?"?${escape$2(column)}"?(?:::\\w+)?`;
1359
+ const key = `'([^']*)'(?:::text)?`;
1360
+ const atoms = [
1361
+ `${col}\\s*=\\s*\\(?\\s*(?:select\\s+)?better_supabase\\.current_tenant_id\\(\\)(?:\\s+as\\s+\\w+)?`,
1362
+ `${col}\\s+in\\s+\\(\\s*select\\s+(?:\\w+\\.)?\\w*_ids_with\\(\\s*${key}\\s*\\)(?:\\s+as\\s+\\w+)?\\s*\\)`,
1363
+ `${col}\\s+in\\s+\\(\\s*select\\s+(?:\\w+\\.)?\\w*member_\\w*ids\\(\\s*\\)(?:\\s+as\\s+\\w+)?\\s*\\)`,
1364
+ `better_supabase\\.can\\(\\s*'tenant'(?:::text)?\\s*,\\s*${col}\\s*,\\s*${key}\\s*\\)`,
1365
+ `better_supabase\\.member_can\\(\\s*${col}\\s*,\\s*${key}\\s*\\)`,
1366
+ `(?:"?\\w+"?\\.)?"?\\w+"?\\s+is\\s+null`
1367
+ ];
1368
+ const used = [];
1369
+ let rest = expression;
1370
+ for (const atom of atoms) rest = rest.replaceAll(new RegExp(atom, "gi"), (_match, key) => {
1371
+ if (typeof key === "string") used.push(key);
1372
+ return " ";
1373
+ });
1374
+ return used.every((key) => keys.has(key)) && rest.replaceAll(/\band\b|[()\s]/gi, "") === "";
1375
+ }
1376
+ /** The top-level `or` branches of `expression`, outer parentheses removed. */
1377
+ function branches(expression) {
1378
+ let text = expression.trim();
1379
+ while (text.startsWith("(") && closingParen(text) === text.length - 1) text = text.slice(1, -1).trim();
1380
+ const parts = [];
1381
+ let depth = 0;
1382
+ let start = 0;
1383
+ for (let index = 0; index < text.length; index += 1) {
1384
+ const char = text[index];
1385
+ if (char === "(") depth += 1;
1386
+ else if (char === ")") depth -= 1;
1387
+ else if (depth === 0 && /^\sor\s/i.test(text.slice(index, index + 4))) {
1388
+ parts.push(text.slice(start, index));
1389
+ start = index + 4;
1390
+ }
1391
+ }
1392
+ parts.push(text.slice(start));
1393
+ return parts;
1394
+ }
1395
+ /** The index of the parenthesis closing the one at the start of `text`. */
1396
+ function closingParen(text) {
1397
+ let depth = 0;
1398
+ for (let index = 0; index < text.length; index += 1) if (text[index] === "(") depth += 1;
1399
+ else if (text[index] === ")") {
1400
+ depth -= 1;
1401
+ if (depth === 0) return index;
1402
+ }
1403
+ return -1;
1404
+ }
1405
+ /**
1406
+ * Tenant membership alone lets the caller through: the whole check, or one `or`
1407
+ * branch of it.
1408
+ */
1409
+ function tenantSuffices(expression, column, keys) {
1410
+ return [expression, ...branches(expression)].some((part) => tenantOnly(part, column, keys));
1411
+ }
1412
+ function attachmentSubjectsOnDefaults(context) {
1413
+ if (!context.config.blockNames.includes("attachments")) return [];
1414
+ const { tables } = catalogOf(context);
1415
+ const overrides = context.config.blocks["attachments"]?.permissions ?? {};
1416
+ const actions = [{
1417
+ action: "read",
1418
+ commands: ["select"],
1419
+ what: "reads"
1420
+ }, {
1421
+ action: "upload",
1422
+ commands: ["insert", "update"],
1423
+ what: "writes"
1424
+ }];
1425
+ return attachmentSubjects(context).flatMap(({ type, table: name, tenant, keys }) => {
1426
+ const [schema, bare] = name.includes(".") ? name.split(".", 2) : ["public", name];
1427
+ const table = tables.find((candidate) => candidate.schema === schema && candidate.name === bare);
1428
+ if (table?.kind !== "table" || !table.rls) return [];
1429
+ const stricter = actions.filter(({ action, commands }) => {
1430
+ if (keys.has(action)) return false;
1431
+ const blockKey = overrides[action] ?? `attachments.${action}`;
1432
+ return commands.some((command) => {
1433
+ const applying = table.policies.filter((policy) => policy.permissive && (policy.command === "all" || policy.command === command) && appliesTo(policy, "authenticated"));
1434
+ if (command !== "select" && applying.length === 0) return false;
1435
+ return !applying.some((policy) => {
1436
+ const expression = command === "select" ? policy.using : policy.check ?? policy.using;
1437
+ return expression !== null && tenantSuffices(expression, tenant, /* @__PURE__ */ new Set([blockKey]));
1438
+ });
1439
+ });
1440
+ });
1441
+ if (stricter.length === 0) return [];
1442
+ const target = `${table.schema}.${table.name}`;
1443
+ return [{
1444
+ message: `Attachments on ${type} use the block-wide ${stricter.map(({ action }) => action).join(" and ")} key, but ${target} checks more than tenant membership for ${stricter.map(({ what: verb }) => verb).join(" and ")}, so any member of the tenant gets the files of a ${type} they can't open. Set ${stricter.map(({ action }) => action === "read" ? `blocks.attachments.on.${type}.permissions.read` : `subjects.${type}.permissions.upload`).join(" and ")} to the key the table checks.`,
1445
+ target,
1446
+ object: tableObject(table)
1447
+ }];
1448
+ });
1449
+ }
1450
+ //#endregion
1451
+ //#region src/cli/doctor/audit-coverage.ts
1452
+ const escapeRegExp$1 = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
1453
+ /** `exempt` entries: a glob, or `{ table, reason }` where `table` is a glob. */
1454
+ function exemptions(value) {
1455
+ return (Array.isArray(value) ? value : []).flatMap((entry) => {
1456
+ if (typeof entry === "string") return [{
1457
+ glob: entry,
1458
+ reason: void 0
1459
+ }];
1460
+ if (typeof entry === "object" && entry !== null && "table" in entry && typeof entry.table === "string") {
1461
+ const reason = "reason" in entry && typeof entry.reason === "string" ? entry.reason.trim() : "";
1462
+ return [{
1463
+ glob: entry.table,
1464
+ reason: reason === "" ? void 0 : reason
1465
+ }];
1466
+ }
1467
+ return [];
1468
+ });
1469
+ }
1470
+ /** `exempt` globs (`public.*_archive`): `*` matches any run of characters. */
1471
+ const globs = (value) => (Array.isArray(value) ? value : []).filter((entry) => typeof entry === "string").map((glob) => new RegExp(`^${glob.split("*").map(escapeRegExp$1).join(".*")}$`, "i"));
1472
+ /**
1473
+ * The module schemas and the app tables SQL modules adopted, which the audit
1474
+ * trigger skips.
1475
+ */
1476
+ function moduleOwned(context) {
1477
+ const schemas = /* @__PURE__ */ new Set(["better_supabase"]);
1478
+ const tables = /* @__PURE__ */ new Set();
1479
+ for (const module of Object.values(context.config.blocks)) {
1480
+ if (module?.schema !== void 0) schemas.add(module.schema);
1481
+ for (const table of Object.values(module?.tables ?? {})) {
1482
+ if (table === null || table === void 0) continue;
1483
+ tables.add(table.includes(".") ? table : `${module?.schema ?? "better_supabase"}.${table}`);
1484
+ }
1485
+ }
1486
+ return {
1487
+ schemas,
1488
+ tables
1489
+ };
1490
+ }
1491
+ /**
1492
+ * Tables in `config.schemas` without the `bsb_audit` trigger, when `audit` is
1493
+ * in `blocks`, and exemptions that give no reason.
1494
+ */
1495
+ function unauditedTables(context) {
1496
+ if (!context.config.blockNames.includes("audit")) return [];
1497
+ const entries = exemptions(context.config.blocks["audit"]?.options?.["exempt"]);
1498
+ const exempt = globs(entries.map((entry) => entry.glob));
1499
+ const owned = moduleOwned(context);
1500
+ const unaudited = catalogOf(context).tables.filter((table) => table.kind === "table" && table.partitionOf === void 0 && 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) => /^bsb?_audit/.test(trigger.name) || trigger.function.split(".").at(-1) === "audit_row_change")).map((table) => ({
1501
+ 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 blocks.audit.options.exempt as \`{ table: '${qualified(table)}', reason: '...' }\`.`,
1502
+ target: qualified(table),
1503
+ object: tableObject(table)
1504
+ }));
1505
+ const unexplained = entries.filter((entry) => entry.reason === void 0).map((entry) => ({
1506
+ message: `blocks.audit.options.exempt lists ${entry.glob} without a reason. Write it as { table: '${entry.glob}', reason: 'why its changes need no audit entry' } so a reviewer can tell an intended gap from a forgotten table.`,
1507
+ target: `blocks.audit.options.exempt:${entry.glob}`
1508
+ }));
1509
+ return [...unaudited, ...unexplained];
1510
+ }
1511
+ //#endregion
1512
+ //#region src/cli/doctor/block-on.ts
1513
+ /** The record types `on` names: list entries or object keys. */
1514
+ function namedTypes(on) {
1515
+ if (Array.isArray(on)) return on.filter((type) => typeof type === "string");
1516
+ return isRecord(on) ? Object.keys(on) : [];
1517
+ }
1518
+ /**
1519
+ * Block entries whose `on` names a type `config.records` doesn't register. The
1520
+ * block's SQL refuses such a type when it renders, so this reports it before
1521
+ * `gen`.
1522
+ */
1523
+ function onUnknownRecordTypes(context) {
1524
+ const registered = new Set(Object.keys(context.config.subjects));
1525
+ return Object.entries(context.config.blocks).flatMap(([instance, entry]) => namedTypes(entry?.on).filter((type) => !registered.has(type)).map((type) => ({
1526
+ message: `blocks.${instance}.on names "${type}", which is not a record type. Register it in config.records, or remove it from on.`,
1527
+ target: `blocks.${instance}.on.${type}`
1528
+ })));
1529
+ }
1530
+ //#endregion
1531
+ //#region src/cli/doctor/modules.ts
1532
+ /** The non-empty string at a dotted path, as membershipsExtension() reads it. */
1533
+ function claimAt(claims, path) {
1534
+ let value = claims;
1535
+ for (const segment of path.split(".")) value = isRecord(value) ? value[segment] : void 0;
1536
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1537
+ }
1538
+ function contractChecks(context) {
1539
+ return customContracts(context.config.blockNames, moduleLayout(context.config, context.config.sql.testsDir, [], void 0, void 0, permissionCatalogKeys(context))).flatMap((contract) => contract.functions.map((fn) => ({
1540
+ module: contract.module,
1541
+ schema: contract.schema,
1542
+ name: fn.name,
1543
+ args: contractSignature(fn, contract.idType),
1544
+ returns: fn.returns.replaceAll("{id}", contract.idType)
1545
+ })));
1546
+ }
1547
+ const escapeRegExp = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
1548
+ const ident = (name) => `"?${escapeRegExp(name)}"?`;
1549
+ /** Whether a `create function schema.name(` appears in the SQL files. */
1550
+ function declared(context, check) {
1551
+ const pattern = new RegExp(`create\\s+(?:or\\s+replace\\s+)?function\\s+${ident(check.schema)}\\.${ident(check.name)}\\s*\\(`, "i");
1552
+ return (context.sqlFiles ?? []).some((file) => pattern.test(file.text));
1553
+ }
1554
+ const normalize$1 = (types) => types.replaceAll(/\s+/g, " ").replaceAll(/\s*,\s*/g, ", ").trim();
1555
+ const PRE_REQUEST_HOOK = `select h.hook,
1556
+ coalesce(pg_catalog.strpos(pg_catalog.pg_get_functiondef(pg_catalog.to_regproc(h.hook)), 'better_supabase.check_request') > 0, false) as calls
1557
+ from (
1558
+ select (
1559
+ select pg_catalog.split_part(setting, '=', 2)
1560
+ from pg_catalog.pg_db_role_setting s
1561
+ join pg_catalog.pg_roles r on r.oid = s.setrole
1562
+ cross join lateral pg_catalog.unnest(s.setconfig) setting
1563
+ where r.rolname = 'authenticator' and s.setdatabase = 0
1564
+ and setting like 'pgrst.db_pre_request=%'
1565
+ limit 1
1566
+ ) as hook
1567
+ ) h`;
1568
+ /**
1569
+ * `rate-limit` in `blocks` without PostgREST's pre-request hook reaching
1570
+ * `check_request()`.
1571
+ */
1572
+ async function rateLimitHook(context) {
1573
+ const db = context.database;
1574
+ if (!context.config.blockNames.includes("rate-limit") || context.config.blocks["rate-limit"]?.options?.["preRequest"] === false || !db || "skipped" in db) return [];
1575
+ let rows;
1576
+ try {
1577
+ rows = await db.query(PRE_REQUEST_HOOK);
1578
+ } catch {
1579
+ return [];
1580
+ }
1581
+ const [row] = rows;
1582
+ if (!row || row.hook === "better_supabase.check_request" || row.calls) return [];
1583
+ return [{
1584
+ message: row.hook === null ? "pgrst.db_pre_request isn't set for authenticator, so better_supabase.check_request() never runs. Apply the migration `better-supabase add --adopt` writes, which sets it." : `pgrst.db_pre_request is ${row.hook}, which doesn't call better_supabase.check_request(), so rate limits never apply. Call it from ${row.hook}.`,
1585
+ target: "authenticator pgrst.db_pre_request"
1586
+ }];
1587
+ }
1588
+ const DANGLING_AUDIT = `select a.target::oid::text as oid
1589
+ from better_supabase.audited_tables a
1590
+ where pg_catalog.to_regclass('better_supabase.audited_tables') is not null
1591
+ and not exists (select 1 from pg_catalog.pg_class c where c.oid = a.target::oid)
1592
+ order by 1`;
1593
+ /** `better_supabase.audited_tables` rows whose table no longer exists. */
1594
+ async function danglingAuditRegistrations(context) {
1595
+ const db = context.database;
1596
+ if (!context.config.blockNames.includes("audit") || !db || "skipped" in db) return [];
1597
+ let rows;
1598
+ try {
1599
+ rows = await db.query(DANGLING_AUDIT);
1600
+ } catch {
1601
+ return [];
1602
+ }
1603
+ return rows.map((row) => ({
1604
+ 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 upgrade\` and apply the migration \`better-supabase add --adopt\` 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)\`.`,
1605
+ target: `better_supabase.audited_tables ${row.oid}`
1606
+ }));
1607
+ }
1608
+ /**
1609
+ * Columns the modules use on the app's tables that the live database lacks:
1610
+ * every table the database has may be the app's.
1611
+ */
1612
+ async function missingAdoptedColumns(context) {
1613
+ const db = context.database;
1614
+ if (!db || "skipped" in db) return [];
1615
+ const tables = adoptedTables(context.config.blocks, appTablesOf(context));
1616
+ if (tables.length === 0) return [];
1617
+ const names = tables.map((table) => `(${literal$1(table.schema)}, ${literal$1(table.table)})`).join(", ");
1618
+ let rows;
1619
+ try {
1620
+ rows = await db.query(`select table_schema as schema, table_name as name, column_name as column
1621
+ from information_schema.columns
1622
+ where (table_schema, table_name) in (${names})`);
1623
+ } catch {
1624
+ return [];
1625
+ }
1626
+ return tables.flatMap((table) => {
1627
+ const target = `${table.schema}.${table.table}`;
1628
+ const object = {
1629
+ kind: "table",
1630
+ schema: table.schema,
1631
+ name: table.table
1632
+ };
1633
+ const present = new Set(rows.filter((row) => row.schema === table.schema && row.name === table.table).map((row) => row.column));
1634
+ if (present.size === 0) return [{
1635
+ message: `blocks.${table.module} uses ${target} for its ${table.logical} table, which the database does not have. Create it, or point blocks.${table.module} at the existing table.`,
1636
+ target,
1637
+ object
1638
+ }];
1639
+ const added = SQL_MODULES[table.module]?.names?.tables[table.logical] !== void 0;
1640
+ const fix = (logical) => added ? `Apply the block's migration (\`better-supabase upgrade\`), which adds it, or map blocks.${table.module}.columns.${table.logical}.${logical} to the column that holds it.` : `Map the column in blocks.${table.module}.on to the one that holds it, or add it.`;
1641
+ return Object.entries(table.columns).filter(([, column]) => !present.has(column)).map(([logical, column]) => ({
1642
+ message: `blocks.${table.module} runs over ${target} and uses ${target}.${column} (${table.logical}.${logical}), but the table has no such column. ${fix(logical)}`,
1643
+ target: `${target}.${column}`,
1644
+ object
1645
+ }));
1646
+ });
1647
+ }
1648
+ 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 upgrade`, then `better-supabase add --adopt`: the module's data migration creates it.";
1649
+ /**
1650
+ * Event triggers the `blocks` create that the database, or every migration,
1651
+ * lacks.
1652
+ */
1653
+ async function missingEventTriggers(context) {
1654
+ const expected = moduleEventTriggers(context.config.blockNames);
1655
+ if (expected.length === 0) return [];
1656
+ const db = context.database;
1657
+ if (db && !("skipped" in db)) {
1658
+ let rows;
1659
+ try {
1660
+ rows = await db.query("select evtname as name from pg_catalog.pg_event_trigger");
1661
+ } catch {
1662
+ return [];
1663
+ }
1664
+ const present = new Set(rows.map((row) => row.name));
1665
+ return expected.filter((trigger) => !present.has(trigger.name)).map((trigger) => ({
1666
+ message: `The database has no event trigger ${trigger.name}, which the ${trigger.module} module creates. ${EVENT_TRIGGER_FIX}`,
1667
+ target: trigger.name
1668
+ }));
1669
+ }
1670
+ const toml = context.configToml;
1671
+ if (toml === void 0) return [];
1672
+ const migrations = (context.sqlFiles ?? []).filter((file) => file.path.startsWith(`${toml.dir}/migrations/`));
1673
+ if (migrations.length === 0) return [];
1674
+ return expected.filter((trigger) => {
1675
+ const pattern = new RegExp(`create\\s+event\\s+trigger\\s+${ident(trigger.name)}\\s`, "i");
1676
+ return !migrations.some((file) => pattern.test(file.text));
1677
+ }).map((trigger) => ({
1678
+ 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}`,
1679
+ target: trigger.name
1680
+ }));
1681
+ }
1682
+ /**
1683
+ * Custom-mode modules whose contract functions the database or SQL files don't
1684
+ * have.
1685
+ */
1686
+ async function missingContracts(context) {
1687
+ const checks = contractChecks(context);
1688
+ if (checks.length === 0) return [];
1689
+ const db = context.database;
1690
+ const live = db && !("skipped" in db) ? db : void 0;
1691
+ if (!live) return checks.filter((check) => !declared(context, check)).map((check) => ({
1692
+ message: `blocks.${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.`,
1693
+ target: `${check.schema}.${check.name}`,
1694
+ object: {
1695
+ kind: "function",
1696
+ schema: check.schema,
1697
+ name: check.name
1698
+ }
1699
+ }));
1700
+ const names = checks.map((check) => `(${literal$1(check.schema)}, ${literal$1(check.name)})`).join(", ");
1701
+ let rows;
1702
+ try {
1703
+ rows = await live.query(`select n.nspname as schema, p.proname as name,
1704
+ oidvectortypes(p.proargtypes) as args,
1705
+ format_type(p.prorettype, null) as returns
1706
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
1707
+ where (n.nspname, p.proname) in (${names})`);
1708
+ } catch (cause) {
1709
+ return [{
1710
+ severity: "info",
1711
+ message: `Reading the custom module contract functions failed: ${errorText(cause)}`,
1712
+ target: "modules"
1713
+ }];
1714
+ }
1715
+ return checks.flatMap((check) => {
1716
+ const found = rows.filter((row) => row.schema === check.schema && row.name === check.name);
1717
+ const target = `${check.schema}.${check.name}`;
1718
+ const object = {
1719
+ kind: "function",
1720
+ schema: check.schema,
1721
+ name: check.name
1722
+ };
1723
+ if (found.length === 0) return [{
1724
+ message: `blocks.${check.module} is in custom mode, but ${target}(${check.args}) doesn't exist. Write it, or switch the module to adopt or managed.`,
1725
+ target,
1726
+ object
1727
+ }];
1728
+ const match = found.find((row) => normalize$1(row.args) === normalize$1(check.args));
1729
+ if (!match) return [{
1730
+ message: `${target} takes (${found.map((row) => row.args).join(" | ")}); the ${check.module} contract calls it with (${check.args}).`,
1731
+ target,
1732
+ object
1733
+ }];
1734
+ if (normalize$1(match.returns) !== normalize$1(check.returns)) return [{
1735
+ message: `${target}(${check.args}) returns ${match.returns}; the ${check.module} contract expects ${check.returns}.`,
1736
+ target,
1737
+ object
1738
+ }];
1739
+ return [];
1740
+ });
1741
+ }
1742
+ /**
1743
+ * With the tenant claim as the active tenant, the hook must put it in the
1744
+ * token.
1745
+ */
1746
+ async function missingTenantClaim(context) {
1747
+ if (!context.config.blockNames.includes("memberships")) return [];
1748
+ if ((context.config.blocks.permissions?.activeTenant ?? DEFAULT_ACTIVE_TENANT) !== "claim") return [];
1749
+ const userId = context.hookUser;
1750
+ const db = context.database;
1751
+ if (!userId || !db || "skipped" in db || !db.session) return [];
1752
+ const claim = context.config.claims.tenant;
1753
+ const findings = [];
1754
+ for (const { config, extras } of configuredHooks(context)) {
1755
+ if (config.hook !== "custom_access_token" || !extras) continue;
1756
+ for (const fn of extras.functions) {
1757
+ const target = `${signatureOf(fn)}:${claim}`;
1758
+ let claims;
1759
+ try {
1760
+ claims = await hookClaims(db, fn, userId);
1761
+ } catch {
1762
+ continue;
1763
+ }
1764
+ if (claims === void 0) continue;
1765
+ if (tenantClaimPaths(claim).some((path) => claimAt(claims, path) !== void 0)) continue;
1766
+ findings.push({
1767
+ message: `${signatureOf(fn)} returns no \`${claim}\` claim for ${userId} (top level or app_metadata), and blocks.permissions.activeTenant is 'claim', so current_tenant_id() is null and membershipsExtension() rejects their requests. Write the claim in the hook, or set blocks.permissions.activeTenant to 'resolver' (with ServerOptions.tenant) or a profile column.`,
1768
+ target,
1769
+ object: {
1770
+ kind: "function",
1771
+ schema: fn.schema,
1772
+ name: fn.name
1773
+ }
1774
+ });
1775
+ }
1776
+ }
1777
+ return findings;
1778
+ }
1779
+ /**
1780
+ * Where a deprecated symbol is used: a call, a qualified name or a claim
1781
+ * lookup.
1782
+ */
1783
+ function deprecationPattern(entry) {
1784
+ const [first = "", second] = entry.symbol.split(".");
1785
+ const qualified = second === void 0 ? ident(first) : `${ident(first)}\\.${ident(second)}`;
1786
+ switch (entry.kind) {
1787
+ case "function": return new RegExp(`${qualified}\\s*\\(`, "i");
1788
+ case "table":
1789
+ case "column": return new RegExp(`(?<![\\w."])${qualified}(?![\\w"])`, "i");
1790
+ case "claim": {
1791
+ const name = escapeRegExp(entry.symbol);
1792
+ return new RegExp(`->>?\\s*'${name}'|#>>?\\s*'\\{[^}]*\\b${name}\\b[^}]*\\}'`, "i");
1793
+ }
1794
+ default: {
1795
+ const never = entry.kind;
1796
+ throw new TypeError(`Unknown deprecation kind ${String(never)}`);
1797
+ }
1798
+ }
1799
+ }
1800
+ const bareWord = (name) => new RegExp(`(?<![\\w"])${ident(name)}(?![\\w"])`, "i");
1801
+ /**
1802
+ * The line where `text` uses a deprecated symbol. A column also counts
1803
+ * unqualified (`m.org_id`) in a statement that names its table, or in a policy
1804
+ * on that table.
1805
+ */
1806
+ function lineOfUse(entry, text, policyTable) {
1807
+ const direct = lineOf(text, deprecationPattern(entry));
1808
+ if (direct !== void 0 || entry.kind !== "column") return direct;
1809
+ const [table = "", column = ""] = entry.symbol.split(".");
1810
+ let offset = 0;
1811
+ for (const statement of text.split(";")) {
1812
+ const named = policyTable === table || bareWord(table).test(statement);
1813
+ const at = statement.search(bareWord(column));
1814
+ if (named && at !== -1) return text.slice(0, offset + at).split("\n").length;
1815
+ offset += statement.length + 1;
1816
+ }
1817
+ }
1818
+ /**
1819
+ * SQL files and policies that still use a symbol a SQL module deprecated or
1820
+ * removed.
1821
+ */
1822
+ function deprecatedSymbols(context) {
1823
+ const configured = new Set(Object.values(context.config.claims));
1824
+ const appsName = keepsAppsName(context);
1825
+ const entries = moduleDeprecations().filter((entry) => context.config.blockNames.includes(entry.module) && !(entry.kind === "claim" && configured.has(entry.symbol)) && !appsName(entry));
1826
+ if (entries.length === 0) return [];
1827
+ const files = (context.sqlFiles ?? []).filter((file) => !/(^|\/)supabase\/migrations\//.test(file.path) && moduleFileVersion(file.text) === void 0);
1828
+ const findings = [];
1829
+ for (const entry of entries) {
1830
+ const status = entry.removed === void 0 ? `deprecated since ${entry.since}` : `removed in ${entry.removed}`;
1831
+ const advice = `the ${entry.module} module's ${entry.kind} ${entry.symbol}, ${status}. Use ${entry.use}.`;
1832
+ for (const file of files) {
1833
+ const line = lineOfUse(entry, file.text);
1834
+ if (line === void 0) continue;
1835
+ findings.push({
1836
+ message: `${file.path} uses ${advice}`,
1837
+ target: `${file.path}:${entry.symbol}`,
1838
+ location: {
1839
+ file: file.path,
1840
+ line
1841
+ }
1842
+ });
1843
+ }
1844
+ for (const table of catalogOf(context).tables) for (const policy of table.policies) {
1845
+ if (lineOfUse(entry, `${policy.using ?? ""}\n${policy.check ?? ""}`, table.name) === void 0) continue;
1846
+ findings.push({
1847
+ message: `Policy "${policy.name}" on ${qualified(table)} uses ${advice}`,
1848
+ target: `${qualified(table)}.${policy.name}:${entry.symbol}`,
1849
+ object: policyObject(table, policy)
1850
+ });
1851
+ }
1852
+ }
1853
+ return findings;
1854
+ }
1855
+ /** Module triggers and the equivalent triggers they replace. */
1856
+ const EQUIVALENT_TRIGGERS = [{
1857
+ trigger: "bsb_updated_at",
1858
+ call: "track_updated_at",
1859
+ pattern: /updated_at|moddatetime|touch/i
1860
+ }, {
1861
+ trigger: "bsb_audit",
1862
+ call: "audit",
1863
+ pattern: /audit/i
1864
+ }];
1865
+ /** Tables where a module trigger and an older trigger do the same work. */
1866
+ function duplicateTriggers(context) {
1867
+ return catalogOf(context).tables.flatMap((table) => EQUIVALENT_TRIGGERS.flatMap(({ trigger, call, pattern }) => {
1868
+ if (!table.triggers.some((entry) => entry.name.replace(/^bs_/, "bsb_") === trigger)) return [];
1869
+ return table.triggers.filter((entry) => entry.name.replace(/^bs_/, "bsb_") !== trigger && pattern.test(entry.function.split(".").at(-1) ?? "")).map((entry) => ({
1870
+ message: `${qualified(table)} has ${trigger} and ${entry.name} (${entry.function}), so both run on every write. Drop ${entry.name}, or run \`select better_supabase.${call}('${qualified(table)}', replace_trigger => true)\` in a migration.`,
1871
+ target: `${qualified(table)}.${entry.name}`,
1872
+ object: tableObject(table)
1873
+ }));
1874
+ }));
1875
+ }
1876
+ /**
1877
+ * RLS tables in `config.schemas` without a restrictive `session_active()`
1878
+ * policy, when `sessions` is in `blocks`.
1879
+ */
1880
+ function sessionlessTables(context) {
1881
+ if (!context.config.blockNames.includes("sessions")) return [];
1882
+ const exclude = globs(context.config.blocks["sessions"]?.options?.["exclude"]);
1883
+ const owned = moduleOwned(context);
1884
+ 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) => ({
1885
+ message: `${qualified(table)} has no restrictive session_active() policy, so a signed-out or revoked session keeps its access until the token expires. Set blocks['sessions'].options.policies to true and run \`better-supabase upgrade\`, add the policy by hand, or list the table in blocks['sessions'].options.exclude.`,
1886
+ target: qualified(table),
1887
+ object: tableObject(table)
1888
+ }));
1889
+ }
1890
+ /**
1891
+ * `better_supabase` and every `blocks.*.schema` that `[api] schemas` serves
1892
+ * through the Data API.
1893
+ */
1894
+ function exposedModuleSchemas(context) {
1895
+ if (context.config.blockNames.length === 0) return [];
1896
+ const moduleSchemas = /* @__PURE__ */ new Set(["better_supabase"]);
1897
+ for (const module of Object.values(context.config.blocks)) if (module?.schema !== void 0) moduleSchemas.add(module.schema);
1898
+ return exposedSchemas(context).filter((schema) => moduleSchemas.has(schema)).map((schema) => ({
1899
+ 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 blocks.<module>.api to an exposed schema such as "api" so \`better-supabase add\` writes security invoker entry points there, and call them with rpcTransport(supabase, { schema: "api" }).`,
1900
+ target: schema
1901
+ }));
1902
+ }
1903
+ function migrationOptions(context) {
1904
+ const module = new Set(context.config.blockNames);
1905
+ return migrationOptionUses(context.config.blocks).filter((use) => module.has(use.module)).map((use) => ({
1906
+ message: `${use.message} It exists to adopt an existing schema; remove it once your data matches the managed default.`,
1907
+ target: `blocks.${use.module}.options.${use.option}`
1908
+ }));
1909
+ }
1910
+ /**
1911
+ * A roles table shared by tenant and platform roles without `where` on both
1912
+ * sides.
1913
+ */
1914
+ function sharedRoleTables(context) {
1915
+ if (context.config.blockNames.length === 0) return [];
1916
+ const access = accessModule(context);
1917
+ if (access.kind !== "provider") return [];
1918
+ let problems;
1919
+ try {
1920
+ problems = sharedRolesProblems(context.config.blockNames, moduleLayout(context.config, context.config.sql.testsDir, [], entitlementsModule(context), access.access, permissionCatalogKeys(context)));
1921
+ } catch {
1922
+ return [];
1923
+ }
1924
+ return problems.map(({ setting, message }) => ({
1925
+ message: `${message} Without it, ${setting.startsWith("blocks.memberships.") ? "an organization invitation, accept or role change can resolve a platform role" : "a platform invitation can resolve a tenant role"}.`,
1926
+ target: setting
1927
+ }));
1928
+ }
1929
+ const MODULE_RULES = [
1930
+ {
1931
+ code: "BS307",
1932
+ severity: "error",
1933
+ title: "Custom SQL module without its contract",
1934
+ description: "A module in `blocks` 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.",
1935
+ check: missingContracts
1936
+ },
1937
+ {
1938
+ code: "BS308",
1939
+ severity: "warning",
1940
+ title: "Tenant claim the hook does not write",
1941
+ description: "With the `tenant` module and `blocks.permissions.activeTenant: 'claim'`, `current_tenant_id()` and `membershipsExtension()` read the tenant from the `auth.organization.claim` 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.",
1942
+ check: missingTenantClaim
1943
+ },
1944
+ {
1945
+ code: "BS309",
1946
+ severity: "warning",
1947
+ title: "Deprecated SQL module symbol",
1948
+ 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.",
1949
+ check: deprecatedSymbols
1950
+ },
1951
+ {
1952
+ code: "BS310",
1953
+ severity: "warning",
1954
+ title: "Duplicate module trigger",
1955
+ description: "A table has a module trigger (`bsb_updated_at`, `bsb_audit`) and another trigger that does the same work, so both run on every write.",
1956
+ check: duplicateTriggers
1957
+ },
1958
+ {
1959
+ code: "BS313",
1960
+ severity: "warning",
1961
+ title: "Rate limits not wired to PostgREST",
1962
+ description: "The `rate-limit` module is in `blocks`, 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.",
1963
+ check: rateLimitHook
1964
+ },
1965
+ {
1966
+ code: "BS312",
1967
+ severity: "error",
1968
+ title: "Module schema exposed through the Data API",
1969
+ description: "`[api] schemas` in `config.toml` lists `better_supabase` or a `blocks.*.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.",
1970
+ check: exposedModuleSchemas
1971
+ },
1972
+ {
1973
+ code: "BS314",
1974
+ severity: "warning",
1975
+ title: "Migration-only module option",
1976
+ description: "A module over the app's tables sets an option that only exists to match an existing schema: plain invitation tokens, webhook secrets or credentials 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.",
1977
+ check: migrationOptions
1978
+ },
1979
+ {
1980
+ code: "BS315",
1981
+ severity: "warning",
1982
+ title: "Table without an audit trigger",
1983
+ description: "The `audit` module is in `blocks`, and a table in `schemas` has no `bsb_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; `blocks.audit.options.exempt` lists more, as `{ table, reason }` entries where `table` is a `schema.table` glob such as `public.*_archive`; a bare glob string still works but gets a finding that asks for the reason. Partitions are covered by their parent.",
1984
+ check: unauditedTables
1985
+ },
1986
+ {
1987
+ code: "BS319",
1988
+ severity: "error",
1989
+ title: "SQL alters a reserved role",
1990
+ 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.",
1991
+ check: reservedRoles
1992
+ },
1993
+ {
1994
+ code: "BS320",
1995
+ severity: "warning",
1996
+ title: "Table without the session policy",
1997
+ description: "The `sessions` module is in `blocks`, 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. `blocks['sessions'].options.policies` writes the policy on every table; `options.exclude` lists the tables to skip, as `schema.table` globs.",
1998
+ check: sessionlessTables
1999
+ },
2000
+ {
2001
+ code: "BS322",
2002
+ severity: "warning",
2003
+ title: "Audit registration of a dropped table",
2004
+ description: "The `audit` module is in `blocks`, 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.",
2005
+ check: danglingAuditRegistrations
2006
+ },
2007
+ {
2008
+ code: "BS323",
2009
+ severity: "warning",
2010
+ title: "Module event trigger missing",
2011
+ description: "A module in `blocks` creates an event trigger (`bsb_audit_forget_dropped` for `audit`, `bsb_ensure_rls` for `rls-guard`) 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.",
2012
+ check: missingEventTriggers
2013
+ },
2014
+ {
2015
+ code: "BS324",
2016
+ severity: "error",
2017
+ title: "Shared roles table without a role condition",
2018
+ 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. `upgrade` refuses the config; set `where` on both, such as `{row}.scope = 'organization'` and `{row}.scope = 'system'`.",
2019
+ check: sharedRoleTables
2020
+ },
2021
+ {
2022
+ code: "BS325",
2023
+ severity: "warning",
2024
+ title: "Audit log readable only as the service role",
2025
+ 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.",
2026
+ check: auditUserSession
2027
+ },
2028
+ {
2029
+ code: "BS326",
2030
+ severity: "warning",
2031
+ title: "Admin Auth call without a secret key in env files",
2032
+ description: "`deleteAccount` and `bsb.admin()` need `SUPABASE_SECRET_KEY`. The env files doctor reads do not set it, so those calls fail at run time.",
2033
+ check: missingSecretKey
2034
+ },
2035
+ {
2036
+ code: "BS327",
2037
+ severity: "warning",
2038
+ title: "aal2 required without MFA enabled",
2039
+ 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.",
2040
+ check: aal2WithoutMfa
2041
+ },
2042
+ {
2043
+ code: "BS330",
2044
+ severity: "error",
2045
+ title: "App table without a column a block uses",
2046
+ description: "A block runs over a table of the app's, and the live database lacks a column its SQL names, so its functions fail until the block's migration adds it. The check reads every column the module's SQL names (`blocks.<name>.tables` and `columns`, with the defaults for unmapped ones); columns mapped to `null` are skipped. A column on a table that `blocks.<name>.on` names isn't added by the block: map it or add it.",
2047
+ check: missingAdoptedColumns
2048
+ },
2049
+ {
2050
+ code: "BS331",
2051
+ severity: "error",
2052
+ title: "Subject registry names a missing table or column",
2053
+ description: "An entry in `config.records` names a table, or a column of it (`id`, `tenant`, `title`, `deletedAt`, the `url` placeholders, `rank` and `search`), that the snapshot lacks. `subject_title`, `subject_url`, `can_subject` and every module that falls back to the registry then fail when they run.",
2054
+ check: missingSubjectColumns
2055
+ },
2056
+ {
2057
+ code: "BS332",
2058
+ severity: "warning",
2059
+ title: "Subject table without RLS",
2060
+ description: "A table in `config.records` has row level security disabled. `can_subject` treats a row the caller can read as visible, so without RLS every signed-in user passes the visibility half of the check for every row.",
2061
+ check: subjectsWithoutRls
2062
+ },
2063
+ {
2064
+ code: "BS345",
2065
+ severity: "warning",
2066
+ title: "Attachments subject on block-wide keys with stricter RLS",
2067
+ description: "An `attachments` subject has no `read` or `upload` key of its own, so its files are checked with the block-wide `attachments.read` and `attachments.upload` keys, but the subject table's own policies check more than tenant membership: a permission key of their own, an owner or `auth.uid()` comparison, or another condition. Any member of the tenant then reads and uploads files of rows they can't open. Doctor recognizes `current_tenant_id()`, `tenant_ids_with`, `member_<tenant>_ids`, `can('tenant', ...)` and `member_can` with the block's key as tenant membership. Set `subjects.<type>.permissions.upload`, or `blocks.attachments.on.<type>.permissions` for `read`, to the keys the table checks.",
2068
+ check: attachmentSubjectsOnDefaults
2069
+ },
2070
+ {
2071
+ code: "BS346",
2072
+ severity: "warning",
2073
+ title: "Block on names an unknown record type",
2074
+ description: "A block entry's `on` lists a type, or has a key, that `config.records` doesn't register, so the block's SQL refuses it when `gen` renders the migration. Register the type in `config.records` or remove it from `on`.",
2075
+ check: onUnknownRecordTypes
2076
+ },
2077
+ {
2078
+ code: "BS347",
2079
+ severity: "warning",
2080
+ title: "Block policy widens an app table's access",
2081
+ description: "A block runs over a table of the app's and adds its own permissive policy there, named `bsb_<table>_<command>` so it never replaces one of the app's. Postgres ORs permissive policies, so when the app has its own policies for that command and none has the same condition (or `true`) for the same roles, the block's policy can let a user reach rows the app's policies keep closed. A block condition that is the app's, or one of its `or` operands, with more `and` operands is narrower and isn't reported, nor is a command the app has no policy for, which is the block's to grant. Doctor compares the conditions as text. When an app policy covers the block's in this sense, `upgrade` leaves the block's policy out. Repeat the block's condition in the app's policy when it is what the app means; otherwise narrow the permission key the block checks, or drop the `bsb_` policy in a schema file that runs after the block's.",
2082
+ check: widenedAppTablePolicies
2083
+ },
2084
+ {
2085
+ code: "BS348",
2086
+ severity: "info",
2087
+ title: "App table keeps its own updated_at trigger",
2088
+ description: "A block runs over a table of the app's that already has a trigger keeping `updated_at` current (one that calls `set_updated_at`, `moddatetime` or a function that sets `new.updated_at`), so the block adds no `bsb_updated_at` trigger there. Keep that trigger firing on update; if you drop it, run `better-supabase upgrade` to add the block's.",
2089
+ check: keptUpdatedAtTriggers
2090
+ },
2091
+ {
2092
+ code: "BS335",
2093
+ severity: "warning",
2094
+ title: "Partitioned table created unpartitioned",
2095
+ description: "A block's `options.partitioned` is on (`audit`, `outbox`, `usage` with `history`, `webhooks` or `notifications`), but its table was created before it: the primary key is `id` alone, so the table isn't partitioned by month and its purge deletes row by row. The tables are `better_supabase.audit_events`, `outbox_events`, `usage_history`, `webhook_deliveries` and `notification_delivery_events`. Postgres can't convert a table in place. Create a partitioned table, copy the rows over and swap the names in a maintenance window, or turn the option off.",
2096
+ check: unpartitionedTables
2097
+ }
2098
+ ];
2099
+ /**
2100
+ * The table each block's `options.partitioned` partitions, and the option it
2101
+ * also needs.
2102
+ */
2103
+ const PARTITIONED_TABLES = [
2104
+ {
2105
+ block: "audit",
2106
+ table: "audit_events"
2107
+ },
2108
+ {
2109
+ block: "outbox",
2110
+ table: "outbox_events"
2111
+ },
2112
+ {
2113
+ block: "usage",
2114
+ table: "usage_history",
2115
+ requires: "history"
2116
+ },
2117
+ {
2118
+ block: "webhooks",
2119
+ table: "webhook_deliveries"
2120
+ },
2121
+ {
2122
+ block: "notifications",
2123
+ table: "notification_delivery_events"
2124
+ }
2125
+ ];
2126
+ function unpartitionedTables(context) {
2127
+ const { tables } = catalogOf(context);
2128
+ return PARTITIONED_TABLES.flatMap(({ block, table, requires }) => {
2129
+ const options = context.config.blocks[block]?.options ?? {};
2130
+ if (!context.config.blockNames.includes(block) || options["partitioned"] !== true || requires !== void 0 && options[requires] !== true) return [];
2131
+ if (tables.find((candidate) => candidate.schema === "better_supabase" && candidate.name === table)?.primaryKey.length !== 1) return [];
2132
+ return [{
2133
+ message: `better_supabase.${table} isn't partitioned, so blocks.${block}.options.partitioned has no effect on it. Postgres can't convert it in place: copy the rows into a partitioned table in a maintenance window, or turn the option off.`,
2134
+ target: `better_supabase.${table}`
2135
+ }];
2136
+ });
2137
+ }
2138
+ function subjectTables(context) {
2139
+ const catalog = catalogOf(context);
2140
+ return Object.entries(context.config.subjects).flatMap(([type, entry]) => {
2141
+ const [schema, name] = entry.table.includes(".") ? entry.table.split(".", 2) : ["public", entry.table];
2142
+ if (schema === void 0 || name === void 0) return [];
2143
+ if (!context.snapshot.schemas.includes(schema)) return [];
2144
+ return [{
2145
+ type,
2146
+ entry,
2147
+ schema,
2148
+ name,
2149
+ table: catalog.tables.find((candidate) => candidate.schema === schema && candidate.name === name)
2150
+ }];
2151
+ });
2152
+ }
2153
+ function missingSubjectColumns(context) {
2154
+ return subjectTables(context).flatMap(({ type, entry, schema, name, table }) => {
2155
+ const target = `${schema}.${name}`;
2156
+ if (!table) return [{
2157
+ message: `subjects.${type} names ${target}, which the database does not have. Create the table, or point subjects.${type}.table at the existing one.`,
2158
+ target
2159
+ }];
2160
+ const present = new Set(table.columns.map((column) => column.name));
2161
+ return subjectColumns(entry).filter(([, column]) => !present.has(column)).map(([path, column]) => ({
2162
+ message: `subjects.${type}.${path} names ${target}.${column}, but the table has no such column. Map it to the column that holds the value, or add the column.`,
2163
+ target: `${target}.${column}`,
2164
+ object: tableObject(table)
2165
+ }));
2166
+ });
2167
+ }
2168
+ function subjectsWithoutRls(context) {
2169
+ return subjectTables(context).flatMap(({ type, table }) => table?.kind === "table" && !table.rls ? [{
2170
+ message: `subjects.${type} is ${table.schema}.${table.name}, which has row level security disabled, so can_subject treats every row as visible. Enable RLS and add the policies that scope it.`,
2171
+ target: `${table.schema}.${table.name}`,
2172
+ object: tableObject(table)
2173
+ }] : []);
2174
+ }
2175
+ function auditUserSession(context) {
2176
+ if (!context.config.blockNames.includes("audit")) return [];
2177
+ const options = context.config.blocks["audit"]?.options ?? {};
2178
+ const roles = Array.isArray(options["eventRoles"]) ? options["eventRoles"].filter((role) => typeof role === "string") : ["service_role"];
2179
+ if (options["readPolicy"] === true && roles.includes("authenticated")) return [];
2180
+ if (!context.sources.some((file) => /\baudit\.(list|listAll)\s*\(/.test(file.text))) return [];
2181
+ return [{
2182
+ message: "blocks.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.",
2183
+ target: "blocks.audit"
2184
+ }];
2185
+ }
2186
+ function missingSecretKey(context) {
2187
+ if (!context.sources.some((file) => /\bdeleteAccount\b|\.admin\s*\(|supabaseAdmin\b/.test(file.text))) return [];
2188
+ if (context.envFiles.some((file) => /^\s*(?:SUPABASE_SECRET_KEY|SUPABASE_SERVICE_ROLE_KEY)\s*=/m.test(file.text))) return [];
2189
+ return [{
2190
+ message: "The app calls deleteAccount or bsb.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.",
2191
+ target: "SUPABASE_SECRET_KEY"
2192
+ }];
2193
+ }
2194
+ function aal2WithoutMfa(context) {
2195
+ const toml = context.configToml;
2196
+ if (!toml) return [];
2197
+ if (!context.sources.some((file) => /requireAal\s*\(\s*['"]aal2['"]/.test(file.text))) return [];
2198
+ const enroll = tomlGet(toml.document, [
2199
+ "auth",
2200
+ "mfa",
2201
+ "totp",
2202
+ "enroll_enabled"
2203
+ ]);
2204
+ const verify = tomlGet(toml.document, [
2205
+ "auth",
2206
+ "mfa",
2207
+ "totp",
2208
+ "verify_enabled"
2209
+ ]);
2210
+ if (enroll === true && verify === true) return [];
2211
+ return [{
2212
+ 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.",
2213
+ target: toml.path,
2214
+ location: {
2215
+ file: toml.path,
2216
+ line: lineOf(toml.text, /^\[auth\.mfa\.totp\]/) ?? 1
2217
+ }
2218
+ }];
2219
+ }
2220
+ //#endregion
2221
+ //#region src/cli/doctor/not-valid.ts
2222
+ /** `(organization_id IS NOT NULL)` as `organization_id`. */
2223
+ const NOT_NULL = /^\(*\s*("(?:[^"]|"")+"|[\w$]+)\s+is\s+not\s+null\s*\)*$/iu;
2224
+ const notNullColumn = (expression) => NOT_NULL.exec(expression)?.[1]?.replace(/^"(.*)"$/su, "$1");
2225
+ /**
2226
+ * Lifts the statement timeout of the introspection connection for one
2227
+ * statement, since a validate scans the whole table.
2228
+ */
2229
+ const untimed = (sql) => `begin; set local statement_timeout = 0; ${sql} commit;`;
2230
+ const NOT_VALID_RULES = [{
2231
+ code: "BS236",
2232
+ severity: "info",
2233
+ title: "Not valid check ready to validate",
2234
+ description: "A check constraint was added `not valid`, the expand step for a column that existing rows don't fill yet, and no row fails it any more. `doctor --fix` runs `alter table ... validate constraint`, which scans the table without blocking writes. After that, `set not null` on the column needs no scan. A check that rows still fail is reported as info, with the `db backfill` command for a not-null check; BS238 reports the scope column of an enriched app table instead. Needs a database; saved snapshots skip it.",
2235
+ async check(context) {
2236
+ const db = context.database;
2237
+ if (!db || "skipped" in db) return [];
2238
+ let checks;
2239
+ try {
2240
+ checks = await db.query(`select n.nspname as schema, t.relname as table, c.conname as name, pg_get_expr(c.conbin, c.conrelid) as expression
2241
+ from pg_catalog.pg_constraint c
2242
+ join pg_catalog.pg_class t on t.oid = c.conrelid
2243
+ join pg_catalog.pg_namespace n on n.oid = t.relnamespace
2244
+ where c.contype = 'c' and not c.convalidated and t.relkind in ('r', 'p')
2245
+ and n.nspname not like 'pg\\_%'
2246
+ and n.nspname not in (${[...PLATFORM_SCHEMAS].map(literal$1).join(", ")})
2247
+ order by 1, 2, 3`);
2248
+ } catch (cause) {
2249
+ return [{ message: `Could not read the not valid checks (${db.describe}): ${errorText(cause)}` }];
2250
+ }
2251
+ const findings = [];
2252
+ const owned = new Set(context.codes?.includes("BS238") === true ? scopedAppTables(context).filter((table) => table.enriched).map((table) => `public.${table.name}:${scopeCheckName(table)}`) : []);
2253
+ for (const check of checks) {
2254
+ const table = `${ident$1(check.schema)}.${ident$1(check.table)}`;
2255
+ const where = `${check.schema}.${check.table}`;
2256
+ const target = `${where}:${check.name}`;
2257
+ const object = {
2258
+ kind: "table",
2259
+ schema: check.schema,
2260
+ name: check.table
2261
+ };
2262
+ const column = notNullColumn(check.expression);
2263
+ let failing;
2264
+ try {
2265
+ const [row] = await db.query(`select exists (select 1 from ${table} where not (${check.expression})) as failing`);
2266
+ failing = row?.failing === true;
2267
+ } catch (cause) {
2268
+ findings.push({
2269
+ message: `Could not check ${check.name} on ${where}: ${errorText(cause)}`,
2270
+ target,
2271
+ object
2272
+ });
2273
+ continue;
2274
+ }
2275
+ if (failing) {
2276
+ if (owned.has(target)) continue;
2277
+ findings.push({
2278
+ message: `${where} has the not valid check ${check.name} (${check.expression}), and rows still fail it.${column === void 0 ? " Fix those rows, then run `better-supabase doctor --fix`." : ` Fill ${column} with \`better-supabase db backfill ${check.schema === "public" ? "" : `${check.schema}.`}${check.table}.${column} --from <table.column> via <column>\` (or --value), then run \`better-supabase doctor --fix\`.`}`,
2279
+ target,
2280
+ object
2281
+ });
2282
+ continue;
2283
+ }
2284
+ findings.push({
2285
+ message: `${where} has the not valid check ${check.name} (${check.expression}), and no row fails it. Run \`better-supabase doctor --fix\` to validate it${column === void 0 ? "." : `; then \`alter table ${table} alter column ${ident$1(column)} set not null\` needs no scan.`}`,
2286
+ target,
2287
+ object,
2288
+ fix: untimed(`alter table ${table} validate constraint ${ident$1(check.name)};`)
2289
+ });
2290
+ }
2291
+ return findings;
2292
+ }
2293
+ }];
2294
+ //#endregion
2295
+ //#region src/cli/doctor/pgdelta.ts
2296
+ /** `config.toml` when the project runs the pg-delta workflow. */
2297
+ function pgdeltaToml(context) {
2298
+ const toml = context.configToml;
2299
+ return toml !== void 0 && workflowOf(context) === "pg-delta" ? toml : void 0;
2300
+ }
2301
+ /**
2302
+ * Comments blanked out, so a commented-out statement never matches and line
2303
+ * numbers stay.
2304
+ */
2305
+ function withoutComments(text) {
2306
+ return text.replaceAll(/\/\*[\s\S]*?\*\//g, (comment) => comment.replaceAll(/[^\n]/g, " ")).replaceAll(/--[^\n]*/g, (comment) => " ".repeat(comment.length));
2307
+ }
2308
+ const lineAt$1 = (text, index) => text.slice(0, index).split("\n").length;
2309
+ /**
2310
+ * The declarative schema files pg-delta loads, outside the SQL module files
2311
+ * better-supabase writes, when the project diffs with pg-delta.
2312
+ */
2313
+ function pgdeltaSchemaFiles(context) {
2314
+ const toml = pgdeltaToml(context);
2315
+ if (toml === void 0) return [];
2316
+ const schemas = `${declarativeSchemasDir(toml, "pg-delta")}/`;
2317
+ return (context.sqlFiles ?? []).filter((file) => file.path.startsWith(schemas) && !/^-- @(?:bsb-block|bs-module) /m.test(file.text));
2318
+ }
2319
+ 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;
2320
+ /** Grantees a bulk grant must not reach: the Data API roles and `public`. */
2321
+ const API_ROLES = /\b(anon|authenticated|public)\b/i;
2322
+ /** `do` blocks; group 2 is the body. */
2323
+ const DO_BLOCK = /\bdo\s+(\$[\w]*\$)([\s\S]*?)\1/gi;
2324
+ const CATALOG = /\b(information_schema\.\w+|(?:pg_catalog\.)?pg_(?:class|tables|views|proc|namespace|attribute|trigger|constraint|index|type|policies|matviews))\b/i;
2325
+ const EXECUTES = /\b(execute|perform)\b/i;
2326
+ function bulkGrants(context) {
2327
+ return pgdeltaSchemaFiles(context).flatMap((file) => {
2328
+ const text = withoutComments(file.text);
2329
+ return [...text.matchAll(BULK_GRANT)].filter((match) => API_ROLES.test(match[3])).map((match) => ({
2330
+ 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.`,
2331
+ target: file.path,
2332
+ location: {
2333
+ file: file.path,
2334
+ line: lineAt$1(text, match.index)
2335
+ }
2336
+ }));
2337
+ });
2338
+ }
2339
+ function catalogLoops(context) {
2340
+ return pgdeltaSchemaFiles(context).flatMap((file) => {
2341
+ const text = withoutComments(file.text);
2342
+ return [...text.matchAll(DO_BLOCK)].flatMap((match) => {
2343
+ const body = match[2];
2344
+ const catalog = CATALOG.exec(body);
2345
+ if (!catalog || !EXECUTES.test(body)) return [];
2346
+ return [{
2347
+ 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.`,
2348
+ target: file.path,
2349
+ location: {
2350
+ file: file.path,
2351
+ line: lineAt$1(text, match.index)
2352
+ }
2353
+ }];
2354
+ });
2355
+ });
2356
+ }
2357
+ const CREATE_EXTENSION = /\bcreate\s+extension\s+(?:if\s+not\s+exists\s+)?"?([\w-]+)"?/gi;
2358
+ /**
2359
+ * `text` without comments and with string literals blanked, keeping every
2360
+ * offset, so an error message that tells the reader to `create extension`
2361
+ * doesn't count as one.
2362
+ */
2363
+ const statements = (text) => withoutComments(text).replaceAll(/'(?:[^']|'')*'/g, (literal) => literal.replaceAll(/[^\n]/g, " "));
2364
+ /**
2365
+ * Extensions a pg-delta schema file creates, SQL module files included, that no
2366
+ * migration creates. pg-delta can leave an extension that owns its own schema
2367
+ * (`pgmq`) out of the generated migration.
2368
+ */
2369
+ function missingExtensions(context) {
2370
+ const toml = pgdeltaToml(context);
2371
+ if (toml === void 0) return [];
2372
+ const schemas = `${declarativeSchemasDir(toml, "pg-delta")}/`;
2373
+ const files = context.sqlFiles ?? [];
2374
+ const migrations = files.filter((file) => file.path.startsWith(`${toml.dir}/migrations/`));
2375
+ if (migrations.length === 0) return [];
2376
+ const created = new Set(migrations.flatMap((file) => [...statements(file.text).matchAll(CREATE_EXTENSION)].map((match) => match[1].toLowerCase())));
2377
+ const seen = /* @__PURE__ */ new Set();
2378
+ return files.filter((file) => file.path.startsWith(schemas)).flatMap((file) => {
2379
+ const text = statements(file.text);
2380
+ return [...text.matchAll(CREATE_EXTENSION)].flatMap((match) => {
2381
+ const name = match[1].toLowerCase();
2382
+ if (created.has(name) || seen.has(name)) return [];
2383
+ seen.add(name);
2384
+ const module = /^-- @(?:bsb-block|bs-module) /m.test(file.text);
2385
+ return [{
2386
+ 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 upgrade`: 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.`}`,
2387
+ target: file.path,
2388
+ location: {
2389
+ file: file.path,
2390
+ line: lineAt$1(text, match.index)
2391
+ }
2392
+ }];
2393
+ });
2394
+ });
2395
+ }
2396
+ /** Statements in declarative schema files that pg-delta can't order. */
2397
+ const PGDELTA_RULES = [
2398
+ {
2399
+ code: "BS317",
2400
+ severity: "warning",
2401
+ title: "Bulk grant in a pg-delta schema file",
2402
+ 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`.",
2403
+ check: bulkGrants
2404
+ },
2405
+ {
2406
+ code: "BS318",
2407
+ severity: "warning",
2408
+ title: "Catalog loop in a pg-delta schema file",
2409
+ 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.",
2410
+ check: catalogLoops
2411
+ },
2412
+ {
2413
+ code: "BS321",
2414
+ severity: "warning",
2415
+ title: "Extension missing from the migrations under pg-delta",
2416
+ 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.",
2417
+ check: missingExtensions
2418
+ }
2419
+ ];
2420
+ //#endregion
2421
+ //#region src/cli/doctor/policies.ts
2422
+ const AUTH_ROLE = /\bauth"?\s*\.\s*"?role"?\s*\(\s*\)/i;
2423
+ /** A check of who is calling: the JWT, the user id, or the session role. */
2424
+ 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;
2425
+ const USER_METADATA = /\buser_metadata\b/i;
2426
+ /** Privileges an API role never needs on a table. */
2427
+ const NEEDLESS = [
2428
+ "TRUNCATE",
2429
+ "REFERENCES",
2430
+ "TRIGGER"
2431
+ ];
2432
+ const policyText = (policy) => withoutStrings(`${policy.using ?? ""} ${policy.check ?? ""}`);
2433
+ /** A permissive policy lets `role` run `command` on the table. */
2434
+ const allows = (table, command, role) => table.policies.some((policy) => policy.permissive && (policy.command === "all" || policy.command === command) && appliesTo(policy, role));
2435
+ /** Policy commands on `storage.objects` the SQL files create, by policy name. */
2436
+ function storagePolicies(context) {
2437
+ const commands = /* @__PURE__ */ new Map();
2438
+ const pattern = /create\s+policy\s+("(?:[^"]|"")+"|\S+)\s+on\s+"?storage"?\s*\.\s*"?objects"?\b([^;]*)/gi;
2439
+ for (const file of context.sqlFiles ?? []) for (const match of withoutStrings(file.text).matchAll(pattern)) {
2440
+ const name = match[1];
2441
+ if (commands.has(name)) continue;
2442
+ const command = /\bfor\s+(select|insert|update|delete|all)\b/i.exec(match[2])?.[1];
2443
+ commands.set(name, command?.toLowerCase() ?? "all");
2444
+ }
2445
+ return commands;
2446
+ }
2447
+ const POLICY_RULES = [
2448
+ {
2449
+ code: "BS109",
2450
+ severity: "warning",
2451
+ title: "auth.role() in a policy or function",
2452
+ description: "Supabase deprecated `auth.role()`. Scope the policy with `to authenticated`, or read the claim with `(select auth.jwt() ->> 'role')`.",
2453
+ check: (context) => {
2454
+ const findings = [];
2455
+ for (const table of catalogOf(context).tables) for (const policy of table.policies) {
2456
+ if (!AUTH_ROLE.test(policyText(policy))) continue;
2457
+ findings.push({
2458
+ message: `Policy "${policy.name}" on ${qualified(table)} calls auth.role(), which Supabase deprecated. Use \`to authenticated\` on the policy, or \`(select auth.jwt() ->> 'role')\`.`,
2459
+ target: `${qualified(table)}.${policy.name}`,
2460
+ object: policyObject(table, policy)
2461
+ });
2462
+ }
2463
+ const bodies = new Map(policyHelpers(context));
2464
+ for (const fn of context.snapshot.generator.functions) {
2465
+ if (!context.config.schemas.includes(fn.schema) || !fn.definition) continue;
2466
+ bodies.set(`${fn.schema}.${fn.name}`, withoutStrings(fn.definition));
2467
+ }
2468
+ for (const [name, body] of bodies) {
2469
+ if (!AUTH_ROLE.test(body)) continue;
2470
+ const dot = name.indexOf(".");
2471
+ findings.push({
2472
+ message: `${name} calls auth.role(), which Supabase deprecated. Read \`auth.jwt() ->> 'role'\` instead.`,
2473
+ target: name,
2474
+ object: {
2475
+ kind: "function",
2476
+ schema: name.slice(0, dot),
2477
+ name: name.slice(dot + 1)
2478
+ }
2479
+ });
2480
+ }
2481
+ return findings;
2482
+ }
2483
+ },
2484
+ {
2485
+ code: "BS110",
2486
+ severity: "warning",
2487
+ title: "Update policy without a select policy or with check",
2488
+ description: "An update has to find the rows first, so without a select policy for the same role it matches no rows and reports success. Without `with check`, Postgres checks the new row against `using`, which hides whether moving a row to another owner or tenant is allowed.",
2489
+ check: (context) => exposed(context).flatMap((table) => {
2490
+ if (!table.rls) return [];
2491
+ return table.policies.flatMap((policy) => {
2492
+ if (!policy.permissive || policy.command !== "update") return [];
2493
+ const blind = (policy.roles.length === 0 || policy.roles.includes("public") ? ["anon", "authenticated"] : policy.roles).filter((role) => !allows(table, "select", role));
2494
+ const target = `${qualified(table)}.${policy.name}`;
2495
+ if (blind.length > 0) return [{
2496
+ message: `Policy "${policy.name}" on ${qualified(table)} allows update for ${blind.join(", ")}, but no select policy does, so updates match no rows without an error. Add a select policy for ${blind.join(", ")}.`,
2497
+ target,
2498
+ object: policyObject(table, policy)
2499
+ }];
2500
+ if (policy.check !== null) return [];
2501
+ return [{
2502
+ severity: "info",
2503
+ message: `Policy "${policy.name}" on ${qualified(table)} has no \`with check\`, so Postgres checks updated rows against \`using\`. State the rule for the new row with \`with check (...)\`.`,
2504
+ target,
2505
+ object: policyObject(table, policy)
2506
+ }];
2507
+ });
2508
+ })
2509
+ },
2510
+ {
2511
+ code: "BS111",
2512
+ severity: "warning",
2513
+ title: "API roles hold privileges they don't use",
2514
+ description: "`anon` and `authenticated` never need `truncate`, `references` or `trigger`, and `truncate` skips RLS. A grant to `anon` on a table no policy opens to it does nothing except list the table in the Data API schema.",
2515
+ check: (context) => exposed(context).flatMap((table) => {
2516
+ if (table.kind !== "table") return [];
2517
+ const findings = [];
2518
+ for (const role of ["anon", "authenticated"]) {
2519
+ const privileges = new Set(table.grants.filter((grant) => grant.role === role).flatMap((grant) => grant.privileges));
2520
+ const needless = NEEDLESS.filter((privilege) => privileges.has(privilege));
2521
+ if (needless.length > 0) findings.push({
2522
+ message: `${role} has ${needless.join(", ").toLowerCase()} on ${qualified(table)}. \`truncate\` bypasses RLS. Run \`revoke ${needless.join(", ").toLowerCase()} on table ${qualified(table)} from ${role};\`.`,
2523
+ target: `${qualified(table)}:${role}`,
2524
+ object: tableObject(table)
2525
+ });
2526
+ if (role !== "anon" || !table.rls) continue;
2527
+ const unused = COMMANDS$2.filter((command) => privileges.has(command.toUpperCase()) && !allows(table, command, role));
2528
+ if (unused.length > 0) findings.push({
2529
+ severity: "info",
2530
+ message: `anon has ${unused.join(", ")} on ${qualified(table)}, but no policy allows it for anon, so the grant only lists the table in the Data API schema. Run \`revoke ${unused.join(", ")} on table ${qualified(table)} from anon;\`.`,
2531
+ target: `${qualified(table)}:anon:unused`,
2532
+ object: tableObject(table)
2533
+ });
2534
+ }
2535
+ return findings;
2536
+ })
2537
+ },
2538
+ {
2539
+ code: "BS112",
2540
+ severity: "warning",
2541
+ title: "Exposed security definer function that doesn't check the caller",
2542
+ description: "A `security definer` function in an exposed schema runs as its owner, past RLS, and the Data API serves it at `/rpc`. When anon or authenticated may execute it and its body never reads `auth.uid()` or `auth.jwt()`, any caller gets the owner's access. Check the caller, revoke `execute`, or move it to a schema the API doesn't expose.",
2543
+ check: (context) => (context.snapshot.extras.functions ?? []).flatMap((fn) => {
2544
+ if (!fn.securityDefiner || !context.config.schemas.includes(fn.schema)) return [];
2545
+ const callers = fn.execute ?? [];
2546
+ if (callers.length === 0) return [];
2547
+ const body = functionBody(context, fn.schema, fn.name);
2548
+ if (body === void 0 || CALLER_CHECK.test(body)) return [];
2549
+ return [{
2550
+ message: `${fn.schema}.${fn.name}(${fn.signature}) is security definer, ${callers.join(" and ")} may execute it through /rpc, and it never checks auth.uid() or auth.jwt(). Check the caller, run \`revoke execute on function ${fn.schema}.${fn.name}(${fn.signature}) from ${callers.join(", ")};\`, or move it to a private schema.`,
2551
+ target: `${fn.schema}.${fn.name}`,
2552
+ object: functionObject(fn)
2553
+ }];
2554
+ })
2555
+ },
2556
+ {
2557
+ code: "BS113",
2558
+ severity: "info",
2559
+ title: "Storage upload policy without the upsert policies",
2560
+ description: "An upload with `upsert: true` (`x-upsert`) needs `select` and `update` policies on `storage.objects` besides `insert`; without them, replacing an existing file fails with a 403. Doctor reads the policies in `supabase/schemas` and the migrations.",
2561
+ check: (context) => {
2562
+ const commands = new Set(storagePolicies(context).values());
2563
+ if (!commands.has("insert") || commands.has("all")) return [];
2564
+ const missing = ["select", "update"].filter((command) => !commands.has(command));
2565
+ if (missing.length === 0) return [];
2566
+ return [{
2567
+ message: `storage.objects has an insert policy but no ${missing.join(" or ")} policy, so uploads with \`upsert: true\` fail with 403 when the file exists. Add ${missing.join(" and ")} policies with the same bucket and path check.`,
2568
+ target: "storage.objects"
2569
+ }];
2570
+ }
2571
+ },
2572
+ {
2573
+ code: "BS114",
2574
+ severity: "error",
2575
+ title: "Policy helper reads user_metadata",
2576
+ description: "Users can change their own `raw_user_meta_data` with `auth.updateUser()`, so a helper that reads `user_metadata` to decide access lets them grant themselves rights. Read `app_metadata`, which only the service role can write. Splinter's `rls_references_user_metadata` covers the policy text; this covers the functions policies call.",
2577
+ check: (context) => [...policyHelpers(context).keys()].flatMap((name) => {
2578
+ const dot = name.indexOf(".");
2579
+ const body = functionBody(context, name.slice(0, dot), name.slice(dot + 1));
2580
+ if (!body || !USER_METADATA.test(body)) return [];
2581
+ return [{
2582
+ message: `${name} reads user_metadata, which users can write themselves, and policies call it. Read \`auth.jwt() -> 'app_metadata'\` instead.`,
2583
+ target: name,
2584
+ object: {
2585
+ kind: "function",
2586
+ schema: name.slice(0, dot),
2587
+ name: name.slice(dot + 1)
2588
+ }
2589
+ }];
2590
+ })
2591
+ },
2592
+ {
2593
+ code: "BS115",
2594
+ severity: "warning",
2595
+ title: "Table without a policy still granted to the Data API",
2596
+ description: "A table with RLS on and no permissive policy for `anon` or `authenticated` answers every Data API request with no rows or a denied write, yet a grant lists it in the API schema. Under `blocks['grants'].options.fromPolicies`, `better-supabase upgrade` revokes these grants and keeps `service_role`; until the migration is applied, doctor reports the live grants. Tables in `expose` are skipped.",
2597
+ check: (context) => exposed(context).flatMap((table) => {
2598
+ if (table.kind !== "table" || !table.rls) return [];
2599
+ const options = configFor(context.config.tables, table.schema, table.name);
2600
+ if (options?.exclude || options?.serviceRole) return [];
2601
+ if (context.config.expose[qualified(table)] ?? context.config.expose[table.name]) return [];
2602
+ const roles = ["anon", "authenticated"].filter((role) => table.grants.some((grant) => (grant.role === role || grant.role === "PUBLIC") && grant.privileges.length > 0));
2603
+ if (roles.length === 0) return [];
2604
+ if (table.policies.some((policy) => policy.permissive && roles.some((role) => appliesTo(policy, role)))) return [];
2605
+ return [{
2606
+ message: `${qualified(table)} has RLS on and no permissive policy for ${roles.join(" or ")}, but ${roles.join(" and ")} still hold grants on it. Run \`revoke all on table ${qualified(table)} from public, anon, authenticated;\`, or set \`blocks['grants'].options.fromPolicies\` and run \`better-supabase upgrade\` so the migration revokes it.`,
2607
+ target: qualified(table),
2608
+ object: tableObject(table)
2609
+ }];
2610
+ })
2611
+ },
2612
+ {
2613
+ code: "BS116",
2614
+ severity: "warning",
2615
+ title: "Security definer function open to the API roles without being exposed",
2616
+ description: "Postgres grants `execute` on a new function to `public`, so a `security definer` function in an exposed schema is callable at `/rpc` by `anon` and `authenticated` unless it is revoked. When no `expose` entry lists it and no policy calls it, nothing in the project says the API roles should reach it. Doctor never revokes it; list it in `expose` with the roles that call it, or revoke `execute` from `public`, `anon` and `authenticated` and grant it back to those roles.",
2617
+ check: (context) => {
2618
+ const schemas = exposedSchemas(context);
2619
+ const listed = /* @__PURE__ */ new Set([...Object.keys(context.config.exposeFunctions).map((signature) => {
2620
+ const name = signature.slice(0, signature.indexOf("(")).trim();
2621
+ return name.includes(".") ? name : `public.${name}`;
2622
+ })]);
2623
+ const inPolicies = new Set(catalogOf(context).tables.flatMap((table) => table.policies.flatMap((policy) => policy.functions ?? [])));
2624
+ return (context.snapshot.extras.functions ?? []).flatMap((fn) => {
2625
+ const name = `${fn.schema}.${fn.name}`;
2626
+ const callers = fn.execute ?? [];
2627
+ if (!fn.securityDefiner || !schemas.includes(fn.schema) || callers.length === 0 || listed.has(name) || inPolicies.has(name)) return [];
2628
+ return [{
2629
+ message: `${name}(${fn.signature}) is security definer, ${callers.join(" and ")} may execute it through /rpc, and no \`expose\` entry or policy uses it. Run \`revoke all on function ${name}(${fn.signature}) from public, anon, authenticated;\`, then \`grant execute on function ${name}(${fn.signature}) to <the roles that call it>;\`, or list it in \`expose\`.`,
2630
+ target: name,
2631
+ object: functionObject(fn)
2632
+ }];
2633
+ });
2634
+ }
2635
+ },
2636
+ {
2637
+ code: "BS117",
2638
+ severity: "warning",
2639
+ title: "Policy without a role",
2640
+ description: "A policy without `to`, or with `to public`, applies to `public`, which is every role, `anon` included. Postgres then evaluates it for requests it was never meant for, and a later change to its expression can open the table to signed-out users. Name the roles with `to authenticated` (or `to anon, authenticated`). Restrictive policies only narrow access, so they are skipped.",
2641
+ check: (context) => catalogOf(context).tables.flatMap((table) => table.policies.filter((policy) => policy.permissive && (policy.roles.length === 0 || policy.roles.includes("public"))).map((policy) => ({
2642
+ message: `Policy "${policy.name}" on ${qualified(table)} applies to public (no \`to\` role, or \`to public\`), which is every role, anon included. Recreate it with \`to authenticated\`, or list the roles that need it.`,
2643
+ target: `${qualified(table)}.${policy.name}`,
2644
+ object: policyObject(table, policy)
2645
+ })))
2646
+ },
2647
+ {
2648
+ code: "BS215",
2649
+ severity: "warning",
2650
+ title: "Policy calls a helper per row that could run once",
2651
+ description: "A call such as `current_tenant_id()` takes no column of the row, so its result is the same for every row. Wrapped as `(select current_tenant_id())`, Postgres runs it once per statement as an InitPlan. Splinter's `auth_rls_initplan` covers `auth.*` calls; this covers your own helpers.",
2652
+ check: (context) => {
2653
+ const volatility = new Map((context.snapshot.extras.functions ?? []).map((fn) => [`${fn.schema}.${fn.name}`, fn.volatility]));
2654
+ return exposed(context).flatMap((table) => {
2655
+ if (!table.rls) return [];
2656
+ return table.policies.flatMap((policy) => {
2657
+ const text = policyText(policy);
2658
+ const bare = (policy.functions ?? []).filter((name) => {
2659
+ const dot = name.indexOf(".");
2660
+ const schema = name.slice(0, dot);
2661
+ if (schema === "auth" || volatility.get(name) === "immutable") return false;
2662
+ return new RegExp(`(?<!\\bselect\\s+)(?<![\\w."])(?:"?${escape$2(schema)}"?\\.)?"?${escape$2(name.slice(dot + 1))}"?\\s*\\(\\s*\\)`, "i").test(text);
2663
+ });
2664
+ if (bare.length === 0) return [];
2665
+ return [{
2666
+ message: `Policy "${policy.name}" on ${qualified(table)} calls ${bare.map((name) => `${name}()`).join(", ")} without \`(select ...)\`, so it runs once per row. Write \`(select ${bare[0]}())\`.`,
2667
+ target: `${qualified(table)}.${policy.name}`,
2668
+ object: policyObject(table, policy)
2669
+ }];
2670
+ });
2671
+ });
2672
+ }
2673
+ }
2674
+ ];
2675
+ //#endregion
2676
+ //#region src/cli/doctor/realtime.ts
2677
+ /** The comment `topic.trigger(..., { payload: "row" })` puts on its function. */
2678
+ const ROW_PAYLOAD_MARKER = "better-supabase: payload row";
2679
+ const BROADCAST_ROW = /realtime\.broadcast_changes\s*\([^;]*\b(?:new|old)\b|'old_record'/i;
2680
+ /** Whether a trigger function body sends `new` or `old` whole. */
2681
+ function sendsWholeRow(source) {
2682
+ return BROADCAST_ROW.test(source.replaceAll(/--[^\n]*/g, ""));
2683
+ }
2684
+ /** Realtime checks that read the trigger functions in the snapshot. */
2685
+ const REALTIME_RULES = [{
2686
+ code: "BS337",
2687
+ severity: "warning",
2688
+ title: "Broadcast trigger sends whole rows",
2689
+ description: "A trigger function sends the whole row to Realtime with `realtime.broadcast_changes(..., new, old)` or `old_record` and `record`, and has no `payload: \"row\"` marker. Subscribers then see every column, including ones their column grants hide.",
2690
+ check: (context) => catalogOf(context).tables.flatMap((table) => table.triggers.filter((trigger) => trigger.source !== void 0 && trigger.comment !== ROW_PAYLOAD_MARKER && sendsWholeRow(trigger.source)).map((trigger) => ({
2691
+ message: `${trigger.name} on ${qualified(table)} calls ${trigger.function}(), which broadcasts whole rows. Regenerate it with \`topic.trigger()\`, whose default payload sends the primary key and the topic values, or pass \`payload: "row"\` to keep whole rows on purpose.`,
2692
+ target: `${qualified(table)}.${trigger.name}`,
2693
+ object: tableObject(table)
2694
+ })))
2695
+ }];
2696
+ //#endregion
2697
+ //#region src/cli/doctor/removed-apis.ts
2698
+ const SPECIFIER = /from\s+["']better-supabase\/([\w./-]+)["']/g;
2699
+ const NAMED_IMPORT = /import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+["']better-supabase(?:\/[\w./-]+)?["']/g;
2700
+ /** Names 0.15 removed without a one-to-one replacement, with what replaces them. */
2701
+ const REMOVED_NAMES = [
2702
+ {
2703
+ pattern: /\b(?:sectionAction|recordServer|defineRecordFeature)\b/,
2704
+ replacement: "the generated record type (`bsb.record(<type>Record)` from `@/lib/supabase/records/<type>`)"
2705
+ },
2706
+ {
2707
+ pattern: /["'][@~]?\/?[\w/]*supabase\/features\//,
2708
+ replacement: "the generated `records/<type>` file"
2709
+ },
2710
+ {
2711
+ pattern: /\b(?:fileMaintenance|updateMaintenance|listMaintenance|maintenanceRequestInputSchema)\b/,
2712
+ replacement: "a tasks instance (`block: \"tasks\"`) and its client"
2713
+ },
2714
+ {
2715
+ pattern: /\bpass_credit_entries\b/,
2716
+ replacement: "`credits(passId)` on the ledger"
2717
+ },
2718
+ {
2719
+ pattern: /\.blockRouter\(/,
2720
+ replacement: "`bsb.router(defineApi(betterSupabase, options))`, which routes every block instance and table"
2721
+ },
2722
+ {
2723
+ pattern: /\brequireTenant\b/,
2724
+ replacement: "`requireOrganization` (`bsb codemod 0.15` renames it)"
2725
+ },
2726
+ {
2727
+ pattern: /["']better-supabase\/(?:blocks\/[\w-]+\/(?:react|vue|svelte|solid)|chat-sdk\/react)["']/,
2728
+ replacement: "the block's React hooks from the registry (`npx shadcn add @better-supabase/use-<block>`), or `useBlockController(create<Block>Controller, options)` from `better-supabase/vue`, `/svelte` or `/solid`"
2729
+ },
2730
+ {
2731
+ pattern: /["']better-supabase\/powersync\/react["']|\buseWatch\b/,
2732
+ replacement: "`engine: powersyncEngine(database, { connector })` on the client and the generated data hooks; `useSyncStatus()` and `useConflicts()` come from `better-supabase/react`"
2733
+ },
2734
+ {
2735
+ pattern: /\bsqliteTables\b/,
2736
+ replacement: "`powersyncEngine(database)`, which refreshes the hooks that read a changed table"
2737
+ },
2738
+ {
2739
+ pattern: /["']better-supabase\/expo-sqlite["']|\bexpoSqliteExecutor\b/,
2740
+ replacement: "`sqliteEngine(expoSqliteDatabase(db))` from `better-supabase/sqlite`, passed as the client's `engine`"
2741
+ },
2742
+ {
2743
+ pattern: /["']better-supabase\/tanstack-db\/supabase["']|\bsupabaseCollection\b/,
2744
+ replacement: "`collectionOptions` from `better-supabase/tanstack-db`, with `tanstackDbEngine({ collections })` and `live: realtimeChanges(...)` for Realtime"
2745
+ },
2746
+ {
2747
+ pattern: /["']better-supabase\/flows["']|\b(?:createFlows|defineFlow|conversationToWorkItem|applicationToEmployee|bookingToEngagement|quoteToOrder)\b/,
2748
+ replacement: "the default automations the blocks ship, run by `createRuleSink` with the conversion actions (`taskFromConversationAction`, `employeeFromApplicationAction`, `orderFromQuoteAction`) in `createActionRegistry`; confirmed bookings become engagements through a database trigger"
2749
+ },
2750
+ {
2751
+ pattern: /\b(?:RuleActionHandler|RuleActionCall)\b/,
2752
+ replacement: "`defineAction` and `createActionRegistry([...])`, passed to `createRuleDispatcher({ actions })` (`ActionHandler`, `ActionCall`)"
2753
+ },
2754
+ {
2755
+ pattern: /\bRULE_ACTION_(?:UNKNOWN|INVALID)\b/,
2756
+ replacement: "the hints `ACTION_UNKNOWN` and `ACTION_CONFIG_INVALID`"
2757
+ },
2758
+ {
2759
+ pattern: /\btriggers\.onEvent\b|\b(?:triggerMatches|triggerWhenProblem|OutboxLikeEvent|WorkflowSinkEvent)\b/,
2760
+ replacement: "an automation whose `workflow` action (`workflowAction(builder)`) starts the definition on the outbox event"
2761
+ }
2762
+ ];
2763
+ function lineAt(text, index) {
2764
+ return text.slice(0, index).split("\n").length;
2765
+ }
2766
+ function renamedSubpath(subpath) {
2767
+ if (Object.hasOwn(BLOCK_SUBPATH_RENAMES, subpath)) return `better-supabase/${BLOCK_SUBPATH_RENAMES[subpath]}`;
2768
+ if (Object.hasOwn(PLUGIN_SUBPATH_RENAMES, subpath)) {
2769
+ const to = PLUGIN_SUBPATH_RENAMES[subpath];
2770
+ return to ? `better-supabase/${to}` : "better-supabase";
2771
+ }
2772
+ if (Object.hasOwn(SPEC_SUBPATH_RENAMES, subpath)) return `better-supabase/${SPEC_SUBPATH_RENAMES[subpath]}`;
2773
+ }
2774
+ /**
2775
+ * App code that still imports a subpath or name 0.15 renamed or removed
2776
+ * (BS418).
2777
+ */
2778
+ function removedApis(context) {
2779
+ const findings = [];
2780
+ for (const file of context.sources) {
2781
+ for (const match of file.text.matchAll(SPECIFIER)) {
2782
+ const subpath = match[1] ?? "";
2783
+ const to = renamedSubpath(subpath);
2784
+ if (to !== void 0) findings.push({
2785
+ message: `${file.path} imports \`better-supabase/${subpath}\`, which 0.15 renamed to \`${to}\`. Run \`bsb codemod 0.15\`.`,
2786
+ target: `${file.path}:${subpath}`,
2787
+ location: {
2788
+ file: file.path,
2789
+ line: lineAt(file.text, match.index)
2790
+ }
2791
+ });
2792
+ }
2793
+ for (const match of file.text.matchAll(NAMED_IMPORT)) {
2794
+ const names = (match[1] ?? "").split(",").map((name) => name.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0]?.trim()).filter((name) => Boolean(name));
2795
+ for (const name of names) if (Object.hasOwn(BLOCK_EXPORT_RENAMES, name)) findings.push({
2796
+ message: `${file.path} imports \`${name}\`, which 0.15 renamed to \`${BLOCK_EXPORT_RENAMES[name]}\`. Run \`bsb codemod 0.15\`.`,
2797
+ target: `${file.path}:${name}`,
2798
+ location: {
2799
+ file: file.path,
2800
+ line: lineAt(file.text, match.index)
2801
+ }
2802
+ });
2803
+ }
2804
+ for (const { pattern, replacement } of REMOVED_NAMES) {
2805
+ const match = pattern.exec(file.text);
2806
+ if (match) findings.push({
2807
+ message: `${file.path} uses \`${match[0].replaceAll(/["']/g, "")}\`, which 0.15 removed. Use ${replacement} instead.`,
2808
+ target: `${file.path}:${match[0]}`,
2809
+ location: {
2810
+ file: file.path,
2811
+ line: lineAt(file.text, match.index)
2812
+ }
2813
+ });
2814
+ }
2815
+ }
2816
+ return findings;
2817
+ }
2818
+ const REMOVED_API_RULES = [{
2819
+ code: "BS418",
2820
+ severity: "warning",
2821
+ title: "Code uses an API that 0.15 renamed or removed",
2822
+ description: "0.15 renamed blocks and their subpaths, turned the built-in plugins into block extensions, moved the OpenAPI, AsyncAPI, Arazzo and Overlay subpaths into `better-supabase/spec`, replaced record features with record types and flows with default automations. `better-supabase codemod 0.15` rewrites the renames; the removed APIs need the replacement the finding names.",
2823
+ check: removedApis
2824
+ }];
2825
+ //#endregion
2826
+ //#region src/cli/doctor/schema-design.ts
2827
+ /** The `format` the database reports for each app column type. */
2828
+ const APP_COLUMN_FORMAT = {
2829
+ text: "text",
2830
+ uuid: "uuid",
2831
+ integer: "int4",
2832
+ bigint: "int8",
2833
+ numeric: "numeric",
2834
+ boolean: "bool",
2835
+ timestamptz: "timestamptz",
2836
+ date: "date",
2837
+ jsonb: "jsonb",
2838
+ "text[]": "_text"
2839
+ };
2840
+ /**
2841
+ * Whether a partial index's predicate only skips rows where a key column is
2842
+ * null, like `(customer_id IS NOT NULL)`: those rows reference nothing, so the
2843
+ * index still serves the key.
2844
+ */
2845
+ const skipsNullKeys = (predicate, columns) => {
2846
+ if (!predicate) return false;
2847
+ return predicate.split(/\s+AND\s+/i).every((part) => columns.some((column) => new RegExp(`^\\(*\\s*"?${escape$2(column)}"?\\s+IS\\s+NOT\\s+NULL\\s*\\)*$`, "i").test(part.trim())));
2848
+ };
2849
+ /**
2850
+ * Whether the first `columns.length` columns of an index are exactly `columns`,
2851
+ * in any order, and it holds every row that references something.
2852
+ */
2853
+ const covers = (index, columns) => (!index.partial || skipsNullKeys(index.predicate, columns)) && index.columns.length >= columns.length && index.columns.slice(0, columns.length).every((column) => columns.includes(column));
2854
+ /** Foreign keys of `table` that no index starts with. */
2855
+ const unindexedForeignKeys = (table) => table.foreignKeys.filter((key) => !table.indexes.some((index) => covers(index, key.columns)));
2856
+ const SOFT_DELETE = /^(?:deleted|archived)_at$/;
2857
+ /** Operators that need a GIN index on jsonb or array columns. */
2858
+ const CONTAINS = String.raw`(?:@>|<@|\?\||\?&|\?|&&)`;
2859
+ /**
2860
+ * `.contains("tags"`, `.containedBy('meta'`, `.overlaps("labels"`, supabase-js
2861
+ * filters.
2862
+ */
2863
+ const CLIENT_CONTAINS = /\.(?:contains|containedBy|overlaps)\(\s*["'`]([A-Za-z_]\w*)["'`]/g;
2864
+ /** Column types Postgres' wiki and the Supabase skills advise against. */
2865
+ const AVOID = {
2866
+ timestamp: "timestamptz",
2867
+ varchar: "text with a check constraint",
2868
+ bpchar: "text",
2869
+ money: "numeric",
2870
+ json: "jsonb"
2871
+ };
2872
+ /**
2873
+ * A Supabase direct (`db.<ref>.supabase.co`) or session-pooler (`:5432`)
2874
+ * connection string.
2875
+ */
2876
+ const DIRECT_URL = /postgres(?:ql)?:\/\/[^\s"'`]*@(?:db\.[a-z0-9]+\.supabase\.co(?::5432)?|[\w.-]+\.pooler\.supabase\.com:5432)\b/i;
2877
+ /** Files that show the app runs on serverless or edge functions. */
2878
+ const SERVERLESS = /\bexport\s+(?:const\s+runtime|async\s+function\s+(?:GET|POST|PUT|PATCH|DELETE)|default\s+\{\s*(?:async\s+)?fetch)\b|\bDeno\.serve\(/;
2879
+ /** A connection string through Supavisor's transaction pooler (port 6543). */
2880
+ const TRANSACTION_POOLER = /postgres(?:ql)?:\/\/[^\s"'`]*:6543\b/i;
2881
+ /** `import postgres from "postgres"` or `require("postgres")`: postgres.js. */
2882
+ const POSTGRES_JS = /\bfrom\s+["']postgres["']|\brequire\(\s*["']postgres["']\s*\)/;
2883
+ const PREPARE_OFF = /\bprepare\s*:\s*false\b/;
2884
+ const PRISMA = /["']@prisma\/client["']|\bnew\s+PrismaClient\b/;
2885
+ const OFFSET_PAGINATION = /\bpagination\s*:\s*["'`]offset["'`]/;
2886
+ const EXACT_COUNT = /\bcount\s*:\s*["'`]exact["'`]/;
2887
+ /** `format_type` names of scalar results that a JSON number can't hold exactly. */
2888
+ const WIDE_RESULT = /^(?:bigint|numeric)(?:\[\])?$/;
2889
+ function lines(file) {
2890
+ return file.text.split("\n").map((text, index) => ({
2891
+ line: index + 1,
2892
+ text
2893
+ }));
2894
+ }
2895
+ const SCHEMA_DESIGN_RULES = [
2896
+ {
2897
+ code: "BS216",
2898
+ severity: "info",
2899
+ title: "Foreign key without an index",
2900
+ 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; a partial index counts when its `where` only skips rows whose key is null (`where customer_id is not null`). 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.",
2901
+ check: async (context) => {
2902
+ const splinter = await splinterTables(context, "unindexed_foreign_keys");
2903
+ return exposed(context).flatMap((table) => {
2904
+ if (table.kind !== "table" || splinter?.has(qualified(table))) return [];
2905
+ return unindexedForeignKeys(table).map((key) => ({
2906
+ 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(", ")});\`.`,
2907
+ target: `${qualified(table)}.${key.name}`,
2908
+ object: tableObject(table)
2909
+ }));
2910
+ });
2911
+ }
2912
+ },
2913
+ {
2914
+ code: "BS217",
2915
+ severity: "warning",
2916
+ title: "Tenant foreign key that can cross tenants",
2917
+ description: "When a tenant table references another tenant table by `id` alone, a row can point at a parent in another organization; RLS checks each row, not the pair. Reference `(id, organization_id)` with a composite foreign key, backed by `unique (id, organization_id)` on the parent.",
2918
+ check: (context) => {
2919
+ const column = context.config.extensions.tenant?.column;
2920
+ if (!column) return [];
2921
+ const tables = new Map(catalogOf(context).tables.map((table) => [qualified(table), table]));
2922
+ const hasTenant = (table) => table?.columns.some((entry) => entry.name === column) ?? false;
2923
+ return exposed(context).flatMap((table) => {
2924
+ if (table.kind !== "table" || !hasTenant(table)) return [];
2925
+ return table.foreignKeys.flatMap((key) => {
2926
+ if (key.columns.includes(column)) return [];
2927
+ const parent = tables.get(`${key.refSchema}.${key.refTable}`);
2928
+ if (!hasTenant(parent) || parent === table) return [];
2929
+ return [{
2930
+ message: `${qualified(table)}.${key.name} references ${key.refSchema}.${key.refTable} (${key.refColumns.join(", ")}) without ${column}, so a row can point at another tenant's ${key.refTable}. Add \`unique (${[...key.refColumns, column].join(", ")})\` to ${key.refTable} and reference \`(${[...key.columns, column].join(", ")})\`.`,
2931
+ target: `${qualified(table)}.${key.name}`,
2932
+ object: tableObject(table)
2933
+ }];
2934
+ });
2935
+ });
2936
+ }
2937
+ },
2938
+ {
2939
+ code: "BS218",
2940
+ severity: "info",
2941
+ title: "Soft-delete table without a partial index",
2942
+ description: "Queries on a table with `deleted_at` or `archived_at` read the live rows (`where deleted_at is null`). A partial index with that predicate stays small and matches those queries.",
2943
+ check: (context) => exposed(context).flatMap((table) => {
2944
+ if (table.kind !== "table") return [];
2945
+ const column = table.columns.find((entry) => SOFT_DELETE.test(entry.name) && entry.nullable);
2946
+ if (!column) return [];
2947
+ const isNull = new RegExp(`"?${escape$2(column.name)}"?\\s+IS\\s+NULL`, "i");
2948
+ if (table.indexes.some((index) => index.partial && (index.predicate === void 0 || index.predicate !== null && isNull.test(index.predicate)))) return [];
2949
+ return [{
2950
+ message: `${qualified(table)} soft-deletes with ${column.name}, but no index has \`where ${column.name} is null\`. Index the live rows you list, for example \`create index on ${qualified(table)} (created_at desc) where ${column.name} is null;\`.`,
2951
+ target: `${qualified(table)}.${column.name}`,
2952
+ object: tableObject(table)
2953
+ }];
2954
+ })
2955
+ },
2956
+ {
2957
+ code: "BS219",
2958
+ severity: "warning",
2959
+ title: "Containment filter on a column without a GIN index",
2960
+ description: "`@>`, `<@`, `?` and `&&` on jsonb and array columns, and the `.contains()`, `.containedBy()` and `.overlaps()` filters, can only use a GIN index. Without one, every query reads the whole table. Doctor looks in policies, exposed functions and `tooling.doctor.sources`.",
2961
+ check: (context) => {
2962
+ const tables = exposed(context).filter((table) => table.kind === "table");
2963
+ const policyText = (table) => withoutStrings(table.policies.map((policy) => `${policy.using ?? ""} ${policy.check ?? ""}`).join(" "));
2964
+ const functions = context.snapshot.generator.functions.filter((fn) => context.config.schemas.includes(fn.schema) && fn.definition).map((fn) => withoutStrings(fn.definition)).join("\n");
2965
+ const filtered = /* @__PURE__ */ new Set();
2966
+ for (const file of context.sources) for (const match of file.text.matchAll(CLIENT_CONTAINS)) filtered.add(match[1]);
2967
+ return tables.flatMap((table) => {
2968
+ const own = policyText(table);
2969
+ return table.columns.filter((column) => {
2970
+ if (!column.isArray && column.udt !== "jsonb") return false;
2971
+ if (table.indexes.some((index) => index.columns[0] === column.name && (index.method === void 0 || index.method === "gin"))) return false;
2972
+ const used = new RegExp(`"?${escape$2(column.name)}"?\\s*${CONTAINS}|${CONTAINS}\\s*[\\w."]*\\b${escape$2(column.name)}\\b`);
2973
+ return filtered.has(column.name) || used.test(own) || functions.includes(table.name) && used.test(functions);
2974
+ }).map((column) => ({
2975
+ message: `${qualified(table)}.${column.name} is filtered by containment but has no GIN index, so each query scans the table. Run \`create index on ${qualified(table)} using gin (${column.name}${column.udt === "jsonb" ? " jsonb_path_ops" : ""});\`${column.udt === "jsonb" ? " (`jsonb_path_ops` supports `@>` only; drop it for `?`)" : ""}.`,
2976
+ target: `${qualified(table)}.${column.name}`,
2977
+ object: tableObject(table)
2978
+ }));
2979
+ });
2980
+ }
2981
+ },
2982
+ {
2983
+ code: "BS220",
2984
+ severity: "info",
2985
+ title: "Column type to avoid",
2986
+ description: "`timestamp` drops the time zone, `varchar(n)` and `char(n)` only add a length check `text` does with a constraint, `money` rounds by locale and `json` reparses on every read. Use `timestamptz`, `text`, `numeric` and `jsonb`.",
2987
+ check: (context) => exposed(context).flatMap((table) => {
2988
+ if (table.kind !== "table") return [];
2989
+ const columns = table.columns.filter((column) => column.typeSchema === "pg_catalog" && column.udt in AVOID);
2990
+ if (columns.length === 0) return [];
2991
+ return [{
2992
+ message: `${qualified(table)}: ${columns.map((column) => `${column.name} is ${column.udt}, use ${AVOID[column.udt]}`).join("; ")}.`,
2993
+ target: qualified(table),
2994
+ object: tableObject(table)
2995
+ }];
2996
+ })
2997
+ },
2998
+ {
2999
+ code: "BS221",
3000
+ severity: "warning",
3001
+ title: "Direct database connection in a serverless app",
3002
+ description: "Each serverless or edge invocation opens its own connection. Through the direct host or the session pooler on port 5432 they exhaust `max_connections`; the transaction pooler on port 6543 shares them. The direct host is also IPv6 only.",
3003
+ check: (context) => {
3004
+ if (!(context.sources.some((file) => SERVERLESS.test(file.text)) || context.envFiles.some((file) => /^\s*VERCEL\w*=/m.test(file.text)))) return [];
3005
+ return context.envFiles.flatMap((file) => lines(file).flatMap(({ line, text }) => {
3006
+ if (/^\s*#/.test(text) || !DIRECT_URL.test(text)) return [];
3007
+ const key = /^\s*(?:export\s+)?(\w+)\s*=/.exec(text)?.[1];
3008
+ return [{
3009
+ message: `${file.path}${key ? `:${key}` : ""} connects through port 5432 or the direct host, but the app runs on serverless functions. Use the transaction pooler: \`postgres://postgres.<ref>:<password>@<region>.pooler.supabase.com:6543/postgres\`.`,
3010
+ target: `${file.path}${key ? `:${key}` : ""}`,
3011
+ location: {
3012
+ file: file.path,
3013
+ line
3014
+ }
3015
+ }];
3016
+ }));
3017
+ }
3018
+ },
3019
+ {
3020
+ code: "BS222",
3021
+ severity: "warning",
3022
+ title: "Column that can exceed a JavaScript number",
3023
+ 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 `gen.codecs.int8` to `bigint` or `string`, or `gen.codecs.numeric` to `string`.",
3024
+ check: (context) => {
3025
+ const { int8, numeric } = context.config.codecs;
3026
+ if (int8 !== "number" && numeric !== "number") return [];
3027
+ return exposed(context).flatMap((table) => {
3028
+ if (configFor(context.config.tables, table.schema, table.name)?.exclude) return [];
3029
+ 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");
3030
+ if (columns.length === 0) return [];
3031
+ const codecs = [...columns.some((column) => column.udt === "int8") ? ["`int8: \"bigint\"`"] : [], ...columns.some((column) => column.udt === "numeric") ? ["`numeric: \"string\"`"] : []];
3032
+ return [{
3033
+ 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\`.`,
3034
+ target: qualified(table),
3035
+ object: tableObject(table)
3036
+ }];
3037
+ });
3038
+ }
3039
+ },
3040
+ {
3041
+ code: "BS223",
3042
+ severity: "info",
3043
+ title: "Function returns an int8 or numeric scalar",
3044
+ description: "PostgREST sends a scalar `int8` or `numeric` function result as a JSON number, which loses precision past 2^53 (or 15 to 17 significant digits) before any codec sees it. `$rpc` casts codec columns of table and record results to text, but a scalar has no column to cast. Return it as `text` when it can be that large.",
3045
+ check: (context) => {
3046
+ const schemas = exposedSchemas(context);
3047
+ return context.snapshot.generator.functions.filter((fn) => schemas.includes(fn.schema) && WIDE_RESULT.test(fn.return_type)).map((fn) => ({
3048
+ message: `${fn.schema}.${fn.name}(${fn.identity_argument_types}) returns ${fn.is_set_returning_function ? "setof " : ""}${fn.return_type}, which PostgREST sends as a JSON number and \`$rpc\` can't cast. Return it as text (\`returns ${fn.return_type.replace("bigint", "text").replace("numeric", "text")}\`, \`select total::text\`) when it can pass 2^53, or add BS223 to \`tooling.doctor.ignore\` when it stays small.`,
3049
+ target: `${fn.schema}.${fn.name}`,
3050
+ object: {
3051
+ kind: "function",
3052
+ schema: fn.schema,
3053
+ name: fn.name
3054
+ }
3055
+ }));
3056
+ }
3057
+ },
3058
+ {
3059
+ code: "BS226",
3060
+ severity: "warning",
3061
+ title: "Prepared statements through the transaction pooler",
3062
+ description: "The transaction pooler on port 6543 hands each transaction to any server connection, so a named prepared statement from one transaction is missing in the next and the query fails with `prepared statement does not exist`. postgres.js prepares statements unless `prepare: false`, and Prisma does unless the URL has `pgbouncer=true`. `pg` (and `better-supabase/postgres`) sends unnamed statements, which are safe. Doctor looks for a `:6543` URL in the env files and `tooling.doctor.sources`.",
3063
+ check: (context) => {
3064
+ const urls = [...context.envFiles, ...context.sources].flatMap((file) => lines(file).filter(({ text }) => !/^\s*(?:#|\/\/)/.test(text) && TRANSACTION_POOLER.test(text)).map(({ line, text }) => ({
3065
+ file,
3066
+ line,
3067
+ text
3068
+ })));
3069
+ if (urls.length === 0) return [];
3070
+ const findings = [];
3071
+ for (const file of context.sources) {
3072
+ if (!POSTGRES_JS.test(file.text) || PREPARE_OFF.test(file.text)) continue;
3073
+ findings.push({
3074
+ message: `${file.path} creates a postgres.js client without \`prepare: false\`, and the project connects through the transaction pooler on port 6543, so its prepared statements fail on the next transaction. Pass \`postgres(url, { prepare: false })\`.`,
3075
+ target: `${file.path}:postgres`,
3076
+ location: {
3077
+ file: file.path,
3078
+ line: lineOf(file.text, POSTGRES_JS) ?? 1
3079
+ }
3080
+ });
3081
+ }
3082
+ if (!context.sources.some((file) => PRISMA.test(file.text))) return findings;
3083
+ for (const { file, line, text } of urls) {
3084
+ if (/[?&]pgbouncer=true\b/.test(text)) continue;
3085
+ const key = /^\s*(?:export\s+)?(\w+)\s*=/.exec(text)?.[1];
3086
+ const target = `${file.path}${key ? `:${key}` : `:${line}`}`;
3087
+ findings.push({
3088
+ message: `${target} connects Prisma through the transaction pooler on port 6543 without \`pgbouncer=true\`, so Prisma's prepared statements fail on the next transaction. Add \`?pgbouncer=true\` to the URL, and use the session pooler or direct URL as \`directUrl\` for migrations.`,
3089
+ target,
3090
+ location: {
3091
+ file: file.path,
3092
+ line
3093
+ }
3094
+ });
3095
+ }
3096
+ return findings;
3097
+ }
3098
+ },
3099
+ {
3100
+ code: "BS227",
3101
+ severity: "info",
3102
+ title: "Offset pagination or exact counts",
3103
+ description: "`pagination: \"offset\"` on a resource or list reads and discards every row before the page, so late pages get slower as the table grows, and rows written between requests shift the pages. `count: \"exact\"` counts every matching row on each request. Use `pagination: \"cursor\"`, and `count: \"planned\"` or `\"estimated\"` on large tables. Doctor looks in `tooling.doctor.sources`.",
3104
+ check: (context) => context.sources.flatMap((file) => lines(file).flatMap(({ line, text }) => {
3105
+ if (/^\s*(?:\/\/|\*)/.test(text)) return [];
3106
+ const found = [];
3107
+ if (OFFSET_PAGINATION.test(text)) found.push({
3108
+ message: `${file.path}:${line} uses \`pagination: "offset"\`, so each page reads and skips every earlier row. Use \`pagination: "cursor"\` unless the UI needs page numbers, and keep \`[api] max_rows\` set so no request reads the whole table.`,
3109
+ target: `${file.path}:${line}:pagination`,
3110
+ location: {
3111
+ file: file.path,
3112
+ line
3113
+ }
3114
+ });
3115
+ if (EXACT_COUNT.test(text)) found.push({
3116
+ message: `${file.path}:${line} uses \`count: "exact"\`, which counts every matching row on each request. Use \`count: "planned"\` or \`"estimated"\` when the table can grow large.`,
3117
+ target: `${file.path}:${line}:count`,
3118
+ location: {
3119
+ file: file.path,
3120
+ line
3121
+ }
3122
+ });
3123
+ return found;
3124
+ }))
3125
+ },
3126
+ {
3127
+ code: "BS228",
3128
+ severity: "warning",
3129
+ title: "App table column drifted from appTables",
3130
+ description: "A column of an app table has a different type or nullability in the database than `appTables` gives it, usually after an edit by hand or a migration that wasn't regenerated. `gen` types the column from `appTables`, so the app reads values the database doesn't hold. A table or column the database doesn't have yet is left out: that is a migration not applied.",
3131
+ check: (context) => {
3132
+ const columns = new Map(context.snapshot.generator.columns.filter((column) => column.schema === "public").map((column) => [`${column.table}.${column.name}`, column]));
3133
+ const enriched = enrichedAppTables(context);
3134
+ return Object.entries(context.config.appTables).flatMap(([table, config]) => Object.entries(config.columns ?? {}).flatMap(([name, column]) => {
3135
+ const found = columns.get(`${table}.${name}`);
3136
+ if (found === void 0) return [];
3137
+ const encrypted = column.encrypted === true;
3138
+ const format = encrypted ? "text" : APP_COLUMN_FORMAT[column.type];
3139
+ const nullable = column.nullable !== false || enriched.has(table) && found.is_nullable && column.default === void 0 && column.identity !== true;
3140
+ const drift = [...found.format === format ? [] : [`is ${found.format} in the database and ${encrypted ? "encrypted text" : column.type} in appTables`], ...found.is_nullable === nullable ? [] : [nullable ? "is not null in the database and nullable in appTables" : "is nullable in the database and required in appTables"]];
3141
+ return drift.length === 0 ? [] : [{
3142
+ message: `public.${table}.${name} ${drift.join(", and ")}. Change appTables to match the database, or regenerate the migration with \`better-supabase upgrade\` and apply it. Follow the expand-and-contract steps from \`better-supabase add\` when rows depend on the old shape.`,
3143
+ target: `public.${table}.${name}`,
3144
+ object: {
3145
+ kind: "table",
3146
+ schema: "public",
3147
+ name: table
3148
+ }
3149
+ }];
3150
+ }));
3151
+ }
3152
+ }
3153
+ ];
3154
+ //#endregion
3155
+ //#region src/cli/doctor/stacks.ts
3156
+ /** The share of the segment limit in use from which BS700 warns. */
3157
+ const SEGMENT_WARNING_SHARE = .8;
3158
+ /**
3159
+ * Larger than the interlock segment every postmaster keeps (56 bytes), so the
3160
+ * stack keeps its shared buffers in System V memory.
3161
+ */
3162
+ const SYSV_SEGMENT_BYTES = 1048576;
3163
+ const nearLimit = (memory) => memory.limit !== void 0 && memory.segments.length >= memory.limit * SEGMENT_WARNING_SHARE;
3164
+ function segmentsByStack(segments) {
3165
+ const byStack = /* @__PURE__ */ new Map();
3166
+ for (const segment of segments) if (segment.stackId !== void 0) byStack.set(segment.stackId, [...byStack.get(segment.stackId) ?? [], segment]);
3167
+ return byStack;
3168
+ }
3169
+ /** Stacks that hold more than the interlock segment, so they use `sysv`. */
3170
+ function sysvStacks(memory) {
3171
+ return [...segmentsByStack(memory.segments)].filter(([, segments]) => segments.length > 1 || segments.some((segment) => (segment.bytes ?? 0) > SYSV_SEGMENT_BYTES));
3172
+ }
3173
+ async function loadLocalStackCheck(root, env) {
3174
+ try {
3175
+ const memory = await readSharedMemory(root, env);
3176
+ if (!memory.supported) return { skipped: "ipcs is not available." };
3177
+ return {
3178
+ memory,
3179
+ stacks: nearLimit(memory) || sysvStacks(memory).length > 0 ? await listStacks(root, env).catch(() => []) : []
3180
+ };
3181
+ } catch (cause) {
3182
+ return { skipped: errorText(cause) };
3183
+ }
3184
+ }
3185
+ function stackName(id, stacks) {
3186
+ const stack = stacks.find((each) => each.id === id);
3187
+ return stack === void 0 ? id.slice(0, 12) : `${id.slice(0, 12)} (${branchName(stack.branch)} in ${stack.projectRoot})`;
3188
+ }
3189
+ /** Stacks BS700 names before it points at `stack ls`. */
3190
+ const NAMED_STACKS = 5;
3191
+ const megabytes = (bytes) => `${(bytes / 1048576).toFixed(0)} MB`;
3192
+ /** Checks on the machine's local stacks (BS700, BS701). */
3193
+ const STACK_RULES = [{
3194
+ code: "BS700",
3195
+ severity: "warning",
3196
+ title: "Shared memory segments almost used up",
3197
+ description: "Every running Postgres holds one System V shared memory segment, even with shared_memory_type = mmap. When the segment limit (32 on macOS) is reached, the next stack or shadow database fails with `could not create shared memory segment: No space left on device`. Prune stacks you no longer need with `better-supabase dev stack prune --stop-idle`, or raise the limit (`kern.sysv.shmmni` on macOS, `kernel.shmmni` on Linux).",
3198
+ check: ({ localStacks }) => {
3199
+ if (localStacks === void 0 || "skipped" in localStacks || !nearLimit(localStacks.memory)) return [];
3200
+ const { memory, stacks } = localStacks;
3201
+ const holders = [...segmentsByStack(memory.segments).keys()].map((id) => stackName(id, stacks));
3202
+ const named = holders.slice(0, NAMED_STACKS).join(", ");
3203
+ const more = holders.length > NAMED_STACKS ? ` and ${String(holders.length - NAMED_STACKS)} more (better-supabase dev stack ls lists them)` : "";
3204
+ return [{
3205
+ target: "kern.sysv.shmmni",
3206
+ message: `${String(memory.segments.length)} of ${String(memory.limit)} shared memory segments are in use${holders.length === 0 ? "" : `, ${String(holders.length)} of them by managed stacks: ${named}${more}`}. Run better-supabase dev stack prune --stop-idle to free them.`
3207
+ }];
3208
+ }
3209
+ }, {
3210
+ code: "BS701",
3211
+ severity: "warning",
3212
+ title: "Local stack uses System V shared memory",
3213
+ description: "A stack whose Postgres runs with shared_memory_type or dynamic_shared_memory_type set to sysv holds its shared buffers and parallel query memory in System V segments, which fills the segment limit quickly. The defaults on macOS and Linux are mmap and posix; remove the override from the stack's Postgres settings.",
3214
+ check: ({ localStacks }) => localStacks === void 0 || "skipped" in localStacks ? [] : sysvStacks(localStacks.memory).map(([id, segments]) => {
3215
+ const bytes = segments.reduce((sum, segment) => sum + (segment.bytes ?? 0), 0);
3216
+ return {
3217
+ target: id,
3218
+ message: `Stack ${stackName(id, localStacks.stacks)} holds ${String(segments.length)} shared memory segment(s)${bytes > 0 ? ` (${megabytes(bytes)})` : ""}, so it sets shared_memory_type or dynamic_shared_memory_type to sysv. Use mmap and posix, the defaults.`
3219
+ };
3220
+ })
3221
+ }];
3222
+ //#endregion
3223
+ //#region src/cli/doctor/wiring.ts
3224
+ const ENV_KEY = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
3225
+ function listedKeys(files) {
3226
+ return new Set(files.flatMap((file) => file.text.split("\n").flatMap((line) => {
3227
+ const match = ENV_KEY.exec(line);
3228
+ return match ? [match[1]] : [];
3229
+ })));
3230
+ }
3231
+ const camel = (name) => name.replaceAll(/-([a-z0-9])/g, (_, letter) => letter.toUpperCase());
3232
+ const escape$1 = (text) => text.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
3233
+ /**
3234
+ * Whether a source line outside a comment passes the instance a provider:
3235
+ * `enrichment: blocks.enrichment.with({ provider })` or `.using(...)`.
3236
+ */
3237
+ function wired(sources, entry) {
3238
+ const keys = [.../* @__PURE__ */ new Set([entry.instance, camel(entry.instance)])].map(escape$1);
3239
+ const pattern = new RegExp(`(?:^|[\\s{,])["']?(?:${keys.join("|")})["']?\\s*:\\s*[\\w$.\\[\\]"'-]*\\.(?:with|using)\\(`);
3240
+ return sources.some((file) => file.text.split("\n").some((line) => {
3241
+ const code = line.trim();
3242
+ return !code.startsWith("//") && !code.startsWith("*") && pattern.test(code);
3243
+ }));
3244
+ }
3245
+ /** The env keys and providers the installed blocks need (BS504, BS505). */
3246
+ const WIRING_RULES = [{
3247
+ code: "BS504",
3248
+ severity: "error",
3249
+ title: "Env key a block needs is missing",
3250
+ description: "An installed block reads a key that no env file lists and the shell doesn't set, so its route or worker fails at runtime. `better-supabase add` appends the keys to .env.example; set them in .env.local and every deployed environment.",
3251
+ check: (context) => {
3252
+ const plan = wiringFor(configInstances(context.config));
3253
+ const listed = listedKeys(context.envFiles);
3254
+ const shell = context.shellEnv ?? {};
3255
+ return plan.env.filter((entry) => entry.required !== false && !listed.has(entry.key) && (shell[entry.key] ?? "") === "").map((entry) => ({
3256
+ message: `${entry.blocks.join(", ")} ${entry.blocks.length === 1 ? "reads" : "read"} ${entry.key} (${entry.description}) but no env file lists it. Add it to .env.example and set it in .env.local and every deployed environment.`,
3257
+ target: entry.key
3258
+ }));
3259
+ }
3260
+ }, {
3261
+ code: "BS505",
3262
+ severity: "error",
3263
+ title: "Required provider is not wired",
3264
+ description: "A block that can't run without an implementation (an enrichment provider, the mail providers) is installed, but no app source passes it one with `.with(...)` or `.using(...)`. Fill in its stub in providers.ts and spread `...providers` into `defineBlocks`.",
3265
+ check: (context) => {
3266
+ return wiringFor(configInstances(context.config)).providers.filter((entry) => entry.provider.required && !wired(context.sources, entry)).map((entry) => ({
3267
+ message: `${entry.instance} needs \`${entry.provider.option}\` (an implementation of ${entry.provider.type} from ${entry.provider.from}). Fill in its stub in providers.ts, next to blocks.generated.ts (for example \`${entry.provider.option}: ${entry.provider.example}\`), and spread \`...providers\` into \`defineBlocks\`.`,
3268
+ target: `${entry.instance}.${entry.provider.option}`
3269
+ }));
3270
+ }
3271
+ }];
3272
+ //#endregion
3273
+ //#region src/cli/doctor/workflow-builder.ts
3274
+ const BUILDER_EXECUTOR = /\b(?:GraphStepCall|executeGraph|nodeRunReporter)\b/;
3275
+ const ADMIN_CALL = /\badmin\s*\(/;
3276
+ function adminInBuilderExecutor(context) {
3277
+ return context.sources.flatMap((file) => {
3278
+ if (!BUILDER_EXECUTOR.test(file.text)) return [];
3279
+ const line = lineOf(file.text, ADMIN_CALL);
3280
+ if (line === void 0) return [];
3281
+ return [{
3282
+ message: `${file.path} runs workflow-editor steps and calls admin(), which bypasses RLS for every step. Resolve the publisher's claims with builder.versions.runAs(call.version) in each step and query as them, so a demoted publisher's workflow stops.`,
3283
+ target: `${file.path}:admin`,
3284
+ location: {
3285
+ file: file.path,
3286
+ line
3287
+ }
3288
+ }];
3289
+ });
3290
+ }
3291
+ const WORKFLOW_BUILDER_RULES = [{
3292
+ code: "BS333",
3293
+ severity: "warning",
3294
+ title: "admin() inside a workflow-editor executor",
3295
+ description: "A file that runs workflow-editor steps calls `admin()`. Steps run with the service role's rights instead of the publisher's, so a published workflow keeps running after its publisher loses a permission.",
3296
+ check: adminInBuilderExecutor
3297
+ }];
3298
+ //#endregion
3299
+ //#region src/cli/doctor/rules.ts
3300
+ const DOCS_URL = "https://bettersupabase.com/docs/cli/doctor";
3301
+ const writes = (policy) => policy.command === "all" || policy.command === "insert" || policy.command === "update" || policy.command === "delete";
3302
+ const publicRoles = (policy) => policy.roles.length === 0 || policy.roles.some((role) => role === "public" || role === "anon");
3303
+ const isTrue = (expression) => expression !== null && /^\(*\s*true\s*\)*$/i.test(expression.trim());
3304
+ async function moduleVersionsBehind(context) {
3305
+ const findings = [];
3306
+ const upgrade = "Run `better-supabase upgrade`";
3307
+ for (const { file, path, installed, behind } of await moduleFiles(context)) {
3308
+ if (!behind || installed === void 0) continue;
3309
+ findings.push({
3310
+ message: `${path} has ${file.instance ?? file.module} version ${String(installed)}; this release ships version ${String(moduleFileVersion(file.contents)?.version ?? 1)}. ${upgrade}, then \`${migrationCommand(workflowOf(context))}\`.`,
3311
+ target: path,
3312
+ location: {
3313
+ file: path,
3314
+ line: 1
3315
+ }
3316
+ });
3317
+ }
3318
+ const db = context.database;
3319
+ if (!db || "skipped" in db || context.config.blockNames.length === 0) return findings;
3320
+ let rows;
3321
+ try {
3322
+ rows = await db.query("select name, version from better_supabase.blocks");
3323
+ } catch {
3324
+ try {
3325
+ rows = await db.query("select name, version from better_supabase.modules");
3326
+ } catch {
3327
+ return findings;
3328
+ }
3329
+ }
3330
+ for (const row of rows) {
3331
+ const module = SQL_MODULES[row.name];
3332
+ if (!module || !context.config.blockNames.includes(row.name)) continue;
3333
+ const version = blockSqlVersion(module);
3334
+ if (row.version >= version) continue;
3335
+ findings.push({
3336
+ message: `The database has ${row.name} version ${String(row.version)}; this release ships version ${String(version)}. ${upgrade} and apply the migrations it prints.`,
3337
+ target: `better_supabase.blocks.${row.name}`
3338
+ });
3339
+ }
3340
+ return findings;
3341
+ }
3342
+ function authSetting(context, key) {
3343
+ return context.configToml ? tomlGet(context.configToml.document, ["auth", key]) : void 0;
3344
+ }
3345
+ function tomlLocation(context, key) {
3346
+ if (!context.configToml) return;
3347
+ const line = lineOf(context.configToml.text, new RegExp(`^\\s*${key}\\s*=`)) ?? lineOf(context.configToml.text, /^\[auth\]/);
3348
+ return line ? {
3349
+ file: context.configToml.path,
3350
+ line
3351
+ } : void 0;
3352
+ }
3353
+ /**
3354
+ * `db.x.aggregate(`, an `_sum:`/`_avg:`/`_min:`/`_max:` include or a list with
3355
+ * `facetCounts: true`.
3356
+ */
3357
+ const AGGREGATE_USE = /\.aggregate\(|\b_(?:sum|avg|min|max)\s*:\s*\{|\bfacetCounts\s*:\s*true\b/;
3358
+ /**
3359
+ * A policy that reads memberships or the tenant claim: `is_member(...)`,
3360
+ * `current_tenant_id()`, `memberships`. The authorization provider's functions
3361
+ * (`authorization.requires`) count too.
3362
+ */
3363
+ const TENANT_HELPER = /member|tenant|current_org|org_id/i;
3364
+ /**
3365
+ * A foreign key to the tenant table itself, whose own policies are usually
3366
+ * read-only.
3367
+ */
3368
+ const TENANT_COLUMN = /^(?:organization|organization|tenant|team|workspace|account)_id$/;
3369
+ const COMMANDS = [
3370
+ "select",
3371
+ "insert",
3372
+ "update",
3373
+ "delete"
3374
+ ];
3375
+ const MFA_FACTORS = /\bauth\.\s*"?mfa_factors\b/i;
3376
+ const RULES = [
3377
+ ...[
3378
+ advisorRule("BS100", "security", "Supabase security advisor", "Findings from the Security Advisor (splinter): RLS disabled in exposed schemas, RLS without policies, mutable search_path, security definer functions callable by anon, exposed auth.users and more. Hosted projects use the Management API; local stacks and direct connections run the pinned splinter.sql."),
3379
+ {
3380
+ code: "BS103",
3381
+ severity: "error",
3382
+ title: "Policy allows anonymous writes",
3383
+ description: "A permissive policy with `true` for anon or public lets anyone insert, update or delete rows.",
3384
+ check: (context) => exposed(context).flatMap((table) => table.policies.filter((policy) => policy.permissive && writes(policy) && publicRoles(policy) && (isTrue(policy.using) || isTrue(policy.check))).map((policy) => ({
3385
+ message: `Policy "${policy.name}" on ${qualified(table)} allows ${policy.command} for ${policy.roles.join(", ") || "public"} with \`true\`.`,
3386
+ target: `${qualified(table)}.${policy.name}`,
3387
+ object: policyObject(table, policy)
3388
+ })))
3389
+ },
3390
+ {
3391
+ code: "BS106",
3392
+ severity: "error",
3393
+ title: "Table not granted to the Data API",
3394
+ description: "Hosted projects grant new tables to anon and authenticated unless the project turns default privileges off (auto_expose_new_tables = false locally). 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, except tables with RLS on and no permissive policy under `blocks['grants'].options.fromPolicies`, which are service-only. Tables marked `serviceRole` in `tables` must have no grants for either role.",
3395
+ check: (context) => {
3396
+ const auto = context.configToml ? tomlGet(context.configToml.document, ["api", "auto_expose_new_tables"]) : void 0;
3397
+ const note = auto === false || auto === "false" ? " config.toml sets [api] auto_expose_new_tables = false, so new tables start without grants." : "";
3398
+ return exposed(context).flatMap((table) => {
3399
+ const options = configFor(context.config.tables, table.schema, table.name);
3400
+ if (options?.exclude) return [];
3401
+ const granted = (role) => new Set([...table.grants, ...table.columnGrants ?? []].filter((grant) => grant.role === role || grant.role === "PUBLIC").flatMap((grant) => grant.privileges.map((privilege) => privilege.toLowerCase())));
3402
+ if (options?.serviceRole) {
3403
+ const fromPublic = table.grants.some((grant) => grant.role === "PUBLIC" && grant.privileges.length > 0);
3404
+ return ["anon", "authenticated"].flatMap((role) => {
3405
+ const have = [...granted(role)];
3406
+ if (have.length === 0) return [];
3407
+ const from = fromPublic ? `${role}, public` : role;
3408
+ return [{
3409
+ 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};\`.`,
3410
+ target: `${qualified(table)}:${role}`,
3411
+ object: tableObject(table)
3412
+ }];
3413
+ });
3414
+ }
3415
+ const serviceOnly = context.config.blockNames.includes("grants") && context.config.blocks["grants"]?.options?.["fromPolicies"] === true && table.rls && !table.policies.some((policy) => policy.permissive && (appliesTo(policy, "anon") || appliesTo(policy, "authenticated")));
3416
+ const wanted = context.config.expose[qualified(table)] ?? context.config.expose[table.name] ?? {
3417
+ anon: [],
3418
+ authenticated: serviceOnly ? [] : ["select"]
3419
+ };
3420
+ return ["anon", "authenticated"].flatMap((role) => {
3421
+ const have = granted(role);
3422
+ const missing = wanted[role].filter((privilege) => !privilege.includes("(") && !have.has(privilege));
3423
+ if (missing.length === 0) return [];
3424
+ return [{
3425
+ message: `${role} has no ${missing.join(", ")} on ${qualified(table)}, so the Data API answers 42501. Add it to \`expose\` and run \`better-supabase add grants\`, or run \`grant ${missing.join(", ")} on table ${qualified(table)} to ${role};\`.${note}`,
3426
+ target: `${qualified(table)}:${role}`,
3427
+ object: tableObject(table)
3428
+ }];
3429
+ });
3430
+ });
3431
+ }
3432
+ },
3433
+ {
3434
+ code: "BS107",
3435
+ severity: "warning",
3436
+ title: "Tenant table without a policy for every command",
3437
+ 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`. On an app table a block runs over, only the app's own policies decide whether it's reported, since the block writes through its own functions.",
3438
+ check: (context) => {
3439
+ const column = context.config.extensions.tenant?.column;
3440
+ const providerFunctions = [...new Set((context.config.authorization?.requires ?? []).flatMap((entry) => [entry.function, entry.function.slice(entry.function.indexOf(".") + 1)]))];
3441
+ const tables = exposed(context);
3442
+ const blocked = blockTargets(context);
3443
+ 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}`)));
3444
+ return tables.flatMap((table) => {
3445
+ if (table.kind !== "table" || !table.rls) return [];
3446
+ if (roots.has(qualified(table))) return [];
3447
+ const options = configFor(context.config.tables, table.schema, table.name);
3448
+ if (options?.exclude || options?.serviceRole) return [];
3449
+ const forAuthenticated = (policy) => policy.roles.some((role) => ["authenticated", "public"].includes(role));
3450
+ const blockTable = blocked.has(qualified(table));
3451
+ const permissive = table.policies.filter((policy) => policy.permissive && forAuthenticated(policy));
3452
+ const granting = permissive.filter((policy) => !(blockTable && policy.name.startsWith("bsb_")));
3453
+ if (granting.length === 0) return [];
3454
+ if (!(column !== void 0 && table.columns.some((entry) => entry.name === column) || granting.some((policy) => {
3455
+ const text = [
3456
+ policy.using ?? "",
3457
+ policy.check ?? "",
3458
+ ...policy.functions ?? []
3459
+ ].join(" ");
3460
+ return TENANT_HELPER.test(text) || providerFunctions.some((fn) => text.includes(fn));
3461
+ }))) return [];
3462
+ const denying = table.policies.filter((policy) => !policy.permissive && forAuthenticated(policy) && /^\s*false\s*$/i.test(policy.using ?? ""));
3463
+ const covered = new Set([...permissive, ...denying].flatMap((policy) => policy.command === "all" ? COMMANDS : [policy.command]));
3464
+ const missing = COMMANDS.filter((command) => !covered.has(command));
3465
+ if (missing.length === 0) return [];
3466
+ return [{
3467
+ message: `${qualified(table)} is tenant-scoped but has no ${missing.join(", ")} policy for authenticated, so ${missing.join(" and ")} silently match no rows. Add the policies, or state the intent with a restrictive \`using (false)\` policy.`,
3468
+ target: qualified(table),
3469
+ object: tableObject(table)
3470
+ }];
3471
+ });
3472
+ }
3473
+ },
3474
+ {
3475
+ code: "BS108",
3476
+ severity: "error",
3477
+ title: "Policy reads auth.mfa_factors directly",
3478
+ 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.",
3479
+ check: (context) => exposed(context).flatMap((table) => table.policies.filter((policy) => MFA_FACTORS.test(`${policy.using ?? ""} ${policy.check ?? ""}`)).map((policy) => ({
3480
+ 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 add mfa\` instead.`,
3481
+ target: `${qualified(table)}.${policy.name}`,
3482
+ object: policyObject(table, policy)
3483
+ })))
3484
+ },
3485
+ 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."),
3486
+ {
3487
+ code: "BS204",
3488
+ severity: "warning",
3489
+ title: "Tenant column without an index",
3490
+ description: "The memberships extension and organization policies filter every query on this column. A view is left out: a query on it uses the indexes of its tables.",
3491
+ check: (context) => {
3492
+ const tenant = context.config.extensions.tenant;
3493
+ if (!tenant) return [];
3494
+ return exposed(context).filter((table) => table.kind === "table" && table.columns.some((column) => column.name === tenant.column) && !table.indexes.some((index) => !index.partial && index.columns[0] === tenant.column)).map((table) => ({
3495
+ message: `${qualified(table)}.${tenant.column} has no index starting with it.`,
3496
+ target: `${qualified(table)}.${tenant.column}`,
3497
+ object: tableObject(table)
3498
+ }));
3499
+ }
3500
+ },
3501
+ {
3502
+ code: "BS210",
3503
+ severity: "warning",
3504
+ title: "Aggregates used while PostgREST disables them",
3505
+ description: "PostgREST rejects `count()`, `sum()` and the other aggregates with PGRST123 unless `pgrst.db_aggregates_enabled` is on for the authenticator role. `aggregate()`, `_sum`/`_avg`/`_min`/`_max` includes and list `facetCounts` need it; `better-supabase/postgres` does not.",
3506
+ check: (context) => {
3507
+ const settings = context.snapshot.extras.roleSettings;
3508
+ if (!settings) return [];
3509
+ const enabled = settings["authenticator"]?.["pgrst.db_aggregates_enabled"];
3510
+ if (enabled === "true" || enabled === "on") return [];
3511
+ for (const file of context.sources) {
3512
+ const line = lineOf(file.text, AGGREGATE_USE);
3513
+ if (line === void 0) continue;
3514
+ return [{
3515
+ message: `${file.path} uses aggregates, but PostgREST has them off, so these requests fail with PGRST123. Run \`alter role authenticator set pgrst.db_aggregates_enabled = 'true'; notify pgrst, 'reload config';\` in a migration.`,
3516
+ target: "authenticator:pgrst.db_aggregates_enabled",
3517
+ location: {
3518
+ file: file.path,
3519
+ line
3520
+ }
3521
+ }];
3522
+ }
3523
+ return [];
3524
+ }
3525
+ },
3526
+ {
3527
+ code: "BS301",
3528
+ severity: "warning",
3529
+ title: "Soft delete hidden by a select policy",
3530
+ description: "PostgREST re-reads updated rows through the select policy. If it hides deleted rows, soft-deleting fails with an RLS error unless the update returns nothing.",
3531
+ check: (context) => {
3532
+ const softDelete = context.config.extensions.softDelete;
3533
+ if (!softDelete) return [];
3534
+ return exposed(context).flatMap((table) => {
3535
+ if (!table.columns.some((column) => column.name === softDelete.column)) return [];
3536
+ return table.policies.filter((policy) => (policy.command === "select" || policy.command === "all") && new RegExp(`\\b${softDelete.column}\\b\\s+IS\\s+NULL`, "i").test(policy.using ?? "")).map((policy) => ({
3537
+ message: `Policy "${policy.name}" on ${qualified(table)} hides rows where ${softDelete.column} is set, so the trash extension can't read the row back. Filter deleted rows in queries (the extension does) instead of in the policy.`,
3538
+ target: `${qualified(table)}.${policy.name}`,
3539
+ object: policyObject(table, policy)
3540
+ }));
3541
+ });
3542
+ }
3543
+ },
3544
+ {
3545
+ code: "BS302",
3546
+ severity: "warning",
3547
+ title: "Bucket differs from the config",
3548
+ description: "A bucket in `buckets` is missing or has different settings in the database.",
3549
+ check: (context) => Object.entries(context.config.buckets).flatMap(([name, bucket]) => {
3550
+ const id = bucket.id ?? name.replaceAll(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
3551
+ const defined = defineBucket({
3552
+ id,
3553
+ path: bucket.path,
3554
+ ...bucket.public === void 0 ? {} : { public: bucket.public },
3555
+ ...bucket.fileSizeLimit === void 0 ? {} : { fileSizeLimit: bucket.fileSizeLimit },
3556
+ ...bucket.allowedMimeTypes === void 0 ? {} : { allowedMimeTypes: bucket.allowedMimeTypes },
3557
+ ...bucket.versioning === void 0 ? {} : { versioning: bucket.versioning },
3558
+ ...bucket.lifecycle === void 0 ? {} : { lifecycle: bucket.lifecycle }
3559
+ });
3560
+ const actual = catalogOf(context).buckets.find((entry) => entry.id === id);
3561
+ const inDatabase = defined.drift(actual).map((drift) => ({
3562
+ message: `Bucket ${id}: ${drift.message}`,
3563
+ target: `storage.buckets.${id}`
3564
+ }));
3565
+ const toml = context.configToml;
3566
+ const declared = toml ? tomlGet(toml.document, [
3567
+ "storage",
3568
+ "buckets",
3569
+ id
3570
+ ]) : void 0;
3571
+ if (!toml || !declared || typeof declared !== "object" || Array.isArray(declared)) return inDatabase;
3572
+ const entry = declared;
3573
+ const size = entry["file_size_limit"];
3574
+ const mimes = entry["allowed_mime_types"];
3575
+ const line = lineOf(toml.text, new RegExp(`^\\[storage\\.buckets\\.${id.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\]`));
3576
+ const inToml = defined.drift({
3577
+ public: entry["public"] === true,
3578
+ fileSizeLimit: typeof size === "string" || typeof size === "number" ? parseSize(size) : null,
3579
+ allowedMimeTypes: Array.isArray(mimes) ? mimes.map(String) : null
3580
+ }).map((drift) => ({
3581
+ message: `${toml.path} [storage.buckets.${id}]: ${drift.message.replace(`Bucket "${id}" `, "")}`,
3582
+ target: `[storage.buckets.${id}]`,
3583
+ ...line ? { location: {
3584
+ file: toml.path,
3585
+ line
3586
+ } } : {}
3587
+ }));
3588
+ return [...inDatabase, ...inToml];
3589
+ })
3590
+ },
3591
+ {
3592
+ code: "BS303",
3593
+ severity: "error",
3594
+ title: "Generated code is out of date",
3595
+ description: "The generated module no longer matches the database, so types and metadata are wrong. Same check as `gen --check`.",
3596
+ check: async (context) => {
3597
+ const stale = [];
3598
+ for (const file of await renderFiles(context.config, context.snapshot)) {
3599
+ const current = await readFile(resolve(context.config.root, file.path), "utf-8").catch(() => void 0);
3600
+ if (!sameText(current, file.contents)) stale.push({
3601
+ message: `${file.path} is ${current === void 0 ? "missing" : "out of date"}. Run \`better-supabase gen\`.`,
3602
+ target: file.path,
3603
+ ...current === void 0 ? {} : { location: {
3604
+ file: file.path,
3605
+ line: 1
3606
+ } }
3607
+ });
3608
+ }
3609
+ return stale;
3610
+ }
3611
+ },
3612
+ {
3613
+ code: "BS304",
3614
+ severity: "warning",
3615
+ title: "SQL module files are out of date",
3616
+ description: "A module listed in `blocks` differs from the version in this release. Same check as `upgrade --check`.",
3617
+ check: async (context) => {
3618
+ const stale = [];
3619
+ const workflow = workflowOf(context);
3620
+ const states = await moduleFiles(context);
3621
+ for (const { file, path, current, behind } of states) {
3622
+ const migrated = workflow === "migrations" && file.kind !== "test";
3623
+ const name = file.instance ?? file.module;
3624
+ if (current !== void 0 && !migrated && blockFileState(current) === "edited") {
3625
+ stale.push({
3626
+ message: `${path} (${name}) was edited by hand. Move the change to an override file or a SQL hook, or run \`better-supabase upgrade --force\`.`,
3627
+ target: path,
3628
+ location: {
3629
+ file: path,
3630
+ line: 1
3631
+ }
3632
+ });
3633
+ continue;
3634
+ }
3635
+ if (behind || sameBlockFile(current, file.contents)) continue;
3636
+ const next = file.kind === "data" && !migrated ? "Run `better-supabase upgrade`, which also writes the rows into a migration, since a schema diff skips them." : `Run \`better-supabase upgrade\`, then \`${migrationCommand(workflow)}\`.`;
3637
+ const subject = migrated ? `${name} in ${path} is ${current === void 0 ? "missing" : "out of date"}` : `${path} (${name}) is ${current === void 0 ? "missing" : "out of date"}`;
3638
+ if (stale.some((finding) => finding.message === `${subject}. ${next}`)) continue;
3639
+ stale.push({
3640
+ message: `${subject}. ${next}`,
3641
+ target: path,
3642
+ ...current === void 0 ? {} : { location: {
3643
+ file: path,
3644
+ line: 1
3645
+ } }
3646
+ });
3647
+ }
3648
+ if (workflow !== "migrations") {
3649
+ const orphans = await orphanedBlockFiles(context.config, {
3650
+ dir: context.config.sql.dir,
3651
+ prefix: context.config.sql.prefix,
3652
+ schemasDir: context.config.sql.dir
3653
+ }, new Set(states.map((state) => state.file.path)));
3654
+ for (const path of orphans) {
3655
+ const current = await readFile(resolve(context.config.root, path), "utf-8").catch(() => "");
3656
+ const edited = blockFileState(current) === "edited";
3657
+ stale.push({
3658
+ message: edited ? `${path} is left from a block that is no longer installed, and it was edited by hand. Move the change out, then delete the file or run \`better-supabase upgrade --force\`.` : `${path} is left from a block that is no longer installed. Run \`better-supabase upgrade\` to remove it.`,
3659
+ target: path,
3660
+ location: {
3661
+ file: path,
3662
+ line: 1
3663
+ }
3664
+ });
3665
+ }
3666
+ }
3667
+ return stale;
3668
+ }
3669
+ },
3670
+ {
3671
+ code: "BS316",
3672
+ severity: "warning",
3673
+ title: "Legacy migra diff engine",
3674
+ 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. Setting `database.sql.workflow` to `migra` or `migrations` silences it.",
3675
+ check: (context) => {
3676
+ const toml = context.configToml;
3677
+ if (toml === void 0 || diffEngine(toml) === "pg-delta") return [];
3678
+ if (context.config.sql.workflow !== void 0 && context.config.sql.workflow !== "pg-delta") return [];
3679
+ const schemas = `${toml.dir}/schemas/`;
3680
+ if (!(context.config.blockNames.length > 0 || (context.sqlFiles ?? []).some((file) => file.path.startsWith(schemas)))) return [];
3681
+ const keep = context.config.sql.workflow === void 0 ? " To stay on migra, set `database.sql.workflow: \"migra\"` in better-supabase.config.ts." : "";
3682
+ return [{
3683
+ 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\`.${keep}`,
3684
+ target: toml.path,
3685
+ location: {
3686
+ file: toml.path,
3687
+ line: 1
3688
+ }
3689
+ }];
3690
+ }
3691
+ },
3692
+ {
3693
+ code: "BS305",
3694
+ severity: "warning",
3695
+ title: "Live query table without change broadcasts",
3696
+ description: "A table in `database.realtime.tables` has no `bsb_realtime` trigger, so live queries never hear about its changes.",
3697
+ check: (context) => {
3698
+ const tables = catalogOf(context).tables;
3699
+ return context.config.realtime.tables.flatMap((name) => {
3700
+ const table = tables.find((entry) => entry.name === name || qualified(entry) === name);
3701
+ if (!table) return [{
3702
+ message: `database.realtime.tables lists "${name}", which doesn't exist.`,
3703
+ target: name
3704
+ }];
3705
+ if (table.triggers.some((trigger) => /^bsb?_realtime/.test(trigger.name))) return [];
3706
+ return [{
3707
+ message: `${qualified(table)} is in database.realtime.tables but has no broadcast trigger. Run \`better-supabase add realtime-tables\`, then \`${migrationCommand(workflowOf(context))}\`.`,
3708
+ target: qualified(table),
3709
+ object: tableObject(table)
3710
+ }];
3711
+ });
3712
+ }
3713
+ },
3714
+ {
3715
+ code: "BS306",
3716
+ severity: "warning",
3717
+ title: "Realtime delete events without keys",
3718
+ description: "Tables in the `supabase_realtime` publication send the replica identity of deleted rows. With `nothing`, or `default` without a primary key, delete events carry no keys and clients cannot tell which row went away.",
3719
+ check: (context) => {
3720
+ const published = new Set(catalogOf(context).realtime);
3721
+ return catalogOf(context).tables.filter((table) => published.has(qualified(table)) && (table.replicaIdentity === "NOTHING" || table.replicaIdentity === "DEFAULT" && table.primaryKey.length === 0)).map((table) => ({
3722
+ message: `${qualified(table)} is published to Realtime with replica identity ${table.replicaIdentity?.toLowerCase() ?? "unknown"}${table.primaryKey.length === 0 ? " and no primary key" : ""}. Add a primary key or run \`alter table ${qualified(table)} replica identity full\`.`,
3723
+ target: qualified(table),
3724
+ object: tableObject(table)
3725
+ }));
3726
+ }
3727
+ },
3728
+ {
3729
+ code: "BS311",
3730
+ severity: "warning",
3731
+ title: "SQL module behind its current version",
3732
+ description: "A block file or `better_supabase.blocks` records an older module version than this release ships. Same check as `upgrade --check`.",
3733
+ check: moduleVersionsBehind
3734
+ },
3735
+ {
3736
+ code: "BS401",
3737
+ severity: "warning",
3738
+ title: "Refresh token reuse interval is 0",
3739
+ description: "Server instances refreshing the same session at once need a reuse interval, or all but one of them sign the user out.",
3740
+ check: (context) => {
3741
+ if (!context.configToml) return [];
3742
+ const rotation = authSetting(context, "enable_refresh_token_rotation");
3743
+ const interval = authSetting(context, "refresh_token_reuse_interval");
3744
+ if (rotation === false || rotation === "false" || interval === void 0 || Number(interval) > 0) return [];
3745
+ const location = tomlLocation(context, "refresh_token_reuse_interval");
3746
+ return [{
3747
+ message: "refresh_token_reuse_interval = 0 with refresh token rotation. Use 10 (the Supabase default) so concurrent refreshes succeed.",
3748
+ target: "[auth] refresh_token_reuse_interval",
3749
+ ...location ? { location } : {}
3750
+ }];
3751
+ }
3752
+ },
3753
+ {
3754
+ code: "BS402",
3755
+ severity: "info",
3756
+ title: "Long access token lifetime",
3757
+ description: "Tokens are verified locally without an auth call, so a revoked session stays valid until the token expires.",
3758
+ check: (context) => {
3759
+ if (!context.configToml) return [];
3760
+ const expiry = Number(authSetting(context, "jwt_expiry") ?? 3600);
3761
+ if (expiry <= 3600) return [];
3762
+ const location = tomlLocation(context, "jwt_expiry");
3763
+ return [{
3764
+ message: `jwt_expiry is ${expiry}s. Keep it at 3600 or less; sessions refresh in the proxy.`,
3765
+ target: "[auth] jwt_expiry",
3766
+ ...location ? { location } : {}
3767
+ }];
3768
+ }
3769
+ },
3770
+ {
3771
+ code: "BS403",
3772
+ severity: "info",
3773
+ title: "Local stack signs tokens with a shared secret",
3774
+ description: "Hosted projects sign with asymmetric keys. Using the same locally means local tokens verify through JWKS just like production.",
3775
+ check: (context) => {
3776
+ if (!context.configToml || authSetting(context, "signing_keys_path") || tomlGet(context.configToml.document, ["db", "driver"]) !== void 0) return [];
3777
+ const location = tomlLocation(context, "signing_keys_path");
3778
+ return [{
3779
+ message: "No [auth] signing_keys_path. Run `better-supabase dev keys` to sign local tokens with ES256.",
3780
+ target: "[auth] signing_keys_path",
3781
+ ...location ? { location } : {}
3782
+ }];
3783
+ }
3784
+ },
3785
+ {
3786
+ code: "BS406",
3787
+ severity: "warning",
3788
+ title: "Foreign key to auth.users blocks account deletion",
3789
+ description: "A foreign key to `auth.users` with `no action` or `restrict` makes `auth.admin.deleteUser` (and `deleteAccount`) fail with \"Database error deleting user\" while the user has rows. Use `on delete cascade` for data the user owns, or `on delete set null` for records that outlive them.",
3790
+ check: (context) => catalogOf(context).tables.flatMap((table) => table.schema === "auth" ? [] : table.foreignKeys.filter((key) => key.refSchema === "auth" && key.refTable === "users" && (key.onDelete === "no action" || key.onDelete === "restrict")).map((key) => ({
3791
+ message: `${qualified(table)}.${key.name} (${key.columns.join(", ")}) references auth.users with on delete ${key.onDelete}. Deleting the user fails while rows point at it; use on delete cascade or set null.`,
3792
+ target: `${qualified(table)}.${key.name}`,
3793
+ object: tableObject(table)
3794
+ })))
3795
+ }
3796
+ ],
3797
+ ...RLS_RULES,
3798
+ ...POLICY_RULES,
3799
+ ...SCHEMA_DESIGN_RULES,
3800
+ ...HOOK_RULES,
3801
+ ...COOKIE_RULES,
3802
+ ...REMOVED_API_RULES,
3803
+ ...AUTHORIZATION_RULES,
3804
+ ...LIVE_RULES,
3805
+ ...MODULE_RULES,
3806
+ ...PGDELTA_RULES,
3807
+ ...LITE_RULES,
3808
+ ...DEPENDENCY_RULES,
3809
+ ...WORKFLOW_BUILDER_RULES,
3810
+ ...FLAG_RULES,
3811
+ ...REALTIME_RULES,
3812
+ ...ADOPTION_RULES,
3813
+ ...MIGRATION_SAFETY_RULES,
3814
+ ...NOT_VALID_RULES,
3815
+ ...APP_TABLE_SCOPE_RULES,
3816
+ ...ENV_RULES,
3817
+ ...WIRING_RULES,
3818
+ ...STACK_RULES
3819
+ ].toSorted((a, b) => byCodePoint(a.code, b.code));
3820
+ const RULE_CODES = RULES.map((rule) => rule.code);
3821
+ async function runRules(context, rules = RULES) {
3822
+ const findings = [];
3823
+ const scoped = {
3824
+ ...context,
3825
+ codes: rules.map((rule) => rule.code)
3826
+ };
3827
+ for (const rule of rules) {
3828
+ const help = `${DOCS_URL}#${rule.code.toLowerCase()}`;
3829
+ const skipped = skippedForMetadata(context.snapshot, rule, help);
3830
+ if (skipped) {
3831
+ findings.push(skipped);
3832
+ continue;
3833
+ }
3834
+ for (const input of await rule.check(scoped)) findings.push({
3835
+ code: rule.code,
3836
+ severity: rule.severity,
3837
+ title: rule.title,
3838
+ help,
3839
+ ...input
3840
+ });
3841
+ }
3842
+ return findings;
3843
+ }
3844
+ //#endregion
3845
+ //#region src/cli/doctor/format.ts
3846
+ const DOCTOR_FORMATS = [
3847
+ "text",
3848
+ "json",
3849
+ "sarif",
3850
+ "github"
3851
+ ];
3852
+ const DOCTOR_REPORT_SCHEMA_URL = "https://unpkg.com/better-supabase/schemas/doctor-report-v1.json";
3853
+ const SARIF_SCHEMA_URL = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";
3854
+ function summarize(findings) {
3855
+ return {
3856
+ errors: findings.filter((finding) => finding.severity === "error").length,
3857
+ warnings: findings.filter((finding) => finding.severity === "warning").length,
3858
+ infos: findings.filter((finding) => finding.severity === "info").length
3859
+ };
3860
+ }
3861
+ const plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
3862
+ const SEVERITY_FORMAT = {
3863
+ error: "red",
3864
+ warning: "yellow",
3865
+ info: "cyan"
3866
+ };
3867
+ function text(findings, paint) {
3868
+ if (findings.length === 0) return "No problems found.";
3869
+ const order = [
3870
+ "error",
3871
+ "warning",
3872
+ "info"
3873
+ ];
3874
+ const lines = [...findings].toSorted((a, b) => order.indexOf(a.severity) - order.indexOf(b.severity) || byCodePoint(a.code, b.code)).flatMap((finding) => [
3875
+ `${paint(SEVERITY_FORMAT[finding.severity], finding.severity.padEnd(7))} ${paint("bold", finding.code)} ${finding.title}${finding.location ? ` ${finding.location.file}:${finding.location.line}` : ""}`,
3876
+ ` ${finding.message}`,
3877
+ ` ${finding.help}`
3878
+ ]);
3879
+ const summary = summarize(findings);
3880
+ lines.push("", `${plural(summary.errors, "error")}, ${plural(summary.warnings, "warning")}, ${plural(summary.infos, "note")}.`);
3881
+ return lines.join("\n");
3882
+ }
3883
+ function sarifLevel(severity) {
3884
+ switch (severity) {
3885
+ case "error": return "error";
3886
+ case "warning": return "warning";
3887
+ case "info": return "note";
3888
+ default: return severity;
3889
+ }
3890
+ }
3891
+ function sarif(findings, rules, version, fallback) {
3892
+ return {
3893
+ $schema: SARIF_SCHEMA_URL,
3894
+ version: "2.1.0",
3895
+ runs: [{
3896
+ tool: { driver: {
3897
+ name: "better-supabase doctor",
3898
+ version,
3899
+ informationUri: DOCS_URL,
3900
+ rules: rules.map((rule) => ({
3901
+ id: rule.code,
3902
+ name: rule.title.replaceAll(/[^A-Za-z0-9]+(.)?/g, (_, char) => (char ?? "").toUpperCase()),
3903
+ shortDescription: { text: rule.title },
3904
+ fullDescription: { text: rule.description },
3905
+ helpUri: `${DOCS_URL}#${rule.code.toLowerCase()}`,
3906
+ defaultConfiguration: { level: sarifLevel(rule.severity) }
3907
+ }))
3908
+ } },
3909
+ results: findings.map((finding) => ({
3910
+ ruleId: finding.code,
3911
+ ruleIndex: rules.findIndex((rule) => rule.code === finding.code),
3912
+ level: sarifLevel(finding.severity),
3913
+ message: { text: finding.message },
3914
+ locations: [{ physicalLocation: {
3915
+ artifactLocation: { uri: finding.location?.file ?? fallback },
3916
+ region: { startLine: finding.location?.line ?? 1 }
3917
+ } }],
3918
+ partialFingerprints: { target: `${finding.code}:${finding.target ?? finding.message}` }
3919
+ }))
3920
+ }]
3921
+ };
3922
+ }
3923
+ const escapeData = (value) => value.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
3924
+ const escapeProperty = (value) => escapeData(value).replaceAll(":", "%3A").replaceAll(",", "%2C");
3925
+ function github(findings) {
3926
+ return findings.map((finding) => {
3927
+ return `::${finding.severity === "info" ? "notice" : finding.severity} ${[...finding.location ? [`file=${escapeProperty(finding.location.file)}`, `line=${finding.location.line}`] : [], `title=${escapeProperty(`${finding.code} ${finding.title}`)}`].join(",")}::${escapeData(`${finding.message} (${finding.help})`)}`;
3928
+ }).join("\n");
3929
+ }
3930
+ function jsonReport(findings, version) {
3931
+ return {
3932
+ $schema: DOCTOR_REPORT_SCHEMA_URL,
3933
+ version: 1,
3934
+ tool: {
3935
+ name: "better-supabase",
3936
+ version
3937
+ },
3938
+ summary: summarize(findings),
3939
+ findings
3940
+ };
3941
+ }
3942
+ function formatReport(findings, options) {
3943
+ switch (options.format) {
3944
+ case "text": return text(findings, options.paint ?? plain);
3945
+ case "json": return JSON.stringify(jsonReport(findings, options.version), null, 2);
3946
+ case "sarif": return JSON.stringify(sarif(findings, options.rules, options.version, options.fallbackFile), null, 2);
3947
+ case "github": return github(findings);
3948
+ default: return options.format;
3949
+ }
3950
+ }
3951
+ //#endregion
3952
+ //#region src/cli/doctor/inputs.ts
3953
+ const ENV_FILES = [
3954
+ ".env",
3955
+ ".env.local",
3956
+ ".env.example",
3957
+ ".env.development",
3958
+ ".env.development.local",
3959
+ ".env.production",
3960
+ ".env.production.local"
3961
+ ];
3962
+ async function readText(root, path) {
3963
+ const absolute = resolve(root, path);
3964
+ return existsSync(absolute) ? {
3965
+ path,
3966
+ text: await readFile(absolute, "utf-8")
3967
+ } : void 0;
3968
+ }
3969
+ /**
3970
+ * The declarative schemas in the diff engine's order, then the migrations
3971
+ * newest first.
3972
+ */
3973
+ async function sqlFiles(root, toml, workflow) {
3974
+ const read = async (path) => ({
3975
+ path,
3976
+ text: await readFile(resolve(root, path), "utf-8")
3977
+ });
3978
+ const schemas = await Promise.all((await schemaPaths(root, toml, workflow)).files.map(read));
3979
+ const migrationsPath = `${toml?.dir ?? "supabase"}/migrations`;
3980
+ const migrationsDir = resolve(root, migrationsPath);
3981
+ const migrations = existsSync(migrationsDir) ? (await readdir(migrationsDir)).filter((name) => name.endsWith(".sql")).toSorted().toReversed().map((name) => `${migrationsPath}/${name}`) : [];
3982
+ return [...schemas, ...await Promise.all(migrations.map(read))];
3983
+ }
3984
+ /** Files matching `doctor.sources`, skipping dependencies and build output. */
3985
+ async function sourceFiles(config) {
3986
+ const files = [];
3987
+ for await (const path of glob([...config.doctor.sources], {
3988
+ cwd: config.root,
3989
+ exclude: (name) => name === "node_modules" || name === ".next"
3990
+ })) files.push({
3991
+ path,
3992
+ text: await readFile(resolve(config.root, path), "utf-8")
3993
+ });
3994
+ return files.toSorted((a, b) => byCodePoint(a.path, b.path));
3995
+ }
3996
+ //#endregion
3997
+ //#region src/cli/doctor/advisors.ts
3998
+ /**
3999
+ * Splinter (https://github.com/supabase/splinter), the linter behind the
4000
+ * dashboard's Security and Performance Advisors. It has no license, so it is
4001
+ * downloaded at this commit and checked against the hash instead of bundled.
4002
+ */
4003
+ const SPLINTER_COMMIT = "e74a9e36cb12258cb67d1464bc1cb196e9cd8446";
4004
+ const SPLINTER_SHA256 = "d8d558baad3e03832e521c527907fa50a9a172fabd899dd0f5c2504a5a0e9349";
4005
+ const SPLINTER_URL = `https://raw.githubusercontent.com/supabase/splinter/${SPLINTER_COMMIT}/splinter.sql`;
4006
+ function isLint(value) {
4007
+ const lint = value;
4008
+ return typeof lint === "object" && lint !== null && typeof lint.name === "string" && typeof lint.level === "string" && Array.isArray(lint.categories);
4009
+ }
4010
+ /** The hosted project's advisors (`GET /v1/projects/{ref}/advisors/{category}`). */
4011
+ function managementAdvisors(options) {
4012
+ const base = (options.apiUrl ?? "https://api.supabase.com").replace(/\/$/, "");
4013
+ return {
4014
+ describe: `project ${options.projectRef} advisors (Management API)`,
4015
+ async lints(category) {
4016
+ const url = `${base}/v1/projects/${encodeURIComponent(options.projectRef)}/advisors/${category}`;
4017
+ const text = await request(url, {
4018
+ what: `Management API ${category} advisors`,
4019
+ headers: { authorization: `Bearer ${options.accessToken}` },
4020
+ ...options.fetch ? { fetch: options.fetch } : {}
4021
+ });
4022
+ const body = JSON.parse(text);
4023
+ if (!Array.isArray(body.lints)) throw new CliError("request_failed", `Management API ${category} advisors returned no "lints".`);
4024
+ return body.lints.filter(isLint);
4025
+ }
4026
+ };
4027
+ }
4028
+ /** The `[api] schemas` list of a `config.toml`, when it names any. */
4029
+ function apiSchemas(document) {
4030
+ const value = document ? tomlGet(document, ["api", "schemas"]) : void 0;
4031
+ if (!Array.isArray(value)) return [];
4032
+ return value.filter((schema) => typeof schema === "string" && schema.trim() !== "");
4033
+ }
4034
+ const literal = (value) => `'${value.replaceAll("'", "''")}'`;
4035
+ /**
4036
+ * The read-only transaction splinter runs in, with `pgrst.db_schemas` set
4037
+ * first.
4038
+ */
4039
+ function splinterScript(sql, schemas = []) {
4040
+ return `begin read only;\n${schemas.length > 0 ? `set local pgrst.db_schemas = ${literal(schemas.join(","))};\n` : ""}${sql}\n;\nrollback;`;
4041
+ }
4042
+ const sha256 = (text) => createHash("sha256").update(text).digest("hex");
4043
+ /** Downloads (once) and verifies the pinned `splinter.sql`. */
4044
+ async function splinterSql(options) {
4045
+ const file = resolve(options.cacheDir, `splinter-${SPLINTER_COMMIT}.sql`);
4046
+ if (existsSync(file)) {
4047
+ const cached = await readFile(file, "utf-8");
4048
+ if (sha256(cached) === SPLINTER_SHA256) return cached;
4049
+ }
4050
+ const text = await request(SPLINTER_URL, {
4051
+ what: `Downloading splinter.sql from ${SPLINTER_URL}`,
4052
+ ...options.fetch ? { fetch: options.fetch } : {}
4053
+ });
4054
+ if (sha256(text) !== SPLINTER_SHA256) throw new CliError("integrity_failed", `splinter.sql from ${SPLINTER_URL} does not match the pinned hash; refusing to run it.`);
4055
+ await mkdir(dirname(file), { recursive: true });
4056
+ await writeFile(file, text);
4057
+ return text;
4058
+ }
4059
+ /** `pg` returns one result per statement for a multi-statement query. */
4060
+ function lintRows(result) {
4061
+ const results = Array.isArray(result) ? result : [result];
4062
+ return results.find((entry) => entry.fields?.some((field) => field.name === "cache_key"))?.rows ?? results.at(-1)?.rows ?? [];
4063
+ }
4064
+ /**
4065
+ * Runs splinter against a database (local stack, `$DATABASE_URL` or
4066
+ * `--db-url-stdin`) in a read-only transaction that is rolled back.
4067
+ */
4068
+ function splinterAdvisors(queryable, describe, options) {
4069
+ let all;
4070
+ const run = async () => {
4071
+ const sql = await splinterSql(options);
4072
+ return lintRows(await queryable.query(splinterScript(sql, options.schemas))).filter(isLint);
4073
+ };
4074
+ return {
4075
+ describe: `${describe} (splinter ${SPLINTER_COMMIT.slice(0, 7)})`,
4076
+ async lints(category) {
4077
+ all ??= run();
4078
+ const wanted = category.toUpperCase();
4079
+ return (await all).filter((lint) => lint.categories.includes(wanted));
4080
+ }
4081
+ };
4082
+ }
4083
+ //#endregion
4084
+ //#region src/cli/doctor/live-source.ts
4085
+ /** Checks that read the database itself rather than the snapshot. */
4086
+ const LIVE_CODES = /* @__PURE__ */ new Set([
4087
+ "BS100",
4088
+ "BS200",
4089
+ "BS208",
4090
+ "BS209",
4091
+ "BS212",
4092
+ "BS225",
4093
+ "BS236",
4094
+ "BS238"
4095
+ ]);
4096
+ /**
4097
+ * The database doctor reads, for advisors and live checks: the Management API
4098
+ * for hosted projects, one lazily opened connection otherwise (splinter runs
4099
+ * over it). Saved snapshots skip both.
4100
+ */
4101
+ function openLive(config, env, source, pg) {
4102
+ const none = { close: () => Promise.resolve() };
4103
+ const file = snapshotFile(config, source);
4104
+ if (file) {
4105
+ const skipped = `reading the saved snapshot ${file}; set $DATABASE_URL or $SUPABASE_DB_URL, or pass --db-url-stdin or --project-ref, to check a database.`;
4106
+ return {
4107
+ ...none,
4108
+ advisors: { skipped },
4109
+ database: { skipped }
4110
+ };
4111
+ }
4112
+ let opened;
4113
+ const open = () => opened ??= openSource(config, env, source, pg);
4114
+ const target = managementTarget(config, env, source);
4115
+ const database = {
4116
+ describe: target ? `project ${target.projectRef} (Management API)` : "database",
4117
+ session: !target,
4118
+ async query(sql) {
4119
+ return (await (await open()).queryable.query(sql)).rows;
4120
+ }
4121
+ };
4122
+ const close = async () => {
4123
+ if (opened) await (await opened).close();
4124
+ };
4125
+ if (target) return {
4126
+ advisors: managementAdvisors(target),
4127
+ database,
4128
+ open,
4129
+ close
4130
+ };
4131
+ let splinter;
4132
+ const cacheDir = resolve(config.root, CACHE_DIR);
4133
+ return {
4134
+ advisors: {
4135
+ describe: "database (splinter)",
4136
+ async lints(category) {
4137
+ splinter ??= Promise.all([open(), readSupabaseToml(config.root).catch(() => void 0)]).then(([db, toml]) => splinterAdvisors(db.queryable, db.describe, {
4138
+ cacheDir,
4139
+ schemas: apiSchemas(toml?.document)
4140
+ }));
4141
+ return (await splinter).lints(category);
4142
+ }
4143
+ },
4144
+ database,
4145
+ open,
4146
+ close
4147
+ };
4148
+ }
4149
+ //#endregion
4150
+ //#region src/cli/doctor/locate.ts
4151
+ const escape = (name) => name.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
4152
+ /**
4153
+ * Where a table, function or policy is declared: declarative schemas first,
4154
+ * then the newest migration.
4155
+ */
4156
+ function locate(files, object) {
4157
+ const name = `"?${escape(object.name)}"?`;
4158
+ const qualifiedName = `(?:"?${escape(object.schema)}"?\\.)?${name}`;
4159
+ if (object.kind === "policy" && object.table !== void 0) {
4160
+ const onTable = new RegExp(`create\\s+policy\\s+${name}\\s+on\\s+(?:only\\s+)?(?:"?${escape(object.schema)}"?\\.)?"?${escape(object.table)}"?(?:\\s|;|$)`, "i");
4161
+ for (const file of files) {
4162
+ const match = onTable.exec(file.text);
4163
+ if (match) return {
4164
+ file: file.path,
4165
+ line: file.text.slice(0, match.index).split("\n").length
4166
+ };
4167
+ }
4168
+ return;
4169
+ }
4170
+ 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");
4171
+ for (const file of files) {
4172
+ const line = lineOf(file.text, pattern);
4173
+ if (line) return {
4174
+ file: file.path,
4175
+ line
4176
+ };
4177
+ }
4178
+ }
4179
+ //#endregion
4180
+ //#region src/cli/doctor/options.ts
4181
+ /** The formats `--format` takes; `--json` picks the JSON one. */
4182
+ const FLAG_FORMATS = DOCTOR_FORMATS.filter((format) => format !== "json");
4183
+ /**
4184
+ * `--json` picks the JSON report; `--format`, then `doctor.format`, picks
4185
+ * between the others.
4186
+ */
4187
+ function reportFormat(args, configured) {
4188
+ if (args.format === "json") throw new CliError("usage", "--format json is now the global --json flag");
4189
+ if (args.json === true) {
4190
+ if (args.format !== void 0) throw new CliError("usage", "Pass --json or --format, not both");
4191
+ return "json";
4192
+ }
4193
+ if (args.format === void 0) return configured;
4194
+ const format = FLAG_FORMATS.find((candidate) => candidate === args.format);
4195
+ if (!format) throw new CliError("invalid_value", `--format must be one of ${FLAG_FORMATS.join(", ")}`);
4196
+ return format;
4197
+ }
4198
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4199
+ /** `--as <uuid>` or `--claims <json>` as `request.jwt.claims`. */
4200
+ function explainClaims(args) {
4201
+ const as = args.as;
4202
+ const claims = args.claims;
4203
+ if (as && claims) return { error: "Pass --as or --claims, not both" };
4204
+ if (as) {
4205
+ if (!UUID.test(as)) return { error: `--as takes a user id (uuid), got "${as}"` };
4206
+ return {
4207
+ sub: as,
4208
+ role: "authenticated"
4209
+ };
4210
+ }
4211
+ if (!claims) return { role: "anon" };
4212
+ try {
4213
+ const parsed = JSON.parse(claims);
4214
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return { error: "--claims takes a JSON object" };
4215
+ return {
4216
+ role: "anon",
4217
+ ...parsed
4218
+ };
4219
+ } catch (cause) {
4220
+ return { error: `--claims is not valid JSON: ${errorText(cause)}` };
4221
+ }
4222
+ }
4223
+ //#endregion
4224
+ //#region src/cli/commands/doctor.ts
4225
+ const ARGS = {
4226
+ format: {
4227
+ type: "string",
4228
+ description: "text, sarif (code scanning) or github (workflow annotations); also tooling.doctor.format, default text. --json for the JSON report",
4229
+ valueHint: "format"
4230
+ },
4231
+ out: {
4232
+ type: "string",
4233
+ description: "Write the report to a file; also tooling.doctor.output",
4234
+ valueHint: "file"
4235
+ },
4236
+ only: {
4237
+ type: "string",
4238
+ description: "Run only these checks, e.g. --only BS100,BS304; also tooling.doctor.only",
4239
+ valueHint: "codes"
4240
+ },
4241
+ ignore: {
4242
+ type: "string",
4243
+ description: "Skip checks; also tooling.doctor.ignore in the config",
4244
+ valueHint: "codes"
4245
+ },
4246
+ "fail-on": {
4247
+ type: "string",
4248
+ description: "Exit 1 on findings at this level: error (the default), warning (the default with tooling.doctor.strict) or never",
4249
+ valueHint: "level"
4250
+ },
4251
+ metadata: {
4252
+ type: "string",
4253
+ description: "Check a GeneratorMetadata document (- for stdin); checks it lacks data for are skipped",
4254
+ valueHint: "path|-"
4255
+ },
4256
+ stats: {
4257
+ type: "boolean",
4258
+ description: "Report slow frequent statements from pg_stat_statements (BS209)"
4259
+ },
4260
+ explain: {
4261
+ type: "string",
4262
+ description: "EXPLAIN ANALYZE these tables under RLS, rolled back (BS212)",
4263
+ valueHint: "tables"
4264
+ },
4265
+ as: {
4266
+ type: "string",
4267
+ description: "Plan as this authenticated user, and measure the claims the custom access token hook returns for them (BS405)",
4268
+ valueHint: "uuid"
4269
+ },
4270
+ claims: {
4271
+ type: "string",
4272
+ description: "Plan with these JWT claims ({\"role\":\"authenticated\",...})",
4273
+ valueHint: "json"
4274
+ },
4275
+ "fix-grants": {
4276
+ type: "boolean",
4277
+ description: "Print the grant and revoke SQL BS404 asks for, to add to your schema or migration"
4278
+ },
4279
+ fix: {
4280
+ type: "boolean",
4281
+ description: "Run the SQL that resolves findings which carry a fix, such as validating a not valid check (BS236), against the database"
4282
+ }
4283
+ };
4284
+ /** Where `--fix` writes, for the consent detail: a host, or the project. */
4285
+ async function fixTarget(config, env, options, projectRef) {
4286
+ if (projectRef !== void 0) return {
4287
+ local: false,
4288
+ where: `project ${projectRef}`
4289
+ };
4290
+ const url = await databaseUrl(config, env, options.dbUrl).catch(() => void 0);
4291
+ return url === void 0 ? {
4292
+ local: false,
4293
+ where: "the database"
4294
+ } : {
4295
+ local: isLocalDatabaseUrl(url),
4296
+ where: databaseHost(url)
4297
+ };
4298
+ }
4299
+ /**
4300
+ * Runs the fix of each finding that has one, and returns the findings left: the
4301
+ * ones without a fix, and the ones whose fix failed, with the error.
4302
+ */
4303
+ async function applyFixes(findings, database, consent, log) {
4304
+ if (findings.filter((finding) => finding.fix !== void 0).length === 0 || !database || "skipped" in database) return {
4305
+ left: [...findings],
4306
+ fixed: 0
4307
+ };
4308
+ await consent();
4309
+ const left = [];
4310
+ let fixed = 0;
4311
+ for (const finding of findings) {
4312
+ if (finding.fix === void 0) {
4313
+ left.push(finding);
4314
+ continue;
4315
+ }
4316
+ try {
4317
+ await database.query(finding.fix);
4318
+ fixed += 1;
4319
+ log(`Fixed ${finding.code} ${finding.target ?? finding.title}.`);
4320
+ } catch (cause) {
4321
+ await database.query("rollback").catch(() => []);
4322
+ left.push({
4323
+ ...finding,
4324
+ message: `${finding.message} doctor --fix failed: ${errorText(cause)}`
4325
+ });
4326
+ }
4327
+ }
4328
+ return {
4329
+ left,
4330
+ fixed
4331
+ };
4332
+ }
4333
+ async function runDoctor(config, args, env, options = {}) {
4334
+ const format = reportFormat(args, config.doctor.format);
4335
+ const only = args.only === void 0 ? [...config.doctor.only] : list(args.only);
4336
+ const ignore = /* @__PURE__ */ new Set([...config.doctor.ignore, ...list(args.ignore)]);
4337
+ const unknown = [...only, ...ignore].filter((code) => !RULE_CODES.includes(code));
4338
+ if (unknown.length > 0) throw new CliError("usage", `Unknown check ${unknown.join(", ")}. Checks: ${RULE_CODES.join(", ")}`);
4339
+ const explainTables = list(args.explain);
4340
+ const stats = args.stats === true;
4341
+ const claims = explainClaims(args);
4342
+ if ("error" in claims) throw new CliError("usage", String(claims.error));
4343
+ if (args.claims !== void 0 && explainTables.length === 0) throw new CliError("usage", "--claims plans a query, so it needs --explain <table>.");
4344
+ const hookUser = args.as;
4345
+ const asked = /* @__PURE__ */ new Set([
4346
+ ...stats ? ["BS209"] : [],
4347
+ ...explainTables.length > 0 ? ["BS212"] : [],
4348
+ ...hookUser ? ["BS405"] : []
4349
+ ]);
4350
+ const fixGrants = args["fix-grants"] === true;
4351
+ const rules = fixGrants ? RULES.filter((rule) => rule.code === "BS404") : RULES.filter((rule) => (only.length === 0 || only.includes(rule.code) || asked.has(rule.code)) && !ignore.has(rule.code));
4352
+ const snapshotPath = args.snapshot;
4353
+ const dbUrl = options.dbUrl;
4354
+ const projectRef = args["project-ref"];
4355
+ const source = {
4356
+ ...stats || explainTables.length > 0 || hookUser || args.fix === true ? { live: true } : {},
4357
+ ...snapshotPath ? { snapshotPath } : {},
4358
+ ...dbUrl ? { dbUrl } : {},
4359
+ ...projectRef ? { projectRef } : {},
4360
+ ...options.signal ? { signal: options.signal } : {}
4361
+ };
4362
+ const pg = options.connect ?? connect;
4363
+ const wantsLive = hookUser !== void 0 || rules.some((rule) => LIVE_CODES.has(rule.code));
4364
+ const opened = options.snapshot || !wantsLive ? {
4365
+ advisors: options.advisors,
4366
+ database: options.database,
4367
+ close: () => Promise.resolve()
4368
+ } : {
4369
+ ...openLive(config, env, source, pg),
4370
+ ...options.advisors === void 0 ? {} : { advisors: options.advisors },
4371
+ ...options.database === void 0 ? {} : { database: options.database }
4372
+ };
4373
+ const [snapshot, envFiles, [configToml, sql], gitignore, sources, readSets, peers, contentMapper, stackEnv, localStacks] = await Promise.all([
4374
+ options.snapshot ?? loadSnapshot(config, env, source, pg, opened.open),
4375
+ Promise.all(ENV_FILES.map((path) => readText(config.root, path))).then((files) => files.filter((file) => file !== void 0)),
4376
+ readSupabaseToml(config.root).then(async (toml) => [toml, await sqlFiles(config.root, toml, sqlWorkflow(config, toml))]),
4377
+ readText(config.root, ".gitignore"),
4378
+ sourceFiles(config),
4379
+ compiledReadSets(config).catch((cause) => ({ skipped: errorText(cause) })),
4380
+ options.peers ?? peerVersions(config.root).catch(() => []),
4381
+ options.contentMapper ?? (config.gen.contentMapper === true && rules.some((rule) => rule.code === "BS602") ? contentMapperSetup$1(config.root) : void 0),
4382
+ options.stackEnv ?? (rules.some((rule) => rule.code === "BS503") ? loadStackEnvCheck(config.root, env, options.signal) : void 0),
4383
+ options.localStacks ?? (rules.some((rule) => rule.code === "BS700" || rule.code === "BS701") ? loadLocalStackCheck(config.root, env) : void 0)
4384
+ ]).catch(async (cause) => {
4385
+ await opened.close();
4386
+ throw cause;
4387
+ });
4388
+ const replay = opened.open ? (await opened.open()).replay : void 0;
4389
+ const live = replay ? {
4390
+ advisors: options.advisors ?? { skipped: replay },
4391
+ database: options.database ?? { skipped: replay }
4392
+ } : opened;
4393
+ const context = {
4394
+ config,
4395
+ snapshot,
4396
+ sqlFiles: sql,
4397
+ configToml,
4398
+ envFiles,
4399
+ gitignore: gitignore?.text ?? "",
4400
+ sources,
4401
+ readSets,
4402
+ peers,
4403
+ ...contentMapper ? { contentMapper } : {},
4404
+ ...stackEnv ? { stackEnv } : {},
4405
+ shellEnv: env[PROCESS_ENV] ?? {},
4406
+ ...localStacks ? { localStacks } : {},
4407
+ ...live.advisors ? { advisors: live.advisors } : {},
4408
+ ...live.database ? { database: live.database } : {},
4409
+ ...stats ? { stats } : {},
4410
+ ...hookUser ? { hookUser } : {},
4411
+ ...explainTables.length > 0 ? { explain: {
4412
+ tables: explainTables,
4413
+ claims
4414
+ } } : {}
4415
+ };
4416
+ if (fixGrants) {
4417
+ await opened.close();
4418
+ return {
4419
+ code: 0,
4420
+ output: hookGrantBlock(hookGrantProblems(context), workflowOf(context))
4421
+ };
4422
+ }
4423
+ let ran;
4424
+ try {
4425
+ ran = await runRules(context, rules);
4426
+ if (args.fix === true) {
4427
+ const count = ran.filter((finding) => finding.fix !== void 0).length;
4428
+ ({left: ran} = await applyFixes(ran, context.database, async () => {
4429
+ const target = await fixTarget(config, env, options, projectRef);
4430
+ if (!target.local) options.requireConsent?.({
4431
+ effect: "database-write",
4432
+ detail: `doctor --fix runs ${String(count)} fix(es) against ${target.where}`
4433
+ });
4434
+ }, options.log ?? (() => {})));
4435
+ }
4436
+ } finally {
4437
+ await opened.close();
4438
+ }
4439
+ const findings = ran.map((finding) => {
4440
+ if (finding.location || !finding.object) return finding;
4441
+ const location = locate(sql, finding.object);
4442
+ return location ? {
4443
+ ...finding,
4444
+ location
4445
+ } : finding;
4446
+ });
4447
+ const out = args.out ?? (format === config.doctor.format ? config.doctor.output : void 0);
4448
+ const fromRepo = format === "sarif" || format === "github";
4449
+ const repoRoot = findRepoRoot(config.root);
4450
+ const repoPath = (file) => display(repoRoot, resolve(config.root, file));
4451
+ const report = formatReport(fromRepo ? findings.map((finding) => finding.location ? {
4452
+ ...finding,
4453
+ location: {
4454
+ ...finding.location,
4455
+ file: repoPath(finding.location.file)
4456
+ }
4457
+ } : finding) : findings, {
4458
+ format,
4459
+ rules,
4460
+ version: VERSION,
4461
+ fallbackFile: repoPath(context.configToml?.path ?? "package.json"),
4462
+ ...options.paint && !out ? { paint: options.paint } : {}
4463
+ });
4464
+ const errors = findings.filter((finding) => finding.severity === "error").length;
4465
+ const warnings = findings.filter((finding) => finding.severity === "warning").length;
4466
+ const failOn = args["fail-on"] ?? (config.doctor.strict ? "warning" : "error");
4467
+ if (failOn !== "error" && failOn !== "warning" && failOn !== "never") throw new CliError("usage", `--fail-on takes error, warning or never, got "${failOn}".`);
4468
+ const code = failOn !== "never" && (errors > 0 || failOn === "warning" && warnings > 0) ? 1 : 0;
4469
+ const failure = code === 0 ? {} : {
4470
+ errorCode: "findings",
4471
+ error: `Doctor found ${errors} errors and ${warnings} warnings${failOn === "warning" ? " (--fail-on warning)" : ""}.`
4472
+ };
4473
+ if (out) {
4474
+ const applied = await applyPlan({
4475
+ root: config.root,
4476
+ regenerate: "better-supabase doctor",
4477
+ files: [{
4478
+ path: insideRoot(config.root, out, "--out"),
4479
+ contents: `${report}\n`,
4480
+ note: `${errors} errors, ${warnings} warnings`
4481
+ }]
4482
+ }, {});
4483
+ return {
4484
+ code,
4485
+ output: changeLines(applied).join("\n"),
4486
+ data: {
4487
+ out,
4488
+ errors,
4489
+ warnings
4490
+ },
4491
+ ...failure
4492
+ };
4493
+ }
4494
+ return format === "json" ? {
4495
+ code,
4496
+ data: jsonReport(findings, VERSION),
4497
+ ...failure
4498
+ } : {
4499
+ code,
4500
+ output: report,
4501
+ ...failure
4502
+ };
4503
+ }
4504
+ const ruleCodes = () => [.../* @__PURE__ */ new Set([...RULE_CODES, ...LIVE_CODES])];
4505
+ const doctorCommand = defineCliCommand({
4506
+ meta: commandMeta("doctor"),
4507
+ args: {
4508
+ ...SOURCE_ARGS,
4509
+ ...ARGS
4510
+ },
4511
+ lists: [
4512
+ "only",
4513
+ "ignore",
4514
+ "explain"
4515
+ ],
4516
+ completions: {
4517
+ format: () => FLAG_FORMATS,
4518
+ only: ruleCodes,
4519
+ ignore: ruleCodes
4520
+ },
4521
+ run: async (args, { config, cwd, env, io, log, paint, requireConsent, signal }) => {
4522
+ if (args.metadata !== void 0) {
4523
+ refuseWithMetadata([
4524
+ ...sourceFlags(args),
4525
+ ...args.stats === true ? ["--stats"] : [],
4526
+ ...args.explain === void 0 ? [] : ["--explain"],
4527
+ ...args.as === void 0 ? [] : ["--as"],
4528
+ ...args.fix === true ? ["--fix"] : []
4529
+ ]);
4530
+ let snapshot;
4531
+ try {
4532
+ snapshot = await readMetadataSnapshot(args.metadata, cwd, io);
4533
+ } catch (cause) {
4534
+ if (cause instanceof MetadataRejectedError) return {
4535
+ code: 65,
4536
+ errorCode: "invalid_input",
4537
+ error: cause.message
4538
+ };
4539
+ throw cause;
4540
+ }
4541
+ const skipped = `reading a GeneratorMetadata document; set $DATABASE_URL or $SUPABASE_DB_URL, or pass --db-url-stdin or --project-ref, to check a database.`;
4542
+ return runDoctor(config, args, env, {
4543
+ snapshot,
4544
+ advisors: { skipped },
4545
+ database: { skipped },
4546
+ paint
4547
+ });
4548
+ }
4549
+ const dbUrl = await stdinDatabaseUrl(args["db-url-stdin"], io);
4550
+ const check = () => runDoctor(config, args, env, {
4551
+ paint,
4552
+ requireConsent,
4553
+ log: log.info,
4554
+ ...dbUrl ? { dbUrl } : {},
4555
+ ...signal ? { signal } : {}
4556
+ });
4557
+ return args.snapshot === void 0 ? log.progress("Checking the database", check) : check();
4558
+ }
4559
+ });
4560
+ //#endregion
4561
+ export { doctorCommand };