better-supabase 0.2.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 (139) hide show
  1. package/README.md +34 -31
  2. package/dist/base64-CQeB1eSf.js +42 -0
  3. package/dist/client/index.d.ts +1 -1
  4. package/dist/client/index.js +4 -14
  5. package/dist/{compiler-CZ9nIuqy.d.ts → compiler-2zqGzxVZ.d.ts} +2 -2
  6. package/dist/config/index.d.ts +4 -3
  7. package/dist/config/index.js +5 -3
  8. package/dist/config-Be4GwsqI.js +129 -0
  9. package/dist/{index-DOsjFvMf.d.ts → config-C1CYcm6o.d.ts} +49 -49
  10. package/dist/{config-ClDXmVtY.js → config-DOsGYSiT.js} +14 -137
  11. package/dist/{define-BXMYD9AY.js → define-DF341GDj.js} +71 -51
  12. package/dist/{define-e3zI4_4g.d.ts → define-DUbydmJa.d.ts} +10 -9
  13. package/dist/edge/index.d.ts +4 -4
  14. package/dist/edge/index.js +4 -4
  15. package/dist/{entitlements-Dng-ctuT.d.ts → entitlements-DvJXoI02.d.ts} +1 -1
  16. package/dist/env/index.js +203 -1
  17. package/dist/events/index.d.ts +1 -1
  18. package/dist/events/index.js +139 -1
  19. package/dist/{events-DOt3nSO-.d.ts → events-Do8jXh4D.d.ts} +3 -3
  20. package/dist/{executor-BYXQ9AGn.d.ts → executor-B26lgbHD.d.ts} +2 -2
  21. package/dist/{executor-DeppvirK.js → executor-C57jx2pn.js} +7 -2
  22. package/dist/{executor-CGdNQ27g.d.ts → executor-DynmIWhq.d.ts} +2 -2
  23. package/dist/hono/index.d.ts +5 -5
  24. package/dist/hono/index.js +4 -4
  25. package/dist/{index-CBDufH1M.d.ts → index-BNsABq7P.d.ts} +8 -8
  26. package/dist/{index-Bw1bwtC0.d.ts → index-CADlNHK-.d.ts} +27 -14
  27. package/dist/{index-DuP14VHo.d.ts → index-CTTMS6_1.d.ts} +19 -8
  28. package/dist/index-Cf-D2jk1.d.ts +50 -0
  29. package/dist/{index-K-3WM2Hp.d.ts → index-CriFFJ_p.d.ts} +4 -4
  30. package/dist/{index-Ciu5Xlml.d.ts → index-GV77NpRP.d.ts} +5 -4
  31. package/dist/{index-D4v7sncu.d.ts → index-UJ-LUxtI.d.ts} +3 -3
  32. package/dist/{index-CG69s15v.d.ts → index-kpgopLNv.d.ts} +4 -3
  33. package/dist/{index-DbxFr7XG.d.ts → index-qriRM9cZ.d.ts} +7 -2
  34. package/dist/index.d.ts +12 -12
  35. package/dist/index.js +16 -7
  36. package/dist/jobs/index.d.ts +25 -24
  37. package/dist/jobs/index.js +16 -10
  38. package/dist/{json-schema-BpYqkCKf.js → json-schema--aOXAGfU.js} +7 -2
  39. package/dist/lint/index.js +1 -1
  40. package/dist/list/index.d.ts +2 -2
  41. package/dist/list/index.js +128 -93
  42. package/dist/{live-DJyZMD6w.d.ts → live-QE3UPbGm.d.ts} +16 -6
  43. package/dist/mcp/index.d.ts +24 -17
  44. package/dist/mcp/index.js +41 -12
  45. package/dist/{mfa-CpLfJSde.js → mfa-BxoR1RkN.js} +74 -19
  46. package/dist/{mfa-TVia554c.d.ts → mfa-x3V3vGh-.d.ts} +1 -1
  47. package/dist/next/index.d.ts +39 -26
  48. package/dist/next/index.js +28 -23
  49. package/dist/openapi/index.d.ts +1 -1
  50. package/dist/openapi/index.js +57 -34
  51. package/dist/orpc/index.d.ts +6 -6
  52. package/dist/orpc/index.js +3 -3
  53. package/dist/otel/index.d.ts +2 -2
  54. package/dist/otel/index.js +4 -2
  55. package/dist/{permdock-sql-CghlHPuH.js → permdock-sql-Bta_G0v-.js} +11 -6
  56. package/dist/permdock-sql-DImJULY-.d.ts +17 -0
  57. package/dist/{plugin-C4TR9J0Z.d.ts → plugin-DVRB8qNE.d.ts} +5 -4
  58. package/dist/plugins/actor/index.d.ts +1 -1
  59. package/dist/plugins/rules/index.d.ts +2 -2
  60. package/dist/plugins/rules/index.js +4 -4
  61. package/dist/plugins/soft-delete/index.d.ts +3 -3
  62. package/dist/plugins/soft-delete/index.js +2 -2
  63. package/dist/plugins/tenant/index.d.ts +2 -2
  64. package/dist/plugins/timestamps/index.d.ts +1 -1
  65. package/dist/plugins/timestamps/index.js +1 -1
  66. package/dist/plugins/validation/index.d.ts +2 -2
  67. package/dist/pool-B9qsLyFR.d.ts +71 -0
  68. package/dist/postgres/index.d.ts +10 -5
  69. package/dist/postgres/index.js +36 -16
  70. package/dist/{postgrest-cAEQ_IQN.d.ts → postgrest-DmBlevh-.d.ts} +1 -1
  71. package/dist/{problem-BTkTtRV9.js → problem-CGt1ADix.js} +10 -4
  72. package/dist/{problem-DlgB0lYl.d.ts → problem-Z2RTQjZD.d.ts} +6 -4
  73. package/dist/query/index.d.ts +2 -2
  74. package/dist/query/index.js +1 -1
  75. package/dist/{query-ChvDbsDE.js → query-BdKLTPlj.js} +2 -2
  76. package/dist/react/index.d.ts +7 -7
  77. package/dist/react/index.js +2 -2
  78. package/dist/react/server.d.ts +3 -3
  79. package/dist/react/server.js +1 -1
  80. package/dist/react/session.d.ts +21 -2
  81. package/dist/react/session.js +19 -1
  82. package/dist/{react-C83BdBGt.js → react-DNBYNazu.js} +20 -16
  83. package/dist/{read-set-4lI3W_gZ.js → read-set-D0qBcboM.js} +3 -3
  84. package/dist/realtime/index.d.ts +4 -3
  85. package/dist/realtime/index.js +7 -6
  86. package/dist/{resolve-CjHILk2w.d.ts → resolve-BRciX2jK.d.ts} +6 -5
  87. package/dist/{resource-CgZa3YG_.d.ts → resource-CwVgiI5q.d.ts} +16 -7
  88. package/dist/{resource-BHEWmQJK.js → resource-D30rp0GN.js} +38 -16
  89. package/dist/{respond-DMcbQwfN.js → respond-C44EZAJo.js} +35 -19
  90. package/dist/{respond-Dne1ARSa.d.ts → respond-DAkPIdTH.d.ts} +15 -9
  91. package/dist/{result-CTkCZ6JA.d.ts → result-c2OYgCV2.d.ts} +13 -8
  92. package/dist/server/index.d.ts +35 -10
  93. package/dist/server/index.js +38 -9
  94. package/dist/{simplify-bwJpLnaS.js → simplify-LakqK1vH.js} +10 -16
  95. package/dist/sql/index.d.ts +139 -0
  96. package/dist/sql/index.js +1983 -0
  97. package/dist/{sql-DTjA1LPj.js → sql-BIgk12ew.js} +72 -16
  98. package/dist/ssr/index.d.ts +4 -4
  99. package/dist/ssr/index.js +2 -2
  100. package/dist/{standard-CyhRzctb.d.ts → standard-eudBiRk1.d.ts} +1 -1
  101. package/dist/{stats-C5W-rBfR.js → stats-D78UQ8Yu.js} +8 -7
  102. package/dist/storage/index.d.ts +4 -3
  103. package/dist/storage/index.js +530 -1
  104. package/dist/{tables-rZxRxNPW.js → tables-Dhha3BXM.js} +5 -5
  105. package/dist/temporal-BwshQk0W.js +27 -0
  106. package/dist/temporal-required-CMxeSVPo.js +19 -0
  107. package/dist/testing/index.d.ts +13 -11
  108. package/dist/testing/index.js +128 -29
  109. package/dist/{types-C83v5SQz.d.ts → types-DONv9SHN.d.ts} +14 -11
  110. package/dist/{version-Cqq1m3UY.js → version-BfpG2gks.js} +1 -1
  111. package/dist/{view-D1s0wa0T.d.ts → view-3Po_PVlr.d.ts} +34 -4
  112. package/dist/{view-aOYizTeE.js → view-wZ-9qIUz.js} +11 -2
  113. package/dist/webhooks/index.d.ts +18 -16
  114. package/dist/webhooks/index.js +1 -1
  115. package/dist/{webhooks-DMipjhSP.js → webhooks-CAIFQSTM.js} +11 -9
  116. package/package.json +24 -28
  117. package/schemas/config-v1.json +31 -1
  118. package/schemas/doctor-report-v1.json +17 -1
  119. package/schemas/snapshot-v2.json +35 -0
  120. package/skills/better-supabase/SKILL.md +23 -11
  121. package/skills/better-supabase/references/plugins.md +21 -18
  122. package/skills/better-supabase/references/troubleshooting.md +15 -15
  123. package/skills/better-supabase-api/SKILL.md +7 -7
  124. package/skills/better-supabase-api/references/adapters.md +30 -25
  125. package/skills/better-supabase-testing/SKILL.md +11 -8
  126. package/bin/better-supabase.js +0 -4
  127. package/dist/casing-da0uRTqt.js +0 -14
  128. package/dist/cli/bin.d.ts +0 -1
  129. package/dist/cli/bin.js +0 -15
  130. package/dist/cli/index.d.ts +0 -226
  131. package/dist/cli/index.js +0 -3
  132. package/dist/cli-DrSgDOQC.js +0 -6646
  133. package/dist/env-BEToab0I.js +0 -204
  134. package/dist/events-BuzTgn2D.js +0 -135
  135. package/dist/pool-R2m_DkXi.d.ts +0 -37
  136. package/dist/seed-CzecODcJ.js +0 -101
  137. package/dist/session-C3LxnQ0e.d.ts +0 -22
  138. package/dist/session-CqEwzVup.js +0 -20
  139. package/dist/storage-DuGn38fr.js +0 -527
@@ -1,10 +1,11 @@
1
- //#region src/list/index.ts
1
+ //#region src/list/list-query.ts
2
2
  /** Facet value meaning "not set": filters with `is null`. */
3
3
  const UNSET = "__unset__";
4
4
  const RESERVED = /* @__PURE__ */ new Set([
5
5
  "q",
6
6
  "sort",
7
7
  "page",
8
+ "after",
8
9
  "size"
9
10
  ]);
10
11
  function first(value) {
@@ -13,8 +14,9 @@ function first(value) {
13
14
  function all(value) {
14
15
  return (typeof value === "string" ? [value] : [...value ?? []]).flatMap((entry) => entry.split(",")).map((entry) => entry.trim()).filter(Boolean);
15
16
  }
17
+ const isParamValue = (value) => value === void 0 || typeof value === "string" || Array.isArray(value) && value.every((entry) => typeof entry === "string");
16
18
  function isSearchParams(value) {
17
- return value instanceof URLSearchParams || !("facets" in value && typeof value.facets === "object");
19
+ return value instanceof URLSearchParams || !("facets" in value && typeof value.facets === "object") && Object.values(value).every(isParamValue);
18
20
  }
19
21
  function readRecord(input) {
20
22
  if (!(input instanceof URLSearchParams)) return input;
@@ -47,6 +49,7 @@ function defineListQuery(sb, table, config) {
47
49
  const pageSize = config.pageSize ?? 50;
48
50
  const maxPageSize = config.maxPageSize ?? 200;
49
51
  const maxSearch = config.maxSearchLength ?? 200;
52
+ const cursor = config.pagination === "cursor";
50
53
  const sortKeys = Object.keys(config.sorts);
51
54
  const facetEntries = Object.entries(config.facets ?? {});
52
55
  for (const [key, column] of facetEntries) {
@@ -96,6 +99,18 @@ function defineListQuery(sb, table, config) {
96
99
  message: "Must be a whole number of at least 1",
97
100
  path: ["page"]
98
101
  });
102
+ else if (cursor && page !== 1) issues.push({
103
+ message: "This list pages with `after`, not `page`",
104
+ path: ["page"]
105
+ });
106
+ let after;
107
+ if (raw.after !== void 0 && raw.after !== "") {
108
+ if (cursor && typeof raw.after === "string") after = raw.after;
109
+ else issues.push({
110
+ message: cursor ? "Must be text" : "This list pages with `page`, not `after`",
111
+ path: ["after"]
112
+ });
113
+ }
99
114
  const size = raw.size === void 0 || raw.size === "" ? pageSize : toInt(raw.size);
100
115
  if (size === void 0 || !Number.isInteger(size) || size < 1 || size > maxPageSize) issues.push({
101
116
  message: `Must be a whole number from 1 to ${String(maxPageSize)}`,
@@ -121,6 +136,7 @@ function defineListQuery(sb, table, config) {
121
136
  ...q ? { q } : {},
122
137
  sort,
123
138
  page: page ?? 1,
139
+ ...after ? { after } : {},
124
140
  size: size ?? pageSize,
125
141
  facets: facetValues
126
142
  }
@@ -143,6 +159,7 @@ function defineListQuery(sb, table, config) {
143
159
  q: first(record["q"]),
144
160
  sort: first(record["sort"]),
145
161
  page: first(record["page"]),
162
+ after: first(record["after"]),
146
163
  size: first(record["size"]),
147
164
  facets: facetValues
148
165
  });
@@ -160,6 +177,7 @@ function defineListQuery(sb, table, config) {
160
177
  q: typed.q,
161
178
  sort: typed.sort,
162
179
  page: typed.page,
180
+ after: typed.after,
163
181
  size: typed.size,
164
182
  facets: rawFacets
165
183
  });
@@ -195,12 +213,15 @@ function defineListQuery(sb, table, config) {
195
213
  }
196
214
  function args(query) {
197
215
  const filter = where(query);
216
+ const paging = cursor ? { after: query.after ?? null } : {
217
+ page: query.page,
218
+ count: config.count ?? "exact"
219
+ };
198
220
  return {
199
221
  ...filter ? { where: filter } : {},
200
222
  orderBy: config.sorts[query.sort],
201
- page: query.page,
202
223
  size: query.size,
203
- count: config.count ?? "exact"
224
+ ...paging
204
225
  };
205
226
  }
206
227
  /** Whether a group's value passes the facet's selected values. */
@@ -249,6 +270,10 @@ function defineListQuery(sb, table, config) {
249
270
  serialize: (value) => value
250
271
  },
251
272
  page: intParser,
273
+ after: {
274
+ parse: (value) => value || null,
275
+ serialize: (value) => value
276
+ },
252
277
  size: intParser,
253
278
  ...Object.fromEntries(facets.map((facet) => [facet.key, listParser]))
254
279
  };
@@ -261,8 +286,102 @@ function defineListQuery(sb, table, config) {
261
286
  uniqueItems: true
262
287
  });
263
288
  const searchDescription = config.search ? Array.isArray(config.search) ? `Case-insensitive search in ${config.search.join(", ")}.` : "Full-text search (web search syntax)." : void 0;
289
+ const openapi = [
290
+ ...searchDescription ? [{
291
+ name: "q",
292
+ in: "query",
293
+ required: false,
294
+ description: searchDescription,
295
+ schema: {
296
+ type: "string",
297
+ maxLength: maxSearch
298
+ }
299
+ }] : [],
300
+ {
301
+ name: "sort",
302
+ in: "query",
303
+ required: false,
304
+ description: `Ordering. Default \`${config.defaultSort}\`.`,
305
+ schema: {
306
+ type: "string",
307
+ enum: sortKeys,
308
+ default: config.defaultSort
309
+ }
310
+ },
311
+ cursor ? {
312
+ name: "after",
313
+ in: "query",
314
+ required: false,
315
+ description: "`nextCursor` of the previous page. Leave it out for the first page.",
316
+ schema: { type: "string" }
317
+ } : {
318
+ name: "page",
319
+ in: "query",
320
+ required: false,
321
+ description: "1-based page number.",
322
+ schema: {
323
+ type: "integer",
324
+ minimum: 1,
325
+ default: 1
326
+ }
327
+ },
328
+ {
329
+ name: "size",
330
+ in: "query",
331
+ required: false,
332
+ description: "Page size.",
333
+ schema: {
334
+ type: "integer",
335
+ minimum: 1,
336
+ maximum: maxPageSize,
337
+ default: pageSize
338
+ }
339
+ },
340
+ ...facets.map((facet) => ({
341
+ name: facet.key,
342
+ in: "query",
343
+ required: false,
344
+ description: `Filter on ${facet.column}. Comma-separated; any value matches.${facet.nullable ? ` \`${UNSET}\` matches empty.` : ""}`,
345
+ schema: facetSchema(facet),
346
+ style: "form",
347
+ explode: false
348
+ }))
349
+ ];
350
+ const jsonSchema = {
351
+ type: "object",
352
+ additionalProperties: false,
353
+ properties: {
354
+ ...searchDescription ? { q: {
355
+ type: "string",
356
+ maxLength: maxSearch,
357
+ description: searchDescription
358
+ } } : {},
359
+ sort: {
360
+ type: "string",
361
+ enum: sortKeys,
362
+ default: config.defaultSort
363
+ },
364
+ ...cursor ? { after: { type: "string" } } : { page: {
365
+ type: "integer",
366
+ minimum: 1,
367
+ default: 1
368
+ } },
369
+ size: {
370
+ type: "integer",
371
+ minimum: 1,
372
+ maximum: maxPageSize,
373
+ default: pageSize
374
+ },
375
+ ...facets.length > 0 ? { facets: {
376
+ type: "object",
377
+ additionalProperties: false,
378
+ properties: Object.fromEntries(facets.map((facet) => [facet.key, facetSchema(facet)]))
379
+ } } : {}
380
+ }
381
+ };
264
382
  return {
265
383
  table,
384
+ pagination: config.pagination ?? "offset",
266
385
  defaults,
267
386
  facets,
268
387
  parse,
@@ -279,7 +398,8 @@ function defineListQuery(sb, table, config) {
279
398
  if (query.q) params.set("q", query.q);
280
399
  if (query.sort && query.sort !== config.defaultSort) params.set("sort", query.sort);
281
400
  if (query.page && query.page !== 1) params.set("page", String(query.page));
282
- if (query.size && query.size !== pageSize) params.set("size", String(query.size));
401
+ if (query.after) params.set("after", query.after);
402
+ if (query.size !== void 0 && query.size > 0 && query.size !== pageSize) params.set("size", String(query.size));
283
403
  for (const facet of facets) {
284
404
  const values = query.facets?.[facet.key];
285
405
  if (values && values.length > 0) params.set(facet.key, values.join(","));
@@ -297,7 +417,7 @@ function defineListQuery(sb, table, config) {
297
417
  const page = repository.paginate({
298
418
  ...extra,
299
419
  ...base,
300
- count: count ?? base["count"],
420
+ ...cursor ? {} : { count: count ?? base["count"] },
301
421
  ...combined ? { where: combined } : {}
302
422
  });
303
423
  if (!config.facetCounts) return page;
@@ -317,93 +437,8 @@ function defineListQuery(sb, table, config) {
317
437
  facetCounts: marginals(rows, query)
318
438
  })));
319
439
  },
320
- openapi: [
321
- ...searchDescription ? [{
322
- name: "q",
323
- in: "query",
324
- required: false,
325
- description: searchDescription,
326
- schema: {
327
- type: "string",
328
- maxLength: maxSearch
329
- }
330
- }] : [],
331
- {
332
- name: "sort",
333
- in: "query",
334
- required: false,
335
- description: `Ordering. Default \`${config.defaultSort}\`.`,
336
- schema: {
337
- type: "string",
338
- enum: sortKeys,
339
- default: config.defaultSort
340
- }
341
- },
342
- {
343
- name: "page",
344
- in: "query",
345
- required: false,
346
- description: "1-based page number.",
347
- schema: {
348
- type: "integer",
349
- minimum: 1,
350
- default: 1
351
- }
352
- },
353
- {
354
- name: "size",
355
- in: "query",
356
- required: false,
357
- description: "Page size.",
358
- schema: {
359
- type: "integer",
360
- minimum: 1,
361
- maximum: maxPageSize,
362
- default: pageSize
363
- }
364
- },
365
- ...facets.map((facet) => ({
366
- name: facet.key,
367
- in: "query",
368
- required: false,
369
- description: `Filter on ${facet.column}. Comma-separated; any value matches.${facet.nullable ? ` \`${UNSET}\` matches empty.` : ""}`,
370
- schema: facetSchema(facet),
371
- style: "form",
372
- explode: false
373
- }))
374
- ],
375
- jsonSchema: {
376
- type: "object",
377
- additionalProperties: false,
378
- properties: {
379
- ...searchDescription ? { q: {
380
- type: "string",
381
- maxLength: maxSearch,
382
- description: searchDescription
383
- } } : {},
384
- sort: {
385
- type: "string",
386
- enum: sortKeys,
387
- default: config.defaultSort
388
- },
389
- page: {
390
- type: "integer",
391
- minimum: 1,
392
- default: 1
393
- },
394
- size: {
395
- type: "integer",
396
- minimum: 1,
397
- maximum: maxPageSize,
398
- default: pageSize
399
- },
400
- ...facets.length > 0 ? { facets: {
401
- type: "object",
402
- additionalProperties: false,
403
- properties: Object.fromEntries(facets.map((facet) => [facet.key, facetSchema(facet)]))
404
- } } : {}
405
- }
406
- }
440
+ openapi,
441
+ jsonSchema
407
442
  };
408
443
  }
409
444
  //#endregion
@@ -1,10 +1,11 @@
1
- import { A as SchemaMeta, N as TableKey, O as Row, n as AnyModels, t as AnyFunctions, x as PermdockTopicPolicy } from "./types-C83v5SQz.js";
2
- import { b as ValidationIssue, o as Result, p as DbError, t as AsyncResult } from "./result-CTkCZ6JA.js";
3
- import { Z as QuerySpec, t as BetterSupabase } from "./define-e3zI4_4g.js";
1
+ import { A as SchemaMeta, N as TableKey, O as Row, n as AnyModels, t as AnyFunctions, x as PermdockTopicPolicy } from "./types-DONv9SHN.js";
2
+ import { b as ValidationIssue, o as Result, p as DbError, t as AsyncResult } from "./result-c2OYgCV2.js";
3
+ import { Z as QuerySpec, t as BetterSupabase } from "./define-DUbydmJa.js";
4
+ import { t as PermdockCatalog } from "./permdock-sql-DImJULY-.js";
4
5
  import { n as TemplateValues, t as TemplateParams } from "./template-mbvlysOo.js";
5
6
  import { RealtimeChannel, SupabaseClient } from "@supabase/supabase-js";
6
7
  import { StandardSchemaV1 } from "@standard-schema/spec";
7
- //#region src/realtime/index.d.ts
8
+ //#region src/realtime/topic.d.ts
8
9
  type EventSchemas = Readonly<Record<string, StandardSchemaV1>>;
9
10
  interface TopicOptions<E extends EventSchemas> {
10
11
  /** Name used for generated policies and functions. Defaults to the template's literal parts. */
@@ -34,9 +35,15 @@ interface TopicOptions<E extends EventSchemas> {
34
35
  * The helpers check role and scope only. Use this just for permissions
35
36
  * whose grants have no row conditions beyond the scope: for a permission
36
37
  * with row conditions (e.g. `authorId = principal.id`) every member of the
37
- * scope may join. Leave those to the policies `permdock rls generate` writes.
38
+ * scope could join. Pass `catalog` to refuse those keys here;
39
+ * `better-supabase doctor` (BS214) refuses them from the catalog file.
38
40
  */
39
41
  readonly permdock?: PermdockTopicPolicy;
42
+ /**
43
+ * PermDock's `permissions.catalog.json`. With it, a `permdock` policy
44
+ * naming a permission with `rowConditions: true` throws.
45
+ */
46
+ readonly catalog?: PermdockCatalog;
40
47
  /** Let clients broadcast on the topic, not only receive. Defaults to `false`. */
41
48
  readonly send?: boolean;
42
49
  /** Also authorize presence. Defaults to `false`. */
@@ -77,7 +84,10 @@ interface TriggerOptions<M extends AnyModels, T extends keyof M, P extends strin
77
84
  readonly values: { readonly [K in TemplateParams<P>]: Extract<keyof Row<M, T>, string>; };
78
85
  /** Defaults to all three. */
79
86
  readonly events?: readonly ("insert" | "update" | "delete")[];
80
- /** Schema for the generated function. Defaults to `public`. */
87
+ /**
88
+ * Schema for the generated function. Defaults to `better_supabase`, which
89
+ * the Data API does not expose.
90
+ */
81
91
  readonly functionSchema?: string;
82
92
  }
83
93
  interface RowChange<R> {
@@ -1,13 +1,13 @@
1
- import { N as TableKey, n as AnyModels, t as AnyFunctions } from "../types-C83v5SQz.js";
2
- import { t as BetterSupabase } from "../define-e3zI4_4g.js";
3
- import { d as ServerContext, f as ServerOptions, l as BetterServer, n as GuardOptions } from "../respond-Dne1ARSa.js";
4
- import { i as ResourceRouteOptions } from "../resource-CgZa3YG_.js";
1
+ import { N as TableKey, n as AnyModels, t as AnyFunctions } from "../types-DONv9SHN.js";
2
+ import { t as BetterSupabase } from "../define-DUbydmJa.js";
3
+ import { d as ServerContext, f as ServerOptions, l as BetterServer, n as GuardOptions } from "../respond-DAkPIdTH.js";
4
+ import { a as ResourceRouteOptions } from "../resource-CwVgiI5q.js";
5
5
  import { StandardSchemaV1 } from "@standard-schema/spec";
6
- //#region src/mcp/index.d.ts
6
+ //#region src/mcp/mcp.d.ts
7
7
  export declare const MCP_PROTOCOL_VERSION: string;
8
8
  type Json = Readonly<Record<string, unknown>>;
9
9
  /** MCP tool annotations (hints for clients, not guarantees). */
10
- export interface ToolAnnotations {
10
+ interface ToolAnnotations {
11
11
  readonly title?: string;
12
12
  readonly readOnlyHint?: boolean;
13
13
  readonly destructiveHint?: boolean;
@@ -15,7 +15,7 @@ export interface ToolAnnotations {
15
15
  readonly openWorldHint?: boolean;
16
16
  }
17
17
  /** A tool as listed by `tools/list`. */
18
- export interface ToolInfo {
18
+ interface ToolInfo {
19
19
  readonly name: string;
20
20
  readonly title?: string;
21
21
  readonly description?: string;
@@ -23,11 +23,11 @@ export interface ToolInfo {
23
23
  readonly outputSchema?: Json;
24
24
  readonly annotations?: ToolAnnotations;
25
25
  }
26
- export interface ToolContext<M extends AnyModels, F extends AnyFunctions, E> extends ServerContext<M, F, E> {
26
+ interface ToolContext<M extends AnyModels, F extends AnyFunctions, E> extends ServerContext<M, F, E> {
27
27
  readonly request: Request;
28
28
  readonly signal: AbortSignal;
29
29
  }
30
- export interface McpTool<M extends AnyModels, F extends AnyFunctions, E> {
30
+ interface McpTool<M extends AnyModels, F extends AnyFunctions, E> {
31
31
  readonly info: ToolInfo;
32
32
  /** Validates arguments; its output is what `run` receives. */
33
33
  readonly input?: StandardSchemaV1;
@@ -36,13 +36,13 @@ export interface McpTool<M extends AnyModels, F extends AnyFunctions, E> {
36
36
  run(args: never, ctx: ToolContext<M, F, E>): unknown;
37
37
  }
38
38
  /** What the `authorize` and `visible` hooks see of a tool. */
39
- export interface ToolRef {
39
+ interface ToolRef {
40
40
  readonly info: ToolInfo;
41
41
  /** The tool's `meta`, e.g. a PermDock permission. `undefined` for table tools. */
42
42
  readonly meta: unknown;
43
43
  }
44
44
  /** The outcome of `authorize`. `scopes` turns a refusal into an OAuth challenge. */
45
- export type ToolDecision = {
45
+ type ToolDecision = {
46
46
  readonly allowed: true;
47
47
  } | {
48
48
  readonly allowed: false;
@@ -50,7 +50,7 @@ export type ToolDecision = {
50
50
  /** OAuth scopes the call needs, sent in an `insufficient_scope` challenge. */
51
51
  readonly scopes?: readonly string[];
52
52
  };
53
- export interface ToolDefinition<M extends AnyModels, F extends AnyFunctions, E, I> {
53
+ interface ToolDefinition<M extends AnyModels, F extends AnyFunctions, E, I> {
54
54
  readonly name: string;
55
55
  readonly title?: string;
56
56
  readonly description: string;
@@ -66,8 +66,8 @@ export interface ToolDefinition<M extends AnyModels, F extends AnyFunctions, E,
66
66
  readonly meta?: unknown;
67
67
  run(args: I, ctx: ToolContext<M, F, E>): unknown;
68
68
  }
69
- export type ToolResources<M extends AnyModels> = { readonly [T in TableKey<M>]?: ResourceRouteOptions<M, T> | true; };
70
- export interface McpOptions<M extends AnyModels, F extends AnyFunctions, E> extends ServerOptions, GuardOptions {
69
+ type ToolResources<M extends AnyModels> = { readonly [T in TableKey<M>]?: ResourceRouteOptions<M, T> | true; };
70
+ interface McpOptions<M extends AnyModels, F extends AnyFunctions, E> extends ServerOptions, GuardOptions {
71
71
  readonly name: string;
72
72
  readonly version: string;
73
73
  readonly title?: string;
@@ -91,6 +91,13 @@ export interface McpOptions<M extends AnyModels, F extends AnyFunctions, E> exte
91
91
  readonly scopes?: readonly string[];
92
92
  /** Origins allowed to call the server (DNS rebinding protection). Defaults to any. */
93
93
  readonly allowedOrigins?: readonly string[];
94
+ /**
95
+ * Hostnames the `Host` header may name, without the port (DNS rebinding
96
+ * protection). List the production, preview and local hosts. Defaults to any.
97
+ */
98
+ readonly allowedHosts?: readonly string[];
99
+ /** A page that explains how to connect, published as RFC 9728 `resource_documentation`. */
100
+ readonly resourceDocumentation?: string;
94
101
  /** Custom jsonb types, as in `createOpenApi`. */
95
102
  readonly json?: Readonly<Record<string, unknown>>;
96
103
  /** Include internal error messages in tool results. Defaults to `NODE_ENV === 'development'`. */
@@ -108,7 +115,7 @@ export interface McpOptions<M extends AnyModels, F extends AnyFunctions, E> exte
108
115
  */
109
116
  readonly visible?: (ctx: ToolContext<M, F, E>, tool: ToolRef) => boolean | Promise<boolean>;
110
117
  }
111
- export interface BetterMcp<M extends AnyModels, F extends AnyFunctions, E> extends BetterServer<M, F, E> {
118
+ interface BetterMcp<M extends AnyModels, F extends AnyFunctions, E> extends BetterServer<M, F, E> {
112
119
  /** Everything in one handler: the MCP endpoint and `/.well-known/oauth-protected-resource`. */
113
120
  readonly fetch: (request: Request) => Promise<Response>;
114
121
  /** The Streamable HTTP endpoint (POST JSON-RPC). */
@@ -121,7 +128,7 @@ export interface BetterMcp<M extends AnyModels, F extends AnyFunctions, E> exten
121
128
  /** Calls a tool directly, e.g. from tests or another transport. */
122
129
  call(name: string, args: unknown, ctx: ToolContext<M, F, E>): Promise<ToolResult>;
123
130
  }
124
- export interface ToolResult {
131
+ interface ToolResult {
125
132
  readonly content: readonly {
126
133
  readonly type: "text";
127
134
  readonly text: string;
@@ -139,4 +146,4 @@ export declare function defineTool<M extends AnyModels, F extends AnyFunctions,
139
146
  */
140
147
  export declare function createMcp<M extends AnyModels, D, F extends AnyFunctions, E>(sb: BetterSupabase<M, D, F, E>, options: McpOptions<M, F, E>): BetterMcp<M, F, E>;
141
148
  //#endregion
142
- export type { GuardOptions, ResourceRouteOptions };
149
+ export type { BetterMcp, GuardOptions, McpOptions, McpTool, ResourceRouteOptions, ToolAnnotations, ToolContext, ToolDecision, ToolDefinition, ToolInfo, ToolRef, ToolResources, ToolResult };
package/dist/mcp/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { n as dbError } from "../errors-Bl3rkOzo.js";
2
+ import { t as base64ToText } from "../base64-CQeB1eSf.js";
2
3
  import { t as validate } from "../standard-FUi3Z3-7.js";
3
- import { o as toProblem } from "../problem-BTkTtRV9.js";
4
+ import { o as toProblem } from "../problem-CGt1ADix.js";
4
5
  import { t as SPEC_PINS } from "../spec-pins-C3oc-Tdp.js";
5
- import { t as buildJsonSchema } from "../json-schema-BpYqkCKf.js";
6
- import { a as settle, c as withExtra, n as guard, o as createServer, s as extendServer, t as defaultExpose } from "../respond-DMcbQwfN.js";
7
- import { t as defineResource } from "../resource-BHEWmQJK.js";
6
+ import { t as buildJsonSchema } from "../json-schema--aOXAGfU.js";
7
+ import { a as settle, c as withExtra, n as guard, o as createServer, s as extendServer, t as defaultExpose } from "../respond-C44EZAJo.js";
8
+ import { t as defineResource } from "../resource-D30rp0GN.js";
8
9
  import { fromSupabaseUrl, unauthorizedResponse } from "@supabase/server/oauth-protected-resource";
9
- //#region src/mcp/index.ts
10
+ //#region src/mcp/mcp.ts
10
11
  const MCP_PROTOCOL_VERSION = SPEC_PINS.mcp;
11
12
  const WELL_KNOWN = "/.well-known/oauth-protected-resource";
12
13
  /** Revisions before 2026-07-28 open with an `initialize` handshake. */
@@ -16,6 +17,16 @@ const LEGACY_VERSIONS = [
16
17
  "2025-03-26"
17
18
  ];
18
19
  const SUPPORTED_VERSIONS = [MCP_PROTOCOL_VERSION, ...LEGACY_VERSIONS];
20
+ /** The `Host` header's hostname is in the list; a missing or malformed header is not. */
21
+ function hostAllowed(request, allowed) {
22
+ const host = request.headers.get("host");
23
+ if (!host) return false;
24
+ try {
25
+ return allowed.includes(new URL(`http://${host}`).hostname);
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
19
30
  /** A custom tool. Pass it in `createMcp({ tools: [...] })`. */
20
31
  function defineTool(definition) {
21
32
  if (!/^[A-Za-z0-9_.-]{1,64}$/.test(definition.name)) throw new TypeError(`Invalid tool name "${definition.name}": use 1-64 letters, digits, "_", "-" or "."`);
@@ -67,15 +78,15 @@ const ANNOTATIONS = {
67
78
  openWorldHint: false
68
79
  }
69
80
  };
70
- function pageSchema(max) {
81
+ function pageSchema(max, cursor) {
71
82
  return {
72
83
  type: "object",
73
84
  properties: {
74
- page: {
85
+ ...cursor ? { after: { type: "string" } } : { page: {
75
86
  type: "integer",
76
87
  minimum: 1,
77
88
  default: 1
78
- },
89
+ } },
79
90
  size: {
80
91
  type: "integer",
81
92
  minimum: 1,
@@ -99,7 +110,23 @@ function tableTools(resource, defs) {
99
110
  required: [keyParam, ...Object.keys(more)],
100
111
  additionalProperties: false
101
112
  });
102
- const page = {
113
+ const cursor = resource.pagination === "cursor";
114
+ const page = cursor ? {
115
+ type: "object",
116
+ properties: {
117
+ items: {
118
+ type: "array",
119
+ items: row
120
+ },
121
+ nextCursor: { type: ["string", "null"] },
122
+ hasMore: { type: "boolean" }
123
+ },
124
+ required: [
125
+ "items",
126
+ "nextCursor",
127
+ "hasMore"
128
+ ]
129
+ } : {
103
130
  type: "object",
104
131
  properties: {
105
132
  items: {
@@ -126,7 +153,7 @@ function tableTools(resource, defs) {
126
153
  };
127
154
  for (const operation of resource.operations) switch (operation) {
128
155
  case "list": {
129
- const { $schema: _, ...listSchema } = resource.list?.jsonSchema ?? pageSchema(resource.maxPageSize);
156
+ const { $schema: _, ...listSchema } = resource.list?.jsonSchema ?? pageSchema(resource.maxPageSize, cursor);
130
157
  add("list", `List ${table} rows visible to the caller, one page at a time.`, listSchema, page);
131
158
  break;
132
159
  }
@@ -174,7 +201,7 @@ function headerName(value) {
174
201
  const encoded = /^=\?base64\?(.*)\?=$/.exec(value)?.[1];
175
202
  if (encoded === void 0) return value;
176
203
  try {
177
- return new TextDecoder().decode(Uint8Array.from(atob(encoded), (char) => char.charCodeAt(0)));
204
+ return base64ToText(encoded);
178
205
  } catch {
179
206
  return value;
180
207
  }
@@ -352,6 +379,7 @@ function createMcp(sb, options) {
352
379
  const handler = async (request) => {
353
380
  const origin = request.headers.get("origin");
354
381
  if (origin && options.allowedOrigins && !options.allowedOrigins.includes(origin)) return rpcError(null, INVALID_REQUEST, "Origin not allowed", 403);
382
+ if (options.allowedHosts && !hostAllowed(request, options.allowedHosts)) return rpcError(null, INVALID_REQUEST, "Host not allowed", 403);
355
383
  if (request.method !== "POST") return new Response(null, {
356
384
  status: 405,
357
385
  headers: { allow: "POST" }
@@ -432,7 +460,8 @@ function createMcp(sb, options) {
432
460
  resource: resourceOf(request),
433
461
  authorization_servers: [...authorizationServers()],
434
462
  bearer_methods_supported: ["header"],
435
- ...scopes.length > 0 ? { scopes_supported: scopes } : {}
463
+ ...scopes.length > 0 ? { scopes_supported: scopes } : {},
464
+ ...options.resourceDocumentation === void 0 ? {} : { resource_documentation: options.resourceDocumentation }
436
465
  }, { headers: { "access-control-allow-origin": "*" } });
437
466
  const mcp = extendServer(server, {
438
467
  get tools() {