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,210 @@
1
+ import { n as AnyModels, t as AnyFunctions } from "../types-C2fslP1z.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-CTfWw-SS.js";
4
+ import { n as AmrEntry, s as Impersonator, t as Aal } from "../mfa-TVia554c.js";
5
+ import { r as AuthState } from "../resolve-DW8h9NPm.js";
6
+ import { d as ServerContext, f as ServerOptions, l as BetterServer, n as GuardOptions, t as AuthKind } from "../respond-PoTpy5DT.js";
7
+ import { n as toSession, t as AuthSession } from "../view-B0ltUmTi.js";
8
+ import { r as LiveCountSeed, t as CountRunner } from "../live-6b9i72Ux.js";
9
+ import { n as hasEntitlement, t as EntitlementKey } from "../entitlements-BBg61DQZ.js";
10
+ import { NextRequest } from "next/server.js";
11
+ import { StandardSchemaV1 } from "@standard-schema/spec";
12
+ //#region src/next/collector.d.ts
13
+ /** Forwarded by the proxy so every scope of one render shares an id. */
14
+ export declare const REQUEST_ID_HEADER = "x-bs-request-id";
15
+ interface DbBudget {
16
+ readonly calls?: number;
17
+ readonly waves?: number;
18
+ }
19
+ //#endregion
20
+ //#region src/next/index.d.ts
21
+ export interface NextOptions extends ServerOptions {
22
+ /** Invalidate `tagFor(table)` cache tags after mutations. Defaults to true. */
23
+ readonly cacheTags?: boolean;
24
+ /** Include internal error messages in Problem Details. Defaults to development only. */
25
+ readonly exposeErrors?: boolean;
26
+ /** Per-request database call counts. See the Budget docs. */
27
+ readonly debug?: NextDebugOptions;
28
+ }
29
+ export interface NextDebugOptions {
30
+ /** Collect outside development too, e.g. for e2e runs against `next start`. */
31
+ readonly enabled?: boolean;
32
+ /** Header with `calls;waves;ms` on route handler responses. Defaults to `x-bs-db-calls`. */
33
+ readonly header?: string;
34
+ /** Warns in development when one render goes over it. */
35
+ readonly budget?: DbBudget;
36
+ /** Where `next.debugRoute()` is mounted. Defaults to `/api/bs-stats`. */
37
+ readonly route?: string;
38
+ }
39
+ /** Response header with the URL of the request's totals on `next.debugRoute()`. */
40
+ export declare const STATS_URL_HEADER = "x-bs-stats";
41
+ export interface ProxyOptions<C = unknown> {
42
+ /**
43
+ * Another middleware to compose with, such as next-intl's. It runs on the
44
+ * original request, alongside auth; its rewrite, redirect or request
45
+ * headers are kept, and refreshed cookies are merged into its response.
46
+ */
47
+ readonly before?: (request: NextRequest) => Response | undefined | Promise<Response | undefined>;
48
+ /** Answer before rendering, e.g. redirect signed-out users. Refreshed cookies are kept. */
49
+ readonly protect?: (auth: AuthState<C>, request: NextRequest) => Response | undefined | Promise<Response | undefined>;
50
+ /** Post-processes the final response; return a new one to replace it. */
51
+ readonly after?: (response: Response, auth: AuthState<C>) => Response | undefined | void | Promise<Response | undefined | void>;
52
+ /**
53
+ * Adds `Server-Timing: bs-proxy;dur=..., bs-verify;dur=...` (ms): the whole
54
+ * proxy, and resolving the session (a local verify, or a refresh).
55
+ */
56
+ readonly serverTiming?: boolean;
57
+ }
58
+ export type ActionResult<T> = {
59
+ readonly ok: true;
60
+ readonly data: T;
61
+ readonly error: null;
62
+ } | {
63
+ readonly ok: false;
64
+ readonly data: null;
65
+ readonly error: DbError;
66
+ };
67
+ export interface ActionOptions<S extends StandardSchemaV1 | undefined> extends GuardOptions {
68
+ /** Validates the input (plain object or `FormData`) with any Standard Schema. */
69
+ readonly input?: S;
70
+ }
71
+ type ActionInput<S> = S extends StandardSchemaV1 ? StandardSchemaV1.InferInput<S> | FormData : unknown;
72
+ type ActionParsed<S> = S extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<S> : unknown;
73
+ type Unwrapped<T> = T extends {
74
+ readonly ok: true;
75
+ readonly data: infer D;
76
+ } | {
77
+ readonly ok: false;
78
+ } ? D : T;
79
+ export interface BetterNext<M extends AnyModels, F extends AnyFunctions, E, C = unknown> extends BetterServer<M, F, E, C> {
80
+ /** `proxy.ts`: refreshes sessions for page loads and server actions. */
81
+ proxy(request: NextRequest, options?: ProxyOptions<C>): Promise<Response>;
82
+ /** Server Components and actions: the caller's context, memoized per request. */
83
+ server(): Promise<ServerContext<M, F, E, C>>;
84
+ /**
85
+ * The verified caller as serializable data (no token, no clients), memoized
86
+ * per request. Wrap it in a `'use cache: private'` function with Cache
87
+ * Components and pass the promise to `<SessionProvider>`.
88
+ */
89
+ session(): Promise<AuthSession<C>>;
90
+ /**
91
+ * The context for a session and its token, without reading the request.
92
+ * The token is verified again (memoized, no network call); a token for
93
+ * another user than `session` gives an `invalid` context.
94
+ */
95
+ serverFor(session: AuthSession<C>, options: {
96
+ readonly token: string | null;
97
+ }): Promise<ServerContext<M, F, E, C>>;
98
+ /**
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`.
102
+ *
103
+ * ```ts
104
+ * async function getCustomers() {
105
+ * 'use cache: private';
106
+ * const { db } = await next.cached();
107
+ * return db.customers.findMany().orThrow();
108
+ * }
109
+ * ```
110
+ */
111
+ cached(options?: CachedOptions): Promise<CachedContext<M, F, E, C>>;
112
+ /** Drops every `next.cached()` entry of a user, e.g. after a role change. */
113
+ invalidateSession(userId: string): void;
114
+ /**
115
+ * Counts on the server and returns a serializable seed for
116
+ * `useLiveCount(seed)`, so the badge renders with a number and the client
117
+ * only refetches on changes. Pass `db` inside `'use cache: private'`
118
+ * (from `next.cached()`); otherwise it uses `next.server()`. A failed
119
+ * count gives `count: null` instead of throwing.
120
+ */
121
+ liveCount<T extends Extract<keyof M, string>>(spec: QuerySpec<T, "count", number>, db?: CountRunner): Promise<LiveCountSeed<T>>;
122
+ /** Route handler with auth, Result unwrapping and Problem Details errors. */
123
+ route<P = Record<string, string | string[]>>(handler: (request: NextRequest, ctx: ServerContext<M, F, E, C> & {
124
+ readonly params: P;
125
+ }) => unknown, options?: GuardOptions): (request: NextRequest, segment: {
126
+ readonly params: Promise<P>;
127
+ }) => Promise<Response>;
128
+ /** Server action returning a serializable `ActionResult`. */
129
+ action<S extends StandardSchemaV1 | undefined, T>(options: ActionOptions<S>, fn: (input: ActionParsed<S>, ctx: ServerContext<M, F, E, C>) => Promise<T> | T): (input: ActionInput<S>) => Promise<ActionResult<Unwrapped<Awaited<T>>>>;
130
+ /** Tags the current `"use cache"` scope with a table (and row) tag. */
131
+ cacheTag(table: Extract<keyof M, string>, id?: string | number): void;
132
+ /**
133
+ * Tags the current `"use cache"` scope with every table the specs (or a
134
+ * read set) read, plus the row of each `findById`, so any mutation that
135
+ * changes one revalidates it.
136
+ */
137
+ cacheTags(target: QuerySpec<Extract<keyof M, string>> | readonly QuerySpec<Extract<keyof M, string>>[] | ReadSet): void;
138
+ /**
139
+ * GET handler serving a request's database totals (`?id=<request id>`) for
140
+ * `expectDbBudget`. Answers 404 unless `debug` is on.
141
+ */
142
+ debugRoute(): (request: Request) => Promise<Response>;
143
+ }
144
+ export interface SessionStaleOptions {
145
+ /** Below this, the result would not be prefetched anyway. Defaults to 30. */
146
+ readonly min?: number;
147
+ /** 300 (5 minutes) joins the route's App Shell. Defaults to 300. */
148
+ readonly max?: number;
149
+ }
150
+ /**
151
+ * `cacheLife` `stale` seconds for a view of `session`: `max`, but never past
152
+ * the token's expiry, and at least `min`.
153
+ */
154
+ export declare function sessionStale(session: AuthSession, options?: SessionStaleOptions, now?: number): number;
155
+ export interface CachedOptions {
156
+ /** `cacheLife` for the entry. `stale` comes from `sessionStale`. */
157
+ readonly life?: SessionStaleOptions & {
158
+ readonly revalidate?: number;
159
+ readonly expire?: number;
160
+ };
161
+ }
162
+ export type CachedContext<M extends AnyModels, F extends AnyFunctions, E, C = unknown> = ServerContext<M, F, E, C> & {
163
+ readonly session: AuthSession<C>;
164
+ };
165
+ /** The tag `next.cached()` puts on a user's entries. */
166
+ export declare function sessionTag(userId: string): string;
167
+ /** `bs:<table>` or `bs:<table>:<id>`. Mutations invalidate both. */
168
+ export declare function tagFor(table: string, id?: string | number): string;
169
+ /**
170
+ * Invalidates `bs:<table>` for every table in the target and
171
+ * `bs:<table>:<id>` for the changed rows. `createNext` attaches it unless
172
+ * `cacheTags: false`.
173
+ */
174
+ export declare function nextCache(): CacheAdapter;
175
+ export interface RequireAalOptions {
176
+ /**
177
+ * Page that enrolls or challenges a second factor. Signed-in users below
178
+ * the level are redirected there with `?next=<path>`; without it they get
179
+ * a 403 Problem Details response.
180
+ */
181
+ readonly redirect?: string;
182
+ /** Paths that need the level. Defaults to every path but `redirect`. */
183
+ readonly match?: (pathname: string) => boolean;
184
+ }
185
+ /**
186
+ * A `proxy` `protect` that keeps signed-in users below `level` out of the
187
+ * matched paths. The JWT carries no factor list, so this also sends users
188
+ * without a factor to `redirect` to enroll one. Anonymous callers pass;
189
+ * combine it with your sign-in redirect.
190
+ *
191
+ * ```ts
192
+ * next.proxy(request, {
193
+ * protect: requireAal('aal2', { redirect: '/mfa', match: (path) => path.startsWith('/settings') }),
194
+ * });
195
+ * ```
196
+ */
197
+ export declare function requireAal(level: Aal, options?: RequireAalOptions): (auth: AuthState, request: NextRequest) => Response | undefined;
198
+ /** Page loads, client navigations and server actions; never prefetches or assets. */
199
+ export declare function shouldRefresh(request: Request): boolean;
200
+ /**
201
+ * The Next.js adapter: proxy, Server Components, route handlers, server
202
+ * actions and cache tags, on top of `createServer`.
203
+ *
204
+ * ```ts
205
+ * export const next = createNext(sb);
206
+ * ```
207
+ */
208
+ export declare function createNext<M extends AnyModels, D, F extends AnyFunctions, E, C = unknown>(sb: BetterSupabase<M, D, F, E, C>, options?: NextOptions): BetterNext<M, F, E, C>;
209
+ //#endregion
210
+ export { type Aal, type AmrEntry, type AuthKind, type AuthSession, type DbBudget, type DbStats, type EntitlementKey, type GuardOptions, type Impersonator, hasEntitlement, toSession };
@@ -0,0 +1,426 @@
1
+ import { n as dbError } from "../errors-Bl3rkOzo.js";
2
+ import { n as StatsRecorder, t as EMPTY_STATS } from "../stats-3iy8u3_P.js";
3
+ import { o as isReadSet } from "../read-set-DhX2d04c.js";
4
+ import { t as validate } from "../standard-FUi3Z3-7.js";
5
+ import { a as problemResponse } from "../problem-DPu6Fh-t.js";
6
+ import { t as hasEntitlement } from "../entitlements-BC_khwRy.js";
7
+ 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-4-dhoV7x.js";
8
+ import { r as checkAal } from "../mfa-CpogP66z.js";
9
+ import { t as toSession } from "../view-B6F9QbG-.js";
10
+ import { cache } from "react";
11
+ import { cacheLife, cacheTag, revalidateTag, updateTag } from "next/cache.js";
12
+ import { cookies, headers } from "next/headers.js";
13
+ import { NextResponse } from "next/server.js";
14
+ //#region src/next/collector.ts
15
+ /** Forwarded by the proxy so every scope of one render shares an id. */
16
+ const REQUEST_ID_HEADER = "x-bs-request-id";
17
+ var CollectedRecorder = class extends StatsRecorder {
18
+ #onIdle;
19
+ constructor(onIdle) {
20
+ super();
21
+ this.#onIdle = onIdle;
22
+ }
23
+ begin(table) {
24
+ const end = super.begin(table);
25
+ return () => {
26
+ end();
27
+ this.#onIdle();
28
+ };
29
+ }
30
+ };
31
+ /** Request totals keyed by request id, bounded by age and count. */
32
+ var StatsCollector = class {
33
+ #entries = /* @__PURE__ */ new Map();
34
+ #options;
35
+ constructor(options) {
36
+ this.#options = options;
37
+ }
38
+ /** The recorder for a request id, created on first use. */
39
+ recorderFor(id) {
40
+ const existing = this.#entries.get(id);
41
+ if (existing) return existing.recorder;
42
+ this.#prune();
43
+ const entry = {
44
+ created: Date.now(),
45
+ recorder: new CollectedRecorder(() => this.#schedule(id))
46
+ };
47
+ this.#entries.set(id, entry);
48
+ return entry.recorder;
49
+ }
50
+ get(id) {
51
+ return this.#entries.get(id)?.recorder.snapshot();
52
+ }
53
+ #schedule(id) {
54
+ const { budget, warn } = this.#options;
55
+ const entry = this.#entries.get(id);
56
+ if (!entry || !warn || !budget) return;
57
+ if (entry.timer) clearTimeout(entry.timer);
58
+ entry.timer = setTimeout(() => {
59
+ entry.timer = void 0;
60
+ const stats = entry.recorder.snapshot();
61
+ if (!overBudget(stats, budget)) return;
62
+ this.#options.logger.warn(`request ${id} made ${stats.calls} database calls in ${stats.waves} waves, over the budget of ${describeBudget(budget)}`, {
63
+ requestId: id,
64
+ ...stats
65
+ });
66
+ }, this.#options.idleMs ?? 250);
67
+ }
68
+ #prune() {
69
+ const cutoff = Date.now() - (this.#options.ttlMs ?? 6e4);
70
+ const max = this.#options.maxEntries ?? 500;
71
+ for (const [id, entry] of this.#entries) {
72
+ if (entry.created >= cutoff && this.#entries.size < max) break;
73
+ if (entry.timer) clearTimeout(entry.timer);
74
+ this.#entries.delete(id);
75
+ }
76
+ }
77
+ };
78
+ const SHARED = Symbol.for("better-supabase.next.stats-collector");
79
+ /**
80
+ * One collector per process. Next.js can load the proxy, pages and route
81
+ * handlers as separate module instances, and they must see the same totals.
82
+ */
83
+ function sharedCollector(options) {
84
+ const scope = globalThis;
85
+ scope[SHARED] ??= new StatsCollector(options);
86
+ return scope[SHARED];
87
+ }
88
+ function overBudget(stats, budget) {
89
+ return budget.calls !== void 0 && stats.calls > budget.calls || budget.waves !== void 0 && stats.waves > budget.waves;
90
+ }
91
+ function describeBudget(budget) {
92
+ return [budget.calls === void 0 ? void 0 : `${budget.calls} calls`, budget.waves === void 0 ? void 0 : `${budget.waves} waves`].filter(Boolean).join(" and ");
93
+ }
94
+ /** `calls;waves;ms`, the value of the debug header. */
95
+ function formatStats(stats) {
96
+ return `${stats.calls};${stats.waves};${stats.ms}`;
97
+ }
98
+ //#endregion
99
+ //#region src/next/proxy.ts
100
+ const OVERRIDE = "x-middleware-override-headers";
101
+ const REQUEST_PREFIX = "x-middleware-request-";
102
+ /** A response whose headers can be changed (`Response.redirect()`'s cannot). */
103
+ function mutableResponse(response) {
104
+ try {
105
+ response.headers.append("x-bs-probe", "1");
106
+ response.headers.delete("x-bs-probe");
107
+ return response;
108
+ } catch {
109
+ return new NextResponse(response.body, {
110
+ status: response.status,
111
+ statusText: response.statusText,
112
+ headers: new Headers(response.headers)
113
+ });
114
+ }
115
+ }
116
+ const isRedirect = (response) => response.status >= 300 && response.status < 400 && response.headers.has("location");
117
+ /**
118
+ * Sets request headers the rest of the request sees, the way
119
+ * `NextResponse.next({ request: { headers } })` does, on a response that may
120
+ * already override some (next-intl sets its locale this way). Next.js keeps
121
+ * only the headers listed in `x-middleware-override-headers`, so the full set
122
+ * is written back.
123
+ */
124
+ function overrideRequestHeaders(response, request, set) {
125
+ const listed = response.headers.get(OVERRIDE);
126
+ const current = new Headers();
127
+ if (listed === null) for (const [name, value] of request.headers) current.set(name, value);
128
+ else for (const name of listed.split(",")) {
129
+ const value = response.headers.get(`${REQUEST_PREFIX}${name.trim()}`);
130
+ if (name.trim() && value !== null) current.set(name.trim(), value);
131
+ }
132
+ for (const [name, value] of Object.entries(set)) current.set(name, value);
133
+ for (const name of [...response.headers.keys()]) if (name.startsWith(REQUEST_PREFIX)) response.headers.delete(name);
134
+ response.headers.set(OVERRIDE, [...current.keys()].join(","));
135
+ for (const [name, value] of current) response.headers.set(`${REQUEST_PREFIX}${name}`, value);
136
+ }
137
+ /** `bs-proxy;dur=1.2, bs-verify;dur=0.4`, per W3C Server Timing. */
138
+ function serverTimingValue(metrics) {
139
+ return Object.entries(metrics).map(([name, ms]) => `${name};dur=${ms.toFixed(1)}`).join(", ");
140
+ }
141
+ //#endregion
142
+ //#region src/next/index.ts
143
+ /** Response header with the URL of the request's totals on `next.debugRoute()`. */
144
+ const STATS_URL_HEADER = "x-bs-stats";
145
+ /**
146
+ * `cacheLife` `stale` seconds for a view of `session`: `max`, but never past
147
+ * the token's expiry, and at least `min`.
148
+ */
149
+ function sessionStale(session, options = {}, now = Date.now()) {
150
+ const min = options.min ?? 30;
151
+ const max = options.max ?? 300;
152
+ if (session.kind !== "user" || session.expiresAt === null) return max;
153
+ const remaining = session.expiresAt - Math.floor(now / 1e3);
154
+ return Math.min(max, Math.max(min, remaining));
155
+ }
156
+ /** The tag `next.cached()` puts on a user's entries. */
157
+ function sessionTag(userId) {
158
+ return `bs:session:${userId}`;
159
+ }
160
+ /** `bs:<table>` or `bs:<table>:<id>`. Mutations invalidate both. */
161
+ function tagFor(table, id) {
162
+ return id === void 0 ? `bs:${table}` : `bs:${table}:${String(id)}`;
163
+ }
164
+ function invalidate(tag) {
165
+ try {
166
+ updateTag(tag);
167
+ } catch {
168
+ try {
169
+ revalidateTag(tag, "max");
170
+ } catch {}
171
+ }
172
+ }
173
+ /**
174
+ * Invalidates `bs:<table>` for every table in the target and
175
+ * `bs:<table>:<id>` for the changed rows. `createNext` attaches it unless
176
+ * `cacheTags: false`.
177
+ */
178
+ function nextCache() {
179
+ return {
180
+ name: "next",
181
+ invalidate: (target) => {
182
+ for (const table of target.tables) invalidate(tagFor(table));
183
+ for (const id of target.ids) invalidate(tagFor(target.table, id));
184
+ }
185
+ };
186
+ }
187
+ function formDataObject(form) {
188
+ const out = {};
189
+ for (const key of new Set(form.keys())) {
190
+ const values = form.getAll(key);
191
+ out[key] = values.length > 1 ? values : values[0];
192
+ }
193
+ return out;
194
+ }
195
+ /**
196
+ * A `proxy` `protect` that keeps signed-in users below `level` out of the
197
+ * matched paths. The JWT carries no factor list, so this also sends users
198
+ * without a factor to `redirect` to enroll one. Anonymous callers pass;
199
+ * combine it with your sign-in redirect.
200
+ *
201
+ * ```ts
202
+ * next.proxy(request, {
203
+ * protect: requireAal('aal2', { redirect: '/mfa', match: (path) => path.startsWith('/settings') }),
204
+ * });
205
+ * ```
206
+ */
207
+ function requireAal(level, options = {}) {
208
+ return (auth, request) => {
209
+ const { pathname, search } = request.nextUrl;
210
+ if (options.redirect !== void 0 && pathname === options.redirect) return void 0;
211
+ if (options.match && !options.match(pathname)) return void 0;
212
+ const denied = checkAal(auth, level);
213
+ if (!denied) return void 0;
214
+ if (options.redirect === void 0) return problemResponse(denied);
215
+ const target = new URL(options.redirect, request.url);
216
+ target.searchParams.set("next", `${pathname}${search}`);
217
+ return NextResponse.redirect(target);
218
+ };
219
+ }
220
+ /** Page loads, client navigations and server actions; never prefetches or assets. */
221
+ function shouldRefresh(request) {
222
+ const h = request.headers;
223
+ if (request.method !== "GET" && request.method !== "HEAD") return h.has("next-action");
224
+ if (h.has("next-router-prefetch") || h.get("purpose") === "prefetch" || h.get("sec-purpose")?.includes("prefetch")) return false;
225
+ if (h.get("rsc") === "1") return true;
226
+ const dest = h.get("sec-fetch-dest");
227
+ if (dest) return dest === "document";
228
+ return h.get("accept")?.includes("text/html") ?? false;
229
+ }
230
+ /**
231
+ * The Next.js adapter: proxy, Server Components, route handlers, server
232
+ * actions and cache tags, on top of `createServer`.
233
+ *
234
+ * ```ts
235
+ * export const next = createNext(sb);
236
+ * ```
237
+ */
238
+ function createNext(sb, options = {}) {
239
+ const base = createServer(sb, options);
240
+ const pinMs = options.replicas?.pinMs ?? 5e3;
241
+ const expose = options.exposeErrors ?? defaultExpose();
242
+ if (options.cacheTags !== false) sb.cache(nextCache());
243
+ const debug = options.debug;
244
+ const statsHeader = debug?.header ?? "x-bs-db-calls";
245
+ const statsRoute = debug?.route ?? "/api/bs-stats";
246
+ const development = typeof process !== "undefined" && process.env["NODE_ENV"] === "development";
247
+ const collector = debug && (debug.enabled ?? development) ? sharedCollector({
248
+ logger: sb.events.logger,
249
+ warn: development,
250
+ ...debug.budget ? { budget: debug.budget } : {}
251
+ }) : void 0;
252
+ const statsFor = (request) => {
253
+ const id = collector && request.headers.get("x-bs-request-id");
254
+ return id ? { stats: collector.recorderFor(id) } : {};
255
+ };
256
+ const incomingRequest = async () => new Request("http://next.local/", { headers: new Headers(await headers()) });
257
+ const server = cache(async () => {
258
+ const request = await incomingRequest();
259
+ return base.context(request, statsFor(request));
260
+ });
261
+ const session = cache(async () => {
262
+ const resolution = await base.resolve(await incomingRequest(), { refresh: false });
263
+ return toSession(resolution.auth);
264
+ });
265
+ const serverFor = async (view, { token }) => {
266
+ const request = new Request("http://next.local/", { headers: token ? { authorization: `Bearer ${token}` } : {} });
267
+ const ctx = await base.context(request);
268
+ if (ctx.auth.kind === "user" ? view.kind === "user" && view.user.id === ctx.auth.user.id : view.kind !== "user") return ctx;
269
+ return base.contextFor({
270
+ kind: "invalid",
271
+ reason: "token",
272
+ error: dbError("unauthorized", "The token does not belong to the session passed to serverFor()")
273
+ });
274
+ };
275
+ const withAccounts = withExtra(base, { deleteAccount: ((userId, deleteOptions) => base.deleteAccount(userId, deleteOptions).map((result) => {
276
+ invalidate(sessionTag(userId));
277
+ return result;
278
+ })) });
279
+ return extendServer(withAccounts, {
280
+ server,
281
+ session,
282
+ serverFor,
283
+ async cached(cachedOptions = {}) {
284
+ const [view, ctx] = await Promise.all([session(), server()]);
285
+ const life = cachedOptions.life;
286
+ cacheLife({
287
+ stale: sessionStale(view, life),
288
+ ...life?.revalidate === void 0 ? {} : { revalidate: life.revalidate },
289
+ ...life?.expire === void 0 ? {} : { expire: life.expire }
290
+ });
291
+ if (view.kind === "user") cacheTag(sessionTag(view.user.id));
292
+ return withExtra(ctx, { session: view });
293
+ },
294
+ invalidateSession(userId) {
295
+ invalidate(sessionTag(userId));
296
+ },
297
+ async liveCount(spec, db) {
298
+ const result = await (db ?? (await server()).db).$run(spec);
299
+ return {
300
+ spec,
301
+ count: result.ok && typeof result.data === "number" ? result.data : null
302
+ };
303
+ },
304
+ async proxy(request, proxyOptions = {}) {
305
+ const started = performance.now();
306
+ const id = collector ? crypto.randomUUID() : void 0;
307
+ let verifyMs = 0;
308
+ const [early, resolution] = await Promise.all([proxyOptions.before?.(request), base.resolve(request, { refresh: shouldRefresh(request) }).then((resolved) => {
309
+ verifyMs = performance.now() - started;
310
+ return resolved;
311
+ })]);
312
+ const custom = await proxyOptions.protect?.(resolution.auth, request);
313
+ const initial = custom ?? early;
314
+ if (!initial && resolution.cookies.length === 0 && !id && !proxyOptions.after && !proxyOptions.serverTiming) return NextResponse.next();
315
+ let response = resolution.apply(mutableResponse(initial ?? NextResponse.next()));
316
+ if (!custom && !isRedirect(response)) {
317
+ const forwarded = {};
318
+ if (resolution.cookies.length > 0) forwarded["cookie"] = resolution.requestCookies.map((cookie) => `${cookie.name}=${encodeURIComponent(cookie.value)}`).join("; ");
319
+ if (id) forwarded[REQUEST_ID_HEADER] = id;
320
+ if (Object.keys(forwarded).length > 0 || early) overrideRequestHeaders(response, request, forwarded);
321
+ }
322
+ if (id) {
323
+ response.headers.set(REQUEST_ID_HEADER, id);
324
+ response.headers.set(STATS_URL_HEADER, `${statsRoute}?id=${id}`);
325
+ }
326
+ if (proxyOptions.after) {
327
+ const replaced = await proxyOptions.after(response, resolution.auth);
328
+ if (replaced) response = mutableResponse(replaced);
329
+ }
330
+ if (proxyOptions.serverTiming) response.headers.append("server-timing", serverTimingValue({
331
+ "bs-proxy": performance.now() - started,
332
+ "bs-verify": verifyMs
333
+ }));
334
+ return response;
335
+ },
336
+ route(handler, guardOptions = {}) {
337
+ return async (request, segment) => {
338
+ const ctx = await base.context(request, statsFor(request));
339
+ const denied = guard(ctx.auth, guardOptions.allow, guardOptions.aal);
340
+ const instance = request.nextUrl.pathname;
341
+ if (denied) return problemResponse(denied, {
342
+ instance,
343
+ expose
344
+ });
345
+ const params = await segment.params;
346
+ const response = await respond(() => handler(request, withExtra(ctx, { params })), {
347
+ instance,
348
+ expose
349
+ });
350
+ if (ctx.replica?.wrote) try {
351
+ response.headers.append("set-cookie", primaryCookie(pinMs));
352
+ } catch {}
353
+ if (collector) try {
354
+ response.headers.set(statsHeader, formatStats(ctx.stats()));
355
+ } catch {}
356
+ return response;
357
+ };
358
+ },
359
+ debugRoute() {
360
+ return async (request) => {
361
+ const id = new URL(request.url).searchParams.get("id");
362
+ if (!collector || !id) return Response.json({ error: collector ? "missing ?id=" : "debug is off" }, {
363
+ status: 404,
364
+ headers: { "cache-control": "no-store" }
365
+ });
366
+ const stats = collector.get(id) ?? EMPTY_STATS;
367
+ return Response.json(stats, { headers: {
368
+ [statsHeader]: formatStats(stats),
369
+ "cache-control": "no-store"
370
+ } });
371
+ };
372
+ },
373
+ action(actionOptions, fn) {
374
+ return async (input) => {
375
+ const ctx = await server();
376
+ const denied = guard(ctx.auth, actionOptions.allow, actionOptions.aal);
377
+ if (denied) return {
378
+ ok: false,
379
+ data: null,
380
+ error: denied
381
+ };
382
+ let parsed = input instanceof FormData ? formDataObject(input) : input;
383
+ if (actionOptions.input) {
384
+ const checked = await validate(actionOptions.input, parsed, "input");
385
+ if (!checked.ok) return {
386
+ ok: false,
387
+ data: null,
388
+ error: checked.error
389
+ };
390
+ parsed = checked.data;
391
+ }
392
+ const settled = await settle(() => fn(parsed, ctx));
393
+ if (ctx.replica?.wrote) (await cookies()).set(PRIMARY_COOKIE, String(Date.now() + pinMs), {
394
+ path: "/",
395
+ maxAge: Math.max(1, Math.ceil(pinMs / 1e3)),
396
+ httpOnly: true,
397
+ sameSite: "lax"
398
+ });
399
+ return settled.ok ? {
400
+ ok: true,
401
+ data: settled.data,
402
+ error: null
403
+ } : {
404
+ ok: false,
405
+ data: null,
406
+ error: settled.error
407
+ };
408
+ };
409
+ },
410
+ cacheTag(table, id) {
411
+ cacheTag(tagFor(table), ...id === void 0 ? [] : [tagFor(table, id)]);
412
+ },
413
+ cacheTags(target) {
414
+ const specs = isReadSet(target) ? Object.values(target.specs) : Array.isArray(target) ? target : [target];
415
+ const tags = /* @__PURE__ */ new Set();
416
+ for (const spec of specs) {
417
+ for (const table of sb.tablesOf(spec)) tags.add(tagFor(table));
418
+ const [id] = spec.args;
419
+ if (spec.method === "findById" && (typeof id === "string" || typeof id === "number")) tags.add(tagFor(spec.table, id));
420
+ }
421
+ cacheTag(...tags);
422
+ }
423
+ });
424
+ }
425
+ //#endregion
426
+ export { REQUEST_ID_HEADER, STATS_URL_HEADER, createNext, hasEntitlement, nextCache, requireAal, sessionStale, sessionTag, shouldRefresh, tagFor, toSession };
@@ -0,0 +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-DRid0qIv.js";
2
+ export { OpenApiDocument, OpenApiOptions, PROBLEM_SCHEMA, ResourceOperation, ResourceOptions, createOpenApi };