better-supabase 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +2 -0
  2. package/dist/channel-FQW-CnGR.d.ts +70 -0
  3. package/dist/cli/bin.js +1 -1
  4. package/dist/cli/codemod-DqV7612m.js +388 -0
  5. package/dist/cli/{config-D9R0o1LH.js → config-CPVpXP5I.js} +78 -8
  6. package/dist/cli/{config-SSKSTUl2.js → config-dtoekM0v.js} +1 -1
  7. package/dist/cli/{read-sets-CMa29t9a.js → diff-SjkO5rNr.js} +1 -220
  8. package/dist/cli/{dist-CFgGknmZ.js → dist-DBPdlPYK.js} +4 -4
  9. package/dist/cli/{dist-B1MvLv-M.js → dist-DreZ24Sz.js} +1 -1
  10. package/dist/cli/{dist-Du7VnEUG.js → dist-xn4wlboc.js} +112 -1
  11. package/dist/cli/{doctor-BOym3mbL.js → doctor-L7Z8mYTx.js} +531 -50
  12. package/dist/cli/{env-BZ0uy8XK.js → env-DmKv_G4A.js} +1 -1
  13. package/dist/cli/gen-BJc9vdyu.js +2 -0
  14. package/dist/cli/{gen-Ccn_p90L.js → gen-C2osRcr5.js} +283 -87
  15. package/dist/cli/index.d.ts +1 -1
  16. package/dist/cli/index.js +4 -4
  17. package/dist/cli/{init-ClnxRYBF.js → init-Saf5-R9T.js} +2 -2
  18. package/dist/cli/{introspect-DxqfkVIv.js → introspect--H7oGmcd.js} +2 -2
  19. package/dist/cli/{openapi-8WHjxiTJ.js → openapi-Cp5Fyta-.js} +1 -1
  20. package/dist/cli/read-sets-BfMQcKy8.js +289 -0
  21. package/dist/cli/{run-CP20LKk_.js → run-BKR2W_79.js} +16 -12
  22. package/dist/cli/{seed-5J6eu_Xg.js → seed-CUlqWh-8.js} +1 -1
  23. package/dist/cli/sql-DzPbfngx.js +348 -0
  24. package/dist/cli/{version-5mW_8sY1.js → version-BDJuO0UR.js} +1 -1
  25. package/dist/client/index.d.ts +1 -1
  26. package/dist/client/index.js +3 -10
  27. package/dist/clone-BfgmLU90.js +21 -0
  28. package/dist/{compiler-DB3aSFOA.d.ts → compiler-BXH_aZ7N.d.ts} +2 -2
  29. package/dist/config/index.d.ts +4 -4
  30. package/dist/config/index.js +4 -4
  31. package/dist/{config-CbvFmGD8.js → config-DreERlKo.js} +1 -1
  32. package/dist/{config-C1CYcm6o.d.ts → config-T_ejwyd3.d.ts} +176 -11
  33. package/dist/{define-CRS2MRV3.d.ts → define-Bp_BZa5h.d.ts} +11 -7
  34. package/dist/{define-Bh0AqTik.js → define-cNKzqwL_.js} +202 -16
  35. package/dist/edge/index.d.ts +16 -5
  36. package/dist/edge/index.js +10 -7
  37. package/dist/{entitlements-Dm-FHpNH.d.ts → entitlements-DUxU4oHe.d.ts} +1 -1
  38. package/dist/env/index.d.ts +2 -2
  39. package/dist/env/index.js +34 -1
  40. package/dist/{errors-L0xK41y_.js → errors-DtwXa6b8.js} +1 -0
  41. package/dist/events/index.d.ts +3 -2
  42. package/dist/events/index.js +65 -12
  43. package/dist/events-aIO7vsIk.d.ts +230 -0
  44. package/dist/{result-CqEsbgYs.d.ts → executor-BunPOO-k.d.ts} +200 -1
  45. package/dist/{executor-CZ7ImRiD.d.ts → executor-Cghdstwu.d.ts} +1 -2
  46. package/dist/{executor-CVt6dD0B.js → executor-XycESQPH.js} +3 -3
  47. package/dist/hono/index.d.ts +17 -6
  48. package/dist/hono/index.js +40 -5
  49. package/dist/impersonation-VfaGc4Q_.js +41 -0
  50. package/dist/{index-BBdvo-5d.d.ts → index-BGdgBPdf.d.ts} +49 -6
  51. package/dist/{index-BhHEC04r.d.ts → index-C7f8cOBM.d.ts} +2 -2
  52. package/dist/{index-Dy6kuF0P.d.ts → index-CLg87OBW.d.ts} +6 -1
  53. package/dist/{index-D5IfUIDh.d.ts → index-DOESKG27.d.ts} +33 -8
  54. package/dist/{index-DFmOpBGC.d.ts → index-DgILJRXj.d.ts} +35 -9
  55. package/dist/{index-RlLXd8dL.d.ts → index-DmXSxbLi.d.ts} +4 -4
  56. package/dist/{index-Cf-D2jk1.d.ts → index-Dxn2a5cw.d.ts} +2 -2
  57. package/dist/{index-528Ugmyp.d.ts → index-SaTsEfGP.d.ts} +76 -8
  58. package/dist/{index-BAy4tsAP.d.ts → index-fag8VJZ2.d.ts} +3 -4
  59. package/dist/index.d.ts +30 -13
  60. package/dist/index.js +10 -9
  61. package/dist/jobs/index.d.ts +147 -101
  62. package/dist/jobs/index.js +744 -237
  63. package/dist/{json-schema-DP6V7cbr.js → json-schema-m1fiwKgN.js} +22 -1
  64. package/dist/kit-events-CTFDfSwb.js +60 -0
  65. package/dist/kit-transport-B82k1djG.d.ts +33 -0
  66. package/dist/kit-transport-DJlHQUBU.js +55 -0
  67. package/dist/{config-BpxDfnbg.js → kits-CGLLrNPd.js} +37 -9
  68. package/dist/list/index.d.ts +1 -1
  69. package/dist/{live-BHakSWWo.d.ts → live-D9W2M5aO.d.ts} +6 -5
  70. package/dist/mcp/index.d.ts +25 -8
  71. package/dist/mcp/index.js +23 -14
  72. package/dist/{mfa-C7MNQ1Ob.d.ts → mfa-CusDgPaD.d.ts} +5 -1
  73. package/dist/next/index.d.ts +31 -10
  74. package/dist/next/index.js +136 -27
  75. package/dist/notifications/index.d.ts +162 -0
  76. package/dist/notifications/index.js +264 -0
  77. package/dist/openapi/index.d.ts +1 -1
  78. package/dist/openapi/index.js +2 -2
  79. package/dist/orgs/index.d.ts +132 -0
  80. package/dist/orgs/index.js +245 -0
  81. package/dist/orpc/index.d.ts +28 -8
  82. package/dist/orpc/index.js +16 -3
  83. package/dist/otel/index.d.ts +21 -2
  84. package/dist/otel/index.js +50 -9
  85. package/dist/permdock-sql-C2Inc6Xc.d.ts +29 -0
  86. package/dist/permdock-sql-DgOfeixi.js +51 -0
  87. package/dist/plugin-DwARMuQp.js +59 -0
  88. package/dist/{plugin-bNhvJwRh.d.ts → plugin-o144f322.d.ts} +17 -5
  89. package/dist/plugins/actor/index.d.ts +5 -2
  90. package/dist/plugins/actor/index.js +22 -8
  91. package/dist/plugins/rules/index.d.ts +2 -2
  92. package/dist/plugins/rules/index.js +15 -10
  93. package/dist/plugins/soft-delete/index.d.ts +3 -3
  94. package/dist/plugins/soft-delete/index.js +14 -5
  95. package/dist/plugins/tenant/index.d.ts +2 -2
  96. package/dist/plugins/tenant/index.js +38 -20
  97. package/dist/plugins/timestamps/index.d.ts +1 -1
  98. package/dist/plugins/timestamps/index.js +4 -3
  99. package/dist/plugins/validation/index.d.ts +2 -2
  100. package/dist/plugins/validation/index.js +26 -6
  101. package/dist/{pool-DFwEQwmN.d.ts → pool-C9voM4Kw.d.ts} +17 -6
  102. package/dist/postgres/index.d.ts +5 -5
  103. package/dist/postgres/index.js +20 -11
  104. package/dist/{postgrest-Bc-0Xw62.d.ts → postgrest-CruYKAvV.d.ts} +1 -1
  105. package/dist/{problem-BihjPR0t.js → problem-BW1KhzSG.js} +2 -1
  106. package/dist/{problem-U8UEKccv.d.ts → problem-Blxe8S31.d.ts} +1 -1
  107. package/dist/query/index.d.ts +2 -2
  108. package/dist/query/index.js +3 -3
  109. package/dist/{query-BMMSWbI7.js → query-CCbo44lG.js} +2 -2
  110. package/dist/queue-BYUCjr59.d.ts +252 -0
  111. package/dist/react/index.d.ts +8 -7
  112. package/dist/react/index.js +4 -3
  113. package/dist/react/server.d.ts +14 -3
  114. package/dist/react/server.js +10 -2
  115. package/dist/react/session.d.ts +7 -1
  116. package/dist/react/session.js +9 -1
  117. package/dist/{react-CQHMaESR.js → react-C_s7kiNz.js} +103 -11
  118. package/dist/{read-set-FWK8qQ58.js → read-set-DQFt1N_Y.js} +22 -1
  119. package/dist/realtime/index.d.ts +3 -3
  120. package/dist/realtime/index.js +5 -4
  121. package/dist/{refresh-BwYkyNOQ.d.ts → refresh-BTrQ_ZTp.d.ts} +1 -1
  122. package/dist/{resolve-BAqQcwMW.d.ts → resolve-C15TJq77.d.ts} +11 -5
  123. package/dist/{resource-My_y-Syh.js → resource-CRxxwkQq.js} +22 -7
  124. package/dist/{resource-BJcaIfxi.d.ts → resource-D8iqWMr-.d.ts} +5 -5
  125. package/dist/respond-D2OQl62-.d.ts +371 -0
  126. package/dist/{result-Bj6yt33D.js → result-BKiliPY7.js} +1 -1
  127. package/dist/server/index.d.ts +14 -12
  128. package/dist/server/index.js +11 -17
  129. package/dist/{server-BA75B0mT.js → server-Csx2sRjg.js} +251 -34
  130. package/dist/{shared-DnoCdMDr.js → shared-D1dRUrAg.js} +16 -2
  131. package/dist/signers-Cc6r98SW.d.ts +162 -0
  132. package/dist/{simplify-Cqvfr9dj.js → simplify-_0oUKqCd.js} +4 -115
  133. package/dist/sql/index.d.ts +247 -10
  134. package/dist/sql/index.js +8590 -779
  135. package/dist/{sql-B34HU2DI.js → sql-Coh6n4cC.js} +9 -2
  136. package/dist/ssr/index.d.ts +4 -4
  137. package/dist/ssr/index.js +1 -2
  138. package/dist/{standard-BRyBGTtp.js → standard-D7r6dMUu.js} +6 -3
  139. package/dist/{standard-BsaCYQJc.d.ts → standard-J4YgMOLm.d.ts} +1 -1
  140. package/dist/{stats-C_YxA2r1.js → stats-Bqob3WqT.js} +4 -4
  141. package/dist/storage/index.d.ts +4 -4
  142. package/dist/storage/index.js +155 -26
  143. package/dist/support-DT-OhSxD.d.ts +88 -0
  144. package/dist/support-Dk6vdmEw.js +291 -0
  145. package/dist/support-view-Da5peKFg.d.ts +17 -0
  146. package/dist/support-view-g4YLBeqX.js +18 -0
  147. package/dist/{permdock-sql-C4KFzLc9.js → template-TxrGI5jR.js} +2 -31
  148. package/dist/{temporal-BwshQk0W.js → temporal-CVlIV2DZ.js} +17 -4
  149. package/dist/{temporal-required-CBNHT6ni.js → temporal-required-CAt4Dc9Z.js} +2 -2
  150. package/dist/testing/index.d.ts +100 -10
  151. package/dist/testing/index.js +336 -8
  152. package/dist/{types-DONv9SHN.d.ts → types-Dnf_wRDz.d.ts} +26 -3
  153. package/dist/user-change-CHFEUro1.js +49 -0
  154. package/dist/{webhooks-Bh_Jf_-l.js → verify-Ce49ETPz.js} +5 -5
  155. package/dist/{version-Bmf7_FzM.js → version-DM_Roffb.js} +1 -1
  156. package/dist/{view-B64o81wp.d.ts → view-BEf0diC5.d.ts} +8 -3
  157. package/dist/{mfa-byfjruR9.js → view-Bq64ki8-.js} +95 -38
  158. package/dist/webhooks/index.d.ts +135 -4
  159. package/dist/webhooks/index.js +588 -2
  160. package/dist/wire-D2dT86vi.js +35 -0
  161. package/package.json +16 -8
  162. package/schemas/config-v1.json +131 -1
  163. package/schemas/doctor-report-v1.json +10 -0
  164. package/skills/better-supabase/SKILL.md +9 -6
  165. package/skills/better-supabase/references/plugins.md +6 -3
  166. package/skills/better-supabase/references/troubleshooting.md +1 -0
  167. package/skills/better-supabase-api/SKILL.md +9 -2
  168. package/skills/better-supabase-api/references/adapters.md +11 -8
  169. package/skills/better-supabase-auth/SKILL.md +3 -0
  170. package/skills/better-supabase-auth/references/permdock.md +56 -11
  171. package/dist/cli/gen-D5cLCULx.js +0 -2
  172. package/dist/cli/sql-D4VbqmXi.js +0 -199
  173. package/dist/events-B21UOhvP.d.ts +0 -84
  174. package/dist/executor-B5IoUxLE.d.ts +0 -195
  175. package/dist/invalidate-CgxmRamI.js +0 -24
  176. package/dist/permdock-sql-DImJULY-.d.ts +0 -17
  177. package/dist/plugin-BqR6wKMB.js +0 -23
  178. package/dist/respond-DgIbdrt8.d.ts +0 -194
  179. package/dist/view-BrJI2LKp.js +0 -45
@@ -1,13 +1,13 @@
1
- import { n as dbError } from "../errors-L0xK41y_.js";
2
- import { i as temporalText } from "../temporal-BwshQk0W.js";
3
- import { t as temporal } from "../temporal-required-CBNHT6ni.js";
4
- import { i as ok } from "../result-Bj6yt33D.js";
5
- import { t as BetterSupabase } from "../define-Bh0AqTik.js";
1
+ import { n as dbError } from "../errors-DtwXa6b8.js";
2
+ import { i as temporalText } from "../temporal-CVlIV2DZ.js";
3
+ import { t as temporal } from "../temporal-required-CAt4Dc9Z.js";
4
+ import { i as ok } from "../result-BKiliPY7.js";
5
+ import { t as BetterSupabase } from "../define-cNKzqwL_.js";
6
6
  import { a as toBase64Url, r as fromBase64, t as base64ToText } from "../base64-D2gsioCY.js";
7
- import { r as resolveConfig } from "../config-BpxDfnbg.js";
7
+ import { a as resolveConfig } from "../kits-CGLLrNPd.js";
8
8
  import { t as isList } from "../guards-BnzDvFrz.js";
9
9
  import { ENV_VARIABLES } from "../env/index.js";
10
- import { n as postgresExecutor } from "../executor-CVt6dD0B.js";
10
+ import { n as postgresExecutor } from "../executor-XycESQPH.js";
11
11
  import { toCloudEvents } from "../events/index.js";
12
12
  import { createClient } from "@supabase/supabase-js";
13
13
  import { existsSync } from "node:fs";
@@ -546,6 +546,147 @@ function testEventSink(sink, options = {}) {
546
546
  }]
547
547
  ]);
548
548
  }
549
+ function claimedJob(queue, row) {
550
+ const instant = temporal().Instant.fromEpochMilliseconds(0);
551
+ return {
552
+ id: Number(row.id),
553
+ queue,
554
+ payload: row.message?.payload,
555
+ attempts: row.attempts,
556
+ maxAttempts: row.message?.max_attempts ?? 5,
557
+ enqueuedAt: instant,
558
+ visibleUntil: instant,
559
+ lastError: row.message?.last_error ?? null,
560
+ context: {}
561
+ };
562
+ }
563
+ /**
564
+ * Proves a `QueueBackend` round-trips payloads, hides claimed messages for
565
+ * their lease, counts attempts, retries and dead-letters on `fail`, and
566
+ * (when it has `leases`) rejects a stale attempt and extends a lease. It
567
+ * writes to `options.queue` on the backend's real store.
568
+ */
569
+ function testQueueBackend(backend, options) {
570
+ const { queue } = options;
571
+ const claimOne = async () => {
572
+ const [row] = await backend.read(queue, 30, 1);
573
+ return row && claimedJob(queue, row);
574
+ };
575
+ return conform(`QueueBackend "${backend.name}"`, [
576
+ hasName(backend),
577
+ ["has apiVersion 1", () => {
578
+ const version = backend.apiVersion;
579
+ expect(version === 1, "apiVersion must be 1");
580
+ }],
581
+ ["round-trips a payload and leases it", async () => {
582
+ const id = await backend.send(queue, { n: 1 }, 0, 3, void 0);
583
+ expect(Number.isFinite(id), "send must return a numeric id");
584
+ const job = await claimOne();
585
+ expect(job?.id === id, "read must return the sent message");
586
+ expect(same(job.payload, { n: 1 }), "the payload must round-trip");
587
+ expect(job.attempts === 1, "the first claim must be attempt 1");
588
+ expect(job.maxAttempts === 3, "max_attempts must round-trip");
589
+ expect(await claimOne() === void 0, "a leased message must stay hidden from other reads");
590
+ expect(await backend.complete(job), "complete must return true");
591
+ }],
592
+ backend.leases && ["rejects a stale attempt and extends a lease", async () => {
593
+ await backend.send(queue, { n: 2 }, 0, 3, void 0);
594
+ const job = await claimOne();
595
+ expect(job, "read must return the sent message");
596
+ expect(!await backend.complete({
597
+ ...job,
598
+ attempts: job.attempts + 1
599
+ }), "complete with another attempt must return false");
600
+ expect(await backend.extend(job, 60), "extend must return true");
601
+ expect(await backend.complete(job), "complete must return true");
602
+ }],
603
+ ["retries, then dead-letters at max attempts", async () => {
604
+ await backend.send(queue, { n: 3 }, 0, 2, void 0);
605
+ const first = await claimOne();
606
+ expect(first, "read must return the sent message");
607
+ expect(await backend.fail(first, "boom", 0) === "queued", "fail before max_attempts must return queued");
608
+ const second = await claimOne();
609
+ expect(second?.attempts === 2, "a retried message must be attempt 2");
610
+ expect(await backend.fail(second, "boom", 0) === "dead", "fail at max_attempts must return dead");
611
+ expect(await claimOne() === void 0, "a dead message must not be claimed again");
612
+ }],
613
+ (options.dedupe ?? backend.leases) && ["returns the first id for a repeated dedupe key", async () => {
614
+ 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");
615
+ const job = await claimOne();
616
+ expect(job, "read must return the sent message");
617
+ await backend.complete(job);
618
+ }]
619
+ ]);
620
+ }
621
+ /**
622
+ * Proves a `SupportSessionStore` starts sessions with the input it was
623
+ * given, shows a running session only to its admin, ends the admin's
624
+ * previous session on start, ends a session once, and lists sessions.
625
+ * It writes sessions to the store; they are all ended when it returns.
626
+ */
627
+ function testSupportSessionStore(store, options) {
628
+ const { admin } = options;
629
+ const [first, second] = options.targets;
630
+ const start = (targetUserId) => store.start({
631
+ adminId: admin.id,
632
+ adminClaims: admin.claims,
633
+ targetUserId,
634
+ reason: "conformance",
635
+ ttlSeconds: 600,
636
+ readOnly: true,
637
+ metadata: { ticket: "T-1" }
638
+ });
639
+ return conform(`SupportSessionStore "${store.name}"`, [
640
+ hasName(store),
641
+ ["has apiVersion 1", () => {
642
+ const version = store.apiVersion;
643
+ expect(version === 1, "apiVersion must be 1");
644
+ }],
645
+ ["starts a session with its input and shows it to its admin only", async () => {
646
+ const session = await start(first);
647
+ try {
648
+ expect(session.id.length > 0, "start must return an id");
649
+ expect(session.adminId === admin.id, "adminId must round-trip");
650
+ expect(session.targetUserId === first, "targetUserId must round-trip");
651
+ expect(session.reason === "conformance", "reason must round-trip");
652
+ expect(session.readOnly, "readOnly must round-trip");
653
+ expect(same(session.metadata, { ticket: "T-1" }), "metadata must round-trip");
654
+ const seconds = (session.expiresAt.epochMilliseconds - session.startedAt.epochMilliseconds) / 1e3;
655
+ expect(Math.abs(seconds - 600) < 5, "expiresAt must be ttlSeconds after startedAt");
656
+ expect((await store.get(session.id, admin.id))?.id === session.id, "get must return the running session");
657
+ expect(await store.get(session.id, first) === void 0, "get must hide a session from anyone but its admin");
658
+ } finally {
659
+ await store.end(session.id, "admin");
660
+ }
661
+ }],
662
+ ["ends the admin's previous session on start", async () => {
663
+ const older = await start(first);
664
+ const newer = await start(second);
665
+ try {
666
+ expect(await store.get(older.id, admin.id) === void 0, "starting a session must end the admin's previous one");
667
+ expect((await store.get(newer.id, admin.id))?.id === newer.id, "the new session must be running");
668
+ } finally {
669
+ await store.end(newer.id, "admin");
670
+ }
671
+ }],
672
+ ["ends a session once and lists it", async () => {
673
+ const session = await start(first);
674
+ expect(await store.end(session.id, "admin"), "end must return true");
675
+ expect(!await store.end(session.id, "admin"), "ending an ended session must return false");
676
+ expect(await store.get(session.id, admin.id) === void 0, "get must not return an ended session");
677
+ const entry = (await store.list({
678
+ adminId: admin.id,
679
+ active: false
680
+ })).find((item) => item.id === session.id);
681
+ expect(entry, "list must include the ended session");
682
+ expect(entry.endedAt !== void 0, "an ended session has endedAt");
683
+ expect((await store.list({
684
+ adminId: admin.id,
685
+ active: true
686
+ })).every((item) => item.id !== session.id), "list({ active: true }) must leave out ended sessions");
687
+ }]
688
+ ]);
689
+ }
549
690
  async function resolveSafely(resolver, request) {
550
691
  try {
551
692
  return await resolver.resolve(request);
@@ -678,6 +819,21 @@ function testPlugin(plugin, options) {
678
819
  expect(first.kind === op.kind, "must return a mutation of the same kind");
679
820
  expect(same(first, second), "two calls with the same input differ");
680
821
  }],
822
+ plugin.afterMutation && create && ["afterMutation cannot change the result", async () => {
823
+ const row = {
824
+ marker: "conformance",
825
+ nested: { kept: true }
826
+ };
827
+ const result = await betterSupabase.use(plugin).connect({
828
+ name: "rows",
829
+ execute: () => Promise.resolve(ok({
830
+ rows: [structuredClone(row)],
831
+ count: 1
832
+ }))
833
+ }, context)[table]["create"](create);
834
+ expect(result.ok, "create failed");
835
+ expect(same(result.data, row), "afterMutation changed the returned row");
836
+ }],
681
837
  plugin.wrapExecutor && ["wrapExecutor keeps results intact", async () => {
682
838
  const op = await captureOp(betterSupabase, (db) => db[table]["findMany"]({ limit: 1 }));
683
839
  const data = {
@@ -915,4 +1071,176 @@ function defineSeed(betterSupabase, fixtures) {
915
1071
  };
916
1072
  }
917
1073
  //#endregion
918
- export { ConformanceError, LOCAL_JWT_SECRET, asUser, createTestSigner, defineSeed, expectDbBudget, expectTenantIsolation, isSeed, localAuth, localSigningKey, signLocalJwt, signTestJwt, signTestJwtWithKey, testAuthResolver, testCacheAdapter, testEventSink, testExecutor, testGenerator, testPlugin };
1074
+ //#region src/testing/notification-channel.ts
1075
+ const STATUSES = /* @__PURE__ */ new Set(["sent", "skipped"]);
1076
+ function sample(channel, email) {
1077
+ const createdAt = temporal().Instant.fromEpochMilliseconds(0);
1078
+ return Object.freeze({
1079
+ deliveryId: "00000000-0000-4000-8000-00000000c0f0",
1080
+ channel,
1081
+ attempts: 1,
1082
+ userId: "00000000-0000-4000-8000-0000000000aa",
1083
+ email,
1084
+ notification: Object.freeze({
1085
+ id: "00000000-0000-4000-8000-00000000c0f1",
1086
+ eventId: "00000000-0000-4000-8000-00000000c0f2",
1087
+ type: "conformance.check",
1088
+ data: Object.freeze({ title: "Conformance" }),
1089
+ tenant: null,
1090
+ actorId: null,
1091
+ subject: Object.freeze({
1092
+ type: "task",
1093
+ id: "42",
1094
+ label: "Conformance"
1095
+ }),
1096
+ summary: "Conformance check",
1097
+ actionPath: "/",
1098
+ priority: null,
1099
+ createdAt,
1100
+ readAt: null,
1101
+ resolvedAt: null
1102
+ }),
1103
+ text: Object.freeze({ title: "Conformance check" })
1104
+ });
1105
+ }
1106
+ /**
1107
+ * Runs the `NotificationChannel` contract against `channel`: the API version,
1108
+ * a name, and one frozen sample message it must send without mutating.
1109
+ * Throws a `ConformanceError` listing every failed check.
1110
+ */
1111
+ function testNotificationChannel(channel, options = {}) {
1112
+ const message = sample(channel.name, options.email ?? null);
1113
+ return conform(`NotificationChannel "${channel.name}"`, [
1114
+ ["has apiVersion 1", () => {
1115
+ const version = channel.apiVersion;
1116
+ expect(version === 1, "apiVersion must be 1");
1117
+ }],
1118
+ ["has a name", () => {
1119
+ expect(typeof channel.name === "string" && channel.name.length > 0, "name must be a non-empty string");
1120
+ }],
1121
+ ["sends a message without mutating it and returns a valid result", async () => {
1122
+ const result = await channel.send(message);
1123
+ if (result === void 0) return;
1124
+ expect(result.status === void 0 || STATUSES.has(result.status), `status must be "sent" or "skipped", got ${String(result.status)}`);
1125
+ for (const key of ["provider", "providerMessageId"]) expect(result[key] === void 0 || typeof result[key] === "string", `${key} must be a string`);
1126
+ }],
1127
+ options.received && ["delivers the message", async () => {
1128
+ expect((await options.received()).includes(message.deliveryId), `delivery ${message.deliveryId} was not received`);
1129
+ }]
1130
+ ]);
1131
+ }
1132
+ //#endregion
1133
+ //#region src/testing/webhooks.ts
1134
+ const SECRET = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
1135
+ function signInput(body, timestamp) {
1136
+ return Object.freeze({
1137
+ id: "msg_conformance",
1138
+ body,
1139
+ timestamp,
1140
+ secrets: Object.freeze([SECRET])
1141
+ });
1142
+ }
1143
+ function isHeaders(value) {
1144
+ return typeof value === "object" && value !== null && Object.values(value).every((entry) => typeof entry === "string");
1145
+ }
1146
+ /**
1147
+ * Runs the `WebhookSigner` contract against `signer`: the API version, a
1148
+ * name, string headers that depend on the body and are stable for the same
1149
+ * input, and, with `verify`, a signature the receiver accepts.
1150
+ */
1151
+ function testWebhookSigner(signer, options = {}) {
1152
+ const body = JSON.stringify({
1153
+ type: "conformance.check",
1154
+ data: { n: 1 }
1155
+ });
1156
+ const now = temporal().Now.instant();
1157
+ return conform(`WebhookSigner "${signer.name}"`, [
1158
+ ["has apiVersion 1", () => {
1159
+ const version = signer.apiVersion;
1160
+ expect(version === 1, "apiVersion must be 1");
1161
+ }],
1162
+ ["has a name", () => {
1163
+ expect(typeof signer.name === "string" && signer.name.length > 0, "name must be a non-empty string");
1164
+ }],
1165
+ ["returns string headers, stable for the same input", async () => {
1166
+ const first = await signer.sign(signInput(body, now));
1167
+ const second = await signer.sign(signInput(body, now));
1168
+ expect(isHeaders(first), "sign must return a record of string headers");
1169
+ expect(isHeaders(first) && Object.keys(first).length > 0, "sign must return at least one header");
1170
+ expect(JSON.stringify(first) === JSON.stringify(second), "the same input must produce the same headers");
1171
+ }],
1172
+ ["changes the headers when the body changes", async () => {
1173
+ const original = await signer.sign(signInput(body, now));
1174
+ const tampered = await signer.sign(signInput(`${body} `, now));
1175
+ expect(JSON.stringify(original) !== JSON.stringify(tampered), "the signature must cover the body");
1176
+ }],
1177
+ options.verify && ["produces a signature the receiver verifies", async () => {
1178
+ const headers = await signer.sign(signInput(body, now));
1179
+ expect(await options.verify({
1180
+ headers,
1181
+ body
1182
+ }, SECRET), "the receiver rejected the signature");
1183
+ }]
1184
+ ]);
1185
+ }
1186
+ /**
1187
+ * Runs the `WebhookTransport` contract against `transport`: the API version,
1188
+ * a name, and one POST to `options.url` that returns an HTTP status and a
1189
+ * string body.
1190
+ */
1191
+ function testWebhookTransport(transport, options) {
1192
+ const body = JSON.stringify({
1193
+ type: "conformance.check",
1194
+ id: crypto.randomUUID()
1195
+ });
1196
+ return conform(`WebhookTransport "${transport.name}"`, [
1197
+ ["has apiVersion 1", () => {
1198
+ const version = transport.apiVersion;
1199
+ expect(version === 1, "apiVersion must be 1");
1200
+ }],
1201
+ ["has a name", () => {
1202
+ expect(typeof transport.name === "string" && transport.name.length > 0, "name must be a non-empty string");
1203
+ }],
1204
+ ["sends the request and returns a status and a body", async () => {
1205
+ const response = await transport.send(Object.freeze({
1206
+ url: options.url,
1207
+ headers: Object.freeze({ "content-type": "application/json" }),
1208
+ body
1209
+ }));
1210
+ expect(Number.isInteger(response.status) && response.status >= 100 && response.status <= 599, `status must be an HTTP status, got ${String(response.status)}`);
1211
+ const responseBody = response.body;
1212
+ expect(typeof responseBody === "string", "body must be a string");
1213
+ }],
1214
+ options.received && ["delivers the body", async () => {
1215
+ expect((await options.received()).includes(body), "the receiver did not get the body");
1216
+ }]
1217
+ ]);
1218
+ }
1219
+ /**
1220
+ * Runs the `WebhookSecretStore` contract against `store`: the API version, a
1221
+ * list of strings, and, when it can rotate, a new secret listed first while
1222
+ * the previous one keeps signing.
1223
+ */
1224
+ function testWebhookSecretStore(store, options) {
1225
+ return conform("WebhookSecretStore", [
1226
+ ["has apiVersion 1", () => {
1227
+ const version = store.apiVersion;
1228
+ expect(version === 1, "apiVersion must be 1");
1229
+ }],
1230
+ ["lists the secrets as strings", async () => {
1231
+ const secrets = await store.secrets(options.endpointId);
1232
+ expect(Array.isArray(secrets) && secrets.every((secret) => typeof secret === "string"), "secrets must return an array of strings");
1233
+ }],
1234
+ store.rotate && ["rotates to a new secret and keeps the previous one during the overlap", async () => {
1235
+ const first = await store.rotate(options.endpointId, { overlap: "1 hour" });
1236
+ const second = await store.rotate(options.endpointId, { overlap: "1 hour" });
1237
+ expect(typeof first === "string" && first.length > 0, "rotate must return the new secret");
1238
+ expect(first !== second, "each rotation must create a new secret");
1239
+ const secrets = await store.secrets(options.endpointId);
1240
+ expect(secrets[0] === second, "the newest secret must be listed first");
1241
+ expect(secrets.includes(first), "the previous secret must keep signing during the overlap");
1242
+ }]
1243
+ ]);
1244
+ }
1245
+ //#endregion
1246
+ export { ConformanceError, LOCAL_JWT_SECRET, asUser, createTestSigner, defineSeed, expectDbBudget, expectTenantIsolation, isSeed, localAuth, localSigningKey, signLocalJwt, signTestJwt, signTestJwtWithKey, testAuthResolver, testCacheAdapter, testEventSink, testExecutor, testGenerator, testNotificationChannel, testPlugin, testQueueBackend, testSupportSessionStore, testWebhookSecretStore, testWebhookSigner, testWebhookTransport };
@@ -62,6 +62,8 @@ interface TableFlags {
62
62
  readonly actor?: {
63
63
  readonly createdBy?: string;
64
64
  readonly updatedBy?: string;
65
+ /** The admin behind an impersonated write; `null` otherwise. */
66
+ readonly impersonatedBy?: string;
65
67
  };
66
68
  /** App column used for optimistic concurrency, usually `updatedAt`. */
67
69
  readonly version?: string;
@@ -140,7 +142,7 @@ interface PermdockBucketPolicy {
140
142
  readonly segment?: number;
141
143
  /**
142
144
  * Schema of the helpers `permdock rls generate` writes: PermDock's
143
- * `rls.schema`. Defaults to `public`.
145
+ * `rls.schema`. Defaults to `permdock`, PermDock's default.
144
146
  */
145
147
  readonly schema?: string;
146
148
  }
@@ -153,14 +155,35 @@ interface PermdockTopicPolicy {
153
155
  readonly scope: string;
154
156
  /** 1-based `:`-separated topic segment holding the scope id. Defaults to the `{orgId}` segment. */
155
157
  readonly segment?: number;
158
+ /** Schema of PermDock's helpers (`rls.schema`). Defaults to `permdock`. */
156
159
  readonly schema?: string;
157
160
  }
158
161
  type BucketPolicyName = "tenant" | "owner" | "public" | "none";
162
+ /**
163
+ * A policy through the SQL kit's [access contract](/docs/kits/access):
164
+ * `tenant_ids_with(permission)` for the tenant id in the path, or
165
+ * `is_platform(permission)`. Works with every access model.
166
+ */
167
+ interface AccessBucketPolicy {
168
+ readonly access: {
169
+ /** Downloads, signed URLs, renders and metadata reads. */
170
+ readonly read: string;
171
+ /** Listing; without it `read` covers listing too. */
172
+ readonly list?: string;
173
+ /** Uploads, updates and moves; also deletes unless `delete` is set. */
174
+ readonly write: string;
175
+ readonly delete?: string;
176
+ };
177
+ /** `tenant` (default) checks the path's tenant segment; `platform` checks platform permissions. */
178
+ readonly scope?: "tenant" | "platform";
179
+ /** 1-based path segment holding the tenant id. Defaults to the `{orgId}` segment. */
180
+ readonly segment?: number;
181
+ }
159
182
  interface BucketMeta {
160
183
  readonly id: string;
161
184
  readonly public: boolean;
162
185
  readonly path: string;
163
- readonly policy?: BucketPolicyName | PermdockBucketPolicy;
186
+ readonly policy?: BucketPolicyName | PermdockBucketPolicy | AccessBucketPolicy;
164
187
  /** Claim paths for the tenant policy, when `config.claims.tenant` is not the default. */
165
188
  readonly tenant?: {
166
189
  readonly claim: readonly string[];
@@ -248,4 +271,4 @@ type IsUnion<T, U = T> = T extends unknown ? [U] extends [T] ? false : true : ne
248
271
  type PrimaryKeyValue<M extends AnyModels, T extends keyof M> = true extends IsUnion<PrimaryKeyColumn<M, T>> ? { readonly [K in PrimaryKeyColumn<M, T>]: Row<M, T>[K]; } : Row<M, T>[PrimaryKeyColumn<M, T>];
249
272
  type Simplify<T> = { [K in keyof T]: T[K]; } & {};
250
273
  //#endregion
251
- export { SchemaMeta as A, PrimaryKeyValue as C, Relations as D, RelationTypes as E, UniqueConstraint as F, UniqueKeyName as I, UniqueWhere as L, TableFlags as M, TableKey as N, Row as O, TableMeta as P, Update as R, PrimaryKeyColumn as S, RelationMeta as T, ModelFlags as _, Casing as a, PermdockBucketPolicy as b, Codec as c, Flags as d, ForeignKeyConstraint as f, Insert as g, FunctionsOf as h, BucketPolicyName as i, Simplify as j, Schema as k, ColumnMeta as l, FunctionTypes as m, AnyModels as n, CheckConstraint as o, FunctionMeta as p, BucketMeta as r, ClaimsMeta as s, AnyFunctions as t, DatabaseOf as u, ModelTypes as v, RealtimeTableMeta as w, PermdockTopicPolicy as x, ModelsOf as y };
274
+ export { Schema as A, PrimaryKeyColumn as C, RelationTypes as D, RelationMeta as E, TableMeta as F, UniqueConstraint as I, UniqueKeyName as L, Simplify as M, TableFlags as N, Relations as O, TableKey as P, UniqueWhere as R, PermdockTopicPolicy as S, RealtimeTableMeta as T, Insert as _, BucketPolicyName as a, ModelsOf as b, ClaimsMeta as c, DatabaseOf as d, Flags as f, FunctionsOf as g, FunctionTypes as h, BucketMeta as i, SchemaMeta as j, Row as k, Codec as l, FunctionMeta as m, AnyFunctions as n, Casing as o, ForeignKeyConstraint as p, AnyModels as r, CheckConstraint as s, AccessBucketPolicy as t, ColumnMeta as u, ModelFlags as v, PrimaryKeyValue as w, PermdockBucketPolicy as x, ModelTypes as y, Update as z };
@@ -0,0 +1,49 @@
1
+ //#region src/query/invalidate.ts
2
+ function isString(value) {
3
+ return typeof value === "string";
4
+ }
5
+ function queryTables(query) {
6
+ const tables = query.meta?.["bsTables"];
7
+ if (Array.isArray(tables)) {
8
+ const list = tables;
9
+ return list.every(isString) ? list : list.filter(isString);
10
+ }
11
+ const [prefix, table] = query.queryKey;
12
+ return prefix === "bs" && typeof table === "string" ? [table] : void 0;
13
+ }
14
+ /**
15
+ * Invalidates every query that read one of `tables`: better-supabase queries
16
+ * by their `meta.bsTables`, and other `['bs', table, ...]` keys by prefix.
17
+ */
18
+ function invalidateTables(client, tables) {
19
+ if (tables.length === 0) return Promise.resolve();
20
+ const changed = new Set(tables);
21
+ return client.invalidateQueries({ predicate: (query) => queryTables(query)?.some((table) => changed.has(table)) ?? false });
22
+ }
23
+ //#endregion
24
+ //#region src/query/user-change.ts
25
+ /**
26
+ * Removes every `['bs', ...]` query when the signed-in user changes or signs
27
+ * out, so the next user never sees the previous user's rows. Returns the
28
+ * function that stops listening. `BetterSupabaseProvider` calls it for React;
29
+ * call it yourself with Vue, Solid, Svelte or Angular Query.
30
+ *
31
+ * ```ts
32
+ * const stop = clearOnUserChange(queryClient, bs.auth);
33
+ * ```
34
+ */
35
+ function clearOnUserChange(queryClient, auth) {
36
+ let user;
37
+ const check = () => {
38
+ const snapshot = auth.current();
39
+ if (snapshot.status === "loading") return;
40
+ const id = snapshot.user?.id ?? null;
41
+ if (user !== void 0 && user !== id) queryClient.removeQueries({ queryKey: ["bs"] });
42
+ user = id;
43
+ };
44
+ const stop = auth.subscribe(check);
45
+ check();
46
+ return stop;
47
+ }
48
+ //#endregion
49
+ export { invalidateTables as n, clearOnUserChange as t };
@@ -1,9 +1,9 @@
1
- import { n as dbError } from "./errors-L0xK41y_.js";
2
- import { n as nowInstant, r as optionalTemporal } from "./temporal-BwshQk0W.js";
3
- import { n as temporalMissing } from "./temporal-required-CBNHT6ni.js";
4
- import { i as ok, n as err } from "./result-Bj6yt33D.js";
1
+ import { n as dbError } from "./errors-DtwXa6b8.js";
2
+ import { n as nowInstant, r as optionalTemporal } from "./temporal-CVlIV2DZ.js";
3
+ import { n as temporalMissing } from "./temporal-required-CAt4Dc9Z.js";
4
+ import { i as ok, n as err } from "./result-BKiliPY7.js";
5
5
  import { i as toBase64, r as fromBase64 } from "./base64-D2gsioCY.js";
6
- import { s as toApp } from "./shared-DnoCdMDr.js";
6
+ import { c as toApp } from "./shared-D1dRUrAg.js";
7
7
  //#region src/webhooks/verify.ts
8
8
  const encoder = /* @__PURE__ */ new TextEncoder();
9
9
  /** Accepts `whsec_…`, `v1,whsec_…` (Supabase Auth hooks) or raw base64. */
@@ -1,4 +1,4 @@
1
1
  //#region src/core/version.ts
2
- const VERSION = "0.4.0";
2
+ const VERSION = "0.5.0";
3
3
  //#endregion
4
4
  export { VERSION as t };
@@ -1,6 +1,6 @@
1
- import { p as DbError } from "./result-CqEsbgYs.js";
2
- import { n as AmrEntry, s as Impersonator, t as Aal } from "./mfa-C7MNQ1Ob.js";
3
- import { i as InvalidReason, r as AuthState } from "./resolve-BAqQcwMW.js";
1
+ import { _ as DbError } from "./executor-BunPOO-k.js";
2
+ import { i as InvalidReason, r as AuthState } from "./resolve-C15TJq77.js";
3
+ import { n as AmrEntry, s as Impersonator, t as Aal } from "./mfa-CusDgPaD.js";
4
4
  import { JWTClaims, UserClaims } from "@supabase/server";
5
5
  //#region src/auth/actor.d.ts
6
6
  /** One level of an RFC 8693 `act` chain: who acts, and who acted before it. */
@@ -48,6 +48,11 @@ type AuthSession<C = unknown, P = unknown> = {
48
48
  readonly profile?: P;
49
49
  /** `aal2` once the user verified a second factor in this session. */
50
50
  readonly aal: Aal;
51
+ /**
52
+ * Signed in with `signInAnonymously()` (the `is_anonymous` claim). Guards
53
+ * refuse these users unless `allow` lists `'anonymous'`.
54
+ */
55
+ readonly anonymous: boolean;
51
56
  /** How the user signed in (`password`, `totp`, `sso/saml`, ...). */
52
57
  readonly amr: readonly AmrEntry[];
53
58
  /** Set when an admin acts as this user (the `act` claim), for a banner. */