@guard-rail/core 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 (111) hide show
  1. package/README.md +339 -0
  2. package/dist/adapters/supabase/auth.d.ts +9 -0
  3. package/dist/adapters/supabase/auth.js +57 -0
  4. package/dist/adapters/supabase/auth.js.map +1 -0
  5. package/dist/adapters/supabase/client.d.ts +28 -0
  6. package/dist/adapters/supabase/client.js +44 -0
  7. package/dist/adapters/supabase/client.js.map +1 -0
  8. package/dist/adapters/supabase/confirm.d.ts +19 -0
  9. package/dist/adapters/supabase/confirm.js +27 -0
  10. package/dist/adapters/supabase/confirm.js.map +1 -0
  11. package/dist/adapters/supabase/db.d.ts +9 -0
  12. package/dist/adapters/supabase/db.js +49 -0
  13. package/dist/adapters/supabase/db.js.map +1 -0
  14. package/dist/adapters/supabase/index.d.ts +25 -0
  15. package/dist/adapters/supabase/index.js +29 -0
  16. package/dist/adapters/supabase/index.js.map +1 -0
  17. package/dist/adapters/supabase/members.d.ts +9 -0
  18. package/dist/adapters/supabase/members.js +73 -0
  19. package/dist/adapters/supabase/members.js.map +1 -0
  20. package/dist/adapters/supabase/repeaters.d.ts +9 -0
  21. package/dist/adapters/supabase/repeaters.js +73 -0
  22. package/dist/adapters/supabase/repeaters.js.map +1 -0
  23. package/dist/adapters/supabase/storage.d.ts +18 -0
  24. package/dist/adapters/supabase/storage.js +93 -0
  25. package/dist/adapters/supabase/storage.js.map +1 -0
  26. package/dist/core/adapter-types.d.ts +212 -0
  27. package/dist/core/adapter-types.js +1 -0
  28. package/dist/core/adapter-types.js.map +1 -0
  29. package/dist/core/bailout.d.ts +9 -0
  30. package/dist/core/bailout.js +14 -0
  31. package/dist/core/bailout.js.map +1 -0
  32. package/dist/core/config.d.ts +27 -0
  33. package/dist/core/config.js +46 -0
  34. package/dist/core/config.js.map +1 -0
  35. package/dist/core/defs.d.ts +22 -0
  36. package/dist/core/defs.js +16 -0
  37. package/dist/core/defs.js.map +1 -0
  38. package/dist/core/types.d.ts +136 -0
  39. package/dist/core/types.js +1 -0
  40. package/dist/core/types.js.map +1 -0
  41. package/dist/index.d.ts +13 -0
  42. package/dist/index.js +43 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/react/Admin.d.ts +8 -0
  45. package/dist/react/Admin.js +116 -0
  46. package/dist/react/Admin.js.map +1 -0
  47. package/dist/react/GRail/client.d.ts +11 -0
  48. package/dist/react/GRail/client.js +242 -0
  49. package/dist/react/GRail/client.js.map +1 -0
  50. package/dist/react/GRail/index.d.ts +12 -0
  51. package/dist/react/GRail/index.js +19 -0
  52. package/dist/react/GRail/index.js.map +1 -0
  53. package/dist/react/GRail/render.d.ts +23 -0
  54. package/dist/react/GRail/render.js +67 -0
  55. package/dist/react/GRail/render.js.map +1 -0
  56. package/dist/react/Icons.d.ts +13 -0
  57. package/dist/react/Icons.js +68 -0
  58. package/dist/react/Icons.js.map +1 -0
  59. package/dist/react/MediaPicker.d.ts +14 -0
  60. package/dist/react/MediaPicker.js +172 -0
  61. package/dist/react/MediaPicker.js.map +1 -0
  62. package/dist/react/Provider.d.ts +11 -0
  63. package/dist/react/Provider.js +26 -0
  64. package/dist/react/Provider.js.map +1 -0
  65. package/dist/react/Repeater/client.d.ts +16 -0
  66. package/dist/react/Repeater/client.js +148 -0
  67. package/dist/react/Repeater/client.js.map +1 -0
  68. package/dist/react/Repeater/index.d.ts +35 -0
  69. package/dist/react/Repeater/index.js +22 -0
  70. package/dist/react/Repeater/index.js.map +1 -0
  71. package/dist/react/Syncer.d.ts +17 -0
  72. package/dist/react/Syncer.js +23 -0
  73. package/dist/react/Syncer.js.map +1 -0
  74. package/dist/react/Team.d.ts +7 -0
  75. package/dist/react/Team.js +190 -0
  76. package/dist/react/Team.js.map +1 -0
  77. package/dist/react/resizeImage.d.ts +25 -0
  78. package/dist/react/resizeImage.js +52 -0
  79. package/dist/react/resizeImage.js.map +1 -0
  80. package/dist/react/styles.d.ts +421 -0
  81. package/dist/react/styles.js +402 -0
  82. package/dist/react/styles.js.map +1 -0
  83. package/dist/react/useAnchorRect.d.ts +17 -0
  84. package/dist/react/useAnchorRect.js +32 -0
  85. package/dist/react/useAnchorRect.js.map +1 -0
  86. package/dist/server/actions.d.ts +41 -0
  87. package/dist/server/actions.js +214 -0
  88. package/dist/server/actions.js.map +1 -0
  89. package/dist/server/data.d.ts +10 -0
  90. package/dist/server/data.js +24 -0
  91. package/dist/server/data.js.map +1 -0
  92. package/dist/server/fieldData.d.ts +26 -0
  93. package/dist/server/fieldData.js +9 -0
  94. package/dist/server/fieldData.js.map +1 -0
  95. package/dist/server/loader.d.ts +6 -0
  96. package/dist/server/loader.js +53 -0
  97. package/dist/server/loader.js.map +1 -0
  98. package/dist/server/repeaterActions.d.ts +20 -0
  99. package/dist/server/repeaterActions.js +72 -0
  100. package/dist/server/repeaterActions.js.map +1 -0
  101. package/dist/server/repeaterData.d.ts +18 -0
  102. package/dist/server/repeaterData.js +27 -0
  103. package/dist/server/repeaterData.js.map +1 -0
  104. package/dist/server/session.d.ts +14 -0
  105. package/dist/server/session.js +37 -0
  106. package/dist/server/session.js.map +1 -0
  107. package/dist/server/wrapper.d.ts +12 -0
  108. package/dist/server/wrapper.js +42 -0
  109. package/dist/server/wrapper.js.map +1 -0
  110. package/package.json +58 -0
  111. package/supabase/migrations/20260911000000_guardrail_init.sql +288 -0
package/README.md ADDED
@@ -0,0 +1,339 @@
1
+ # Guardrail
2
+
3
+ Lightweight inline content editing for developer-owned Next.js sites.
4
+
5
+ Guardrail lets a marketer edit the copy and images on a site you built, without
6
+ taking control of your markup, styling, routes, or build. There is no content
7
+ model to define and no dashboard to configure — you wrap the strings you want
8
+ editable, and that's the whole surface area.
9
+
10
+ ## How it works
11
+
12
+ - **Anonymous visitors get server-rendered HTML.** `<GRail>` is an async Server
13
+ Component; it resolves content during SSR and ships no editor JavaScript.
14
+ - **The editor only loads for a verified session.** Client components are
15
+ reached only after the auth adapter confirms a real session.
16
+ - **Un-edited fields render the JSX you wrote.** Nothing is written to the
17
+ database until an editor saves, so a fresh install renders your hardcoded
18
+ content unchanged.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pnpm add @guard-rail/core @supabase/ssr @supabase/supabase-js
24
+ ```
25
+
26
+ Set up the database:
27
+
28
+ 1. Copy `node_modules/guardrail/supabase/migrations/*.sql` into your own
29
+ project's `supabase/migrations/`, then run `supabase db push`. No Supabase
30
+ CLI yet? Paste the file's contents into the SQL editor instead — it runs
31
+ the same statements, just without the CLI's version tracking.
32
+ 2. **Required:** add `guardrail` under **Settings > API > Exposed schemas** in
33
+ the Supabase dashboard. PostgREST only serves schemas on that list; every
34
+ table below is otherwise invisible to the client with a "not found in
35
+ schema cache" error that looks identical to a missing table.
36
+
37
+ Guardrail's tables live in their own `guardrail` Postgres schema, not `public`
38
+ — so a `supabase db reset`, a broad `DROP ... CASCADE`, or a migration tool
39
+ that treats `public` as its own to reconcile can't touch them.
40
+
41
+ ```bash
42
+ SUPABASE_URL=...
43
+ SUPABASE_ANON_KEY=...
44
+ GUARDRAIL_SITE_ID=my-site # optional; defaults to "default"
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ Wrap the root layout:
50
+
51
+ ```tsx
52
+ // app/layout.tsx
53
+ import { withGuardrail } from "@guard-rail/core";
54
+
55
+ export default withGuardrail(function RootLayout({ children }) {
56
+ return (
57
+ <html lang="en">
58
+ <body>{children}</body>
59
+ </html>
60
+ );
61
+ });
62
+ ```
63
+
64
+ Mark fields editable:
65
+
66
+ ```tsx
67
+ import { GRail } from "@guard-rail/core";
68
+
69
+ <GRail id="home.hero.title" as="h1" className="text-3xl font-bold">
70
+ Ship faster with less friction
71
+ </GRail>
72
+
73
+ <GRail id="home.hero.logo" type="image" as="img" src="/logo.svg" alt="Logo" />
74
+ ```
75
+
76
+ `id` is the content key. `className` and every other prop pass straight through
77
+ to the element — Guardrail never styles your content.
78
+
79
+ To edit, visit any page with `?edit=true`, sign in with a Supabase user, and
80
+ click a field.
81
+
82
+ ## Team and roles
83
+
84
+ Two roles, deliberately not a permission matrix:
85
+
86
+ | Role | Can |
87
+ |---|---|
88
+ | `admin` | edit content, manage the team |
89
+ | `editor` | edit content |
90
+
91
+ There is no read-only "viewer" — without edit access, a member sees exactly
92
+ what an anonymous visitor sees, which is already the site's own frontend.
93
+
94
+ Membership is Guardrail's own table, not the auth provider's, so the identity
95
+ vendor and the database vendor never have to be the same. Invites are recorded
96
+ against an email address and claimed when that person first signs in.
97
+
98
+ Guardrail records an invite but does not send it — wire your own mail provider:
99
+
100
+ ```tsx
101
+ export default withGuardrail(RootLayout, {
102
+ onInvite: async ({ email, role, siteId, invitedBy }) => {
103
+ await resend.emails.send({ to: email, /* your template */ });
104
+ },
105
+ });
106
+ ```
107
+
108
+ **Invite emails (Supabase).** Inviting someone creates their account on first
109
+ use and emails them a sign-in link. Three one-time steps in the Supabase
110
+ dashboard, plus one route in your app:
111
+
112
+ 1. **Route** — `app/guardrail/confirm/route.ts`:
113
+ ```ts
114
+ export const GET = supabaseConfirmRoute({ url, anonKey });
115
+ ```
116
+ 2. **Auth → URL Configuration** — set Site URL to your app's origin, and add
117
+ `<origin>/**` to Redirect URLs.
118
+ 3. **Auth → Email Templates** — in both **Confirm signup** and **Magic Link**,
119
+ point the link at the confirm route using the token hash:
120
+ ```html
121
+ <a href="{{ .SiteURL }}/guardrail/confirm?token_hash={{ .TokenHash }}&type=email">Sign in</a>
122
+ ```
123
+ The default `{{ .ConfirmationURL }}` uses a PKCE code that only works in
124
+ the browser that requested it — the admin's, not the invitee's.
125
+ 4. **Auth → SMTP** — Supabase's built-in mailer allows only a few emails an
126
+ hour. Configure custom SMTP (Resend, Postmark, SES) before relying on
127
+ invites. Submitting the same address again re-sends a pending invite.
128
+
129
+ **Bootstrapping.** A site with no members has nobody who can add members, and
130
+ there is deliberately no in-app way to grant yourself a role — that would mean
131
+ an RLS policy letting a user write their own membership row, which is the same
132
+ as letting anyone make themselves an admin. Seed the first admin with the SQL
133
+ at the bottom of the init migration.
134
+
135
+ The bundled drawer lives behind the toolbar's **Team** button. To build your
136
+ own instead, `listTeam`, `inviteMember`, `updateMemberRole`, and `removeMember`
137
+ are exported from the package root.
138
+
139
+ ## Images and storage
140
+
141
+ Storage is its own adapter slot, separate from the database, so media can live
142
+ with a different provider than content. It is optional — without it, image
143
+ fields fall back to a plain URL input.
144
+
145
+ ```tsx
146
+ storage: supabaseStorage({
147
+ url, anonKey,
148
+ bucket: process.env.GUARDRAIL_STORAGE_BUCKET, // optional, defaults to "gr-media"
149
+ }),
150
+ ```
151
+
152
+ Editors get a picker to browse, upload, and select images. Files go straight
153
+ from the browser to storage via a signed URL — server actions cap bodies at
154
+ ~1MB, so bytes never pass through Next.js. PNG, JPEG, WebP, GIF, and AVIF up to
155
+ 10 MB; SVG is excluded because it can carry script.
156
+
157
+ Images are scaled down in the browser before upload so their longest edge is at
158
+ most 2560px — WordPress's default — and re-encoding strips EXIF data such as GPS
159
+ location. Anything already within that size uploads untouched. GIF (animation)
160
+ and AVIF (limited browser encoding) are never resized. Because this runs
161
+ client-side it is a convenience, not enforcement; the 10 MB cap still applies to
162
+ the file after resizing.
163
+
164
+ **Setup**
165
+
166
+ 1. The init migration creates the default `gr-media` bucket and storage
167
+ policies scoped to the `gr-` prefix. To use a different bucket, just pass
168
+ its name: any `gr-*` bucket is created automatically on the first upload,
169
+ no SQL edits needed.
170
+ 2. Allow the storage host in `next.config.ts`, or `next/image` will throw:
171
+ ```ts
172
+ images: {
173
+ remotePatterns: [{
174
+ protocol: "https",
175
+ hostname: new URL(process.env.SUPABASE_URL!).hostname,
176
+ pathname: "/storage/v1/object/public/**",
177
+ }],
178
+ },
179
+ ```
180
+
181
+ Alt text is stored on the field, not the file — one image can need different
182
+ alt text on different pages. Natural size is measured when an image is picked;
183
+ the developer's `width` stays fixed and height follows the new image's
184
+ proportions, so a replacement never stretches or breaks the layout.
185
+
186
+ ## Repeaters
187
+
188
+ Field editing answers "change what's here." It has no answer for "add a
189
+ thing" — a new service card, blog post, or product — because there is no JSX
190
+ for a marketer to click on. `<GRepeater>` is the one primitive for that: an
191
+ ordered list of instances of **one developer-authored template component**.
192
+ Not a schema, not a content model — the shape of an item is whatever JSX the
193
+ template renders, using ordinary `<GRail>` fields namespaced under the item's
194
+ id. Adding a field to an item means adding another `<GRail>`, same as
195
+ anywhere else.
196
+
197
+ ```tsx
198
+ // components/ServiceCard.tsx — the one place the item's shape lives
199
+ function ServiceCard({ item }: RepeaterItemProps) {
200
+ return (
201
+ <div>
202
+ <GRail id={`services.${item.id}.title`} as="h3">New service</GRail>
203
+ <GRail id={`services.${item.id}.description`}>Describe it.</GRail>
204
+ </div>
205
+ );
206
+ }
207
+
208
+ // A row of cards, all on one page:
209
+ <GRepeater id="services" as={ServiceCard} />
210
+ ```
211
+
212
+ Editors get add/remove/reorder controls; anonymous visitors get the template
213
+ rendered N times with no admin JS, same SSR/CSR split as `<GRail>` itself.
214
+
215
+ **Routed items** — a product, a blog post — additionally get their own page.
216
+ Guardrail never owns routing: pass `routed`, which prompts for a slug when an
217
+ item is added, and the developer writes an ordinary dynamic route that
218
+ resolves the item by that slug:
219
+
220
+ ```tsx
221
+ // app/products/[slug]/page.tsx
222
+ export async function generateStaticParams() {
223
+ const items = await listRepeaterItems("products");
224
+ return items.filter((i) => i.slug).map((i) => ({ slug: i.slug! }));
225
+ }
226
+
227
+ export default async function ProductPage({ params }) {
228
+ const { slug } = await params;
229
+ const item = await getRepeaterItemBySlug("products", slug);
230
+ if (!item) notFound();
231
+ return <GRail id={`products.${item.id}.name`} as="h1">New product</GRail>;
232
+ }
233
+ ```
234
+
235
+ The developer still owns the template, the route pattern, and whether the page
236
+ is static or dynamic. A repeater only ever answers "how many, and in what
237
+ order" — never "what shape."
238
+
239
+ On a routed item's own summary card, give its fields `editable={false}` so the
240
+ card stays purely a link to the item's page rather than competing with it for
241
+ clicks — editing then only happens on the canonical route, where the same
242
+ fields render fully editable with no `editable` prop needed:
243
+
244
+ ```tsx
245
+ // The card that links elsewhere: display only.
246
+ <GRail id={`products.${item.id}.name`} editable={false}>New product</GRail>
247
+
248
+ // The item's own page: same field, fully editable.
249
+ <GRail id={`products.${item.id}.name`}>New product</GRail>
250
+ ```
251
+
252
+ `editable={false}` works on any `<GRail>`, not only inside a repeater — useful
253
+ anywhere content is shown without wanting an edit affordance there.
254
+
255
+ **Not covered, on purpose:** relations between items, custom field types,
256
+ filtering/querying, commerce logic, or a marketer freely composing a page from
257
+ scratch. A visual page builder (Puck, say) is a better fit for that and can
258
+ sit alongside `<GRepeater>` later — it would decide composition, while
259
+ `<GRail>` still owns each block's leaf content.
260
+
261
+ ## Extending the toolbar
262
+
263
+ The bundled toolbar (edit indicator, Team, Log out) has one extension point:
264
+
265
+ ```tsx
266
+ withGuardrail(RootLayout, {
267
+ ...supabase,
268
+ siteId,
269
+ toolbarExtra: <a href="/orders">Orders</a>, // your JSX, your styling
270
+ });
271
+ ```
272
+
273
+ Plain content, not a component reference or a structured item list — same
274
+ "expose the primitive, developer owns the styling" rule as `onInvite`.
275
+
276
+ ## Reading a value without rendering it
277
+
278
+ `<GRail>` always renders something. For the cases where the final markup has
279
+ to be something Guardrail doesn't produce — an embed iframe's `src`, an
280
+ `og:image` meta tag, structured data — `getFieldValue` reads the same stored
281
+ value with no markup attached:
282
+
283
+ ```tsx
284
+ const video = await getFieldValue("hero.video");
285
+ // video?.href — build your own <iframe src={video.href}> from it
286
+ ```
287
+
288
+ It's read-only: give the same id a normal `<GRail>` somewhere so an editor can
289
+ change it. `getFieldValue` returns `null` until something is saved — there's
290
+ no `children` to fall back to when nothing is rendered — so supply your own
291
+ default. It shares `<GRail>`'s batching, so calling it alongside other fields
292
+ (including several via `Promise.all`) still resolves in one query.
293
+
294
+ ## Custom adapters
295
+
296
+ Guardrail defaults to Supabase. To use another backend, implement `GRDBAdapter`
297
+ and `GRAuthAdapter` and pass them to `withGuardrail`:
298
+
299
+ ```tsx
300
+ export default withGuardrail(RootLayout, {
301
+ db: new MyDBAdapter(),
302
+ auth: new MyAuthAdapter(),
303
+ siteId: "my-site",
304
+ });
305
+ ```
306
+
307
+ Only `getSession()` and `logout()` are required. `authenticate` is optional —
308
+ adapters built on magic links, SSO, or a hosted sign-in page omit it and set
309
+ `signInUrl`, and Guardrail links there instead of rendering a password form it
310
+ cannot fulfil. `inviteUser` is optional too; without it, invites fall back to
311
+ your `onInvite` callback.
312
+
313
+ ```ts
314
+ const clerkAuth: GRAuthAdapter = {
315
+ getSession: async () => { /* ... */ },
316
+ logout: async () => { /* ... */ },
317
+ signInUrl: "/sign-in", // no password form; Guardrail links here
318
+ };
319
+ ```
320
+
321
+ `getSession()` is the sole authorization check for writes. It must validate the
322
+ session against the provider — never trust the presence of a cookie.
323
+
324
+ `getOverrides(pathKeys, siteId)` is batched by design — it receives every key a
325
+ render pass needs, already deduplicated. Adapters map their own storage naming
326
+ at this boundary; core speaks `pathKey` / `siteId` only.
327
+
328
+ ## Performance
329
+
330
+ Fields are resolved through a request-scoped batching loader: every `<GRail>`
331
+ rendered in the same pass is collected and fetched in a single query. Set
332
+ `GUARDRAIL_DEBUG=1` to log how many keys each batch resolved.
333
+
334
+ ## Known limitations
335
+
336
+ - `withGuardrail` reads cookies in the root layout, which opts routes into
337
+ dynamic rendering. See the repo README for the plan.
338
+ - Saving revalidates the entire layout tree rather than the affected route.
339
+ - No drafts, revisions, or scheduled publishing. Saves are live.
@@ -0,0 +1,9 @@
1
+ import { SupabaseCredentials } from './client.js';
2
+ import { GRAuthAdapter } from '../../core/adapter-types.js';
3
+ import '@supabase/supabase-js';
4
+ import 'react';
5
+ import '../../core/types.js';
6
+
7
+ declare function supabaseAuth(creds: SupabaseCredentials): GRAuthAdapter;
8
+
9
+ export { supabaseAuth };
@@ -0,0 +1,57 @@
1
+ import { anonClient, assertCredentials, sessionClient } from "./client";
2
+ function supabaseAuth(creds) {
3
+ assertCredentials(creds);
4
+ return {
5
+ async authenticate(email, password) {
6
+ const client = await sessionClient(creds);
7
+ const { data, error } = await client.auth.signInWithPassword({
8
+ email,
9
+ password
10
+ });
11
+ if (error) return { success: false, error: error.message };
12
+ if (!data.user) return { success: false, error: "User not found." };
13
+ return {
14
+ success: true,
15
+ session: { userId: data.user.id, email: data.user.email }
16
+ };
17
+ },
18
+ /**
19
+ * Uses getUser(), which revalidates the JWT against the Supabase auth
20
+ * server. getSession() only decodes the cookie, so it would trust a forged
21
+ * token.
22
+ */
23
+ async getSession() {
24
+ const client = await sessionClient(creds);
25
+ const { data, error } = await client.auth.getUser();
26
+ if (error || !data.user) return null;
27
+ return { userId: data.user.id, email: data.user.email };
28
+ },
29
+ /**
30
+ * Sends a sign-in link that creates the account on first use.
31
+ *
32
+ * Requested through the cookie-free client on purpose. A session client
33
+ * would use PKCE and drop the code verifier into the *admin's* browser,
34
+ * so the link would fail when the invitee opened it anywhere else. The
35
+ * email template must point at the confirm route with {{ .TokenHash }},
36
+ * which verifies server-side and needs no verifier.
37
+ */
38
+ async inviteUser(email, options) {
39
+ const { error } = await anonClient(creds).auth.signInWithOtp({
40
+ email,
41
+ options: {
42
+ shouldCreateUser: true,
43
+ ...options?.redirectTo ? { emailRedirectTo: options.redirectTo } : {}
44
+ }
45
+ });
46
+ return error ? { success: false, error: error.message } : { success: true };
47
+ },
48
+ async logout() {
49
+ const client = await sessionClient(creds);
50
+ await client.auth.signOut();
51
+ }
52
+ };
53
+ }
54
+ export {
55
+ supabaseAuth
56
+ };
57
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/adapters/supabase/auth.ts"],"sourcesContent":["import { anonClient, assertCredentials, sessionClient } from \"./client\";\nimport type { SupabaseCredentials } from \"./client\";\nimport type { GRAuthAdapter, GRSession } from \"../../core/adapter-types\";\n\nexport function supabaseAuth(creds: SupabaseCredentials): GRAuthAdapter {\n assertCredentials(creds);\n\n return {\n async authenticate(email, password) {\n const client = await sessionClient(creds);\n const { data, error } = await client.auth.signInWithPassword({\n email,\n password,\n });\n\n if (error) return { success: false, error: error.message };\n if (!data.user) return { success: false, error: \"User not found.\" };\n\n return {\n success: true,\n session: { userId: data.user.id, email: data.user.email },\n };\n },\n\n /**\n * Uses getUser(), which revalidates the JWT against the Supabase auth\n * server. getSession() only decodes the cookie, so it would trust a forged\n * token.\n */\n async getSession(): Promise<GRSession | null> {\n const client = await sessionClient(creds);\n const { data, error } = await client.auth.getUser();\n if (error || !data.user) return null;\n return { userId: data.user.id, email: data.user.email };\n },\n\n /**\n * Sends a sign-in link that creates the account on first use.\n *\n * Requested through the cookie-free client on purpose. A session client\n * would use PKCE and drop the code verifier into the *admin's* browser,\n * so the link would fail when the invitee opened it anywhere else. The\n * email template must point at the confirm route with {{ .TokenHash }},\n * which verifies server-side and needs no verifier.\n */\n async inviteUser(email, options) {\n const { error } = await anonClient(creds).auth.signInWithOtp({\n email,\n options: {\n shouldCreateUser: true,\n ...(options?.redirectTo ? { emailRedirectTo: options.redirectTo } : {}),\n },\n });\n return error\n ? { success: false, error: error.message }\n : { success: true };\n },\n\n async logout() {\n const client = await sessionClient(creds);\n await client.auth.signOut();\n },\n };\n}\n"],"mappings":"AAAA,SAAS,YAAY,mBAAmB,qBAAqB;AAItD,SAAS,aAAa,OAA2C;AACtE,oBAAkB,KAAK;AAEvB,SAAO;AAAA,IACL,MAAM,aAAa,OAAO,UAAU;AAClC,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK,mBAAmB;AAAA,QAC3D;AAAA,QACA;AAAA,MACF,CAAC;AAED,UAAI,MAAO,QAAO,EAAE,SAAS,OAAO,OAAO,MAAM,QAAQ;AACzD,UAAI,CAAC,KAAK,KAAM,QAAO,EAAE,SAAS,OAAO,OAAO,kBAAkB;AAElE,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,EAAE,QAAQ,KAAK,KAAK,IAAI,OAAO,KAAK,KAAK,MAAM;AAAA,MAC1D;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,MAAM,aAAwC;AAC5C,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK,QAAQ;AAClD,UAAI,SAAS,CAAC,KAAK,KAAM,QAAO;AAChC,aAAO,EAAE,QAAQ,KAAK,KAAK,IAAI,OAAO,KAAK,KAAK,MAAM;AAAA,IACxD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAWA,MAAM,WAAW,OAAO,SAAS;AAC/B,YAAM,EAAE,MAAM,IAAI,MAAM,WAAW,KAAK,EAAE,KAAK,cAAc;AAAA,QAC3D;AAAA,QACA,SAAS;AAAA,UACP,kBAAkB;AAAA,UAClB,GAAI,SAAS,aAAa,EAAE,iBAAiB,QAAQ,WAAW,IAAI,CAAC;AAAA,QACvE;AAAA,MACF,CAAC;AACD,aAAO,QACH,EAAE,SAAS,OAAO,OAAO,MAAM,QAAQ,IACvC,EAAE,SAAS,KAAK;AAAA,IACtB;AAAA,IAEA,MAAM,SAAS;AACb,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,OAAO,KAAK,QAAQ;AAAA,IAC5B;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,28 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+
3
+ /**
4
+ * Credentials are passed in, never read from process.env here.
5
+ *
6
+ * An adapter that reaches into the environment hides its own requirements —
7
+ * nothing in the type signature says which vars must exist, so a missing or
8
+ * stale one surfaces as a runtime fetch failure. It also makes two sites on
9
+ * two different projects impossible, since the credentials become global.
10
+ */
11
+ interface SupabaseCredentials {
12
+ url: string;
13
+ anonKey: string;
14
+ }
15
+ declare function assertCredentials(creds: SupabaseCredentials): void;
16
+ /**
17
+ * Cookie-free client for reads. A single cookies() call in the render path
18
+ * opts the whole route out of static generation, so public content reads must
19
+ * never touch them.
20
+ */
21
+ declare function anonClient(creds: SupabaseCredentials): SupabaseClient;
22
+ /**
23
+ * Request-scoped client carrying the editor's auth cookies, so writes run as
24
+ * the signed-in user and RLS can enforce authorization independently.
25
+ */
26
+ declare function sessionClient(creds: SupabaseCredentials): Promise<SupabaseClient>;
27
+
28
+ export { type SupabaseCredentials, anonClient, assertCredentials, sessionClient };
@@ -0,0 +1,44 @@
1
+ import { createServerClient } from "@supabase/ssr";
2
+ import { createClient } from "@supabase/supabase-js";
3
+ import { cookies } from "next/headers";
4
+ function assertCredentials(creds) {
5
+ if (!creds?.url || !creds?.anonKey) {
6
+ throw new Error(
7
+ "Guardrail (Supabase): both `url` and `anonKey` are required."
8
+ );
9
+ }
10
+ }
11
+ const anonClients = /* @__PURE__ */ new Map();
12
+ function anonClient(creds) {
13
+ const key = `${creds.url}::${creds.anonKey}`;
14
+ let client = anonClients.get(key);
15
+ if (!client) {
16
+ client = createClient(creds.url, creds.anonKey, {
17
+ auth: { persistSession: false, autoRefreshToken: false }
18
+ });
19
+ anonClients.set(key, client);
20
+ }
21
+ return client;
22
+ }
23
+ async function sessionClient(creds) {
24
+ const cookieStore = await cookies();
25
+ return createServerClient(creds.url, creds.anonKey, {
26
+ cookies: {
27
+ getAll: () => cookieStore.getAll(),
28
+ setAll: (cookiesToSet) => {
29
+ try {
30
+ cookiesToSet.forEach(
31
+ ({ name, value, options }) => cookieStore.set(name, value, options)
32
+ );
33
+ } catch {
34
+ }
35
+ }
36
+ }
37
+ });
38
+ }
39
+ export {
40
+ anonClient,
41
+ assertCredentials,
42
+ sessionClient
43
+ };
44
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/adapters/supabase/client.ts"],"sourcesContent":["import { createServerClient } from \"@supabase/ssr\";\nimport { createClient, type SupabaseClient } from \"@supabase/supabase-js\";\nimport { cookies } from \"next/headers\";\n\n/**\n * Credentials are passed in, never read from process.env here.\n *\n * An adapter that reaches into the environment hides its own requirements —\n * nothing in the type signature says which vars must exist, so a missing or\n * stale one surfaces as a runtime fetch failure. It also makes two sites on\n * two different projects impossible, since the credentials become global.\n */\nexport interface SupabaseCredentials {\n url: string;\n anonKey: string;\n}\n\nexport function assertCredentials(creds: SupabaseCredentials): void {\n if (!creds?.url || !creds?.anonKey) {\n throw new Error(\n \"Guardrail (Supabase): both `url` and `anonKey` are required.\",\n );\n }\n}\n\nconst anonClients = new Map<string, SupabaseClient>();\n\n/**\n * Cookie-free client for reads. A single cookies() call in the render path\n * opts the whole route out of static generation, so public content reads must\n * never touch them.\n */\nexport function anonClient(creds: SupabaseCredentials): SupabaseClient {\n const key = `${creds.url}::${creds.anonKey}`;\n let client = anonClients.get(key);\n if (!client) {\n client = createClient(creds.url, creds.anonKey, {\n auth: { persistSession: false, autoRefreshToken: false },\n });\n anonClients.set(key, client);\n }\n return client;\n}\n\n/**\n * Request-scoped client carrying the editor's auth cookies, so writes run as\n * the signed-in user and RLS can enforce authorization independently.\n */\nexport async function sessionClient(\n creds: SupabaseCredentials,\n): Promise<SupabaseClient> {\n const cookieStore = await cookies();\n\n return createServerClient(creds.url, creds.anonKey, {\n cookies: {\n getAll: () => cookieStore.getAll(),\n setAll: (cookiesToSet) => {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options),\n );\n } catch {\n // Read-only cookies in a Server Component; safe to ignore when a\n // Server Action or middleware refreshes the session.\n }\n },\n },\n });\n}\n"],"mappings":"AAAA,SAAS,0BAA0B;AACnC,SAAS,oBAAyC;AAClD,SAAS,eAAe;AAejB,SAAS,kBAAkB,OAAkC;AAClE,MAAI,CAAC,OAAO,OAAO,CAAC,OAAO,SAAS;AAClC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;AAEA,MAAM,cAAc,oBAAI,IAA4B;AAO7C,SAAS,WAAW,OAA4C;AACrE,QAAM,MAAM,GAAG,MAAM,GAAG,KAAK,MAAM,OAAO;AAC1C,MAAI,SAAS,YAAY,IAAI,GAAG;AAChC,MAAI,CAAC,QAAQ;AACX,aAAS,aAAa,MAAM,KAAK,MAAM,SAAS;AAAA,MAC9C,MAAM,EAAE,gBAAgB,OAAO,kBAAkB,MAAM;AAAA,IACzD,CAAC;AACD,gBAAY,IAAI,KAAK,MAAM;AAAA,EAC7B;AACA,SAAO;AACT;AAMA,eAAsB,cACpB,OACyB;AACzB,QAAM,cAAc,MAAM,QAAQ;AAElC,SAAO,mBAAmB,MAAM,KAAK,MAAM,SAAS;AAAA,IAClD,SAAS;AAAA,MACP,QAAQ,MAAM,YAAY,OAAO;AAAA,MACjC,QAAQ,CAAC,iBAAiB;AACxB,YAAI;AACF,uBAAa;AAAA,YAAQ,CAAC,EAAE,MAAM,OAAO,QAAQ,MAC3C,YAAY,IAAI,MAAM,OAAO,OAAO;AAAA,UACtC;AAAA,QACF,QAAQ;AAAA,QAGR;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -0,0 +1,19 @@
1
+ import { SupabaseCredentials } from './client.js';
2
+ import '@supabase/supabase-js';
3
+
4
+ /**
5
+ * Route handler that turns an emailed link into a session.
6
+ *
7
+ * Mount it in the app:
8
+ * // app/guardrail/confirm/route.ts
9
+ * export const GET = supabaseConfirmRoute({ url, anonKey });
10
+ *
11
+ * verifyOtp with a token_hash works in whichever browser opens the link, unlike
12
+ * a PKCE code exchange. Once the session cookie is set, the next render claims
13
+ * any pending invitation addressed to that email.
14
+ */
15
+ declare function supabaseConfirmRoute(creds: SupabaseCredentials, options?: {
16
+ next?: string;
17
+ }): (request: Request) => Promise<Response>;
18
+
19
+ export { supabaseConfirmRoute };
@@ -0,0 +1,27 @@
1
+ import { redirect } from "next/navigation";
2
+ import { assertCredentials, sessionClient } from "./client";
3
+ function safePath(value, fallback) {
4
+ if (!value || !value.startsWith("/") || value.startsWith("//")) return fallback;
5
+ return value;
6
+ }
7
+ function supabaseConfirmRoute(creds, options = {}) {
8
+ assertCredentials(creds);
9
+ const fallback = options.next ?? "/?edit=true";
10
+ return async function GET(request) {
11
+ const url = new URL(request.url);
12
+ const tokenHash = url.searchParams.get("token_hash");
13
+ const type = url.searchParams.get("type");
14
+ const next = safePath(url.searchParams.get("next"), fallback);
15
+ if (tokenHash && type) {
16
+ const client = await sessionClient(creds);
17
+ const { error } = await client.auth.verifyOtp({ type, token_hash: tokenHash });
18
+ if (!error) redirect(next);
19
+ console.error("Guardrail (Supabase): link verification failed", error.message);
20
+ }
21
+ redirect(`${fallback}${fallback.includes("?") ? "&" : "?"}gr_auth_error=link`);
22
+ };
23
+ }
24
+ export {
25
+ supabaseConfirmRoute
26
+ };
27
+ //# sourceMappingURL=confirm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/adapters/supabase/confirm.ts"],"sourcesContent":["import type { EmailOtpType } from \"@supabase/supabase-js\";\nimport { redirect } from \"next/navigation\";\nimport { assertCredentials, sessionClient } from \"./client\";\nimport type { SupabaseCredentials } from \"./client\";\n\n/**\n * Only same-origin paths. Accepting an arbitrary `next` would make this route\n * an open redirect — a signed-in link on your domain that forwards anywhere.\n */\nfunction safePath(value: string | null, fallback: string): string {\n if (!value || !value.startsWith(\"/\") || value.startsWith(\"//\")) return fallback;\n return value;\n}\n\n/**\n * Route handler that turns an emailed link into a session.\n *\n * Mount it in the app:\n * // app/guardrail/confirm/route.ts\n * export const GET = supabaseConfirmRoute({ url, anonKey });\n *\n * verifyOtp with a token_hash works in whichever browser opens the link, unlike\n * a PKCE code exchange. Once the session cookie is set, the next render claims\n * any pending invitation addressed to that email.\n */\nexport function supabaseConfirmRoute(\n creds: SupabaseCredentials,\n options: { next?: string } = {},\n) {\n assertCredentials(creds);\n const fallback = options.next ?? \"/?edit=true\";\n\n return async function GET(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const tokenHash = url.searchParams.get(\"token_hash\");\n const type = url.searchParams.get(\"type\") as EmailOtpType | null;\n const next = safePath(url.searchParams.get(\"next\"), fallback);\n\n if (tokenHash && type) {\n const client = await sessionClient(creds);\n const { error } = await client.auth.verifyOtp({ type, token_hash: tokenHash });\n if (!error) redirect(next);\n console.error(\"Guardrail (Supabase): link verification failed\", error.message);\n }\n\n redirect(`${fallback}${fallback.includes(\"?\") ? \"&\" : \"?\"}gr_auth_error=link`);\n };\n}\n"],"mappings":"AACA,SAAS,gBAAgB;AACzB,SAAS,mBAAmB,qBAAqB;AAOjD,SAAS,SAAS,OAAsB,UAA0B;AAChE,MAAI,CAAC,SAAS,CAAC,MAAM,WAAW,GAAG,KAAK,MAAM,WAAW,IAAI,EAAG,QAAO;AACvE,SAAO;AACT;AAaO,SAAS,qBACd,OACA,UAA6B,CAAC,GAC9B;AACA,oBAAkB,KAAK;AACvB,QAAM,WAAW,QAAQ,QAAQ;AAEjC,SAAO,eAAe,IAAI,SAAqC;AAC7D,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,UAAM,YAAY,IAAI,aAAa,IAAI,YAAY;AACnD,UAAM,OAAO,IAAI,aAAa,IAAI,MAAM;AACxC,UAAM,OAAO,SAAS,IAAI,aAAa,IAAI,MAAM,GAAG,QAAQ;AAE5D,QAAI,aAAa,MAAM;AACrB,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,IAAI,MAAM,OAAO,KAAK,UAAU,EAAE,MAAM,YAAY,UAAU,CAAC;AAC7E,UAAI,CAAC,MAAO,UAAS,IAAI;AACzB,cAAQ,MAAM,kDAAkD,MAAM,OAAO;AAAA,IAC/E;AAEA,aAAS,GAAG,QAAQ,GAAG,SAAS,SAAS,GAAG,IAAI,MAAM,GAAG,oBAAoB;AAAA,EAC/E;AACF;","names":[]}
@@ -0,0 +1,9 @@
1
+ import { SupabaseCredentials } from './client.js';
2
+ import { GRDBAdapter } from '../../core/adapter-types.js';
3
+ import '@supabase/supabase-js';
4
+ import 'react';
5
+ import '../../core/types.js';
6
+
7
+ declare function supabaseDB(creds: SupabaseCredentials): GRDBAdapter;
8
+
9
+ export { supabaseDB };
@@ -0,0 +1,49 @@
1
+ import { anonClient, assertCredentials, sessionClient } from "./client";
2
+ import { GR_SCHEMA, GR_TABLE } from "../../core/defs";
3
+ function toOverrideRow(row) {
4
+ return {
5
+ id: row.id,
6
+ pathKey: row.path_key,
7
+ // Some drivers hand back jsonb columns as strings.
8
+ value: typeof row.value === "string" ? JSON.parse(row.value) : row.value
9
+ };
10
+ }
11
+ function supabaseDB(creds) {
12
+ assertCredentials(creds);
13
+ return {
14
+ async getOverrides(pathKeys, siteId) {
15
+ if (pathKeys.length === 0) return [];
16
+ const { data, error } = await anonClient(creds).schema(GR_SCHEMA).from(GR_TABLE).select("id, path_key, value").eq("site_id", siteId).in("path_key", pathKeys);
17
+ if (error) {
18
+ console.error("Guardrail (Supabase): read failed", error.message);
19
+ return [];
20
+ }
21
+ return (data ?? []).map(toOverrideRow);
22
+ },
23
+ async setOverride(row) {
24
+ const client = await sessionClient(creds);
25
+ const { error } = await client.schema(GR_SCHEMA).from(GR_TABLE).upsert(
26
+ {
27
+ path_key: row.pathKey,
28
+ site_id: row.siteId,
29
+ type: row.type,
30
+ value: row.value,
31
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
32
+ },
33
+ { onConflict: "path_key,site_id" }
34
+ );
35
+ return error ? { success: false, error: error.message } : { success: true };
36
+ },
37
+ async deleteOverrides(pathKeyPrefix, siteId) {
38
+ const client = await sessionClient(creds);
39
+ const { error } = await client.schema(GR_SCHEMA).from(GR_TABLE).delete().eq("site_id", siteId).like("path_key", `${pathKeyPrefix}%`);
40
+ if (error) {
41
+ console.error("Guardrail (Supabase): deleteOverrides failed", error.message);
42
+ }
43
+ }
44
+ };
45
+ }
46
+ export {
47
+ supabaseDB
48
+ };
49
+ //# sourceMappingURL=db.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/adapters/supabase/db.ts"],"sourcesContent":["import { anonClient, assertCredentials, sessionClient } from \"./client\";\nimport type { SupabaseCredentials } from \"./client\";\nimport { GR_SCHEMA, GR_TABLE } from \"../../core/defs\";\nimport type { GRDBAdapter, GRStorageRow } from \"../../core/adapter-types\";\nimport type { OverrideRow, OverrideValue } from \"../../core/types\";\n\n/** Shape as stored in Postgres. Column naming stops at this boundary. */\ninterface ContentRow {\n id: string;\n path_key: string;\n value: OverrideValue | string;\n}\n\nfunction toOverrideRow(row: ContentRow): OverrideRow {\n return {\n id: row.id,\n pathKey: row.path_key,\n // Some drivers hand back jsonb columns as strings.\n value:\n typeof row.value === \"string\"\n ? (JSON.parse(row.value) as OverrideValue)\n : row.value,\n };\n}\n\nexport function supabaseDB(creds: SupabaseCredentials): GRDBAdapter {\n assertCredentials(creds);\n\n return {\n async getOverrides(pathKeys, siteId) {\n if (pathKeys.length === 0) return [];\n\n // Anon client keeps the read path cookie-free so routes stay static.\n const { data, error } = await anonClient(creds)\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .select(\"id, path_key, value\")\n .eq(\"site_id\", siteId)\n .in(\"path_key\", pathKeys);\n\n if (error) {\n console.error(\"Guardrail (Supabase): read failed\", error.message);\n return [];\n }\n return ((data ?? []) as ContentRow[]).map(toOverrideRow);\n },\n\n async setOverride(row: GRStorageRow) {\n const client = await sessionClient(creds);\n const { error } = await client\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .upsert(\n {\n path_key: row.pathKey,\n site_id: row.siteId,\n type: row.type,\n value: row.value,\n updated_at: new Date().toISOString(),\n },\n { onConflict: \"path_key,site_id\" },\n );\n return error\n ? { success: false, error: error.message }\n : { success: true };\n },\n\n async deleteOverrides(pathKeyPrefix: string, siteId: string) {\n const client = await sessionClient(creds);\n const { error } = await client\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .delete()\n .eq(\"site_id\", siteId)\n .like(\"path_key\", `${pathKeyPrefix}%`);\n if (error) {\n console.error(\"Guardrail (Supabase): deleteOverrides failed\", error.message);\n }\n },\n };\n}\n"],"mappings":"AAAA,SAAS,YAAY,mBAAmB,qBAAqB;AAE7D,SAAS,WAAW,gBAAgB;AAWpC,SAAS,cAAc,KAA8B;AACnD,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,SAAS,IAAI;AAAA;AAAA,IAEb,OACE,OAAO,IAAI,UAAU,WAChB,KAAK,MAAM,IAAI,KAAK,IACrB,IAAI;AAAA,EACZ;AACF;AAEO,SAAS,WAAW,OAAyC;AAClE,oBAAkB,KAAK;AAEvB,SAAO;AAAA,IACL,MAAM,aAAa,UAAU,QAAQ;AACnC,UAAI,SAAS,WAAW,EAAG,QAAO,CAAC;AAGnC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,WAAW,KAAK,EAC3C,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb,OAAO,qBAAqB,EAC5B,GAAG,WAAW,MAAM,EACpB,GAAG,YAAY,QAAQ;AAE1B,UAAI,OAAO;AACT,gBAAQ,MAAM,qCAAqC,MAAM,OAAO;AAChE,eAAO,CAAC;AAAA,MACV;AACA,cAAS,QAAQ,CAAC,GAAoB,IAAI,aAAa;AAAA,IACzD;AAAA,IAEA,MAAM,YAAY,KAAmB;AACnC,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,IAAI,MAAM,OACrB,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb;AAAA,QACC;AAAA,UACE,UAAU,IAAI;AAAA,UACd,SAAS,IAAI;AAAA,UACb,MAAM,IAAI;AAAA,UACV,OAAO,IAAI;AAAA,UACX,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,QACrC;AAAA,QACA,EAAE,YAAY,mBAAmB;AAAA,MACnC;AACF,aAAO,QACH,EAAE,SAAS,OAAO,OAAO,MAAM,QAAQ,IACvC,EAAE,SAAS,KAAK;AAAA,IACtB;AAAA,IAEA,MAAM,gBAAgB,eAAuB,QAAgB;AAC3D,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,IAAI,MAAM,OACrB,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb,OAAO,EACP,GAAG,WAAW,MAAM,EACpB,KAAK,YAAY,GAAG,aAAa,GAAG;AACvC,UAAI,OAAO;AACT,gBAAQ,MAAM,gDAAgD,MAAM,OAAO;AAAA,MAC7E;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,25 @@
1
+ import { GRDBAdapter, GRAuthAdapter, GRMembershipAdapter, GRRepeaterAdapter } from '../../core/adapter-types.js';
2
+ export { supabaseDB } from './db.js';
3
+ export { supabaseAuth } from './auth.js';
4
+ export { supabaseMembers } from './members.js';
5
+ export { supabaseConfirmRoute } from './confirm.js';
6
+ export { DEFAULT_BUCKET, SupabaseStorageOptions, supabaseStorage } from './storage.js';
7
+ export { supabaseRepeaters } from './repeaters.js';
8
+ import { SupabaseCredentials } from './client.js';
9
+ import 'react';
10
+ import '../../core/types.js';
11
+ import '@supabase/supabase-js';
12
+
13
+ /**
14
+ * All three slots backed by one Supabase project. Use the individual factories
15
+ * instead when mixing backends — Supabase for content, another provider for
16
+ * identity, say.
17
+ */
18
+ declare function supabaseAdapters(creds: SupabaseCredentials): {
19
+ db: GRDBAdapter;
20
+ auth: GRAuthAdapter;
21
+ members: GRMembershipAdapter;
22
+ repeaters: GRRepeaterAdapter;
23
+ };
24
+
25
+ export { SupabaseCredentials, supabaseAdapters };