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
@@ -0,0 +1,655 @@
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 { r as userContext, t as actClaim } from "./impersonation-VfaGc4Q_.js";
6
+ import { loadEnv } from "./env/index.js";
7
+ import { a as checkAal, b as delegationOf, c as prefetchJwks, l as rememberVerified, o as authContext, t as isAnonymousUser, u as resolveAuth, y as actorOf } from "./view-Bq64ki8-.js";
8
+ import { parseCookieHeader, serializeCookieHeader } from "@supabase/ssr";
9
+ import { createClient } from "@supabase/supabase-js";
10
+ import { PostgrestClient } from "@supabase/postgrest-js";
11
+ //#region src/server/replicas.ts
12
+ /** Cookie holding the epoch ms until which reads stay on the primary. */
13
+ const PRIMARY_COOKIE = "bs-primary-until";
14
+ /** Default time reads stay on the primary after a write. */
15
+ const DEFAULT_PIN_MS = 5e3;
16
+ function replicaState(pinnedUntil = 0, now = Date.now()) {
17
+ let wrote = false;
18
+ const inherited = pinnedUntil > now;
19
+ return {
20
+ get pinned() {
21
+ return wrote || inherited;
22
+ },
23
+ get wrote() {
24
+ return wrote;
25
+ },
26
+ pin() {
27
+ wrote = true;
28
+ }
29
+ };
30
+ }
31
+ /** `bs-primary-until` from a `cookie` header or parsed cookies, or 0. */
32
+ function pinnedUntil(cookies) {
33
+ if (!cookies) return 0;
34
+ const value = (typeof cookies === "string" ? parseCookieHeader(cookies) : cookies).find((cookie) => cookie.name === PRIMARY_COOKIE)?.value;
35
+ const until = Number(value);
36
+ return value !== void 0 && Number.isFinite(until) ? until : 0;
37
+ }
38
+ /** Options of the `bs-primary-until` cookie, for frameworks that set cookies themselves. */
39
+ function primaryCookieOptions(pinMs) {
40
+ return {
41
+ path: "/",
42
+ maxAge: Math.max(1, Math.ceil(pinMs / 1e3)),
43
+ httpOnly: true,
44
+ sameSite: "lax"
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
+ }
67
+ }
68
+ /**
69
+ * Reads go to `replica` until the context is pinned; writes, and every read
70
+ * after a successful write, go to `primary`.
71
+ */
72
+ function routedExecutor(primary, replica, state) {
73
+ const batch = primary.batch?.bind(primary);
74
+ const replicaBatch = replica.batch?.bind(replica);
75
+ return {
76
+ name: primary.name,
77
+ ...primary.functionSources && replica.functionSources ? { functionSources: true } : {},
78
+ async execute(op, context) {
79
+ if (op.kind === "select" && !state.pinned) return replica.execute(op, context);
80
+ const result = await primary.execute(op, context);
81
+ if (op.kind !== "select" && result.ok) state.pin();
82
+ return result;
83
+ },
84
+ ...primary.rpc ? { async rpc(name, args, context) {
85
+ if (context.get && !state.pinned && replica.rpc) return replica.rpc(name, args, context);
86
+ const result = await primary.rpc(name, args, context);
87
+ if (!context.get && result.ok) state.pin();
88
+ return result;
89
+ } } : {},
90
+ ...batch ? { batch: (ops, context) => !state.pinned && replicaBatch ? replicaBatch(ops, context) : batch(ops, context) } : {}
91
+ };
92
+ }
93
+ //#endregion
94
+ //#region src/server/delete-account.ts
95
+ const REMOVE_BATCH = 1e3;
96
+ /** Maps an `AuthApiError` from `auth.admin.deleteUser` to a `DbError`. */
97
+ function fromAuthError(raw) {
98
+ if (isDbError(raw)) return raw;
99
+ const failure = typeof raw === "object" && raw !== null ? raw : {};
100
+ const message = failure.message ?? "Auth request failed";
101
+ const base = {
102
+ table: "auth.users",
103
+ ...failure.code ? { code: failure.code } : {}
104
+ };
105
+ if (failure.code === "user_not_found" || failure.status === 404) return dbError("not_found", message, base);
106
+ if (/database error deleting user/i.test(message)) return dbError("conflict", message, {
107
+ ...base,
108
+ hint: "A foreign key to auth.users without on delete cascade or set null blocks the delete (doctor BS406)"
109
+ });
110
+ if (failure.status === 401) return dbError("unauthorized", message, base);
111
+ if (failure.status === 403) return dbError("forbidden", message, base);
112
+ return failure.name === "AuthRetryableFetchError" || (failure.status ?? 0) >= 500 ? dbError("network", message, base) : dbError("unexpected", message, base);
113
+ }
114
+ /**
115
+ * Removes a user's Storage objects, then the Auth user, then announces the
116
+ * delete. Rows referencing `auth.users` follow their foreign keys.
117
+ */
118
+ function deleteAccount(betterSupabase, service, userId, options = {}) {
119
+ return AsyncResult.from(async () => {
120
+ const client = service();
121
+ const removed = {};
122
+ for (const bucket of options.buckets ?? []) {
123
+ const param = bucket.owner;
124
+ if (!param) continue;
125
+ options.signal?.throwIfAborted();
126
+ const storage = bucket.connect(client, { allTenants: true });
127
+ const listed = await storage.list({ [param]: userId }, options.signal ? { signal: options.signal } : {});
128
+ if (!listed.ok) return listed;
129
+ const paths = listed.data.map((object) => object.path).filter((path) => {
130
+ return bucket.match(path)?.[param] === userId;
131
+ });
132
+ for (let start = 0; start < paths.length; start += REMOVE_BATCH) {
133
+ const batch = await storage.remove(paths.slice(start, start + REMOVE_BATCH));
134
+ if (!batch.ok) return batch;
135
+ }
136
+ removed[bucket.id] = paths.length;
137
+ }
138
+ options.signal?.throwIfAborted();
139
+ try {
140
+ const { error } = await client.auth.admin.deleteUser(userId);
141
+ if (error) return err(fromAuthError(error));
142
+ } catch (cause) {
143
+ return err(fromAuthError(cause));
144
+ }
145
+ const context = options.context ?? {};
146
+ betterSupabase.events.emit("mutation", {
147
+ table: "auth.users",
148
+ kind: "delete",
149
+ rows: [{ id: userId }],
150
+ context
151
+ });
152
+ for (const table of options.cascades ?? []) betterSupabase.events.emit("mutation", {
153
+ table,
154
+ kind: "delete",
155
+ rows: [],
156
+ context
157
+ });
158
+ return ok({
159
+ userId,
160
+ removed
161
+ });
162
+ });
163
+ }
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
180
+ //#region src/server/respond.ts
181
+ /** The scopes a delegated user token lacks; empty for the user's own session. */
182
+ function missingScopes(auth, required) {
183
+ if (required.length === 0) return [];
184
+ const outcome = actorOf(auth.claims);
185
+ if (!outcome.ok || !outcome.actor) return [];
186
+ const granted = new Set(delegationOf(auth.claims)?.scopes);
187
+ return required.filter((scope) => !granted.has(scope));
188
+ }
189
+ /** `undefined` when `auth` may pass; otherwise the 401/403 error to send. */
190
+ function guard(auth, allow = ["user"], aal = "aal1", scopes = []) {
191
+ if (auth.kind === "invalid") return auth.error;
192
+ if (allow.includes(auth.kind)) {
193
+ const denied = checkAal(auth, aal);
194
+ if (denied || auth.kind !== "user") return denied;
195
+ if (isAnonymousUser(auth.claims) && !allow.includes("anonymous") && !allow.includes("anon")) return dbError("forbidden", "Sign up to continue", { code: "ANONYMOUS_USER" });
196
+ const missing = missingScopes(auth, scopes);
197
+ return missing.length > 0 ? dbError("forbidden", `The token lacks the scope ${missing.join(" ")}`, {
198
+ code: "INSUFFICIENT_SCOPE",
199
+ scopes
200
+ }) : void 0;
201
+ }
202
+ return auth.kind === "anon" ? dbError("unauthorized", "Sign in to continue", { code: "MISSING_CREDENTIALS" }) : dbError("forbidden", `A ${auth.kind} caller is not allowed here`);
203
+ }
204
+ function isResult(value) {
205
+ return typeof value === "object" && value !== null && "ok" in value && "data" in value && "error" in value;
206
+ }
207
+ /**
208
+ * Settles a handler's return value: `Result`s and `AsyncResult`s unwrap,
209
+ * thrown `DbException`s (or errors whose `cause` is a `DbError`) become
210
+ * errors, anything else is data. Other thrown
211
+ * errors propagate.
212
+ */
213
+ async function settle(run) {
214
+ try {
215
+ const value = await run();
216
+ if (!isResult(value)) return {
217
+ ok: true,
218
+ data: value
219
+ };
220
+ if (value.ok) return {
221
+ ok: true,
222
+ data: value.data
223
+ };
224
+ return {
225
+ ok: false,
226
+ error: isDbError(value.error) ? value.error : toDbError(value.error)
227
+ };
228
+ } catch (cause) {
229
+ const thrown = dbErrorOf(cause);
230
+ if (thrown) return {
231
+ ok: false,
232
+ error: thrown
233
+ };
234
+ throw cause;
235
+ }
236
+ }
237
+ /** A handler's value as a response: `Response` as is, data as JSON, errors as Problem Details. */
238
+ async function respond(run, options = {}) {
239
+ const settled = await settle(run);
240
+ if (!settled.ok) return problemResponse(settled.error, options);
241
+ if (settled.data instanceof Response) return settled.data;
242
+ if (settled.data === void 0) return new Response(null, { status: 204 });
243
+ return jsonResponse(settled.data, { status: options.status ?? 200 });
244
+ }
245
+ /** Prefetching the JWKS would add a network call to every test that builds a server. */
246
+ function defaultPrefetchJwks() {
247
+ return typeof process === "undefined" || process.env["NODE_ENV"] !== "test";
248
+ }
249
+ function defaultExpose() {
250
+ return typeof process !== "undefined" && process.env["NODE_ENV"] === "development";
251
+ }
252
+ //#endregion
253
+ //#region src/auth/support-cookie.ts
254
+ /** The cookie that carries the support session id. */
255
+ const SUPPORT_COOKIE = "bs-support";
256
+ /** `Set-Cookie` for a session: HttpOnly, SameSite=Lax, gone when the session expires. */
257
+ function supportCookie(session, options = {}, now = Date.now()) {
258
+ const maxAge = Math.max(0, Math.ceil((session.expiresAt.epochMilliseconds - now) / 1e3));
259
+ return cookieLine(options, encodeURIComponent(session.id), maxAge);
260
+ }
261
+ /** `Set-Cookie` that removes the support cookie. */
262
+ function clearSupportCookie(options = {}) {
263
+ return cookieLine(options, "", 0);
264
+ }
265
+ function cookieLine(options, value, maxAge) {
266
+ return [
267
+ `${options.name ?? "bs-support"}=${value}`,
268
+ `Path=${options.path ?? "/"}`,
269
+ `Max-Age=${String(maxAge)}`,
270
+ "HttpOnly",
271
+ "SameSite=Lax",
272
+ ...options.secure === false ? [] : ["Secure"]
273
+ ].join("; ");
274
+ }
275
+ /** The support session id in a `Cookie` header, or `undefined`. */
276
+ function supportCookieValue(cookieHeader, name = SUPPORT_COOKIE) {
277
+ if (!cookieHeader) return void 0;
278
+ for (const part of cookieHeader.split(";")) {
279
+ const index = part.indexOf("=");
280
+ if (index === -1 || part.slice(0, index).trim() !== name) continue;
281
+ const value = decodeURIComponent(part.slice(index + 1).trim());
282
+ return value === "" ? void 0 : value;
283
+ }
284
+ }
285
+ //#endregion
286
+ //#region src/server/support-off.ts
287
+ const NOT_CONFIGURED = "Support sessions need createServer(betterSupabase, { support: supportSessions({ store }) })";
288
+ const notConfigured = () => AsyncResult.err(dbError("unexpected", NOT_CONFIGURED));
289
+ /** `bs.support` without `ServerOptions.support`: every call fails. */
290
+ const supportOff = {
291
+ start: notConfigured,
292
+ stop: notConfigured,
293
+ revoke: notConfigured,
294
+ current: () => Promise.resolve(void 0),
295
+ list: notConfigured,
296
+ sessionIdOf: () => void 0,
297
+ clearCookie: () => clearSupportCookie()
298
+ };
299
+ //#endregion
300
+ //#region src/server/server.ts
301
+ /** The header `current_tenant_id()` reads over the Data API. */
302
+ const TENANT_HEADER = "x-bs-tenant";
303
+ const STATELESS = {
304
+ persistSession: false,
305
+ autoRefreshToken: false,
306
+ detectSessionInUrl: false
307
+ };
308
+ /** `{ ...server, ...extra }` without reading the lazy `env` getter. */
309
+ function extendServer(server, extra) {
310
+ return Object.defineProperties({}, {
311
+ ...Object.getOwnPropertyDescriptors(server),
312
+ ...Object.getOwnPropertyDescriptors(extra)
313
+ });
314
+ }
315
+ /** `{ ...base, ...extra }` that keeps lazy getters lazy. */
316
+ function withExtra(base, extra) {
317
+ return Object.defineProperties({}, {
318
+ ...Object.getOwnPropertyDescriptors(base),
319
+ ...Object.getOwnPropertyDescriptors(extra)
320
+ });
321
+ }
322
+ /**
323
+ * Server-side entry point for APIs, jobs, MCP servers and framework
324
+ * adapters. Holds the secrets; `betterSupabase` stays isomorphic.
325
+ */
326
+ function createServer(betterSupabase, options = {}) {
327
+ let loaded;
328
+ const env = () => loaded ??= options.env ?? loadEnv();
329
+ let adminClient;
330
+ let anonClient;
331
+ if ((options.prefetchJwks ?? defaultPrefetchJwks()) && options.auth?.jwks === void 0) try {
332
+ prefetchJwks({
333
+ ...options.auth,
334
+ env: env()
335
+ });
336
+ } catch {}
337
+ const secretKey = () => {
338
+ const key = env().secretKey;
339
+ if (!key) throw new TypeError("admin() needs SUPABASE_SECRET_KEY; it is not set");
340
+ return key;
341
+ };
342
+ const customFetch = options.fetch ? { fetch: options.fetch } : void 0;
343
+ const sharedGlobal = customFetch ? { global: customFetch } : {};
344
+ const serviceClient = () => {
345
+ adminClient ??= createClient(env().url, secretKey(), {
346
+ auth: STATELESS,
347
+ ...sharedGlobal
348
+ });
349
+ return adminClient;
350
+ };
351
+ const readUrl = () => options.readUrl === false ? void 0 : options.readUrl?.replace(/\/+$/, "") ?? env().readUrl;
352
+ /** Clients for the primary share the service and anon clients; others don't. */
353
+ const supabaseAt = (url, auth, headers = {}) => {
354
+ const custom = Object.keys(headers).length > 0;
355
+ const global = custom ? { global: {
356
+ headers,
357
+ ...customFetch
358
+ } } : sharedGlobal;
359
+ const shared = !custom && url === env().url;
360
+ switch (auth.kind) {
361
+ case "user": return createClient(url, env().publishableKey, {
362
+ accessToken: () => Promise.resolve(auth.token),
363
+ ...global
364
+ });
365
+ case "service": return shared ? serviceClient() : createClient(url, secretKey(), {
366
+ auth: STATELESS,
367
+ ...global
368
+ });
369
+ case "anon":
370
+ case "invalid":
371
+ if (!shared) return createClient(url, env().publishableKey, {
372
+ auth: STATELESS,
373
+ ...global
374
+ });
375
+ anonClient ??= createClient(url, env().publishableKey, {
376
+ auth: STATELESS,
377
+ ...sharedGlobal
378
+ });
379
+ return anonClient;
380
+ default: return auth;
381
+ }
382
+ };
383
+ const supabaseFor = (auth, headers = {}) => supabaseAt(env().url, auth, headers);
384
+ /**
385
+ * The client queries run on. A user's requests need only PostgREST, so they
386
+ * skip building Realtime, Storage and Auth clients per request; the shared
387
+ * service and anon clients are built once anyway.
388
+ */
389
+ const restAt = (url, auth, headers = {}) => {
390
+ if (auth.kind !== "user") return supabaseAt(url, auth, headers);
391
+ const key = env().publishableKey;
392
+ return new PostgrestClient(new URL("rest/v1", url.endsWith("/") ? url : `${url}/`).href, {
393
+ headers: {
394
+ ...headers,
395
+ apikey: key,
396
+ Authorization: `Bearer ${auth.token}`
397
+ },
398
+ ...customFetch
399
+ });
400
+ };
401
+ /** `$client` stays the full Supabase client, built when first read. */
402
+ const withLazyClient = (db, client) => Object.defineProperty(db, "$client", {
403
+ get: client,
404
+ enumerable: true,
405
+ configurable: true
406
+ });
407
+ const dbFor = (auth, context = {}) => {
408
+ let supabase;
409
+ const rest = restAt(env().url, auth);
410
+ const db = betterSupabase.connect(rest, {
411
+ ...authContext(auth),
412
+ ...context
413
+ });
414
+ return withLazyClient(db, () => supabase ??= supabaseFor(auth));
415
+ };
416
+ const sqlFor = (claims, context, stats, session) => {
417
+ if (!options.postgres) throw new TypeError("Direct Postgres access needs createServer(betterSupabase, { postgres: createPostgres() })");
418
+ return betterSupabase.connect(session ? options.postgres.executorFor(claims, session) : options.postgres.executorFor(claims), context, stats ? { stats } : {});
419
+ };
420
+ /** `ServerOptions.tenant` outside `context()`, where it can't be awaited. */
421
+ const syncTenant = (request, auth) => {
422
+ const tenant = options.tenant?.(request, auth);
423
+ if (tenant !== void 0 && typeof tenant !== "string") throw new TypeError("ServerOptions.tenant returned a promise: use context(request), or pass { tenant } to contextFromResolution");
424
+ return tenant;
425
+ };
426
+ const pinMs = options.replicas?.pinMs ?? 5e3;
427
+ if (options.support && !options.postgres) throw new TypeError("Support sessions need createServer(betterSupabase, { postgres: createPostgres(), support })");
428
+ const support = options.support ? options.support.create(betterSupabase.events, options.auth?.claims ?? betterSupabase.claimsSchema) : supportOff;
429
+ /**
430
+ * The target's context in a support session. Queries run over direct
431
+ * Postgres with the target's claims plus `act`, read-only unless the
432
+ * session allows writes; nothing reaches Supabase with the admin's token.
433
+ */
434
+ const supportContextFor = (resolution, active, parent, tenant) => {
435
+ const { session, claims } = active;
436
+ const record = (key) => {
437
+ const value = claims[key];
438
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : void 0;
439
+ };
440
+ const appMetadata = record("app_metadata");
441
+ const userMetadata = record("user_metadata");
442
+ const user = {
443
+ id: session.targetUserId,
444
+ role: typeof claims["role"] === "string" ? claims["role"] : "authenticated",
445
+ ...typeof claims["email"] === "string" ? { email: claims["email"] } : {},
446
+ ...appMetadata ? { appMetadata } : {},
447
+ ...userMetadata ? { userMetadata } : {}
448
+ };
449
+ const auth = {
450
+ kind: "user",
451
+ token: "",
452
+ claims,
453
+ user,
454
+ source: "support",
455
+ expiresAt: Math.floor(session.expiresAt.epochMilliseconds / 1e3)
456
+ };
457
+ const scope = tenant ?? session.tenant;
458
+ const base = userContext(user, claims);
459
+ const context = scope === void 0 ? base : {
460
+ ...base,
461
+ tenant: scope
462
+ };
463
+ const sessionOptions = {
464
+ ...session.readOnly ? { readOnly: true } : {},
465
+ ...scope === void 0 ? {} : { settings: { "better_supabase.tenant": scope } }
466
+ };
467
+ const recorder = new StatsRecorder(parent);
468
+ const sqlClaims = claims;
469
+ const unavailable = () => {
470
+ 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");
471
+ };
472
+ let db;
473
+ let sql;
474
+ return {
475
+ auth,
476
+ resolution,
477
+ support: active,
478
+ get supabase() {
479
+ return unavailable();
480
+ },
481
+ get db() {
482
+ db ??= Object.defineProperty(sqlFor(sqlClaims, context, recorder, sessionOptions), "$client", {
483
+ get: unavailable,
484
+ enumerable: true,
485
+ configurable: true
486
+ });
487
+ return db;
488
+ },
489
+ get sql() {
490
+ sql ??= sqlFor(sqlClaims, context, recorder, sessionOptions);
491
+ return sql;
492
+ },
493
+ replica: void 0,
494
+ stats: () => recorder.snapshot(),
495
+ apply: (response) => resolution.apply(response)
496
+ };
497
+ };
498
+ /** Clients and repositories are built on first access: most scopes use one of them. */
499
+ const contextFor = (resolution, requestHeaders, parent, until = 0, tenant, active) => {
500
+ if (active) return supportContextFor(resolution, active, parent, tenant);
501
+ const { auth } = resolution;
502
+ const context = tenant === void 0 ? authContext(auth) : {
503
+ ...authContext(auth),
504
+ tenant
505
+ };
506
+ const headers = tenant === void 0 ? requestHeaders : {
507
+ ...requestHeaders,
508
+ [TENANT_HEADER]: tenant
509
+ };
510
+ const session = tenant === void 0 ? void 0 : { settings: { "better_supabase.tenant": tenant } };
511
+ const recorder = new StatsRecorder(parent);
512
+ let supabase;
513
+ let db;
514
+ let sql;
515
+ const sqlClaims = auth.kind === "user" ? auth.claims : auth.kind === "anon" ? { role: "anon" } : void 0;
516
+ const client = () => supabase ??= supabaseFor(auth, headers);
517
+ let replica;
518
+ const replicaUrl = () => {
519
+ const url = readUrl();
520
+ if (url) replica ??= replicaState(until);
521
+ return url;
522
+ };
523
+ return {
524
+ auth,
525
+ resolution,
526
+ get supabase() {
527
+ return client();
528
+ },
529
+ get db() {
530
+ if (db) return db;
531
+ const url = replicaUrl();
532
+ const rest = supabase ?? restAt(env().url, auth, headers);
533
+ const connected = betterSupabase.connect(rest, context, {
534
+ stats: recorder,
535
+ ...url && replica ? { executor: routedExecutor(postgrestExecutor(rest), postgrestExecutor(restAt(url, auth, headers)), replica) } : {}
536
+ });
537
+ db = withLazyClient(connected, client);
538
+ return db;
539
+ },
540
+ get replica() {
541
+ replicaUrl();
542
+ return replica;
543
+ },
544
+ get sql() {
545
+ if (!options.postgres || !sqlClaims) return;
546
+ sql ??= sqlFor(sqlClaims, context, recorder, session);
547
+ return sql;
548
+ },
549
+ stats: () => recorder.snapshot(),
550
+ apply: (response) => withPrimaryPin(resolution.apply(response), replica, pinMs)
551
+ };
552
+ };
553
+ const fromResolution = (resolution, request, contextOptions, tenant) => contextFor(resolution, options.headers?.(request) ?? {}, contextOptions.stats, contextOptions.pinnedUntil ?? (readUrl() ? pinnedUntil(resolution.requestCookies) : 0), tenant, contextOptions.support);
554
+ const resolve = async (request, resolveOptions = {}) => {
555
+ const claims = options.auth?.claims ?? betterSupabase.claimsSchema;
556
+ const userMetadata = betterSupabase.userMetadataSchema;
557
+ const resolution = await resolveAuth(request, {
558
+ logger: betterSupabase.events.logger,
559
+ ...options.auth,
560
+ ...claims ? { claims } : {},
561
+ ...userMetadata ? { userMetadata } : {},
562
+ env: env(),
563
+ ...resolveOptions.refresh === void 0 ? {} : { refresh: resolveOptions.refresh },
564
+ ...resolveOptions.cookies === void 0 ? {} : { cookies: resolveOptions.cookies },
565
+ onRefresh: (event) => {
566
+ options.auth?.onRefresh?.(event);
567
+ betterSupabase.events.emit("refresh", event);
568
+ }
569
+ });
570
+ if (betterSupabase.events.has("auth")) {
571
+ const { auth } = resolution;
572
+ betterSupabase.events.emit("auth", {
573
+ source: auth.kind === "user" ? auth.source === "cookie" ? "cookie" : "bearer" : auth.kind === "service" ? "bearer" : "none",
574
+ ok: auth.kind !== "invalid",
575
+ ...auth.kind === "user" ? {
576
+ userId: auth.user.id,
577
+ rawSource: auth.source
578
+ } : {},
579
+ ...auth.kind === "anon" || auth.kind === "invalid" ? { reason: auth.reason } : {}
580
+ });
581
+ }
582
+ return resolution;
583
+ };
584
+ const server = {
585
+ get env() {
586
+ return env();
587
+ },
588
+ resolve,
589
+ supabaseFor,
590
+ dbFor,
591
+ contextFor: (auth, contextOptions = {}) => contextFor({
592
+ auth,
593
+ cookies: [],
594
+ headers: {},
595
+ requestCookies: [],
596
+ apply: (response) => response
597
+ }, {}, contextOptions.stats, contextOptions.pinnedUntil, contextOptions.tenant, contextOptions.support),
598
+ async context(request, contextOptions = {}) {
599
+ const resolution = await resolve(request, {
600
+ refresh: contextOptions.refresh ?? false,
601
+ ...contextOptions.cookies === void 0 ? {} : { cookies: contextOptions.cookies }
602
+ });
603
+ const [tenant, active] = await Promise.all([contextOptions.tenant ?? options.tenant?.(request, resolution.auth), contextOptions.support ?? support.current(request, resolution.auth)]);
604
+ return fromResolution(resolution, request, active ? {
605
+ ...contextOptions,
606
+ support: active
607
+ } : contextOptions, tenant);
608
+ },
609
+ contextFromResolution: (resolution, request, contextOptions = {}) => fromResolution(resolution, request, contextOptions, contextOptions.tenant ?? syncTenant(request, resolution.auth)),
610
+ admin: (context = {}) => betterSupabase.connect(serviceClient(), {
611
+ actor: {
612
+ id: "service",
613
+ kind: "service",
614
+ role: "service_role"
615
+ },
616
+ claims: { role: "service_role" },
617
+ ...context
618
+ }),
619
+ support,
620
+ deleteAccount: (userId, deleteOptions) => deleteAccount(betterSupabase, serviceClient, userId, deleteOptions),
621
+ actingAs: (userId, claims = {}, impersonation) => {
622
+ const full = {
623
+ role: "authenticated",
624
+ ...claims,
625
+ sub: userId,
626
+ ...impersonation ? { act: actClaim(impersonation) } : {}
627
+ };
628
+ return sqlFor(full, {
629
+ actor: {
630
+ id: userId,
631
+ kind: "user",
632
+ role: full.role ?? "authenticated",
633
+ ...impersonation ? { impersonator: impersonation.actor } : {}
634
+ },
635
+ claims: full
636
+ });
637
+ }
638
+ };
639
+ verifiedSeeders.set(server, (verified) => rememberVerified({
640
+ ...options.auth,
641
+ env: env()
642
+ }, verified));
643
+ return server;
644
+ }
645
+ const verifiedSeeders = /* @__PURE__ */ new WeakMap();
646
+ /**
647
+ * Hands `server` a bearer token another layer verified for this request
648
+ * (`@supabase/server`'s `withSupabase`), so `context()` doesn't verify it
649
+ * again. See `rememberVerified` for when it is accepted.
650
+ */
651
+ function rememberVerifiedFor(server, verified) {
652
+ return verifiedSeeders.get(server)?.(verified) ?? false;
653
+ }
654
+ //#endregion
655
+ 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,5 @@
1
- import { n as dbError, t as DbException } from "./errors-Bl3rkOzo.js";
1
+ import { n as dbError, t as DbException } from "./errors-DtwXa6b8.js";
2
+ import { t as lookupOf } from "./lookup-BaP5bI27.js";
2
3
  //#region src/plugins/shared.ts
3
4
  /** Database column name for an app column, or `undefined` if it is unknown. */
4
5
  function dbName(table, app) {
@@ -34,6 +35,20 @@ function withDefault(row, column, value) {
34
35
  [column]: value
35
36
  };
36
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
+ }
37
52
  /** Upserts that may update existing rows must not overwrite "created" columns. */
38
53
  function insertsOnly(op) {
39
54
  return !op.onConflict || op.onConflict.action === "ignore";
@@ -44,14 +59,14 @@ function forbidden(message) {
44
59
  /** Database-keyed row to app-keyed row. */
45
60
  function toApp(table, row) {
46
61
  const out = {};
47
- for (const [app, meta] of Object.entries(table.columns)) if (meta.db in row) out[app] = row[meta.db];
62
+ for (const [app, meta] of lookupOf(table).entries) if (meta.db in row) out[app] = row[meta.db];
48
63
  return out;
49
64
  }
50
65
  /** App-keyed row to database-keyed row. Unknown keys are dropped. */
51
66
  function toDb(table, row) {
52
67
  const out = {};
53
- for (const [app, meta] of Object.entries(table.columns)) if (app in row) out[meta.db] = row[app];
68
+ for (const [app, meta] of lookupOf(table).entries) if (app in row) out[meta.db] = row[app];
54
69
  return out;
55
70
  }
56
71
  //#endregion
57
- 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 };