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,175 @@
1
+ import { n as dbError } from "./errors-Bl3rkOzo.js";
2
+ import { i as ok, n as err } from "./result-DWXatkd6.js";
3
+ import { t as validate } from "./standard-FUi3Z3-7.js";
4
+ import { i as respond } from "./respond-DMcbQwfN.js";
5
+ //#region src/server/resource.ts
6
+ const INTEGER = /* @__PURE__ */ new Set([
7
+ "int2",
8
+ "int4",
9
+ "int8"
10
+ ]);
11
+ function keyValue(table, raw) {
12
+ const invalid = err(dbError("invalid_request", `${JSON.stringify(raw)} is not a valid ${table.key} key`));
13
+ const column = table.columns[table.primaryKey[0]];
14
+ if (column && INTEGER.has(column.type)) {
15
+ const value = typeof raw === "string" ? Number(raw) : raw;
16
+ return typeof value === "number" && Number.isSafeInteger(value) ? ok(value) : invalid;
17
+ }
18
+ return typeof raw === "string" && raw !== "" ? ok(raw) : invalid;
19
+ }
20
+ async function readBody(request) {
21
+ try {
22
+ const body = await request.json();
23
+ if (typeof body === "object" && body !== null && !Array.isArray(body)) return ok(body);
24
+ } catch {}
25
+ return err(dbError("invalid_request", "The request body must be a JSON object"));
26
+ }
27
+ function pageArgs(query, max) {
28
+ 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
+ };
34
+ const page = read("page", 1);
35
+ 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
+ message: `Must be between 1 and ${max}`,
41
+ path: ["size"]
42
+ }] : []] }));
43
+ return ok({
44
+ page,
45
+ size
46
+ });
47
+ }
48
+ function asObject(value) {
49
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? ok(value) : err(dbError("invalid_request", "Expected an object of column values"));
50
+ }
51
+ async function settled(value) {
52
+ return await value;
53
+ }
54
+ function notAllowed(allowed) {
55
+ return new Response(null, {
56
+ status: 405,
57
+ headers: { allow: allowed.join(", ") }
58
+ });
59
+ }
60
+ /**
61
+ * Operations over one table, matching the paths and responses of
62
+ * `createOpenApi`: `GET/POST /<table>` and `GET/PATCH/DELETE /<table>/{id}`.
63
+ * Shared by the HTTP adapters and the MCP table tools.
64
+ */
65
+ function defineResource(sb, table, options = {}) {
66
+ const meta = sb.meta.tables[table];
67
+ if (!meta) throw new TypeError(`Unknown table "${table}"`);
68
+ if (options.list && options.list.table !== table) throw new TypeError(`The list query is for "${options.list.table}", not "${table}"`);
69
+ const operations = options.operations ?? (meta.kind === "view" ? ["list", "get"] : [
70
+ "list",
71
+ "get",
72
+ "create",
73
+ "update",
74
+ "delete"
75
+ ]);
76
+ const keyParam = meta.primaryKey.length === 1 ? meta.primaryKey[0] : void 0;
77
+ const has = (operation) => operations.includes(operation);
78
+ const extra = options.select === void 0 ? {} : { select: options.select };
79
+ const maxPageSize = options.maxPageSize ?? 200;
80
+ const list = options.list;
81
+ const execute = async (db, operation, input = {}) => {
82
+ const repository = db[table];
83
+ if (!repository) throw new TypeError(`The db has no "${table}" repository`);
84
+ if (!has(operation)) return err(dbError("forbidden", `${operation} is not enabled for ${table}`));
85
+ const key = () => keyParam ? keyValue(meta, input.id) : err(dbError("invalid_request", `${table} has no single-column key`));
86
+ switch (operation) {
87
+ case "list": {
88
+ if (list) {
89
+ const query = list.parse(input.query ?? {});
90
+ if (!query.ok) return err(dbError("validation", "Invalid list query", { issues: query.issues }));
91
+ return settled(list.run(db, query.value, extra));
92
+ }
93
+ const page = pageArgs(input.query, maxPageSize);
94
+ if (!page.ok) return page;
95
+ return settled(repository.paginate({
96
+ ...extra,
97
+ ...page.data,
98
+ count: "exact"
99
+ }));
100
+ }
101
+ case "get": {
102
+ const id = key();
103
+ if (!id.ok) return id;
104
+ return settled(repository.findById(id.data, extra));
105
+ }
106
+ case "create": {
107
+ const body = asObject(input.data);
108
+ if (!body.ok) return body;
109
+ const data = options.input?.create ? await validate(options.input.create, body.data, "data") : body;
110
+ if (!data.ok) return data;
111
+ return settled(repository.create(data.data, extra));
112
+ }
113
+ case "update": {
114
+ const id = key();
115
+ if (!id.ok) return id;
116
+ const body = asObject(input.data);
117
+ if (!body.ok) return body;
118
+ const data = options.input?.update ? await validate(options.input.update, body.data, "data") : body;
119
+ if (!data.ok) return data;
120
+ return settled(repository.update(id.data, data.data, extra));
121
+ }
122
+ case "delete": {
123
+ const id = key();
124
+ if (!id.ok) return id;
125
+ return settled(repository.delete(id.data));
126
+ }
127
+ default: throw new TypeError(`Unknown operation ${String(operation)}`);
128
+ }
129
+ };
130
+ const collectionMethods = [...has("list") ? ["GET"] : [], ...has("create") ? ["POST"] : []];
131
+ const itemMethods = [
132
+ ...has("get") ? ["GET"] : [],
133
+ ...has("update") ? ["PATCH"] : [],
134
+ ...has("delete") ? ["DELETE"] : []
135
+ ];
136
+ const route = async (request, db, id) => {
137
+ if (id === void 0) {
138
+ if (request.method === "GET" && has("list")) return execute(db, "list", { query: new URL(request.url).searchParams });
139
+ if (request.method === "POST" && has("create")) {
140
+ const body = await readBody(request);
141
+ return body.ok ? execute(db, "create", { data: body.data }) : body;
142
+ }
143
+ return notAllowed(collectionMethods);
144
+ }
145
+ if (!keyParam) return new Response(null, { status: 404 });
146
+ if (request.method === "GET" && has("get")) return execute(db, "get", { id });
147
+ if (request.method === "PATCH" && has("update")) {
148
+ const body = await readBody(request);
149
+ return body.ok ? execute(db, "update", {
150
+ id,
151
+ data: body.data
152
+ }) : body;
153
+ }
154
+ if (request.method === "DELETE" && has("delete")) return execute(db, "delete", { id });
155
+ return notAllowed(itemMethods);
156
+ };
157
+ return {
158
+ table,
159
+ meta,
160
+ operations,
161
+ keyParam,
162
+ list,
163
+ maxPageSize,
164
+ execute,
165
+ handle(request, db, id, respondOptions = {}) {
166
+ const status = id === void 0 && request.method === "POST" ? 201 : void 0;
167
+ return respond(() => route(request, db, id), status === void 0 ? respondOptions : {
168
+ ...respondOptions,
169
+ status
170
+ });
171
+ }
172
+ };
173
+ }
174
+ //#endregion
175
+ export { defineResource as t };
@@ -0,0 +1,65 @@
1
+ import { N as TableKey, P as TableMeta, R as Update, _ as Insert, n as AnyModels, t as AnyFunctions } from "./types-C83v5SQz.js";
2
+ import { b as ValidationIssue, o as Result } from "./result-CTkCZ6JA.js";
3
+ import { t as BetterSupabase, xt as SelectArg } from "./define-e3zI4_4g.js";
4
+ import { r as RespondOptions } from "./respond-Dne1ARSa.js";
5
+ import { i as ResourceOperation } from "./index-DbxFr7XG.js";
6
+ import { StandardSchemaV1 } from "@standard-schema/spec";
7
+ //#region src/server/resource.d.ts
8
+ /** What a resource needs from `defineListQuery`. */
9
+ interface ResourceList {
10
+ readonly table: string;
11
+ readonly openapi: readonly unknown[];
12
+ readonly jsonSchema: Readonly<Record<string, unknown>>;
13
+ parse(input: never): {
14
+ readonly ok: true;
15
+ readonly value: unknown;
16
+ } | {
17
+ readonly ok: false;
18
+ readonly issues: readonly ValidationIssue[];
19
+ };
20
+ run(db: never, query: never, extra?: never): PromiseLike<unknown>;
21
+ }
22
+ interface ResourceRouteOptions<M extends AnyModels, T extends TableKey<M>> {
23
+ /** Defaults to every operation for tables, `list` and `get` for views. */
24
+ readonly operations?: readonly ResourceOperation[];
25
+ /** Filters, search and sorts for `list`, from `defineListQuery`. */
26
+ readonly list?: ResourceList;
27
+ /** Columns returned by every operation. */
28
+ readonly select?: SelectArg<M, T>;
29
+ /** Validates request bodies before they reach the repository. */
30
+ readonly input?: {
31
+ readonly create?: StandardSchemaV1<unknown, Insert<M, T>>;
32
+ readonly update?: StandardSchemaV1<unknown, Update<M, T>>;
33
+ };
34
+ /** Largest page for the default `page`/`size` list. Defaults to 200. */
35
+ readonly maxPageSize?: number;
36
+ }
37
+ interface ResourceInput {
38
+ /** Primary key value; strings are converted for integer keys. */
39
+ readonly id?: unknown;
40
+ /** Body of `create` and `update`. */
41
+ readonly data?: unknown;
42
+ /** `list` parameters: URL params or typed values. */
43
+ readonly query?: URLSearchParams | Readonly<Record<string, unknown>>;
44
+ }
45
+ interface ResourceHandler {
46
+ readonly table: string;
47
+ readonly meta: TableMeta;
48
+ readonly operations: readonly ResourceOperation[];
49
+ /** Name of the key parameter, or `undefined` for tables without a single-column key. */
50
+ readonly keyParam: string | undefined;
51
+ readonly list: ResourceList | undefined;
52
+ readonly maxPageSize: number;
53
+ /** Runs one operation as whoever `db` is bound to. Resolves to a `Result`. */
54
+ execute(db: object, operation: ResourceOperation, input?: ResourceInput): Promise<Result<unknown>>;
55
+ /** Serves a collection request (`id` undefined) or an item request. */
56
+ handle(request: Request, db: object, id: string | undefined, options?: RespondOptions): Promise<Response>;
57
+ }
58
+ /**
59
+ * Operations over one table, matching the paths and responses of
60
+ * `createOpenApi`: `GET/POST /<table>` and `GET/PATCH/DELETE /<table>/{id}`.
61
+ * Shared by the HTTP adapters and the MCP table tools.
62
+ */
63
+ 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
+ //#endregion
65
+ export { defineResource as a, ResourceRouteOptions as i, ResourceInput as n, ResourceList as r, ResourceHandler as t };
@@ -0,0 +1,383 @@
1
+ import { n as dbError, o as isDbError, r as dbErrorOf } from "./errors-Bl3rkOzo.js";
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-C5W-rBfR.js";
4
+ import { a as problemResponse } from "./problem-BTkTtRV9.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-CpLfJSde.js";
7
+ import { n as postgresExecutor } from "./executor-DeppvirK.js";
8
+ import { createClient } from "@supabase/supabase-js";
9
+ //#region src/server/replicas.ts
10
+ /** Cookie holding the epoch ms until which reads stay on the primary. */
11
+ const PRIMARY_COOKIE = "bs-primary-until";
12
+ /** Default time reads stay on the primary after a write. */
13
+ const DEFAULT_PIN_MS = 5e3;
14
+ function replicaState(pinnedUntil = 0, now = Date.now()) {
15
+ let wrote = false;
16
+ const inherited = pinnedUntil > now;
17
+ return {
18
+ get pinned() {
19
+ return wrote || inherited;
20
+ },
21
+ get wrote() {
22
+ return wrote;
23
+ },
24
+ pin() {
25
+ wrote = true;
26
+ }
27
+ };
28
+ }
29
+ /** `bs-primary-until` from a `cookie` header, or 0. */
30
+ function pinnedUntil(cookieHeader) {
31
+ 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;
40
+ }
41
+ /** A `Set-Cookie` value keeping the next requests on the primary for `pinMs`. */
42
+ 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`;
45
+ }
46
+ /**
47
+ * Reads go to `replica` until the context is pinned; writes, and every read
48
+ * after a successful write, go to `primary`.
49
+ */
50
+ function routedExecutor(primary, replica, state) {
51
+ const batch = primary.batch?.bind(primary);
52
+ const replicaBatch = replica.batch?.bind(replica);
53
+ return {
54
+ name: primary.name,
55
+ ...primary.functionSources && replica.functionSources ? { functionSources: true } : {},
56
+ async execute(op, context) {
57
+ if (op.kind === "select" && !state.pinned) return replica.execute(op, context);
58
+ const result = await primary.execute(op, context);
59
+ if (op.kind !== "select" && result.ok) state.pin();
60
+ return result;
61
+ },
62
+ ...primary.rpc ? { async rpc(name, args, context) {
63
+ if (context.get && !state.pinned && replica.rpc) return replica.rpc(name, args, context);
64
+ const result = await primary.rpc(name, args, context);
65
+ if (!context.get && result.ok) state.pin();
66
+ return result;
67
+ } } : {},
68
+ ...batch ? { batch: (ops, context) => !state.pinned && replicaBatch ? replicaBatch(ops, context) : batch(ops, context) } : {}
69
+ };
70
+ }
71
+ //#endregion
72
+ //#region src/server/delete-account.ts
73
+ const REMOVE_BATCH = 1e3;
74
+ /** Maps an `AuthApiError` from `auth.admin.deleteUser` to a `DbError`. */
75
+ function fromAuthError(raw) {
76
+ if (isDbError(raw)) return raw;
77
+ const failure = typeof raw === "object" && raw !== null ? raw : {};
78
+ const message = failure.message ?? "Auth request failed";
79
+ const base = {
80
+ table: "auth.users",
81
+ ...failure.code ? { code: failure.code } : {}
82
+ };
83
+ if (failure.code === "user_not_found" || failure.status === 404) return dbError("not_found", message, base);
84
+ if (/database error deleting user/i.test(message)) return dbError("conflict", message, {
85
+ ...base,
86
+ hint: "A foreign key to auth.users without on delete cascade or set null blocks the delete (doctor BS406)"
87
+ });
88
+ if (failure.status === 401) return dbError("unauthorized", message, base);
89
+ if (failure.status === 403) return dbError("forbidden", message, base);
90
+ return failure.name === "AuthRetryableFetchError" || (failure.status ?? 0) >= 500 ? dbError("network", message, base) : dbError("unexpected", message, base);
91
+ }
92
+ /**
93
+ * Removes a user's Storage objects, then the Auth user, then announces the
94
+ * delete. Rows referencing `auth.users` follow their foreign keys.
95
+ */
96
+ function deleteAccount(sb, service, userId, options = {}) {
97
+ return AsyncResult.from(async () => {
98
+ const client = service();
99
+ const removed = {};
100
+ for (const bucket of options.buckets ?? []) {
101
+ const param = bucket.owner;
102
+ if (!param) continue;
103
+ options.signal?.throwIfAborted();
104
+ const storage = bucket.connect(client);
105
+ const listed = await storage.list({ [param]: userId }, options.signal ? { signal: options.signal } : {});
106
+ if (!listed.ok) return listed;
107
+ const paths = listed.data.map((object) => object.path).filter((path) => {
108
+ return bucket.match(path)?.[param] === userId;
109
+ });
110
+ for (let start = 0; start < paths.length; start += REMOVE_BATCH) {
111
+ const batch = await storage.remove(paths.slice(start, start + REMOVE_BATCH));
112
+ if (!batch.ok) return batch;
113
+ }
114
+ removed[bucket.id] = paths.length;
115
+ }
116
+ options.signal?.throwIfAborted();
117
+ try {
118
+ const { error } = await client.auth.admin.deleteUser(userId);
119
+ if (error) return err(fromAuthError(error));
120
+ } catch (cause) {
121
+ return err(fromAuthError(cause));
122
+ }
123
+ const context = options.context ?? {};
124
+ sb.events.emit("mutation", {
125
+ table: "auth.users",
126
+ kind: "delete",
127
+ rows: [{ id: userId }],
128
+ context
129
+ });
130
+ for (const table of options.cascades ?? []) sb.events.emit("mutation", {
131
+ table,
132
+ kind: "delete",
133
+ rows: [],
134
+ context
135
+ });
136
+ return ok({
137
+ userId,
138
+ removed
139
+ });
140
+ });
141
+ }
142
+ //#endregion
143
+ //#region src/server/server.ts
144
+ const STATELESS = {
145
+ persistSession: false,
146
+ autoRefreshToken: false,
147
+ detectSessionInUrl: false
148
+ };
149
+ /** `{ ...server, ...extra }` without reading the lazy `env` getter. */
150
+ function extendServer(server, extra) {
151
+ return Object.defineProperties({}, {
152
+ ...Object.getOwnPropertyDescriptors(server),
153
+ ...Object.getOwnPropertyDescriptors(extra)
154
+ });
155
+ }
156
+ /** `{ ...base, ...extra }` that keeps lazy getters lazy. */
157
+ function withExtra(base, extra) {
158
+ return Object.defineProperties({}, {
159
+ ...Object.getOwnPropertyDescriptors(base),
160
+ ...Object.getOwnPropertyDescriptors(extra)
161
+ });
162
+ }
163
+ /**
164
+ * Server-side entry point for APIs, jobs, MCP servers and framework
165
+ * adapters. Holds the secrets; `sb` stays isomorphic.
166
+ */
167
+ function createServer(sb, options = {}) {
168
+ let loaded;
169
+ const env = () => loaded ??= options.env ?? loadEnv();
170
+ let adminClient;
171
+ let anonClient;
172
+ const secretKey = () => {
173
+ const key = env().secretKey;
174
+ if (!key) throw new TypeError("admin() needs SUPABASE_SECRET_KEY; it is not set");
175
+ return key;
176
+ };
177
+ const serviceClient = () => {
178
+ adminClient ??= createClient(env().url, secretKey(), { auth: STATELESS });
179
+ return adminClient;
180
+ };
181
+ const readUrl = () => options.readUrl === false ? void 0 : options.readUrl?.replace(/\/+$/, "") ?? env().readUrl;
182
+ /** Clients for the primary share the service and anon clients; others don't. */
183
+ const supabaseAt = (url, auth, headers = {}) => {
184
+ const global = Object.keys(headers).length > 0 ? { global: { headers } } : void 0;
185
+ const shared = !global && url === env().url;
186
+ switch (auth.kind) {
187
+ case "user": return createClient(url, env().publishableKey, {
188
+ accessToken: () => Promise.resolve(auth.token),
189
+ ...global
190
+ });
191
+ case "service": return shared ? serviceClient() : createClient(url, secretKey(), {
192
+ auth: STATELESS,
193
+ ...global
194
+ });
195
+ case "anon":
196
+ case "invalid":
197
+ if (!shared) return createClient(url, env().publishableKey, {
198
+ auth: STATELESS,
199
+ ...global
200
+ });
201
+ anonClient ??= createClient(url, env().publishableKey, { auth: STATELESS });
202
+ return anonClient;
203
+ default: return auth;
204
+ }
205
+ };
206
+ const supabaseFor = (auth, headers = {}) => supabaseAt(env().url, auth, headers);
207
+ const dbFor = (auth, context = {}) => sb.connect(supabaseFor(auth), {
208
+ ...authContext(auth),
209
+ ...context
210
+ });
211
+ const sqlFor = (claims, context, stats) => {
212
+ if (!options.postgres) throw new TypeError("Direct Postgres access needs createServer(sb, { postgres: createPostgres() })");
213
+ return sb.connect(postgresExecutor(options.postgres.asUser(claims)), context, stats ? { stats } : {});
214
+ };
215
+ /** Clients and repositories are built on first access: most scopes use one of them. */
216
+ const contextFor = (resolution, headers, parent, until = 0) => {
217
+ const { auth } = resolution;
218
+ const context = authContext(auth);
219
+ const recorder = new StatsRecorder(parent);
220
+ let supabase;
221
+ let db;
222
+ let sql;
223
+ const sqlClaims = auth.kind === "user" ? auth.claims : auth.kind === "anon" ? { role: "anon" } : void 0;
224
+ const client = () => supabase ??= supabaseFor(auth, headers);
225
+ let replica;
226
+ const replicaUrl = () => {
227
+ const url = readUrl();
228
+ if (url) replica ??= replicaState(until);
229
+ return url;
230
+ };
231
+ return {
232
+ auth,
233
+ resolution,
234
+ get supabase() {
235
+ return client();
236
+ },
237
+ get db() {
238
+ if (db) return db;
239
+ const url = replicaUrl();
240
+ db = sb.connect(client(), context, {
241
+ stats: recorder,
242
+ ...url && replica ? { executor: routedExecutor(postgrestExecutor(client()), postgrestExecutor(supabaseAt(url, auth, headers)), replica) } : {}
243
+ });
244
+ return db;
245
+ },
246
+ get replica() {
247
+ replicaUrl();
248
+ return replica;
249
+ },
250
+ get sql() {
251
+ if (!options.postgres || !sqlClaims) return void 0;
252
+ sql ??= sqlFor(sqlClaims, context, recorder);
253
+ return sql;
254
+ },
255
+ stats: () => recorder.snapshot()
256
+ };
257
+ };
258
+ const resolve = async (request, resolveOptions = {}) => {
259
+ const claims = options.auth?.claims ?? sb.claimsSchema;
260
+ const userMetadata = sb.userMetadataSchema;
261
+ const resolution = await resolveAuth(request, {
262
+ logger: sb.events.logger,
263
+ ...options.auth,
264
+ ...claims ? { claims } : {},
265
+ ...userMetadata ? { userMetadata } : {},
266
+ env: env(),
267
+ ...resolveOptions.refresh !== void 0 ? { refresh: resolveOptions.refresh } : {},
268
+ onRefresh: (event) => {
269
+ options.auth?.onRefresh?.(event);
270
+ sb.events.emit("refresh", event);
271
+ }
272
+ });
273
+ if (sb.events.has("auth")) {
274
+ const { auth } = resolution;
275
+ sb.events.emit("auth", {
276
+ source: auth.kind === "user" ? auth.source === "cookie" ? "cookie" : "bearer" : auth.kind === "service" ? "bearer" : "none",
277
+ ok: auth.kind !== "invalid",
278
+ ...auth.kind === "user" ? { userId: auth.user.id } : {}
279
+ });
280
+ }
281
+ return resolution;
282
+ };
283
+ return {
284
+ get env() {
285
+ return env();
286
+ },
287
+ resolve,
288
+ supabaseFor,
289
+ dbFor,
290
+ contextFor: (auth, contextOptions = {}) => contextFor({
291
+ auth,
292
+ cookies: [],
293
+ headers: {},
294
+ requestCookies: [],
295
+ apply: (response) => response
296
+ }, {}, contextOptions.stats, contextOptions.pinnedUntil),
297
+ async context(request, contextOptions = {}) {
298
+ const resolution = await resolve(request, { refresh: contextOptions.refresh ?? false });
299
+ return contextFor(resolution, options.headers?.(request) ?? {}, contextOptions.stats, contextOptions.pinnedUntil ?? pinnedUntil(request.headers.get("cookie")));
300
+ },
301
+ admin: (context = {}) => sb.connect(serviceClient(), {
302
+ actor: {
303
+ id: "service",
304
+ kind: "service",
305
+ role: "service_role"
306
+ },
307
+ claims: { role: "service_role" },
308
+ ...context
309
+ }),
310
+ deleteAccount: (userId, deleteOptions) => deleteAccount(sb, serviceClient, userId, deleteOptions),
311
+ actingAs: (userId, claims = {}, impersonation) => {
312
+ const full = {
313
+ role: "authenticated",
314
+ ...claims,
315
+ sub: userId,
316
+ ...impersonation ? { act: actClaim(impersonation) } : {}
317
+ };
318
+ return sqlFor(full, {
319
+ actor: {
320
+ id: userId,
321
+ kind: "user",
322
+ role: full.role ?? "authenticated",
323
+ ...impersonation ? { impersonator: impersonation.actor } : {}
324
+ },
325
+ claims: full
326
+ });
327
+ }
328
+ };
329
+ }
330
+ //#endregion
331
+ //#region src/server/respond.ts
332
+ /** `undefined` when `auth` may pass; otherwise the 401/403 error to send. */
333
+ function guard(auth, allow = ["user"], aal = "aal1") {
334
+ if (auth.kind === "invalid") return auth.error;
335
+ if (allow.includes(auth.kind)) return checkAal(auth, aal);
336
+ return auth.kind === "anon" ? dbError("unauthorized", "Sign in to continue", { code: "MISSING_CREDENTIALS" }) : dbError("forbidden", `A ${auth.kind} caller is not allowed here`);
337
+ }
338
+ function isResult(value) {
339
+ return typeof value === "object" && value !== null && "ok" in value && "data" in value && "error" in value;
340
+ }
341
+ /**
342
+ * Settles a handler's return value: `Result`s and `AsyncResult`s unwrap,
343
+ * thrown `DbException`s (or errors whose `cause` is a `DbError`) become
344
+ * errors, anything else is data. Other thrown
345
+ * errors propagate.
346
+ */
347
+ async function settle(run) {
348
+ try {
349
+ const value = await run();
350
+ if (!isResult(value)) return {
351
+ ok: true,
352
+ data: value
353
+ };
354
+ if (value.ok) return {
355
+ ok: true,
356
+ data: value.data
357
+ };
358
+ return {
359
+ ok: false,
360
+ error: isDbError(value.error) ? value.error : toDbError(value.error)
361
+ };
362
+ } catch (cause) {
363
+ const thrown = dbErrorOf(cause);
364
+ if (thrown) return {
365
+ ok: false,
366
+ error: thrown
367
+ };
368
+ throw cause;
369
+ }
370
+ }
371
+ /** A handler's value as a response: `Response` as is, data as JSON, errors as Problem Details. */
372
+ async function respond(run, options = {}) {
373
+ const settled = await settle(run);
374
+ if (!settled.ok) return problemResponse(settled.error, options);
375
+ if (settled.data instanceof Response) return settled.data;
376
+ if (settled.data === void 0) return new Response(null, { status: 204 });
377
+ return Response.json(settled.data, { status: options.status ?? 200 });
378
+ }
379
+ function defaultExpose() {
380
+ return typeof process !== "undefined" && process.env["NODE_ENV"] === "development";
381
+ }
382
+ //#endregion
383
+ export { settle as a, withExtra as c, primaryCookie as d, respond as i, DEFAULT_PIN_MS as l, guard as n, createServer as o, isResult as r, extendServer as s, defaultExpose as t, PRIMARY_COOKIE as u };