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,49 @@
1
+ //#region src/query/invalidate.ts
2
+ function isString(value) {
3
+ return typeof value === "string";
4
+ }
5
+ function queryTables(query) {
6
+ const tables = query.meta?.["bsTables"];
7
+ if (Array.isArray(tables)) {
8
+ const list = tables;
9
+ return list.every(isString) ? list : list.filter(isString);
10
+ }
11
+ const [prefix, table] = query.queryKey;
12
+ return prefix === "bs" && typeof table === "string" ? [table] : void 0;
13
+ }
14
+ /**
15
+ * Invalidates every query that read one of `tables`: better-supabase queries
16
+ * by their `meta.bsTables`, and other `['bs', table, ...]` keys by prefix.
17
+ */
18
+ function invalidateTables(client, tables) {
19
+ if (tables.length === 0) return Promise.resolve();
20
+ const changed = new Set(tables);
21
+ return client.invalidateQueries({ predicate: (query) => queryTables(query)?.some((table) => changed.has(table)) ?? false });
22
+ }
23
+ //#endregion
24
+ //#region src/query/user-change.ts
25
+ /**
26
+ * Removes every `['bs', ...]` query when the signed-in user changes or signs
27
+ * out, so the next user never sees the previous user's rows. Returns the
28
+ * function that stops listening. `BetterSupabaseProvider` calls it for React;
29
+ * call it yourself with Vue, Solid, Svelte or Angular Query.
30
+ *
31
+ * ```ts
32
+ * const stop = clearOnUserChange(queryClient, bs.auth);
33
+ * ```
34
+ */
35
+ function clearOnUserChange(queryClient, auth) {
36
+ let user;
37
+ const check = () => {
38
+ const snapshot = auth.current();
39
+ if (snapshot.status === "loading") return;
40
+ const id = snapshot.user?.id ?? null;
41
+ if (user !== void 0 && user !== id) queryClient.removeQueries({ queryKey: ["bs"] });
42
+ user = id;
43
+ };
44
+ const stop = auth.subscribe(check);
45
+ check();
46
+ return stop;
47
+ }
48
+ //#endregion
49
+ export { invalidateTables as n, clearOnUserChange as t };
@@ -1,11 +1,11 @@
1
- import { n as dbError } from "./errors-Bl3rkOzo.js";
2
- import { n as nowInstant, r as optionalTemporal } from "./temporal-BwshQk0W.js";
3
- import { n as temporalMissing } from "./temporal-required-CMxeSVPo.js";
4
- import { i as ok, n as err } from "./result-DWXatkd6.js";
5
- import { i as toBase64, r as fromBase64 } from "./base64-CQeB1eSf.js";
6
- import { s as toApp } from "./shared-_6-uPL7E.js";
1
+ import { n as dbError } from "./errors-DtwXa6b8.js";
2
+ import { n as nowInstant, r as optionalTemporal } from "./temporal-CVlIV2DZ.js";
3
+ import { n as temporalMissing } from "./temporal-required-CAt4Dc9Z.js";
4
+ import { i as ok, n as err } from "./result-BKiliPY7.js";
5
+ import { i as toBase64, r as fromBase64 } from "./base64-D2gsioCY.js";
6
+ import { c as toApp } from "./shared-D1dRUrAg.js";
7
7
  //#region src/webhooks/verify.ts
8
- const encoder = new TextEncoder();
8
+ const encoder = /* @__PURE__ */ new TextEncoder();
9
9
  /** Accepts `whsec_…`, `v1,whsec_…` (Supabase Auth hooks) or raw base64. */
10
10
  function secretBytes(secret) {
11
11
  const raw = secret.replace(/^v1,/, "").replace(/^whsec_/, "");
@@ -15,12 +15,31 @@ function secretBytes(secret) {
15
15
  throw new TypeError("Webhook secret is not valid base64");
16
16
  }
17
17
  }
18
+ /** Secrets come from configuration, so a handful of keys suffice. */
19
+ const keys = /* @__PURE__ */ new Map();
20
+ function keyFor(secret) {
21
+ let key = keys.get(secret);
22
+ if (!key) {
23
+ key = crypto.subtle.importKey("raw", secretBytes(secret), {
24
+ name: "HMAC",
25
+ hash: "SHA-256"
26
+ }, false, ["sign", "verify"]);
27
+ if (keys.size < 16) {
28
+ keys.set(secret, key);
29
+ key.catch(() => keys.delete(secret));
30
+ }
31
+ }
32
+ return key;
33
+ }
18
34
  async function signature(secret, content) {
19
- const key = await crypto.subtle.importKey("raw", secretBytes(secret), {
20
- name: "HMAC",
21
- hash: "SHA-256"
22
- }, false, ["sign"]);
23
- return toBase64(new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(content))));
35
+ return toBase64(new Uint8Array(await crypto.subtle.sign("HMAC", await keyFor(secret), encoder.encode(content))));
36
+ }
37
+ function decodeSignature(value) {
38
+ try {
39
+ return fromBase64(value);
40
+ } catch {
41
+ return;
42
+ }
24
43
  }
25
44
  /** Constant-time string comparison. */
26
45
  function timingSafeEqual(a, b) {
@@ -51,7 +70,6 @@ async function signWebhook(secret, message) {
51
70
  */
52
71
  async function verifyWebhook(input, secrets, options = {}) {
53
72
  const headers = input.headers;
54
- const body = input instanceof Request ? await input.text() : input.body;
55
73
  const id = header(headers, "webhook-id");
56
74
  const timestamp = header(headers, "webhook-timestamp");
57
75
  const signatures = header(headers, "webhook-signature");
@@ -64,12 +82,13 @@ async function verifyWebhook(input, secrets, options = {}) {
64
82
  const tolerance = options.tolerance ?? 300;
65
83
  if (seconds < now - tolerance) return err(unauthorized("WEBHOOK_TIMESTAMP_TOO_OLD", "Webhook timestamp is too old"));
66
84
  if (seconds > now + tolerance) return err(unauthorized("WEBHOOK_TIMESTAMP_TOO_NEW", "Webhook timestamp is in the future"));
67
- const content = `${id}.${timestamp}.${body}`;
68
- const offered = signatures.split(" ").map((entry) => entry.split(",")).filter(([version, value]) => version === "v1" && value).map(([, value]) => value);
85
+ const body = input instanceof Request ? await input.text() : input.body;
86
+ const content = encoder.encode(`${id}.${timestamp}.${body}`);
87
+ const offered = signatures.split(" ").map((entry) => entry.split(",")).filter(([version, value]) => version === "v1" && value).flatMap(([, value]) => decodeSignature(value) ?? []);
69
88
  let valid = false;
70
89
  for (const secret of typeof secrets === "string" ? [secrets] : secrets) {
71
- const expected = await signature(secret, content);
72
- for (const candidate of offered) if (timingSafeEqual(candidate, expected)) valid = true;
90
+ const key = await keyFor(secret);
91
+ for (const candidate of offered) if (await crypto.subtle.verify("HMAC", key, candidate, content)) valid = true;
73
92
  }
74
93
  if (!valid) return err(unauthorized("WEBHOOK_INVALID_SIGNATURE", "Invalid webhook signature"));
75
94
  try {
@@ -127,8 +146,8 @@ function isDatabaseWebhook(value) {
127
146
  * Reads a database webhook payload for `table` as an app-cased change.
128
147
  * Returns `null` for other tables or payloads that aren't database webhooks.
129
148
  */
130
- function databaseChange(sb, table, payload) {
131
- const meta = sb.meta.tables[table];
149
+ function databaseChange(betterSupabase, table, payload) {
150
+ const meta = betterSupabase.meta.tables[table];
132
151
  if (!meta || !isDatabaseWebhook(payload) || payload.table !== meta.name || payload.schema !== meta.schema) return null;
133
152
  return {
134
153
  type: payload.type,
@@ -1,4 +1,4 @@
1
1
  //#region src/core/version.ts
2
- const VERSION = "0.0.0";
2
+ const VERSION = "0.5.0";
3
3
  //#endregion
4
4
  export { VERSION as t };
@@ -1,6 +1,6 @@
1
- import { p as DbError } from "./result-c2OYgCV2.js";
2
- import { n as AmrEntry, s as Impersonator, t as Aal } from "./mfa-x3V3vGh-.js";
3
- import { i as InvalidReason, r as AuthState } from "./resolve-BRciX2jK.js";
1
+ import { _ as DbError } from "./executor-BunPOO-k.js";
2
+ import { i as InvalidReason, r as AuthState } from "./resolve-C15TJq77.js";
3
+ import { n as AmrEntry, s as Impersonator, t as Aal } from "./mfa-CusDgPaD.js";
4
4
  import { JWTClaims, UserClaims } from "@supabase/server";
5
5
  //#region src/auth/actor.d.ts
6
6
  /** One level of an RFC 8693 `act` chain: who acts, and who acted before it. */
@@ -35,19 +35,24 @@ type AuthSession<C = unknown, P = unknown> = {
35
35
  readonly user: UserClaims;
36
36
  /**
37
37
  * The verified JWT payload, including custom access token hook claims,
38
- * typed and validated by `sb.claims(schema)`.
38
+ * typed and validated by `betterSupabase.claims(schema)`.
39
39
  */
40
40
  readonly claims: JWTClaims & C;
41
41
  /** Seconds since epoch, from the token's `exp`. */
42
42
  readonly expiresAt: number | null;
43
43
  /**
44
- * `user_metadata` parsed by `sb.userMetadata(schema)`, for display.
44
+ * `user_metadata` parsed by `betterSupabase.userMetadata(schema)`, for display.
45
45
  * Absent without a schema or when the metadata fails it. Users
46
46
  * write this data with `auth.updateUser()`: never base access on it.
47
47
  */
48
48
  readonly profile?: P;
49
49
  /** `aal2` once the user verified a second factor in this session. */
50
50
  readonly aal: Aal;
51
+ /**
52
+ * Signed in with `signInAnonymously()` (the `is_anonymous` claim). Guards
53
+ * refuse these users unless `allow` lists `'anonymous'`.
54
+ */
55
+ readonly anonymous: boolean;
51
56
  /** How the user signed in (`password`, `totp`, `sso/saml`, ...). */
52
57
  readonly amr: readonly AmrEntry[];
53
58
  /** Set when an admin acts as this user (the `act` claim), for a banner. */
@@ -1,28 +1,9 @@
1
- import { n as dbError } from "./errors-Bl3rkOzo.js";
1
+ import { n as dbError } from "./errors-DtwXa6b8.js";
2
2
  import { t as consoleLogger } from "./logger-glM28NVN.js";
3
- import { n as decodeJwtPayload } from "./base64-CQeB1eSf.js";
3
+ import { n as decodeJwtPayload } from "./base64-D2gsioCY.js";
4
+ import { n as impersonatorOf, r as userContext } from "./impersonation-VfaGc4Q_.js";
4
5
  import { DEFAULT_COOKIE_OPTIONS, createChunks, isChunkLike, parseCookieHeader, serializeCookieHeader, stringFromBase64URL, stringToBase64URL } from "@supabase/ssr";
5
6
  import { extractCredentials, verifyCredentials } from "@supabase/server/core";
6
- //#region src/auth/impersonation.ts
7
- /** The `act` claim for an impersonated session. */
8
- function actClaim(options) {
9
- return {
10
- sub: options.actor,
11
- reason: options.reason
12
- };
13
- }
14
- /** The impersonator in `claims.act`, or `undefined` for a normal session. */
15
- function impersonatorOf(claims) {
16
- const act = claims["act"];
17
- if (typeof act !== "object" || act === null) return void 0;
18
- const { sub, reason } = act;
19
- if (typeof sub !== "string" || sub === "") return void 0;
20
- return typeof reason === "string" ? {
21
- id: sub,
22
- reason
23
- } : { id: sub };
24
- }
25
- //#endregion
26
7
  //#region src/auth/actor.ts
27
8
  function isRecord(value) {
28
9
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -89,6 +70,7 @@ function delegationOf(claims) {
89
70
  * refresh-token reuse interval of Supabase Auth.
90
71
  */
91
72
  const REUSE_MS = 1e4;
73
+ const DEFAULT_TIMEOUT_MS = 5e3;
92
74
  const inflight = /* @__PURE__ */ new Map();
93
75
  const recent = /* @__PURE__ */ new Map();
94
76
  function shared(outcome) {
@@ -98,10 +80,14 @@ function shared(outcome) {
98
80
  } : outcome;
99
81
  }
100
82
  function prune(now) {
101
- for (const [token, entry] of recent) if (entry.until <= now) recent.delete(token);
83
+ for (const [token, entry] of recent) {
84
+ if (entry.until > now) return;
85
+ recent.delete(token);
86
+ }
102
87
  }
103
88
  async function request(refreshToken, options, now) {
104
89
  const doFetch = options.fetch ?? fetch;
90
+ const signal = AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
105
91
  let response;
106
92
  try {
107
93
  response = await doFetch(`${options.url}/auth/v1/token?grant_type=refresh_token`, {
@@ -111,7 +97,8 @@ async function request(refreshToken, options, now) {
111
97
  "content-type": "application/json",
112
98
  ...options.forwardedFor ? { "sb-forwarded-for": options.forwardedFor.ip } : {}
113
99
  },
114
- body: JSON.stringify({ refresh_token: refreshToken })
100
+ body: JSON.stringify({ refresh_token: refreshToken }),
101
+ signal
115
102
  });
116
103
  } catch (cause) {
117
104
  return {
@@ -121,6 +108,11 @@ async function request(refreshToken, options, now) {
121
108
  };
122
109
  }
123
110
  const body = await response.json().catch(() => null);
111
+ if (signal.aborted) return {
112
+ ok: false,
113
+ reason: "network",
114
+ message: "refresh timed out"
115
+ };
124
116
  if (!response.ok || !body || typeof body["access_token"] !== "string" || typeof body["refresh_token"] !== "string") {
125
117
  const message = typeof body?.["msg"] === "string" ? body["msg"] : typeof body?.["error_description"] === "string" ? body["error_description"] : `Refresh failed with ${String(response.status)}`;
126
118
  return {
@@ -154,12 +146,16 @@ function refreshSession(refreshToken, options) {
154
146
  const pending = inflight.get(refreshToken);
155
147
  if (pending) return pending.then(shared);
156
148
  const promise = request(refreshToken, options, now).then((outcome) => {
157
- inflight.delete(refreshToken);
158
- if (outcome.ok) recent.set(refreshToken, {
159
- outcome,
160
- until: now + REUSE_MS
161
- });
149
+ if (outcome.ok || outcome.reason === "rejected") {
150
+ recent.delete(refreshToken);
151
+ recent.set(refreshToken, {
152
+ outcome,
153
+ until: now + REUSE_MS
154
+ });
155
+ }
162
156
  return outcome;
157
+ }).finally(() => {
158
+ inflight.delete(refreshToken);
163
159
  });
164
160
  inflight.set(refreshToken, promise);
165
161
  return promise;
@@ -175,8 +171,15 @@ const AUTH_CACHE_HEADERS = {
175
171
  };
176
172
  /** `sb-<first label of the hostname>-auth-token`, the supabase-js default. */
177
173
  function sessionCookieName(url) {
178
- return `sb-${new URL(url).hostname.split(".")[0] ?? ""}-auth-token`;
174
+ let name = cookieNames.get(url);
175
+ if (name === void 0) {
176
+ name = `sb-${new URL(url).hostname.split(".")[0] ?? ""}-auth-token`;
177
+ if (cookieNames.size < 16) cookieNames.set(url, name);
178
+ }
179
+ return name;
179
180
  }
181
+ /** Project URLs come from configuration, so a handful of entries suffice. */
182
+ const cookieNames = /* @__PURE__ */ new Map();
180
183
  function parseCookies(header) {
181
184
  return header ? parseCookieHeader(header) : [];
182
185
  }
@@ -262,7 +265,7 @@ function serverEnv(options) {
262
265
  url: env.url,
263
266
  publishableKeys: { default: env.publishableKey },
264
267
  secretKeys: env.secretKeys ?? (env.secretKey ? { default: env.secretKey } : {}),
265
- jwks: options.jwks ?? env.jwksUrl
268
+ jwks: options.jwks ?? env.jwks ?? env.jwksUrl
266
269
  };
267
270
  }
268
271
  const MEMO_SIZE = 256;
@@ -273,7 +276,7 @@ let memoByJwks = /* @__PURE__ */ new WeakMap();
273
276
  * and island resolves the same token again; this skips the signature check.
274
277
  */
275
278
  function memoFor(options) {
276
- const jwks = options.jwks;
279
+ const jwks = options.jwks ?? options.env.jwks;
277
280
  if (jwks && typeof jwks === "object" && !(jwks instanceof URL)) {
278
281
  let memo = memoByJwks.get(jwks);
279
282
  if (!memo) memoByJwks.set(jwks, memo = /* @__PURE__ */ new Map());
@@ -289,6 +292,42 @@ function memoFor(options) {
289
292
  if (!memo) memoByUrl.set(key, memo = /* @__PURE__ */ new Map());
290
293
  return memo;
291
294
  }
295
+ function sameJson(a, b) {
296
+ if (a === b) return true;
297
+ if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
298
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
299
+ const left = Object.entries(a);
300
+ const right = new Map(Object.entries(b));
301
+ return left.length === right.size && left.every(([key, value]) => right.has(key) && sameJson(value, right.get(key)));
302
+ }
303
+ /**
304
+ * Records a token that `@supabase/server` verified for this request, so the
305
+ * next `resolveAuth` with these options doesn't verify it again. Only when
306
+ * `claims` is the token's own payload, it hasn't expired, and its `iss` and
307
+ * `aud` pass the checks these options would apply; otherwise returns false
308
+ * and the token is verified as usual.
309
+ */
310
+ function rememberVerified(options, verified) {
311
+ const { token, claims, user } = verified;
312
+ const now = Math.floor((options.now ?? Date.now)() / 1e3);
313
+ const expiresAt = typeof claims.exp === "number" ? claims.exp : null;
314
+ if (expiresAt === null || expiresAt <= now) return false;
315
+ if (!sameJson(decodeJwtPayload(token), claims)) return false;
316
+ const issuers = [options.issuer ?? `${options.env.url}/auth/v1`].flat();
317
+ if (typeof claims.iss !== "string" || !issuers.includes(claims.iss)) return false;
318
+ const audiences = [options.audience ?? "authenticated"].flat();
319
+ const aud = claims.aud;
320
+ const given = Array.isArray(aud) ? aud : [aud];
321
+ if (!audiences.some((audience) => given.includes(audience))) return false;
322
+ remember(memoFor(options), token, {
323
+ kind: "user",
324
+ token,
325
+ claims,
326
+ user,
327
+ expiresAt
328
+ }, expiresAt);
329
+ return true;
330
+ }
292
331
  function remembered(memo, token, now) {
293
332
  const hit = memo.get(token);
294
333
  if (!hit) return void 0;
@@ -297,8 +336,31 @@ function remembered(memo, token, now) {
297
336
  memo.set(token, hit);
298
337
  return hit;
299
338
  }
300
- function remember(memo, token, user) {
301
- if (user.expiresAt === null) return;
339
+ const NO_SCHEMA = {};
340
+ const checkedByMemo = /* @__PURE__ */ new WeakMap();
341
+ /**
342
+ * Tokens that already passed (or failed) the claims and `userMetadata`
343
+ * schemas, per verification memo and schema pair, so a memo hit skips
344
+ * validation too.
345
+ */
346
+ function checkedFor(memo, options) {
347
+ const claims = options.claims ?? NO_SCHEMA;
348
+ const metadata = options.userMetadata ?? NO_SCHEMA;
349
+ let byClaims = checkedByMemo.get(memo);
350
+ if (!byClaims) checkedByMemo.set(memo, byClaims = /* @__PURE__ */ new WeakMap());
351
+ let byMetadata = byClaims.get(claims);
352
+ if (!byMetadata) byClaims.set(claims, byMetadata = /* @__PURE__ */ new WeakMap());
353
+ let checked = byMetadata.get(metadata);
354
+ if (!checked) byMetadata.set(metadata, checked = /* @__PURE__ */ new Map());
355
+ return checked;
356
+ }
357
+ function withoutSource(state) {
358
+ if (state.kind !== "user") return state;
359
+ const { source: _source, ...rest } = state;
360
+ return rest;
361
+ }
362
+ function remember(memo, token, user, expiresAt) {
363
+ if (expiresAt === null) return;
302
364
  memo.set(token, user);
303
365
  if (memo.size > MEMO_SIZE) memo.delete(memo.keys().next().value);
304
366
  }
@@ -307,10 +369,20 @@ async function verify(credentials, modes, options, source) {
307
369
  const memo = token ? memoFor(options) : void 0;
308
370
  if (token && memo) {
309
371
  const hit = remembered(memo, token, Math.floor((options.now ?? Date.now)() / 1e3));
310
- if (hit) return checkUser({
311
- ...hit,
312
- source
313
- }, options);
372
+ if (hit) {
373
+ const checked = checkedFor(memo, options);
374
+ const known = checked.get(token);
375
+ if (known) return known.kind === "user" ? {
376
+ ...known,
377
+ source
378
+ } : known;
379
+ const state = await checkUser({
380
+ ...hit,
381
+ source
382
+ }, options);
383
+ remember(checked, token, withoutSource(state), hit.expiresAt);
384
+ return state;
385
+ }
314
386
  }
315
387
  const state = await verifyOnce(credentials, modes, options, source);
316
388
  if (token && memo && state.kind === "user") remember(memo, token, {
@@ -319,8 +391,10 @@ async function verify(credentials, modes, options, source) {
319
391
  claims: state.claims,
320
392
  user: state.user,
321
393
  expiresAt: state.expiresAt
322
- });
323
- return checkUser(state, options);
394
+ }, state.expiresAt);
395
+ const checked = await checkUser(state, options);
396
+ if (token && memo && state.kind === "user") remember(checkedFor(memo, options), token, withoutSource(checked), state.expiresAt);
397
+ return checked;
324
398
  }
325
399
  /** Validates a user's claims against `options.claims`, keeping every JWT claim. */
326
400
  async function checkClaims(state, options) {
@@ -368,10 +442,34 @@ async function checkUser(state, options) {
368
442
  };
369
443
  if (!warnedMetadata.has(schema)) {
370
444
  warnedMetadata.add(schema);
371
- (options.logger ?? consoleLogger).warn("user_metadata does not match sb.userMetadata(schema); session.profile is undefined", { paths: outcome.issues.map((issue) => issue.path?.map((segment) => typeof segment === "object" ? String(segment.key) : String(segment)).join(".") ?? "") });
445
+ (options.logger ?? consoleLogger).warn("user_metadata does not match betterSupabase.userMetadata(schema); session.profile is undefined", { paths: outcome.issues.map((issue) => issue.path?.map((segment) => typeof segment === "object" ? String(segment.key) : String(segment)).join(".") ?? "") });
372
446
  }
373
447
  return user;
374
448
  }
449
+ /** A token whose `kid` matches no key: verifying it loads the JWKS and fails. */
450
+ const PREFETCH_TOKEN = `${base64url(JSON.stringify({
451
+ alg: "ES256",
452
+ kid: "better-supabase-prefetch",
453
+ typ: "JWT"
454
+ }))}.e30.AA`;
455
+ function base64url(text) {
456
+ return btoa(text).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
457
+ }
458
+ /**
459
+ * Loads the remote JWKS into the verifier's cache so the first request
460
+ * doesn't wait for it. Never throws; a failure leaves the first request to
461
+ * fetch it as before.
462
+ */
463
+ function prefetchJwks(options) {
464
+ if (options.jwks !== void 0 || options.env.jwks !== void 0) return Promise.resolve();
465
+ return verifyCredentials({
466
+ token: PREFETCH_TOKEN,
467
+ apikey: null
468
+ }, {
469
+ auth: ["user"],
470
+ env: serverEnv(options)
471
+ }).then(() => {}, () => {});
472
+ }
375
473
  async function verifyOnce(credentials, modes, options, source) {
376
474
  const { data, error } = await verifyCredentials(credentials, {
377
475
  auth: modes,
@@ -406,11 +504,16 @@ async function verifyOnce(credentials, modes, options, source) {
406
504
  }
407
505
  function resolution(auth, cookies, writes = []) {
408
506
  const headers = writes.length > 0 ? AUTH_CACHE_HEADERS : {};
507
+ let requestCookies;
409
508
  return {
410
509
  auth,
411
510
  cookies: writes,
412
511
  headers,
413
- requestCookies: writes.length > 0 ? applyCookieWrites(cookies, writes) : cookies,
512
+ /** Parsed on first read: bearer requests rarely need their cookies. */
513
+ get requestCookies() {
514
+ requestCookies ??= writes.length > 0 ? applyCookieWrites(cookies(), writes) : cookies();
515
+ return requestCookies;
516
+ },
414
517
  apply(response) {
415
518
  if (writes.length === 0) return response;
416
519
  let target = response;
@@ -440,7 +543,8 @@ function clientIp(request) {
440
543
  * set, through a single-flight request, and only then are cookies written.
441
544
  */
442
545
  async function resolveAuth(request, options) {
443
- const cookies = parseCookies(request.headers.get("cookie"));
546
+ let parsed;
547
+ const cookies = () => parsed ??= parseCookies(request.headers.get("cookie"));
444
548
  for (const resolver of options.resolvers ?? []) {
445
549
  const state = await resolver.resolve(request);
446
550
  if (!state) continue;
@@ -459,11 +563,15 @@ async function resolveAuth(request, options) {
459
563
  token: null,
460
564
  apikey: credentials.apikey
461
565
  }, options.secret === true ? ["secret"] : options.secret.map((name) => `secret:${name}`), options, "bearer"), cookies);
566
+ if (options.cookies === false) return resolution({
567
+ kind: "anon",
568
+ reason: "none"
569
+ }, cookies);
462
570
  const name = options.cookie?.name ?? sessionCookieName(options.env.url);
463
- const session = readSession(cookies, name);
571
+ const session = readSession(cookies(), name);
464
572
  if (!session) {
465
- const stale = cookies.some((cookie) => cookie.name === name || cookie.name.startsWith(`${name}.`));
466
- const writes = stale && options.refresh ? writeSession(cookies, name, null, options.cookie?.options) : [];
573
+ const stale = cookies().some((cookie) => cookie.name === name || cookie.name.startsWith(`${name}.`));
574
+ const writes = stale && options.refresh ? writeSession(cookies(), name, null, options.cookie?.options) : [];
467
575
  return resolution({
468
576
  kind: "anon",
469
577
  reason: stale ? "signed_out" : "none"
@@ -471,7 +579,8 @@ async function resolveAuth(request, options) {
471
579
  }
472
580
  const now = Math.floor((options.now ?? Date.now)() / 1e3);
473
581
  const expiry = expiryOf(session);
474
- if (expiry !== void 0 && expiry - (options.leeway ?? 60) > now) {
582
+ const leeway = options.refresh ? options.leeway ?? 60 : 0;
583
+ if (expiry !== void 0 && expiry - leeway > now) {
475
584
  const state = await verify({
476
585
  token: session.access_token,
477
586
  apikey: null
@@ -496,7 +605,8 @@ async function resolveAuth(request, options) {
496
605
  secretKey
497
606
  } } : {},
498
607
  ...options.fetch ? { fetch: options.fetch } : {},
499
- ...options.now ? { now: options.now } : {}
608
+ ...options.now ? { now: options.now } : {},
609
+ ...options.refreshTimeoutMs === void 0 ? {} : { timeoutMs: options.refreshTimeoutMs }
500
610
  });
501
611
  options.onRefresh?.({
502
612
  ok: outcome.ok,
@@ -511,9 +621,9 @@ async function resolveAuth(request, options) {
511
621
  return resolution({
512
622
  kind: "anon",
513
623
  reason: "signed_out"
514
- }, cookies, writeSession(cookies, name, null, options.cookie?.options));
624
+ }, cookies, writeSession(cookies(), name, null, options.cookie?.options));
515
625
  }
516
- const writes = writeSession(cookies, name, outcome.session, options.cookie?.options);
626
+ const writes = writeSession(cookies(), name, outcome.session, options.cookie?.options);
517
627
  return resolution(await verify({
518
628
  token: outcome.session.access_token,
519
629
  apikey: null
@@ -522,19 +632,7 @@ async function resolveAuth(request, options) {
522
632
  /** The repository context (`actor`, `claims`) for an auth state. */
523
633
  function authContext(auth) {
524
634
  switch (auth.kind) {
525
- case "user": {
526
- const impersonator = impersonatorOf(auth.claims);
527
- return {
528
- actor: {
529
- id: auth.user.id,
530
- kind: "user",
531
- ...auth.user.role === void 0 ? {} : { role: auth.user.role },
532
- ...auth.user.email === void 0 ? {} : { email: auth.user.email },
533
- ...impersonator ? { impersonator: impersonator.id } : {}
534
- },
535
- claims: auth.claims
536
- };
537
- }
635
+ case "user": return userContext(auth.user, auth.claims);
538
636
  case "service": return {
539
637
  actor: {
540
638
  id: `service:${auth.keyName}`,
@@ -593,4 +691,52 @@ function checkAal(auth, required) {
593
691
  });
594
692
  }
595
693
  //#endregion
596
- export { actClaim as _, clientIp as a, applyCookieWrites as c, serializeCookie as d, sessionCookieName as f, delegationOf as g, actorOf as h, authContext as i, parseCookies as l, refreshSession as m, amrOf as n, resolveAuth as o, writeSession as p, checkAal as r, AUTH_CACHE_HEADERS as s, aalOf as t, readSession as u, impersonatorOf as v };
694
+ //#region src/auth/view.ts
695
+ /** Whether the token belongs to an anonymous user (`signInAnonymously()`). */
696
+ function isAnonymousUser(claims) {
697
+ return "is_anonymous" in claims && claims.is_anonymous === true;
698
+ }
699
+ /** Drops the token from an `AuthState`, leaving only serializable fields. */
700
+ function toSession(auth) {
701
+ switch (auth.kind) {
702
+ case "user": {
703
+ const impersonator = impersonatorOf(auth.claims);
704
+ const outcome = actorOf(auth.claims);
705
+ const actor = outcome.ok ? outcome.actor : void 0;
706
+ const scopes = actor ? delegationOf(auth.claims)?.scopes : void 0;
707
+ const delegation = actor && (scopes || actor.chain) ? {
708
+ scopes: scopes ?? [],
709
+ ...actor.chain ? { chain: actor.chain } : {}
710
+ } : void 0;
711
+ return {
712
+ kind: "user",
713
+ user: auth.user,
714
+ claims: auth.claims,
715
+ expiresAt: auth.expiresAt,
716
+ ...auth.profile === void 0 ? {} : { profile: auth.profile },
717
+ aal: aalOf(auth.claims),
718
+ anonymous: isAnonymousUser(auth.claims),
719
+ amr: amrOf(auth.claims),
720
+ ...impersonator ? { impersonator } : {},
721
+ ...actor ? { actor } : {},
722
+ ...delegation ? { delegation } : {}
723
+ };
724
+ }
725
+ case "service": return {
726
+ kind: "service",
727
+ keyName: auth.keyName
728
+ };
729
+ case "anon": return {
730
+ kind: "anon",
731
+ reason: auth.reason
732
+ };
733
+ case "invalid": return {
734
+ kind: "invalid",
735
+ reason: auth.reason,
736
+ error: auth.error
737
+ };
738
+ default: return auth;
739
+ }
740
+ }
741
+ //#endregion
742
+ export { writeSession as _, checkAal as a, delegationOf as b, prefetchJwks as c, AUTH_CACHE_HEADERS as d, applyCookieWrites as f, sessionCookieName as g, serializeCookie as h, amrOf as i, rememberVerified as l, readSession as m, toSession as n, authContext as o, parseCookies as p, aalOf as r, clientIp as s, isAnonymousUser as t, resolveAuth as u, refreshSession as v, actorOf as y };