better-supabase 0.1.0 → 0.3.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 (149) hide show
  1. package/README.md +34 -31
  2. package/dist/base64-CQeB1eSf.js +42 -0
  3. package/dist/claims-CoTobNC-.js +25 -0
  4. package/dist/client/index.d.ts +1 -1
  5. package/dist/client/index.js +4 -14
  6. package/dist/{compiler-DL18ukPe.d.ts → compiler-2zqGzxVZ.d.ts} +2 -2
  7. package/dist/config/index.d.ts +4 -3
  8. package/dist/config/index.js +5 -3
  9. package/dist/config-Be4GwsqI.js +129 -0
  10. package/dist/{index-BD6wKthp.d.ts → config-C1CYcm6o.d.ts} +75 -54
  11. package/dist/{config-D-aR2ZoN.js → config-DOsGYSiT.js} +14 -136
  12. package/dist/{define-QGkq04oe.js → define-DF341GDj.js} +133 -110
  13. package/dist/{define-CTfWw-SS.d.ts → define-DUbydmJa.d.ts} +32 -14
  14. package/dist/edge/index.d.ts +4 -4
  15. package/dist/edge/index.js +4 -4
  16. package/dist/entitlements-CTEBm7IB.js +20 -0
  17. package/dist/entitlements-DvJXoI02.d.ts +42 -0
  18. package/dist/env/index.js +203 -1
  19. package/dist/events/index.d.ts +1 -1
  20. package/dist/events/index.js +139 -1
  21. package/dist/{events-2OJsVG_d.d.ts → events-Do8jXh4D.d.ts} +3 -3
  22. package/dist/{executor-Bje4suQF.d.ts → executor-B26lgbHD.d.ts} +2 -2
  23. package/dist/{executor-Ceq60hPM.js → executor-C57jx2pn.js} +7 -2
  24. package/dist/{executor-Bazhgsgq.d.ts → executor-DynmIWhq.d.ts} +2 -2
  25. package/dist/guards-BnzDvFrz.js +10 -0
  26. package/dist/hono/index.d.ts +5 -5
  27. package/dist/hono/index.js +4 -4
  28. package/dist/{index-Hf8PP9sP.d.ts → index-BNsABq7P.d.ts} +17 -13
  29. package/dist/{index-JXHBuS53.d.ts → index-CADlNHK-.d.ts} +27 -14
  30. package/dist/{index-Nxo3TJcm.d.ts → index-CTTMS6_1.d.ts} +27 -8
  31. package/dist/index-Cf-D2jk1.d.ts +50 -0
  32. package/dist/{index-Dup-nSe9.d.ts → index-CriFFJ_p.d.ts} +4 -4
  33. package/dist/{index-DQVvHzjW.d.ts → index-GV77NpRP.d.ts} +5 -4
  34. package/dist/{index-Sw43J8HG.d.ts → index-UJ-LUxtI.d.ts} +6 -6
  35. package/dist/{index-cEeweTeM.d.ts → index-kpgopLNv.d.ts} +4 -3
  36. package/dist/{index-DRid0qIv.d.ts → index-qriRM9cZ.d.ts} +7 -2
  37. package/dist/index.d.ts +13 -13
  38. package/dist/index.js +18 -8
  39. package/dist/jobs/index.d.ts +25 -24
  40. package/dist/jobs/index.js +19 -12
  41. package/dist/{json-schema-BbwO2mZg.js → json-schema--aOXAGfU.js} +9 -3
  42. package/dist/lint/index.js +1 -1
  43. package/dist/list/index.d.ts +2 -2
  44. package/dist/list/index.js +128 -93
  45. package/dist/{live-6b9i72Ux.d.ts → live-QE3UPbGm.d.ts} +27 -6
  46. package/dist/logger-glM28NVN.js +21 -0
  47. package/dist/mcp/index.d.ts +59 -15
  48. package/dist/mcp/index.js +229 -67
  49. package/dist/{mfa-CpogP66z.js → mfa-BxoR1RkN.js} +97 -22
  50. package/dist/{mfa-TVia554c.d.ts → mfa-x3V3vGh-.d.ts} +1 -1
  51. package/dist/next/index.d.ts +51 -38
  52. package/dist/next/index.js +31 -25
  53. package/dist/openapi/index.d.ts +1 -1
  54. package/dist/openapi/index.js +58 -35
  55. package/dist/orpc/index.d.ts +6 -6
  56. package/dist/orpc/index.js +3 -3
  57. package/dist/otel/index.d.ts +3 -3
  58. package/dist/otel/index.js +5 -3
  59. package/dist/{template-BeOGnynM.js → permdock-sql-Bta_G0v-.js} +32 -3
  60. package/dist/permdock-sql-DImJULY-.d.ts +17 -0
  61. package/dist/{plugin-HvGqCurB.d.ts → plugin-DVRB8qNE.d.ts} +5 -4
  62. package/dist/plugins/actor/index.d.ts +1 -1
  63. package/dist/plugins/rules/index.d.ts +4 -3
  64. package/dist/plugins/rules/index.js +8 -15
  65. package/dist/plugins/soft-delete/index.d.ts +3 -3
  66. package/dist/plugins/soft-delete/index.js +2 -2
  67. package/dist/plugins/tenant/index.d.ts +10 -4
  68. package/dist/plugins/tenant/index.js +15 -13
  69. package/dist/plugins/timestamps/index.d.ts +1 -1
  70. package/dist/plugins/timestamps/index.js +1 -1
  71. package/dist/plugins/validation/index.d.ts +2 -2
  72. package/dist/pool-B9qsLyFR.d.ts +71 -0
  73. package/dist/postgres/index.d.ts +10 -5
  74. package/dist/postgres/index.js +36 -16
  75. package/dist/{postgrest-Dm-vhY6R.d.ts → postgrest-DmBlevh-.d.ts} +1 -1
  76. package/dist/{problem-DPu6Fh-t.js → problem-CGt1ADix.js} +14 -8
  77. package/dist/{problem-DlgB0lYl.d.ts → problem-Z2RTQjZD.d.ts} +6 -4
  78. package/dist/query/index.d.ts +2 -2
  79. package/dist/query/index.js +1 -1
  80. package/dist/{query-ChvDbsDE.js → query-BdKLTPlj.js} +2 -2
  81. package/dist/react/index.d.ts +7 -7
  82. package/dist/react/index.js +3 -3
  83. package/dist/react/server.d.ts +5 -5
  84. package/dist/react/server.js +1 -1
  85. package/dist/react/session.d.ts +21 -2
  86. package/dist/react/session.js +19 -1
  87. package/dist/{react-CrOvrnIP.js → react-DNBYNazu.js} +28 -23
  88. package/dist/{read-set-DhX2d04c.js → read-set-D0qBcboM.js} +3 -3
  89. package/dist/realtime/index.d.ts +4 -2
  90. package/dist/realtime/index.js +30 -14
  91. package/dist/{resolve-DW8h9NPm.d.ts → resolve-BRciX2jK.d.ts} +25 -9
  92. package/dist/{resource-SkPTRJDD.d.ts → resource-CwVgiI5q.d.ts} +16 -7
  93. package/dist/{resource-CYiu6Ygi.js → resource-D30rp0GN.js} +38 -16
  94. package/dist/{respond-4-dhoV7x.js → respond-C44EZAJo.js} +38 -19
  95. package/dist/{respond-PoTpy5DT.d.ts → respond-DAkPIdTH.d.ts} +23 -17
  96. package/dist/{result-CTkCZ6JA.d.ts → result-c2OYgCV2.d.ts} +13 -8
  97. package/dist/server/index.d.ts +35 -10
  98. package/dist/server/index.js +38 -9
  99. package/dist/{simplify-oHiDKRVQ.js → simplify-LakqK1vH.js} +22 -28
  100. package/dist/{spec-pins-t6sPaHvl.js → spec-pins-C3oc-Tdp.js} +1 -1
  101. package/dist/{spec-pins-CJPVghHy.d.ts → spec-pins-CqorWDnO.d.ts} +1 -1
  102. package/dist/sql/index.d.ts +139 -0
  103. package/dist/sql/index.js +1983 -0
  104. package/dist/{sql-M8l3fnS0.js → sql-BIgk12ew.js} +72 -16
  105. package/dist/ssr/index.d.ts +5 -5
  106. package/dist/ssr/index.js +3 -3
  107. package/dist/{standard-CyhRzctb.d.ts → standard-eudBiRk1.d.ts} +1 -1
  108. package/dist/{stats-3iy8u3_P.js → stats-D78UQ8Yu.js} +8 -7
  109. package/dist/storage/index.d.ts +4 -2
  110. package/dist/storage/index.js +530 -1
  111. package/dist/{tables-rZxRxNPW.js → tables-Dhha3BXM.js} +5 -5
  112. package/dist/temporal-BwshQk0W.js +27 -0
  113. package/dist/temporal-required-CMxeSVPo.js +19 -0
  114. package/dist/testing/index.d.ts +49 -16
  115. package/dist/testing/index.js +233 -66
  116. package/dist/{types-C2fslP1z.d.ts → types-DONv9SHN.d.ts} +68 -12
  117. package/dist/{version-Cqq1m3UY.js → version-BfpG2gks.js} +1 -1
  118. package/dist/view-3Po_PVlr.d.ts +79 -0
  119. package/dist/{view-B6F9QbG-.js → view-wZ-9qIUz.js} +12 -2
  120. package/dist/webhooks/index.d.ts +19 -17
  121. package/dist/webhooks/index.js +1 -1
  122. package/dist/{webhooks-DbMTJJX2.js → webhooks-CAIFQSTM.js} +12 -10
  123. package/package.json +30 -30
  124. package/schemas/config-v1.json +80 -5
  125. package/schemas/doctor-report-v1.json +18 -1
  126. package/schemas/snapshot-v2.json +39 -0
  127. package/skills/better-supabase/SKILL.md +23 -11
  128. package/skills/better-supabase/references/plugins.md +28 -16
  129. package/skills/better-supabase/references/troubleshooting.md +15 -15
  130. package/skills/better-supabase-api/SKILL.md +9 -9
  131. package/skills/better-supabase-api/references/adapters.md +30 -25
  132. package/skills/better-supabase-testing/SKILL.md +15 -11
  133. package/bin/better-supabase.js +0 -3
  134. package/dist/casing-da0uRTqt.js +0 -14
  135. package/dist/cli/bin.d.ts +0 -1
  136. package/dist/cli/bin.js +0 -15
  137. package/dist/cli/index.d.ts +0 -226
  138. package/dist/cli/index.js +0 -3
  139. package/dist/cli-CoByWYja.js +0 -6472
  140. package/dist/entitlements-BBg61DQZ.d.ts +0 -20
  141. package/dist/entitlements-BC_khwRy.js +0 -17
  142. package/dist/env-BEToab0I.js +0 -204
  143. package/dist/events-BuzTgn2D.js +0 -135
  144. package/dist/pool-BFeqzwQS.d.ts +0 -37
  145. package/dist/seed-CzecODcJ.js +0 -101
  146. package/dist/session-C3osL21k.d.ts +0 -21
  147. package/dist/session-CD-3orO_.js +0 -19
  148. package/dist/storage-BWXVUXGk.js +0 -461
  149. package/dist/view-B0ltUmTi.d.ts +0 -43
@@ -1,3 +1,21 @@
1
1
  "use client";
2
- import { n as useSession, t as SessionProvider } from "../session-CD-3orO_.js";
2
+ import { createContext, createElement, use, useContext } from "react";
3
+ //#region src/react/session.ts
4
+ const SessionContext = createContext(null);
5
+ /** Shares the server-verified session with Client Components. */
6
+ function SessionProvider(props) {
7
+ return createElement(SessionContext.Provider, { value: props.sessionPromise }, props.children);
8
+ }
9
+ /**
10
+ * The session from the nearest `<SessionProvider>`. Suspends until the
11
+ * promise resolves, so render it inside `<Suspense>`. `C` types the claims
12
+ * (the output of `sb.claims(schema)`) and `P` the profile (the output of
13
+ * `sb.userMetadata(schema)`); `createHooks().useSession` infers both.
14
+ */
15
+ function useSession() {
16
+ const promise = useContext(SessionContext);
17
+ if (!promise) throw new Error("better-supabase: useSession() needs <SessionProvider sessionPromise={...}>");
18
+ return use(promise);
19
+ }
20
+ //#endregion
3
21
  export { SessionProvider, useSession };
@@ -1,8 +1,9 @@
1
+ import { i as tenantClaimPaths, n as claimAt, r as claimsOf } from "./claims-CoTobNC-.js";
1
2
  import { t as invalidateTables } from "./invalidate-DbxuL2jL.js";
2
3
  import { n as liveQuery, t as liveCount } from "./live-D8rV1Ldw.js";
3
- import { n as useSession } from "./session-CD-3orO_.js";
4
+ import { useSession } from "./react/session.js";
4
5
  import { createContext, createElement, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
- //#region src/react/index.ts
6
+ //#region src/react/hooks.ts
6
7
  const BrowserContext = createContext(null);
7
8
  const LOADING = {
8
9
  status: "loading",
@@ -90,9 +91,9 @@ function useBroadcast(topic, values, handlers, options) {
90
91
  const userId = auth.user?.id ?? null;
91
92
  if (options?.invalidate && !queryClient) throw new Error("better-supabase: useBroadcast({ invalidate }) needs <BetterSupabaseProvider queryClient={...}>");
92
93
  useEffect(() => {
93
- if (!name || auth.status === "loading") return void 0;
94
+ if (!name || auth.status === "loading") return;
94
95
  const matched = topic.match(name);
95
- if (!matched) return void 0;
96
+ if (!matched) return;
96
97
  const forward = (payload, message) => {
97
98
  const current = latest.current;
98
99
  const table = current.handlers;
@@ -124,12 +125,12 @@ function useBroadcast(topic, values, handlers, options) {
124
125
  ]);
125
126
  return status;
126
127
  }
127
- function claimedTenant(auth) {
128
+ function claimedTenant(auth, meta) {
128
129
  if (auth.status !== "signed-in") return void 0;
129
- const top = auth.claims["org_id"];
130
- if (typeof top === "string") return top;
131
- const app = auth.claims["app_metadata"];
132
- return typeof app?.["org_id"] === "string" ? app["org_id"] : void 0;
130
+ for (const path of tenantClaimPaths(claimsOf(meta).tenant)) {
131
+ const value = claimAt(auth.claims, path);
132
+ if (value !== void 0) return value;
133
+ }
133
134
  }
134
135
  /**
135
136
  * Keeps a query fresh: invalidates every cached query that read a table the
@@ -148,11 +149,11 @@ function useLiveQuery(spec, options = {}) {
148
149
  const [status, setStatus] = useState("closed");
149
150
  if (spec && !queryClient) throw new Error("better-supabase: useLiveQuery needs <BetterSupabaseProvider queryClient={...}>");
150
151
  const key = spec ? JSON.stringify(spec) : null;
151
- const tenant = options.tenant ?? claimedTenant(auth);
152
+ const tenant = options.tenant ?? claimedTenant(auth, browser.sb.meta);
152
153
  const userId = auth.user?.id ?? null;
153
154
  const debounceMs = options.debounceMs;
154
155
  useEffect(() => {
155
- if (!key || !queryClient || auth.status === "loading") return void 0;
156
+ if (!key || !queryClient || auth.status === "loading") return;
156
157
  const live = liveQuery(browser.sb, browser.supabase, JSON.parse(key), {
157
158
  onChange: (tables) => void invalidateTables(queryClient, tables),
158
159
  onStatus: setStatus,
@@ -196,24 +197,28 @@ function useLiveCount(source, options = {}) {
196
197
  });
197
198
  const [status, setStatus] = useState("closed");
198
199
  const key = spec ? JSON.stringify(spec) : null;
199
- const tenant = options.tenant ?? claimedTenant(auth);
200
+ const tenant = options.tenant ?? claimedTenant(auth, browser.sb.meta);
200
201
  const userId = auth.user?.id ?? null;
201
202
  const debounceMs = options.debounceMs;
202
203
  const hasInitial = initial !== void 0;
203
204
  useEffect(() => {
204
- if (!key || auth.status === "loading") return void 0;
205
+ if (!key || auth.status === "loading") return;
205
206
  const live = liveCount(browser.sb, browser.supabase, browser.db, JSON.parse(key), {
206
207
  immediate: !hasInitial,
207
- onCount: (count) => setState({
208
- key,
209
- count,
210
- error: void 0
211
- }),
212
- onError: (error) => setState((previous) => ({
213
- key,
214
- count: previous.key === key ? previous.count : void 0,
215
- error
216
- })),
208
+ onCount: (count) => {
209
+ setState({
210
+ key,
211
+ count,
212
+ error: void 0
213
+ });
214
+ },
215
+ onError: (error) => {
216
+ setState((previous) => ({
217
+ key,
218
+ count: previous.key === key ? previous.count : void 0,
219
+ error
220
+ }));
221
+ },
217
222
  onStatus: setStatus,
218
223
  ...tenant === void 0 ? {} : { tenant },
219
224
  ...debounceMs === void 0 ? {} : { debounceMs }
@@ -1,5 +1,5 @@
1
- import { r as IrBuilder, u as tableMeta } from "./simplify-oHiDKRVQ.js";
2
- import { n as touchedTables } from "./tables-rZxRxNPW.js";
1
+ import { r as IrBuilder, u as tableMeta } from "./simplify-LakqK1vH.js";
2
+ import { n as touchedTables } from "./tables-Dhha3BXM.js";
3
3
  //#region src/core/spec.ts
4
4
  const READ_METHODS = [
5
5
  "findMany",
@@ -87,7 +87,7 @@ const TYPE = /^(?:[a-z_][a-z0-9_]*\.)?[a-z_][a-z0-9_]*(?:\[\])?$/;
87
87
  /** `jsonb_build_object` takes at most 100 arguments. */
88
88
  const MAX_ENTRIES = 50;
89
89
  /** Placeholders are `NUL bs:<name> NUL`: no real parameter value holds a NUL. */
90
- const NUL = String.fromCharCode(0);
90
+ const NUL = "\0";
91
91
  const SENTINEL = new RegExp(`${NUL}bs:([a-z_][a-z0-9_]*)${NUL}`, "gi");
92
92
  const EXACT = new RegExp(`^${NUL}bs:([a-z_][a-z0-9_]*)${NUL}$`, "i");
93
93
  function sentinel(name) {
@@ -1,3 +1,5 @@
1
+ import { x as PermdockTopicPolicy } from "../types-DONv9SHN.js";
2
+ import { t as PermdockCatalog } from "../permdock-sql-DImJULY-.js";
1
3
  import { n as TemplateValues, t as TemplateParams } from "../template-mbvlysOo.js";
2
- import { C as defineTopic, S as TriggerOptions, _ as TopicEvent, a as LiveSource, b as TopicOptions, c as liveQuery, d as RealtimeClient, f as RowChange, g as Topic, h as SubscriptionStatus, i as LiveQueryOptions, l as liveTopic, m as Subscription, n as LiveCountOptions, o as LiveSubscription, p as SubscribeOptions, r as LiveCountSeed, s as liveCount, t as CountRunner, u as EventSchemas, v as TopicHandlers, w as rowChange, x as TopicPayload, y as TopicMessage } from "../live-6b9i72Ux.js";
3
- export { type CountRunner, EventSchemas, type LiveCountOptions, type LiveCountSeed, type LiveQueryOptions, type LiveSource, type LiveSubscription, RealtimeClient, RowChange, SubscribeOptions, Subscription, SubscriptionStatus, type TemplateParams, type TemplateValues, Topic, TopicEvent, TopicHandlers, TopicMessage, TopicOptions, TopicPayload, TriggerOptions, defineTopic, liveCount, liveQuery, liveTopic, rowChange };
4
+ import { C as defineTopic, S as TriggerOptions, _ as TopicEvent, a as LiveSource, b as TopicOptions, c as liveQuery, d as RealtimeClient, f as RowChange, g as Topic, h as SubscriptionStatus, i as LiveQueryOptions, l as liveTopic, m as Subscription, n as LiveCountOptions, o as LiveSubscription, p as SubscribeOptions, r as LiveCountSeed, s as liveCount, t as CountRunner, u as EventSchemas, v as TopicHandlers, w as rowChange, x as TopicPayload, y as TopicMessage } from "../live-QE3UPbGm.js";
5
+ export { type CountRunner, type EventSchemas, type LiveCountOptions, type LiveCountSeed, type LiveQueryOptions, type LiveSource, type LiveSubscription, type PermdockCatalog, type PermdockTopicPolicy, type RealtimeClient, type RowChange, type SubscribeOptions, type Subscription, type SubscriptionStatus, type TemplateParams, type TemplateValues, type Topic, type TopicEvent, type TopicHandlers, type TopicMessage, type TopicOptions, type TopicPayload, type TriggerOptions, defineTopic, liveCount, liveQuery, liveTopic, rowChange };
@@ -1,10 +1,11 @@
1
1
  import { n as dbError } from "../errors-Bl3rkOzo.js";
2
2
  import { i as ok, n as err, t as AsyncResult } from "../result-DWXatkd6.js";
3
+ import { i as tenantClaimPaths } from "../claims-CoTobNC-.js";
3
4
  import { n as liveQuery, r as liveTopic, t as liveCount } from "../live-D8rV1Ldw.js";
4
5
  import { s as toApp } from "../shared-_6-uPL7E.js";
5
- import { i as sqlString, n as slug, r as sqlIdent, t as parseTemplate } from "../template-BeOGnynM.js";
6
+ import { a as sqlIdent, i as slug, o as sqlString, r as parseTemplate, t as permdockCheck } from "../permdock-sql-Bta_G0v-.js";
6
7
  import { REALTIME_SUBSCRIBE_STATES } from "@supabase/supabase-js";
7
- //#region src/realtime/index.ts
8
+ //#region src/realtime/topic.ts
8
9
  const VALUE = /^[\w.@+=-]+$/;
9
10
  function validateValue(_name, value) {
10
11
  return VALUE.test(value) ? void 0 : "contains characters topics do not allow";
@@ -54,7 +55,7 @@ function sendError(status, message) {
54
55
  */
55
56
  function defineTopic(template, options = {}) {
56
57
  const parsed = parseTemplate(template, ":", validateValue);
57
- const literal = template.replace(/\{[^}]+\}/g, " ");
58
+ const literal = template.replaceAll(/\{[^}]+\}/g, " ");
58
59
  const name = slug(options.name ?? literal) || "topic";
59
60
  const isPrivate = options.private ?? true;
60
61
  const schemas = options.events ?? {};
@@ -64,10 +65,21 @@ function defineTopic(template, options = {}) {
64
65
  return index;
65
66
  };
66
67
  const checks = [];
68
+ const permdock = options.permdock;
67
69
  const tenantParam = options.tenant === false ? void 0 : options.tenant?.param ?? "orgId";
68
- if (tenantParam && (options.tenant || parsed.params.includes(tenantParam))) {
69
- const tenant = options.tenant || {};
70
- const expression = tenant.sql ?? claimSql(tenant.claim ?? ["org_id", "app_metadata.org_id"]);
70
+ const permdockChecks = (() => {
71
+ if (!permdock) return void 0;
72
+ const where = `defineTopic(${template})`;
73
+ const id = permdock.scope === "global" ? void 0 : `split_part((select realtime.topic()), ':', ${String(permdock.segment ?? segment(tenantParam ?? "orgId", "PermDock"))})`;
74
+ const receive = permdockCheck(where, permdock, permdock.receive, id, options.catalog);
75
+ return {
76
+ receive,
77
+ send: permdock.send === void 0 ? receive : permdockCheck(where, permdock, permdock.send, id, options.catalog)
78
+ };
79
+ })();
80
+ if (!permdock && tenantParam && (options.tenant || parsed.params.includes(tenantParam))) {
81
+ const tenant = options.tenant === false ? {} : options.tenant ?? {};
82
+ const expression = tenant.sql ?? claimSql(tenant.claim ?? tenantClaimPaths());
71
83
  checks.push(`split_part((select realtime.topic()), ':', ${String(segment(tenantParam, "tenant"))}) = (${expression})`);
72
84
  }
73
85
  const ownerParam = options.owner === false ? void 0 : options.owner?.param ?? "userId";
@@ -82,25 +94,28 @@ function defineTopic(template, options = {}) {
82
94
  match: (topic) => parsed.match(topic),
83
95
  sql() {
84
96
  const extensions = options.presence ? "('broadcast', 'presence')" : "('broadcast')";
85
- const condition = [
97
+ const condition = (extra) => [
86
98
  `(select realtime.topic()) ~ ${sqlString(parsed.sqlPattern)}`,
87
99
  `realtime.messages.extension in ${extensions}`,
88
- ...checks
100
+ ...checks,
101
+ ...extra === void 0 ? [] : [extra]
89
102
  ].join("\n and ");
90
103
  const lines = [`-- better-supabase: topic ${template}`];
91
104
  const policies = [[
92
105
  "receive",
93
106
  "select",
94
- "using"
107
+ "using",
108
+ condition(permdockChecks?.receive)
95
109
  ]];
96
- if (options.send || options.presence) policies.push([
110
+ if (options.send || options.presence || permdock?.send !== void 0) policies.push([
97
111
  "send",
98
112
  "insert",
99
- "with check"
113
+ "with check",
114
+ condition(permdockChecks?.send)
100
115
  ]);
101
- for (const [suffix, command, clause] of policies) {
116
+ for (const [suffix, command, clause, check] of policies) {
102
117
  const policy = sqlIdent(`bs_topic_${name}_${suffix}`);
103
- lines.push(`drop policy if exists ${policy} on realtime.messages;`, `create policy ${policy} on realtime.messages for ${command} to authenticated\n ${clause} (\n ${condition}\n );`);
118
+ lines.push(`drop policy if exists ${policy} on realtime.messages;`, `create policy ${policy} on realtime.messages for ${command} to authenticated\n ${clause} (\n ${check}\n );`);
104
119
  }
105
120
  return `${lines.join("\n")}\n`;
106
121
  },
@@ -116,7 +131,7 @@ function defineTopic(template, options = {}) {
116
131
  if (!db) throw new TypeError(`defineTopic: no column for {${param}} on "${table}"`);
117
132
  return `rec.${sqlIdent(db)}::text`;
118
133
  });
119
- const schema = trigger.functionSchema ?? "public";
134
+ const schema = trigger.functionSchema ?? "better_supabase";
120
135
  const fn = `${sqlIdent(schema)}.${sqlIdent(`bs_broadcast_${name}_${slug(meta.name)}`)}`;
121
136
  const triggerName = sqlIdent(`bs_broadcast_${name}`);
122
137
  const target = `${sqlIdent(meta.schema)}.${sqlIdent(meta.name)}`;
@@ -127,6 +142,7 @@ function defineTopic(template, options = {}) {
127
142
  ]).join(" or ");
128
143
  return [
129
144
  `-- better-supabase: broadcast ${meta.name} changes to ${template}`,
145
+ `create schema if not exists ${sqlIdent(schema)};`,
130
146
  `create or replace function ${fn}()`,
131
147
  "returns trigger",
132
148
  "language plpgsql",
@@ -1,7 +1,7 @@
1
- import { p as DbError } from "./result-CTkCZ6JA.js";
2
- import { l as RefreshEvent } from "./events-2OJsVG_d.js";
3
- import { h as RequestContext } from "./plugin-HvGqCurB.js";
4
- import { t as StandardSchemaV1 } from "./standard-CyhRzctb.js";
1
+ import { p as DbError } from "./result-c2OYgCV2.js";
2
+ import { f as Logger, l as RefreshEvent } from "./events-Do8jXh4D.js";
3
+ import { h as RequestContext } from "./plugin-DVRB8qNE.js";
4
+ import { t as StandardSchemaV1 } from "./standard-eudBiRk1.js";
5
5
  import { t as BetterSupabaseEnv } from "./index-Dy6kuF0P.js";
6
6
  import { CookieOptions } from "@supabase/ssr";
7
7
  import { JWTClaims, SupabaseEnv, UserClaims } from "@supabase/server";
@@ -47,9 +47,10 @@ declare function applyCookieWrites(cookies: readonly CookieRecord[], writes: rea
47
47
  //#region src/auth/resolve.d.ts
48
48
  /**
49
49
  * Who is calling. `C` is the claims type from `sb.claims(schema)`; the
50
- * verified payload keeps every JWT claim and adds the schema's output.
50
+ * verified payload keeps every JWT claim and adds the schema's output. `P`
51
+ * is the `user_metadata` type from `sb.userMetadata(schema)`.
51
52
  */
52
- type AuthState<C = unknown> = {
53
+ type AuthState<C = unknown, P = unknown> = {
53
54
  readonly kind: "user";
54
55
  readonly token: string;
55
56
  readonly claims: JWTClaims & C;
@@ -57,6 +58,12 @@ type AuthState<C = unknown> = {
57
58
  readonly source: "bearer" | "cookie" | (string & {});
58
59
  /** Seconds since epoch, from the token's `exp`. */
59
60
  readonly expiresAt: number | null;
61
+ /**
62
+ * `user_metadata` parsed by `sb.userMetadata(schema)`; `undefined`
63
+ * without a schema or when the metadata fails it. Users can write this
64
+ * data themselves: use it for display, never for access.
65
+ */
66
+ readonly profile?: P;
60
67
  } | {
61
68
  readonly kind: "service";
62
69
  readonly keyName: string;
@@ -69,7 +76,7 @@ type AuthState<C = unknown> = {
69
76
  readonly reason: InvalidReason;
70
77
  readonly error: DbError;
71
78
  };
72
- type InvalidReason = "token" | "claims";
79
+ type InvalidReason = "token" | "claims" | "actor";
73
80
  /**
74
81
  * What an `AuthResolver` returns. `reason` may be left out of an `invalid`
75
82
  * state; it then counts as `token`.
@@ -114,6 +121,14 @@ interface ResolveAuthOptions {
114
121
  * resolves to `{ kind: 'invalid', reason: 'claims' }`.
115
122
  */
116
123
  readonly claims?: StandardSchemaV1;
124
+ /**
125
+ * Parses `user_metadata` into `profile` (`sb.userMetadata(schema)` sets
126
+ * it). A failure leaves `profile` undefined and warns once; the session
127
+ * stays valid.
128
+ */
129
+ readonly userMetadata?: StandardSchemaV1;
130
+ /** Receives the `userMetadata` warning. Defaults to `console`. */
131
+ readonly logger?: Logger;
117
132
  readonly resolvers?: readonly AuthResolver[];
118
133
  /**
119
134
  * The caller's IP, sent to Auth as `Sb-Forwarded-For` on refreshes so its
@@ -125,10 +140,11 @@ interface ResolveAuthOptions {
125
140
  /** Called after every refresh attempt (metrics, logging). */
126
141
  readonly onRefresh?: (event: RefreshEvent) => void;
127
142
  readonly fetch?: typeof fetch;
143
+ /** Epoch milliseconds, the unit JWT `exp` and `iat` math needs. */
128
144
  readonly now?: () => number;
129
145
  }
130
- interface AuthResolution<C = unknown> {
131
- readonly auth: AuthState<C>;
146
+ interface AuthResolution<C = unknown, P = unknown> {
147
+ readonly auth: AuthState<C, P>;
132
148
  /** Cookie writes for the response. Empty unless the session was refreshed or cleared. */
133
149
  readonly cookies: readonly CookieWrite[];
134
150
  /** No-store headers, set whenever `cookies` is not empty. */
@@ -1,13 +1,14 @@
1
- import { A as TableMeta, P as Update, h as Insert, k as TableKey, n as AnyModels, t as AnyFunctions } from "./types-C2fslP1z.js";
2
- import { b as ValidationIssue, o as Result } from "./result-CTkCZ6JA.js";
3
- import { t as BetterSupabase, xt as SelectArg } from "./define-CTfWw-SS.js";
4
- import { r as RespondOptions } from "./respond-PoTpy5DT.js";
5
- import { i as ResourceOperation } from "./index-DRid0qIv.js";
1
+ import { N as TableKey, P as TableMeta, R as Update, g as Insert, n as AnyModels, t as AnyFunctions } from "./types-DONv9SHN.js";
2
+ import { b as ValidationIssue, o as Result } from "./result-c2OYgCV2.js";
3
+ import { t as BetterSupabase, xt as SelectArg } from "./define-DUbydmJa.js";
4
+ import { r as RespondOptions } from "./respond-DAkPIdTH.js";
5
+ import { i as ResourceOperation } from "./index-qriRM9cZ.js";
6
6
  import { StandardSchemaV1 } from "@standard-schema/spec";
7
7
  //#region src/server/resource.d.ts
8
8
  /** What a resource needs from `defineListQuery`. */
9
9
  interface ResourceList {
10
10
  readonly table: string;
11
+ readonly pagination?: ResourcePagination;
11
12
  readonly openapi: readonly unknown[];
12
13
  readonly jsonSchema: Readonly<Record<string, unknown>>;
13
14
  parse(input: never): {
@@ -31,9 +32,16 @@ interface ResourceRouteOptions<M extends AnyModels, T extends TableKey<M>> {
31
32
  readonly create?: StandardSchemaV1<unknown, Insert<M, T>>;
32
33
  readonly update?: StandardSchemaV1<unknown, Update<M, T>>;
33
34
  };
34
- /** Largest page for the default `page`/`size` list. Defaults to 200. */
35
+ /**
36
+ * Paging of the default list: `offset` (`page` and `size`) or `cursor`
37
+ * (`after` and `size`, keyset). A `list` query brings its own. Defaults
38
+ * to `offset`.
39
+ */
40
+ readonly pagination?: ResourcePagination;
41
+ /** Largest page for the default list. Defaults to 200. */
35
42
  readonly maxPageSize?: number;
36
43
  }
44
+ type ResourcePagination = "offset" | "cursor";
37
45
  interface ResourceInput {
38
46
  /** Primary key value; strings are converted for integer keys. */
39
47
  readonly id?: unknown;
@@ -49,6 +57,7 @@ interface ResourceHandler {
49
57
  /** Name of the key parameter, or `undefined` for tables without a single-column key. */
50
58
  readonly keyParam: string | undefined;
51
59
  readonly list: ResourceList | undefined;
60
+ readonly pagination: ResourcePagination;
52
61
  readonly maxPageSize: number;
53
62
  /** Runs one operation as whoever `db` is bound to. Resolves to a `Result`. */
54
63
  execute(db: object, operation: ResourceOperation, input?: ResourceInput): Promise<Result<unknown>>;
@@ -62,4 +71,4 @@ interface ResourceHandler {
62
71
  */
63
72
  declare function defineResource<M extends AnyModels, D, F extends AnyFunctions, E, T extends TableKey<M>>(sb: BetterSupabase<M, D, F, E>, table: T, options?: ResourceRouteOptions<M, T>): ResourceHandler;
64
73
  //#endregion
65
- export { defineResource as a, ResourceRouteOptions as i, ResourceInput as n, ResourceList as r, ResourceHandler as t };
74
+ export { ResourceRouteOptions as a, ResourcePagination as i, ResourceInput as n, defineResource as o, ResourceList as r, ResourceHandler as t };
@@ -1,7 +1,7 @@
1
1
  import { n as dbError } from "./errors-Bl3rkOzo.js";
2
2
  import { i as ok, n as err } from "./result-DWXatkd6.js";
3
3
  import { t as validate } from "./standard-FUi3Z3-7.js";
4
- import { i as respond } from "./respond-4-dhoV7x.js";
4
+ import { i as respond } from "./respond-C44EZAJo.js";
5
5
  //#region src/server/resource.ts
6
6
  const INTEGER = /* @__PURE__ */ new Set([
7
7
  "int2",
@@ -24,24 +24,44 @@ async function readBody(request) {
24
24
  } catch {}
25
25
  return err(dbError("invalid_request", "The request body must be a JSON object"));
26
26
  }
27
- function pageArgs(query, max) {
27
+ function pageArgs(query, max, pagination) {
28
+ const raw = (name) => {
29
+ const value = query instanceof URLSearchParams ? query.get(name) : query?.[name];
30
+ return value === null || value === "" ? void 0 : value;
31
+ };
28
32
  const read = (name, fallback) => {
29
- const raw = query instanceof URLSearchParams ? query.get(name) : query?.[name];
30
- if (raw === null || raw === void 0 || raw === "") return fallback;
31
- const value = Number(raw);
32
- return Number.isSafeInteger(value) && value >= 1 ? value : void 0;
33
+ const value = raw(name);
34
+ if (value === void 0) return fallback;
35
+ const number = Number(value);
36
+ return Number.isSafeInteger(number) && number >= 1 ? number : void 0;
33
37
  };
34
- const page = read("page", 1);
38
+ const issues = [];
35
39
  const size = read("size", 50);
36
- if (page === void 0 || size === void 0 || size > max) return err(dbError("validation", "Invalid page parameters", { issues: [...page === void 0 ? [{
37
- message: "Must be a positive integer",
38
- path: ["page"]
39
- }] : [], ...size === void 0 || size > max ? [{
40
+ if (size === void 0 || size > max) issues.push({
40
41
  message: `Must be between 1 and ${max}`,
41
42
  path: ["size"]
42
- }] : []] }));
43
- return ok({
44
- page,
43
+ });
44
+ const [used, unused] = pagination === "cursor" ? ["after", "page"] : ["page", "after"];
45
+ if (raw(unused) !== void 0) issues.push({
46
+ message: `This list pages with \`${used}\``,
47
+ path: [unused]
48
+ });
49
+ const page = read("page", 1);
50
+ if (pagination === "offset" && page === void 0) issues.push({
51
+ message: "Must be a positive integer",
52
+ path: ["page"]
53
+ });
54
+ const after = raw("after");
55
+ if (pagination === "cursor" && after !== void 0 && typeof after !== "string") issues.push({
56
+ message: "Must be text",
57
+ path: ["after"]
58
+ });
59
+ if (issues.length > 0 || size === void 0) return err(dbError("validation", "Invalid page parameters", { issues }));
60
+ return pagination === "cursor" ? ok({
61
+ after: typeof after === "string" ? after : null,
62
+ size
63
+ }) : ok({
64
+ page: page ?? 1,
45
65
  size
46
66
  });
47
67
  }
@@ -78,6 +98,7 @@ function defineResource(sb, table, options = {}) {
78
98
  const extra = options.select === void 0 ? {} : { select: options.select };
79
99
  const maxPageSize = options.maxPageSize ?? 200;
80
100
  const list = options.list;
101
+ const pagination = list ? list.pagination ?? "offset" : options.pagination ?? "offset";
81
102
  const execute = async (db, operation, input = {}) => {
82
103
  const repository = db[table];
83
104
  if (!repository) throw new TypeError(`The db has no "${table}" repository`);
@@ -90,12 +111,12 @@ function defineResource(sb, table, options = {}) {
90
111
  if (!query.ok) return err(dbError("validation", "Invalid list query", { issues: query.issues }));
91
112
  return settled(list.run(db, query.value, extra));
92
113
  }
93
- const page = pageArgs(input.query, maxPageSize);
114
+ const page = pageArgs(input.query, maxPageSize, pagination);
94
115
  if (!page.ok) return page;
95
116
  return settled(repository.paginate({
96
117
  ...extra,
97
118
  ...page.data,
98
- count: "exact"
119
+ ..."page" in page.data ? { count: "exact" } : {}
99
120
  }));
100
121
  }
101
122
  case "get": {
@@ -160,6 +181,7 @@ function defineResource(sb, table, options = {}) {
160
181
  operations,
161
182
  keyParam,
162
183
  list,
184
+ pagination,
163
185
  maxPageSize,
164
186
  execute,
165
187
  handle(request, db, id, respondOptions = {}) {
@@ -1,10 +1,11 @@
1
1
  import { n as dbError, o as isDbError, r as dbErrorOf } from "./errors-Bl3rkOzo.js";
2
2
  import { i as ok, n as err, o as toDbError, t as AsyncResult } from "./result-DWXatkd6.js";
3
- import { a as postgrestExecutor, n as StatsRecorder } from "./stats-3iy8u3_P.js";
4
- import { a as problemResponse } from "./problem-DPu6Fh-t.js";
5
- import { i as loadEnv } from "./env-BEToab0I.js";
6
- import { h as actClaim, i as authContext, o as resolveAuth, r as checkAal } from "./mfa-CpogP66z.js";
7
- import { n as postgresExecutor } from "./executor-Ceq60hPM.js";
3
+ import { a as postgrestExecutor, n as StatsRecorder } from "./stats-D78UQ8Yu.js";
4
+ import { a as problemResponse } from "./problem-CGt1ADix.js";
5
+ import { loadEnv } from "./env/index.js";
6
+ import { _ as actClaim, g as delegationOf, h as actorOf, i as authContext, o as resolveAuth, r as checkAal } from "./mfa-BxoR1RkN.js";
7
+ import { n as postgresExecutor } from "./executor-C57jx2pn.js";
8
+ import { parseCookieHeader, serializeCookieHeader } from "@supabase/ssr";
8
9
  import { createClient } from "@supabase/supabase-js";
9
10
  //#region src/server/replicas.ts
10
11
  /** Cookie holding the epoch ms until which reads stay on the primary. */
@@ -29,19 +30,18 @@ function replicaState(pinnedUntil = 0, now = Date.now()) {
29
30
  /** `bs-primary-until` from a `cookie` header, or 0. */
30
31
  function pinnedUntil(cookieHeader) {
31
32
  if (!cookieHeader) return 0;
32
- for (const part of cookieHeader.split(";")) {
33
- const [name, value] = part.trim().split("=", 2);
34
- if (name === "bs-primary-until") {
35
- const until = Number(value);
36
- return Number.isFinite(until) ? until : 0;
37
- }
38
- }
39
- return 0;
33
+ const value = parseCookieHeader(cookieHeader).find((cookie) => cookie.name === PRIMARY_COOKIE)?.value;
34
+ const until = Number(value);
35
+ return value !== void 0 && Number.isFinite(until) ? until : 0;
40
36
  }
41
37
  /** A `Set-Cookie` value keeping the next requests on the primary for `pinMs`. */
42
38
  function primaryCookie(pinMs, now = Date.now()) {
43
- const seconds = Math.max(1, Math.ceil(pinMs / 1e3));
44
- return `${PRIMARY_COOKIE}=${String(now + pinMs)}; Path=/; Max-Age=${String(seconds)}; HttpOnly; SameSite=Lax`;
39
+ return serializeCookieHeader(PRIMARY_COOKIE, String(now + pinMs), {
40
+ path: "/",
41
+ maxAge: Math.max(1, Math.ceil(pinMs / 1e3)),
42
+ httpOnly: true,
43
+ sameSite: "lax"
44
+ });
45
45
  }
46
46
  /**
47
47
  * Reads go to `replica` until the context is pinned; writes, and every read
@@ -248,7 +248,7 @@ function createServer(sb, options = {}) {
248
248
  return replica;
249
249
  },
250
250
  get sql() {
251
- if (!options.postgres || !sqlClaims) return void 0;
251
+ if (!options.postgres || !sqlClaims) return;
252
252
  sql ??= sqlFor(sqlClaims, context, recorder);
253
253
  return sql;
254
254
  },
@@ -257,11 +257,14 @@ function createServer(sb, options = {}) {
257
257
  };
258
258
  const resolve = async (request, resolveOptions = {}) => {
259
259
  const claims = options.auth?.claims ?? sb.claimsSchema;
260
+ const userMetadata = sb.userMetadataSchema;
260
261
  const resolution = await resolveAuth(request, {
262
+ logger: sb.events.logger,
261
263
  ...options.auth,
262
264
  ...claims ? { claims } : {},
265
+ ...userMetadata ? { userMetadata } : {},
263
266
  env: env(),
264
- ...resolveOptions.refresh !== void 0 ? { refresh: resolveOptions.refresh } : {},
267
+ ...resolveOptions.refresh === void 0 ? {} : { refresh: resolveOptions.refresh },
265
268
  onRefresh: (event) => {
266
269
  options.auth?.onRefresh?.(event);
267
270
  sb.events.emit("refresh", event);
@@ -326,10 +329,26 @@ function createServer(sb, options = {}) {
326
329
  }
327
330
  //#endregion
328
331
  //#region src/server/respond.ts
332
+ /** The scopes a delegated user token lacks; empty for the user's own session. */
333
+ function missingScopes(auth, required) {
334
+ if (required.length === 0) return [];
335
+ const outcome = actorOf(auth.claims);
336
+ if (!outcome.ok || !outcome.actor) return [];
337
+ const granted = new Set(delegationOf(auth.claims)?.scopes);
338
+ return required.filter((scope) => !granted.has(scope));
339
+ }
329
340
  /** `undefined` when `auth` may pass; otherwise the 401/403 error to send. */
330
- function guard(auth, allow = ["user"], aal = "aal1") {
341
+ function guard(auth, allow = ["user"], aal = "aal1", scopes = []) {
331
342
  if (auth.kind === "invalid") return auth.error;
332
- if (allow.includes(auth.kind)) return checkAal(auth, aal);
343
+ if (allow.includes(auth.kind)) {
344
+ const denied = checkAal(auth, aal);
345
+ if (denied || auth.kind !== "user") return denied;
346
+ const missing = missingScopes(auth, scopes);
347
+ return missing.length > 0 ? dbError("forbidden", `The token lacks the scope ${missing.join(" ")}`, {
348
+ code: "INSUFFICIENT_SCOPE",
349
+ scopes
350
+ }) : void 0;
351
+ }
333
352
  return auth.kind === "anon" ? dbError("unauthorized", "Sign in to continue", { code: "MISSING_CREDENTIALS" }) : dbError("forbidden", `A ${auth.kind} caller is not allowed here`);
334
353
  }
335
354
  function isResult(value) {