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,5 +1,6 @@
1
1
  import { n as dbError } from "./errors-Bl3rkOzo.js";
2
2
  import { t as consoleLogger } from "./logger-glM28NVN.js";
3
+ import { n as decodeJwtPayload } from "./base64-CQeB1eSf.js";
3
4
  import { DEFAULT_COOKIE_OPTIONS, createChunks, isChunkLike, parseCookieHeader, serializeCookieHeader, stringFromBase64URL, stringToBase64URL } from "@supabase/ssr";
4
5
  import { extractCredentials, verifyCredentials } from "@supabase/server/core";
5
6
  //#region src/auth/impersonation.ts
@@ -22,6 +23,65 @@ function impersonatorOf(claims) {
22
23
  } : { id: sub };
23
24
  }
24
25
  //#endregion
26
+ //#region src/auth/actor.ts
27
+ function isRecord(value) {
28
+ return typeof value === "object" && value !== null && !Array.isArray(value);
29
+ }
30
+ function copyAct(level) {
31
+ const entries = Object.entries(level).map(([key, value]) => [key, key === "act" && isRecord(value) ? copyAct(value) : value]);
32
+ return Object.fromEntries(entries);
33
+ }
34
+ /**
35
+ * Reads only `act` and `client_id`, the way PermDock's `actorOf` does. An
36
+ * `act` that is not a chain of objects each with a non-empty `sub` is
37
+ * `{ ok: false }`: the session must not pass as the user alone.
38
+ */
39
+ function actorOf(claims) {
40
+ const outer = claims["act"];
41
+ if (outer !== void 0) {
42
+ let level = outer;
43
+ while (level !== void 0) {
44
+ if (!isRecord(level)) return {
45
+ ok: false,
46
+ reason: "invalid-chain"
47
+ };
48
+ const sub = level["sub"];
49
+ if (typeof sub !== "string" || sub === "") return {
50
+ ok: false,
51
+ reason: "invalid-chain"
52
+ };
53
+ level = level["act"];
54
+ }
55
+ if (!isRecord(outer) || typeof outer["sub"] !== "string") return {
56
+ ok: false,
57
+ reason: "invalid-chain"
58
+ };
59
+ return {
60
+ ok: true,
61
+ actor: {
62
+ id: outer["sub"],
63
+ kind: "oauth-client",
64
+ chain: copyAct(outer)
65
+ }
66
+ };
67
+ }
68
+ const client = claims["client_id"];
69
+ if (typeof client === "string" && client !== "") return {
70
+ ok: true,
71
+ actor: {
72
+ id: client,
73
+ kind: "oauth-client"
74
+ }
75
+ };
76
+ return { ok: true };
77
+ }
78
+ /** The OAuth `scope` claim (a space-separated string or a list); `undefined` when empty. */
79
+ function delegationOf(claims) {
80
+ const scope = claims["scope"];
81
+ const scopes = typeof scope === "string" ? scope.split(/\s+/u).filter(Boolean) : Array.isArray(scope) ? scope.filter((item) => typeof item === "string") : [];
82
+ return scopes.length > 0 ? { scopes } : void 0;
83
+ }
84
+ //#endregion
25
85
  //#region src/auth/refresh.ts
26
86
  /**
27
87
  * How long a finished refresh is reused for requests still carrying the old
@@ -191,20 +251,9 @@ function applyCookieWrites(cookies, writes) {
191
251
  }
192
252
  //#endregion
193
253
  //#region src/auth/resolve.ts
194
- function decodePayload(token) {
195
- const part = token.split(".")[1];
196
- if (!part) return void 0;
197
- try {
198
- const json = atob(part.replace(/-/g, "+").replace(/_/g, "/"));
199
- const parsed = JSON.parse(json);
200
- return typeof parsed === "object" && parsed !== null ? parsed : void 0;
201
- } catch {
202
- return;
203
- }
204
- }
205
254
  function expiryOf(session) {
206
255
  if (typeof session.expires_at === "number") return session.expires_at;
207
- const exp = decodePayload(session.access_token)?.["exp"];
256
+ const exp = decodeJwtPayload(session.access_token)?.["exp"];
208
257
  return typeof exp === "number" ? exp : void 0;
209
258
  }
210
259
  function serverEnv(options) {
@@ -275,7 +324,13 @@ async function verify(credentials, modes, options, source) {
275
324
  }
276
325
  /** Validates a user's claims against `options.claims`, keeping every JWT claim. */
277
326
  async function checkClaims(state, options) {
278
- if (state.kind !== "user" || !options.claims) return state;
327
+ if (state.kind !== "user") return state;
328
+ if (!actorOf(state.claims).ok) return {
329
+ kind: "invalid",
330
+ reason: "actor",
331
+ error: dbError("unauthorized", "The token's act claim is not a chain of actors with a sub", { code: "ACTOR_INVALID" })
332
+ };
333
+ if (!options.claims) return state;
279
334
  let outcome = options.claims["~standard"].validate(state.claims);
280
335
  if (outcome instanceof Promise) outcome = await outcome;
281
336
  if (outcome.issues) {
@@ -321,8 +376,8 @@ async function verifyOnce(credentials, modes, options, source) {
321
376
  const { data, error } = await verifyCredentials(credentials, {
322
377
  auth: modes,
323
378
  env: serverEnv(options),
324
- ...options.audience !== void 0 ? { audience: options.audience } : {},
325
- ...options.issuer !== void 0 ? { issuer: options.issuer } : {}
379
+ ...options.audience === void 0 ? {} : { audience: options.audience },
380
+ ...options.issuer === void 0 ? {} : { issuer: options.issuer }
326
381
  });
327
382
  if (error) {
328
383
  const kind = error.code === "JWKS_FETCH_FAILED" ? "network" : error.status >= 500 ? "unexpected" : "unauthorized";
@@ -421,7 +476,7 @@ async function resolveAuth(request, options) {
421
476
  token: session.access_token,
422
477
  apikey: null
423
478
  }, ["user"], options, "cookie");
424
- if (state.kind !== "invalid" || state.reason === "claims" || state.error.kind === "network") return resolution(state, cookies);
479
+ if (state.kind !== "invalid" || state.reason !== "token" || state.error.kind === "network") return resolution(state, cookies);
425
480
  if (!options.refresh) return resolution({
426
481
  kind: "anon",
427
482
  reason: "signed_out"
@@ -473,8 +528,8 @@ function authContext(auth) {
473
528
  actor: {
474
529
  id: auth.user.id,
475
530
  kind: "user",
476
- ...auth.user.role !== void 0 ? { role: auth.user.role } : {},
477
- ...auth.user.email !== void 0 ? { email: auth.user.email } : {},
531
+ ...auth.user.role === void 0 ? {} : { role: auth.user.role },
532
+ ...auth.user.email === void 0 ? {} : { email: auth.user.email },
478
533
  ...impersonator ? { impersonator: impersonator.id } : {}
479
534
  },
480
535
  claims: auth.claims
@@ -538,4 +593,4 @@ function checkAal(auth, required) {
538
593
  });
539
594
  }
540
595
  //#endregion
541
- 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 };
596
+ export { actClaim as _, clientIp as a, applyCookieWrites as c, serializeCookie as d, sessionCookieName as f, delegationOf as g, actorOf 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, impersonatorOf as v };
@@ -1,4 +1,4 @@
1
- import { p as DbError } from "./result-CTkCZ6JA.js";
1
+ import { p as DbError } from "./result-c2OYgCV2.js";
2
2
  //#region src/auth/impersonation.d.ts
3
3
  /** The admin acting as the user, from the RFC 8693 `act` claim. */
4
4
  interface Impersonator {
@@ -1,12 +1,12 @@
1
- import { n as AnyModels, t as AnyFunctions } from "../types-C83v5SQz.js";
2
- import { p as DbError } from "../result-CTkCZ6JA.js";
3
- import { B as ReadSet, P as DbStats, Z as QuerySpec, l as CacheAdapter, t as BetterSupabase } from "../define-e3zI4_4g.js";
4
- import { n as AmrEntry, s as Impersonator, t as Aal } from "../mfa-TVia554c.js";
5
- import { r as AuthState } from "../resolve-CjHILk2w.js";
6
- import { d as ServerContext, f as ServerOptions, l as BetterServer, n as GuardOptions, t as AuthKind } from "../respond-Dne1ARSa.js";
7
- import { n as toSession, t as AuthSession } from "../view-D1s0wa0T.js";
8
- import { r as LiveCountSeed, t as CountRunner } from "../live-DJyZMD6w.js";
9
- import { n as MembershipClaim, r as hasEntitlement, t as EntitlementKey } from "../entitlements-Dng-ctuT.js";
1
+ import { n as AnyModels, t as AnyFunctions } from "../types-DONv9SHN.js";
2
+ import { p as DbError } from "../result-c2OYgCV2.js";
3
+ import { B as ReadSet, P as DbStats, Z as QuerySpec, l as CacheAdapter, t as BetterSupabase } from "../define-DUbydmJa.js";
4
+ import { n as AmrEntry, s as Impersonator, t as Aal } from "../mfa-x3V3vGh-.js";
5
+ import { r as AuthState } from "../resolve-BRciX2jK.js";
6
+ import { d as ServerContext, f as ServerOptions, l as BetterServer, n as GuardOptions, t as AuthKind } from "../respond-DAkPIdTH.js";
7
+ import { n as toSession, t as AuthSession } from "../view-3Po_PVlr.js";
8
+ import { r as LiveCountSeed, t as CountRunner } from "../live-QE3UPbGm.js";
9
+ import { n as MembershipClaim, r as hasEntitlement, t as EntitlementKey } from "../entitlements-DvJXoI02.js";
10
10
  import { NextRequest } from "next/server.js";
11
11
  import { StandardSchemaV1 } from "@standard-schema/spec";
12
12
  //#region src/next/collector.d.ts
@@ -17,8 +17,8 @@ interface DbBudget {
17
17
  readonly waves?: number;
18
18
  }
19
19
  //#endregion
20
- //#region src/next/index.d.ts
21
- export interface NextOptions extends ServerOptions {
20
+ //#region src/next/create.d.ts
21
+ interface NextOptions extends ServerOptions {
22
22
  /** Invalidate `tagFor(table)` cache tags after mutations. Defaults to true. */
23
23
  readonly cacheTags?: boolean;
24
24
  /** Include internal error messages in Problem Details. Defaults to development only. */
@@ -26,7 +26,7 @@ export interface NextOptions extends ServerOptions {
26
26
  /** Per-request database call counts. See the Budget docs. */
27
27
  readonly debug?: NextDebugOptions;
28
28
  }
29
- export interface NextDebugOptions {
29
+ interface NextDebugOptions {
30
30
  /** Collect outside development too, e.g. for e2e runs against `next start`. */
31
31
  readonly enabled?: boolean;
32
32
  /** Header with `calls;waves;ms` on route handler responses. Defaults to `x-bs-db-calls`. */
@@ -38,7 +38,7 @@ export interface NextDebugOptions {
38
38
  }
39
39
  /** Response header with the URL of the request's totals on `next.debugRoute()`. */
40
40
  export declare const STATS_URL_HEADER = "x-bs-stats";
41
- export interface ProxyOptions<C = unknown, U = unknown> {
41
+ interface ProxyOptions<C = unknown, U = unknown> {
42
42
  /**
43
43
  * Another middleware to compose with, such as next-intl's. It runs on the
44
44
  * original request, alongside auth; its rewrite, redirect or request
@@ -55,7 +55,7 @@ export interface ProxyOptions<C = unknown, U = unknown> {
55
55
  */
56
56
  readonly serverTiming?: boolean;
57
57
  }
58
- export type ActionResult<T> = {
58
+ type ActionResult<T> = {
59
59
  readonly ok: true;
60
60
  readonly data: T;
61
61
  readonly error: null;
@@ -64,7 +64,7 @@ export type ActionResult<T> = {
64
64
  readonly data: null;
65
65
  readonly error: DbError;
66
66
  };
67
- export interface ActionOptions<S extends StandardSchemaV1 | undefined> extends GuardOptions {
67
+ interface ActionOptions<S extends StandardSchemaV1 | undefined> extends GuardOptions {
68
68
  /** Validates the input (plain object or `FormData`) with any Standard Schema. */
69
69
  readonly input?: S;
70
70
  }
@@ -76,7 +76,7 @@ type Unwrapped<T> = T extends {
76
76
  } | {
77
77
  readonly ok: false;
78
78
  } ? D : T;
79
- export interface BetterNext<M extends AnyModels, F extends AnyFunctions, E, C = unknown, U = unknown> extends BetterServer<M, F, E, C, U> {
79
+ interface BetterNext<M extends AnyModels, F extends AnyFunctions, E, C = unknown, U = unknown> extends BetterServer<M, F, E, C, U> {
80
80
  /** `proxy.ts`: refreshes sessions for page loads and server actions. */
81
81
  proxy(request: NextRequest, options?: ProxyOptions<C, U>): Promise<Response>;
82
82
  /** Server Components and actions: the caller's context, memoized per request. */
@@ -97,8 +97,9 @@ export interface BetterNext<M extends AnyModels, F extends AnyFunctions, E, C =
97
97
  }): Promise<ServerContext<M, F, E, C, U>>;
98
98
  /**
99
99
  * First statement of an app-authored `'use cache: private'` function: sets
100
- * `cacheLife` from the session's expiry (`sessionStale`), tags the entry
101
- * `bs:session:<user id>` and returns the caller's context plus `session`.
100
+ * `cacheLife` from the session's expiry (`sessionStale`, capped by
101
+ * `life.stale`), tags the entry `bs:session:<user id>` plus `tags`, and
102
+ * returns the caller's context plus `session`.
102
103
  *
103
104
  * ```ts
104
105
  * async function getCustomers() {
@@ -109,8 +110,13 @@ export interface BetterNext<M extends AnyModels, F extends AnyFunctions, E, C =
109
110
  * ```
110
111
  */
111
112
  cached(options?: CachedOptions): Promise<CachedContext<M, F, E, C, U>>;
112
- /** Drops every `next.cached()` entry of a user, e.g. after a role change. */
113
- invalidateSession(userId: string): void;
113
+ /**
114
+ * Drops every `next.cached()` entry of a user, e.g. after a role change,
115
+ * and the entries tagged with `tags` (such as PermDock's `snapshotTag(userId)`).
116
+ */
117
+ invalidateSession(userId: string, options?: {
118
+ readonly tags?: readonly string[];
119
+ }): void;
114
120
  /**
115
121
  * Counts on the server and returns a serializable seed for
116
122
  * `useLiveCount(seed)`, so the badge renders with a number and the client
@@ -141,7 +147,7 @@ export interface BetterNext<M extends AnyModels, F extends AnyFunctions, E, C =
141
147
  */
142
148
  debugRoute(): (request: Request) => Promise<Response>;
143
149
  }
144
- export interface SessionStaleOptions {
150
+ interface SessionStaleOptions {
145
151
  /** Below this, the result would not be prefetched anyway. Defaults to 30. */
146
152
  readonly min?: number;
147
153
  /** 300 (5 minutes) joins the route's App Shell. Defaults to 300. */
@@ -152,14 +158,21 @@ export interface SessionStaleOptions {
152
158
  * the token's expiry, and at least `min`.
153
159
  */
154
160
  export declare function sessionStale(session: AuthSession, options?: SessionStaleOptions, now?: number): number;
155
- export interface CachedOptions {
156
- /** `cacheLife` for the entry. `stale` comes from `sessionStale`. */
161
+ interface CachedOptions {
162
+ /**
163
+ * `cacheLife` for the entry. `stale` is `sessionStale`, and never more than
164
+ * `stale` when it is set: pass PermDock's `cacheLifeFor(snapshot)` so the
165
+ * entry goes stale with the snapshot too.
166
+ */
157
167
  readonly life?: SessionStaleOptions & {
168
+ readonly stale?: number;
158
169
  readonly revalidate?: number;
159
170
  readonly expire?: number;
160
171
  };
172
+ /** More `cacheTag`s for the entry, e.g. `snapshotTag(sub)` or `org:<id>`. */
173
+ readonly tags?: readonly string[];
161
174
  }
162
- export type CachedContext<M extends AnyModels, F extends AnyFunctions, E, C = unknown, U = unknown> = ServerContext<M, F, E, C, U> & {
175
+ type CachedContext<M extends AnyModels, F extends AnyFunctions, E, C = unknown, U = unknown> = ServerContext<M, F, E, C, U> & {
163
176
  readonly session: AuthSession<C, U>;
164
177
  };
165
178
  /** The tag `next.cached()` puts on a user's entries. */
@@ -172,7 +185,7 @@ export declare function tagFor(table: string, id?: string | number): string;
172
185
  * `cacheTags: false`.
173
186
  */
174
187
  export declare function nextCache(): CacheAdapter;
175
- export interface RequireAalOptions {
188
+ interface RequireAalOptions {
176
189
  /**
177
190
  * Page that enrolls or challenges a second factor. Signed-in users below
178
191
  * the level are redirected there with `?next=<path>`; without it they get
@@ -207,4 +220,4 @@ export declare function shouldRefresh(request: Request): boolean;
207
220
  */
208
221
  export declare function createNext<M extends AnyModels, D, F extends AnyFunctions, E, C = unknown, U = unknown>(sb: BetterSupabase<M, D, F, E, C, U>, options?: NextOptions): BetterNext<M, F, E, C, U>;
209
222
  //#endregion
210
- export { type Aal, type AmrEntry, type AuthKind, type AuthSession, type DbBudget, type DbStats, type EntitlementKey, type GuardOptions, type Impersonator, type MembershipClaim, hasEntitlement, toSession };
223
+ export { type Aal, type ActionOptions, type ActionResult, type AmrEntry, type AuthKind, type AuthSession, type BetterNext, type CachedContext, type CachedOptions, type DbBudget, type DbStats, type EntitlementKey, type GuardOptions, type Impersonator, type MembershipClaim, type NextDebugOptions, type NextOptions, type ProxyOptions, type RequireAalOptions, type SessionStaleOptions, hasEntitlement, toSession };
@@ -1,13 +1,13 @@
1
1
  import { n as dbError } from "../errors-Bl3rkOzo.js";
2
- import { n as StatsRecorder, t as EMPTY_STATS } from "../stats-C5W-rBfR.js";
3
- import { o as isReadSet } from "../read-set-4lI3W_gZ.js";
2
+ import { n as StatsRecorder, t as EMPTY_STATS } from "../stats-D78UQ8Yu.js";
3
+ import { o as isReadSet } from "../read-set-D0qBcboM.js";
4
4
  import { t as validate } from "../standard-FUi3Z3-7.js";
5
- import { a as problemResponse } from "../problem-BTkTtRV9.js";
5
+ import { a as problemResponse } from "../problem-CGt1ADix.js";
6
6
  import { t as isList } from "../guards-BnzDvFrz.js";
7
7
  import { t as hasEntitlement } from "../entitlements-CTEBm7IB.js";
8
- import { a as settle, c as withExtra, d as primaryCookie, i as respond, n as guard, o as createServer, s as extendServer, t as defaultExpose, u as PRIMARY_COOKIE } from "../respond-DMcbQwfN.js";
9
- import { r as checkAal } from "../mfa-CpLfJSde.js";
10
- import { t as toSession } from "../view-aOYizTeE.js";
8
+ import { a as settle, c as withExtra, d as primaryCookie, i as respond, n as guard, o as createServer, s as extendServer, t as defaultExpose, u as PRIMARY_COOKIE } from "../respond-C44EZAJo.js";
9
+ import { r as checkAal } from "../mfa-BxoR1RkN.js";
10
+ import { t as toSession } from "../view-wZ-9qIUz.js";
11
11
  import { cache } from "react";
12
12
  import { cacheLife, cacheTag, revalidateTag, updateTag } from "next/cache.js";
13
13
  import { cookies, headers } from "next/headers.js";
@@ -43,7 +43,9 @@ var StatsCollector = class {
43
43
  this.#prune();
44
44
  const entry = {
45
45
  created: Date.now(),
46
- recorder: new CollectedRecorder(() => this.#schedule(id))
46
+ recorder: new CollectedRecorder(() => {
47
+ this.#schedule(id);
48
+ })
47
49
  };
48
50
  this.#entries.set(id, entry);
49
51
  return entry.recorder;
@@ -140,7 +142,7 @@ function serverTimingValue(metrics) {
140
142
  return Object.entries(metrics).map(([name, ms]) => `${name};dur=${ms.toFixed(1)}`).join(", ");
141
143
  }
142
144
  //#endregion
143
- //#region src/next/index.ts
145
+ //#region src/next/create.ts
144
146
  /** Response header with the URL of the request's totals on `next.debugRoute()`. */
145
147
  const STATS_URL_HEADER = "x-bs-stats";
146
148
  /**
@@ -208,10 +210,10 @@ function formDataObject(form) {
208
210
  function requireAal(level, options = {}) {
209
211
  return (auth, request) => {
210
212
  const { pathname, search } = request.nextUrl;
211
- if (options.redirect !== void 0 && pathname === options.redirect) return void 0;
212
- if (options.match && !options.match(pathname)) return void 0;
213
+ if (options.redirect !== void 0 && pathname === options.redirect) return;
214
+ if (options.match && !options.match(pathname)) return;
213
215
  const denied = checkAal(auth, level);
214
- if (!denied) return void 0;
216
+ if (!denied) return;
215
217
  if (options.redirect === void 0) return problemResponse(denied);
216
218
  const target = new URL(options.redirect, request.url);
217
219
  target.searchParams.set("next", `${pathname}${search}`);
@@ -244,8 +246,8 @@ function createNext(sb, options = {}) {
244
246
  const debug = options.debug;
245
247
  const statsHeader = debug?.header ?? "x-bs-db-calls";
246
248
  const statsRoute = debug?.route ?? "/api/bs-stats";
247
- const development = typeof process !== "undefined" && process.env["NODE_ENV"] === "development";
248
- const collector = debug && (debug.enabled ?? development) ? sharedCollector({
249
+ const development = process.env["NODE_ENV"] === "development";
250
+ const collector = debug?.enabled ? sharedCollector({
249
251
  logger: sb.events.logger,
250
252
  warn: development,
251
253
  ...debug.budget ? { budget: debug.budget } : {}
@@ -284,16 +286,19 @@ function createNext(sb, options = {}) {
284
286
  async cached(cachedOptions = {}) {
285
287
  const [view, ctx] = await Promise.all([session(), server()]);
286
288
  const life = cachedOptions.life;
289
+ const stale = sessionStale(view, life);
287
290
  cacheLife({
288
- stale: sessionStale(view, life),
291
+ stale: life?.stale === void 0 ? stale : Math.min(stale, life.stale),
289
292
  ...life?.revalidate === void 0 ? {} : { revalidate: life.revalidate },
290
293
  ...life?.expire === void 0 ? {} : { expire: life.expire }
291
294
  });
292
- if (view.kind === "user") cacheTag(sessionTag(view.user.id));
295
+ const tags = [...view.kind === "user" ? [sessionTag(view.user.id)] : [], ...cachedOptions.tags ?? []];
296
+ if (tags.length > 0) cacheTag(...tags);
293
297
  return withExtra(ctx, { session: view });
294
298
  },
295
- invalidateSession(userId) {
299
+ invalidateSession(userId, invalidateOptions = {}) {
296
300
  invalidate(sessionTag(userId));
301
+ for (const tag of invalidateOptions.tags ?? []) invalidate(tag);
297
302
  },
298
303
  async liveCount(spec, db) {
299
304
  const result = await (db ?? (await server()).db).$run(spec);
@@ -337,7 +342,7 @@ function createNext(sb, options = {}) {
337
342
  route(handler, guardOptions = {}) {
338
343
  return async (request, segment) => {
339
344
  const ctx = await base.context(request, statsFor(request));
340
- const denied = guard(ctx.auth, guardOptions.allow, guardOptions.aal);
345
+ const denied = guard(ctx.auth, guardOptions.allow, guardOptions.aal, guardOptions.scopes);
341
346
  const instance = request.nextUrl.pathname;
342
347
  if (denied) return problemResponse(denied, {
343
348
  instance,
@@ -358,23 +363,23 @@ function createNext(sb, options = {}) {
358
363
  };
359
364
  },
360
365
  debugRoute() {
361
- return async (request) => {
366
+ return (request) => {
362
367
  const id = new URL(request.url).searchParams.get("id");
363
- if (!collector || !id) return Response.json({ error: collector ? "missing ?id=" : "debug is off" }, {
368
+ if (!collector || !id) return Promise.resolve(Response.json({ error: collector ? "missing ?id=" : "debug is off" }, {
364
369
  status: 404,
365
370
  headers: { "cache-control": "no-store" }
366
- });
371
+ }));
367
372
  const stats = collector.get(id) ?? EMPTY_STATS;
368
- return Response.json(stats, { headers: {
373
+ return Promise.resolve(Response.json(stats, { headers: {
369
374
  [statsHeader]: formatStats(stats),
370
375
  "cache-control": "no-store"
371
- } });
376
+ } }));
372
377
  };
373
378
  },
374
379
  action(actionOptions, fn) {
375
380
  return async (input) => {
376
381
  const ctx = await server();
377
- const denied = guard(ctx.auth, actionOptions.allow, actionOptions.aal);
382
+ const denied = guard(ctx.auth, actionOptions.allow, actionOptions.aal, actionOptions.scopes);
378
383
  if (denied) return {
379
384
  ok: false,
380
385
  data: null,
@@ -1,2 +1,2 @@
1
- import { a as ResourceOptions, i as ResourceOperation, n as OpenApiOptions, o as createOpenApi, r as PROBLEM_SCHEMA, t as OpenApiDocument } from "../index-DbxFr7XG.js";
1
+ import { a as ResourceOptions, i as ResourceOperation, n as OpenApiOptions, o as createOpenApi, r as PROBLEM_SCHEMA, t as OpenApiDocument } from "../index-qriRM9cZ.js";
2
2
  export { OpenApiDocument, OpenApiOptions, PROBLEM_SCHEMA, ResourceOperation, ResourceOptions, createOpenApi };
@@ -1,6 +1,6 @@
1
- import { n as PROBLEM_TYPE_BASE, t as PROBLEM_CONTENT_TYPE } from "../problem-BTkTtRV9.js";
1
+ import { n as PROBLEM_TYPE_BASE, t as PROBLEM_CONTENT_TYPE } from "../problem-CGt1ADix.js";
2
2
  import { t as SPEC_PINS } from "../spec-pins-C3oc-Tdp.js";
3
- import { t as buildJsonSchema } from "../json-schema-BpYqkCKf.js";
3
+ import { t as buildJsonSchema } from "../json-schema--aOXAGfU.js";
4
4
  //#region src/openapi/index.ts
5
5
  /** JSON Schema for RFC 9457 Problem Details as better-supabase sends them. */
6
6
  const PROBLEM_SCHEMA = {
@@ -81,7 +81,7 @@ const ERRORS = [
81
81
  "Validation or a check constraint failed."
82
82
  ]
83
83
  ];
84
- const pascal = (value) => value.replace(/(^|[_-])(\w)/g, (_, _sep, char) => char.toUpperCase());
84
+ const pascal = (value) => value.replaceAll(/(^|[_-])(\w)/g, (_, _sep, char) => char.toUpperCase());
85
85
  const ref = (name) => ({ $ref: `#/components/schemas/${name}` });
86
86
  const response = (name) => ({ $ref: `#/components/responses/${name}` });
87
87
  function errorResponses(codes) {
@@ -95,8 +95,14 @@ function jsonBody(schema, description) {
95
95
  content: { "application/json": { schema } }
96
96
  };
97
97
  }
98
- function pageParameters() {
99
- return [{
98
+ function pageParameters(cursor, maxPageSize) {
99
+ return [cursor ? {
100
+ name: "after",
101
+ in: "query",
102
+ required: false,
103
+ description: "`nextCursor` of the previous page. Leave it out for the first page.",
104
+ schema: { type: "string" }
105
+ } : {
100
106
  name: "page",
101
107
  in: "query",
102
108
  required: false,
@@ -114,11 +120,53 @@ function pageParameters() {
114
120
  schema: {
115
121
  type: "integer",
116
122
  minimum: 1,
117
- maximum: 200,
123
+ maximum: maxPageSize,
118
124
  default: 50
119
125
  }
120
126
  }];
121
127
  }
128
+ function pageSchema(row, cursor) {
129
+ const items = {
130
+ type: "array",
131
+ items: row
132
+ };
133
+ return cursor ? {
134
+ type: "object",
135
+ required: [
136
+ "items",
137
+ "nextCursor",
138
+ "hasMore"
139
+ ],
140
+ properties: {
141
+ items,
142
+ nextCursor: { type: ["string", "null"] },
143
+ hasMore: { type: "boolean" }
144
+ }
145
+ } : {
146
+ type: "object",
147
+ required: ["items", "page"],
148
+ properties: {
149
+ items,
150
+ page: {
151
+ type: "object",
152
+ required: [
153
+ "number",
154
+ "size",
155
+ "total",
156
+ "pages",
157
+ "hasMore"
158
+ ],
159
+ properties: {
160
+ number: { type: "integer" },
161
+ size: { type: "integer" },
162
+ total: { type: ["integer", "null"] },
163
+ pages: { type: ["integer", "null"] },
164
+ hasMore: { type: "boolean" }
165
+ }
166
+ }
167
+ }
168
+ };
169
+ }
122
170
  /**
123
171
  * An OpenAPI 3.1 document for REST resources over your tables, with schemas
124
172
  * from the generated metadata, list-query parameters, Supabase security
@@ -167,33 +215,8 @@ function createOpenApi(sb, options) {
167
215
  const schema = defs[`${key}${variant}`];
168
216
  if (schema) schemas[`${name}${variant}`] = schema;
169
217
  }
170
- schemas[`${name}Page`] = {
171
- type: "object",
172
- required: ["items", "page"],
173
- properties: {
174
- items: {
175
- type: "array",
176
- items: ref(`${name}Row`)
177
- },
178
- page: {
179
- type: "object",
180
- required: [
181
- "number",
182
- "size",
183
- "total",
184
- "pages",
185
- "hasMore"
186
- ],
187
- properties: {
188
- number: { type: "integer" },
189
- size: { type: "integer" },
190
- total: { type: ["integer", "null"] },
191
- pages: { type: ["integer", "null"] },
192
- hasMore: { type: "boolean" }
193
- }
194
- }
195
- }
196
- };
218
+ const cursor = (resource.list ? resource.list.pagination : resource.pagination) === "cursor";
219
+ schemas[`${name}Page`] = pageSchema(ref(`${name}Row`), cursor);
197
220
  const collection = `${base}${resource.path ?? `/${key}`}`;
198
221
  const keyColumn = table.primaryKey.length === 1 ? table.primaryKey[0] : void 0;
199
222
  const idSchema = keyColumn ? (defs[`${key}Row`]?.["properties"])?.[keyColumn] ?? { type: "string" } : void 0;
@@ -215,7 +238,7 @@ function createOpenApi(sb, options) {
215
238
  for (const operation of operations) switch (operation) {
216
239
  case "list":
217
240
  (paths[collection] ??= {})["get"] = op("list", `List ${key}`, {
218
- parameters: resource.list?.openapi ?? pageParameters(),
241
+ parameters: resource.list?.openapi ?? pageParameters(cursor, resource.maxPageSize ?? 200),
219
242
  responses: {
220
243
  "200": jsonBody(ref(`${name}Page`), `A page of ${key}.`),
221
244
  ...errorResponses([
@@ -1,9 +1,9 @@
1
- import { n as AnyModels, t as AnyFunctions } from "../types-C83v5SQz.js";
2
- import { o as Result, p as DbError } from "../result-CTkCZ6JA.js";
3
- import { t as BetterSupabase } from "../define-e3zI4_4g.js";
4
- import { r as AuthState } from "../resolve-CjHILk2w.js";
5
- import { d as ServerContext, f as ServerOptions, l as BetterServer, n as GuardOptions } from "../respond-Dne1ARSa.js";
6
- import { r as ProblemDetails } from "../problem-DlgB0lYl.js";
1
+ import { n as AnyModels, t as AnyFunctions } from "../types-DONv9SHN.js";
2
+ import { o as Result, p as DbError } from "../result-c2OYgCV2.js";
3
+ import { t as BetterSupabase } from "../define-DUbydmJa.js";
4
+ import { r as AuthState } from "../resolve-BRciX2jK.js";
5
+ import { d as ServerContext, f as ServerOptions, l as BetterServer, n as GuardOptions } from "../respond-DAkPIdTH.js";
6
+ import { r as ProblemDetails } from "../problem-Z2RTQjZD.js";
7
7
  import { DecoratedMiddleware, ORPCError } from "@orpc/server";
8
8
  //#region src/orpc/index.d.ts
9
9
  /** Initial context: pass `{ context: { request } }` to the oRPC handler. */
@@ -1,6 +1,6 @@
1
1
  import { r as dbErrorOf } from "../errors-Bl3rkOzo.js";
2
- import { o as toProblem } from "../problem-BTkTtRV9.js";
3
- import { a as settle, n as guard, o as createServer, s as extendServer, t as defaultExpose } from "../respond-DMcbQwfN.js";
2
+ import { o as toProblem } from "../problem-CGt1ADix.js";
3
+ import { a as settle, n as guard, o as createServer, s as extendServer, t as defaultExpose } from "../respond-C44EZAJo.js";
4
4
  import { COMMON_ERROR_STATUS_MAP, ORPCError, os } from "@orpc/server";
5
5
  //#region src/orpc/index.ts
6
6
  const CODES = new Map(Object.entries(COMMON_ERROR_STATUS_MAP).map(([code, status]) => [status, code]));
@@ -24,7 +24,7 @@ function createOrpc(sb, options = {}) {
24
24
  middleware(middlewareOptions = {}) {
25
25
  return os.$context().middleware(async ({ context, next }) => {
26
26
  const ctx = await server.context(context.request, { refresh: middlewareOptions.refresh ?? false });
27
- const denied = guard(ctx.auth, middlewareOptions.allow, middlewareOptions.aal);
27
+ const denied = guard(ctx.auth, middlewareOptions.allow, middlewareOptions.aal, middlewareOptions.scopes);
28
28
  if (denied) throw toORPCError(denied);
29
29
  try {
30
30
  return await next({ context: {
@@ -1,5 +1,5 @@
1
- import { a as EventHub } from "../events-DOt3nSO-.js";
2
- import { f as Plugin } from "../plugin-C4TR9J0Z.js";
1
+ import { a as EventHub } from "../events-Do8jXh4D.js";
2
+ import { f as Plugin } from "../plugin-DVRB8qNE.js";
3
3
  import { t as SPEC_PINS } from "../spec-pins-CqorWDnO.js";
4
4
  import { Attributes, Meter, Tracer } from "@opentelemetry/api";
5
5
  //#region src/otel/index.d.ts
@@ -1,6 +1,6 @@
1
1
  import { n as definePlugin } from "../plugin-BqR6wKMB.js";
2
2
  import { t as SPEC_PINS } from "../spec-pins-C3oc-Tdp.js";
3
- import "../version-Cqq1m3UY.js";
3
+ import "../version-BfpG2gks.js";
4
4
  import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
5
5
  //#region src/otel/index.ts
6
6
  const INSTRUMENTATION_NAME = "better-supabase";
@@ -157,7 +157,9 @@ function traceAuth(sb, options = {}) {
157
157
  function tracedFetch(base = fetch) {
158
158
  return (input, init) => {
159
159
  const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
160
- propagation.inject(context.active(), headers, { set: (carrier, key, value) => carrier.set(key, value) });
160
+ propagation.inject(context.active(), headers, { set: (carrier, key, value) => {
161
+ carrier.set(key, value);
162
+ } });
161
163
  return base(input, {
162
164
  ...init,
163
165
  headers