better-supabase 0.7.0 → 0.8.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 (500) hide show
  1. package/README.md +6 -2
  2. package/dist/{access-sql-B0dF-SoF.js → access-sql-CrSbK1q-.js} +1 -1
  3. package/dist/{adapter-B-tL8hqg.js → adapter-CN45ibbV.js} +663 -157
  4. package/dist/{agents-CVHGd7It.d.ts → agents-BY_LQXtQ.d.ts} +4 -4
  5. package/dist/{ai-cache-CBWs1W0k.js → ai-cache-Dfgk8nBT.js} +2 -1
  6. package/dist/{ai-cache-DaDu_G5L.d.ts → ai-cache-DrUMMgp6.d.ts} +4 -4
  7. package/dist/{ai-chat-B24UI962.js → ai-chat-CVLRWbtS.js} +4 -3
  8. package/dist/{ai-chat-7amvXjmQ.d.ts → ai-chat-DXkMy74O.d.ts} +5 -5
  9. package/dist/{ai-files-CSPNswUY.js → ai-files-CioLu5du.js} +5 -4
  10. package/dist/ai-message-DfaP8sis.js +14 -0
  11. package/dist/ai-sdk/agents/index.d.ts +4 -4
  12. package/dist/ai-sdk/batches/index.d.ts +4 -4
  13. package/dist/ai-sdk/batches/index.js +4 -4
  14. package/dist/ai-sdk/cache/index.d.ts +2 -2
  15. package/dist/ai-sdk/cache/index.js +2 -2
  16. package/dist/ai-sdk/chat/index.d.ts +1 -1
  17. package/dist/ai-sdk/chat/index.js +8 -20
  18. package/dist/ai-sdk/embeddings/index.d.ts +2 -2
  19. package/dist/ai-sdk/files/index.d.ts +3 -3
  20. package/dist/ai-sdk/files/index.js +4 -4
  21. package/dist/ai-sdk/index.d.ts +37 -9
  22. package/dist/ai-sdk/index.js +3 -2
  23. package/dist/ai-sdk/mcp/index.d.ts +43 -6
  24. package/dist/ai-sdk/mcp/index.js +36 -7
  25. package/dist/ai-sdk/memory/index.d.ts +2 -2
  26. package/dist/ai-sdk/memory/index.js +1 -1
  27. package/dist/ai-sdk/workflow/index.d.ts +6 -6
  28. package/dist/ai-sdk/workflow/index.js +13 -24
  29. package/dist/{ai-tasks-Cd1HMFvB.d.ts → ai-tasks-BRQ6iQLc.d.ts} +4 -4
  30. package/dist/{announcements-ZRGnScWI.d.ts → announcements-Bouy0Ski.d.ts} +3 -3
  31. package/dist/{announcements-CiyOs-xP.js → announcements-p_BFofiR.js} +1 -1
  32. package/dist/api-Dnf2q-Ht.d.ts +370 -0
  33. package/dist/api-DtHFC037.js +676 -0
  34. package/dist/{api-keys-CIjghYjV.d.ts → api-keys-C8BlF3L2.d.ts} +4 -4
  35. package/dist/arazzo/index.d.ts +255 -0
  36. package/dist/arazzo/index.js +622 -0
  37. package/dist/{assistant-DwdpDH1q.d.ts → assistant-DBlFDZsi.d.ts} +5 -5
  38. package/dist/astro/index.d.ts +7 -7
  39. package/dist/astro/index.js +7 -7
  40. package/dist/asyncapi/index.d.ts +103 -0
  41. package/dist/asyncapi/index.js +569 -0
  42. package/dist/authorizer-ayxUDXXP.js +126 -0
  43. package/dist/authorizer-eiOjo-3l.d.ts +73 -0
  44. package/dist/better-result/index.d.ts +118 -0
  45. package/dist/better-result/index.js +64 -0
  46. package/dist/{bind-l4kAvYSv.js → bind-ButSzrHe.js} +1 -1
  47. package/dist/{bind-Bu_ZNa3W.d.ts → bind-CQ5mPyX1.d.ts} +3 -3
  48. package/dist/{block-events-CrT8r7Fe.js → block-events-DTxU5tfr.js} +18 -3
  49. package/dist/{block-fields-GFkxl4dP.js → block-fields-CroxUuF2.js} +3 -3
  50. package/dist/{block-fields-SJ6fEf3o.d.ts → block-fields-D1pfrcIY.d.ts} +2 -2
  51. package/dist/{block-helpers-CKtbFcRv.d.ts → block-helpers-C0Xj8War.d.ts} +2 -2
  52. package/dist/{block-helpers-DWHxHsEQ.js → block-helpers-Cy3pB96I.js} +41 -9
  53. package/dist/{block-hooks-BmjMyXTa.js → block-hooks-BPNMIlLl.js} +3 -3
  54. package/dist/{block-hooks-plbZ0AbJ.d.ts → block-hooks-DaOVC7SC.d.ts} +3 -3
  55. package/dist/{block-transport-BLr-ZgLc.js → block-transport-bAKh-dGc.js} +28 -2
  56. package/dist/{block-transport-DvaoPuZ9.d.ts → block-transport-inD4fFq-.d.ts} +24 -2
  57. package/dist/blocks/agents/index.d.ts +3 -3
  58. package/dist/blocks/agents/index.js +5 -4
  59. package/dist/blocks/ai-cache/index.d.ts +3 -3
  60. package/dist/blocks/ai-cache/index.js +3 -3
  61. package/dist/blocks/ai-chat/index.d.ts +9 -4
  62. package/dist/blocks/ai-chat/index.js +5 -5
  63. package/dist/blocks/ai-chat/react/index.d.ts +3 -3
  64. package/dist/blocks/ai-chat/react/index.js +5 -5
  65. package/dist/blocks/ai-files/index.d.ts +3 -3
  66. package/dist/blocks/ai-files/index.js +3 -3
  67. package/dist/blocks/ai-providers/index.d.ts +3 -3
  68. package/dist/blocks/ai-providers/index.js +8 -7
  69. package/dist/blocks/ai-tasks/index.d.ts +3 -3
  70. package/dist/blocks/ai-tasks/index.js +7 -6
  71. package/dist/blocks/announcements/index.d.ts +3 -3
  72. package/dist/blocks/announcements/index.js +3 -3
  73. package/dist/blocks/announcements/react/index.d.ts +3 -3
  74. package/dist/blocks/announcements/react/index.js +3 -3
  75. package/dist/blocks/api-keys/index.d.ts +5 -5
  76. package/dist/blocks/api-keys/index.js +8 -8
  77. package/dist/blocks/attachments/index.d.ts +12 -8
  78. package/dist/blocks/attachments/index.js +22 -13
  79. package/dist/blocks/audit/index.d.ts +21 -10
  80. package/dist/blocks/audit/index.js +30 -11
  81. package/dist/blocks/billing/index.d.ts +11 -9
  82. package/dist/blocks/billing/index.js +15 -15
  83. package/dist/blocks/comments/index.d.ts +8 -8
  84. package/dist/blocks/comments/index.js +8 -7
  85. package/dist/blocks/connectors/index.d.ts +3 -3
  86. package/dist/blocks/connectors/index.js +7 -6
  87. package/dist/blocks/data-lifecycle/index.d.ts +12 -8
  88. package/dist/blocks/data-lifecycle/index.js +13 -8
  89. package/dist/blocks/entitlements/index.d.ts +3 -3
  90. package/dist/blocks/entitlements/index.js +1 -1
  91. package/dist/blocks/flags/index.d.ts +4 -4
  92. package/dist/blocks/flags/index.js +4 -3
  93. package/dist/blocks/inbox/index.d.ts +3 -3
  94. package/dist/blocks/inbox/index.js +4 -5
  95. package/dist/blocks/inbox/react/index.d.ts +1 -1
  96. package/dist/blocks/inbox/react/index.js +1 -1
  97. package/dist/blocks/inbox/react/server.d.ts +1 -1
  98. package/dist/blocks/index.d.ts +10 -10
  99. package/dist/blocks/index.js +6 -6
  100. package/dist/blocks/jobs/index.d.ts +48 -32
  101. package/dist/blocks/jobs/index.js +44 -287
  102. package/dist/blocks/knowledge/index.d.ts +3 -3
  103. package/dist/blocks/knowledge/index.js +3 -3
  104. package/dist/blocks/memory/index.d.ts +4 -4
  105. package/dist/blocks/memory/index.js +7 -6
  106. package/dist/blocks/notifications/index.d.ts +11 -9
  107. package/dist/blocks/notifications/index.js +20 -25
  108. package/dist/blocks/notifications/react/index.d.ts +1 -1
  109. package/dist/blocks/notifications/react/index.js +1 -1
  110. package/dist/blocks/onboarding/index.d.ts +3 -3
  111. package/dist/blocks/onboarding/index.js +3 -3
  112. package/dist/blocks/onboarding/react/index.d.ts +2 -2
  113. package/dist/blocks/onboarding/react/index.js +2 -2
  114. package/dist/blocks/onboarding/react/server.d.ts +1 -1
  115. package/dist/blocks/organizations/index.d.ts +9 -9
  116. package/dist/blocks/organizations/index.js +11 -18
  117. package/dist/blocks/outbox/index.d.ts +2 -2
  118. package/dist/blocks/outbox/index.js +16 -25
  119. package/dist/blocks/profiles/index.d.ts +7 -7
  120. package/dist/blocks/profiles/index.js +7 -6
  121. package/dist/blocks/push/index.d.ts +4 -4
  122. package/dist/blocks/push/index.js +3 -3
  123. package/dist/blocks/settings/index.d.ts +4 -4
  124. package/dist/blocks/settings/index.js +7 -6
  125. package/dist/blocks/sso/index.d.ts +5 -5
  126. package/dist/blocks/sso/index.js +6 -5
  127. package/dist/blocks/usage/index.d.ts +3 -3
  128. package/dist/blocks/usage/index.js +5 -4
  129. package/dist/blocks/waitlist/index.d.ts +5 -5
  130. package/dist/blocks/waitlist/index.js +5 -4
  131. package/dist/blocks/webhooks/index.d.ts +22 -13
  132. package/dist/blocks/webhooks/index.js +41 -70
  133. package/dist/blocks/workflow-builder/index.d.ts +4 -4
  134. package/dist/blocks/workflow-builder/index.js +3 -3
  135. package/dist/blocks/workflow-builder/react/index.d.ts +4 -4
  136. package/dist/blocks/workflow-builder/react/index.js +4 -4
  137. package/dist/blocks/workflows/index.d.ts +3 -3
  138. package/dist/blocks/workflows/index.js +3 -3
  139. package/dist/blocks/workflows/react/index.d.ts +3 -3
  140. package/dist/blocks/workflows/react/index.js +3 -3
  141. package/dist/build-hPwk3_X5.js +1296 -0
  142. package/dist/{build-BXIzE6Xv.js → build-meLuYGri.js} +3 -3
  143. package/dist/casing-DdFWEtqr.js +16 -0
  144. package/dist/chat-sdk/index.d.ts +6 -6
  145. package/dist/chat-sdk/index.js +5 -4
  146. package/dist/chat-sdk/react/index.d.ts +1 -1
  147. package/dist/chat-sdk/react/server.d.ts +1 -1
  148. package/dist/{claims-DDBQT6BK.js → claims-0KZVT804.js} +1 -1
  149. package/dist/cli/add-1dc5wM5k.js +69 -0
  150. package/dist/cli/arazzo-1.0-BalU61-_.js +532 -0
  151. package/dist/cli/arazzo-1.1-Bnfasbun.js +705 -0
  152. package/dist/cli/asyncapi-3.0.0-C78dBjR6.js +5593 -0
  153. package/dist/cli/asyncapi-3.1.0-g-7aIfV1.js +5833 -0
  154. package/dist/cli/bin.js +4 -4
  155. package/dist/cli/cache-CIfA-jBV.js +483 -0
  156. package/dist/cli/{codemod-Bl_tQpHn.js → codemod-JVlTQQG2.js} +4 -4
  157. package/dist/cli/{command-CK14fP91.js → command-DV-StG1x.js} +5 -0
  158. package/dist/cli/{config-DU7EJImt.js → config-C2Pz2_4A.js} +7 -4
  159. package/dist/cli/config-DpbF8ML3.js +2 -0
  160. package/dist/cli/{config-NiPxAmUF.js → config-M-WzL9O2.js} +116 -18
  161. package/dist/cli/{db-DOBPt660.js → db-BkcfOlyB.js} +2 -1
  162. package/dist/cli/{diff-SjkO5rNr.js → diff-D1jGdb3Y.js} +1 -1
  163. package/dist/cli/{doctor-CVdnLahA.js → doctor-VcDtuusV.js} +317 -222
  164. package/dist/cli/emit-BHHfwUoT.js +1136 -0
  165. package/dist/cli/{env-qXgyMV6r.js → env-B--LMhEe.js} +13 -10
  166. package/dist/cli/env-vzBG90Um.js +2 -0
  167. package/dist/cli/{env-CJLbUVPB.js → env-zHJtMIBo.js} +11 -3
  168. package/dist/cli/{errors-4v4DTtda.js → errors-CqTLXe2M.js} +18 -2
  169. package/dist/cli/{fastest-levenshtein-CFN-lNDv.js → fastest-levenshtein-CF7BCi9l.js} +1 -1
  170. package/dist/cli/files-BVupXPIC.js +34 -0
  171. package/dist/cli/gen-DmVj3u4L.js +322 -0
  172. package/dist/cli/{init-Cr6arjXU.js → glue-C1UE8gQN.js} +16 -313
  173. package/dist/cli/index.d.ts +59 -31
  174. package/dist/cli/index.js +8 -6
  175. package/dist/cli/init-CVZ3s5kl.js +258 -0
  176. package/dist/cli/{introspect-Bzuv1HHj.js → introspect-BToGNfm-.js} +14 -14
  177. package/dist/cli/{io-DI1mTdSA.js → io-CFzJVJ68.js} +5 -1
  178. package/dist/cli/{keys-BGCnnb-s.js → keys-CJhtIxJ3.js} +4 -4
  179. package/dist/cli/metadata-DSRKCaf-.js +133 -0
  180. package/dist/cli/names-D6LrQnOv.js +6 -0
  181. package/dist/cli/openapi-3.0-t7xwkuRH.js +928 -0
  182. package/dist/cli/openapi-3.1-CVze7tBS.js +870 -0
  183. package/dist/cli/openapi-3.2-CFoMYM01.js +1031 -0
  184. package/dist/cli/{openapi-BlRJ18s3.js → openapi-DWa4Ir1q.js} +8 -14
  185. package/dist/cli/prompts-BdeVmVpg.js +2 -0
  186. package/dist/cli/{prompts-BNcNfAve.js → prompts-DZJB1nTw.js} +6 -6
  187. package/dist/cli/{rolldown-runtime-BPOCksWG.js → rolldown-runtime-CMFfr-1z.js} +3 -1
  188. package/dist/cli/run-DA6_lg3w.js +389 -0
  189. package/dist/cli/run-DuN1MX-7.js +705 -0
  190. package/dist/cli/scaffold-5DenHSxF.js +292 -0
  191. package/dist/cli/scaffold-CwQUMFRb.js +2 -0
  192. package/dist/cli/{seed-DQzqT7LP.js → seed-CzBmrGON.js} +14 -18
  193. package/dist/cli/seed-DOmit9Hq.js +2 -0
  194. package/dist/cli/{skills-QQs4q68J.js → skills-CnO8QFS7.js} +7 -6
  195. package/dist/cli/snapshot-Bstk6ulj.js +287 -0
  196. package/dist/cli/source-args-BYfq6imV.js +31 -0
  197. package/dist/cli/spec-CzosTYm3.js +7438 -0
  198. package/dist/cli/spec-T_0Bcyw7.js +2 -0
  199. package/dist/cli/{sql-B8csQPyn.js → sql-BKOOQAX3.js} +240 -218
  200. package/dist/cli/sql-DJrNDxQm.js +3 -0
  201. package/dist/cli/{style-C0NFdJRL.js → style-CB3vxgh0.js} +3 -3
  202. package/dist/cli/typegen-Demi5-VQ.js +486 -0
  203. package/dist/cli/validate-Ca13ktgz.js +7522 -0
  204. package/dist/cli/{version-Bp4fyubI.js → version-B4k0aUxS.js} +1 -1
  205. package/dist/client/index.d.ts +4 -3
  206. package/dist/client/index.js +60 -3
  207. package/dist/client/native/index.d.ts +3 -3
  208. package/dist/client/native/index.js +3 -3
  209. package/dist/{client-CI0Z2zWX.js → client-wxeTQFtB.js} +36 -3
  210. package/dist/{clone-BfgmLU90.js → clone-dN9XOL1K.js} +9 -1
  211. package/dist/{cloud-event-BnmD5fb9.js → cloud-event-CTh9Sr5w.js} +16 -2
  212. package/dist/{cloud-event-C1JCc1bH.d.ts → cloud-event-Dune0KI7.d.ts} +16 -2
  213. package/dist/{compiler-DY_wkj4R.d.ts → compiler-V1qvcST1.d.ts} +2 -2
  214. package/dist/{compose-BIBNgAnq.js → compose-BHej1acw.js} +3 -3
  215. package/dist/{composite-CpHLEo-G.d.ts → composite-CWAJTRBF.d.ts} +6 -6
  216. package/dist/{composite-C6I8ZyaF.js → composite-zTe0TCqj.js} +4 -4
  217. package/dist/config/index.d.ts +3 -4
  218. package/dist/config/index.js +4 -4
  219. package/dist/config-DlKvtt95.js +540 -0
  220. package/dist/{connector-Dv8kK0MO.d.ts → connector-CySQEFcW.d.ts} +1 -1
  221. package/dist/{connectors-CrpknU0P.d.ts → connectors--PGhmiu0.d.ts} +4 -4
  222. package/dist/{context-BCrWe7ow.d.ts → context-DsV2SueA.d.ts} +11 -2
  223. package/dist/context-nPDvQ-Zl.js +41 -0
  224. package/dist/credentials/index.d.ts +5 -5
  225. package/dist/credentials/index.js +9 -17
  226. package/dist/{cron-DVPUg5k3.js → cron-D25Ukpxy.js} +1 -1
  227. package/dist/cron-route-D2AN9SA1.js +24 -0
  228. package/dist/{csv-DCK-PT1n.js → csv-BZnHA1Py.js} +1 -1
  229. package/dist/{define-B-TVjC25.js → define-CFQcvM20.js} +263 -229
  230. package/dist/{define-CJJkkr1-.d.ts → define-DmcfoNXB.d.ts} +136 -111
  231. package/dist/edge/index.d.ts +66 -13
  232. package/dist/edge/index.js +65 -39
  233. package/dist/elysia/index.d.ts +1 -1
  234. package/dist/elysia/index.js +1 -1
  235. package/dist/{embeddings-C9KGQnXt.d.ts → embeddings-B4vhS-dY.d.ts} +1 -1
  236. package/dist/{ended-XjeGQ5XW.d.ts → ended-CcvcRqEd.d.ts} +7 -1
  237. package/dist/{entitlements-DRl6ZJUm.js → entitlements-BYFf0Cdh.js} +2 -2
  238. package/dist/{entitlements-BKSVGtrG.d.ts → entitlements-Ds8qoYZ-.d.ts} +1 -1
  239. package/dist/env/index.d.ts +1 -1
  240. package/dist/env/index.js +9 -2
  241. package/dist/{errors-B11Duf73.js → errors-2M6CKMP4.js} +5 -0
  242. package/dist/{errors-DLjJHPLK.js → errors-rZ7VIqp6.js} +2 -2
  243. package/dist/{escape-like-ImTK0ps_.d.ts → escape-like-DDWoHzh_.d.ts} +1 -1
  244. package/dist/eve/index.d.ts +5 -5
  245. package/dist/eve/index.js +5 -11
  246. package/dist/{eve-gT2Fpjut.d.ts → eve-CoAIcCkq.d.ts} +2 -2
  247. package/dist/events/index.d.ts +4 -4
  248. package/dist/events/index.js +7 -2
  249. package/dist/{events-DWDhx77e.d.ts → events-D1BOCll8.d.ts} +6 -4
  250. package/dist/{executor-Bq28caBo.js → executor-B-fD7DLt.js} +52 -35
  251. package/dist/executor-B_Hfobee.js +162 -0
  252. package/dist/{executor-BVBFM86G.d.ts → executor-CpxLPCiz.d.ts} +1 -1
  253. package/dist/{executor-CaWigTyR.d.ts → executor-DfEtf0cq.d.ts} +43 -27
  254. package/dist/{executor-DvsBbNBS.d.ts → executor-Drzu3dBK.d.ts} +2 -2
  255. package/dist/{executor-DbvExCFC.d.ts → executor-ngjKL_Pe.d.ts} +2 -2
  256. package/dist/expo/index.d.ts +4 -4
  257. package/dist/expo/index.js +6 -6
  258. package/dist/expo-sqlite/index.d.ts +3 -3
  259. package/dist/expo-sqlite/index.js +2 -2
  260. package/dist/extra-CGQ2_q3S.js +10 -0
  261. package/dist/{field-errors-BFOa7WhB.d.ts → field-errors-DIe9XYWL.d.ts} +17 -7
  262. package/dist/{field-errors-VF1yb8yI.js → field-errors-DZVRWRkf.js} +1 -1
  263. package/dist/format-DTXdu8ya.d.ts +70 -0
  264. package/dist/{framework-KIPvRQJU.d.ts → framework-148t1m3C.d.ts} +5 -5
  265. package/dist/{framework-BDCpJI7o.js → framework-3ey3QmrO.js} +2 -2
  266. package/dist/function-contract-BcZenXMO.d.ts +8 -0
  267. package/dist/{gateway-C1ssRHnf.d.ts → gateway-S9vicgeo.d.ts} +4 -4
  268. package/dist/{guards-BnzDvFrz.js → guards-6F2bCtQR.js} +3 -1
  269. package/dist/h3/index.d.ts +1 -1
  270. package/dist/h3/index.js +1 -1
  271. package/dist/h3/v1/index.d.ts +1 -1
  272. package/dist/h3/v1/index.js +1 -1
  273. package/dist/{message-CaKlqdsX.js → history-GH1HJWQV.js} +65 -13
  274. package/dist/hmac-DCud0myt.js +22 -0
  275. package/dist/hono/index.d.ts +19 -14
  276. package/dist/hono/index.js +21 -16
  277. package/dist/{hooks-1BTCJsiw.js → hooks-Bhza9Hnr.js} +68 -100
  278. package/dist/http-sbmBe5d2.js +25 -0
  279. package/dist/{inbox-i7E1ppo6.d.ts → inbox-Cd1KCEBL.d.ts} +3 -3
  280. package/dist/index-B72Rvv_j.d.ts +89 -0
  281. package/dist/{index-DunNRdnT.d.ts → index-BCHR4Ucl.d.ts} +4 -4
  282. package/dist/{index-CLg87OBW.d.ts → index-BUK7jUzR.d.ts} +6 -0
  283. package/dist/{config-D44Y7PAv.d.ts → index-Ba1SI-uO.d.ts} +625 -278
  284. package/dist/{index-D48jVS-N.d.ts → index-BfxOaOYs.d.ts} +4 -5
  285. package/dist/{index-ClmjK_o-.d.ts → index-CicIyqOe.d.ts} +4 -4
  286. package/dist/{index-ACiNPN4Z.d.ts → index-D69h4QqD.d.ts} +1 -1
  287. package/dist/index-DG2SUXDQ.d.ts +58 -0
  288. package/dist/{index-Bepi-Hgg.d.ts → index-DoNVLHOU.d.ts} +3 -3
  289. package/dist/{index-CdZKQF98.d.ts → index-R274XFlg.d.ts} +1 -1
  290. package/dist/{index-CewnYSpv.d.ts → index-_4R4b01v.d.ts} +8 -18
  291. package/dist/index.d.ts +89 -14
  292. package/dist/index.js +12 -11
  293. package/dist/{json-schema-CvER4kgT.js → json-schema-CT4tvrkA.js} +148 -102
  294. package/dist/{kit-Fm-Fv7q5.js → kit-BhHr9T9v.js} +36 -10
  295. package/dist/{kit-CkNzZUqv.d.ts → kit-W99LCPBS.d.ts} +13 -7
  296. package/dist/{knowledge-BXBbT0Tr.d.ts → knowledge-CAReFpRA.d.ts} +5 -5
  297. package/dist/{knowledge-CDmiMLew.js → knowledge-DnHtb64A.js} +4 -3
  298. package/dist/list/index.d.ts +1 -2
  299. package/dist/{list-query-BRt-zDF7.d.ts → list-query-D3JQeEL7.d.ts} +4 -4
  300. package/dist/mcp/index.d.ts +49 -19
  301. package/dist/mcp/index.js +158 -94
  302. package/dist/mcp/sdk/index.d.ts +38 -6
  303. package/dist/mcp/sdk/index.js +88 -6
  304. package/dist/{message-CfDoR0Y6.d.ts → message-B0fNJgq8.d.ts} +1 -2
  305. package/dist/{messages-B9-5887d.js → messages-CJoHHCWT.js} +4 -205
  306. package/dist/{mfa-Daak8ukG.d.ts → mfa-SZc9YYyb.d.ts} +1 -1
  307. package/dist/model-OgBxvk3N.d.ts +300 -0
  308. package/dist/model-messages-Cxw_Up4f.js +206 -0
  309. package/dist/model-messages-D4GZllOD.d.ts +34 -0
  310. package/dist/modules-DksjZyMW.js +77 -0
  311. package/dist/nestjs/index.d.ts +2 -2
  312. package/dist/nestjs/index.js +1 -1
  313. package/dist/next/index.d.ts +105 -55
  314. package/dist/next/index.js +204 -170
  315. package/dist/node/index.d.ts +2 -2
  316. package/dist/node/index.js +1 -1
  317. package/dist/{onboarding-BZGHYJks.d.ts → onboarding-LZvlK1P4.d.ts} +3 -3
  318. package/dist/openapi/index.d.ts +4 -2
  319. package/dist/openapi/index.js +21 -343
  320. package/dist/optional-peer-D1pi-skw.js +24 -0
  321. package/dist/orpc/index.d.ts +8 -8
  322. package/dist/orpc/index.js +21 -20
  323. package/dist/otel/index.d.ts +3 -3
  324. package/dist/otel/index.js +7 -7
  325. package/dist/{outbox-Cq7pPYvl.d.ts → outbox-DZfAvfzC.d.ts} +6 -6
  326. package/dist/overlay/index.d.ts +233 -0
  327. package/dist/overlay/index.js +554 -0
  328. package/dist/{plugin-FGY8UhYm.d.ts → plugin-CPK2HsbY.d.ts} +42 -3
  329. package/dist/plugins/actor/index.d.ts +1 -1
  330. package/dist/plugins/actor/index.js +1 -1
  331. package/dist/plugins/rules/index.d.ts +2 -2
  332. package/dist/plugins/rules/index.js +1 -1
  333. package/dist/plugins/soft-delete/index.d.ts +3 -3
  334. package/dist/plugins/soft-delete/index.js +9 -2
  335. package/dist/plugins/tenant/index.d.ts +7 -2
  336. package/dist/plugins/tenant/index.js +16 -2
  337. package/dist/plugins/timestamps/index.d.ts +1 -1
  338. package/dist/plugins/timestamps/index.js +8 -1
  339. package/dist/plugins/validation/index.d.ts +2 -2
  340. package/dist/plugins/validation/index.js +4 -4
  341. package/dist/{pool-CAu2nLrL.d.ts → pool-gkhbGp2E.d.ts} +2 -2
  342. package/dist/postgres/index.d.ts +4 -4
  343. package/dist/postgres/index.js +2 -1
  344. package/dist/{postgrest-BLVdEnfe.d.ts → postgrest-BUqLag2Q.d.ts} +1 -1
  345. package/dist/powersync/index.d.ts +5 -5
  346. package/dist/powersync/index.js +4 -4
  347. package/dist/powersync/react/index.d.ts +3 -3
  348. package/dist/powersync/react/index.js +1 -1
  349. package/dist/{problem-BreOl8b8.js → problem-CJvFeGu7.js} +13 -2
  350. package/dist/{problem-DD8LZFjc.d.ts → problem-TvGdxO1t.d.ts} +7 -1
  351. package/dist/{provider-Di3sRutW.js → provider-DxRTex4x.js} +1 -1
  352. package/dist/{provider-D-UEMvSD.d.ts → provider-ib9wzUDz.d.ts} +2 -2
  353. package/dist/query/index.d.ts +2 -2
  354. package/dist/query/index.js +1 -1
  355. package/dist/{query-BQ-NaZon.js → query-Ctz-qKRx.js} +1 -1
  356. package/dist/rate-limit-Bw7HgmFj.js +260 -0
  357. package/dist/react/index.d.ts +9 -9
  358. package/dist/react/index.js +4 -4
  359. package/dist/react/native/index.d.ts +1 -1
  360. package/dist/react/native/index.js +2 -2
  361. package/dist/react/server.d.ts +10 -10
  362. package/dist/react/server.js +3 -3
  363. package/dist/react/session.d.ts +2 -2
  364. package/dist/{react-F80mF4-t.js → react-Bmpo59D7.js} +4 -4
  365. package/dist/react-router/index.d.ts +6 -6
  366. package/dist/react-router/index.js +9 -11
  367. package/dist/{read-set-DuKWC9Mj.js → read-set-DAeWhqPf.js} +2 -2
  368. package/dist/realtime/index.d.ts +3 -3
  369. package/dist/realtime/index.js +30 -36
  370. package/dist/{refusal-DhSU2Vvr.d.ts → refusal-CFI8-5nf.d.ts} +4 -1
  371. package/dist/{refusal-CeJWxmkH.js → refusal-K78A1Vmq.js} +5 -5
  372. package/dist/{resolve-tJZY3FEa.js → resolve-Bvgf3ut_.js} +131 -42
  373. package/dist/{resolve-ChTYeTEl.d.ts → resolve-MxXVWyhd.d.ts} +12 -8
  374. package/dist/resource--vTrBe6I.d.ts +103 -0
  375. package/dist/resource-BqNqlYg9.js +91 -0
  376. package/dist/resource-DA811u_4.js +463 -0
  377. package/dist/resource-execute-rbKvuCsY.d.ts +12 -0
  378. package/dist/resource-schemas-BJ518zFH.js +79 -0
  379. package/dist/resource-types-CqB6GWGT.d.ts +141 -0
  380. package/dist/resources-BVYPnuLo.js +56 -0
  381. package/dist/resources-DZSk8tXJ.d.ts +7 -0
  382. package/dist/{respond-DprrgyXX.d.ts → respond-Cw-zanCm.d.ts} +3 -3
  383. package/dist/{respond-BUgsXNgk.js → respond-volM3ByS.js} +4 -4
  384. package/dist/{result-D7y2fT87.d.ts → result-BchM1XZn.d.ts} +7 -1
  385. package/dist/{result-DzUbqgdW.js → result-DEqHCMKP.js} +1 -1
  386. package/dist/server/index.d.ts +81 -19
  387. package/dist/server/index.js +16 -14
  388. package/dist/{server-ZSJ9rCcV.d.ts → server-types-CrAmZ8jv.d.ts} +110 -36
  389. package/dist/{shared-CxWmcyqB.js → shared-DHjhfRdV.js} +2 -15
  390. package/dist/shared-DTneRoxi.js +14 -0
  391. package/dist/{sign-in-Cb1mwv6X.js → sign-in-D_yoP0PF.js} +1 -1
  392. package/dist/signatures-BRS_p3WD.d.ts +34 -0
  393. package/dist/signatures-DsWv_bYR.js +103 -0
  394. package/dist/{signers-4y62_VIi.d.ts → signers-cpdmZYrv.d.ts} +1 -1
  395. package/dist/solid/index.d.ts +9 -18
  396. package/dist/solid/index.js +2 -2
  397. package/dist/solid-start/index.d.ts +6 -6
  398. package/dist/solid-start/index.js +7 -7
  399. package/dist/spec/index.d.ts +134 -0
  400. package/dist/spec/index.js +353 -0
  401. package/dist/{spec-pins-C89s6DyW.d.ts → spec-pins-CwzDY25w.d.ts} +18 -1
  402. package/dist/{spec-pins--j6mTok4.js → spec-pins-DsLfUH7l.js} +14 -1
  403. package/dist/sql/index.d.ts +145 -119
  404. package/dist/sql/index.js +19921 -19798
  405. package/dist/{executor-De1Of3m3.js → sql-CwuqaQ4Y.js} +34 -169
  406. package/dist/ssr/index.d.ts +4 -4
  407. package/dist/ssr/index.js +3 -2
  408. package/dist/{standard-BK0cqJ7y.js → standard-DkzZUneg.js} +2 -2
  409. package/dist/{standard-CYsm7xpT.d.ts → standard-lmkbOtPB.d.ts} +1 -1
  410. package/dist/{stats-D_AD9GGu.js → stats-DhU78Ctf.js} +8 -4
  411. package/dist/storage/index.d.ts +4 -6
  412. package/dist/storage/index.js +16 -17
  413. package/dist/{bucket-Cl46E3jS.d.ts → storage-api-CB7Zl1Ci.d.ts} +243 -8
  414. package/dist/{store-CGjgWu5n.d.ts → store-DsIYEBnv.d.ts} +1 -1
  415. package/dist/streams/index.d.ts +4 -4
  416. package/dist/streams/index.js +5 -4
  417. package/dist/streams/redis/index.d.ts +1 -1
  418. package/dist/streams/redis/index.js +5 -11
  419. package/dist/{stripe-BMj5LtsT.js → stripe-v-Y2lmop.js} +2 -8
  420. package/dist/supabase-mcp-C6tMoEjn.d.ts +43 -0
  421. package/dist/supabase-mcp-Cpn5A4i-.js +28 -0
  422. package/dist/{support-DWwzVf-O.js → support-D1pFFouF.js} +71 -35
  423. package/dist/{support-view-CVOJJLpc.d.ts → support-view-scqMgtOo.d.ts} +1 -1
  424. package/dist/svelte/index.d.ts +8 -17
  425. package/dist/svelte/index.js +2 -2
  426. package/dist/sveltekit/index.d.ts +7 -7
  427. package/dist/sveltekit/index.js +10 -12
  428. package/dist/table-schema-htM_SbCW.js +286 -0
  429. package/dist/{tags-Cx-LnIQP.d.ts → tags-BO9LjtWG.d.ts} +1 -1
  430. package/dist/tanstack-db/index.d.ts +2 -2
  431. package/dist/tanstack-db/supabase/index.d.ts +52 -0
  432. package/dist/tanstack-db/supabase/index.js +97 -0
  433. package/dist/tanstack-start/index.d.ts +6 -6
  434. package/dist/tanstack-start/index.js +8 -12
  435. package/dist/{tee-CoEppCER.js → tee-Cb98Vn3b.js} +1 -1
  436. package/dist/{template--jDkl3UP.js → template-lPVh-EwW.js} +11 -2
  437. package/dist/{temporal-required-BVvW7SYJ.js → temporal-required-BtynQtM-.js} +1 -1
  438. package/dist/{tenant-BxNFk9bx.d.ts → tenant-CM368LXb.d.ts} +2 -2
  439. package/dist/testing/index.d.ts +249 -118
  440. package/dist/testing/index.js +1418 -870
  441. package/dist/ttl-cache-XzZ-Q2eX.js +29 -0
  442. package/dist/types-BRTSpOIh.js +5 -0
  443. package/dist/{types-C05bxW-W.d.ts → types-Bb_4-vdW.d.ts} +4 -4
  444. package/dist/{types-AYOvcyep.d.ts → types-CD6yCDx3.d.ts} +5 -5
  445. package/dist/{types-6rocye-y.d.ts → types-CbOUc5IH.d.ts} +4 -4
  446. package/dist/{types-6BdhkmZe.d.ts → types-CoE5Zwy4.d.ts} +8 -0
  447. package/dist/{types-DGs5L0VN.d.ts → types-DSJ3-m9X.d.ts} +4 -4
  448. package/dist/{queue-CLJurhdy.d.ts → types-D__3EN0q.d.ts} +17 -19
  449. package/dist/{usage-CrvrL2vw.d.ts → usage-D0NNUFfr.d.ts} +3 -3
  450. package/dist/{vault-wlbBhqm7.d.ts → vault-BpmApxxW.d.ts} +3 -3
  451. package/dist/vercel-connect/index.d.ts +1 -1
  452. package/dist/vercel-connect/index.js +5 -11
  453. package/dist/{verify-DPuWngD4.d.ts → verify-C91Wyp40.d.ts} +5 -35
  454. package/dist/verify-CJbYfADA.js +124 -0
  455. package/dist/{version--AolB5ej.js → version-Cq8gfDbd.js} +1 -1
  456. package/dist/{view-BeFcRV6G.js → view-D5ryq3gz.js} +1 -1
  457. package/dist/{view-CF7ihnkg.d.ts → view-TpXEUcVN.d.ts} +3 -3
  458. package/dist/vue/index.d.ts +9 -18
  459. package/dist/vue/index.js +2 -2
  460. package/dist/{watch-Xuxrvxq-.js → watch-BcvtAHOd.js} +2 -2
  461. package/dist/{watch-DI34G86j.d.ts → watch-COzRWvVN.d.ts} +23 -9
  462. package/dist/{wire-DtZQ4sdE.js → wire-Df3EWkG8.js} +2 -2
  463. package/dist/{workflow-builder-DJ1oHn8g.js → workflow-builder-CHqHpAVt.js} +6 -5
  464. package/dist/workflow-sdk/builder/index.d.ts +3 -3
  465. package/dist/workflow-sdk/index.d.ts +1 -1
  466. package/dist/workflow-sdk/world/index.d.ts +23 -17
  467. package/dist/workflow-sdk/world/index.js +59 -76
  468. package/dist/{workflow-sdk-B3yLD6Br.d.ts → workflow-sdk-DTZyrvNo.d.ts} +3 -3
  469. package/dist/{workflow-sdk-world-D-prir6O.js → workflow-sdk-world-CMV1ahFW.js} +528 -526
  470. package/dist/{workflows-B0j9WKPu.d.ts → workflows-CcpCBfIe.d.ts} +3 -3
  471. package/dist/{workflows-wu1WWBn7.js → workflows-IvxzG2cl.js} +5 -4
  472. package/package.json +54 -3
  473. package/schemas/config-v1.json +243 -0
  474. package/schemas/doctor-report-v1.json +4 -1
  475. package/schemas/spec-manifest-v1.json +95 -0
  476. package/skills/better-supabase/SKILL.md +8 -4
  477. package/skills/better-supabase/references/troubleshooting.md +20 -20
  478. package/skills/better-supabase-api/references/expo.md +6 -3
  479. package/skills/better-supabase-api/references/powersync.md +2 -1
  480. package/skills/better-supabase-auth/SKILL.md +8 -4
  481. package/dist/cli/config-B9HkL8nF.js +0 -2
  482. package/dist/cli/gen-AIf4MREI.js +0 -3100
  483. package/dist/cli/gen-DSrahEdX.js +0 -2
  484. package/dist/cli/prompts-BTYQGunc.js +0 -2
  485. package/dist/cli/run-BO052mki.js +0 -361
  486. package/dist/config-Dn7F5k9A.js +0 -202
  487. package/dist/history-BZih6SFP.js +0 -64
  488. package/dist/index-BkWT_uBO.d.ts +0 -51
  489. package/dist/index-Bxih3l0u.d.ts +0 -81
  490. package/dist/index-CaxrMD1A.d.ts +0 -1
  491. package/dist/index-DQh4VCc9.d.ts +0 -98
  492. package/dist/keys-4MnkWTTh.js +0 -35
  493. package/dist/messages-DYP0sUTe.d.ts +0 -59
  494. package/dist/modules-dxrVK3ID.js +0 -228
  495. package/dist/path-Cv__-OTj.d.ts +0 -17
  496. package/dist/resource-BsvzCMlX.js +0 -212
  497. package/dist/resource-IPS5xi9q.d.ts +0 -74
  498. package/dist/template-CwO38Do8.d.ts +0 -6
  499. package/dist/verify-DL41hbvY.js +0 -232
  500. package/dist/cli/{read-sets-CETOSFx4.js → read-sets-DjJrm8Wj.js} +1 -1
@@ -1,20 +1,22 @@
1
1
  import { t as DbException } from "../db-exception-CnWcYPxL.js";
2
- import { n as dbError } from "../errors-B11Duf73.js";
2
+ import { n as dbError } from "../errors-2M6CKMP4.js";
3
3
  import { d as temporalText } from "../temporal-BJGitm-h.js";
4
- import { t as temporal } from "../temporal-required-BVvW7SYJ.js";
5
- import { a as ok } from "../result-DzUbqgdW.js";
6
- import { t as BetterSupabase } from "../define-B-TVjC25.js";
4
+ import { t as temporal } from "../temporal-required-BtynQtM-.js";
5
+ import { a as ok } from "../result-DEqHCMKP.js";
6
+ import { t as BetterSupabase } from "../define-CFQcvM20.js";
7
+ import { t as isList } from "../guards-6F2bCtQR.js";
7
8
  import { a as toBase64Url, r as fromBase64, t as base64ToText } from "../base64-D2gsioCY.js";
8
- import { t as isList } from "../guards-BnzDvFrz.js";
9
- import { a as resolveConfig } from "../modules-dxrVK3ID.js";
9
+ import { o as resolveConfig } from "../config-DlKvtt95.js";
10
10
  import { ENV_VARIABLES } from "../env/index.js";
11
- import { n as postgresExecutor } from "../executor-De1Of3m3.js";
11
+ import { n as postgresExecutor } from "../executor-B_Hfobee.js";
12
12
  import "../replicas-CwrYp5Zj.js";
13
- import { a as tenantCredentialRef } from "../provider-Di3sRutW.js";
13
+ import { a as tenantCredentialRef } from "../provider-DxRTex4x.js";
14
+ import { i as requestOf, n as checkPermission, r as defineAuthorizer } from "../authorizer-ayxUDXXP.js";
14
15
  import { n as parseDbStats } from "../timing-CyTQPubY.js";
15
- import { t as rawError } from "../block-transport-BLr-ZgLc.js";
16
- import { a as isScopeName, c as templateFunctions, l as templatePlaceholders, n as TEMPLATE_PLACEHOLDERS, s as scopeProblems, u as templateProblems } from "../access-sql-B0dF-SoF.js";
16
+ import { n as rawError } from "../block-transport-bAKh-dGc.js";
17
+ import { a as isScopeName, c as templateFunctions, l as templatePlaceholders, n as TEMPLATE_PLACEHOLDERS, s as scopeProblems, u as templateProblems } from "../access-sql-CrSbK1q-.js";
17
18
  import { toCloudEvents } from "../events/index.js";
19
+ import { t as buildApiModel } from "../build-hPwk3_X5.js";
18
20
  import { timestamps } from "../plugins/timestamps/index.js";
19
21
  import { softDelete } from "../plugins/soft-delete/index.js";
20
22
  import { tenant } from "../plugins/tenant/index.js";
@@ -230,7 +232,10 @@ async function asUser(betterSupabase, claims, stack = {}) {
230
232
  },
231
233
  claims: full
232
234
  };
233
- const supabase = createClient(url, publishableKey, { accessToken: () => Promise.resolve(token) });
235
+ const supabase = createClient(url, publishableKey, {
236
+ accessToken: () => Promise.resolve(token),
237
+ ...stack.fetch ? { global: { fetch: stack.fetch } } : {}
238
+ });
234
239
  return {
235
240
  token,
236
241
  claims: full,
@@ -240,6 +245,141 @@ async function asUser(betterSupabase, claims, stack = {}) {
240
245
  };
241
246
  }
242
247
  //#endregion
248
+ //#region src/lite/runtime.ts
249
+ const LITE_INSTALL = "Supabase Lite is an optional peer: install it with `pnpm add -D @supabase/lite`";
250
+ /**
251
+ * `@supabase/lite` is an optional peer: variable specifiers keep bundlers
252
+ * from resolving it, and a missing install returns `undefined` so callers
253
+ * can say how to add it.
254
+ */
255
+ async function loadLite() {
256
+ const root = "@supabase/lite";
257
+ const pglite = "@supabase/lite/pglite";
258
+ const sqlite = "@supabase/lite/sqlite";
259
+ try {
260
+ const [{ App }, pg, sq] = await Promise.all([
261
+ import(root),
262
+ import(pglite),
263
+ import(sqlite)
264
+ ]);
265
+ return {
266
+ App,
267
+ createPgliteConnection: () => pg.createPgliteConnection(),
268
+ createSqliteConnection: (url) => sq.createConnection({ url })
269
+ };
270
+ } catch {
271
+ return;
272
+ }
273
+ }
274
+ /** Applies migrations, then declarative schemas, with Lite's migrator. */
275
+ async function migrateLite(connection, sql) {
276
+ const ddl = sql.filter((part) => part.trim() !== "").join("\n;\n");
277
+ if (ddl !== "") await connection.createMigrator(ddl).migrate();
278
+ }
279
+ /** Runs seed SQL: as is on PGlite, translated to SQLite on `sqlite-postgres`. */
280
+ async function seedLite(target, sql) {
281
+ if (sql.trim() === "") return;
282
+ if (target.driver === "pglite") {
283
+ await target.connection.driver.exec(sql);
284
+ return;
285
+ }
286
+ const { connection } = target;
287
+ const translated = await connection.translateDdl(sql);
288
+ const statements = typeof translated === "string" ? translated : typeof translated === "object" && translated !== null && "ddl" in translated && typeof translated.ddl === "string" ? translated.ddl : "";
289
+ if (statements !== "") await connection.transaction([statements]);
290
+ }
291
+ //#endregion
292
+ //#region src/testing/lite.ts
293
+ const LITE_URL = "http://localhost:54321";
294
+ function randomToken(bytes) {
295
+ return toBase64Url(crypto.getRandomValues(new Uint8Array(bytes)));
296
+ }
297
+ function list(value) {
298
+ if (value === void 0) return [];
299
+ return typeof value === "string" ? [value] : [...value];
300
+ }
301
+ /**
302
+ * Supabase Lite in this process, for tests without Docker: an in-memory
303
+ * database with your schema, Auth with fresh API keys, and supabase-js
304
+ * clients whose requests go straight to Lite's `fetch` handler (no port).
305
+ *
306
+ * ```ts
307
+ * await using stack = await liteStack({ schemas: [await readFile("supabase/schemas/notes.sql", "utf8")] });
308
+ * const alice = await stack.asUser(betterSupabase, { sub: aliceId });
309
+ * expect(await alice.db.notes.count().orThrow()).toBe(1);
310
+ * ```
311
+ */
312
+ async function liteStack(options = {}) {
313
+ const lite = await loadLite();
314
+ if (!lite) throw new Error(`liteStack() needs @supabase/lite. ${LITE_INSTALL}`);
315
+ const driver = options.driver ?? "sqlite-postgres";
316
+ const jwtSecret = options.jwtSecret ?? randomToken(32);
317
+ const publishableKey = `sb_publishable_${randomToken(16)}`;
318
+ const secretKey = `sb_secret_${randomToken(16)}`;
319
+ let target;
320
+ if (driver === "pglite") target = {
321
+ driver,
322
+ connection: await lite.createPgliteConnection()
323
+ };
324
+ else target = {
325
+ driver,
326
+ connection: await lite.createSqliteConnection(":memory:")
327
+ };
328
+ const { connection } = target;
329
+ try {
330
+ const app = new lite.App({
331
+ connection,
332
+ auth: {
333
+ enabled: true,
334
+ jwt_secret: jwtSecret,
335
+ publishable_key: publishableKey,
336
+ secret_key: secretKey
337
+ }
338
+ });
339
+ await app.init();
340
+ await migrateLite(connection, [...options.migrations ?? [], ...options.schemas ?? []]);
341
+ for (const seed of list(options.seed)) await seedLite(target, seed);
342
+ const fetch = (input, init) => app.fetch(new Request(input, init));
343
+ const stateless = {
344
+ auth: {
345
+ persistSession: false,
346
+ autoRefreshToken: false
347
+ },
348
+ global: { fetch }
349
+ };
350
+ const close = () => connection.close();
351
+ return {
352
+ url: LITE_URL,
353
+ publishableKey,
354
+ secretKey,
355
+ jwtSecret,
356
+ fetch,
357
+ env: {
358
+ url: LITE_URL,
359
+ publishableKey,
360
+ secretKey,
361
+ secretKeys: { default: secretKey },
362
+ jwksUrl: new URL(`${LITE_URL}/auth/v1/.well-known/jwks.json`),
363
+ jwtSecret
364
+ },
365
+ supabase: createClient(LITE_URL, publishableKey, stateless),
366
+ admin: createClient(LITE_URL, secretKey, stateless),
367
+ asUser: (betterSupabase, claims) => asUser(betterSupabase, claims, {
368
+ url: LITE_URL,
369
+ publishableKey,
370
+ alg: "HS256",
371
+ jwtSecret,
372
+ fetch
373
+ }),
374
+ close,
375
+ [Symbol.asyncDispose]: close
376
+ };
377
+ } catch (error) {
378
+ await connection.close();
379
+ throw error;
380
+ }
381
+ }
382
+ //#endregion
243
383
  //#region src/testing/budget.ts
244
384
  function isPrefetch(response) {
245
385
  const headers = response.request().headers();
@@ -449,905 +589,932 @@ function deepFreeze(value) {
449
589
  }
450
590
  const frozenCopy = (value) => deepFreeze(structuredClone(value));
451
591
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
452
- const recorder = (ops) => ({
453
- name: "recorder",
454
- execute: (op) => {
455
- ops.push(op);
456
- return Promise.resolve(ok({
457
- rows: [],
458
- count: 0
459
- }));
460
- }
461
- });
462
- /** The operation `run` sends to the executor, built by `betterSupabase` without touching a database. */
463
- async function captureOp(betterSupabase, run) {
464
- const ops = [];
465
- await run(betterSupabase.connect(recorder(ops)));
466
- const [op] = ops;
467
- if (!op) throw new Error("the repository call did not reach the executor");
468
- return op;
592
+ //#endregion
593
+ //#region src/testing/isolation.ts
594
+ const scalar = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
595
+ function keyOf(table, row) {
596
+ return Object.fromEntries(table.primaryKey.map((column) => [column, row[column]]));
469
597
  }
470
- function tableKey(betterSupabase, table) {
471
- const key = table ?? Object.keys(betterSupabase.meta.tables)[0];
472
- if (!key || !(key in betterSupabase.meta.tables)) throw new TypeError(`better-supabase: unknown table "${String(key)}"`);
473
- return key;
598
+ async function rowsOf(outcome) {
599
+ const result = await outcome;
600
+ if (!result.ok) throw new Error(result.error.message);
601
+ return Array.isArray(result.data) ? result.data : [result.data];
602
+ }
603
+ function env(names) {
604
+ if (typeof process === "undefined") return void 0;
605
+ for (const name of names) {
606
+ const value = process.env[name];
607
+ if (value) return value;
608
+ }
474
609
  }
475
- const hasName = (subject) => ["has a name", () => {
476
- expect(typeof subject.name === "string" && subject.name.length > 0, "name must be a non-empty string");
477
- }];
478
610
  /**
479
- * Proves an `Executor` meets the contract: rows keyed by the selection's
480
- * aliases, counts, `aborted` errors, failures returned as `Result`s rather
481
- * than thrown, and (with `create`) a write round trip and a `deleteMany`
482
- * that `maxAffected` refuses without deleting anything.
611
+ * Checks row-level security between two tenants against a running stack: for
612
+ * each table, a user of one tenant must not select, insert, update or delete
613
+ * the other tenant's rows. Runs without `betterSupabase`'s plugins, so the `tenant()`
614
+ * plugin can't hide a missing policy. Throws a `ConformanceError` naming each
615
+ * table and command that leaks; removes every row it created.
483
616
  *
484
617
  * ```ts
485
- * it('conforms', () => testExecutor(myExecutor, { betterSupabase, table: 'tags', create: { name: 'x' } }));
618
+ * await expectTenantIsolation(betterSupabase, {
619
+ * tenants: [
620
+ * { id: ACME, claims: { sub: alice, tenant_id: ACME } },
621
+ * { id: GLOBEX, claims: { sub: bob, tenant_id: GLOBEX } },
622
+ * ],
623
+ * tables: {
624
+ * tags: {
625
+ * row: (tenant, n) => ({ organizationId: tenant.id, name: `iso-${n}` }),
626
+ * update: { name: 'changed' },
627
+ * },
628
+ * },
629
+ * });
486
630
  * ```
487
631
  */
488
- function testExecutor(executor, options) {
489
- const { betterSupabase } = options;
490
- const table = tableKey(betterSupabase, options.table);
491
- const context = { errorMappers: [] };
492
- const read = () => captureOp(betterSupabase, (db) => db[table]["findMany"]({ limit: 2 }));
493
- const create = options.create;
494
- return conform(`Executor "${executor.name}"`, [
495
- hasName(executor),
496
- ["reads rows keyed by the selection aliases", async () => {
497
- const op = await read();
498
- const result = await executor.execute(op, context);
499
- expect(result.ok, `select failed: ${result.ok ? "" : result.error.message}`);
500
- expect(isList(result.data.rows), "rows must be an array");
501
- expect(result.data.rows.length <= 2, `limit 2 returned ${result.data.rows.length} rows`);
502
- expect(result.data.count === null || typeof result.data.count === "number", "count must be a number or null");
503
- if (op.kind !== "select") return;
504
- const aliases = new Set(op.selection.columns.map((column) => column.alias));
505
- for (const row of result.data.rows) {
506
- const extra = Object.keys(row).filter((key) => !aliases.has(key));
507
- expect(extra.length === 0, `rows must use selection aliases; got ${extra.join(", ")}`);
508
- }
509
- }],
510
- ["counts rows", async () => {
511
- const op = await captureOp(betterSupabase, (db) => db[table]["count"]());
512
- const result = await executor.execute(op, context);
513
- expect(result.ok, `count failed: ${result.ok ? "" : result.error.message}`);
514
- expect(typeof result.data.count === "number" && result.data.count >= 0, "count must be a number");
515
- }],
516
- ["returns an aborted error for an aborted signal", async () => {
517
- const controller = new AbortController();
518
- controller.abort();
519
- const result = await executor.execute(await read(), {
520
- ...context,
521
- signal: controller.signal
522
- });
523
- expect(!result.ok, "an aborted request must fail");
524
- expect(result.error.kind === "aborted", `expected kind "aborted", got "${result.error.kind}"`);
525
- }],
526
- ["returns failures as results instead of throwing", async () => {
527
- const op = await read();
528
- const missing = {
529
- ...op,
530
- table: {
531
- ...op.table,
532
- name: "__better_supabase_missing__"
533
- }
534
- };
535
- const result = await executor.execute(missing, context);
536
- expect(!result.ok, "a missing table must fail");
537
- expect(typeof result.error.kind === "string" && typeof result.error.message === "string", "errors must be DbErrors");
538
- }],
539
- executor.rpc && ["returns rpc failures as results", async () => {
540
- expect(!(await executor.rpc("__better_supabase_missing__", {}, {
541
- ...context,
542
- schema: "public"
543
- })).ok, "calling a missing function must fail");
544
- }],
545
- executor.functionSources && ["reads from SelectOp.source instead of the table", async () => {
546
- const op = await read();
547
- if (op.kind !== "select") return;
548
- expect(!(await executor.execute({
549
- ...op,
550
- source: {
551
- schema: op.table.schema,
552
- name: "__better_supabase_missing__",
553
- args: {}
554
- }
555
- }, context)).ok, "a missing source function must fail; reading the table ignores source");
556
- }],
557
- executor.batch && ["batch returns one result per operation, in order", async () => {
558
- const rows = await read();
559
- const count = await captureOp(betterSupabase, (db) => db[table]["count"]());
560
- const missing = {
561
- ...rows,
562
- table: {
563
- ...rows.table,
564
- name: "__better_supabase_missing__"
565
- }
566
- };
567
- const results = await executor.batch([
568
- rows,
569
- count,
570
- missing
571
- ], context);
572
- expect(results.length === 3, `expected 3 results, got ${results.length}`);
573
- const [first, second, third] = results;
574
- expect(first?.ok === true && Array.isArray(first.data.rows), "the first operation must return rows");
575
- expect(second?.ok === true && typeof second.data.count === "number", "the second operation must return a count");
576
- expect(third?.ok === false, "a failing operation must fail on its own, as a result");
577
- }],
578
- create && ["round-trips a write", async () => {
579
- const repository = betterSupabase.connect(executor)[table];
580
- const created = await repository["create"](create);
581
- expect(created.ok, `create failed: ${JSON.stringify(created.error)}`);
582
- const row = created.data;
583
- for (const [key, value] of Object.entries(create)) expect(same(row[key], value), `created row has ${key} = ${JSON.stringify(row[key])}`);
584
- const [primary] = betterSupabase.meta.tables[table].primaryKey;
585
- expect(primary, "write checks need a table with a primary key");
586
- const removed = await repository["delete"](row[primary]);
587
- expect(removed.ok, `delete failed: ${JSON.stringify(removed.error)}`);
588
- const again = await repository["findFirst"]({ where: { [primary]: row[primary] } });
589
- expect(again.ok && again.data === null, "the deleted row is still readable");
590
- }],
591
- create && ["refuses a write past maxAffected and writes nothing", async () => {
592
- const repository = betterSupabase.connect(executor)[table];
593
- const [primary] = betterSupabase.meta.tables[table].primaryKey;
594
- expect(primary, "write checks need a table with a primary key");
595
- const created = await repository["create"](create);
596
- expect(created.ok, `create failed: ${JSON.stringify(created.error)}`);
597
- const id = created.data[primary];
598
- try {
599
- const refused = await repository["deleteMany"]({
600
- where: { [primary]: id },
601
- maxAffected: 0
602
- });
603
- expect(!refused.ok, "deleteMany past maxAffected must fail");
604
- const kind = refused.error?.kind;
605
- expect(kind === "max_affected", `expected kind "max_affected", got "${String(kind)}"`);
606
- const kept = await repository["findFirst"]({ where: { [primary]: id } });
607
- expect(kept.ok && kept.data !== null, "a refused deleteMany must not delete the row");
608
- const allowed = await repository["deleteMany"]({
609
- where: { [primary]: id },
610
- maxAffected: 1
611
- });
612
- expect(allowed.ok, `deleteMany within maxAffected failed: ${JSON.stringify(allowed.error)}`);
613
- } finally {
614
- await repository["delete"](id);
632
+ async function expectTenantIsolation(betterSupabase, options) {
633
+ const stack = options.stack ?? {};
634
+ const bare = new BetterSupabase(betterSupabase.schema);
635
+ const url = stack.url ?? env(ENV_VARIABLES.url) ?? "http://127.0.0.1:54321";
636
+ const secretKey = stack.secretKey ?? env(ENV_VARIABLES.secretKey);
637
+ if (!secretKey) throw new TypeError("expectTenantIsolation needs stack.secretKey or $SUPABASE_SECRET_KEY to seed rows.");
638
+ const admin = bare.connect(createClient(url, secretKey, { auth: {
639
+ persistSession: false,
640
+ autoRefreshToken: false
641
+ } }));
642
+ await options.seed?.();
643
+ const tenants = options.tenants;
644
+ const users = await Promise.all(tenants.map(async (tenant) => (await asUser(bare, tenant.claims, stack)).db));
645
+ const label = (tenant) => tenant.name ?? tenant.id;
646
+ const cleanup = [];
647
+ const reads = [];
648
+ const deletes = [];
649
+ try {
650
+ for (const [key, spec] of Object.entries(options.tables)) {
651
+ const table = bare.meta.tables[key];
652
+ if (!table || table.primaryKey.length === 0) throw new TypeError(`expectTenantIsolation: "${key}" is not a table with a primary key`);
653
+ const seeded = [];
654
+ for (const tenant of tenants) {
655
+ const [row] = await rowsOf(admin[key].create(spec.row(tenant, 0)));
656
+ const where = keyOf(table, row);
657
+ cleanup.push(() => admin[key].deleteMany({ where }));
658
+ seeded.push(row);
615
659
  }
616
- }]
617
- ]);
618
- }
619
- /**
620
- * Proves a `CacheAdapter` accepts table and row targets, repeated and
621
- * concurrent calls, and never mutates its input.
622
- */
623
- function testCacheAdapter(adapter, options = {}) {
624
- const table = options.table ?? "customers";
625
- const invalidate = (target) => Promise.resolve(adapter.invalidate(deepFreeze(target)));
626
- return conform(`CacheAdapter "${adapter.name}"`, [
627
- hasName(adapter),
628
- ["invalidates a table", () => invalidate({
629
- table,
630
- tables: [table],
631
- ids: []
632
- })],
633
- ["invalidates rows of a tenant", () => invalidate({
634
- table,
635
- tables: [table],
636
- ids: ["1", "a,b"],
637
- tenant: "organization_1"
638
- })],
639
- ["accepts repeated and concurrent calls", async () => {
640
- await invalidate({
641
- table,
642
- tables: [table],
643
- ids: ["1"]
644
- });
645
- await Promise.all(Array.from({ length: 10 }, (_, index) => invalidate({
646
- table,
647
- tables: [table],
648
- ids: [String(index)]
649
- })));
650
- }],
651
- ["accepts tables it has never seen", () => invalidate({
652
- table: "__better_supabase_unknown__",
653
- tables: ["__better_supabase_unknown__"],
654
- ids: []
655
- })],
656
- ["invalidates tables reached through foreign keys", () => invalidate({
657
- table,
658
- tables: [table, `${table}_children`],
659
- ids: ["1"]
660
- })]
661
- ]);
662
- }
663
- /** Proves an `EventSink` accepts empty and full batches without mutating them, and delivers them when readable. */
664
- function testEventSink(sink, options = {}) {
665
- const events = toCloudEvents({
666
- table: "customers",
667
- kind: "insert",
668
- rows: [{
669
- id: "1",
670
- name: "Acme"
671
- }, {
672
- id: "2",
673
- name: "Globex"
674
- }],
675
- context: { tenant: "organization_1" }
676
- }, { source: "/better-supabase/conformance" });
677
- return conform("EventSink", [
678
- ["accepts an empty batch", () => sink.send(deepFreeze([]))],
679
- ["accepts a batch without mutating it", () => sink.send(frozenCopy(events))],
680
- options.received && ["delivers every event", async () => {
681
- const ids = new Set((await options.received()).map((event) => event.id));
682
- const missing = events.filter((event) => !ids.has(event.id));
683
- expect(missing.length === 0, `${missing.length} of ${events.length} events were not delivered`);
684
- }]
685
- ]);
686
- }
687
- function claimedJob(queue, row) {
688
- const instant = temporal().Instant.fromEpochMilliseconds(0);
689
- return {
690
- id: Number(row.id),
691
- queue,
692
- payload: row.message?.payload,
693
- attempts: row.attempts,
694
- maxAttempts: row.message?.max_attempts ?? 5,
695
- enqueuedAt: instant,
696
- visibleUntil: instant,
697
- lastError: row.message?.last_error ?? null,
698
- context: {}
699
- };
700
- }
701
- /**
702
- * Proves a `QueueBackend` round-trips payloads, hides claimed messages for
703
- * their lease, counts attempts, retries and dead-letters on `fail`, and
704
- * (when it has `leases`) rejects a stale attempt and extends a lease. It
705
- * writes to `options.queue` on the backend's real store.
706
- */
707
- function testQueueBackend(backend, options) {
708
- const { queue } = options;
709
- const claimOne = async () => {
710
- const [row] = await backend.read(queue, 30, 1);
711
- return row && claimedJob(queue, row);
712
- };
713
- return conform(`QueueBackend "${backend.name}"`, [
714
- hasName(backend),
715
- ["has apiVersion 1", () => {
716
- const version = backend.apiVersion;
717
- expect(version === 1, "apiVersion must be 1");
718
- }],
719
- ["round-trips a payload and leases it", async () => {
720
- const id = await backend.send(queue, { n: 1 }, 0, 3, void 0);
721
- expect(Number.isFinite(id), "send must return a numeric id");
722
- const job = await claimOne();
723
- expect(job?.id === id, "read must return the sent message");
724
- expect(same(job.payload, { n: 1 }), "the payload must round-trip");
725
- expect(job.attempts === 1, "the first claim must be attempt 1");
726
- expect(job.maxAttempts === 3, "max_attempts must round-trip");
727
- expect(await claimOne() === void 0, "a leased message must stay hidden from other reads");
728
- expect(await backend.complete(job), "complete must return true");
729
- }],
730
- backend.leases && ["rejects a stale attempt and extends a lease", async () => {
731
- await backend.send(queue, { n: 2 }, 0, 3, void 0);
732
- const job = await claimOne();
733
- expect(job, "read must return the sent message");
734
- expect(!await backend.complete({
735
- ...job,
736
- attempts: job.attempts + 1
737
- }), "complete with another attempt must return false");
738
- expect(await backend.extend(job, 60), "extend must return true");
739
- expect(await backend.complete(job), "complete must return true");
740
- }],
741
- ["retries, then dead-letters at max attempts", async () => {
742
- await backend.send(queue, { n: 3 }, 0, 2, void 0);
743
- const first = await claimOne();
744
- expect(first, "read must return the sent message");
745
- expect(await backend.fail(first, "boom", 0) === "queued", "fail before max_attempts must return queued");
746
- const second = await claimOne();
747
- expect(second?.attempts === 2, "a retried message must be attempt 2");
748
- expect(await backend.fail(second, "boom", 0) === "dead", "fail at max_attempts must return dead");
749
- expect(await claimOne() === void 0, "a dead message must not be claimed again");
750
- }],
751
- backend.stats && backend.listDead && ["counts and lists dead letters", async () => {
752
- expect(((await backend.stats?.(queue))?.dead ?? 0) >= 1, "stats must count the dead letter");
753
- const [dead] = await backend.listDead?.(queue, 10, void 0) ?? [];
754
- expect(dead, "listDead must return the dead letter");
755
- expect(same(dead.message?.payload, { n: 3 }), "listDead must return the newest dead letter first");
756
- if (backend.retryDead) {
757
- expect(await backend.retryDead(queue, [Number(dead.id)], 10) === 1, "retryDead must enqueue the dead letter again");
758
- const job = await claimOne();
759
- expect(job, "the retried job must be claimable");
760
- expect(same(job.payload, { n: 3 }), "the retried job must keep its payload");
761
- await backend.complete(job);
660
+ for (const [self, other] of [[0, 1], [1, 0]]) {
661
+ const user = users[self][key];
662
+ const who = label(tenants[self]);
663
+ const whose = `${label(tenants[other])}'s`;
664
+ const own = keyOf(table, seeded[self]);
665
+ const target = keyOf(table, seeded[other]);
666
+ const current = async () => (await rowsOf(admin[key].findMany({ where: target })))[0];
667
+ reads.push([`${key}: ${who} reads its own row`, async () => {
668
+ expect((await rowsOf(user.findMany({ where: own }))).length === 1, `${who} can't read its own row, so the other checks prove nothing; check its claims and memberships`);
669
+ }], [`${key}: ${who} can't select ${whose} rows`, async () => {
670
+ expect((await rowsOf(user.findMany({ where: target }))).length === 0, `select returned ${whose} row`);
671
+ }], [`${key}: ${who} can't insert ${whose} rows`, async () => {
672
+ const row = spec.row(tenants[other], 1);
673
+ const result = await user.create(row, { returning: false });
674
+ if (result.ok) {
675
+ const where = Object.fromEntries(Object.entries(row).filter(([, value]) => scalar(value)));
676
+ cleanup.push(() => admin[key].deleteMany({ where }));
677
+ }
678
+ expect(!result.ok, `insert of a row for ${whose} tenant succeeded`);
679
+ expect(result.error.kind === "forbidden", `insert failed with ${result.error.kind} (${result.error.message}), not an RLS error; check the row factory`);
680
+ }], [`${key}: ${who} can't update ${whose} rows`, async () => {
681
+ const before = await current();
682
+ const patch = spec.update;
683
+ expect(Object.entries(patch).some(([column, value]) => before?.[column] !== value), "update must change the row to detect a leak");
684
+ await user.updateMany({
685
+ where: target,
686
+ data: patch,
687
+ returning: false
688
+ });
689
+ const after = await current();
690
+ expect(Object.entries(patch).every(([column]) => after?.[column] === before?.[column]), `update changed ${whose} row`);
691
+ }]);
692
+ deletes.push([`${key}: ${who} can't delete ${whose} rows`, async () => {
693
+ await user.deleteMany({ where: target });
694
+ expect(await current() !== void 0, `delete removed ${whose} row`);
695
+ }]);
762
696
  }
763
- }],
764
- (options.dedupe ?? backend.leases) && ["returns the first id for a repeated dedupe key", async () => {
765
- expect(await backend.send(queue, { n: 4 }, 0, 3, "same") === await backend.send(queue, { n: 4 }, 0, 3, "same"), "a waiting dedupe key must return its id");
766
- const job = await claimOne();
767
- expect(job, "read must return the sent message");
768
- await backend.complete(job);
769
- }]
770
- ]);
697
+ }
698
+ return await conform("Tenant isolation", [...reads, ...deletes]);
699
+ } finally {
700
+ for (const remove of cleanup.reverse()) await remove();
701
+ }
771
702
  }
703
+ //#endregion
704
+ //#region src/testing/claims-fixtures.ts
705
+ const base = {
706
+ sub: "6f1c2c1e-5d0a-4d9e-9a51-6b1f0e7c2a10",
707
+ aud: "authenticated",
708
+ role: "authenticated",
709
+ iss: "https://project.supabase.co/auth/v1",
710
+ aal: "aal1",
711
+ session_id: "b7a8f9c0-1111-4222-8333-944455556666",
712
+ is_anonymous: false
713
+ };
714
+ const admin = "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d";
715
+ const supportSessionId = "4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b";
716
+ const client = "5f0e4d3c-2b1a-4098-8776-655443322110";
717
+ const chain = {
718
+ sub: "agent-runner",
719
+ act: { sub: "mcp-client-42" }
720
+ };
772
721
  /**
773
- * Proves a `SupportSessionStore` starts sessions with the input it was
774
- * given, shows a running session only to its admin, ends the admin's
775
- * previous session on start, ends a session once, and lists sessions.
776
- * It writes sessions to the store; they are all ended when it returns.
722
+ * `supportSession` and `supportSessionReadOnly` are what `supportClaims`
723
+ * mints (`act.kind: "support"`), `impersonation` what `actingAs` mints
724
+ * (`act.kind: "impersonation"`), `oauthClient` a Supabase OAuth server token
725
+ * and `agentChain` an RFC 8693 chain without `kind`.
777
726
  */
778
- function testSupportSessionStore(store, options) {
779
- const { admin } = options;
780
- const [first, second] = options.targets;
781
- const start = (targetUserId) => store.start({
782
- adminId: admin.id,
783
- adminClaims: admin.claims,
784
- targetUserId,
785
- reason: "conformance",
786
- ttlSeconds: 600,
787
- readOnly: true,
788
- metadata: { ticket: "T-1" }
789
- });
790
- return conform(`SupportSessionStore "${store.name}"`, [
791
- hasName(store),
792
- ["has apiVersion 1", () => {
793
- const version = store.apiVersion;
794
- expect(version === 1, "apiVersion must be 1");
795
- }],
796
- ["starts a session with its input and shows it to its admin only", async () => {
797
- const session = await start(first);
798
- try {
799
- expect(session.id.length > 0, "start must return an id");
800
- expect(session.adminId === admin.id, "adminId must round-trip");
801
- expect(session.targetUserId === first, "targetUserId must round-trip");
802
- expect(session.reason === "conformance", "reason must round-trip");
803
- expect(session.readOnly, "readOnly must round-trip");
804
- expect(same(session.metadata, { ticket: "T-1" }), "metadata must round-trip");
805
- const seconds = (session.expiresAt.epochMilliseconds - session.startedAt.epochMilliseconds) / 1e3;
806
- expect(Math.abs(seconds - 600) < 5, "expiresAt must be ttlSeconds after startedAt");
807
- expect((await store.get(session.id, admin.id))?.id === session.id, "get must return the running session");
808
- expect(await store.get(session.id, first) === void 0, "get must hide a session from anyone but its admin");
809
- } finally {
810
- await store.end(session.id, "admin");
727
+ const supabaseClaimFixtures = {
728
+ supportSession: {
729
+ claims: {
730
+ ...base,
731
+ act: {
732
+ kind: "support",
733
+ sub: admin,
734
+ reason: "ticket 42",
735
+ session_id: supportSessionId,
736
+ read_only: false
811
737
  }
812
- }],
813
- ["ends the admin's previous session on start", async () => {
814
- const older = await start(first);
815
- const newer = await start(second);
816
- try {
817
- expect(await store.get(older.id, admin.id) === void 0, "starting a session must end the admin's previous one");
818
- expect((await store.get(newer.id, admin.id))?.id === newer.id, "the new session must be running");
819
- } finally {
820
- await store.end(newer.id, "admin");
738
+ },
739
+ expect: {
740
+ actor: {
741
+ kind: "support",
742
+ id: admin,
743
+ sessionId: supportSessionId,
744
+ readOnly: false,
745
+ reason: "ticket 42"
746
+ },
747
+ impersonator: {
748
+ kind: "support",
749
+ id: admin,
750
+ sessionId: supportSessionId,
751
+ readOnly: false,
752
+ reason: "ticket 42"
821
753
  }
822
- }],
823
- ["ends a session once and lists it", async () => {
824
- const session = await start(first);
825
- expect(await store.end(session.id, "admin"), "end must return true");
826
- expect(!await store.end(session.id, "admin"), "ending an ended session must return false");
827
- expect(await store.get(session.id, admin.id) === void 0, "get must not return an ended session");
828
- const entry = (await store.list({
829
- adminId: admin.id,
830
- active: false
831
- })).find((item) => item.id === session.id);
832
- expect(entry, "list must include the ended session");
833
- expect(entry.endedAt !== void 0, "an ended session has endedAt");
834
- expect((await store.list({
835
- adminId: admin.id,
836
- active: true
837
- })).every((item) => item.id !== session.id), "list({ active: true }) must leave out ended sessions");
838
- }]
839
- ]);
754
+ }
755
+ },
756
+ supportSessionReadOnly: {
757
+ claims: {
758
+ ...base,
759
+ act: {
760
+ kind: "support",
761
+ sub: admin,
762
+ reason: "ticket 42",
763
+ session_id: supportSessionId,
764
+ read_only: true
765
+ }
766
+ },
767
+ expect: {
768
+ actor: {
769
+ kind: "support",
770
+ id: admin,
771
+ sessionId: supportSessionId,
772
+ readOnly: true,
773
+ reason: "ticket 42"
774
+ },
775
+ impersonator: {
776
+ kind: "support",
777
+ id: admin,
778
+ sessionId: supportSessionId,
779
+ readOnly: true,
780
+ reason: "ticket 42"
781
+ }
782
+ }
783
+ },
784
+ impersonation: {
785
+ claims: {
786
+ ...base,
787
+ act: {
788
+ kind: "impersonation",
789
+ sub: admin,
790
+ reason: "ticket 42"
791
+ }
792
+ },
793
+ expect: {
794
+ actor: {
795
+ kind: "impersonation",
796
+ id: admin,
797
+ reason: "ticket 42"
798
+ },
799
+ impersonator: {
800
+ kind: "impersonation",
801
+ id: admin,
802
+ reason: "ticket 42"
803
+ }
804
+ }
805
+ },
806
+ oauthClient: {
807
+ claims: {
808
+ ...base,
809
+ client_id: client,
810
+ scope: "openid email posts:read"
811
+ },
812
+ expect: {
813
+ actor: {
814
+ kind: "oauth-client",
815
+ id: client
816
+ },
817
+ delegation: { scopes: [
818
+ "openid",
819
+ "email",
820
+ "posts:read"
821
+ ] }
822
+ }
823
+ },
824
+ agentChain: {
825
+ claims: {
826
+ ...base,
827
+ scope: "posts:read",
828
+ act: chain
829
+ },
830
+ expect: {
831
+ actor: {
832
+ kind: "oauth-client",
833
+ id: "agent-runner",
834
+ chain
835
+ },
836
+ delegation: {
837
+ scopes: ["posts:read"],
838
+ chain
839
+ }
840
+ }
841
+ }
842
+ };
843
+ //#endregion
844
+ //#region src/testing/seed.ts
845
+ const SEED = Symbol.for("better-supabase.seed");
846
+ function isSeed(value) {
847
+ return typeof value === "object" && value !== null && SEED in value;
840
848
  }
841
- async function resolveSafely(resolver, request) {
842
- try {
843
- return await resolver.resolve(request);
844
- } catch (cause) {
845
- throw new Violation(`resolve() threw: ${cause instanceof Error ? cause.message : String(cause)}`);
849
+ const quote = (text) => `'${text.replaceAll("'", "''")}'`;
850
+ function arrayLiteral(values) {
851
+ return `{${values.map((value) => {
852
+ if (value === null) return "NULL";
853
+ if (Array.isArray(value)) return arrayLiteral(value);
854
+ return `"${(temporalText(value) ?? String(value)).replaceAll(/[\\"]/g, "\\$&")}"`;
855
+ }).join(",")}}`;
856
+ }
857
+ function literal(value, column, where) {
858
+ if (value === null) return "null";
859
+ if (column.json) return quote(JSON.stringify(value));
860
+ if (Array.isArray(value)) return quote(arrayLiteral(value));
861
+ const temporalValue = temporalText(value);
862
+ if (temporalValue !== void 0) return quote(temporalValue);
863
+ const kind = typeof value;
864
+ switch (kind) {
865
+ case "string": return quote(value);
866
+ case "boolean":
867
+ case "bigint": return String(value);
868
+ case "number":
869
+ if (!Number.isFinite(value)) throw new TypeError(`${where}: ${String(value)} is not a finite number`);
870
+ return String(value);
871
+ case "object":
872
+ case "function":
873
+ case "symbol":
874
+ case "undefined": throw new TypeError(`${where}: unsupported value of type ${kind}`);
875
+ default: throw new TypeError(`${where}: unsupported value of type ${String(kind)}`);
846
876
  }
847
877
  }
848
- /**
849
- * Proves an `AuthResolver` passes on requests it doesn't understand, fails
850
- * closed (`invalid`, never anon) on bad credentials, and never throws.
851
- */
852
- function testAuthResolver(resolver, options) {
853
- return conform(`AuthResolver "${resolver.name}"`, [
854
- hasName(resolver),
855
- ["passes on requests without its credentials", async () => {
856
- const state = await resolveSafely(resolver, options.unrelated ?? new Request("https://example.com/"));
857
- expect(state === void 0, `expected undefined, got kind "${state?.kind}"`);
858
- }],
859
- ["fails closed on invalid credentials", async () => {
860
- expect(options.invalid.length > 0, "pass at least one invalid request");
861
- for (const request of options.invalid) {
862
- const state = await resolveSafely(resolver, request);
863
- expect(state?.kind === "invalid", `expected kind "invalid", got "${state?.kind ?? "undefined"}"`);
864
- expect(typeof state.error.kind === "string", "invalid states carry a DbError");
865
- }
866
- }],
867
- options.valid && ["resolves valid credentials", async () => {
868
- for (const { request, userId } of options.valid) {
869
- const state = await resolveSafely(resolver, request);
870
- expect(state?.kind === "user" || state?.kind === "service", `expected a user or service, got "${state?.kind}"`);
871
- if (userId !== void 0) expect(state.kind === "user" && state.user.id === userId, `expected user ${userId}`);
872
- }
873
- }]
874
- ]);
878
+ const ident = (name) => `"${name.replaceAll("\"", "\"\"")}"`;
879
+ /** Parents first (by forward relations), otherwise in the order given. */
880
+ function order(meta, keys) {
881
+ const sorted = [];
882
+ const visiting = /* @__PURE__ */ new Set();
883
+ const visit = (key) => {
884
+ if (sorted.includes(key) || visiting.has(key)) return;
885
+ visiting.add(key);
886
+ for (const relation of Object.values(meta.tables[key]?.relations ?? {})) if (relation.direction === "forward" && relation.table !== key && keys.includes(relation.table)) visit(relation.table);
887
+ visiting.delete(key);
888
+ sorted.push(key);
889
+ };
890
+ for (const key of keys) visit(key);
891
+ return sorted;
892
+ }
893
+ function statement(table, rows) {
894
+ const entries = Object.entries(rows);
895
+ if (entries.length === 0) return void 0;
896
+ const columns = [];
897
+ for (const [, row] of entries) for (const [key, value] of Object.entries(row)) {
898
+ if (value === void 0 || columns.includes(key)) continue;
899
+ const column = table.columns[key];
900
+ if (!column) throw new TypeError(`seed ${table.key}: unknown column "${key}"`);
901
+ if (column.generated) throw new TypeError(`seed ${table.key}: "${key}" is a generated column`);
902
+ columns.push(key);
903
+ }
904
+ const values = entries.map(([name, row]) => {
905
+ return ` (${columns.map((key) => row[key] === void 0 ? "default" : literal(row[key], table.columns[key], `seed ${table.key}.${name}.${key}`)).join(", ")})`;
906
+ });
907
+ return `insert into ${`${ident(table.schema)}.${ident(table.name)}`} (${columns.map((key) => ident(table.columns[key].db)).join(", ")}) values\n${values.join(",\n")}\non conflict do nothing;`;
875
908
  }
876
909
  /**
877
- * Proves a `Plugin` targets API v1, installs cleanly on its own and next to
878
- * the first-party plugins in either order, adds repository methods without
879
- * replacing base ones, keeps `context`, `transformQuery` and
880
- * `beforeMutation` pure and deterministic, and keeps executors'
881
- * results intact when it wraps them.
910
+ * Typed fixtures shared by `supabase/seeds` and your tests. Rows use app
911
+ * casing and Insert types; `better-supabase seed` renders them to SQL.
912
+ *
913
+ * ```ts
914
+ * export const seed = defineSeed(betterSupabase, {
915
+ * organizations: { acme: { id: ACME, name: 'Acme' } },
916
+ * customers: { first: { organizationId: ACME, name: 'First customer' } },
917
+ * });
918
+ * ```
882
919
  */
883
- function testPlugin(plugin, options) {
884
- const { betterSupabase } = options;
885
- const table = tableKey(betterSupabase, options.table);
886
- const context = options.context ?? {};
887
- const hook = (op) => ({
888
- table: op.table,
889
- schema: betterSupabase.meta,
890
- context,
891
- options: {},
892
- now: () => temporal().Instant.fromEpochMilliseconds(0)
893
- });
894
- const create = options.create;
895
- return conform(`Plugin "${plugin.name}"`, [
896
- hasName(plugin),
897
- ["targets plugin API v1", () => {
898
- expect(plugin.apiVersion === 1, `apiVersion is ${String(plugin.apiVersion)}`);
899
- }],
900
- ["installs and builds repositories", () => {
901
- const db = betterSupabase.use(plugin).connect(recorder([]), context);
902
- for (const key of Object.keys(betterSupabase.meta.tables)) expect(db[key], `db.${key} is missing`);
903
- }],
904
- ["declares a known hook order", () => {
905
- const enforce = plugin.enforce;
906
- expect(enforce === void 0 || enforce === "first" || enforce === "pre" || enforce === "post", `enforce is ${String(enforce)}; use "first", "pre", "post" or leave it out`);
907
- }],
908
- ["installs next to the first-party plugins in either order", () => {
909
- const others = [
910
- timestamps(),
911
- softDelete(),
912
- tenant(),
913
- actor()
914
- ].filter((other) => other.name !== plugin.name);
915
- let before = betterSupabase;
916
- for (const other of others) before = before.use(other);
917
- let after = betterSupabase.use(plugin);
918
- for (const other of others) after = after.use(other);
919
- for (const installed of [before.use(plugin), after]) {
920
- const db = installed.connect(recorder([]), context);
921
- for (const key of Object.keys(betterSupabase.meta.tables)) expect(db[key], `db.${key} is missing`);
922
- }
923
- }],
924
- plugin.repository && ["repository adds functions and keeps the base methods", () => {
925
- const db = betterSupabase.connect(recorder([]), context);
926
- for (const [key, table] of Object.entries(betterSupabase.meta.tables)) {
927
- const base = db[key];
928
- const added = plugin.repository({
929
- table,
930
- base
931
- });
932
- if (added === void 0) continue;
933
- for (const [name, method] of Object.entries(added)) {
934
- expect(typeof method === "function", `${key}.${name} is not a function`);
935
- expect(!(name in base), `${key}.${name} replaces a base method`);
936
- }
937
- }
938
- }],
939
- plugin.transformQuery && ["transformQuery is pure and deterministic", async () => {
940
- const op = await captureOp(betterSupabase, (db) => db[table]["findMany"]({ limit: 1 }));
941
- const first = plugin.transformQuery(frozenCopy(op), hook(op));
942
- const second = plugin.transformQuery(frozenCopy(op), hook(op));
943
- expect(typeof first === "object" && first !== null && first.kind === op.kind, "must return an operation of the same kind");
944
- expect(same(first, second), "two calls with the same input differ");
945
- }],
946
- plugin.beforeMutation && create && ["beforeMutation is pure and deterministic", async () => {
947
- const op = await captureOp(betterSupabase, (db) => db[table]["create"](create));
948
- if (op.kind === "select") throw new Violation("create did not produce a mutation");
949
- const first = await plugin.beforeMutation(frozenCopy(op), hook(op));
950
- const second = await plugin.beforeMutation(frozenCopy(op), hook(op));
951
- expect(first.kind === op.kind, "must return a mutation of the same kind");
952
- expect(same(first, second), "two calls with the same input differ");
953
- }],
954
- plugin.afterMutation && create && ["afterMutation cannot change the result", async () => {
955
- const row = {
956
- marker: "conformance",
957
- nested: { kept: true }
958
- };
959
- const result = await betterSupabase.use(plugin).connect({
960
- name: "rows",
961
- execute: () => Promise.resolve(ok({
962
- rows: [structuredClone(row)],
963
- count: 1
964
- }))
965
- }, context)[table]["create"](create);
966
- expect(result.ok, "create failed");
967
- expect(same(result.data, row), "afterMutation changed the returned row");
968
- }],
969
- plugin.wrapExecutor && ["wrapExecutor keeps results intact", async () => {
970
- const op = await captureOp(betterSupabase, (db) => db[table]["findMany"]({ limit: 1 }));
971
- const data = {
972
- rows: [{ marker: "conformance" }],
973
- count: 1
974
- };
975
- const wrapped = plugin.wrapExecutor({
976
- name: "inner",
977
- execute: () => Promise.resolve(ok(data))
978
- });
979
- expect(typeof wrapped.name === "string", "the wrapped executor needs a name");
980
- const result = await wrapped.execute(op, { errorMappers: [] });
981
- expect(result.ok && same(result.data, data), "the wrapped executor changed the result");
982
- }],
983
- plugin.context && ["context is pure and deterministic", () => {
984
- const args = { schema: betterSupabase.meta };
985
- const first = plugin.context(frozenCopy(context), args);
986
- const second = plugin.context(frozenCopy(context), args);
987
- expect(typeof first === "object" && first !== null, "must return a context object");
988
- expect(same(first, second), "two calls with the same input differ");
989
- }],
990
- plugin.mapError && ["mapError returns a DbError or undefined", () => {
991
- const fallback = dbError("unexpected", "internal error");
992
- const mapped = plugin.mapError({
993
- code: "XX000",
994
- message: "internal error"
995
- }, fallback);
996
- expect(mapped === void 0 || typeof mapped === "object" && typeof mapped.kind === "string", "must return a DbError or undefined");
997
- }]
998
- ]);
920
+ function defineSeed(betterSupabase, fixtures) {
921
+ const meta = betterSupabase.meta;
922
+ const statements = () => {
923
+ const keys = Object.keys(fixtures).filter((key) => fixtures[key] !== void 0);
924
+ for (const key of keys) {
925
+ const table = meta.tables[key];
926
+ if (!table) throw new TypeError(`seed: unknown table "${key}"`);
927
+ if (table.kind === "view") throw new TypeError(`seed: "${key}" is a view`);
928
+ }
929
+ return order(meta, keys).flatMap((key) => {
930
+ const sql = statement(meta.tables[key], fixtures[key]);
931
+ return sql ? [sql] : [];
932
+ });
933
+ };
934
+ return {
935
+ [SEED]: true,
936
+ rows: fixtures,
937
+ statements,
938
+ sql: () => statements().join("\n\n"),
939
+ async insert(sql) {
940
+ for (const text of statements()) await sql.queryRaw(text);
941
+ }
942
+ };
999
943
  }
1000
944
  //#endregion
1001
- //#region src/testing/isolation.ts
1002
- const scalar = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
1003
- function keyOf(table, row) {
1004
- return Object.fromEntries(table.primaryKey.map((column) => [column, row[column]]));
1005
- }
1006
- async function rowsOf(outcome) {
1007
- const result = await outcome;
1008
- if (!result.ok) throw new Error(result.error.message);
1009
- return Array.isArray(result.data) ? result.data : [result.data];
1010
- }
1011
- function env(names) {
1012
- if (typeof process === "undefined") return void 0;
1013
- for (const name of names) {
1014
- const value = process.env[name];
1015
- if (value) return value;
945
+ //#region src/testing/kits/executor.ts
946
+ const recorder = (ops) => ({
947
+ name: "recorder",
948
+ execute: (op) => {
949
+ ops.push(op);
950
+ return Promise.resolve(ok({
951
+ rows: [],
952
+ count: 0
953
+ }));
1016
954
  }
955
+ });
956
+ /** The operation `run` sends to the executor, built by `betterSupabase` without touching a database. */
957
+ async function captureOp(betterSupabase, run) {
958
+ const ops = [];
959
+ await run(betterSupabase.connect(recorder(ops)));
960
+ const [op] = ops;
961
+ if (!op) throw new Error("the repository call did not reach the executor");
962
+ return op;
1017
963
  }
964
+ function tableKey(betterSupabase, table) {
965
+ const key = table ?? Object.keys(betterSupabase.meta.tables)[0];
966
+ if (!key || !(key in betterSupabase.meta.tables)) throw new TypeError(`better-supabase: unknown table "${String(key)}"`);
967
+ return key;
968
+ }
969
+ const hasName = (subject) => ["has a name", () => {
970
+ expect(typeof subject.name === "string" && subject.name.length > 0, "name must be a non-empty string");
971
+ }];
1018
972
  /**
1019
- * Checks row-level security between two tenants against a running stack: for
1020
- * each table, a user of one tenant must not select, insert, update or delete
1021
- * the other tenant's rows. Runs without `betterSupabase`'s plugins, so the `tenant()`
1022
- * plugin can't hide a missing policy. Throws a `ConformanceError` naming each
1023
- * table and command that leaks; removes every row it created.
973
+ * Proves an `Executor` meets the contract: rows keyed by the selection's
974
+ * aliases, counts, `aborted` errors, failures returned as `Result`s rather
975
+ * than thrown, and (with `create`) a write round trip and a `deleteMany`
976
+ * that `maxAffected` refuses without deleting anything.
1024
977
  *
1025
978
  * ```ts
1026
- * await expectTenantIsolation(betterSupabase, {
1027
- * tenants: [
1028
- * { id: ACME, claims: { sub: alice, tenant_id: ACME } },
1029
- * { id: GLOBEX, claims: { sub: bob, tenant_id: GLOBEX } },
1030
- * ],
1031
- * tables: {
1032
- * tags: {
1033
- * row: (tenant, n) => ({ organizationId: tenant.id, name: `iso-${n}` }),
1034
- * update: { name: 'changed' },
1035
- * },
1036
- * },
1037
- * });
979
+ * it('conforms', () => testExecutor(myExecutor, { betterSupabase, table: 'tags', create: { name: 'x' } }));
1038
980
  * ```
1039
981
  */
1040
- async function expectTenantIsolation(betterSupabase, options) {
1041
- const stack = options.stack ?? {};
1042
- const bare = new BetterSupabase(betterSupabase.schema);
1043
- const url = stack.url ?? env(ENV_VARIABLES.url) ?? "http://127.0.0.1:54321";
1044
- const secretKey = stack.secretKey ?? env(ENV_VARIABLES.secretKey);
1045
- if (!secretKey) throw new TypeError("expectTenantIsolation needs stack.secretKey or $SUPABASE_SECRET_KEY to seed rows.");
1046
- const admin = bare.connect(createClient(url, secretKey, { auth: {
1047
- persistSession: false,
1048
- autoRefreshToken: false
1049
- } }));
1050
- await options.seed?.();
1051
- const tenants = options.tenants;
1052
- const users = await Promise.all(tenants.map(async (tenant) => (await asUser(bare, tenant.claims, stack)).db));
1053
- const label = (tenant) => tenant.name ?? tenant.id;
1054
- const cleanup = [];
1055
- const reads = [];
1056
- const deletes = [];
1057
- try {
1058
- for (const [key, spec] of Object.entries(options.tables)) {
1059
- const table = bare.meta.tables[key];
1060
- if (!table || table.primaryKey.length === 0) throw new TypeError(`expectTenantIsolation: "${key}" is not a table with a primary key`);
1061
- const seeded = [];
1062
- for (const tenant of tenants) {
1063
- const [row] = await rowsOf(admin[key].create(spec.row(tenant, 0)));
1064
- const where = keyOf(table, row);
1065
- cleanup.push(() => admin[key].deleteMany({ where }));
1066
- seeded.push(row);
982
+ function testExecutor(executor, options) {
983
+ const { betterSupabase } = options;
984
+ const table = tableKey(betterSupabase, options.table);
985
+ const context = { errorMappers: [] };
986
+ const read = () => captureOp(betterSupabase, (db) => db[table]["findMany"]({ limit: 2 }));
987
+ const create = options.create;
988
+ return conform(`Executor "${executor.name}"`, [
989
+ hasName(executor),
990
+ ["reads rows keyed by the selection aliases", async () => {
991
+ const op = await read();
992
+ const result = await executor.execute(op, context);
993
+ expect(result.ok, `select failed: ${result.ok ? "" : result.error.message}`);
994
+ expect(isList(result.data.rows), "rows must be an array");
995
+ expect(result.data.rows.length <= 2, `limit 2 returned ${result.data.rows.length} rows`);
996
+ expect(result.data.count === null || typeof result.data.count === "number", "count must be a number or null");
997
+ if (op.kind !== "select") return;
998
+ const aliases = new Set(op.selection.columns.map((column) => column.alias));
999
+ for (const row of result.data.rows) {
1000
+ const extra = Object.keys(row).filter((key) => !aliases.has(key));
1001
+ expect(extra.length === 0, `rows must use selection aliases; got ${extra.join(", ")}`);
1067
1002
  }
1068
- for (const [self, other] of [[0, 1], [1, 0]]) {
1069
- const user = users[self][key];
1070
- const who = label(tenants[self]);
1071
- const whose = `${label(tenants[other])}'s`;
1072
- const own = keyOf(table, seeded[self]);
1073
- const target = keyOf(table, seeded[other]);
1074
- const current = async () => (await rowsOf(admin[key].findMany({ where: target })))[0];
1075
- reads.push([`${key}: ${who} reads its own row`, async () => {
1076
- expect((await rowsOf(user.findMany({ where: own }))).length === 1, `${who} can't read its own row, so the other checks prove nothing; check its claims and memberships`);
1077
- }], [`${key}: ${who} can't select ${whose} rows`, async () => {
1078
- expect((await rowsOf(user.findMany({ where: target }))).length === 0, `select returned ${whose} row`);
1079
- }], [`${key}: ${who} can't insert ${whose} rows`, async () => {
1080
- const row = spec.row(tenants[other], 1);
1081
- const result = await user.create(row, { returning: false });
1082
- if (result.ok) {
1083
- const where = Object.fromEntries(Object.entries(row).filter(([, value]) => scalar(value)));
1084
- cleanup.push(() => admin[key].deleteMany({ where }));
1085
- }
1086
- expect(!result.ok, `insert of a row for ${whose} tenant succeeded`);
1087
- expect(result.error.kind === "forbidden", `insert failed with ${result.error.kind} (${result.error.message}), not an RLS error; check the row factory`);
1088
- }], [`${key}: ${who} can't update ${whose} rows`, async () => {
1089
- const before = await current();
1090
- const patch = spec.update;
1091
- expect(Object.entries(patch).some(([column, value]) => before?.[column] !== value), "update must change the row to detect a leak");
1092
- await user.updateMany({
1093
- where: target,
1094
- data: patch,
1095
- returning: false
1096
- });
1097
- const after = await current();
1098
- expect(Object.entries(patch).every(([column]) => after?.[column] === before?.[column]), `update changed ${whose} row`);
1099
- }]);
1100
- deletes.push([`${key}: ${who} can't delete ${whose} rows`, async () => {
1101
- await user.deleteMany({ where: target });
1102
- expect(await current() !== void 0, `delete removed ${whose} row`);
1103
- }]);
1003
+ }],
1004
+ ["counts rows", async () => {
1005
+ const op = await captureOp(betterSupabase, (db) => db[table]["count"]());
1006
+ const result = await executor.execute(op, context);
1007
+ expect(result.ok, `count failed: ${result.ok ? "" : result.error.message}`);
1008
+ expect(typeof result.data.count === "number" && result.data.count >= 0, "count must be a number");
1009
+ }],
1010
+ ["returns an aborted error for an aborted signal", async () => {
1011
+ const controller = new AbortController();
1012
+ controller.abort();
1013
+ const result = await executor.execute(await read(), {
1014
+ ...context,
1015
+ signal: controller.signal
1016
+ });
1017
+ expect(!result.ok, "an aborted request must fail");
1018
+ expect(result.error.kind === "aborted", `expected kind "aborted", got "${result.error.kind}"`);
1019
+ }],
1020
+ ["returns failures as results instead of throwing", async () => {
1021
+ const op = await read();
1022
+ const missing = {
1023
+ ...op,
1024
+ table: {
1025
+ ...op.table,
1026
+ name: "__better_supabase_missing__"
1027
+ }
1028
+ };
1029
+ const result = await executor.execute(missing, context);
1030
+ expect(!result.ok, "a missing table must fail");
1031
+ expect(typeof result.error.kind === "string" && typeof result.error.message === "string", "errors must be DbErrors");
1032
+ }],
1033
+ executor.rpc && ["returns rpc failures as results", async () => {
1034
+ expect(!(await executor.rpc("__better_supabase_missing__", {}, {
1035
+ ...context,
1036
+ schema: "public"
1037
+ })).ok, "calling a missing function must fail");
1038
+ }],
1039
+ executor.functionSources && ["reads from SelectOp.source instead of the table", async () => {
1040
+ const op = await read();
1041
+ if (op.kind !== "select") return;
1042
+ expect(!(await executor.execute({
1043
+ ...op,
1044
+ source: {
1045
+ schema: op.table.schema,
1046
+ name: "__better_supabase_missing__",
1047
+ args: {}
1048
+ }
1049
+ }, context)).ok, "a missing source function must fail; reading the table ignores source");
1050
+ }],
1051
+ executor.batch && ["batch returns one result per operation, in order", async () => {
1052
+ const rows = await read();
1053
+ const count = await captureOp(betterSupabase, (db) => db[table]["count"]());
1054
+ const missing = {
1055
+ ...rows,
1056
+ table: {
1057
+ ...rows.table,
1058
+ name: "__better_supabase_missing__"
1059
+ }
1060
+ };
1061
+ const results = await executor.batch([
1062
+ rows,
1063
+ count,
1064
+ missing
1065
+ ], context);
1066
+ expect(results.length === 3, `expected 3 results, got ${results.length}`);
1067
+ const [first, second, third] = results;
1068
+ expect(first?.ok === true && Array.isArray(first.data.rows), "the first operation must return rows");
1069
+ expect(second?.ok === true && typeof second.data.count === "number", "the second operation must return a count");
1070
+ expect(third?.ok === false, "a failing operation must fail on its own, as a result");
1071
+ }],
1072
+ create && ["round-trips a write", async () => {
1073
+ const repository = betterSupabase.connect(executor)[table];
1074
+ const created = await repository["create"](create);
1075
+ expect(created.ok, `create failed: ${JSON.stringify(created.error)}`);
1076
+ const row = created.data;
1077
+ for (const [key, value] of Object.entries(create)) expect(same(row[key], value), `created row has ${key} = ${JSON.stringify(row[key])}`);
1078
+ const [primary] = betterSupabase.meta.tables[table].primaryKey;
1079
+ expect(primary, "write checks need a table with a primary key");
1080
+ const removed = await repository["delete"](row[primary]);
1081
+ expect(removed.ok, `delete failed: ${JSON.stringify(removed.error)}`);
1082
+ const again = await repository["findFirst"]({ where: { [primary]: row[primary] } });
1083
+ expect(again.ok && again.data === null, "the deleted row is still readable");
1084
+ }],
1085
+ create && ["refuses a write past maxAffected and writes nothing", async () => {
1086
+ const repository = betterSupabase.connect(executor)[table];
1087
+ const [primary] = betterSupabase.meta.tables[table].primaryKey;
1088
+ expect(primary, "write checks need a table with a primary key");
1089
+ const created = await repository["create"](create);
1090
+ expect(created.ok, `create failed: ${JSON.stringify(created.error)}`);
1091
+ const id = created.data[primary];
1092
+ try {
1093
+ const refused = await repository["deleteMany"]({
1094
+ where: { [primary]: id },
1095
+ maxAffected: 0
1096
+ });
1097
+ expect(!refused.ok, "deleteMany past maxAffected must fail");
1098
+ const kind = refused.error?.kind;
1099
+ expect(kind === "max_affected", `expected kind "max_affected", got "${String(kind)}"`);
1100
+ const kept = await repository["findFirst"]({ where: { [primary]: id } });
1101
+ expect(kept.ok && kept.data !== null, "a refused deleteMany must not delete the row");
1102
+ const allowed = await repository["deleteMany"]({
1103
+ where: { [primary]: id },
1104
+ maxAffected: 1
1105
+ });
1106
+ expect(allowed.ok, `deleteMany within maxAffected failed: ${JSON.stringify(allowed.error)}`);
1107
+ } finally {
1108
+ await repository["delete"](id);
1104
1109
  }
1105
- }
1106
- return await conform("Tenant isolation", [...reads, ...deletes]);
1107
- } finally {
1108
- for (const remove of cleanup.reverse()) await remove();
1109
- }
1110
+ }]
1111
+ ]);
1110
1112
  }
1111
1113
  //#endregion
1112
- //#region src/testing/claims-fixtures.ts
1113
- const base = {
1114
- sub: "6f1c2c1e-5d0a-4d9e-9a51-6b1f0e7c2a10",
1115
- aud: "authenticated",
1116
- role: "authenticated",
1117
- iss: "https://project.supabase.co/auth/v1",
1118
- aal: "aal1",
1119
- session_id: "b7a8f9c0-1111-4222-8333-944455556666",
1120
- is_anonymous: false
1121
- };
1122
- const admin = "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d";
1123
- const supportSessionId = "4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b";
1124
- const client = "5f0e4d3c-2b1a-4098-8776-655443322110";
1125
- const chain = {
1126
- sub: "agent-runner",
1127
- act: { sub: "mcp-client-42" }
1128
- };
1114
+ //#region src/testing/kits/auth-resolver.ts
1115
+ async function resolveSafely(resolver, request) {
1116
+ try {
1117
+ return await resolver.resolve(request);
1118
+ } catch (cause) {
1119
+ throw new Violation(`resolve() threw: ${cause instanceof Error ? cause.message : String(cause)}`);
1120
+ }
1121
+ }
1129
1122
  /**
1130
- * `supportSession` and `supportSessionReadOnly` are what `supportClaims`
1131
- * mints (`act.kind: "support"`), `impersonation` what `actingAs` mints
1132
- * (`act.kind: "impersonation"`), `oauthClient` a Supabase OAuth server token
1133
- * and `agentChain` an RFC 8693 chain without `kind`.
1123
+ * Proves an `AuthResolver` passes on requests it doesn't understand, fails
1124
+ * closed (`invalid`, never anon) on bad credentials, and never throws.
1134
1125
  */
1135
- const supabaseClaimFixtures = {
1136
- supportSession: {
1137
- claims: {
1138
- ...base,
1139
- act: {
1140
- kind: "support",
1141
- sub: admin,
1142
- reason: "ticket 42",
1143
- session_id: supportSessionId,
1144
- read_only: false
1145
- }
1146
- },
1147
- expect: {
1148
- actor: {
1149
- kind: "support",
1150
- id: admin,
1151
- sessionId: supportSessionId,
1152
- readOnly: false,
1153
- reason: "ticket 42"
1154
- },
1155
- impersonator: {
1156
- kind: "support",
1157
- id: admin,
1158
- sessionId: supportSessionId,
1159
- readOnly: false,
1160
- reason: "ticket 42"
1161
- }
1162
- }
1163
- },
1164
- supportSessionReadOnly: {
1165
- claims: {
1166
- ...base,
1167
- act: {
1168
- kind: "support",
1169
- sub: admin,
1170
- reason: "ticket 42",
1171
- session_id: supportSessionId,
1172
- read_only: true
1173
- }
1174
- },
1175
- expect: {
1176
- actor: {
1177
- kind: "support",
1178
- id: admin,
1179
- sessionId: supportSessionId,
1180
- readOnly: true,
1181
- reason: "ticket 42"
1182
- },
1183
- impersonator: {
1184
- kind: "support",
1185
- id: admin,
1186
- sessionId: supportSessionId,
1187
- readOnly: true,
1188
- reason: "ticket 42"
1189
- }
1190
- }
1191
- },
1192
- impersonation: {
1193
- claims: {
1194
- ...base,
1195
- act: {
1196
- kind: "impersonation",
1197
- sub: admin,
1198
- reason: "ticket 42"
1126
+ function testAuthResolver(resolver, options) {
1127
+ return conform(`AuthResolver "${resolver.name}"`, [
1128
+ hasName(resolver),
1129
+ ["passes on requests without its credentials", async () => {
1130
+ const state = await resolveSafely(resolver, options.unrelated ?? new Request("https://example.com/"));
1131
+ expect(state === void 0, `expected undefined, got kind "${state?.kind}"`);
1132
+ }],
1133
+ ["fails closed on invalid credentials", async () => {
1134
+ expect(options.invalid.length > 0, "pass at least one invalid request");
1135
+ for (const request of options.invalid) {
1136
+ const state = await resolveSafely(resolver, request);
1137
+ expect(state?.kind === "invalid", `expected kind "invalid", got "${state?.kind ?? "undefined"}"`);
1138
+ expect(typeof state.error.kind === "string", "invalid states carry a DbError");
1199
1139
  }
1200
- },
1201
- expect: {
1202
- actor: {
1203
- kind: "impersonation",
1204
- id: admin,
1205
- reason: "ticket 42"
1206
- },
1207
- impersonator: {
1208
- kind: "impersonation",
1209
- id: admin,
1210
- reason: "ticket 42"
1140
+ }],
1141
+ options.valid && ["resolves valid credentials", async () => {
1142
+ for (const { request, userId } of options.valid) {
1143
+ const state = await resolveSafely(resolver, request);
1144
+ expect(state?.kind === "user" || state?.kind === "service", `expected a user or service, got "${state?.kind}"`);
1145
+ if (userId !== void 0) expect(state.kind === "user" && state.user.id === userId, `expected user ${userId}`);
1211
1146
  }
1212
- }
1213
- },
1214
- oauthClient: {
1215
- claims: {
1216
- ...base,
1217
- client_id: client,
1218
- scope: "openid email posts:read"
1219
- },
1220
- expect: {
1221
- actor: {
1222
- kind: "oauth-client",
1223
- id: client
1224
- },
1225
- delegation: { scopes: [
1226
- "openid",
1227
- "email",
1228
- "posts:read"
1229
- ] }
1230
- }
1231
- },
1232
- agentChain: {
1233
- claims: {
1234
- ...base,
1235
- scope: "posts:read",
1236
- act: chain
1237
- },
1238
- expect: {
1239
- actor: {
1240
- kind: "oauth-client",
1241
- id: "agent-runner",
1242
- chain
1243
- },
1244
- delegation: {
1245
- scopes: ["posts:read"],
1246
- chain
1247
- }
1248
- }
1249
- }
1250
- };
1251
- //#endregion
1252
- //#region src/testing/seed.ts
1253
- const SEED = Symbol.for("better-supabase.seed");
1254
- function isSeed(value) {
1255
- return typeof value === "object" && value !== null && SEED in value;
1256
- }
1257
- const quote = (text) => `'${text.replaceAll("'", "''")}'`;
1258
- function arrayLiteral(values) {
1259
- return `{${values.map((value) => {
1260
- if (value === null) return "NULL";
1261
- if (Array.isArray(value)) return arrayLiteral(value);
1262
- return `"${(temporalText(value) ?? String(value)).replaceAll(/[\\"]/g, "\\$&")}"`;
1263
- }).join(",")}}`;
1147
+ }]
1148
+ ]);
1264
1149
  }
1265
- function literal(value, column, where) {
1266
- if (value === null) return "null";
1267
- if (column.json) return quote(JSON.stringify(value));
1268
- if (Array.isArray(value)) return quote(arrayLiteral(value));
1269
- const temporalValue = temporalText(value);
1270
- if (temporalValue !== void 0) return quote(temporalValue);
1271
- const kind = typeof value;
1272
- switch (kind) {
1273
- case "string": return quote(value);
1274
- case "boolean":
1275
- case "bigint": return String(value);
1276
- case "number":
1277
- if (!Number.isFinite(value)) throw new TypeError(`${where}: ${String(value)} is not a finite number`);
1278
- return String(value);
1279
- case "object":
1280
- case "function":
1281
- case "symbol":
1282
- case "undefined": throw new TypeError(`${where}: unsupported value of type ${kind}`);
1283
- default: throw new TypeError(`${where}: unsupported value of type ${String(kind)}`);
1284
- }
1150
+ //#endregion
1151
+ //#region src/testing/kits/cache-adapter.ts
1152
+ /**
1153
+ * Proves a `CacheAdapter` accepts table and row targets, repeated and
1154
+ * concurrent calls, and never mutates its input.
1155
+ */
1156
+ function testCacheAdapter(adapter, options = {}) {
1157
+ const table = options.table ?? "customers";
1158
+ const invalidate = (target) => Promise.resolve(adapter.invalidate(deepFreeze(target)));
1159
+ return conform(`CacheAdapter "${adapter.name}"`, [
1160
+ hasName(adapter),
1161
+ ["invalidates a table", () => invalidate({
1162
+ table,
1163
+ tables: [table],
1164
+ ids: []
1165
+ })],
1166
+ ["invalidates rows of a tenant", () => invalidate({
1167
+ table,
1168
+ tables: [table],
1169
+ ids: ["1", "a,b"],
1170
+ tenant: "organization_1"
1171
+ })],
1172
+ ["accepts repeated and concurrent calls", async () => {
1173
+ await invalidate({
1174
+ table,
1175
+ tables: [table],
1176
+ ids: ["1"]
1177
+ });
1178
+ await Promise.all(Array.from({ length: 10 }, (_, index) => invalidate({
1179
+ table,
1180
+ tables: [table],
1181
+ ids: [String(index)]
1182
+ })));
1183
+ }],
1184
+ ["accepts tables it has never seen", () => invalidate({
1185
+ table: "__better_supabase_unknown__",
1186
+ tables: ["__better_supabase_unknown__"],
1187
+ ids: []
1188
+ })],
1189
+ ["invalidates tables reached through foreign keys", () => invalidate({
1190
+ table,
1191
+ tables: [table, `${table}_children`],
1192
+ ids: ["1"]
1193
+ })]
1194
+ ]);
1285
1195
  }
1286
- const ident = (name) => `"${name.replaceAll("\"", "\"\"")}"`;
1287
- /** Parents first (by forward relations), otherwise in the order given. */
1288
- function order(meta, keys) {
1289
- const sorted = [];
1290
- const visiting = /* @__PURE__ */ new Set();
1291
- const visit = (key) => {
1292
- if (sorted.includes(key) || visiting.has(key)) return;
1293
- visiting.add(key);
1294
- for (const relation of Object.values(meta.tables[key]?.relations ?? {})) if (relation.direction === "forward" && relation.table !== key && keys.includes(relation.table)) visit(relation.table);
1295
- visiting.delete(key);
1296
- sorted.push(key);
1297
- };
1298
- for (const key of keys) visit(key);
1299
- return sorted;
1196
+ //#endregion
1197
+ //#region src/testing/kits/event-sink.ts
1198
+ /** Proves an `EventSink` accepts empty and full batches without mutating them, and delivers them when readable. */
1199
+ function testEventSink(sink, options = {}) {
1200
+ const events = toCloudEvents({
1201
+ table: "customers",
1202
+ kind: "insert",
1203
+ rows: [{
1204
+ id: "1",
1205
+ name: "Acme"
1206
+ }, {
1207
+ id: "2",
1208
+ name: "Globex"
1209
+ }],
1210
+ context: { tenant: "organization_1" }
1211
+ }, { source: "/better-supabase/conformance" });
1212
+ return conform("EventSink", [
1213
+ ["accepts an empty batch", () => sink.send(deepFreeze([]))],
1214
+ ["accepts a batch without mutating it", () => sink.send(frozenCopy(events))],
1215
+ options.received && ["delivers every event", async () => {
1216
+ const ids = new Set((await options.received()).map((event) => event.id));
1217
+ const missing = events.filter((event) => !ids.has(event.id));
1218
+ expect(missing.length === 0, `${missing.length} of ${events.length} events were not delivered`);
1219
+ }]
1220
+ ]);
1300
1221
  }
1301
- function statement(table, rows) {
1302
- const entries = Object.entries(rows);
1303
- if (entries.length === 0) return void 0;
1304
- const columns = [];
1305
- for (const [, row] of entries) for (const [key, value] of Object.entries(row)) {
1306
- if (value === void 0 || columns.includes(key)) continue;
1307
- const column = table.columns[key];
1308
- if (!column) throw new TypeError(`seed ${table.key}: unknown column "${key}"`);
1309
- if (column.generated) throw new TypeError(`seed ${table.key}: "${key}" is a generated column`);
1310
- columns.push(key);
1311
- }
1312
- const values = entries.map(([name, row]) => {
1313
- return ` (${columns.map((key) => row[key] === void 0 ? "default" : literal(row[key], table.columns[key], `seed ${table.key}.${name}.${key}`)).join(", ")})`;
1222
+ //#endregion
1223
+ //#region src/testing/kits/plugin.ts
1224
+ /**
1225
+ * Proves a `Plugin` targets API v1, installs cleanly on its own and next to
1226
+ * the first-party plugins in either order, adds repository methods without
1227
+ * replacing base ones, keeps `context`, `transformQuery` and
1228
+ * `beforeMutation` pure and deterministic, and keeps executors'
1229
+ * results intact when it wraps them.
1230
+ */
1231
+ function testPlugin(plugin, options) {
1232
+ const { betterSupabase } = options;
1233
+ const table = tableKey(betterSupabase, options.table);
1234
+ const context = options.context ?? {};
1235
+ const hook = (op) => ({
1236
+ table: op.table,
1237
+ schema: betterSupabase.meta,
1238
+ context,
1239
+ options: {},
1240
+ now: () => temporal().Instant.fromEpochMilliseconds(0)
1314
1241
  });
1315
- return `insert into ${`${ident(table.schema)}.${ident(table.name)}`} (${columns.map((key) => ident(table.columns[key].db)).join(", ")}) values\n${values.join(",\n")}\non conflict do nothing;`;
1242
+ const create = options.create;
1243
+ return conform(`Plugin "${plugin.name}"`, [
1244
+ hasName(plugin),
1245
+ ["targets plugin API v1", () => {
1246
+ expect(plugin.apiVersion === 1, `apiVersion is ${String(plugin.apiVersion)}`);
1247
+ }],
1248
+ ["installs and builds repositories", () => {
1249
+ const db = betterSupabase.use(plugin).connect(recorder([]), context);
1250
+ for (const key of Object.keys(betterSupabase.meta.tables)) expect(db[key], `db.${key} is missing`);
1251
+ }],
1252
+ ["declares a known hook order", () => {
1253
+ const enforce = plugin.enforce;
1254
+ expect(enforce === void 0 || enforce === "first" || enforce === "pre" || enforce === "post", `enforce is ${String(enforce)}; use "first", "pre", "post" or leave it out`);
1255
+ }],
1256
+ ["installs next to the first-party plugins in either order", () => {
1257
+ const others = [
1258
+ timestamps(),
1259
+ softDelete(),
1260
+ tenant(),
1261
+ actor()
1262
+ ].filter((other) => other.name !== plugin.name);
1263
+ let before = betterSupabase;
1264
+ for (const other of others) before = before.use(other);
1265
+ let after = betterSupabase.use(plugin);
1266
+ for (const other of others) after = after.use(other);
1267
+ for (const installed of [before.use(plugin), after]) {
1268
+ const db = installed.connect(recorder([]), context);
1269
+ for (const key of Object.keys(betterSupabase.meta.tables)) expect(db[key], `db.${key} is missing`);
1270
+ }
1271
+ }],
1272
+ plugin.repository && ["repository adds functions and keeps the base methods", () => {
1273
+ const db = betterSupabase.connect(recorder([]), context);
1274
+ for (const [key, table] of Object.entries(betterSupabase.meta.tables)) {
1275
+ const base = db[key];
1276
+ const added = plugin.repository({
1277
+ table,
1278
+ base
1279
+ });
1280
+ if (added === void 0) continue;
1281
+ for (const [name, method] of Object.entries(added)) {
1282
+ expect(typeof method === "function", `${key}.${name} is not a function`);
1283
+ expect(!(name in base), `${key}.${name} replaces a base method`);
1284
+ }
1285
+ }
1286
+ }],
1287
+ plugin.transformQuery && ["transformQuery is pure and deterministic", async () => {
1288
+ const op = await captureOp(betterSupabase, (db) => db[table]["findMany"]({ limit: 1 }));
1289
+ const first = plugin.transformQuery(frozenCopy(op), hook(op));
1290
+ const second = plugin.transformQuery(frozenCopy(op), hook(op));
1291
+ expect(typeof first === "object" && first !== null && first.kind === op.kind, "must return an operation of the same kind");
1292
+ expect(same(first, second), "two calls with the same input differ");
1293
+ }],
1294
+ plugin.beforeMutation && create && ["beforeMutation is pure and deterministic", async () => {
1295
+ const op = await captureOp(betterSupabase, (db) => db[table]["create"](create));
1296
+ if (op.kind === "select") throw new Violation("create did not produce a mutation");
1297
+ const first = await plugin.beforeMutation(frozenCopy(op), hook(op));
1298
+ const second = await plugin.beforeMutation(frozenCopy(op), hook(op));
1299
+ expect(first.kind === op.kind, "must return a mutation of the same kind");
1300
+ expect(same(first, second), "two calls with the same input differ");
1301
+ }],
1302
+ plugin.afterMutation && create && ["afterMutation cannot change the result", async () => {
1303
+ const row = {
1304
+ marker: "conformance",
1305
+ nested: { kept: true }
1306
+ };
1307
+ const result = await betterSupabase.use(plugin).connect({
1308
+ name: "rows",
1309
+ execute: () => Promise.resolve(ok({
1310
+ rows: [structuredClone(row)],
1311
+ count: 1
1312
+ }))
1313
+ }, context)[table]["create"](create);
1314
+ expect(result.ok, "create failed");
1315
+ expect(same(result.data, row), "afterMutation changed the returned row");
1316
+ }],
1317
+ plugin.wrapExecutor && ["wrapExecutor keeps results intact", async () => {
1318
+ const op = await captureOp(betterSupabase, (db) => db[table]["findMany"]({ limit: 1 }));
1319
+ const data = {
1320
+ rows: [{ marker: "conformance" }],
1321
+ count: 1
1322
+ };
1323
+ const wrapped = plugin.wrapExecutor({
1324
+ name: "inner",
1325
+ execute: () => Promise.resolve(ok(data))
1326
+ });
1327
+ expect(typeof wrapped.name === "string", "the wrapped executor needs a name");
1328
+ const result = await wrapped.execute(op, { errorMappers: [] });
1329
+ expect(result.ok && same(result.data, data), "the wrapped executor changed the result");
1330
+ }],
1331
+ plugin.context && ["context is pure and deterministic", () => {
1332
+ const args = { schema: betterSupabase.meta };
1333
+ const first = plugin.context(frozenCopy(context), args);
1334
+ const second = plugin.context(frozenCopy(context), args);
1335
+ expect(typeof first === "object" && first !== null, "must return a context object");
1336
+ expect(same(first, second), "two calls with the same input differ");
1337
+ }],
1338
+ plugin.mapError && ["mapError returns a DbError or undefined", () => {
1339
+ const fallback = dbError("unexpected", "internal error");
1340
+ const mapped = plugin.mapError({
1341
+ code: "XX000",
1342
+ message: "internal error"
1343
+ }, fallback);
1344
+ expect(mapped === void 0 || typeof mapped === "object" && typeof mapped.kind === "string", "must return a DbError or undefined");
1345
+ }]
1346
+ ]);
1347
+ }
1348
+ //#endregion
1349
+ //#region src/testing/kits/queue-backend.ts
1350
+ function claimedJob(queue, row) {
1351
+ const instant = temporal().Instant.fromEpochMilliseconds(0);
1352
+ return {
1353
+ id: Number(row.id),
1354
+ queue,
1355
+ payload: row.message?.payload,
1356
+ attempts: row.attempts,
1357
+ maxAttempts: row.message?.max_attempts ?? 5,
1358
+ enqueuedAt: instant,
1359
+ visibleUntil: instant,
1360
+ lastError: row.message?.last_error ?? null,
1361
+ context: {}
1362
+ };
1316
1363
  }
1317
1364
  /**
1318
- * Typed fixtures shared by `supabase/seeds` and your tests. Rows use app
1319
- * casing and Insert types; `better-supabase seed` renders them to SQL.
1320
- *
1321
- * ```ts
1322
- * export const seed = defineSeed(betterSupabase, {
1323
- * organizations: { acme: { id: ACME, name: 'Acme' } },
1324
- * customers: { first: { organizationId: ACME, name: 'First customer' } },
1325
- * });
1326
- * ```
1365
+ * Proves a `QueueBackend` round-trips payloads, hides claimed messages for
1366
+ * their lease, counts attempts, retries and dead-letters on `fail`, and
1367
+ * (when it has `leases`) rejects a stale attempt and extends a lease. It
1368
+ * writes to `options.queue` on the backend's real store.
1327
1369
  */
1328
- function defineSeed(betterSupabase, fixtures) {
1329
- const meta = betterSupabase.meta;
1330
- const statements = () => {
1331
- const keys = Object.keys(fixtures).filter((key) => fixtures[key] !== void 0);
1332
- for (const key of keys) {
1333
- const table = meta.tables[key];
1334
- if (!table) throw new TypeError(`seed: unknown table "${key}"`);
1335
- if (table.kind === "view") throw new TypeError(`seed: "${key}" is a view`);
1336
- }
1337
- return order(meta, keys).flatMap((key) => {
1338
- const sql = statement(meta.tables[key], fixtures[key]);
1339
- return sql ? [sql] : [];
1340
- });
1341
- };
1342
- return {
1343
- [SEED]: true,
1344
- rows: fixtures,
1345
- statements,
1346
- sql: () => statements().join("\n\n"),
1347
- async insert(sql) {
1348
- for (const text of statements()) await sql.queryRaw(text);
1349
- }
1370
+ function testQueueBackend(backend, options) {
1371
+ const { queue } = options;
1372
+ const claimOne = async () => {
1373
+ const [row] = await backend.read(queue, 30, 1);
1374
+ return row && claimedJob(queue, row);
1350
1375
  };
1376
+ return conform(`QueueBackend "${backend.name}"`, [
1377
+ hasName(backend),
1378
+ ["has apiVersion 1", () => {
1379
+ const version = backend.apiVersion;
1380
+ expect(version === 1, "apiVersion must be 1");
1381
+ }],
1382
+ ["round-trips a payload and leases it", async () => {
1383
+ const id = await backend.send(queue, { n: 1 }, 0, 3, void 0);
1384
+ expect(Number.isFinite(id), "send must return a numeric id");
1385
+ const job = await claimOne();
1386
+ expect(job?.id === id, "read must return the sent message");
1387
+ expect(same(job.payload, { n: 1 }), "the payload must round-trip");
1388
+ expect(job.attempts === 1, "the first claim must be attempt 1");
1389
+ expect(job.maxAttempts === 3, "max_attempts must round-trip");
1390
+ expect(await claimOne() === void 0, "a leased message must stay hidden from other reads");
1391
+ expect(await backend.complete(job), "complete must return true");
1392
+ }],
1393
+ backend.leases && ["rejects a stale attempt and extends a lease", async () => {
1394
+ await backend.send(queue, { n: 2 }, 0, 3, void 0);
1395
+ const job = await claimOne();
1396
+ expect(job, "read must return the sent message");
1397
+ expect(!await backend.complete({
1398
+ ...job,
1399
+ attempts: job.attempts + 1
1400
+ }), "complete with another attempt must return false");
1401
+ expect(await backend.extend(job, 60), "extend must return true");
1402
+ expect(await backend.complete(job), "complete must return true");
1403
+ }],
1404
+ ["retries, then dead-letters at max attempts", async () => {
1405
+ await backend.send(queue, { n: 3 }, 0, 2, void 0);
1406
+ const first = await claimOne();
1407
+ expect(first, "read must return the sent message");
1408
+ expect(await backend.fail(first, "boom", 0) === "queued", "fail before max_attempts must return queued");
1409
+ const second = await claimOne();
1410
+ expect(second?.attempts === 2, "a retried message must be attempt 2");
1411
+ expect(await backend.fail(second, "boom", 0) === "dead", "fail at max_attempts must return dead");
1412
+ expect(await claimOne() === void 0, "a dead message must not be claimed again");
1413
+ }],
1414
+ backend.stats && backend.listDead && ["counts and lists dead letters", async () => {
1415
+ expect(((await backend.stats?.(queue))?.dead ?? 0) >= 1, "stats must count the dead letter");
1416
+ const [dead] = await backend.listDead?.(queue, 10, void 0) ?? [];
1417
+ expect(dead, "listDead must return the dead letter");
1418
+ expect(same(dead.message?.payload, { n: 3 }), "listDead must return the newest dead letter first");
1419
+ if (backend.retryDead) {
1420
+ expect(await backend.retryDead(queue, [Number(dead.id)], 10) === 1, "retryDead must enqueue the dead letter again");
1421
+ const job = await claimOne();
1422
+ expect(job, "the retried job must be claimable");
1423
+ expect(same(job.payload, { n: 3 }), "the retried job must keep its payload");
1424
+ await backend.complete(job);
1425
+ }
1426
+ }],
1427
+ (options.dedupe ?? backend.leases) && ["returns the first id for a repeated dedupe key", async () => {
1428
+ expect(await backend.send(queue, { n: 4 }, 0, 3, "same") === await backend.send(queue, { n: 4 }, 0, 3, "same"), "a waiting dedupe key must return its id");
1429
+ const job = await claimOne();
1430
+ expect(job, "read must return the sent message");
1431
+ await backend.complete(job);
1432
+ }]
1433
+ ]);
1434
+ }
1435
+ //#endregion
1436
+ //#region src/testing/kits/support-session-store.ts
1437
+ /**
1438
+ * Proves a `SupportSessionStore` starts sessions with the input it was
1439
+ * given, shows a running session only to its admin, ends the admin's
1440
+ * previous session on start, ends a session once, lists sessions and, with
1441
+ * `expire`, leaves running sessions alone.
1442
+ * It writes sessions to the store; they are all ended when it returns.
1443
+ */
1444
+ function testSupportSessionStore(store, options) {
1445
+ const { admin } = options;
1446
+ const [first, second] = options.targets;
1447
+ const start = (targetUserId) => store.start({
1448
+ adminId: admin.id,
1449
+ adminClaims: admin.claims,
1450
+ targetUserId,
1451
+ reason: "conformance",
1452
+ ttlSeconds: 600,
1453
+ readOnly: true,
1454
+ metadata: { ticket: "T-1" }
1455
+ });
1456
+ return conform(`SupportSessionStore "${store.name}"`, [
1457
+ hasName(store),
1458
+ ["has apiVersion 1", () => {
1459
+ const version = store.apiVersion;
1460
+ expect(version === 1, "apiVersion must be 1");
1461
+ }],
1462
+ ["starts a session with its input and shows it to its admin only", async () => {
1463
+ const session = await start(first);
1464
+ try {
1465
+ expect(session.id.length > 0, "start must return an id");
1466
+ expect(session.adminId === admin.id, "adminId must round-trip");
1467
+ expect(session.targetUserId === first, "targetUserId must round-trip");
1468
+ expect(session.reason === "conformance", "reason must round-trip");
1469
+ expect(session.readOnly, "readOnly must round-trip");
1470
+ expect(same(session.metadata, { ticket: "T-1" }), "metadata must round-trip");
1471
+ const seconds = (session.expiresAt.epochMilliseconds - session.startedAt.epochMilliseconds) / 1e3;
1472
+ expect(Math.abs(seconds - 600) < 5, "expiresAt must be ttlSeconds after startedAt");
1473
+ expect((await store.get(session.id, admin.id))?.id === session.id, "get must return the running session");
1474
+ expect(await store.get(session.id, first) === void 0, "get must hide a session from anyone but its admin");
1475
+ } finally {
1476
+ await store.end(session.id, "admin");
1477
+ }
1478
+ }],
1479
+ ["ends the admin's previous session on start", async () => {
1480
+ const older = await start(first);
1481
+ const newer = await start(second);
1482
+ try {
1483
+ expect(await store.get(older.id, admin.id) === void 0, "starting a session must end the admin's previous one");
1484
+ expect((await store.get(newer.id, admin.id))?.id === newer.id, "the new session must be running");
1485
+ } finally {
1486
+ await store.end(newer.id, "admin");
1487
+ }
1488
+ }],
1489
+ ["ends a session once and lists it", async () => {
1490
+ const session = await start(first);
1491
+ expect(await store.end(session.id, "admin"), "end must return true");
1492
+ expect(!await store.end(session.id, "admin"), "ending an ended session must return false");
1493
+ expect(await store.get(session.id, admin.id) === void 0, "get must not return an ended session");
1494
+ const entry = (await store.list({
1495
+ adminId: admin.id,
1496
+ active: false
1497
+ })).find((item) => item.id === session.id);
1498
+ expect(entry, "list must include the ended session");
1499
+ expect(entry.endedAt !== void 0, "an ended session has endedAt");
1500
+ expect((await store.list({
1501
+ adminId: admin.id,
1502
+ active: true
1503
+ })).every((item) => item.id !== session.id), "list({ active: true }) must leave out ended sessions");
1504
+ }],
1505
+ ["expires only sessions past their expiry, when it has expire", async () => {
1506
+ if (!store.expire) return;
1507
+ const session = await start(first);
1508
+ try {
1509
+ const expired = await store.expire();
1510
+ expect(expired.every((item) => item.id !== session.id), "expire must leave a running session alone");
1511
+ expect(expired.every((item) => item.endedBy === "expired"), "expire must end sessions with endedBy expired");
1512
+ expect((await store.get(session.id, admin.id))?.id === session.id, "the running session must still be running after expire");
1513
+ } finally {
1514
+ await store.end(session.id, "admin");
1515
+ }
1516
+ }]
1517
+ ]);
1351
1518
  }
1352
1519
  //#endregion
1353
1520
  //#region src/testing/adapter.ts
@@ -1484,13 +1651,13 @@ function testBlockTransportMiddleware(middleware) {
1484
1651
  }
1485
1652
  //#endregion
1486
1653
  //#region src/testing/authorization-provider.ts
1487
- function isPlainJson(value) {
1654
+ function isPlainJson$1(value) {
1488
1655
  if (value === null || typeof value === "string" || typeof value === "boolean") return true;
1489
1656
  if (typeof value === "number") return Number.isFinite(value);
1490
- if (Array.isArray(value)) return value.every(isPlainJson);
1657
+ if (Array.isArray(value)) return value.every(isPlainJson$1);
1491
1658
  if (typeof value !== "object") return false;
1492
1659
  const prototype = Object.getPrototypeOf(value);
1493
- return (prototype === Object.prototype || prototype === null) && Object.values(value).every((entry) => entry === void 0 || isPlainJson(entry));
1660
+ return (prototype === Object.prototype || prototype === null) && Object.values(value).every((entry) => entry === void 0 || isPlainJson$1(entry));
1494
1661
  }
1495
1662
  const QUALIFIED = /^[a-z_][\w$]*\.[a-z_][\w${}]*$/i;
1496
1663
  const TABLE = /^[a-z_][\w$]*\.[a-z_][\w$]*$/i;
@@ -1520,7 +1687,7 @@ function testAuthorizationProvider(provider) {
1520
1687
  expect(provider.apiVersion === 1, `apiVersion is ${String(provider.apiVersion)}`);
1521
1688
  }],
1522
1689
  ["is plain JSON", () => {
1523
- expect(isPlainJson(provider), "it doesn't survive a JSON round trip");
1690
+ expect(isPlainJson$1(provider), "it doesn't survive a JSON round trip");
1524
1691
  }],
1525
1692
  ["names valid, unique scopes", () => {
1526
1693
  expect(scopes.size > 0, "scopes is empty");
@@ -1603,6 +1770,387 @@ function testAuthorizationProvider(provider) {
1603
1770
  ]);
1604
1771
  }
1605
1772
  //#endregion
1773
+ //#region src/testing/authorizer.ts
1774
+ const OUTCOMES = /* @__PURE__ */ new Set([
1775
+ "granted",
1776
+ "denied",
1777
+ "approval-required"
1778
+ ]);
1779
+ const isOutcome = (value) => typeof value === "object" && value !== null && "outcome" in value && typeof value.outcome === "string" && OUTCOMES.has(value.outcome);
1780
+ /** Input that claims a privileged caller; the subject must decide, never this. */
1781
+ const FORGED = {
1782
+ subject: {
1783
+ type: "service",
1784
+ id: "service"
1785
+ },
1786
+ actor: {
1787
+ id: "service",
1788
+ kind: "service",
1789
+ role: "service_role"
1790
+ },
1791
+ role: "service_role",
1792
+ user_id: "00000000-0000-4000-8000-000000000000",
1793
+ memberships: [{
1794
+ tenant: "conformance",
1795
+ role: "owner"
1796
+ }],
1797
+ claims: {
1798
+ role: "service_role",
1799
+ user_role: "admin",
1800
+ roles: ["admin"],
1801
+ memberships: [{
1802
+ tenant: "conformance",
1803
+ role: "owner"
1804
+ }]
1805
+ }
1806
+ };
1807
+ /**
1808
+ * Proves an `Authorizer` targets API v1, keys every permission to the same
1809
+ * string each time (and after a JSON round trip, so references are plain
1810
+ * JSON), answers every request with a known outcome without mutating it,
1811
+ * keeps the request order in `evaluations`, decides from the subject rather
1812
+ * than the input (forged roles, claims and memberships in the resource
1813
+ * properties or the context never turn a denial into a grant), never grants an unknown permission, and grants
1814
+ * `options.granted` through `checkPermission`.
1815
+ */
1816
+ function testAuthorizer(authorizer, options) {
1817
+ const { subject, context } = requestOf(void 0);
1818
+ const requestFor = (action, id, properties) => ({
1819
+ subject,
1820
+ action,
1821
+ resource: {
1822
+ type: "conformance",
1823
+ id,
1824
+ ...properties ? { properties } : {}
1825
+ },
1826
+ context
1827
+ });
1828
+ const requests = options.permissions.flatMap((permission) => [
1829
+ "a",
1830
+ "b",
1831
+ "c"
1832
+ ].map((id) => requestFor(permission, id)));
1833
+ return conform(`Authorizer "${authorizer.name}"`, [
1834
+ hasName(authorizer),
1835
+ ["targets API v1", () => {
1836
+ const version = authorizer.apiVersion;
1837
+ expect(version === 1, `apiVersion is ${String(version)}`);
1838
+ expect(options.permissions.length > 0, "options.permissions lists no permission to check");
1839
+ }],
1840
+ ["keys every permission stably, as plain JSON", () => {
1841
+ for (const permission of options.permissions) {
1842
+ const key = authorizer.key(permission);
1843
+ expect(typeof key === "string" && key.length > 0, "key() must return a non-empty string");
1844
+ expect(authorizer.key(permission) === key, `key() gave two keys for ${key}`);
1845
+ const copy = JSON.parse(JSON.stringify(permission));
1846
+ expect(same(copy, permission) && authorizer.key(copy) === key, `the reference for ${key} is not plain JSON`);
1847
+ }
1848
+ }],
1849
+ ["answers every request with a known outcome, without mutating it", async () => {
1850
+ for (const request of requests) {
1851
+ const outcome = await authorizer.evaluate(frozenCopy(request));
1852
+ expect(isOutcome(outcome), `evaluate() gave no known outcome for ${authorizer.key(request.action)}`);
1853
+ }
1854
+ }],
1855
+ authorizer.evaluations !== void 0 && ["keeps the request order in evaluations", async () => {
1856
+ const evaluations = authorizer.evaluations.bind(authorizer);
1857
+ const single = await Promise.all(requests.map((request) => Promise.resolve(authorizer.evaluate(request))));
1858
+ const batch = await evaluations(frozenCopy(requests));
1859
+ expect(batch.length === requests.length, `evaluations() gave ${batch.length} outcomes for ${requests.length} requests`);
1860
+ expect(same(batch.map((outcome) => outcome.outcome), single.map((outcome) => outcome.outcome)), "evaluations() disagrees with evaluate() for the same requests");
1861
+ const reversed = await evaluations(frozenCopy(requests.toReversed()));
1862
+ expect(same(reversed.toReversed().map((outcome) => outcome.outcome), batch.map((outcome) => outcome.outcome)), "evaluations() does not keep the request order");
1863
+ }],
1864
+ ["decides from the subject, never from the input", async () => {
1865
+ for (const permission of options.permissions) {
1866
+ const plain = await authorizer.evaluate(requestFor(permission, "a"));
1867
+ const forgedResource = await authorizer.evaluate(requestFor(permission, "a", FORGED));
1868
+ const forgedContext = await authorizer.evaluate({
1869
+ ...requestFor(permission, "a"),
1870
+ context: {
1871
+ ...context,
1872
+ ...FORGED
1873
+ }
1874
+ });
1875
+ for (const forged of [forgedResource, forgedContext]) expect(forged.outcome !== "granted" || plain.outcome === "granted", `input claiming a service caller was granted ${authorizer.key(permission)}`);
1876
+ }
1877
+ }],
1878
+ options.unknown !== void 0 && ["never grants an unknown permission", async () => {
1879
+ expect(!(await checkPermission({
1880
+ authorizer,
1881
+ permission: options.unknown,
1882
+ resource: { type: "conformance" },
1883
+ context: options.granted?.context
1884
+ })).ok, "an unknown permission was granted");
1885
+ }],
1886
+ options.granted !== void 0 && ["grants the granted permission through checkPermission", async () => {
1887
+ const checked = await checkPermission({
1888
+ authorizer,
1889
+ permission: options.granted.permission,
1890
+ resource: { type: "conformance" },
1891
+ context: options.granted.context
1892
+ });
1893
+ expect(checked.ok, checked.ok ? "" : `the permission was refused: ${checked.error.message}`);
1894
+ }]
1895
+ ]);
1896
+ }
1897
+ /**
1898
+ * An in-memory `Authorizer` for tests and examples: permissions come from
1899
+ * the subject's `role` and id only, never from the request's resource or
1900
+ * context, and it ignores `subject.properties.claims`.
1901
+ */
1902
+ function staticAuthorizer(options) {
1903
+ const approvals = new Set(options.approvals ?? []);
1904
+ const evaluate = (request) => {
1905
+ const { subject, action } = request;
1906
+ if (approvals.has(action)) return {
1907
+ outcome: "approval-required",
1908
+ approval: { id: `approval:${action}` }
1909
+ };
1910
+ const role = subject.properties?.["role"];
1911
+ return [...options.roles?.[typeof role === "string" ? role : subject.type] ?? [], ...options.grants?.[subject.id] ?? []].includes(action) ? { outcome: "granted" } : {
1912
+ outcome: "denied",
1913
+ reason: `Permission ${action} is required`
1914
+ };
1915
+ };
1916
+ return defineAuthorizer({
1917
+ name: "static",
1918
+ key: (ref) => ref,
1919
+ evaluate,
1920
+ evaluations: (requests) => Promise.resolve(requests.map(evaluate))
1921
+ });
1922
+ }
1923
+ //#endregion
1924
+ //#region src/testing/document-format.ts
1925
+ const STATUSES$1 = /* @__PURE__ */ new Set([
1926
+ "stable",
1927
+ "legacy",
1928
+ "preview"
1929
+ ]);
1930
+ const SEVERITIES = /* @__PURE__ */ new Set([
1931
+ "error",
1932
+ "warning",
1933
+ "info"
1934
+ ]);
1935
+ const CODE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1936
+ const jsonSchema = (schema) => ({ "~standard": {
1937
+ version: 1,
1938
+ vendor: "better-supabase-conformance",
1939
+ validate: (value) => ({ value }),
1940
+ jsonSchema: {
1941
+ input: () => schema,
1942
+ output: () => schema
1943
+ }
1944
+ } });
1945
+ function sampleModel() {
1946
+ const customer = {
1947
+ type: "object",
1948
+ required: ["id", "name"],
1949
+ properties: {
1950
+ id: {
1951
+ type: "string",
1952
+ format: "uuid"
1953
+ },
1954
+ name: { type: "string" },
1955
+ note: { type: ["string", "null"] }
1956
+ }
1957
+ };
1958
+ return buildApiModel({ meta: {
1959
+ version: 1,
1960
+ casing: "camel",
1961
+ tables: {},
1962
+ enums: {},
1963
+ functions: {}
1964
+ } }, {
1965
+ info: {
1966
+ title: "Conformance",
1967
+ version: "1.0.0"
1968
+ },
1969
+ servers: [{
1970
+ url: "https://api.test",
1971
+ name: "test"
1972
+ }],
1973
+ tags: [{
1974
+ name: "customers",
1975
+ summary: "Customers"
1976
+ }],
1977
+ basePath: "/api",
1978
+ routes: [
1979
+ {
1980
+ method: "get",
1981
+ path: "/customers/{id}",
1982
+ operationId: "getCustomer",
1983
+ tags: ["customers"],
1984
+ output: jsonSchema(customer)
1985
+ },
1986
+ {
1987
+ method: "post",
1988
+ path: "/customers",
1989
+ operationId: "createCustomer",
1990
+ tags: ["customers"],
1991
+ input: jsonSchema(customer),
1992
+ output: jsonSchema(customer),
1993
+ status: 201
1994
+ },
1995
+ {
1996
+ method: "get",
1997
+ path: "/customers/changes",
1998
+ operationId: "streamCustomerChanges",
1999
+ stream: { item: jsonSchema(customer) }
2000
+ }
2001
+ ],
2002
+ webhooks: [{
2003
+ id: "customer.created",
2004
+ payload: customer,
2005
+ standardWebhooks: true
2006
+ }],
2007
+ realtimeUrl: "wss://abc.supabase.co/realtime/v1/websocket",
2008
+ messages: { customerChanged: {
2009
+ id: "customerChanged",
2010
+ payload: customer
2011
+ } },
2012
+ channels: [{
2013
+ id: "customers",
2014
+ address: "bs:t:public.customers",
2015
+ protocol: "ws",
2016
+ messages: ["customerChanged"],
2017
+ operations: [{
2018
+ id: "onCustomerChanged",
2019
+ action: "receive"
2020
+ }]
2021
+ }],
2022
+ workflows: [{
2023
+ id: "createThenGet",
2024
+ steps: [{
2025
+ id: "create",
2026
+ operationId: "createCustomer",
2027
+ outputs: { id: "$response.body#/id" }
2028
+ }, {
2029
+ id: "get",
2030
+ operationId: "getCustomer",
2031
+ parameters: [{
2032
+ name: "id",
2033
+ in: "path",
2034
+ value: "$steps.create.outputs.id"
2035
+ }]
2036
+ }]
2037
+ }],
2038
+ security: ["bearer"]
2039
+ }).model;
2040
+ }
2041
+ function isPlainJson(value) {
2042
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
2043
+ if (typeof value === "number") return Number.isFinite(value);
2044
+ if (Array.isArray(value)) return value.every(isPlainJson);
2045
+ if (typeof value !== "object") return false;
2046
+ const prototype = Object.getPrototypeOf(value);
2047
+ if (prototype !== Object.prototype && prototype !== null) return false;
2048
+ return Object.values(value).every((entry) => entry !== void 0 && isPlainJson(entry));
2049
+ }
2050
+ function diagnosticProblem(diagnostic) {
2051
+ if (typeof diagnostic.code !== "string" || !CODE.test(diagnostic.code)) return `the code ${JSON.stringify(diagnostic.code)} is not kebab-case`;
2052
+ const severity = diagnostic.severity;
2053
+ if (typeof severity !== "string" || !SEVERITIES.has(severity)) return `${diagnostic.code} has the severity ${String(severity)}`;
2054
+ if (typeof diagnostic.message !== "string" || diagnostic.message === "") return `${diagnostic.code} has no message`;
2055
+ if (diagnostic.pointer !== void 0 && !/^(?:\/|$)/.test(diagnostic.pointer)) return `${diagnostic.code} has the pointer ${diagnostic.pointer}, which is not a JSON Pointer`;
2056
+ }
2057
+ /**
2058
+ * Proves a `DocumentFormat` targets API v1, lists versions with pins and a
2059
+ * known status, defaults to a version it lists that is not a preview, and,
2060
+ * for every version: lowers the model without mutating it (also when the
2061
+ * model is frozen), renders the same document twice, renders plain JSON or
2062
+ * text, reports diagnostics with a kebab-case code, a severity and a
2063
+ * message, finds no error in its own output with `lint`, and names a bare,
2064
+ * stable file.
2065
+ */
2066
+ function testDocumentFormat(format, options = {}) {
2067
+ const subject = format;
2068
+ const id = subject.id;
2069
+ const defaultVersion = subject.defaultVersion;
2070
+ const versions = options.versions ?? Object.keys(subject.versions);
2071
+ let model;
2072
+ const modelOf = () => model ??= options.model ?? sampleModel();
2073
+ const lower = (version, input) => {
2074
+ const diagnostics = [];
2075
+ return {
2076
+ document: subject.lower(input, version, { report: (diagnostic) => {
2077
+ diagnostics.push(diagnostic);
2078
+ } }),
2079
+ diagnostics
2080
+ };
2081
+ };
2082
+ return conform(`DocumentFormat "${String(id)}"`, [
2083
+ ["targets API v1 with an id", () => {
2084
+ const version = subject.apiVersion;
2085
+ expect(version === 1, `apiVersion is ${String(version)}`);
2086
+ expect(typeof id === "string" && id.length > 0, "id must be a non-empty string");
2087
+ }],
2088
+ ["lists versions with a pin and a known status", () => {
2089
+ const listed = Object.entries(subject.versions);
2090
+ expect(listed.length > 0, "versions lists no version");
2091
+ for (const [version, entry] of listed) {
2092
+ const status = entry.status;
2093
+ expect(typeof entry.pin === "string" && entry.pin.length > 0, `version ${version} has no pin`);
2094
+ expect(typeof status === "string" && STATUSES$1.has(status), `version ${version} has the status ${String(status)}`);
2095
+ }
2096
+ for (const version of versions) expect(Object.hasOwn(subject.versions, version), `options.versions names ${version}, which the format does not list`);
2097
+ }],
2098
+ ["defaults to a listed version that is not a preview", () => {
2099
+ const entry = Object.hasOwn(subject.versions, subject.defaultVersion) ? subject.versions[subject.defaultVersion] : void 0;
2100
+ expect(entry !== void 0, `the default version ${String(defaultVersion)} is not in versions`);
2101
+ expect(entry.status !== "preview", `the default version ${String(defaultVersion)} is a preview`);
2102
+ }],
2103
+ ["lowers every version without mutating the model", () => {
2104
+ const original = modelOf();
2105
+ const before = JSON.stringify(original);
2106
+ for (const version of versions) {
2107
+ lower(version, original);
2108
+ expect(JSON.stringify(original) === before, `lower(model, "${version}") changed the model`);
2109
+ lower(version, frozenCopy(original));
2110
+ }
2111
+ }],
2112
+ ["renders the same document twice", () => {
2113
+ for (const version of versions) {
2114
+ const first = lower(version, structuredClone(modelOf()));
2115
+ const second = lower(version, structuredClone(modelOf()));
2116
+ expect(same(first.document, second.document), `lower(model, "${version}") is not deterministic`);
2117
+ expect(same(first.diagnostics, second.diagnostics), `lower(model, "${version}") reports different diagnostics each time`);
2118
+ }
2119
+ }],
2120
+ ["renders plain JSON or text", () => {
2121
+ for (const version of versions) {
2122
+ const { document } = lower(version, modelOf());
2123
+ expect(typeof document === "string" || typeof document === "object" && document !== null && isPlainJson(document), `lower(model, "${version}") returned something other than a JSON object or a string`);
2124
+ }
2125
+ }],
2126
+ ["reports diagnostics with a code, a severity and a message", () => {
2127
+ for (const version of versions) {
2128
+ const { document, diagnostics } = lower(version, modelOf());
2129
+ const linted = subject.lint?.(document, version) ?? [];
2130
+ for (const diagnostic of [...diagnostics, ...linted]) {
2131
+ const problem = diagnosticProblem(diagnostic);
2132
+ expect(problem === void 0, `${version}: ${problem ?? ""}`);
2133
+ }
2134
+ }
2135
+ }],
2136
+ subject.lint !== void 0 && ["finds no error in its own output", () => {
2137
+ for (const version of versions) {
2138
+ const { document, diagnostics } = lower(version, modelOf());
2139
+ if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) continue;
2140
+ const errors = (subject.lint?.(document, version) ?? []).filter((diagnostic) => diagnostic.severity === "error");
2141
+ expect(errors.length === 0, `lint(lower(model, "${version}")) reports ${errors.map((error) => error.code).join(", ")}`);
2142
+ }
2143
+ }],
2144
+ ["names a bare, stable file per version", () => {
2145
+ for (const version of versions) {
2146
+ const name = subject.fileName(version);
2147
+ expect(typeof name === "string" && /^[^/\\]+\.[A-Za-z0-9]+$/.test(name), `fileName("${version}") is ${JSON.stringify(name)}, not a file name with an extension`);
2148
+ expect(subject.fileName(version) === name, `fileName("${version}") changes between calls`);
2149
+ }
2150
+ }]
2151
+ ]);
2152
+ }
2153
+ //#endregion
1606
2154
  //#region src/testing/generator.ts
1607
2155
  function relativeImport(from, to) {
1608
2156
  const fromParts = from.split("/").slice(0, -1);
@@ -2350,4 +2898,4 @@ function testCredentialProvider(provider, options) {
2350
2898
  ]);
2351
2899
  }
2352
2900
  //#endregion
2353
- export { ConformanceError, LOCAL_JWT_SECRET, asUser, createTestSigner, defineSeed, expectDbBudget, expectInstant, expectTenantIsolation, isSeed, localAuth, localSigningKey, signLocalJwt, signTestJwt, signTestJwtWithKey, supabaseClaimFixtures, testAdapter, testAiTaskRunner, testAuthResolver, testAuthorizationProvider, testBlockTransportMiddleware, testBuilderStarter, testCacheAdapter, testChatState, testCredentialProvider, testEmbedder, testEveDocumentBackend, testEventSink, testExecutor, testGenerator, testGraphCompiler, testNotificationChannel, testPlugin, testQueueBackend, testStreamStore, testSupportSessionStore, testWebhookSecretStore, testWebhookSigner, testWebhookTransport };
2901
+ export { ConformanceError, LOCAL_JWT_SECRET, asUser, createTestSigner, defineSeed, expectDbBudget, expectInstant, expectTenantIsolation, isSeed, liteStack, localAuth, localSigningKey, signLocalJwt, signTestJwt, signTestJwtWithKey, staticAuthorizer, supabaseClaimFixtures, testAdapter, testAiTaskRunner, testAuthResolver, testAuthorizationProvider, testAuthorizer, testBlockTransportMiddleware, testBuilderStarter, testCacheAdapter, testChatState, testCredentialProvider, testDocumentFormat, testEmbedder, testEveDocumentBackend, testEventSink, testExecutor, testGenerator, testGraphCompiler, testNotificationChannel, testPlugin, testQueueBackend, testStreamStore, testSupportSessionStore, testWebhookSecretStore, testWebhookSigner, testWebhookTransport };