better-supabase 0.0.0 → 0.1.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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -1
  3. package/bin/better-supabase.js +3 -0
  4. package/dist/casing-da0uRTqt.js +14 -0
  5. package/dist/cli/bin.d.ts +1 -0
  6. package/dist/cli/bin.js +15 -0
  7. package/dist/cli/index.d.ts +226 -0
  8. package/dist/cli/index.js +3 -0
  9. package/dist/cli-CoByWYja.js +6472 -0
  10. package/dist/client/index.d.ts +2 -0
  11. package/dist/client/index.js +112 -0
  12. package/dist/compiler-DL18ukPe.d.ts +14 -0
  13. package/dist/config/index.d.ts +3 -0
  14. package/dist/config/index.js +3 -0
  15. package/dist/config-D-aR2ZoN.js +276 -0
  16. package/dist/define-CTfWw-SS.d.ts +662 -0
  17. package/dist/define-QGkq04oe.js +1068 -0
  18. package/dist/edge/index.d.ts +43 -0
  19. package/dist/edge/index.js +103 -0
  20. package/dist/entitlements-BBg61DQZ.d.ts +20 -0
  21. package/dist/entitlements-BC_khwRy.js +17 -0
  22. package/dist/env/index.d.ts +2 -0
  23. package/dist/env/index.js +2 -0
  24. package/dist/env-BEToab0I.js +204 -0
  25. package/dist/errors-Bl3rkOzo.js +196 -0
  26. package/dist/events/index.d.ts +2 -0
  27. package/dist/events/index.js +2 -0
  28. package/dist/events-2OJsVG_d.d.ts +84 -0
  29. package/dist/events-BuzTgn2D.js +135 -0
  30. package/dist/executor-Bazhgsgq.d.ts +195 -0
  31. package/dist/executor-Bje4suQF.d.ts +25 -0
  32. package/dist/executor-Ceq60hPM.js +89 -0
  33. package/dist/hono/index.d.ts +45 -0
  34. package/dist/hono/index.js +70 -0
  35. package/dist/index-BD6wKthp.d.ts +597 -0
  36. package/dist/index-DQVvHzjW.d.ts +74 -0
  37. package/dist/index-DRid0qIv.d.ts +76 -0
  38. package/dist/index-Dup-nSe9.d.ts +117 -0
  39. package/dist/index-Dy6kuF0P.d.ts +71 -0
  40. package/dist/index-Hf8PP9sP.d.ts +116 -0
  41. package/dist/index-JXHBuS53.d.ts +144 -0
  42. package/dist/index-Nxo3TJcm.d.ts +208 -0
  43. package/dist/index-Sw43J8HG.d.ts +62 -0
  44. package/dist/index-cEeweTeM.d.ts +38 -0
  45. package/dist/index.d.ts +110 -0
  46. package/dist/index.js +42 -0
  47. package/dist/invalidate-DbxuL2jL.js +18 -0
  48. package/dist/jobs/index.d.ts +173 -0
  49. package/dist/jobs/index.js +477 -0
  50. package/dist/json-schema-BbwO2mZg.js +181 -0
  51. package/dist/lint/index.d.ts +75 -0
  52. package/dist/lint/index.js +167 -0
  53. package/dist/list/index.d.ts +2 -0
  54. package/dist/list/index.js +410 -0
  55. package/dist/live-6b9i72Ux.d.ts +180 -0
  56. package/dist/live-D8rV1Ldw.js +168 -0
  57. package/dist/mcp/index.d.ts +105 -0
  58. package/dist/mcp/index.js +330 -0
  59. package/dist/mfa-CpogP66z.js +521 -0
  60. package/dist/mfa-TVia554c.d.ts +44 -0
  61. package/dist/next/image/index.d.ts +34 -0
  62. package/dist/next/image/index.js +40 -0
  63. package/dist/next/index.d.ts +210 -0
  64. package/dist/next/index.js +426 -0
  65. package/dist/openapi/index.d.ts +2 -0
  66. package/dist/openapi/index.js +332 -0
  67. package/dist/orpc/index.d.ts +46 -0
  68. package/dist/orpc/index.js +50 -0
  69. package/dist/otel/index.d.ts +40 -0
  70. package/dist/otel/index.js +168 -0
  71. package/dist/path-Cv__-OTj.d.ts +17 -0
  72. package/dist/plugin-BqR6wKMB.js +23 -0
  73. package/dist/plugin-HvGqCurB.d.ts +118 -0
  74. package/dist/plugins/actor/index.d.ts +13 -0
  75. package/dist/plugins/actor/index.js +43 -0
  76. package/dist/plugins/rules/index.d.ts +88 -0
  77. package/dist/plugins/rules/index.js +174 -0
  78. package/dist/plugins/soft-delete/index.d.ts +38 -0
  79. package/dist/plugins/soft-delete/index.js +69 -0
  80. package/dist/plugins/tenant/index.d.ts +43 -0
  81. package/dist/plugins/tenant/index.js +73 -0
  82. package/dist/plugins/timestamps/index.d.ts +9 -0
  83. package/dist/plugins/timestamps/index.js +37 -0
  84. package/dist/plugins/validation/index.d.ts +24 -0
  85. package/dist/plugins/validation/index.js +59 -0
  86. package/dist/pool-BFeqzwQS.d.ts +37 -0
  87. package/dist/postgres/index.d.ts +28 -0
  88. package/dist/postgres/index.js +79 -0
  89. package/dist/postgrest-Dm-vhY6R.d.ts +43 -0
  90. package/dist/problem-DPu6Fh-t.js +97 -0
  91. package/dist/problem-DlgB0lYl.d.ts +47 -0
  92. package/dist/query/index.d.ts +2 -0
  93. package/dist/query/index.js +3 -0
  94. package/dist/query-ChvDbsDE.js +186 -0
  95. package/dist/react/index.d.ts +7 -0
  96. package/dist/react/index.js +5 -0
  97. package/dist/react/server.d.ts +13 -0
  98. package/dist/react/server.js +25 -0
  99. package/dist/react/session.d.ts +2 -0
  100. package/dist/react/session.js +3 -0
  101. package/dist/react-CrOvrnIP.js +241 -0
  102. package/dist/read-set-DhX2d04c.js +216 -0
  103. package/dist/realtime/index.d.ts +3 -0
  104. package/dist/realtime/index.js +253 -0
  105. package/dist/resolve-DW8h9NPm.d.ts +155 -0
  106. package/dist/resource-CYiu6Ygi.js +175 -0
  107. package/dist/resource-SkPTRJDD.d.ts +65 -0
  108. package/dist/respond-4-dhoV7x.js +380 -0
  109. package/dist/respond-PoTpy5DT.d.ts +169 -0
  110. package/dist/result-CTkCZ6JA.d.ts +198 -0
  111. package/dist/result-DWXatkd6.js +103 -0
  112. package/dist/scope-CDHcS7dE.js +90 -0
  113. package/dist/seed-CzecODcJ.js +101 -0
  114. package/dist/server/index.d.ts +42 -0
  115. package/dist/server/index.js +68 -0
  116. package/dist/session-C3osL21k.d.ts +21 -0
  117. package/dist/session-CD-3orO_.js +19 -0
  118. package/dist/shared-_6-uPL7E.js +57 -0
  119. package/dist/simplify-oHiDKRVQ.js +665 -0
  120. package/dist/spec-pins-CJPVghHy.d.ts +20 -0
  121. package/dist/spec-pins-t6sPaHvl.js +20 -0
  122. package/dist/sql-M8l3fnS0.js +260 -0
  123. package/dist/ssr/index.d.ts +5 -0
  124. package/dist/ssr/index.js +4 -0
  125. package/dist/standard-CyhRzctb.d.ts +7 -0
  126. package/dist/standard-FUi3Z3-7.js +21 -0
  127. package/dist/stats-3iy8u3_P.js +566 -0
  128. package/dist/storage/index.d.ts +4 -0
  129. package/dist/storage/index.js +2 -0
  130. package/dist/storage-BWXVUXGk.js +461 -0
  131. package/dist/tables-rZxRxNPW.js +59 -0
  132. package/dist/template-BeOGnynM.js +93 -0
  133. package/dist/template-mbvlysOo.d.ts +6 -0
  134. package/dist/testing/index.d.ts +294 -0
  135. package/dist/testing/index.js +751 -0
  136. package/dist/types-6cYK9pbJ.js +29 -0
  137. package/dist/types-C2fslP1z.d.ts +195 -0
  138. package/dist/version-Cqq1m3UY.js +4 -0
  139. package/dist/view-B0ltUmTi.d.ts +43 -0
  140. package/dist/view-B6F9QbG-.js +35 -0
  141. package/dist/webhooks/index.d.ts +152 -0
  142. package/dist/webhooks/index.js +2 -0
  143. package/dist/webhooks-DbMTJJX2.js +139 -0
  144. package/package.json +246 -17
  145. package/schemas/config-v1.json +328 -0
  146. package/schemas/doctor-report-v1.json +105 -0
  147. package/schemas/snapshot-v2.json +1002 -0
  148. package/skills/better-supabase/SKILL.md +84 -0
  149. package/skills/better-supabase/references/plugins.md +56 -0
  150. package/skills/better-supabase/references/troubleshooting.md +39 -0
  151. package/skills/better-supabase-api/SKILL.md +67 -0
  152. package/skills/better-supabase-api/references/adapters.md +108 -0
  153. package/skills/better-supabase-testing/SKILL.md +65 -0
  154. package/index.js +0 -1
@@ -0,0 +1,521 @@
1
+ import { n as dbError } from "./errors-Bl3rkOzo.js";
2
+ import { DEFAULT_COOKIE_OPTIONS, createChunks, isChunkLike, parseCookieHeader, serializeCookieHeader, stringFromBase64URL, stringToBase64URL } from "@supabase/ssr";
3
+ import { extractCredentials, verifyCredentials } from "@supabase/server/core";
4
+ //#region src/auth/impersonation.ts
5
+ /** The `act` claim for an impersonated session. */
6
+ function actClaim(options) {
7
+ return {
8
+ sub: options.actor,
9
+ reason: options.reason
10
+ };
11
+ }
12
+ /** The impersonator in `claims.act`, or `undefined` for a normal session. */
13
+ function impersonatorOf(claims) {
14
+ const act = claims["act"];
15
+ if (typeof act !== "object" || act === null) return void 0;
16
+ const { sub, reason } = act;
17
+ if (typeof sub !== "string" || sub === "") return void 0;
18
+ return typeof reason === "string" ? {
19
+ id: sub,
20
+ reason
21
+ } : { id: sub };
22
+ }
23
+ //#endregion
24
+ //#region src/auth/refresh.ts
25
+ /**
26
+ * How long a finished refresh is reused for requests still carrying the old
27
+ * refresh token (parallel page loads, prefetches). Matches the default
28
+ * refresh-token reuse interval of Supabase Auth.
29
+ */
30
+ const REUSE_MS = 1e4;
31
+ const inflight = /* @__PURE__ */ new Map();
32
+ const recent = /* @__PURE__ */ new Map();
33
+ function shared(outcome) {
34
+ return outcome.ok ? {
35
+ ...outcome,
36
+ shared: true
37
+ } : outcome;
38
+ }
39
+ function prune(now) {
40
+ for (const [token, entry] of recent) if (entry.until <= now) recent.delete(token);
41
+ }
42
+ async function request(refreshToken, options, now) {
43
+ const doFetch = options.fetch ?? fetch;
44
+ let response;
45
+ try {
46
+ response = await doFetch(`${options.url}/auth/v1/token?grant_type=refresh_token`, {
47
+ method: "POST",
48
+ headers: {
49
+ apikey: options.forwardedFor?.secretKey ?? options.publishableKey,
50
+ "content-type": "application/json",
51
+ ...options.forwardedFor ? { "sb-forwarded-for": options.forwardedFor.ip } : {}
52
+ },
53
+ body: JSON.stringify({ refresh_token: refreshToken })
54
+ });
55
+ } catch (cause) {
56
+ return {
57
+ ok: false,
58
+ reason: "network",
59
+ message: cause instanceof Error ? cause.message : "fetch failed"
60
+ };
61
+ }
62
+ const body = await response.json().catch(() => null);
63
+ if (!response.ok || !body || typeof body["access_token"] !== "string" || typeof body["refresh_token"] !== "string") {
64
+ const message = typeof body?.["msg"] === "string" ? body["msg"] : typeof body?.["error_description"] === "string" ? body["error_description"] : `Refresh failed with ${String(response.status)}`;
65
+ return {
66
+ ok: false,
67
+ reason: response.status >= 500 || response.status === 429 ? "network" : "rejected",
68
+ status: response.status,
69
+ message
70
+ };
71
+ }
72
+ const expiresIn = typeof body["expires_in"] === "number" ? body["expires_in"] : void 0;
73
+ return {
74
+ ok: true,
75
+ session: {
76
+ ...body,
77
+ access_token: body["access_token"],
78
+ refresh_token: body["refresh_token"],
79
+ ...typeof body["expires_at"] !== "number" && expiresIn !== void 0 ? { expires_at: Math.floor(now / 1e3) + expiresIn } : {}
80
+ }
81
+ };
82
+ }
83
+ /**
84
+ * Exchanges a refresh token for a new session. Concurrent calls with the same
85
+ * token share one request, and a successful result is reused for a short
86
+ * window so parallel requests carrying the old cookie converge on one session.
87
+ */
88
+ function refreshSession(refreshToken, options) {
89
+ const now = (options.now ?? Date.now)();
90
+ prune(now);
91
+ const cached = recent.get(refreshToken);
92
+ if (cached) return Promise.resolve(shared(cached.outcome));
93
+ const pending = inflight.get(refreshToken);
94
+ if (pending) return pending.then(shared);
95
+ const promise = request(refreshToken, options, now).then((outcome) => {
96
+ inflight.delete(refreshToken);
97
+ if (outcome.ok) recent.set(refreshToken, {
98
+ outcome,
99
+ until: now + REUSE_MS
100
+ });
101
+ return outcome;
102
+ });
103
+ inflight.set(refreshToken, promise);
104
+ return promise;
105
+ }
106
+ //#endregion
107
+ //#region src/auth/session.ts
108
+ const BASE64_PREFIX = "base64-";
109
+ /** Headers that keep responses carrying session cookies out of shared caches. */
110
+ const AUTH_CACHE_HEADERS = {
111
+ "Cache-Control": "private, no-cache, no-store, must-revalidate, max-age=0",
112
+ Expires: "0",
113
+ Pragma: "no-cache"
114
+ };
115
+ /** `sb-<first label of the hostname>-auth-token`, the supabase-js default. */
116
+ function sessionCookieName(url) {
117
+ return `sb-${new URL(url).hostname.split(".")[0] ?? ""}-auth-token`;
118
+ }
119
+ function parseCookies(header) {
120
+ return header ? parseCookieHeader(header) : [];
121
+ }
122
+ function serializeCookie(write) {
123
+ return serializeCookieHeader(write.name, write.value, write.options);
124
+ }
125
+ function combine(cookies, name) {
126
+ const byName = new Map(cookies.map((cookie) => [cookie.name, cookie.value]));
127
+ const whole = byName.get(name);
128
+ if (whole) return whole;
129
+ const parts = [];
130
+ for (let index = 0;; index++) {
131
+ const part = byName.get(`${name}.${index}`);
132
+ if (!part) break;
133
+ parts.push(part);
134
+ }
135
+ return parts.length > 0 ? parts.join("") : void 0;
136
+ }
137
+ function isStoredSession(value) {
138
+ if (typeof value !== "object" || value === null) return false;
139
+ const session = value;
140
+ return typeof session["access_token"] === "string" && typeof session["refresh_token"] === "string";
141
+ }
142
+ /**
143
+ * Reads the session written by `@supabase/ssr` (plain or `base64-`, whole or
144
+ * chunked). Mismatched chunks from a partial write read as no session.
145
+ */
146
+ function readSession(cookies, name) {
147
+ const raw = combine(cookies, name);
148
+ if (!raw) return null;
149
+ try {
150
+ const json = raw.startsWith(BASE64_PREFIX) ? stringFromBase64URL(raw.slice(7)) : raw;
151
+ const parsed = JSON.parse(json);
152
+ return isStoredSession(parsed) ? parsed : null;
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+ /**
158
+ * Cookie writes that store `session` (or clear it with `null`) exactly like
159
+ * `@supabase/ssr`, and expire chunks the new value no longer uses.
160
+ */
161
+ function writeSession(existing, name, session, options = {}) {
162
+ const base = {
163
+ ...DEFAULT_COOKIE_OPTIONS,
164
+ ...options
165
+ };
166
+ const chunks = session ? createChunks(name, BASE64_PREFIX + stringToBase64URL(JSON.stringify(session))) : [];
167
+ const kept = new Set(chunks.map((chunk) => chunk.name));
168
+ return [...existing.filter((cookie) => isChunkLike(cookie.name, name) && !kept.has(cookie.name)).map((cookie) => ({
169
+ name: cookie.name,
170
+ value: "",
171
+ options: {
172
+ ...base,
173
+ maxAge: 0
174
+ }
175
+ })), ...chunks.map((chunk) => ({
176
+ name: chunk.name,
177
+ value: chunk.value,
178
+ options: base
179
+ }))];
180
+ }
181
+ /** Applies cookie writes to a cookie list, as the next request would see it. */
182
+ function applyCookieWrites(cookies, writes) {
183
+ const next = new Map(cookies.map((cookie) => [cookie.name, cookie.value]));
184
+ for (const write of writes) if (write.options.maxAge === 0) next.delete(write.name);
185
+ else next.set(write.name, write.value);
186
+ return [...next].map(([name, value]) => ({
187
+ name,
188
+ value
189
+ }));
190
+ }
191
+ //#endregion
192
+ //#region src/auth/resolve.ts
193
+ function decodePayload(token) {
194
+ const part = token.split(".")[1];
195
+ if (!part) return void 0;
196
+ try {
197
+ const json = atob(part.replace(/-/g, "+").replace(/_/g, "/"));
198
+ const parsed = JSON.parse(json);
199
+ return typeof parsed === "object" && parsed !== null ? parsed : void 0;
200
+ } catch {
201
+ return;
202
+ }
203
+ }
204
+ function expiryOf(session) {
205
+ if (typeof session.expires_at === "number") return session.expires_at;
206
+ const exp = decodePayload(session.access_token)?.["exp"];
207
+ return typeof exp === "number" ? exp : void 0;
208
+ }
209
+ function serverEnv(options) {
210
+ const { env } = options;
211
+ return {
212
+ url: env.url,
213
+ publishableKeys: { default: env.publishableKey },
214
+ secretKeys: env.secretKeys ?? (env.secretKey ? { default: env.secretKey } : {}),
215
+ jwks: options.jwks ?? env.jwksUrl
216
+ };
217
+ }
218
+ const MEMO_SIZE = 256;
219
+ const memoByUrl = /* @__PURE__ */ new Map();
220
+ let memoByJwks = /* @__PURE__ */ new WeakMap();
221
+ /**
222
+ * Tokens that already verified, until they expire. Every private-cache scope
223
+ * and island resolves the same token again; this skips the signature check.
224
+ */
225
+ function memoFor(options) {
226
+ const jwks = options.jwks;
227
+ if (jwks && typeof jwks === "object" && !(jwks instanceof URL)) {
228
+ let memo = memoByJwks.get(jwks);
229
+ if (!memo) memoByJwks.set(jwks, memo = /* @__PURE__ */ new Map());
230
+ return memo;
231
+ }
232
+ const key = JSON.stringify([
233
+ String(jwks ?? options.env.jwksUrl),
234
+ options.env.url,
235
+ options.audience ?? null,
236
+ options.issuer ?? null
237
+ ]);
238
+ let memo = memoByUrl.get(key);
239
+ if (!memo) memoByUrl.set(key, memo = /* @__PURE__ */ new Map());
240
+ return memo;
241
+ }
242
+ function remembered(memo, token, now) {
243
+ const hit = memo.get(token);
244
+ if (!hit) return void 0;
245
+ memo.delete(token);
246
+ if (hit.expiresAt === null || hit.expiresAt <= now) return void 0;
247
+ memo.set(token, hit);
248
+ return hit;
249
+ }
250
+ function remember(memo, token, user) {
251
+ if (user.expiresAt === null) return;
252
+ memo.set(token, user);
253
+ if (memo.size > MEMO_SIZE) memo.delete(memo.keys().next().value);
254
+ }
255
+ async function verify(credentials, modes, options, source) {
256
+ const token = modes.length === 1 && modes[0] === "user" && credentials.token;
257
+ const memo = token ? memoFor(options) : void 0;
258
+ if (token && memo) {
259
+ const hit = remembered(memo, token, Math.floor((options.now ?? Date.now)() / 1e3));
260
+ if (hit) return checkClaims({
261
+ ...hit,
262
+ source
263
+ }, options);
264
+ }
265
+ const state = await verifyOnce(credentials, modes, options, source);
266
+ if (token && memo && state.kind === "user") remember(memo, token, {
267
+ kind: "user",
268
+ token: state.token,
269
+ claims: state.claims,
270
+ user: state.user,
271
+ expiresAt: state.expiresAt
272
+ });
273
+ return checkClaims(state, options);
274
+ }
275
+ /** Validates a user's claims against `options.claims`, keeping every JWT claim. */
276
+ async function checkClaims(state, options) {
277
+ if (state.kind !== "user" || !options.claims) return state;
278
+ let outcome = options.claims["~standard"].validate(state.claims);
279
+ if (outcome instanceof Promise) outcome = await outcome;
280
+ if (outcome.issues) {
281
+ const issues = outcome.issues.map((issue) => {
282
+ const path = issue.path?.map((segment) => typeof segment === "object" ? String(segment.key) : String(segment)).join(".");
283
+ return path ? `${path}: ${issue.message}` : issue.message;
284
+ }).join("; ");
285
+ return {
286
+ kind: "invalid",
287
+ reason: "claims",
288
+ error: dbError("unauthorized", `The token claims are invalid (${issues})`, { code: "CLAIMS_INVALID" })
289
+ };
290
+ }
291
+ const extra = outcome.value;
292
+ return typeof extra === "object" && extra !== null ? {
293
+ ...state,
294
+ claims: {
295
+ ...state.claims,
296
+ ...extra
297
+ }
298
+ } : state;
299
+ }
300
+ async function verifyOnce(credentials, modes, options, source) {
301
+ const { data, error } = await verifyCredentials(credentials, {
302
+ auth: modes,
303
+ env: serverEnv(options),
304
+ ...options.audience !== void 0 ? { audience: options.audience } : {},
305
+ ...options.issuer !== void 0 ? { issuer: options.issuer } : {}
306
+ });
307
+ if (error) {
308
+ const kind = error.code === "JWKS_FETCH_FAILED" ? "network" : error.status >= 500 ? "unexpected" : "unauthorized";
309
+ return {
310
+ kind: "invalid",
311
+ reason: "token",
312
+ error: dbError(kind, error.message, { code: error.code })
313
+ };
314
+ }
315
+ if (data.authMode === "secret") return {
316
+ kind: "service",
317
+ keyName: data.keyName ?? "default"
318
+ };
319
+ if (data.authMode === "user" && data.token && data.jwtClaims && data.userClaims) return {
320
+ kind: "user",
321
+ token: data.token,
322
+ claims: data.jwtClaims,
323
+ user: data.userClaims,
324
+ source,
325
+ expiresAt: typeof data.jwtClaims.exp === "number" ? data.jwtClaims.exp : null
326
+ };
327
+ return {
328
+ kind: "anon",
329
+ reason: "none"
330
+ };
331
+ }
332
+ function resolution(auth, cookies, writes = []) {
333
+ const headers = writes.length > 0 ? AUTH_CACHE_HEADERS : {};
334
+ return {
335
+ auth,
336
+ cookies: writes,
337
+ headers,
338
+ requestCookies: writes.length > 0 ? applyCookieWrites(cookies, writes) : cookies,
339
+ apply(response) {
340
+ if (writes.length === 0) return response;
341
+ let target = response;
342
+ try {
343
+ for (const write of writes) target.headers.append("set-cookie", serializeCookie(write));
344
+ } catch {
345
+ target = new Response(response.body, response);
346
+ for (const write of writes) target.headers.append("set-cookie", serializeCookie(write));
347
+ }
348
+ for (const [name, value] of Object.entries(headers)) target.headers.set(name, value);
349
+ return target;
350
+ }
351
+ };
352
+ }
353
+ const IP = /^[0-9a-f:.]+$/i;
354
+ /** The first `x-forwarded-for` hop, the client as your edge saw it. */
355
+ function clientIp(request) {
356
+ const first = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
357
+ return first && IP.test(first) ? first : void 0;
358
+ }
359
+ /**
360
+ * Resolves who is calling: custom resolvers, then `Authorization: Bearer`
361
+ * (APIs, MCP, mobile), then the `@supabase/ssr` session cookie (browsers).
362
+ *
363
+ * A valid token costs no network call: it is verified locally against the
364
+ * cached JWKS. An expiring cookie session is refreshed only when `refresh` is
365
+ * set, through a single-flight request, and only then are cookies written.
366
+ */
367
+ async function resolveAuth(request, options) {
368
+ const cookies = parseCookies(request.headers.get("cookie"));
369
+ for (const resolver of options.resolvers ?? []) {
370
+ const state = await resolver.resolve(request);
371
+ if (!state) continue;
372
+ return resolution(state.kind === "invalid" ? {
373
+ kind: "invalid",
374
+ reason: state.reason ?? "token",
375
+ error: state.error
376
+ } : await checkClaims(state, options), cookies);
377
+ }
378
+ const credentials = extractCredentials(request);
379
+ if (credentials.token) return resolution(await verify({
380
+ token: credentials.token,
381
+ apikey: null
382
+ }, ["user"], options, "bearer"), cookies);
383
+ if (options.secret && credentials.apikey?.startsWith("sb_secret_")) return resolution(await verify({
384
+ token: null,
385
+ apikey: credentials.apikey
386
+ }, options.secret === true ? ["secret"] : options.secret.map((name) => `secret:${name}`), options, "bearer"), cookies);
387
+ const name = options.cookie?.name ?? sessionCookieName(options.env.url);
388
+ const session = readSession(cookies, name);
389
+ if (!session) {
390
+ const stale = cookies.some((cookie) => cookie.name === name || cookie.name.startsWith(`${name}.`));
391
+ const writes = stale && options.refresh ? writeSession(cookies, name, null, options.cookie?.options) : [];
392
+ return resolution({
393
+ kind: "anon",
394
+ reason: stale ? "signed_out" : "none"
395
+ }, cookies, writes);
396
+ }
397
+ const now = Math.floor((options.now ?? Date.now)() / 1e3);
398
+ const expiry = expiryOf(session);
399
+ if (expiry !== void 0 && expiry - (options.leeway ?? 60) > now) {
400
+ const state = await verify({
401
+ token: session.access_token,
402
+ apikey: null
403
+ }, ["user"], options, "cookie");
404
+ if (state.kind !== "invalid" || state.reason === "claims" || state.error.kind === "network") return resolution(state, cookies);
405
+ if (!options.refresh) return resolution({
406
+ kind: "anon",
407
+ reason: "signed_out"
408
+ }, cookies);
409
+ } else if (!options.refresh) return resolution({
410
+ kind: "anon",
411
+ reason: "expired"
412
+ }, cookies);
413
+ const secretKey = options.env.secretKey;
414
+ const ip = secretKey && options.clientIp !== false ? (options.clientIp ?? clientIp)(request) : void 0;
415
+ const started = performance.now();
416
+ const outcome = await refreshSession(session.refresh_token, {
417
+ url: options.env.url,
418
+ publishableKey: options.env.publishableKey,
419
+ ...secretKey && ip ? { forwardedFor: {
420
+ ip,
421
+ secretKey
422
+ } } : {},
423
+ ...options.fetch ? { fetch: options.fetch } : {},
424
+ ...options.now ? { now: options.now } : {}
425
+ });
426
+ options.onRefresh?.({
427
+ ok: outcome.ok,
428
+ shared: outcome.ok && outcome.shared === true,
429
+ durationMs: performance.now() - started
430
+ });
431
+ if (!outcome.ok) {
432
+ if (outcome.reason === "network") return resolution({
433
+ kind: "anon",
434
+ reason: "refresh_failed"
435
+ }, cookies);
436
+ return resolution({
437
+ kind: "anon",
438
+ reason: "signed_out"
439
+ }, cookies, writeSession(cookies, name, null, options.cookie?.options));
440
+ }
441
+ const writes = writeSession(cookies, name, outcome.session, options.cookie?.options);
442
+ return resolution(await verify({
443
+ token: outcome.session.access_token,
444
+ apikey: null
445
+ }, ["user"], options, "cookie"), cookies, writes);
446
+ }
447
+ /** The repository context (`actor`, `claims`) for an auth state. */
448
+ function authContext(auth) {
449
+ switch (auth.kind) {
450
+ case "user": {
451
+ const impersonator = impersonatorOf(auth.claims);
452
+ return {
453
+ actor: {
454
+ id: auth.user.id,
455
+ kind: "user",
456
+ ...auth.user.role !== void 0 ? { role: auth.user.role } : {},
457
+ ...auth.user.email !== void 0 ? { email: auth.user.email } : {},
458
+ ...impersonator ? { impersonator: impersonator.id } : {}
459
+ },
460
+ claims: auth.claims
461
+ };
462
+ }
463
+ case "service": return {
464
+ actor: {
465
+ id: `service:${auth.keyName}`,
466
+ kind: "service",
467
+ role: "service_role"
468
+ },
469
+ claims: { role: "service_role" }
470
+ };
471
+ case "anon":
472
+ case "invalid": return {
473
+ actor: {
474
+ id: "anon",
475
+ kind: "anon",
476
+ role: "anon"
477
+ },
478
+ claims: { role: "anon" }
479
+ };
480
+ default: return auth;
481
+ }
482
+ }
483
+ //#endregion
484
+ //#region src/auth/mfa.ts
485
+ /** The session's assurance level from its claims; anything but `aal2` reads as `aal1`. */
486
+ function aalOf(claims) {
487
+ return claims["aal"] === "aal2" ? "aal2" : "aal1";
488
+ }
489
+ /** The `amr` claim as typed entries, skipping malformed ones. */
490
+ function amrOf(claims) {
491
+ const amr = claims["amr"];
492
+ if (!Array.isArray(amr)) return [];
493
+ return amr.flatMap((entry) => {
494
+ if (typeof entry !== "object" || entry === null) return [];
495
+ const { method, timestamp, provider } = entry;
496
+ if (typeof method !== "string" || typeof timestamp !== "number") return [];
497
+ return [typeof provider === "string" ? {
498
+ method,
499
+ timestamp,
500
+ provider
501
+ } : {
502
+ method,
503
+ timestamp
504
+ }];
505
+ });
506
+ }
507
+ /**
508
+ * `undefined` when a user session meets `required`; otherwise a `forbidden`
509
+ * error with `required` and code `INSUFFICIENT_AAL`, for the client to start
510
+ * an MFA challenge. Non-user sessions pass: `allow` decides about them.
511
+ */
512
+ function checkAal(auth, required) {
513
+ if (required === "aal1" || auth.kind !== "user" || !("claims" in auth)) return void 0;
514
+ if (aalOf(auth.claims) === "aal2") return void 0;
515
+ return dbError("forbidden", "Verify a second factor to continue", {
516
+ code: "INSUFFICIENT_AAL",
517
+ required
518
+ });
519
+ }
520
+ //#endregion
521
+ export { clientIp as a, applyCookieWrites as c, serializeCookie as d, sessionCookieName as f, impersonatorOf as g, actClaim as h, authContext as i, parseCookies as l, refreshSession as m, amrOf as n, resolveAuth as o, writeSession as p, checkAal as r, AUTH_CACHE_HEADERS as s, aalOf as t, readSession as u };
@@ -0,0 +1,44 @@
1
+ import { p as DbError } from "./result-CTkCZ6JA.js";
2
+ //#region src/auth/impersonation.d.ts
3
+ /** The admin acting as the user, from the RFC 8693 `act` claim. */
4
+ interface Impersonator {
5
+ readonly id: string;
6
+ readonly reason?: string;
7
+ }
8
+ /** `actingAs(userId, claims, { actor, reason })`: who acts, and why (recorded by the audit module). */
9
+ interface ImpersonationOptions {
10
+ readonly actor: string;
11
+ readonly reason: string;
12
+ }
13
+ /** The impersonator in `claims.act`, or `undefined` for a normal session. */
14
+ declare function impersonatorOf(claims: Readonly<Record<string, unknown>>): Impersonator | undefined;
15
+ //#endregion
16
+ //#region src/auth/mfa.d.ts
17
+ /** Authenticator assurance level: `aal2` once a second factor is verified in this session. */
18
+ type Aal = "aal1" | "aal2";
19
+ /** One way the user proved who they are in this session (the JWT `amr` claim). */
20
+ interface AmrEntry {
21
+ /** `password`, `otp`, `oauth`, `totp`, `sso/saml`, `anonymous`, ... */
22
+ readonly method: string;
23
+ /** Seconds since epoch. */
24
+ readonly timestamp: number;
25
+ /** SSO provider id, for `sso/saml`. */
26
+ readonly provider?: string;
27
+ }
28
+ /** The session's assurance level from its claims; anything but `aal2` reads as `aal1`. */
29
+ declare function aalOf(claims: Readonly<Record<string, unknown>>): Aal;
30
+ /** The `amr` claim as typed entries, skipping malformed ones. */
31
+ declare function amrOf(claims: Readonly<Record<string, unknown>>): readonly AmrEntry[];
32
+ /**
33
+ * `undefined` when a user session meets `required`; otherwise a `forbidden`
34
+ * error with `required` and code `INSUFFICIENT_AAL`, for the client to start
35
+ * an MFA challenge. Non-user sessions pass: `allow` decides about them.
36
+ */
37
+ declare function checkAal(auth: {
38
+ readonly kind: "user";
39
+ readonly claims: object;
40
+ } | {
41
+ readonly kind: string;
42
+ }, required: Aal): DbError | undefined;
43
+ //#endregion
44
+ export { checkAal as a, impersonatorOf as c, amrOf as i, AmrEntry as n, ImpersonationOptions as o, aalOf as r, Impersonator as s, Aal as t };
@@ -0,0 +1,34 @@
1
+ //#region src/next/image/index.d.ts
2
+ /** What `next/image` passes to a custom loader. */
3
+ export interface ImageLoaderProps {
4
+ readonly src: string;
5
+ readonly width: number;
6
+ readonly quality?: number;
7
+ }
8
+ export type ImageLoader = (props: ImageLoaderProps) => string;
9
+ export interface ImageLoaderOptions {
10
+ /** Project URL, e.g. `process.env.NEXT_PUBLIC_SUPABASE_URL`. */
11
+ readonly url: string;
12
+ /** Resize mode for the transform. Defaults to Storage's own (`cover`). */
13
+ readonly resize?: "cover" | "contain" | "fill";
14
+ /**
15
+ * For images that are not public Storage objects (local files, other
16
+ * hosts). Defaults to returning `src` unchanged.
17
+ */
18
+ readonly fallback?: ImageLoader;
19
+ }
20
+ /**
21
+ * A `next/image` loader that serves public Storage objects through
22
+ * Supabase image transformations (`/render/image/public`), so Next.js does
23
+ * not proxy or re-encode them:
24
+ *
25
+ * ```ts
26
+ * // src/image-loader.ts, set as `images.loaderFile` in next.config.ts
27
+ * export default createImageLoader({ url: process.env.NEXT_PUBLIC_SUPABASE_URL! });
28
+ * ```
29
+ *
30
+ * Signed URLs pass through unchanged: their token covers the transform they
31
+ * were signed with, so sign them with `renderUrl()` at the size you render.
32
+ */
33
+ export declare function createImageLoader(options: ImageLoaderOptions): ImageLoader;
34
+ //#endregion
@@ -0,0 +1,40 @@
1
+ //#region src/next/image/index.ts
2
+ /** Storage's image transformation limits. */
3
+ const MAX_WIDTH = 2500;
4
+ const QUALITY = {
5
+ min: 20,
6
+ max: 100
7
+ };
8
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, Math.round(value)));
9
+ /**
10
+ * A `next/image` loader that serves public Storage objects through
11
+ * Supabase image transformations (`/render/image/public`), so Next.js does
12
+ * not proxy or re-encode them:
13
+ *
14
+ * ```ts
15
+ * // src/image-loader.ts, set as `images.loaderFile` in next.config.ts
16
+ * export default createImageLoader({ url: process.env.NEXT_PUBLIC_SUPABASE_URL! });
17
+ * ```
18
+ *
19
+ * Signed URLs pass through unchanged: their token covers the transform they
20
+ * were signed with, so sign them with `renderUrl()` at the size you render.
21
+ */
22
+ function createImageLoader(options) {
23
+ const storage = `${options.url.replace(/\/+$/, "")}/storage/v1`;
24
+ const publicPrefixes = [`${storage}/object/public/`, `${storage}/render/image/public/`];
25
+ const fallback = options.fallback ?? (({ src }) => src);
26
+ return (props) => {
27
+ const prefix = publicPrefixes.find((candidate) => props.src.startsWith(candidate));
28
+ if (prefix === void 0) return fallback(props);
29
+ const source = new URL(props.src);
30
+ const object = source.pathname.slice(new URL(prefix).pathname.length);
31
+ const target = new URL(`${storage}/render/image/public/${object}`);
32
+ for (const [key, value] of source.searchParams) target.searchParams.set(key, value);
33
+ target.searchParams.set("width", String(clamp(props.width, 1, MAX_WIDTH)));
34
+ if (props.quality !== void 0) target.searchParams.set("quality", String(clamp(props.quality, QUALITY.min, QUALITY.max)));
35
+ if (options.resize) target.searchParams.set("resize", options.resize);
36
+ return target.toString();
37
+ };
38
+ }
39
+ //#endregion
40
+ export { createImageLoader };