better-supabase 0.4.0 → 0.5.1

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 (180) hide show
  1. package/README.md +135 -16
  2. package/dist/actor-DaJjqD5R.d.ts +43 -0
  3. package/dist/channel-FQW-CnGR.d.ts +70 -0
  4. package/dist/cli/bin.js +1 -1
  5. package/dist/cli/codemod-DqV7612m.js +388 -0
  6. package/dist/cli/{config-D9R0o1LH.js → config-CPVpXP5I.js} +78 -8
  7. package/dist/cli/{config-SSKSTUl2.js → config-dtoekM0v.js} +1 -1
  8. package/dist/cli/{read-sets-CMa29t9a.js → diff-SjkO5rNr.js} +1 -220
  9. package/dist/cli/{dist-CFgGknmZ.js → dist-DBPdlPYK.js} +4 -4
  10. package/dist/cli/{dist-B1MvLv-M.js → dist-DreZ24Sz.js} +1 -1
  11. package/dist/cli/{dist-Du7VnEUG.js → dist-xn4wlboc.js} +112 -1
  12. package/dist/cli/{doctor-BOym3mbL.js → doctor-C_A-KEAz.js} +533 -50
  13. package/dist/cli/{env-BZ0uy8XK.js → env-DmKv_G4A.js} +1 -1
  14. package/dist/cli/gen-CIdSQZad.js +2 -0
  15. package/dist/cli/{gen-Ccn_p90L.js → gen-CjElD1Vw.js} +321 -88
  16. package/dist/cli/index.d.ts +1 -1
  17. package/dist/cli/index.js +4 -4
  18. package/dist/cli/{init-ClnxRYBF.js → init-DMa6M7hV.js} +2 -2
  19. package/dist/cli/{introspect-DxqfkVIv.js → introspect-BKlaIu_M.js} +2 -2
  20. package/dist/cli/{openapi-8WHjxiTJ.js → openapi-Cp5Fyta-.js} +1 -1
  21. package/dist/cli/read-sets-VQcPgxU9.js +436 -0
  22. package/dist/cli/{run-CP20LKk_.js → run-Bo_NLT5n.js} +16 -12
  23. package/dist/cli/{seed-5J6eu_Xg.js → seed-CUlqWh-8.js} +1 -1
  24. package/dist/cli/sql-CVx-1O8G.js +373 -0
  25. package/dist/cli/{version-5mW_8sY1.js → version-BCtlgDQx.js} +1 -1
  26. package/dist/client/index.d.ts +1 -1
  27. package/dist/client/index.js +3 -10
  28. package/dist/clone-BfgmLU90.js +21 -0
  29. package/dist/{compiler-DB3aSFOA.d.ts → compiler-BXH_aZ7N.d.ts} +2 -2
  30. package/dist/config/index.d.ts +4 -4
  31. package/dist/config/index.js +4 -4
  32. package/dist/{config-C1CYcm6o.d.ts → config-Bv7gvfzC.d.ts} +185 -11
  33. package/dist/{config-CbvFmGD8.js → config-DreERlKo.js} +1 -1
  34. package/dist/{define-CRS2MRV3.d.ts → define-Bp_BZa5h.d.ts} +11 -7
  35. package/dist/{define-Bh0AqTik.js → define-cNKzqwL_.js} +202 -16
  36. package/dist/edge/index.d.ts +16 -5
  37. package/dist/edge/index.js +10 -7
  38. package/dist/{entitlements-Dm-FHpNH.d.ts → entitlements-BiJZ4t49.d.ts} +1 -1
  39. package/dist/env/index.d.ts +2 -2
  40. package/dist/env/index.js +34 -1
  41. package/dist/{errors-L0xK41y_.js → errors-DtwXa6b8.js} +1 -0
  42. package/dist/events/index.d.ts +3 -2
  43. package/dist/events/index.js +65 -12
  44. package/dist/events-aIO7vsIk.d.ts +230 -0
  45. package/dist/{result-CqEsbgYs.d.ts → executor-BunPOO-k.d.ts} +200 -1
  46. package/dist/{executor-CZ7ImRiD.d.ts → executor-Cghdstwu.d.ts} +1 -2
  47. package/dist/{executor-CVt6dD0B.js → executor-XycESQPH.js} +3 -3
  48. package/dist/hono/index.d.ts +17 -6
  49. package/dist/hono/index.js +40 -5
  50. package/dist/impersonation-BgSHAYLf.js +133 -0
  51. package/dist/{resolve-BAqQcwMW.d.ts → impersonation-BmePsuxz.d.ts} +36 -6
  52. package/dist/{index-BhHEC04r.d.ts → index-C7f8cOBM.d.ts} +2 -2
  53. package/dist/{index-Dy6kuF0P.d.ts → index-CLg87OBW.d.ts} +6 -1
  54. package/dist/{index-Cf-D2jk1.d.ts → index-CWMDdEr4.d.ts} +2 -2
  55. package/dist/{index-D5IfUIDh.d.ts → index-DOESKG27.d.ts} +33 -8
  56. package/dist/{index-DFmOpBGC.d.ts → index-DgILJRXj.d.ts} +35 -9
  57. package/dist/{index-RlLXd8dL.d.ts → index-DmXSxbLi.d.ts} +4 -4
  58. package/dist/{index-BBdvo-5d.d.ts → index-Dp2VDwog.d.ts} +49 -6
  59. package/dist/{index-528Ugmyp.d.ts → index-SaTsEfGP.d.ts} +76 -8
  60. package/dist/{index-BAy4tsAP.d.ts → index-fag8VJZ2.d.ts} +3 -4
  61. package/dist/index.d.ts +30 -13
  62. package/dist/index.js +10 -9
  63. package/dist/jobs/index.d.ts +147 -101
  64. package/dist/jobs/index.js +744 -237
  65. package/dist/{json-schema-DP6V7cbr.js → json-schema-m1fiwKgN.js} +22 -1
  66. package/dist/kit-events-CTFDfSwb.js +60 -0
  67. package/dist/kit-transport-B82k1djG.d.ts +33 -0
  68. package/dist/kit-transport-DJlHQUBU.js +55 -0
  69. package/dist/{config-BpxDfnbg.js → kits-CGLLrNPd.js} +37 -9
  70. package/dist/list/index.d.ts +1 -1
  71. package/dist/{live-BHakSWWo.d.ts → live-D9W2M5aO.d.ts} +6 -5
  72. package/dist/mcp/index.d.ts +25 -8
  73. package/dist/mcp/index.js +23 -14
  74. package/dist/{mfa-C7MNQ1Ob.d.ts → mfa-Cu96oJdw.d.ts} +2 -16
  75. package/dist/next/index.d.ts +31 -10
  76. package/dist/next/index.js +136 -27
  77. package/dist/notifications/index.d.ts +162 -0
  78. package/dist/notifications/index.js +264 -0
  79. package/dist/openapi/index.d.ts +1 -1
  80. package/dist/openapi/index.js +2 -2
  81. package/dist/orgs/index.d.ts +132 -0
  82. package/dist/orgs/index.js +245 -0
  83. package/dist/orpc/index.d.ts +28 -8
  84. package/dist/orpc/index.js +16 -3
  85. package/dist/otel/index.d.ts +21 -2
  86. package/dist/otel/index.js +50 -9
  87. package/dist/permdock-sql-C2Inc6Xc.d.ts +29 -0
  88. package/dist/permdock-sql-DgOfeixi.js +51 -0
  89. package/dist/plugin-DwARMuQp.js +59 -0
  90. package/dist/{plugin-bNhvJwRh.d.ts → plugin-o144f322.d.ts} +17 -5
  91. package/dist/plugins/actor/index.d.ts +5 -2
  92. package/dist/plugins/actor/index.js +22 -8
  93. package/dist/plugins/rules/index.d.ts +2 -2
  94. package/dist/plugins/rules/index.js +15 -10
  95. package/dist/plugins/soft-delete/index.d.ts +3 -3
  96. package/dist/plugins/soft-delete/index.js +14 -5
  97. package/dist/plugins/tenant/index.d.ts +2 -2
  98. package/dist/plugins/tenant/index.js +38 -20
  99. package/dist/plugins/timestamps/index.d.ts +1 -1
  100. package/dist/plugins/timestamps/index.js +4 -3
  101. package/dist/plugins/validation/index.d.ts +2 -2
  102. package/dist/plugins/validation/index.js +26 -6
  103. package/dist/{pool-DFwEQwmN.d.ts → pool-C9voM4Kw.d.ts} +17 -6
  104. package/dist/postgres/index.d.ts +5 -5
  105. package/dist/postgres/index.js +20 -11
  106. package/dist/{postgrest-Bc-0Xw62.d.ts → postgrest-CruYKAvV.d.ts} +1 -1
  107. package/dist/{problem-BihjPR0t.js → problem-BW1KhzSG.js} +2 -1
  108. package/dist/{problem-U8UEKccv.d.ts → problem-Blxe8S31.d.ts} +1 -1
  109. package/dist/query/index.d.ts +2 -2
  110. package/dist/query/index.js +3 -3
  111. package/dist/{query-BMMSWbI7.js → query-CCbo44lG.js} +2 -2
  112. package/dist/queue-BYUCjr59.d.ts +252 -0
  113. package/dist/react/index.d.ts +8 -7
  114. package/dist/react/index.js +4 -3
  115. package/dist/react/server.d.ts +14 -3
  116. package/dist/react/server.js +10 -2
  117. package/dist/react/session.d.ts +7 -1
  118. package/dist/react/session.js +9 -1
  119. package/dist/{react-CQHMaESR.js → react-C_s7kiNz.js} +103 -11
  120. package/dist/{read-set-FWK8qQ58.js → read-set-DQFt1N_Y.js} +22 -1
  121. package/dist/realtime/index.d.ts +3 -3
  122. package/dist/realtime/index.js +5 -4
  123. package/dist/{refresh-BwYkyNOQ.d.ts → refresh-CFPX0Qft.d.ts} +1 -1
  124. package/dist/{resource-BJcaIfxi.d.ts → resource-DNRFP7KR.d.ts} +5 -5
  125. package/dist/{resource-My_y-Syh.js → resource-mqfJW4YF.js} +22 -7
  126. package/dist/respond-BR1fJWOB.d.ts +377 -0
  127. package/dist/{result-Bj6yt33D.js → result-BKiliPY7.js} +1 -1
  128. package/dist/server/index.d.ts +15 -12
  129. package/dist/server/index.js +11 -17
  130. package/dist/{server-BA75B0mT.js → server-B1mZ7CgQ.js} +259 -37
  131. package/dist/{shared-DnoCdMDr.js → shared-D1dRUrAg.js} +16 -2
  132. package/dist/signers-Cc6r98SW.d.ts +162 -0
  133. package/dist/{simplify-Cqvfr9dj.js → simplify-_0oUKqCd.js} +4 -115
  134. package/dist/sql/index.d.ts +283 -11
  135. package/dist/sql/index.js +8705 -775
  136. package/dist/{sql-B34HU2DI.js → sql-Coh6n4cC.js} +9 -2
  137. package/dist/ssr/index.d.ts +4 -4
  138. package/dist/ssr/index.js +1 -2
  139. package/dist/{standard-BRyBGTtp.js → standard-D7r6dMUu.js} +6 -3
  140. package/dist/{standard-BsaCYQJc.d.ts → standard-J4YgMOLm.d.ts} +1 -1
  141. package/dist/{stats-C_YxA2r1.js → stats-Bqob3WqT.js} +4 -4
  142. package/dist/storage/index.d.ts +4 -4
  143. package/dist/storage/index.js +155 -26
  144. package/dist/support-CEPwlw2G.d.ts +89 -0
  145. package/dist/support-view-B8SlToQX.d.ts +17 -0
  146. package/dist/support-view-g4YLBeqX.js +18 -0
  147. package/dist/support-wHnK6zp-.js +293 -0
  148. package/dist/{permdock-sql-C4KFzLc9.js → template-TxrGI5jR.js} +2 -31
  149. package/dist/{temporal-BwshQk0W.js → temporal-CVlIV2DZ.js} +17 -4
  150. package/dist/{temporal-required-CBNHT6ni.js → temporal-required-CAt4Dc9Z.js} +2 -2
  151. package/dist/testing/index.d.ts +132 -10
  152. package/dist/testing/index.js +476 -8
  153. package/dist/{types-DONv9SHN.d.ts → types-Dnf_wRDz.d.ts} +26 -3
  154. package/dist/user-change-CHFEUro1.js +49 -0
  155. package/dist/{webhooks-Bh_Jf_-l.js → verify-Ce49ETPz.js} +5 -5
  156. package/dist/{version-Bmf7_FzM.js → version-DOYTYwHP.js} +1 -1
  157. package/dist/{mfa-byfjruR9.js → view-Bn6HNKUe.js} +97 -98
  158. package/dist/{view-B64o81wp.d.ts → view-CbgObAeb.d.ts} +22 -31
  159. package/dist/webhooks/index.d.ts +135 -4
  160. package/dist/webhooks/index.js +588 -2
  161. package/dist/wire-D2dT86vi.js +35 -0
  162. package/package.json +27 -19
  163. package/schemas/config-v1.json +131 -1
  164. package/schemas/doctor-report-v1.json +11 -0
  165. package/skills/better-supabase/SKILL.md +9 -6
  166. package/skills/better-supabase/references/plugins.md +6 -3
  167. package/skills/better-supabase/references/troubleshooting.md +1 -0
  168. package/skills/better-supabase-api/SKILL.md +9 -2
  169. package/skills/better-supabase-api/references/adapters.md +11 -8
  170. package/skills/better-supabase-auth/SKILL.md +18 -7
  171. package/skills/better-supabase-auth/references/permdock.md +78 -11
  172. package/dist/cli/gen-D5cLCULx.js +0 -2
  173. package/dist/cli/sql-D4VbqmXi.js +0 -199
  174. package/dist/events-B21UOhvP.d.ts +0 -84
  175. package/dist/executor-B5IoUxLE.d.ts +0 -195
  176. package/dist/invalidate-CgxmRamI.js +0 -24
  177. package/dist/permdock-sql-DImJULY-.d.ts +0 -17
  178. package/dist/plugin-BqR6wKMB.js +0 -23
  179. package/dist/respond-DgIbdrt8.d.ts +0 -194
  180. package/dist/view-BrJI2LKp.js +0 -45
@@ -1,9 +1,10 @@
1
- import { n as dbError, o as isDbError, r as dbErrorOf } from "./errors-L0xK41y_.js";
2
- import { i as ok, n as err, o as toDbError, t as AsyncResult } from "./result-Bj6yt33D.js";
3
- import { a as postgrestExecutor, n as StatsRecorder } from "./stats-C_YxA2r1.js";
4
- import { a as problemResponse } from "./problem-BihjPR0t.js";
1
+ import { n as dbError, o as isDbError, r as dbErrorOf } from "./errors-DtwXa6b8.js";
2
+ import { i as ok, n as err, o as toDbError, t as AsyncResult } from "./result-BKiliPY7.js";
3
+ import { a as postgrestExecutor, n as StatsRecorder } from "./stats-Bqob3WqT.js";
4
+ import { a as problemResponse } from "./problem-BW1KhzSG.js";
5
+ import { a as delegationOf, i as actorOf, r as userContext, t as actClaim } from "./impersonation-BgSHAYLf.js";
5
6
  import { loadEnv } from "./env/index.js";
6
- import { _ as delegationOf, g as actorOf, i as authContext, o as prefetchJwks, r as checkAal, s as resolveAuth, v as actClaim } from "./mfa-byfjruR9.js";
7
+ import { a as checkAal, c as prefetchJwks, l as rememberVerified, o as authContext, t as isAnonymousUser, u as resolveAuth } from "./view-Bn6HNKUe.js";
7
8
  import { parseCookieHeader, serializeCookieHeader } from "@supabase/ssr";
8
9
  import { createClient } from "@supabase/supabase-js";
9
10
  import { PostgrestClient } from "@supabase/postgrest-js";
@@ -34,14 +35,35 @@ function pinnedUntil(cookies) {
34
35
  const until = Number(value);
35
36
  return value !== void 0 && Number.isFinite(until) ? until : 0;
36
37
  }
37
- /** A `Set-Cookie` value keeping the next requests on the primary for `pinMs`. */
38
- function primaryCookie(pinMs, now = Date.now()) {
39
- return serializeCookieHeader(PRIMARY_COOKIE, String(now + pinMs), {
38
+ /** Options of the `bs-primary-until` cookie, for frameworks that set cookies themselves. */
39
+ function primaryCookieOptions(pinMs) {
40
+ return {
40
41
  path: "/",
41
42
  maxAge: Math.max(1, Math.ceil(pinMs / 1e3)),
42
43
  httpOnly: true,
43
44
  sameSite: "lax"
44
- });
45
+ };
46
+ }
47
+ /** A `Set-Cookie` value keeping the next requests on the primary for `pinMs`. */
48
+ function primaryCookie(pinMs, now = Date.now()) {
49
+ return serializeCookieHeader(PRIMARY_COOKIE, String(now + pinMs), primaryCookieOptions(pinMs));
50
+ }
51
+ /**
52
+ * `response` with the `bs-primary-until` cookie when the request wrote, so the
53
+ * caller's next requests read their own writes. A response with immutable
54
+ * headers is copied.
55
+ */
56
+ function withPrimaryPin(response, replica, pinMs) {
57
+ if (!replica?.wrote) return response;
58
+ const cookie = primaryCookie(pinMs);
59
+ try {
60
+ response.headers.append("set-cookie", cookie);
61
+ return response;
62
+ } catch {
63
+ const copy = new Response(response.body, response);
64
+ copy.headers.append("set-cookie", cookie);
65
+ return copy;
66
+ }
45
67
  }
46
68
  /**
47
69
  * Reads go to `replica` until the context is pinned; writes, and every read
@@ -101,7 +123,7 @@ function deleteAccount(betterSupabase, service, userId, options = {}) {
101
123
  const param = bucket.owner;
102
124
  if (!param) continue;
103
125
  options.signal?.throwIfAborted();
104
- const storage = bucket.connect(client);
126
+ const storage = bucket.connect(client, { allTenants: true });
105
127
  const listed = await storage.list({ [param]: userId }, options.signal ? { signal: options.signal } : {});
106
128
  if (!listed.ok) return listed;
107
129
  const paths = listed.data.map((object) => object.path).filter((path) => {
@@ -140,19 +162,39 @@ function deleteAccount(betterSupabase, service, userId, options = {}) {
140
162
  });
141
163
  }
142
164
  //#endregion
165
+ //#region src/core/json.ts
166
+ /** A `JSON.stringify` replacer: `bigint` values become decimal strings, as the wire codec writes them. */
167
+ function jsonReplacer(_key, value) {
168
+ return typeof value === "bigint" ? value.toString() : value;
169
+ }
170
+ /** `Response.json` that writes `bigint` values as decimal strings instead of throwing. */
171
+ function jsonResponse(data, init = {}) {
172
+ const headers = new Headers(init.headers);
173
+ if (!headers.has("content-type")) headers.set("content-type", "application/json");
174
+ return new Response(JSON.stringify(data, jsonReplacer), {
175
+ ...init,
176
+ headers
177
+ });
178
+ }
179
+ //#endregion
143
180
  //#region src/server/respond.ts
144
- /** The scopes a delegated user token lacks; empty for the user's own session. */
181
+ /**
182
+ * The scopes a delegated user token lacks. Empty for the user's own session,
183
+ * which a support or impersonated session counts as: only an `oauth-client`
184
+ * actor is limited to what the user delegated.
185
+ */
145
186
  function missingScopes(auth, required) {
146
187
  if (required.length === 0) return [];
147
188
  const outcome = actorOf(auth.claims);
148
- if (!outcome.ok || !outcome.actor) return [];
189
+ if (!outcome.ok || outcome.actor?.kind !== "oauth-client") return [];
149
190
  const granted = new Set(delegationOf(auth.claims)?.scopes);
150
191
  return required.filter((scope) => !granted.has(scope));
151
192
  }
152
193
  /** `undefined` when `auth` may pass; otherwise the 401/403 error to send. */
153
194
  function guard(auth, allow = ["user"], aal = "aal1", scopes = []) {
154
195
  if (auth.kind === "invalid") return auth.error;
155
- if (allow.includes(auth.kind)) {
196
+ const anonymous = auth.kind === "user" && isAnonymousUser(auth.claims);
197
+ if (anonymous ? allow.includes("anonymous") : allow.includes(auth.kind)) {
156
198
  const denied = checkAal(auth, aal);
157
199
  if (denied || auth.kind !== "user") return denied;
158
200
  const missing = missingScopes(auth, scopes);
@@ -161,6 +203,7 @@ function guard(auth, allow = ["user"], aal = "aal1", scopes = []) {
161
203
  scopes
162
204
  }) : void 0;
163
205
  }
206
+ if (anonymous) return dbError("forbidden", "Sign up to continue", { code: "ANONYMOUS_USER" });
164
207
  return auth.kind === "anon" ? dbError("unauthorized", "Sign in to continue", { code: "MISSING_CREDENTIALS" }) : dbError("forbidden", `A ${auth.kind} caller is not allowed here`);
165
208
  }
166
209
  function isResult(value) {
@@ -202,7 +245,7 @@ async function respond(run, options = {}) {
202
245
  if (!settled.ok) return problemResponse(settled.error, options);
203
246
  if (settled.data instanceof Response) return settled.data;
204
247
  if (settled.data === void 0) return new Response(null, { status: 204 });
205
- return Response.json(settled.data, { status: options.status ?? 200 });
248
+ return jsonResponse(settled.data, { status: options.status ?? 200 });
206
249
  }
207
250
  /** Prefetching the JWKS would add a network call to every test that builds a server. */
208
251
  function defaultPrefetchJwks() {
@@ -212,7 +255,56 @@ function defaultExpose() {
212
255
  return typeof process !== "undefined" && process.env["NODE_ENV"] === "development";
213
256
  }
214
257
  //#endregion
258
+ //#region src/auth/support-cookie.ts
259
+ /** The cookie that carries the support session id. */
260
+ const SUPPORT_COOKIE = "bs-support";
261
+ /** `Set-Cookie` for a session: HttpOnly, SameSite=Lax, gone when the session expires. */
262
+ function supportCookie(session, options = {}, now = Date.now()) {
263
+ const maxAge = Math.max(0, Math.ceil((session.expiresAt.epochMilliseconds - now) / 1e3));
264
+ return cookieLine(options, encodeURIComponent(session.id), maxAge);
265
+ }
266
+ /** `Set-Cookie` that removes the support cookie. */
267
+ function clearSupportCookie(options = {}) {
268
+ return cookieLine(options, "", 0);
269
+ }
270
+ function cookieLine(options, value, maxAge) {
271
+ return [
272
+ `${options.name ?? "bs-support"}=${value}`,
273
+ `Path=${options.path ?? "/"}`,
274
+ `Max-Age=${String(maxAge)}`,
275
+ "HttpOnly",
276
+ "SameSite=Lax",
277
+ ...options.secure === false ? [] : ["Secure"]
278
+ ].join("; ");
279
+ }
280
+ /** The support session id in a `Cookie` header, or `undefined`. */
281
+ function supportCookieValue(cookieHeader, name = SUPPORT_COOKIE) {
282
+ if (!cookieHeader) return void 0;
283
+ for (const part of cookieHeader.split(";")) {
284
+ const index = part.indexOf("=");
285
+ if (index === -1 || part.slice(0, index).trim() !== name) continue;
286
+ const value = decodeURIComponent(part.slice(index + 1).trim());
287
+ return value === "" ? void 0 : value;
288
+ }
289
+ }
290
+ //#endregion
291
+ //#region src/server/support-off.ts
292
+ const NOT_CONFIGURED = "Support sessions need createServer(betterSupabase, { support: supportSessions({ store }) })";
293
+ const notConfigured = () => AsyncResult.err(dbError("unexpected", NOT_CONFIGURED));
294
+ /** `bs.support` without `ServerOptions.support`: every call fails. */
295
+ const supportOff = {
296
+ start: notConfigured,
297
+ stop: notConfigured,
298
+ revoke: notConfigured,
299
+ current: () => Promise.resolve(void 0),
300
+ list: notConfigured,
301
+ sessionIdOf: () => void 0,
302
+ clearCookie: () => clearSupportCookie()
303
+ };
304
+ //#endregion
215
305
  //#region src/server/server.ts
306
+ /** The header `current_tenant_id()` reads over the Data API. */
307
+ const TENANT_HEADER = "x-bs-tenant";
216
308
  const STATELESS = {
217
309
  persistSession: false,
218
310
  autoRefreshToken: false,
@@ -252,15 +344,24 @@ function createServer(betterSupabase, options = {}) {
252
344
  if (!key) throw new TypeError("admin() needs SUPABASE_SECRET_KEY; it is not set");
253
345
  return key;
254
346
  };
347
+ const customFetch = options.fetch ? { fetch: options.fetch } : void 0;
348
+ const sharedGlobal = customFetch ? { global: customFetch } : {};
255
349
  const serviceClient = () => {
256
- adminClient ??= createClient(env().url, secretKey(), { auth: STATELESS });
350
+ adminClient ??= createClient(env().url, secretKey(), {
351
+ auth: STATELESS,
352
+ ...sharedGlobal
353
+ });
257
354
  return adminClient;
258
355
  };
259
356
  const readUrl = () => options.readUrl === false ? void 0 : options.readUrl?.replace(/\/+$/, "") ?? env().readUrl;
260
357
  /** Clients for the primary share the service and anon clients; others don't. */
261
358
  const supabaseAt = (url, auth, headers = {}) => {
262
- const global = Object.keys(headers).length > 0 ? { global: { headers } } : void 0;
263
- const shared = !global && url === env().url;
359
+ const custom = Object.keys(headers).length > 0;
360
+ const global = custom ? { global: {
361
+ headers,
362
+ ...customFetch
363
+ } } : sharedGlobal;
364
+ const shared = !custom && url === env().url;
264
365
  switch (auth.kind) {
265
366
  case "user": return createClient(url, env().publishableKey, {
266
367
  accessToken: () => Promise.resolve(auth.token),
@@ -276,7 +377,10 @@ function createServer(betterSupabase, options = {}) {
276
377
  auth: STATELESS,
277
378
  ...global
278
379
  });
279
- anonClient ??= createClient(url, env().publishableKey, { auth: STATELESS });
380
+ anonClient ??= createClient(url, env().publishableKey, {
381
+ auth: STATELESS,
382
+ ...sharedGlobal
383
+ });
280
384
  return anonClient;
281
385
  default: return auth;
282
386
  }
@@ -290,11 +394,14 @@ function createServer(betterSupabase, options = {}) {
290
394
  const restAt = (url, auth, headers = {}) => {
291
395
  if (auth.kind !== "user") return supabaseAt(url, auth, headers);
292
396
  const key = env().publishableKey;
293
- return new PostgrestClient(new URL("rest/v1", url.endsWith("/") ? url : `${url}/`).href, { headers: {
294
- ...headers,
295
- apikey: key,
296
- Authorization: `Bearer ${auth.token}`
297
- } });
397
+ return new PostgrestClient(new URL("rest/v1", url.endsWith("/") ? url : `${url}/`).href, {
398
+ headers: {
399
+ ...headers,
400
+ apikey: key,
401
+ Authorization: `Bearer ${auth.token}`
402
+ },
403
+ ...customFetch
404
+ });
298
405
  };
299
406
  /** `$client` stays the full Supabase client, built when first read. */
300
407
  const withLazyClient = (db, client) => Object.defineProperty(db, "$client", {
@@ -311,14 +418,101 @@ function createServer(betterSupabase, options = {}) {
311
418
  });
312
419
  return withLazyClient(db, () => supabase ??= supabaseFor(auth));
313
420
  };
314
- const sqlFor = (claims, context, stats) => {
421
+ const sqlFor = (claims, context, stats, session) => {
315
422
  if (!options.postgres) throw new TypeError("Direct Postgres access needs createServer(betterSupabase, { postgres: createPostgres() })");
316
- return betterSupabase.connect(options.postgres.executorFor(claims), context, stats ? { stats } : {});
423
+ return betterSupabase.connect(session ? options.postgres.executorFor(claims, session) : options.postgres.executorFor(claims), context, stats ? { stats } : {});
424
+ };
425
+ /** `ServerOptions.tenant` outside `context()`, where it can't be awaited. */
426
+ const syncTenant = (request, auth) => {
427
+ const tenant = options.tenant?.(request, auth);
428
+ if (tenant !== void 0 && typeof tenant !== "string") throw new TypeError("ServerOptions.tenant returned a promise: use context(request), or pass { tenant } to contextFromResolution");
429
+ return tenant;
430
+ };
431
+ const pinMs = options.replicas?.pinMs ?? 5e3;
432
+ if (options.support && !options.postgres) throw new TypeError("Support sessions need createServer(betterSupabase, { postgres: createPostgres(), support })");
433
+ const support = options.support ? options.support.create(betterSupabase.events, options.auth?.claims ?? betterSupabase.claimsSchema) : supportOff;
434
+ /**
435
+ * The target's context in a support session. Queries run over direct
436
+ * Postgres with the target's claims plus `act`, read-only unless the
437
+ * session allows writes; nothing reaches Supabase with the admin's token.
438
+ */
439
+ const supportContextFor = (resolution, active, parent, tenant) => {
440
+ const { session, claims } = active;
441
+ const record = (key) => {
442
+ const value = claims[key];
443
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : void 0;
444
+ };
445
+ const appMetadata = record("app_metadata");
446
+ const userMetadata = record("user_metadata");
447
+ const user = {
448
+ id: session.targetUserId,
449
+ role: typeof claims["role"] === "string" ? claims["role"] : "authenticated",
450
+ ...typeof claims["email"] === "string" ? { email: claims["email"] } : {},
451
+ ...appMetadata ? { appMetadata } : {},
452
+ ...userMetadata ? { userMetadata } : {}
453
+ };
454
+ const auth = {
455
+ kind: "user",
456
+ token: "",
457
+ claims,
458
+ user,
459
+ source: "support",
460
+ expiresAt: Math.floor(session.expiresAt.epochMilliseconds / 1e3)
461
+ };
462
+ const scope = tenant ?? session.tenant;
463
+ const base = userContext(user, claims);
464
+ const context = scope === void 0 ? base : {
465
+ ...base,
466
+ tenant: scope
467
+ };
468
+ const sessionOptions = {
469
+ ...session.readOnly ? { readOnly: true } : {},
470
+ ...scope === void 0 ? {} : { settings: { "better_supabase.tenant": scope } }
471
+ };
472
+ const recorder = new StatsRecorder(parent);
473
+ const sqlClaims = claims;
474
+ const unavailable = () => {
475
+ throw new TypeError("ctx.supabase and ctx.db.$client are not available in a support session; ctx.db and ctx.sql run as the target");
476
+ };
477
+ let db;
478
+ let sql;
479
+ return {
480
+ auth,
481
+ resolution,
482
+ support: active,
483
+ get supabase() {
484
+ return unavailable();
485
+ },
486
+ get db() {
487
+ db ??= Object.defineProperty(sqlFor(sqlClaims, context, recorder, sessionOptions), "$client", {
488
+ get: unavailable,
489
+ enumerable: true,
490
+ configurable: true
491
+ });
492
+ return db;
493
+ },
494
+ get sql() {
495
+ sql ??= sqlFor(sqlClaims, context, recorder, sessionOptions);
496
+ return sql;
497
+ },
498
+ replica: void 0,
499
+ stats: () => recorder.snapshot(),
500
+ apply: (response) => resolution.apply(response)
501
+ };
317
502
  };
318
503
  /** Clients and repositories are built on first access: most scopes use one of them. */
319
- const contextFor = (resolution, headers, parent, until = 0) => {
504
+ const contextFor = (resolution, requestHeaders, parent, until = 0, tenant, active) => {
505
+ if (active) return supportContextFor(resolution, active, parent, tenant);
320
506
  const { auth } = resolution;
321
- const context = authContext(auth);
507
+ const context = tenant === void 0 ? authContext(auth) : {
508
+ ...authContext(auth),
509
+ tenant
510
+ };
511
+ const headers = tenant === void 0 ? requestHeaders : {
512
+ ...requestHeaders,
513
+ [TENANT_HEADER]: tenant
514
+ };
515
+ const session = tenant === void 0 ? void 0 : { settings: { "better_supabase.tenant": tenant } };
322
516
  const recorder = new StatsRecorder(parent);
323
517
  let supabase;
324
518
  let db;
@@ -354,13 +548,14 @@ function createServer(betterSupabase, options = {}) {
354
548
  },
355
549
  get sql() {
356
550
  if (!options.postgres || !sqlClaims) return;
357
- sql ??= sqlFor(sqlClaims, context, recorder);
551
+ sql ??= sqlFor(sqlClaims, context, recorder, session);
358
552
  return sql;
359
553
  },
360
- stats: () => recorder.snapshot()
554
+ stats: () => recorder.snapshot(),
555
+ apply: (response) => withPrimaryPin(resolution.apply(response), replica, pinMs)
361
556
  };
362
557
  };
363
- const fromResolution = (resolution, request, contextOptions) => contextFor(resolution, options.headers?.(request) ?? {}, contextOptions.stats, contextOptions.pinnedUntil ?? (readUrl() ? pinnedUntil(resolution.requestCookies) : 0));
558
+ const fromResolution = (resolution, request, contextOptions, tenant) => contextFor(resolution, options.headers?.(request) ?? {}, contextOptions.stats, contextOptions.pinnedUntil ?? (readUrl() ? pinnedUntil(resolution.requestCookies) : 0), tenant, contextOptions.support);
364
559
  const resolve = async (request, resolveOptions = {}) => {
365
560
  const claims = options.auth?.claims ?? betterSupabase.claimsSchema;
366
561
  const userMetadata = betterSupabase.userMetadataSchema;
@@ -371,6 +566,7 @@ function createServer(betterSupabase, options = {}) {
371
566
  ...userMetadata ? { userMetadata } : {},
372
567
  env: env(),
373
568
  ...resolveOptions.refresh === void 0 ? {} : { refresh: resolveOptions.refresh },
569
+ ...resolveOptions.cookies === void 0 ? {} : { cookies: resolveOptions.cookies },
374
570
  onRefresh: (event) => {
375
571
  options.auth?.onRefresh?.(event);
376
572
  betterSupabase.events.emit("refresh", event);
@@ -381,12 +577,16 @@ function createServer(betterSupabase, options = {}) {
381
577
  betterSupabase.events.emit("auth", {
382
578
  source: auth.kind === "user" ? auth.source === "cookie" ? "cookie" : "bearer" : auth.kind === "service" ? "bearer" : "none",
383
579
  ok: auth.kind !== "invalid",
384
- ...auth.kind === "user" ? { userId: auth.user.id } : {}
580
+ ...auth.kind === "user" ? {
581
+ userId: auth.user.id,
582
+ rawSource: auth.source
583
+ } : {},
584
+ ...auth.kind === "anon" || auth.kind === "invalid" ? { reason: auth.reason } : {}
385
585
  });
386
586
  }
387
587
  return resolution;
388
588
  };
389
- return {
589
+ const server = {
390
590
  get env() {
391
591
  return env();
392
592
  },
@@ -399,12 +599,19 @@ function createServer(betterSupabase, options = {}) {
399
599
  headers: {},
400
600
  requestCookies: [],
401
601
  apply: (response) => response
402
- }, {}, contextOptions.stats, contextOptions.pinnedUntil),
602
+ }, {}, contextOptions.stats, contextOptions.pinnedUntil, contextOptions.tenant, contextOptions.support),
403
603
  async context(request, contextOptions = {}) {
404
- const resolution = await resolve(request, { refresh: contextOptions.refresh ?? false });
405
- return fromResolution(resolution, request, contextOptions);
604
+ const resolution = await resolve(request, {
605
+ refresh: contextOptions.refresh ?? false,
606
+ ...contextOptions.cookies === void 0 ? {} : { cookies: contextOptions.cookies }
607
+ });
608
+ const [tenant, active] = await Promise.all([contextOptions.tenant ?? options.tenant?.(request, resolution.auth), contextOptions.support ?? support.current(request, resolution.auth)]);
609
+ return fromResolution(resolution, request, active ? {
610
+ ...contextOptions,
611
+ support: active
612
+ } : contextOptions, tenant);
406
613
  },
407
- contextFromResolution: (resolution, request, contextOptions = {}) => fromResolution(resolution, request, contextOptions),
614
+ contextFromResolution: (resolution, request, contextOptions = {}) => fromResolution(resolution, request, contextOptions, contextOptions.tenant ?? syncTenant(request, resolution.auth)),
408
615
  admin: (context = {}) => betterSupabase.connect(serviceClient(), {
409
616
  actor: {
410
617
  id: "service",
@@ -414,6 +621,7 @@ function createServer(betterSupabase, options = {}) {
414
621
  claims: { role: "service_role" },
415
622
  ...context
416
623
  }),
624
+ support,
417
625
  deleteAccount: (userId, deleteOptions) => deleteAccount(betterSupabase, serviceClient, userId, deleteOptions),
418
626
  actingAs: (userId, claims = {}, impersonation) => {
419
627
  const full = {
@@ -433,6 +641,20 @@ function createServer(betterSupabase, options = {}) {
433
641
  });
434
642
  }
435
643
  };
644
+ verifiedSeeders.set(server, (verified) => rememberVerified({
645
+ ...options.auth,
646
+ env: env()
647
+ }, verified));
648
+ return server;
649
+ }
650
+ const verifiedSeeders = /* @__PURE__ */ new WeakMap();
651
+ /**
652
+ * Hands `server` a bearer token another layer verified for this request
653
+ * (`@supabase/server`'s `withSupabase`), so `context()` doesn't verify it
654
+ * again. See `rememberVerified` for when it is accepted.
655
+ */
656
+ function rememberVerifiedFor(server, verified) {
657
+ return verifiedSeeders.get(server)?.(verified) ?? false;
436
658
  }
437
659
  //#endregion
438
- export { guard as a, settle as c, primaryCookie as d, defaultExpose as i, DEFAULT_PIN_MS as l, extendServer as n, isResult as o, withExtra as r, respond as s, createServer as t, PRIMARY_COOKIE as u };
660
+ export { DEFAULT_PIN_MS as _, withExtra as a, withPrimaryPin as b, supportCookie as c, guard as d, isResult as f, jsonResponse as g, jsonReplacer as h, rememberVerifiedFor as i, supportCookieValue as l, settle as m, createServer as n, SUPPORT_COOKIE as o, respond as p, extendServer as r, clearSupportCookie as s, TENANT_HEADER as t, defaultExpose as u, PRIMARY_COOKIE as v, primaryCookieOptions as y };
@@ -1,4 +1,4 @@
1
- import { n as dbError, t as DbException } from "./errors-L0xK41y_.js";
1
+ import { n as dbError, t as DbException } from "./errors-DtwXa6b8.js";
2
2
  import { t as lookupOf } from "./lookup-BaP5bI27.js";
3
3
  //#region src/plugins/shared.ts
4
4
  /** Database column name for an app column, or `undefined` if it is unknown. */
@@ -35,6 +35,20 @@ function withDefault(row, column, value) {
35
35
  [column]: value
36
36
  };
37
37
  }
38
+ /**
39
+ * Refuses caller-supplied values for columns a plugin fills (timestamps,
40
+ * actor, soft delete) unless the call passes `{ override: true }`. Returns
41
+ * whether the caller may keep its own values.
42
+ */
43
+ function guardManaged(op, columns, plugin, options) {
44
+ if (options["override"] === true) return true;
45
+ const rows = op.kind === "insert" ? op.rows : op.kind === "update" ? [op.set] : [];
46
+ for (const column of columns) {
47
+ if (column === void 0) continue;
48
+ if (rows.some((row) => column in row)) throw new DbException(dbError("invalid_request", `"${column}" on ${op.table.key} is filled by ${plugin}(); pass { override: true } to set it yourself`, { table: op.table.key }));
49
+ }
50
+ return false;
51
+ }
38
52
  /** Upserts that may update existing rows must not overwrite "created" columns. */
39
53
  function insertsOnly(op) {
40
54
  return !op.onConflict || op.onConflict.action === "ignore";
@@ -55,4 +69,4 @@ function toDb(table, row) {
55
69
  return out;
56
70
  }
57
71
  //#endregion
58
- export { isNotNull as a, toDb as c, insertsOnly as i, withDefault as l, equals as n, isNull as o, forbidden as r, toApp as s, dbName as t };
72
+ export { insertsOnly as a, toApp as c, guardManaged as i, toDb as l, equals as n, isNotNull as o, forbidden as r, isNull as s, dbName as t, withDefault as u };
@@ -0,0 +1,162 @@
1
+ /// <reference lib="esnext.temporal" />
2
+ import { t as KitTransport } from "./kit-transport-B82k1djG.js";
3
+ //#region src/webhooks/url-policy.d.ts
4
+ /** Decides whether a webhook may be sent to `url`. Rejecting counts as `false`. */
5
+ type AllowUrl = (url: URL) => boolean | Promise<boolean>;
6
+ /** The addresses a host name resolves to. */
7
+ type ResolveHost = (hostname: string) => Promise<readonly string[]>;
8
+ interface PublicUrlOptions {
9
+ /**
10
+ * Resolves host names so each address can be checked. Defaults to
11
+ * `node:dns` where the runtime has it (Node, Bun, Deno); pass one on other
12
+ * runtimes, or `false` to check only the URL.
13
+ */
14
+ readonly resolve?: ResolveHost | false;
15
+ /** Allow `http:` URLs, e.g. for local development. */
16
+ readonly allowHttp?: boolean;
17
+ /** Host names that skip every check, e.g. an internal receiver you trust. */
18
+ readonly allowHosts?: readonly string[];
19
+ }
20
+ /**
21
+ * Whether an IP address is reachable on the public internet: not loopback,
22
+ * private, link-local (cloud metadata), carrier-grade NAT, multicast or
23
+ * reserved. Returns `false` for anything that isn't an IP address.
24
+ */
25
+ declare function isPublicAddress(address: string): boolean;
26
+ /**
27
+ * The default `allowUrl`: HTTPS only, no credentials in the URL, no local
28
+ * host names, and every address the host resolves to must be public. The
29
+ * delivery transport calls it again for each redirect.
30
+ *
31
+ * DNS can change between this check and the connection. Route webhooks
32
+ * through an egress proxy when that matters.
33
+ */
34
+ declare function publicUrl(options?: PublicUrlOptions): AllowUrl;
35
+ //#endregion
36
+ //#region src/webhooks/http.d.ts
37
+ interface WebhookRequest {
38
+ readonly url: string;
39
+ readonly headers: Readonly<Record<string, string>>;
40
+ readonly body: string;
41
+ readonly signal?: AbortSignal;
42
+ }
43
+ interface WebhookResponse {
44
+ readonly status: number;
45
+ readonly body: string;
46
+ /** Seconds from the `Retry-After` header, when the receiver sent one. */
47
+ readonly retryAfter?: number;
48
+ }
49
+ /**
50
+ * Sends one signed webhook request. Throw `WebhookPolicyError` when the
51
+ * request must never be retried (a blocked URL); any other error retries.
52
+ */
53
+ interface WebhookTransport {
54
+ readonly apiVersion: 1;
55
+ readonly name: string;
56
+ send(request: WebhookRequest): Promise<WebhookResponse>;
57
+ }
58
+ /** A delivery that must not be retried, e.g. a URL `allowUrl` rejected. */
59
+ declare class WebhookPolicyError extends Error {
60
+ override readonly name = "WebhookPolicyError";
61
+ }
62
+ interface FetchTransportOptions {
63
+ readonly fetch?: typeof fetch;
64
+ /** Checked before the first request and before each redirect. */
65
+ readonly allowUrl?: AllowUrl;
66
+ /**
67
+ * Defaults to 0: a redirect fails the delivery without a retry. Each hop
68
+ * you allow is checked with `allowUrl` first.
69
+ */
70
+ readonly maxRedirects?: number;
71
+ /** Per request. Defaults to 10 seconds. */
72
+ readonly timeoutMs?: number;
73
+ /** The response body bytes kept; the rest is not read. Defaults to 64 KiB. */
74
+ readonly maxResponseBytes?: number;
75
+ }
76
+ /** Sends webhooks with `fetch`, following redirects only to allowed URLs. */
77
+ declare function fetchTransport(options?: FetchTransportOptions): WebhookTransport;
78
+ //#endregion
79
+ //#region src/webhooks/secrets.d.ts
80
+ interface RotateSecretOptions {
81
+ /** How long older secrets keep signing, e.g. `'24 hours'` (the default). */
82
+ readonly overlap?: string;
83
+ /** Use this secret instead of a generated one, e.g. when migrating. */
84
+ readonly secret?: string;
85
+ }
86
+ /**
87
+ * Where endpoint signing secrets live. `sqlSecretStore` (Vault or a
88
+ * column, set in `kits.webhooks-out.options.secretStorage`) is the default;
89
+ * implement it for a KMS or a secrets manager.
90
+ */
91
+ interface WebhookSecretStore {
92
+ readonly apiVersion: 1;
93
+ /** The live secrets of an endpoint, newest first. */
94
+ secrets(endpointId: string): Promise<readonly string[]>;
95
+ /** Creates a new secret and returns it once. */
96
+ rotate?(endpointId: string, options?: RotateSecretOptions): Promise<string>;
97
+ }
98
+ /** The kit's `webhook_secrets` and `rotate_webhook_secret` functions. */
99
+ declare function sqlSecretStore(transport: KitTransport, options?: {
100
+ readonly schema?: string;
101
+ }): WebhookSecretStore;
102
+ //#endregion
103
+ //#region src/webhooks/signers.d.ts
104
+ interface WebhookSignInput {
105
+ /** The delivery id, the same on every retry so receivers can deduplicate. */
106
+ readonly id: string;
107
+ readonly body: string;
108
+ readonly timestamp: Temporal.Instant;
109
+ /** The endpoint's live secrets, newest first. Sign with each while rotating. */
110
+ readonly secrets: readonly string[];
111
+ }
112
+ /**
113
+ * Adds the signature headers to an outgoing webhook. `standardWebhooks()`
114
+ * is the default; `hmacSigner()` covers custom header and payload formats.
115
+ */
116
+ interface WebhookSigner {
117
+ readonly apiVersion: 1;
118
+ readonly name: string;
119
+ sign(input: WebhookSignInput): Readonly<Record<string, string>> | Promise<Readonly<Record<string, string>>>;
120
+ }
121
+ interface StandardWebhooksOptions {
122
+ /** Header names, for receivers that expect a prefix such as `svix-`. */
123
+ readonly headers?: {
124
+ readonly id?: string;
125
+ readonly timestamp?: string;
126
+ readonly signature?: string;
127
+ };
128
+ }
129
+ /**
130
+ * Signs with the [Standard Webhooks](https://www.standardwebhooks.com/)
131
+ * scheme: `webhook-id`, `webhook-timestamp` and one `v1,` signature per
132
+ * secret in `webhook-signature`. `verifyWebhook` checks it.
133
+ */
134
+ declare function standardWebhooks(options?: StandardWebhooksOptions): WebhookSigner;
135
+ interface HmacSignerOptions {
136
+ readonly name?: string;
137
+ /** e.g. `x-acme-signature`. */
138
+ readonly signatureHeader: string;
139
+ /** Sends the epoch-seconds timestamp in this header. */
140
+ readonly timestampHeader?: string;
141
+ /** Sends the delivery id in this header. */
142
+ readonly idHeader?: string;
143
+ /** The signed text. Defaults to `${timestamp}.${body}`. */
144
+ readonly content?: (input: {
145
+ readonly id: string;
146
+ readonly timestamp: string;
147
+ readonly body: string;
148
+ }) => string;
149
+ /** Defaults to `hex`. */
150
+ readonly encoding?: "hex" | "base64";
151
+ /** Written before each signature. Defaults to `v1=`. */
152
+ readonly prefix?: string;
153
+ /** Joins the signatures of several secrets. Defaults to `,`. */
154
+ readonly separator?: string;
155
+ }
156
+ /**
157
+ * HMAC-SHA256 with the secret's UTF-8 bytes as the key, for receivers that
158
+ * verify an existing format, e.g. `v1=<hex>` over `timestamp.body`.
159
+ */
160
+ declare function hmacSigner(options: HmacSignerOptions): WebhookSigner;
161
+ //#endregion
162
+ export { PublicUrlOptions as _, hmacSigner as a, publicUrl as b, WebhookSecretStore as c, WebhookPolicyError as d, WebhookRequest as f, AllowUrl as g, fetchTransport as h, WebhookSigner as i, sqlSecretStore as l, WebhookTransport as m, StandardWebhooksOptions as n, standardWebhooks as o, WebhookResponse as p, WebhookSignInput as r, RotateSecretOptions as s, HmacSignerOptions as t, FetchTransportOptions as u, ResolveHost as v, isPublicAddress as y };