better-supabase 0.3.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 (211) hide show
  1. package/README.md +9 -6
  2. package/bin/better-supabase.js +4 -0
  3. package/dist/{base64-CQeB1eSf.js → base64-D2gsioCY.js} +2 -2
  4. package/dist/channel-FQW-CnGR.d.ts +70 -0
  5. package/dist/claims-BAf6c7is.js +48 -0
  6. package/dist/cli/_format-Cax5EsM9.js +101 -0
  7. package/dist/cli/bin.d.ts +1 -0
  8. package/dist/cli/bin.js +48 -0
  9. package/dist/cli/codemod-DqV7612m.js +388 -0
  10. package/dist/cli/command-CK14fP91.js +502 -0
  11. package/dist/cli/compare-BmIWVEui.js +9 -0
  12. package/dist/cli/config-CPVpXP5I.js +1965 -0
  13. package/dist/cli/config-dtoekM0v.js +2 -0
  14. package/dist/cli/db-DOBPt660.js +52 -0
  15. package/dist/cli/diff-SjkO5rNr.js +752 -0
  16. package/dist/cli/dist-DBPdlPYK.js +1112 -0
  17. package/dist/cli/dist-DreZ24Sz.js +129 -0
  18. package/dist/cli/dist-xn4wlboc.js +732 -0
  19. package/dist/cli/doctor-L7Z8mYTx.js +2863 -0
  20. package/dist/cli/env-DmKv_G4A.js +108 -0
  21. package/dist/cli/env-jIFUADLY.js +511 -0
  22. package/dist/cli/errors-4v4DTtda.js +56 -0
  23. package/dist/cli/fastest-levenshtein-CFN-lNDv.js +120 -0
  24. package/dist/cli/gen-BJc9vdyu.js +2 -0
  25. package/dist/cli/gen-C2osRcr5.js +2204 -0
  26. package/dist/cli/index.d.ts +493 -0
  27. package/dist/cli/index.js +184 -0
  28. package/dist/cli/init-Saf5-R9T.js +550 -0
  29. package/dist/cli/introspect--H7oGmcd.js +81 -0
  30. package/dist/cli/io-k41_z-jT.js +24 -0
  31. package/dist/cli/json5-ChRycquj.js +787 -0
  32. package/dist/cli/jsonc-BUniUpOk.js +2 -0
  33. package/dist/cli/jsonc-D5Fu67jw.js +45 -0
  34. package/dist/cli/keys-ByKHnvP9.js +83 -0
  35. package/dist/cli/ohash-D7bG_EOb.js +277 -0
  36. package/dist/cli/openapi-Cp5Fyta-.js +66 -0
  37. package/dist/cli/project-B_GeXyHh.js +70 -0
  38. package/dist/cli/prompts-BNcNfAve.js +1142 -0
  39. package/dist/cli/prompts-BTYQGunc.js +2 -0
  40. package/dist/cli/read-sets-BfMQcKy8.js +289 -0
  41. package/dist/cli/rolldown-runtime-BPOCksWG.js +24 -0
  42. package/dist/cli/run-BKR2W_79.js +351 -0
  43. package/dist/cli/seed-CUlqWh-8.js +98 -0
  44. package/dist/cli/skills-LJJIhLFR.js +137 -0
  45. package/dist/cli/sql-DzPbfngx.js +348 -0
  46. package/dist/cli/style-C0NFdJRL.js +29 -0
  47. package/dist/cli/toml-DB-MNnFs.js +424 -0
  48. package/dist/cli/version-BDJuO0UR.js +4 -0
  49. package/dist/cli/yaml-CYCbJ6et.js +2252 -0
  50. package/dist/client/index.d.ts +2 -2
  51. package/dist/client/index.js +15 -22
  52. package/dist/clone-BfgmLU90.js +21 -0
  53. package/dist/{compiler-2zqGzxVZ.d.ts → compiler-BXH_aZ7N.d.ts} +2 -2
  54. package/dist/config/index.d.ts +4 -4
  55. package/dist/config/index.js +5 -5
  56. package/dist/{config-DOsGYSiT.js → config-DreERlKo.js} +1 -1
  57. package/dist/{config-C1CYcm6o.d.ts → config-T_ejwyd3.d.ts} +176 -11
  58. package/dist/{define-DUbydmJa.d.ts → define-Bp_BZa5h.d.ts} +37 -29
  59. package/dist/{define-DF341GDj.js → define-cNKzqwL_.js} +356 -89
  60. package/dist/edge/index.d.ts +20 -13
  61. package/dist/edge/index.js +55 -26
  62. package/dist/{entitlements-DvJXoI02.d.ts → entitlements-DUxU4oHe.d.ts} +2 -2
  63. package/dist/env/index.d.ts +2 -2
  64. package/dist/env/index.js +34 -1
  65. package/dist/{errors-Bl3rkOzo.js → errors-DtwXa6b8.js} +2 -1
  66. package/dist/events/index.d.ts +3 -2
  67. package/dist/events/index.js +69 -16
  68. package/dist/events-aIO7vsIk.d.ts +230 -0
  69. package/dist/executor-BunPOO-k.d.ts +402 -0
  70. package/dist/{executor-B26lgbHD.d.ts → executor-Cghdstwu.d.ts} +1 -2
  71. package/dist/{executor-C57jx2pn.js → executor-XycESQPH.js} +3 -3
  72. package/dist/hono/index.d.ts +27 -23
  73. package/dist/hono/index.js +46 -11
  74. package/dist/impersonation-VfaGc4Q_.js +41 -0
  75. package/dist/{index-BNsABq7P.d.ts → index-BGdgBPdf.d.ts} +66 -25
  76. package/dist/{index-qriRM9cZ.d.ts → index-C7f8cOBM.d.ts} +4 -4
  77. package/dist/{index-Dy6kuF0P.d.ts → index-CLg87OBW.d.ts} +6 -1
  78. package/dist/{index-GV77NpRP.d.ts → index-DOESKG27.d.ts} +34 -9
  79. package/dist/{index-CriFFJ_p.d.ts → index-DgILJRXj.d.ts} +43 -17
  80. package/dist/{index-UJ-LUxtI.d.ts → index-DmXSxbLi.d.ts} +14 -14
  81. package/dist/{index-Cf-D2jk1.d.ts → index-Dxn2a5cw.d.ts} +2 -2
  82. package/dist/{index-CTTMS6_1.d.ts → index-SaTsEfGP.d.ts} +86 -9
  83. package/dist/{index-CADlNHK-.d.ts → index-fag8VJZ2.d.ts} +5 -6
  84. package/dist/index.d.ts +34 -17
  85. package/dist/index.js +15 -14
  86. package/dist/jobs/index.d.ts +147 -95
  87. package/dist/jobs/index.js +773 -250
  88. package/dist/{json-schema--aOXAGfU.js → json-schema-m1fiwKgN.js} +23 -2
  89. package/dist/kit-events-CTFDfSwb.js +60 -0
  90. package/dist/kit-transport-B82k1djG.d.ts +33 -0
  91. package/dist/kit-transport-DJlHQUBU.js +55 -0
  92. package/dist/{config-Be4GwsqI.js → kits-CGLLrNPd.js} +38 -10
  93. package/dist/list/index.d.ts +1 -1
  94. package/dist/list/index.js +3 -3
  95. package/dist/{live-D8rV1Ldw.js → live-CdJBOc4f.js} +13 -9
  96. package/dist/{live-QE3UPbGm.d.ts → live-D9W2M5aO.d.ts} +16 -15
  97. package/dist/lookup-BaP5bI27.js +18 -0
  98. package/dist/mcp/index.d.ts +39 -22
  99. package/dist/mcp/index.js +39 -25
  100. package/dist/{mfa-x3V3vGh-.d.ts → mfa-CusDgPaD.d.ts} +5 -1
  101. package/dist/next/image/index.js +6 -3
  102. package/dist/next/index.d.ts +60 -36
  103. package/dist/next/index.js +167 -50
  104. package/dist/notifications/index.d.ts +162 -0
  105. package/dist/notifications/index.js +264 -0
  106. package/dist/openapi/index.d.ts +1 -1
  107. package/dist/openapi/index.js +6 -6
  108. package/dist/orgs/index.d.ts +132 -0
  109. package/dist/orgs/index.js +245 -0
  110. package/dist/orpc/index.d.ts +37 -20
  111. package/dist/orpc/index.js +24 -11
  112. package/dist/otel/index.d.ts +24 -5
  113. package/dist/otel/index.js +124 -64
  114. package/dist/permdock-sql-C2Inc6Xc.d.ts +29 -0
  115. package/dist/permdock-sql-DgOfeixi.js +51 -0
  116. package/dist/plugin-DwARMuQp.js +59 -0
  117. package/dist/{plugin-DVRB8qNE.d.ts → plugin-o144f322.d.ts} +17 -5
  118. package/dist/plugins/actor/index.d.ts +5 -2
  119. package/dist/plugins/actor/index.js +22 -8
  120. package/dist/plugins/rules/index.d.ts +2 -2
  121. package/dist/plugins/rules/index.js +52 -17
  122. package/dist/plugins/soft-delete/index.d.ts +3 -3
  123. package/dist/plugins/soft-delete/index.js +14 -5
  124. package/dist/plugins/tenant/index.d.ts +3 -3
  125. package/dist/plugins/tenant/index.js +45 -21
  126. package/dist/plugins/timestamps/index.d.ts +1 -1
  127. package/dist/plugins/timestamps/index.js +4 -3
  128. package/dist/plugins/validation/index.d.ts +2 -2
  129. package/dist/plugins/validation/index.js +26 -6
  130. package/dist/{pool-B9qsLyFR.d.ts → pool-C9voM4Kw.d.ts} +24 -6
  131. package/dist/postgres/index.d.ts +5 -5
  132. package/dist/postgres/index.js +20 -10
  133. package/dist/{postgrest-DmBlevh-.d.ts → postgrest-CruYKAvV.d.ts} +1 -1
  134. package/dist/{problem-CGt1ADix.js → problem-BW1KhzSG.js} +2 -1
  135. package/dist/{problem-Z2RTQjZD.d.ts → problem-Blxe8S31.d.ts} +1 -1
  136. package/dist/query/index.d.ts +2 -2
  137. package/dist/query/index.js +3 -3
  138. package/dist/{query-BdKLTPlj.js → query-CCbo44lG.js} +18 -18
  139. package/dist/queue-BYUCjr59.d.ts +252 -0
  140. package/dist/react/index.d.ts +8 -7
  141. package/dist/react/index.js +4 -3
  142. package/dist/react/server.d.ts +16 -5
  143. package/dist/react/server.js +12 -4
  144. package/dist/react/session.d.ts +10 -4
  145. package/dist/react/session.js +11 -3
  146. package/dist/{react-DNBYNazu.js → react-C_s7kiNz.js} +138 -46
  147. package/dist/{read-set-D0qBcboM.js → read-set-DQFt1N_Y.js} +71 -15
  148. package/dist/realtime/index.d.ts +3 -3
  149. package/dist/realtime/index.js +14 -12
  150. package/dist/{index-kpgopLNv.d.ts → refresh-BTrQ_ZTp.d.ts} +6 -3
  151. package/dist/{resolve-BRciX2jK.d.ts → resolve-C15TJq77.d.ts} +22 -11
  152. package/dist/{resource-D30rp0GN.js → resource-CRxxwkQq.js} +24 -9
  153. package/dist/{resource-CwVgiI5q.d.ts → resource-D8iqWMr-.d.ts} +6 -6
  154. package/dist/respond-D2OQl62-.d.ts +371 -0
  155. package/dist/{result-DWXatkd6.js → result-BKiliPY7.js} +2 -2
  156. package/dist/server/index.d.ts +17 -15
  157. package/dist/server/index.js +16 -22
  158. package/dist/server-Csx2sRjg.js +655 -0
  159. package/dist/{shared-_6-uPL7E.js → shared-D1dRUrAg.js} +19 -4
  160. package/dist/signers-Cc6r98SW.d.ts +162 -0
  161. package/dist/{simplify-LakqK1vH.js → simplify-_0oUKqCd.js} +27 -95
  162. package/dist/sql/index.d.ts +247 -10
  163. package/dist/sql/index.js +8591 -780
  164. package/dist/{sql-BIgk12ew.js → sql-Coh6n4cC.js} +11 -3
  165. package/dist/ssr/index.d.ts +4 -4
  166. package/dist/ssr/index.js +1 -2
  167. package/dist/{standard-FUi3Z3-7.js → standard-D7r6dMUu.js} +6 -3
  168. package/dist/{standard-eudBiRk1.d.ts → standard-J4YgMOLm.d.ts} +1 -1
  169. package/dist/{stats-D78UQ8Yu.js → stats-Bqob3WqT.js} +34 -9
  170. package/dist/storage/index.d.ts +4 -4
  171. package/dist/storage/index.js +174 -27
  172. package/dist/support-DT-OhSxD.d.ts +88 -0
  173. package/dist/support-Dk6vdmEw.js +291 -0
  174. package/dist/support-view-Da5peKFg.d.ts +17 -0
  175. package/dist/support-view-g4YLBeqX.js +18 -0
  176. package/dist/{tables-Dhha3BXM.js → tables-Cb-3_VWc.js} +14 -0
  177. package/dist/{permdock-sql-Bta_G0v-.js → template-TxrGI5jR.js} +2 -31
  178. package/dist/{temporal-BwshQk0W.js → temporal-CVlIV2DZ.js} +17 -4
  179. package/dist/{temporal-required-CMxeSVPo.js → temporal-required-CAt4Dc9Z.js} +2 -2
  180. package/dist/testing/index.d.ts +113 -24
  181. package/dist/testing/index.js +375 -47
  182. package/dist/{types-DONv9SHN.d.ts → types-Dnf_wRDz.d.ts} +26 -3
  183. package/dist/user-change-CHFEUro1.js +49 -0
  184. package/dist/{webhooks-CAIFQSTM.js → verify-Ce49ETPz.js} +38 -19
  185. package/dist/{version-BfpG2gks.js → version-DM_Roffb.js} +1 -1
  186. package/dist/{view-3Po_PVlr.d.ts → view-BEf0diC5.d.ts} +10 -5
  187. package/dist/{mfa-BxoR1RkN.js → view-Bq64ki8-.js} +210 -64
  188. package/dist/webhooks/index.d.ts +136 -5
  189. package/dist/webhooks/index.js +588 -2
  190. package/dist/wire-D2dT86vi.js +35 -0
  191. package/package.json +50 -10
  192. package/schemas/config-v1.json +131 -1
  193. package/schemas/doctor-report-v1.json +10 -0
  194. package/skills/better-supabase/SKILL.md +60 -10
  195. package/skills/better-supabase/references/plugins.md +13 -14
  196. package/skills/better-supabase/references/troubleshooting.md +25 -4
  197. package/skills/better-supabase-api/SKILL.md +74 -19
  198. package/skills/better-supabase-api/references/adapters.md +48 -27
  199. package/skills/better-supabase-auth/SKILL.md +149 -0
  200. package/skills/better-supabase-auth/references/permdock.md +100 -0
  201. package/skills/better-supabase-testing/SKILL.md +58 -4
  202. package/dist/claims-CoTobNC-.js +0 -25
  203. package/dist/events-Do8jXh4D.d.ts +0 -84
  204. package/dist/executor-DynmIWhq.d.ts +0 -195
  205. package/dist/invalidate-DbxuL2jL.js +0 -18
  206. package/dist/permdock-sql-DImJULY-.d.ts +0 -17
  207. package/dist/plugin-BqR6wKMB.js +0 -23
  208. package/dist/respond-C44EZAJo.js +0 -399
  209. package/dist/respond-DAkPIdTH.d.ts +0 -175
  210. package/dist/result-c2OYgCV2.d.ts +0 -203
  211. package/dist/view-wZ-9qIUz.js +0 -45
@@ -1,20 +1,20 @@
1
- import { n as dbError } from "../errors-Bl3rkOzo.js";
2
- import { i as temporalText } from "../temporal-BwshQk0W.js";
3
- import { t as temporal } from "../temporal-required-CMxeSVPo.js";
4
- import { i as ok } from "../result-DWXatkd6.js";
5
- import { t as BetterSupabase } from "../define-DF341GDj.js";
6
- import { a as toBase64Url, r as fromBase64, t as base64ToText } from "../base64-CQeB1eSf.js";
7
- import { r as resolveConfig } from "../config-Be4GwsqI.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
+ import { a as toBase64Url, r as fromBase64, t as base64ToText } from "../base64-D2gsioCY.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-C57jx2pn.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";
14
14
  import { readFile } from "node:fs/promises";
15
15
  import { dirname, join, resolve } from "node:path";
16
16
  //#region src/testing/jwt.ts
17
- const encoder = new TextEncoder();
17
+ const encoder = /* @__PURE__ */ new TextEncoder();
18
18
  /**
19
19
  * Signs an HS256 access token the local Supabase stack accepts. Test-only:
20
20
  * production tokens come from Supabase Auth.
@@ -44,7 +44,7 @@ async function signTestJwt(secret, claims) {
44
44
  }
45
45
  /**
46
46
  * Accepts `signTestJwt` tokens as users, for API servers tested against the
47
- * local stack: `createServer(sb, { auth: { resolvers: [localAuth(secret)] } })`.
47
+ * local stack: `createServer(betterSupabase, { auth: { resolvers: [localAuth(secret)] } })`.
48
48
  * Tokens that are not HS256-signed with `secret` fall through. Test-only.
49
49
  */
50
50
  function localAuth(secret) {
@@ -198,11 +198,11 @@ async function signFor(stack, url, claims) {
198
198
  * still uses the shared JWT secret.
199
199
  *
200
200
  * ```ts
201
- * const alice = await asUser(sb, { sub: aliceId, tenant_id: acme });
201
+ * const alice = await asUser(betterSupabase, { sub: aliceId, tenant_id: acme });
202
202
  * expect(await alice.db.customers.count().orThrow()).toBe(3);
203
203
  * ```
204
204
  */
205
- async function asUser(sb, claims, stack = {}) {
205
+ async function asUser(betterSupabase, claims, stack = {}) {
206
206
  const url = stack.url ?? env$1(...ENV_VARIABLES.url) ?? "http://127.0.0.1:54321";
207
207
  const publishableKey = stack.publishableKey ?? env$1(...ENV_VARIABLES.publishableKey, "SUPABASE_ANON_KEY");
208
208
  if (!publishableKey) throw new TypeError("asUser needs publishableKey or $SUPABASE_PUBLISHABLE_KEY. Run `better-supabase env` to write it to .env.local.");
@@ -225,8 +225,8 @@ async function asUser(sb, claims, stack = {}) {
225
225
  token,
226
226
  claims: full,
227
227
  supabase,
228
- db: sb.connect(supabase, context),
229
- sql: stack.postgres ? sb.connect(postgresExecutor(stack.postgres.asUser(full)), context) : void 0
228
+ db: betterSupabase.connect(supabase, context),
229
+ sql: stack.postgres ? betterSupabase.connect(postgresExecutor(stack.postgres.asUser(full)), context) : void 0
230
230
  };
231
231
  }
232
232
  //#endregion
@@ -246,7 +246,7 @@ function settle(promise, ms) {
246
246
  /**
247
247
  * Measures every document and RSC navigation response during `during` and
248
248
  * fails when one render makes more database calls or waves than allowed.
249
- * Needs `createNext(sb, { debug })` and `next.debugRoute()` in the app.
249
+ * Needs `createNext(betterSupabase, { debug })` and `bs.debugRoute()` in the app.
250
250
  */
251
251
  async function expectDbBudget(page, expectation) {
252
252
  const seen = [];
@@ -267,12 +267,12 @@ async function expectDbBudget(page, expectation) {
267
267
  } finally {
268
268
  page.off("response", listener);
269
269
  }
270
- if (seen.length === 0) throw new Error("expectDbBudget: no response carried x-bs-request-id. Enable createNext(sb, { debug: { enabled: true } }) and run the proxy.");
270
+ if (seen.length === 0) throw new Error("expectDbBudget: no response carried x-bs-request-id. Enable createNext(betterSupabase, { debug: { enabled: true } }) and run the proxy.");
271
271
  await Promise.all(seen.map(({ response }) => settle(response.finished(), expectation.timeoutMs ?? 1e4)));
272
272
  const renders = [];
273
273
  for (const { response, id, url } of seen) {
274
274
  const reply = await page.request.get(url);
275
- if (!reply.ok()) throw new Error(`expectDbBudget: ${url} answered ${reply.status()}. Mount next.debugRoute() at that path.`);
275
+ if (!reply.ok()) throw new Error(`expectDbBudget: ${url} answered ${reply.status()}. Mount bs.debugRoute() at that path.`);
276
276
  renders.push({
277
277
  url: response.url(),
278
278
  requestId: id,
@@ -348,17 +348,17 @@ const recorder = (ops) => ({
348
348
  }));
349
349
  }
350
350
  });
351
- /** The operation `run` sends to the executor, built by `sb` without touching a database. */
352
- async function captureOp(sb, run) {
351
+ /** The operation `run` sends to the executor, built by `betterSupabase` without touching a database. */
352
+ async function captureOp(betterSupabase, run) {
353
353
  const ops = [];
354
- await run(sb.connect(recorder(ops)));
354
+ await run(betterSupabase.connect(recorder(ops)));
355
355
  const [op] = ops;
356
356
  if (!op) throw new Error("the repository call did not reach the executor");
357
357
  return op;
358
358
  }
359
- function tableKey(sb, table) {
360
- const key = table ?? Object.keys(sb.meta.tables)[0];
361
- if (!key || !(key in sb.meta.tables)) throw new TypeError(`better-supabase: unknown table "${String(key)}"`);
359
+ function tableKey(betterSupabase, table) {
360
+ const key = table ?? Object.keys(betterSupabase.meta.tables)[0];
361
+ if (!key || !(key in betterSupabase.meta.tables)) throw new TypeError(`better-supabase: unknown table "${String(key)}"`);
362
362
  return key;
363
363
  }
364
364
  const hasName = (subject) => ["has a name", () => {
@@ -370,14 +370,14 @@ const hasName = (subject) => ["has a name", () => {
370
370
  * than thrown, and (with `create`) a write round trip.
371
371
  *
372
372
  * ```ts
373
- * it('conforms', () => testExecutor(myExecutor, { sb, table: 'tags', create: { name: 'x' } }));
373
+ * it('conforms', () => testExecutor(myExecutor, { betterSupabase, table: 'tags', create: { name: 'x' } }));
374
374
  * ```
375
375
  */
376
376
  function testExecutor(executor, options) {
377
- const { sb } = options;
378
- const table = tableKey(sb, options.table);
377
+ const { betterSupabase } = options;
378
+ const table = tableKey(betterSupabase, options.table);
379
379
  const context = { errorMappers: [] };
380
- const read = () => captureOp(sb, (db) => db[table]["findMany"]({ limit: 2 }));
380
+ const read = () => captureOp(betterSupabase, (db) => db[table]["findMany"]({ limit: 2 }));
381
381
  const create = options.create;
382
382
  return conform(`Executor "${executor.name}"`, [
383
383
  hasName(executor),
@@ -396,7 +396,7 @@ function testExecutor(executor, options) {
396
396
  }
397
397
  }],
398
398
  ["counts rows", async () => {
399
- const op = await captureOp(sb, (db) => db[table]["count"]());
399
+ const op = await captureOp(betterSupabase, (db) => db[table]["count"]());
400
400
  const result = await executor.execute(op, context);
401
401
  expect(result.ok, `count failed: ${result.ok ? "" : result.error.message}`);
402
402
  expect(typeof result.data.count === "number" && result.data.count >= 0, "count must be a number");
@@ -444,7 +444,7 @@ function testExecutor(executor, options) {
444
444
  }],
445
445
  executor.batch && ["batch returns one result per operation, in order", async () => {
446
446
  const rows = await read();
447
- const count = await captureOp(sb, (db) => db[table]["count"]());
447
+ const count = await captureOp(betterSupabase, (db) => db[table]["count"]());
448
448
  const missing = {
449
449
  ...rows,
450
450
  table: {
@@ -464,12 +464,12 @@ function testExecutor(executor, options) {
464
464
  expect(third?.ok === false, "a failing operation must fail on its own, as a result");
465
465
  }],
466
466
  create && ["round-trips a write", async () => {
467
- const repository = sb.connect(executor)[table];
467
+ const repository = betterSupabase.connect(executor)[table];
468
468
  const created = await repository["create"](create);
469
469
  expect(created.ok, `create failed: ${JSON.stringify(created.error)}`);
470
470
  const row = created.data;
471
471
  for (const [key, value] of Object.entries(create)) expect(same(row[key], value), `created row has ${key} = ${JSON.stringify(row[key])}`);
472
- const [primary] = sb.meta.tables[table].primaryKey;
472
+ const [primary] = betterSupabase.meta.tables[table].primaryKey;
473
473
  expect(primary, "write checks need a table with a primary key");
474
474
  const removed = await repository["delete"](row[primary]);
475
475
  expect(removed.ok, `delete failed: ${JSON.stringify(removed.error)}`);
@@ -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);
@@ -643,12 +784,12 @@ function testGenerator(generator, options) {
643
784
  * results intact when it wraps them.
644
785
  */
645
786
  function testPlugin(plugin, options) {
646
- const { sb } = options;
647
- const table = tableKey(sb, options.table);
787
+ const { betterSupabase } = options;
788
+ const table = tableKey(betterSupabase, options.table);
648
789
  const context = options.context ?? {};
649
790
  const hook = (op) => ({
650
791
  table: op.table,
651
- schema: sb.meta,
792
+ schema: betterSupabase.meta,
652
793
  context,
653
794
  options: {},
654
795
  now: () => temporal().Instant.fromEpochMilliseconds(0)
@@ -660,26 +801,41 @@ function testPlugin(plugin, options) {
660
801
  expect(plugin.apiVersion === 1, `apiVersion is ${String(plugin.apiVersion)}`);
661
802
  }],
662
803
  ["installs and builds repositories", () => {
663
- const db = sb.use(plugin).connect(recorder([]), context);
664
- for (const key of Object.keys(sb.meta.tables)) expect(db[key], `db.${key} is missing`);
804
+ const db = betterSupabase.use(plugin).connect(recorder([]), context);
805
+ for (const key of Object.keys(betterSupabase.meta.tables)) expect(db[key], `db.${key} is missing`);
665
806
  }],
666
807
  plugin.transformQuery && ["transformQuery is pure and deterministic", async () => {
667
- const op = await captureOp(sb, (db) => db[table]["findMany"]({ limit: 1 }));
808
+ const op = await captureOp(betterSupabase, (db) => db[table]["findMany"]({ limit: 1 }));
668
809
  const first = plugin.transformQuery(frozenCopy(op), hook(op));
669
810
  const second = plugin.transformQuery(frozenCopy(op), hook(op));
670
811
  expect(typeof first === "object" && first !== null && first.kind === op.kind, "must return an operation of the same kind");
671
812
  expect(same(first, second), "two calls with the same input differ");
672
813
  }],
673
814
  plugin.beforeMutation && create && ["beforeMutation is pure and deterministic", async () => {
674
- const op = await captureOp(sb, (db) => db[table]["create"](create));
815
+ const op = await captureOp(betterSupabase, (db) => db[table]["create"](create));
675
816
  if (op.kind === "select") throw new Violation("create did not produce a mutation");
676
817
  const first = await plugin.beforeMutation(frozenCopy(op), hook(op));
677
818
  const second = await plugin.beforeMutation(frozenCopy(op), hook(op));
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
- const op = await captureOp(sb, (db) => db[table]["findMany"]({ limit: 1 }));
838
+ const op = await captureOp(betterSupabase, (db) => db[table]["findMany"]({ limit: 1 }));
683
839
  const data = {
684
840
  rows: [{ marker: "conformance" }],
685
841
  count: 1
@@ -723,12 +879,12 @@ function env(names) {
723
879
  /**
724
880
  * Checks row-level security between two tenants against a running stack: for
725
881
  * each table, a user of one tenant must not select, insert, update or delete
726
- * the other tenant's rows. Runs without `sb`'s plugins, so the `tenant()`
882
+ * the other tenant's rows. Runs without `betterSupabase`'s plugins, so the `tenant()`
727
883
  * plugin can't hide a missing policy. Throws a `ConformanceError` naming each
728
884
  * table and command that leaks; removes every row it created.
729
885
  *
730
886
  * ```ts
731
- * await expectTenantIsolation(sb, {
887
+ * await expectTenantIsolation(betterSupabase, {
732
888
  * tenants: [
733
889
  * { id: ACME, claims: { sub: alice, tenant_id: ACME } },
734
890
  * { id: GLOBEX, claims: { sub: bob, tenant_id: GLOBEX } },
@@ -742,9 +898,9 @@ function env(names) {
742
898
  * });
743
899
  * ```
744
900
  */
745
- async function expectTenantIsolation(sb, options) {
901
+ async function expectTenantIsolation(betterSupabase, options) {
746
902
  const stack = options.stack ?? {};
747
- const bare = new BetterSupabase(sb.schema);
903
+ const bare = new BetterSupabase(betterSupabase.schema);
748
904
  const url = stack.url ?? env(ENV_VARIABLES.url) ?? "http://127.0.0.1:54321";
749
905
  const secretKey = stack.secretKey ?? env(ENV_VARIABLES.secretKey);
750
906
  if (!secretKey) throw new TypeError("expectTenantIsolation needs stack.secretKey or $SUPABASE_SECRET_KEY to seed rows.");
@@ -884,14 +1040,14 @@ function statement(table, rows) {
884
1040
  * casing and Insert types; `better-supabase seed` renders them to SQL.
885
1041
  *
886
1042
  * ```ts
887
- * export const seed = defineSeed(sb, {
1043
+ * export const seed = defineSeed(betterSupabase, {
888
1044
  * organizations: { acme: { id: ACME, name: 'Acme' } },
889
1045
  * customers: { first: { organizationId: ACME, name: 'First customer' } },
890
1046
  * });
891
1047
  * ```
892
1048
  */
893
- function defineSeed(sb, fixtures) {
894
- const meta = sb.meta;
1049
+ function defineSeed(betterSupabase, fixtures) {
1050
+ const meta = betterSupabase.meta;
895
1051
  const statements = () => {
896
1052
  const keys = Object.keys(fixtures).filter((key) => fixtures[key] !== void 0);
897
1053
  for (const key of keys) {
@@ -915,4 +1071,176 @@ function defineSeed(sb, 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 };