better-supabase 0.0.0 → 0.2.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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -1
  3. package/bin/better-supabase.js +4 -0
  4. package/dist/casing-da0uRTqt.js +14 -0
  5. package/dist/claims-CoTobNC-.js +25 -0
  6. package/dist/cli/bin.d.ts +1 -0
  7. package/dist/cli/bin.js +15 -0
  8. package/dist/cli/index.d.ts +226 -0
  9. package/dist/cli/index.js +3 -0
  10. package/dist/cli-DrSgDOQC.js +6646 -0
  11. package/dist/client/index.d.ts +2 -0
  12. package/dist/client/index.js +112 -0
  13. package/dist/compiler-CZ9nIuqy.d.ts +14 -0
  14. package/dist/config/index.d.ts +3 -0
  15. package/dist/config/index.js +3 -0
  16. package/dist/config-ClDXmVtY.js +277 -0
  17. package/dist/define-BXMYD9AY.js +1071 -0
  18. package/dist/define-e3zI4_4g.d.ts +679 -0
  19. package/dist/edge/index.d.ts +43 -0
  20. package/dist/edge/index.js +103 -0
  21. package/dist/entitlements-CTEBm7IB.js +20 -0
  22. package/dist/entitlements-Dng-ctuT.d.ts +42 -0
  23. package/dist/env/index.d.ts +2 -0
  24. package/dist/env/index.js +2 -0
  25. package/dist/env-BEToab0I.js +204 -0
  26. package/dist/errors-Bl3rkOzo.js +196 -0
  27. package/dist/events/index.d.ts +2 -0
  28. package/dist/events/index.js +2 -0
  29. package/dist/events-BuzTgn2D.js +135 -0
  30. package/dist/events-DOt3nSO-.d.ts +84 -0
  31. package/dist/executor-BYXQ9AGn.d.ts +25 -0
  32. package/dist/executor-CGdNQ27g.d.ts +195 -0
  33. package/dist/executor-DeppvirK.js +89 -0
  34. package/dist/guards-BnzDvFrz.js +10 -0
  35. package/dist/hono/index.d.ts +45 -0
  36. package/dist/hono/index.js +70 -0
  37. package/dist/index-Bw1bwtC0.d.ts +144 -0
  38. package/dist/index-CBDufH1M.d.ts +120 -0
  39. package/dist/index-CG69s15v.d.ts +38 -0
  40. package/dist/index-Ciu5Xlml.d.ts +74 -0
  41. package/dist/index-D4v7sncu.d.ts +62 -0
  42. package/dist/index-DOsjFvMf.d.ts +618 -0
  43. package/dist/index-DbxFr7XG.d.ts +76 -0
  44. package/dist/index-DuP14VHo.d.ts +216 -0
  45. package/dist/index-Dy6kuF0P.d.ts +71 -0
  46. package/dist/index-K-3WM2Hp.d.ts +117 -0
  47. package/dist/index.d.ts +110 -0
  48. package/dist/index.js +43 -0
  49. package/dist/invalidate-DbxuL2jL.js +18 -0
  50. package/dist/jobs/index.d.ts +173 -0
  51. package/dist/jobs/index.js +478 -0
  52. package/dist/json-schema-BpYqkCKf.js +182 -0
  53. package/dist/lint/index.d.ts +75 -0
  54. package/dist/lint/index.js +167 -0
  55. package/dist/list/index.d.ts +2 -0
  56. package/dist/list/index.js +410 -0
  57. package/dist/live-D8rV1Ldw.js +168 -0
  58. package/dist/live-DJyZMD6w.d.ts +191 -0
  59. package/dist/logger-glM28NVN.js +21 -0
  60. package/dist/mcp/index.d.ts +142 -0
  61. package/dist/mcp/index.js +463 -0
  62. package/dist/mfa-CpLfJSde.js +541 -0
  63. package/dist/mfa-TVia554c.d.ts +44 -0
  64. package/dist/next/image/index.d.ts +34 -0
  65. package/dist/next/image/index.js +40 -0
  66. package/dist/next/index.d.ts +210 -0
  67. package/dist/next/index.js +427 -0
  68. package/dist/openapi/index.d.ts +2 -0
  69. package/dist/openapi/index.js +332 -0
  70. package/dist/orpc/index.d.ts +46 -0
  71. package/dist/orpc/index.js +50 -0
  72. package/dist/otel/index.d.ts +40 -0
  73. package/dist/otel/index.js +168 -0
  74. package/dist/path-Cv__-OTj.d.ts +17 -0
  75. package/dist/permdock-sql-CghlHPuH.js +117 -0
  76. package/dist/plugin-BqR6wKMB.js +23 -0
  77. package/dist/plugin-C4TR9J0Z.d.ts +118 -0
  78. package/dist/plugins/actor/index.d.ts +13 -0
  79. package/dist/plugins/actor/index.js +43 -0
  80. package/dist/plugins/rules/index.d.ts +89 -0
  81. package/dist/plugins/rules/index.js +167 -0
  82. package/dist/plugins/soft-delete/index.d.ts +38 -0
  83. package/dist/plugins/soft-delete/index.js +69 -0
  84. package/dist/plugins/tenant/index.d.ts +49 -0
  85. package/dist/plugins/tenant/index.js +75 -0
  86. package/dist/plugins/timestamps/index.d.ts +9 -0
  87. package/dist/plugins/timestamps/index.js +37 -0
  88. package/dist/plugins/validation/index.d.ts +24 -0
  89. package/dist/plugins/validation/index.js +59 -0
  90. package/dist/pool-R2m_DkXi.d.ts +37 -0
  91. package/dist/postgres/index.d.ts +28 -0
  92. package/dist/postgres/index.js +79 -0
  93. package/dist/postgrest-cAEQ_IQN.d.ts +43 -0
  94. package/dist/problem-BTkTtRV9.js +97 -0
  95. package/dist/problem-DlgB0lYl.d.ts +47 -0
  96. package/dist/query/index.d.ts +2 -0
  97. package/dist/query/index.js +3 -0
  98. package/dist/query-ChvDbsDE.js +186 -0
  99. package/dist/react/index.d.ts +7 -0
  100. package/dist/react/index.js +5 -0
  101. package/dist/react/server.d.ts +13 -0
  102. package/dist/react/server.js +25 -0
  103. package/dist/react/session.d.ts +2 -0
  104. package/dist/react/session.js +3 -0
  105. package/dist/react-C83BdBGt.js +242 -0
  106. package/dist/read-set-4lI3W_gZ.js +216 -0
  107. package/dist/realtime/index.d.ts +4 -0
  108. package/dist/realtime/index.js +268 -0
  109. package/dist/resolve-CjHILk2w.d.ts +170 -0
  110. package/dist/resource-BHEWmQJK.js +175 -0
  111. package/dist/resource-CgZa3YG_.d.ts +65 -0
  112. package/dist/respond-DMcbQwfN.js +383 -0
  113. package/dist/respond-Dne1ARSa.d.ts +169 -0
  114. package/dist/result-CTkCZ6JA.d.ts +198 -0
  115. package/dist/result-DWXatkd6.js +103 -0
  116. package/dist/scope-CDHcS7dE.js +90 -0
  117. package/dist/seed-CzecODcJ.js +101 -0
  118. package/dist/server/index.d.ts +42 -0
  119. package/dist/server/index.js +68 -0
  120. package/dist/session-C3LxnQ0e.d.ts +22 -0
  121. package/dist/session-CqEwzVup.js +20 -0
  122. package/dist/shared-_6-uPL7E.js +57 -0
  123. package/dist/simplify-bwJpLnaS.js +665 -0
  124. package/dist/spec-pins-C3oc-Tdp.js +20 -0
  125. package/dist/spec-pins-CqorWDnO.d.ts +20 -0
  126. package/dist/sql-DTjA1LPj.js +260 -0
  127. package/dist/ssr/index.d.ts +5 -0
  128. package/dist/ssr/index.js +4 -0
  129. package/dist/standard-CyhRzctb.d.ts +7 -0
  130. package/dist/standard-FUi3Z3-7.js +21 -0
  131. package/dist/stats-C5W-rBfR.js +566 -0
  132. package/dist/storage/index.d.ts +5 -0
  133. package/dist/storage/index.js +2 -0
  134. package/dist/storage-DuGn38fr.js +527 -0
  135. package/dist/tables-rZxRxNPW.js +59 -0
  136. package/dist/template-mbvlysOo.d.ts +6 -0
  137. package/dist/testing/index.d.ts +325 -0
  138. package/dist/testing/index.js +819 -0
  139. package/dist/types-6cYK9pbJ.js +29 -0
  140. package/dist/types-C83v5SQz.d.ts +248 -0
  141. package/dist/version-Cqq1m3UY.js +4 -0
  142. package/dist/view-D1s0wa0T.d.ts +49 -0
  143. package/dist/view-aOYizTeE.js +36 -0
  144. package/dist/webhooks/index.d.ts +152 -0
  145. package/dist/webhooks/index.js +2 -0
  146. package/dist/webhooks-DMipjhSP.js +139 -0
  147. package/package.json +250 -17
  148. package/schemas/config-v1.json +373 -0
  149. package/schemas/doctor-report-v1.json +106 -0
  150. package/schemas/snapshot-v2.json +1006 -0
  151. package/skills/better-supabase/SKILL.md +84 -0
  152. package/skills/better-supabase/references/plugins.md +65 -0
  153. package/skills/better-supabase/references/troubleshooting.md +39 -0
  154. package/skills/better-supabase-api/SKILL.md +67 -0
  155. package/skills/better-supabase-api/references/adapters.md +108 -0
  156. package/skills/better-supabase-testing/SKILL.md +66 -0
  157. package/index.js +0 -1
@@ -0,0 +1,216 @@
1
+ import { r as IrBuilder, u as tableMeta } from "./simplify-bwJpLnaS.js";
2
+ import { n as touchedTables } from "./tables-rZxRxNPW.js";
3
+ //#region src/core/spec.ts
4
+ const READ_METHODS = [
5
+ "findMany",
6
+ "findFirst",
7
+ "findUnique",
8
+ "findById",
9
+ "count",
10
+ "aggregate",
11
+ "exists",
12
+ "paginate"
13
+ ];
14
+ function createSpecs(meta) {
15
+ const specs = {};
16
+ for (const table of Object.keys(meta.tables)) {
17
+ const methods = {};
18
+ for (const method of READ_METHODS) methods[method] = (...args) => ({
19
+ v: 1,
20
+ table,
21
+ method,
22
+ args: trimUndefined(args)
23
+ });
24
+ specs[table] = methods;
25
+ }
26
+ return specs;
27
+ }
28
+ function trimUndefined(args) {
29
+ const out = [...args];
30
+ while (out.length > 0 && out.at(-1) === void 0) out.pop();
31
+ return out;
32
+ }
33
+ function isQuerySpec(value) {
34
+ if (typeof value !== "object" || value === null) return false;
35
+ const spec = value;
36
+ return spec.v === 1 && typeof spec.table === "string" && READ_METHODS.includes(spec.method) && Array.isArray(spec.args);
37
+ }
38
+ /** The argument object of a spec's method (`findById` takes it second). */
39
+ function specOptions(spec) {
40
+ const value = spec.method === "findById" ? spec.args[1] : spec.args[0];
41
+ return typeof value === "object" && value !== null ? value : void 0;
42
+ }
43
+ const builders = /* @__PURE__ */ new WeakMap();
44
+ /**
45
+ * App keys of every table a spec reads: its table, included relations and
46
+ * relations used in `where`. Queries keyed by a spec go stale when any of
47
+ * them changes. Invalid arguments yield just the spec's table; running the
48
+ * spec reports the error.
49
+ */
50
+ function specTables(meta, spec) {
51
+ try {
52
+ return readTables(meta, spec);
53
+ } catch {
54
+ return [spec.table];
55
+ }
56
+ }
57
+ function readTables(meta, spec) {
58
+ let builder = builders.get(meta);
59
+ if (!builder) {
60
+ builder = new IrBuilder(meta);
61
+ builders.set(meta, builder);
62
+ }
63
+ const table = tableMeta(meta, spec.table);
64
+ const args = specOptions(spec);
65
+ const counts = spec.method === "count" || spec.method === "exists" || spec.method === "aggregate";
66
+ return touchedTables({
67
+ kind: "select",
68
+ table,
69
+ selection: counts ? {
70
+ columns: [],
71
+ includes: []
72
+ } : builder.selection(table, args?.["select"], args?.["include"]),
73
+ where: builder.where(table, args?.["where"]),
74
+ orderBy: [],
75
+ limit: void 0,
76
+ offset: void 0,
77
+ count: void 0,
78
+ head: counts,
79
+ single: void 0
80
+ });
81
+ }
82
+ //#endregion
83
+ //#region src/core/read-set.ts
84
+ const NAME = /^[a-z][a-z0-9_]{0,59}$/;
85
+ const PARAM = /^[a-z_][a-z0-9_]*$/i;
86
+ const TYPE = /^(?:[a-z_][a-z0-9_]*\.)?[a-z_][a-z0-9_]*(?:\[\])?$/;
87
+ /** `jsonb_build_object` takes at most 100 arguments. */
88
+ const MAX_ENTRIES = 50;
89
+ /** Placeholders are `NUL bs:<name> NUL`: no real parameter value holds a NUL. */
90
+ const NUL = String.fromCharCode(0);
91
+ const SENTINEL = new RegExp(`${NUL}bs:([a-z_][a-z0-9_]*)${NUL}`, "gi");
92
+ const EXACT = new RegExp(`^${NUL}bs:([a-z_][a-z0-9_]*)${NUL}$`, "i");
93
+ function sentinel(name) {
94
+ return `${NUL}bs:${name}${NUL}`;
95
+ }
96
+ /**
97
+ * Registers reads that run as one round trip:
98
+ *
99
+ * ```ts
100
+ * export const appChrome = defineReadSet(
101
+ * sb,
102
+ * 'app_chrome',
103
+ * { params: { userId: 'uuid' } },
104
+ * (s, p) => ({
105
+ * unread: s.notifications.count({ where: { userId: p.userId, readAt: null } }),
106
+ * me: s.profiles.findFirst({ where: { id: p.userId } }),
107
+ * }),
108
+ * );
109
+ * ```
110
+ *
111
+ * `p` holds placeholders, not values: pass them straight into `where` (or
112
+ * `in`), where the function reads them from its `p jsonb` argument.
113
+ */
114
+ function defineReadSet(sb, name, options, build) {
115
+ if (!NAME.test(name)) throw new TypeError(`better-supabase: read set name "${name}" must be snake_case and at most 60 characters`);
116
+ const params = options.params ?? {};
117
+ const placeholders = {};
118
+ for (const [key, type] of Object.entries(params)) {
119
+ if (!PARAM.test(key)) throw new TypeError(`better-supabase: read set "${name}" has an invalid parameter name "${key}"`);
120
+ if (!TYPE.test(type)) throw new TypeError(`better-supabase: read set "${name}" parameter "${key}" has an invalid type "${type}"`);
121
+ placeholders[key] = type.endsWith("[]") ? [sentinel(key)] : sentinel(key);
122
+ }
123
+ const specs = build(sb.spec, placeholders);
124
+ const entries = Object.entries(specs);
125
+ if (entries.length === 0 || entries.length > MAX_ENTRIES) throw new TypeError(`better-supabase: read set "${name}" needs between 1 and ${MAX_ENTRIES} reads`);
126
+ for (const [key, spec] of entries) if (!isQuerySpec(spec)) throw new TypeError(`better-supabase: read set "${name}" entry "${key}" is not a spec from sb.spec`);
127
+ return {
128
+ kind: "read-set",
129
+ name,
130
+ functionName: `rs_${name}`,
131
+ params,
132
+ roles: options.roles ?? ["authenticated"],
133
+ specs,
134
+ definition: sb
135
+ };
136
+ }
137
+ function isReadSet(value) {
138
+ return typeof value === "object" && value !== null && value.kind === "read-set" && typeof value.name === "string";
139
+ }
140
+ /** A parameter a string or array carries, if it is exactly a placeholder. */
141
+ function placeholderOf(value) {
142
+ if (typeof value === "string") {
143
+ const match = EXACT.exec(value);
144
+ return match ? {
145
+ name: match[1],
146
+ array: false
147
+ } : void 0;
148
+ }
149
+ if (Array.isArray(value) && value.length === 1) {
150
+ const inner = placeholderOf(value[0]);
151
+ return inner && !inner.array ? {
152
+ name: inner.name,
153
+ array: true
154
+ } : inner;
155
+ }
156
+ }
157
+ /** Splits a string around the placeholders inside it. */
158
+ function splitPlaceholders(value) {
159
+ const parts = [];
160
+ let last = 0;
161
+ for (const match of value.matchAll(SENTINEL)) {
162
+ if (match.index > last) parts.push({ text: value.slice(last, match.index) });
163
+ parts.push({ param: match[1] });
164
+ last = match.index + match[0].length;
165
+ }
166
+ if (last < value.length) parts.push({ text: value.slice(last) });
167
+ return parts;
168
+ }
169
+ function hasPlaceholder(value) {
170
+ return value.includes(`${NUL}bs:`);
171
+ }
172
+ /** Whether any string inside `value` is (or holds) a placeholder. */
173
+ function containsPlaceholder(value) {
174
+ if (typeof value === "string") return hasPlaceholder(value);
175
+ if (Array.isArray(value)) return value.some(containsPlaceholder);
176
+ if (typeof value === "object" && value !== null) return Object.values(value).some(containsPlaceholder);
177
+ return false;
178
+ }
179
+ /** Missing or wrongly typed parameters, as messages. */
180
+ function checkParams(set, values) {
181
+ const problems = [];
182
+ for (const [key, type] of Object.entries(set.params)) {
183
+ const value = values?.[key];
184
+ if (value === void 0 || value === null) problems.push(`missing parameter "${key}"`);
185
+ else if (type.endsWith("[]") !== Array.isArray(value)) problems.push(`parameter "${key}" must be ${type.endsWith("[]") ? "an array" : "a single value"}`);
186
+ }
187
+ return problems;
188
+ }
189
+ /** The specs with every placeholder replaced by its value. */
190
+ function bindParams(set, values) {
191
+ const bind = (value) => {
192
+ const placeholder = placeholderOf(value);
193
+ if (placeholder) return values[placeholder.name];
194
+ if (typeof value === "string") {
195
+ if (!hasPlaceholder(value)) return value;
196
+ return splitPlaceholders(value).map((part) => "text" in part ? part.text : String(values[part.param] ?? "")).join("");
197
+ }
198
+ if (Array.isArray(value)) return value.map(bind);
199
+ if (typeof value === "object" && value !== null) {
200
+ if (Object.getPrototypeOf(value) !== Object.prototype) return value;
201
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, bind(entry)]));
202
+ }
203
+ return value;
204
+ };
205
+ return Object.fromEntries(Object.entries(set.specs).map(([key, spec]) => [key, {
206
+ ...spec,
207
+ args: spec.args.map(bind)
208
+ }]));
209
+ }
210
+ /** App keys of every table the set reads. */
211
+ function readSetTables(set, meta) {
212
+ const schema = meta ?? set.definition.meta;
213
+ return [...new Set(Object.values(set.specs).flatMap((spec) => specTables(schema, spec)))];
214
+ }
215
+ //#endregion
216
+ export { hasPlaceholder as a, readSetTables as c, isQuerySpec as d, specTables as f, defineReadSet as i, splitPlaceholders as l, checkParams as n, isReadSet as o, containsPlaceholder as r, placeholderOf as s, bindParams as t, createSpecs as u };
@@ -0,0 +1,4 @@
1
+ import { x as PermdockTopicPolicy } from "../types-C83v5SQz.js";
2
+ import { n as TemplateValues, t as TemplateParams } from "../template-mbvlysOo.js";
3
+ 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-DJyZMD6w.js";
4
+ export { type CountRunner, EventSchemas, type LiveCountOptions, type LiveCountSeed, type LiveQueryOptions, type LiveSource, type LiveSubscription, type PermdockTopicPolicy, RealtimeClient, RowChange, SubscribeOptions, Subscription, SubscriptionStatus, type TemplateParams, type TemplateValues, Topic, TopicEvent, TopicHandlers, TopicMessage, TopicOptions, TopicPayload, TriggerOptions, defineTopic, liveCount, liveQuery, liveTopic, rowChange };
@@ -0,0 +1,268 @@
1
+ import { n as dbError } from "../errors-Bl3rkOzo.js";
2
+ import { i as ok, n as err, t as AsyncResult } from "../result-DWXatkd6.js";
3
+ import { i as tenantClaimPaths } from "../claims-CoTobNC-.js";
4
+ import { n as liveQuery, r as liveTopic, t as liveCount } from "../live-D8rV1Ldw.js";
5
+ import { s as toApp } from "../shared-_6-uPL7E.js";
6
+ import { a as sqlString, i as sqlIdent, n as parseTemplate, r as slug, t as permdockCheck } from "../permdock-sql-CghlHPuH.js";
7
+ import { REALTIME_SUBSCRIBE_STATES } from "@supabase/supabase-js";
8
+ //#region src/realtime/index.ts
9
+ const VALUE = /^[\w.@+=-]+$/;
10
+ function validateValue(_name, value) {
11
+ return VALUE.test(value) ? void 0 : "contains characters topics do not allow";
12
+ }
13
+ function claimSql(claim) {
14
+ const expressions = (typeof claim === "string" ? [claim] : claim).map((path) => {
15
+ const keys = path.split(".");
16
+ const last = keys.pop();
17
+ return `(select auth.jwt())${keys.map((key) => ` -> ${sqlString(key)}`).join("")} ->> ${sqlString(last)}`;
18
+ });
19
+ return expressions.length === 1 ? expressions[0] : `coalesce(${expressions.join(", ")})`;
20
+ }
21
+ async function validate(schema, value) {
22
+ if (!schema) return {
23
+ ok: true,
24
+ value
25
+ };
26
+ const result = await schema["~standard"].validate(value);
27
+ if (result.issues) return {
28
+ ok: false,
29
+ issues: result.issues.map((issue) => ({
30
+ message: issue.message,
31
+ ...issue.path ? { path: issue.path.map((part) => typeof part === "object" ? part.key : part) } : {}
32
+ }))
33
+ };
34
+ return {
35
+ ok: true,
36
+ value: result.value
37
+ };
38
+ }
39
+ function sendError(status, message) {
40
+ if (status === 401) return dbError("unauthorized", message);
41
+ if (status === 403) return dbError("forbidden", message);
42
+ if (status === 429 || status >= 500 || status === 0) return dbError("network", message, status ? { status } : {});
43
+ return dbError("invalid_request", message, { status });
44
+ }
45
+ /**
46
+ * A typed Realtime broadcast topic: names from a template, RLS policies for
47
+ * private channels, row-change triggers, and disposable subscriptions.
48
+ *
49
+ * ```ts
50
+ * export const notifications = defineTopic('org:{orgId}:notifications:{userId}', {
51
+ * events: { created: v.object({ id: v.string(), title: v.string() }) },
52
+ * });
53
+ * using sub = notifications.subscribe(supabase, { orgId, userId }, { created: (n) => toast(n.title) });
54
+ * ```
55
+ */
56
+ function defineTopic(template, options = {}) {
57
+ const parsed = parseTemplate(template, ":", validateValue);
58
+ const literal = template.replace(/\{[^}]+\}/g, " ");
59
+ const name = slug(options.name ?? literal) || "topic";
60
+ const isPrivate = options.private ?? true;
61
+ const schemas = options.events ?? {};
62
+ const segment = (param, kind) => {
63
+ const index = parsed.segmentOf(param);
64
+ if (index === void 0) throw new TypeError(`defineTopic: the ${kind} check needs {${param}} as a whole segment in "${template}"`);
65
+ return index;
66
+ };
67
+ const checks = [];
68
+ const permdock = options.permdock;
69
+ const tenantParam = options.tenant === false ? void 0 : options.tenant?.param ?? "orgId";
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);
75
+ return {
76
+ receive,
77
+ send: permdock.send === void 0 ? receive : permdockCheck(where, permdock, permdock.send, id)
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());
83
+ checks.push(`split_part((select realtime.topic()), ':', ${String(segment(tenantParam, "tenant"))}) = (${expression})`);
84
+ }
85
+ const ownerParam = options.owner === false ? void 0 : options.owner?.param ?? "userId";
86
+ if (ownerParam && (options.owner || parsed.params.includes(ownerParam))) checks.push(`split_part((select realtime.topic()), ':', ${String(segment(ownerParam, "owner"))}) = (select auth.uid())::text`);
87
+ const topicOf = (values) => parsed.build(values);
88
+ return {
89
+ template,
90
+ name,
91
+ params: parsed.params,
92
+ private: isPrivate,
93
+ topic: topicOf,
94
+ match: (topic) => parsed.match(topic),
95
+ sql() {
96
+ const extensions = options.presence ? "('broadcast', 'presence')" : "('broadcast')";
97
+ const condition = (extra) => [
98
+ `(select realtime.topic()) ~ ${sqlString(parsed.sqlPattern)}`,
99
+ `realtime.messages.extension in ${extensions}`,
100
+ ...checks,
101
+ ...extra === void 0 ? [] : [extra]
102
+ ].join("\n and ");
103
+ const lines = [`-- better-supabase: topic ${template}`];
104
+ const policies = [[
105
+ "receive",
106
+ "select",
107
+ "using",
108
+ condition(permdockChecks?.receive)
109
+ ]];
110
+ if (options.send || options.presence || permdock?.send !== void 0) policies.push([
111
+ "send",
112
+ "insert",
113
+ "with check",
114
+ condition(permdockChecks?.send)
115
+ ]);
116
+ for (const [suffix, command, clause, check] of policies) {
117
+ const policy = sqlIdent(`bs_topic_${name}_${suffix}`);
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 );`);
119
+ }
120
+ return `${lines.join("\n")}\n`;
121
+ },
122
+ triggerSql(sb, table, trigger) {
123
+ const meta = sb.meta.tables[table];
124
+ if (!meta) throw new TypeError(`defineTopic: unknown table "${table}"`);
125
+ const values = trigger.values;
126
+ const parts = template.split(/(\{[^}]+\})/).filter(Boolean).map((part) => {
127
+ const param = /^\{(.+)\}$/.exec(part)?.[1];
128
+ if (param === void 0) return sqlString(part);
129
+ const column = values[param];
130
+ const db = column === void 0 ? void 0 : meta.columns[column]?.db;
131
+ if (!db) throw new TypeError(`defineTopic: no column for {${param}} on "${table}"`);
132
+ return `rec.${sqlIdent(db)}::text`;
133
+ });
134
+ const schema = trigger.functionSchema ?? "public";
135
+ const fn = `${sqlIdent(schema)}.${sqlIdent(`bs_broadcast_${name}_${slug(meta.name)}`)}`;
136
+ const triggerName = sqlIdent(`bs_broadcast_${name}`);
137
+ const target = `${sqlIdent(meta.schema)}.${sqlIdent(meta.name)}`;
138
+ const events = (trigger.events ?? [
139
+ "insert",
140
+ "update",
141
+ "delete"
142
+ ]).join(" or ");
143
+ return [
144
+ `-- better-supabase: broadcast ${meta.name} changes to ${template}`,
145
+ `create or replace function ${fn}()`,
146
+ "returns trigger",
147
+ "language plpgsql",
148
+ "security definer",
149
+ "set search_path = ''",
150
+ "as $$",
151
+ "declare",
152
+ " rec record;",
153
+ "begin",
154
+ " if tg_op = 'DELETE' then rec := old; else rec := new; end if;",
155
+ " perform realtime.broadcast_changes(",
156
+ ` ${parts.join(" || ")},`,
157
+ " tg_op, tg_op, tg_table_name, tg_table_schema, new, old",
158
+ " );",
159
+ " return null;",
160
+ "end;",
161
+ "$$;",
162
+ `revoke execute on function ${fn}() from public, anon, authenticated;`,
163
+ "",
164
+ `drop trigger if exists ${triggerName} on ${target};`,
165
+ `create trigger ${triggerName} after ${events} on ${target}`,
166
+ ` for each row execute function ${fn}();`,
167
+ ""
168
+ ].join("\n");
169
+ },
170
+ subscribe(client, values, handlers, subscribeOptions = {}) {
171
+ const topic = topicOf(values);
172
+ const channel = client.channel(topic, { config: {
173
+ private: isPrivate,
174
+ broadcast: { self: subscribeOptions.self ?? false }
175
+ } });
176
+ const table = handlers;
177
+ channel.on("broadcast", { event: "*" }, (raw) => {
178
+ const message = {
179
+ event: raw.event,
180
+ payload: raw["payload"],
181
+ topic
182
+ };
183
+ const handler = table[message.event] ?? table["*"];
184
+ if (!handler) return;
185
+ validate(schemas[message.event], message.payload).then((checked) => {
186
+ if (checked.ok) handler(checked.value, message);
187
+ else subscribeOptions.onInvalid?.(message, checked.issues);
188
+ });
189
+ });
190
+ let closed = false;
191
+ subscribeOptions.onStatus?.("joining");
192
+ const ready = (async () => {
193
+ if (isPrivate) await client.realtime.setAuth();
194
+ if (closed) return;
195
+ await new Promise((resolve, reject) => {
196
+ channel.subscribe((status, error) => {
197
+ switch (status) {
198
+ case REALTIME_SUBSCRIBE_STATES.SUBSCRIBED:
199
+ subscribeOptions.onStatus?.("subscribed");
200
+ resolve();
201
+ return;
202
+ case REALTIME_SUBSCRIBE_STATES.CLOSED:
203
+ subscribeOptions.onStatus?.("closed");
204
+ resolve();
205
+ return;
206
+ case REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR:
207
+ case REALTIME_SUBSCRIBE_STATES.TIMED_OUT: {
208
+ const failure = error ?? /* @__PURE__ */ new Error(`Realtime ${status.toLowerCase()} on ${topic}`);
209
+ subscribeOptions.onStatus?.("error", failure);
210
+ reject(failure);
211
+ return;
212
+ }
213
+ default: reject(/* @__PURE__ */ new Error(`Unknown realtime status ${String(status)}`));
214
+ }
215
+ });
216
+ });
217
+ })();
218
+ ready.catch(() => void 0);
219
+ const unsubscribe = async () => {
220
+ if (closed) return;
221
+ closed = true;
222
+ await client.removeChannel(channel);
223
+ };
224
+ return {
225
+ topic,
226
+ channel,
227
+ ready,
228
+ unsubscribe,
229
+ [Symbol.dispose]: () => void unsubscribe(),
230
+ [Symbol.asyncDispose]: unsubscribe
231
+ };
232
+ },
233
+ send(client, values, event, payload) {
234
+ return AsyncResult.from(async () => {
235
+ const topic = topicOf(values);
236
+ const checked = await validate(schemas[event], payload);
237
+ if (!checked.ok) return err(dbError("validation", `Invalid payload for ${event}`, { issues: checked.issues }));
238
+ if (isPrivate) await client.realtime.setAuth();
239
+ const channel = client.channel(topic, { config: { private: isPrivate } });
240
+ try {
241
+ const response = await channel.httpSend(event, checked.value);
242
+ return response.success ? ok(void 0) : err(sendError(response.status, response.error));
243
+ } finally {
244
+ await client.removeChannel(channel);
245
+ }
246
+ });
247
+ }
248
+ };
249
+ }
250
+ /**
251
+ * Reads a `realtime.broadcast_changes` message as an app-cased row change.
252
+ * Returns `null` for messages about other tables.
253
+ */
254
+ function rowChange(sb, table, message) {
255
+ const meta = sb.meta.tables[table];
256
+ const payload = message.payload;
257
+ if (!meta || !payload || payload.table !== meta.name || payload.schema && payload.schema !== meta.schema) return null;
258
+ const operation = payload.operation ?? message.event;
259
+ if (operation !== "INSERT" && operation !== "UPDATE" && operation !== "DELETE") return null;
260
+ return {
261
+ operation,
262
+ table,
263
+ record: payload.record ? toApp(meta, payload.record) : null,
264
+ oldRecord: payload.old_record ? toApp(meta, payload.old_record) : null
265
+ };
266
+ }
267
+ //#endregion
268
+ export { defineTopic, liveCount, liveQuery, liveTopic, rowChange };
@@ -0,0 +1,170 @@
1
+ import { p as DbError } from "./result-CTkCZ6JA.js";
2
+ import { f as Logger, l as RefreshEvent } from "./events-DOt3nSO-.js";
3
+ import { h as RequestContext } from "./plugin-C4TR9J0Z.js";
4
+ import { t as StandardSchemaV1 } from "./standard-CyhRzctb.js";
5
+ import { t as BetterSupabaseEnv } from "./index-Dy6kuF0P.js";
6
+ import { CookieOptions } from "@supabase/ssr";
7
+ import { JWTClaims, SupabaseEnv, UserClaims } from "@supabase/server";
8
+ //#region src/auth/session.d.ts
9
+ /** The session object auth-js persists (`sb-<ref>-auth-token`). */
10
+ interface StoredSession {
11
+ readonly access_token: string;
12
+ readonly refresh_token: string;
13
+ readonly expires_at?: number;
14
+ readonly expires_in?: number;
15
+ readonly token_type?: string;
16
+ readonly user?: unknown;
17
+ readonly [key: string]: unknown;
18
+ }
19
+ interface CookieRecord {
20
+ readonly name: string;
21
+ readonly value: string;
22
+ }
23
+ interface CookieWrite {
24
+ readonly name: string;
25
+ readonly value: string;
26
+ readonly options: CookieOptions;
27
+ }
28
+ /** Headers that keep responses carrying session cookies out of shared caches. */
29
+ declare const AUTH_CACHE_HEADERS: Readonly<Record<string, string>>;
30
+ /** `sb-<first label of the hostname>-auth-token`, the supabase-js default. */
31
+ declare function sessionCookieName(url: string): string;
32
+ declare function parseCookies(header: string | null | undefined): CookieRecord[];
33
+ declare function serializeCookie(write: CookieWrite): string;
34
+ /**
35
+ * Reads the session written by `@supabase/ssr` (plain or `base64-`, whole or
36
+ * chunked). Mismatched chunks from a partial write read as no session.
37
+ */
38
+ declare function readSession(cookies: readonly CookieRecord[], name: string): StoredSession | null;
39
+ /**
40
+ * Cookie writes that store `session` (or clear it with `null`) exactly like
41
+ * `@supabase/ssr`, and expire chunks the new value no longer uses.
42
+ */
43
+ declare function writeSession(existing: readonly CookieRecord[], name: string, session: StoredSession | null, options?: CookieOptions): CookieWrite[];
44
+ /** Applies cookie writes to a cookie list, as the next request would see it. */
45
+ declare function applyCookieWrites(cookies: readonly CookieRecord[], writes: readonly CookieWrite[]): CookieRecord[];
46
+ //#endregion
47
+ //#region src/auth/resolve.d.ts
48
+ /**
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. `P`
51
+ * is the `user_metadata` type from `sb.userMetadata(schema)`.
52
+ */
53
+ type AuthState<C = unknown, P = unknown> = {
54
+ readonly kind: "user";
55
+ readonly token: string;
56
+ readonly claims: JWTClaims & C;
57
+ readonly user: UserClaims;
58
+ readonly source: "bearer" | "cookie" | (string & {});
59
+ /** Seconds since epoch, from the token's `exp`. */
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;
67
+ } | {
68
+ readonly kind: "service";
69
+ readonly keyName: string;
70
+ } | {
71
+ readonly kind: "anon";
72
+ /** `expired`: the cookie session needs a refresh that was not allowed here (run the proxy). */
73
+ readonly reason: "none" | "expired" | "signed_out" | "refresh_failed";
74
+ } | {
75
+ readonly kind: "invalid";
76
+ readonly reason: InvalidReason;
77
+ readonly error: DbError;
78
+ };
79
+ type InvalidReason = "token" | "claims";
80
+ /**
81
+ * What an `AuthResolver` returns. `reason` may be left out of an `invalid`
82
+ * state; it then counts as `token`.
83
+ */
84
+ type ResolvedState = Exclude<AuthState, {
85
+ kind: "invalid";
86
+ }> | {
87
+ readonly kind: "invalid";
88
+ readonly reason?: InvalidReason;
89
+ readonly error: DbError;
90
+ };
91
+ /**
92
+ * Extension point for other credentials (API keys, third-party auth, custom
93
+ * tokens). Return `undefined` when the request carries nothing this resolver
94
+ * understands; the built-in Bearer and cookie resolution runs next.
95
+ */
96
+ interface AuthResolver {
97
+ readonly name: string;
98
+ resolve(request: Request): Promise<ResolvedState | undefined> | ResolvedState | undefined;
99
+ }
100
+ interface ResolveAuthOptions {
101
+ readonly env: BetterSupabaseEnv;
102
+ /** Allow refreshing an expiring cookie session. Only where cookies can be written (proxy). */
103
+ readonly refresh?: boolean;
104
+ /** Refresh when the token expires within this many seconds. Defaults to 60. */
105
+ readonly leeway?: number;
106
+ /**
107
+ * Accept `sb_secret_` keys (`apikey` header) as service callers: `true` for
108
+ * any configured key, or the key names allowed (`['cron']`). Defaults to false.
109
+ */
110
+ readonly secret?: boolean | readonly string[];
111
+ readonly audience?: string | string[];
112
+ readonly issuer?: string | string[];
113
+ readonly cookie?: {
114
+ readonly name?: string;
115
+ readonly options?: CookieOptions;
116
+ };
117
+ /** Inline JWKS instead of fetching `env.jwksUrl` (tests, air-gapped). */
118
+ readonly jwks?: SupabaseEnv["jwks"];
119
+ /**
120
+ * Validates the verified claims (`sb.claims(schema)` sets it). A failure
121
+ * resolves to `{ kind: 'invalid', reason: 'claims' }`.
122
+ */
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;
132
+ readonly resolvers?: readonly AuthResolver[];
133
+ /**
134
+ * The caller's IP, sent to Auth as `Sb-Forwarded-For` on refreshes so its
135
+ * per-IP rate limit counts users, not your server. Only with a secret key
136
+ * (`env.secretKey`), which Auth requires for the header. Defaults to
137
+ * `clientIp` (the first `x-forwarded-for` hop); `false` turns it off.
138
+ */
139
+ readonly clientIp?: ((request: Request) => string | undefined) | false;
140
+ /** Called after every refresh attempt (metrics, logging). */
141
+ readonly onRefresh?: (event: RefreshEvent) => void;
142
+ readonly fetch?: typeof fetch;
143
+ readonly now?: () => number;
144
+ }
145
+ interface AuthResolution<C = unknown, P = unknown> {
146
+ readonly auth: AuthState<C, P>;
147
+ /** Cookie writes for the response. Empty unless the session was refreshed or cleared. */
148
+ readonly cookies: readonly CookieWrite[];
149
+ /** No-store headers, set whenever `cookies` is not empty. */
150
+ readonly headers: Readonly<Record<string, string>>;
151
+ /** Cookies the rest of this request should see, including a refreshed session. */
152
+ readonly requestCookies: readonly CookieRecord[];
153
+ /** Adds `cookies` and `headers` to a response. */
154
+ apply(response: Response): Response;
155
+ }
156
+ /** The first `x-forwarded-for` hop, the client as your edge saw it. */
157
+ declare function clientIp(request: Request): string | undefined;
158
+ /**
159
+ * Resolves who is calling: custom resolvers, then `Authorization: Bearer`
160
+ * (APIs, MCP, mobile), then the `@supabase/ssr` session cookie (browsers).
161
+ *
162
+ * A valid token costs no network call: it is verified locally against the
163
+ * cached JWKS. An expiring cookie session is refreshed only when `refresh` is
164
+ * set, through a single-flight request, and only then are cookies written.
165
+ */
166
+ declare function resolveAuth(request: Request, options: ResolveAuthOptions): Promise<AuthResolution>;
167
+ /** The repository context (`actor`, `claims`) for an auth state. */
168
+ declare function authContext(auth: AuthState): RequestContext;
169
+ //#endregion
170
+ export { readSession as _, ResolveAuthOptions as a, writeSession as b, clientIp as c, CookieOptions as d, CookieRecord as f, parseCookies as g, applyCookieWrites as h, InvalidReason as i, resolveAuth as l, StoredSession as m, AuthResolver as n, ResolvedState as o, CookieWrite as p, AuthState as r, authContext as s, AuthResolution as t, AUTH_CACHE_HEADERS as u, serializeCookie as v, sessionCookieName as y };