@farm.js/supabase 0.1.0-beta.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.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Farm.js Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @farm.js/supabase
2
+
3
+ Supabase integration for Farm.js
4
+
5
+ Farm.js is currently in beta.
6
+
7
+ ```bash
8
+ npm install @farm.js/supabase@beta
9
+ ```
10
+
11
+ See the [Farm.js repository](https://github.com/farming-labs/farm.js) for documentation, examples, and support.
@@ -0,0 +1,37 @@
1
+ export declare const supabaseAuthFormFields: {
2
+ readonly email: "email";
3
+ readonly password: "password";
4
+ readonly returnTo: "returnTo";
5
+ };
6
+ export interface SupabaseCredentials {
7
+ email: string;
8
+ password: string;
9
+ returnTo?: string;
10
+ }
11
+ export interface SupabaseOAuthInput {
12
+ provider: string;
13
+ returnTo?: string;
14
+ }
15
+ export interface SupabaseRedirectResult {
16
+ redirectTo: string;
17
+ }
18
+ export interface SupabaseSignUpResult extends SupabaseRedirectResult {
19
+ emailConfirmationRequired?: boolean;
20
+ email?: string;
21
+ message?: string;
22
+ }
23
+ export interface SupabaseSessionResult {
24
+ authenticated: boolean;
25
+ user?: Record<string, unknown>;
26
+ }
27
+ export interface SupabaseLogoutInput {
28
+ returnTo?: string;
29
+ }
30
+ export declare const supabaseClient: {
31
+ login: import("@farm.js/core/client").FarmIntegrationAPIOperation<SupabaseCredentials, never, SupabaseRedirectResult, false, "POST">;
32
+ signup: import("@farm.js/core/client").FarmIntegrationAPIOperation<SupabaseCredentials, never, SupabaseSignUpResult, false, "POST">;
33
+ oauth: import("@farm.js/core/client").FarmIntegrationAPIOperation<never, SupabaseOAuthInput, SupabaseRedirectResult, false, "GET">;
34
+ logout: import("@farm.js/core/client").FarmIntegrationAPIOperation<SupabaseLogoutInput, never, SupabaseRedirectResult, false, "POST">;
35
+ session: import("@farm.js/core/client").FarmIntegrationAPIOperation<never, never, SupabaseSessionResult, false, "GET">;
36
+ };
37
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,sBAAsB;;;;CAIzB,CAAC;AAEX,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAqB,SAAQ,sBAAsB;IAClE,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,cAAc;;;;;;CAU1B,CAAC"}
package/dist/client.js ADDED
@@ -0,0 +1,17 @@
1
+ import { api } from "@farm.js/core/client";
2
+ export const supabaseAuthFormFields = {
3
+ email: "email",
4
+ password: "password",
5
+ returnTo: "returnTo",
6
+ };
7
+ export const supabaseClient = {
8
+ login: api.post("/auth/login"),
9
+ signup: api.post("/auth/signup"),
10
+ oauth: api.get("/auth/login", {
11
+ responseFormat: "json",
12
+ }),
13
+ logout: api.post("/auth/logout"),
14
+ session: api.get("/auth/session", {
15
+ responseFormat: "json",
16
+ }),
17
+ };
@@ -0,0 +1,135 @@
1
+ import { type FarmIntegrationHandlerContext, type FarmIntegrationLogger } from "@farm.js/core";
2
+ export interface SupabaseIntegrationPages {
3
+ signIn?: string;
4
+ signUp?: string;
5
+ }
6
+ export interface SupabaseIntegrationInput {
7
+ url?: string;
8
+ anonKey?: string;
9
+ appBaseUrl?: string;
10
+ callbackUrl?: string;
11
+ loginPath?: string;
12
+ signupPath?: string;
13
+ callbackPath?: string;
14
+ logoutPath?: string;
15
+ sessionPath?: string;
16
+ protectedRoutes?: string | string[];
17
+ providers?: string[];
18
+ defaultProvider?: string;
19
+ pages?: SupabaseIntegrationPages;
20
+ log?: FarmIntegrationLogger;
21
+ }
22
+ interface ResolvedSupabaseEnv {
23
+ url: string;
24
+ anonKey: string;
25
+ appBaseUrl?: string;
26
+ }
27
+ interface ResolvedSupabasePages {
28
+ signIn?: string;
29
+ signUp?: string;
30
+ }
31
+ export declare function supabase(input?: SupabaseIntegrationInput): import("@farm.js/core").DefinedIntegration<{
32
+ category: "auth";
33
+ type: string;
34
+ instance: {
35
+ url: string;
36
+ protectedRoutes: string[];
37
+ providers: string[];
38
+ pages: ResolvedSupabasePages;
39
+ };
40
+ config: import("@farm.js/core").FarmIntegrationConfigDefinition<ResolvedSupabaseEnv, import("@farm.js/core").FarmIntegrationSchema | undefined>;
41
+ api: {
42
+ [x: string]: {
43
+ post: {
44
+ readonly kind: "farm-integration-api-operation";
45
+ path: string;
46
+ method: "POST";
47
+ bodyFormat?: import("@farm.js/core/client").FarmIntegrationAPIBodyFormat | undefined;
48
+ responseFormat?: import("@farm.js/core/client").FarmIntegrationAPIResponseFormat | undefined;
49
+ headers?: {
50
+ [x: string]: string;
51
+ } | undefined;
52
+ credentials?: RequestCredentials | undefined;
53
+ isServer?: false | undefined;
54
+ __pathless?: boolean | undefined;
55
+ __types?: {
56
+ body: {
57
+ email: string;
58
+ password: string;
59
+ returnTo?: string | undefined;
60
+ };
61
+ query: never;
62
+ response: {
63
+ redirectTo: string;
64
+ emailConfirmationRequired?: boolean | undefined;
65
+ email?: string | undefined;
66
+ message?: string | undefined;
67
+ };
68
+ } | undefined;
69
+ };
70
+ get: {
71
+ readonly kind: "farm-integration-api-operation";
72
+ path: string;
73
+ method: "GET";
74
+ bodyFormat?: import("@farm.js/core/client").FarmIntegrationAPIBodyFormat | undefined;
75
+ responseFormat?: import("@farm.js/core/client").FarmIntegrationAPIResponseFormat | undefined;
76
+ headers?: {
77
+ [x: string]: string;
78
+ } | undefined;
79
+ credentials?: RequestCredentials | undefined;
80
+ isServer?: false | undefined;
81
+ __pathless?: boolean | undefined;
82
+ __types?: {
83
+ body: never;
84
+ query: never;
85
+ response: {
86
+ authenticated: boolean;
87
+ user?: {
88
+ [x: string]: unknown;
89
+ } | undefined;
90
+ };
91
+ } | undefined;
92
+ };
93
+ };
94
+ oauth: {
95
+ get: {
96
+ readonly kind: "farm-integration-api-operation";
97
+ path: string;
98
+ method: "GET";
99
+ bodyFormat?: import("@farm.js/core/client").FarmIntegrationAPIBodyFormat | undefined;
100
+ responseFormat?: import("@farm.js/core/client").FarmIntegrationAPIResponseFormat | undefined;
101
+ headers?: {
102
+ [x: string]: string;
103
+ } | undefined;
104
+ credentials?: RequestCredentials | undefined;
105
+ isServer?: false | undefined;
106
+ __pathless?: boolean | undefined;
107
+ __types?: {
108
+ body: never;
109
+ query: {
110
+ provider: string;
111
+ returnTo?: string | undefined;
112
+ };
113
+ response: {
114
+ redirectTo: string;
115
+ };
116
+ } | undefined;
117
+ };
118
+ };
119
+ };
120
+ log: FarmIntegrationLogger | undefined;
121
+ documentNavigations: {
122
+ matcher: string[];
123
+ }[];
124
+ routes: {
125
+ path: string;
126
+ methods: ("POST" | "GET")[];
127
+ handler(_request: Request, context: FarmIntegrationHandlerContext): Promise<Response>;
128
+ }[];
129
+ middleware: {
130
+ matcher: string[];
131
+ handler(_request: Request, context: FarmIntegrationHandlerContext): Promise<Response | undefined>;
132
+ }[];
133
+ }>;
134
+ export {};
135
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,EAC3B,MAAM,eAAe,CAAC;AAkCvB,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,wBAAwB,CAAC;IACjC,GAAG,CAAC,EAAE,qBAAqB,CAAC;CAC7B;AAED,UAAU,mBAAmB;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,qBAAqB;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA6tBD,wBAAgB,QAAQ,CAAC,KAAK,GAAE,wBAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BAuE7B,OAAO,WAAW,6BAA6B;;;;0BAkbzC,OAAO,WAAW,6BAA6B;;GAkBpF"}
package/dist/index.js ADDED
@@ -0,0 +1,1053 @@
1
+ import { createServerClient } from "@supabase/ssr";
2
+ import { defineIntegration, } from "@farm.js/core";
3
+ import { api as clientApi } from "@farm.js/core/client";
4
+ import { createPathInferredClientApi, createDocumentNavigationMatchers, escapeHtml, getOrigin, getReturnTo, integrationConfig, normalizeMatchers, parseCookieHeaderList, resolveAppPath, resolveCallbackSettings, toAbsoluteUrl, withSearchParams, } from "@farm.js/integration-utils";
5
+ import { supabaseAuthFormFields, } from "./client.js";
6
+ function createSupabaseApi(input = {}) {
7
+ const loginPath = input.loginPath ?? "/auth/login";
8
+ const signupPath = input.signupPath ?? "/auth/signup";
9
+ const logoutPath = input.logoutPath ?? "/auth/logout";
10
+ const sessionPath = input.sessionPath ?? "/auth/session";
11
+ return createPathInferredClientApi({
12
+ path: loginPath,
13
+ operation: clientApi.post(loginPath),
14
+ }, {
15
+ path: signupPath,
16
+ operation: clientApi.post(signupPath),
17
+ }, {
18
+ path: loginPath,
19
+ leafName: "oauth",
20
+ operation: clientApi.get(loginPath, {
21
+ responseFormat: "json",
22
+ }),
23
+ }, {
24
+ path: logoutPath,
25
+ operation: clientApi.post(logoutPath),
26
+ }, {
27
+ path: sessionPath,
28
+ operation: clientApi.get(sessionPath, {
29
+ responseFormat: "json",
30
+ }),
31
+ });
32
+ }
33
+ function resolveEnv(input) {
34
+ const url = input.url ?? process.env.SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL ?? "";
35
+ const anonKey = input.anonKey ??
36
+ process.env.SUPABASE_ANON_KEY ??
37
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ??
38
+ process.env.SUPABASE_PUBLISHABLE_KEY ??
39
+ process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY ??
40
+ process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY ??
41
+ "";
42
+ const appBaseUrl = input.appBaseUrl ?? process.env.APP_BASE_URL ?? undefined;
43
+ if (!url || !anonKey) {
44
+ throw new Error("Supabase integration requires SUPABASE_URL (or NEXT_PUBLIC_SUPABASE_URL) and SUPABASE_ANON_KEY (or a Supabase publishable key env).");
45
+ }
46
+ return {
47
+ url,
48
+ anonKey,
49
+ appBaseUrl,
50
+ };
51
+ }
52
+ function resolvePages(input) {
53
+ return {
54
+ signIn: resolveAppPath(input.pages?.signIn, "Supabase integration pages.signIn"),
55
+ signUp: resolveAppPath(input.pages?.signUp, "Supabase integration pages.signUp"),
56
+ };
57
+ }
58
+ function serializeCookie(name, value, options = {}) {
59
+ const parts = [`${name}=${encodeURIComponent(value)}`];
60
+ if (options.maxAge != null) {
61
+ parts.push(`Max-Age=${Math.floor(options.maxAge)}`);
62
+ }
63
+ if (options.domain) {
64
+ parts.push(`Domain=${options.domain}`);
65
+ }
66
+ parts.push(`Path=${options.path || "/"}`);
67
+ if (options.expires) {
68
+ parts.push(`Expires=${options.expires.toUTCString()}`);
69
+ }
70
+ if (options.httpOnly) {
71
+ parts.push("HttpOnly");
72
+ }
73
+ if (options.secure) {
74
+ parts.push("Secure");
75
+ }
76
+ if (options.sameSite) {
77
+ const sameSite = typeof options.sameSite === "string"
78
+ ? options.sameSite
79
+ : options.sameSite === true
80
+ ? "Strict"
81
+ : undefined;
82
+ if (sameSite) {
83
+ parts.push(`SameSite=${sameSite}`);
84
+ }
85
+ }
86
+ return parts.join("; ");
87
+ }
88
+ function appendSetCookies(headers, cookies) {
89
+ for (const cookie of cookies) {
90
+ headers.append("set-cookie", cookie);
91
+ }
92
+ }
93
+ function formatProviderLabel(provider) {
94
+ return provider
95
+ .split(/[-_ ]+/)
96
+ .filter(Boolean)
97
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
98
+ .join(" ");
99
+ }
100
+ function redirectToPage(pagePath, context, configuredBaseUrl, params) {
101
+ const targetUrl = withSearchParams(toAbsoluteUrl(pagePath, context.request, configuredBaseUrl), params);
102
+ return Response.redirect(targetUrl, 302);
103
+ }
104
+ function readPageFeedback(context) {
105
+ const error = context.url.searchParams.get("error");
106
+ if (error) {
107
+ return {
108
+ tone: "error",
109
+ message: normalizeSupabaseMessage(error),
110
+ };
111
+ }
112
+ const message = context.url.searchParams.get("message");
113
+ if (!message) {
114
+ return undefined;
115
+ }
116
+ if (message === "check-email") {
117
+ const email = context.url.searchParams.get("email");
118
+ return {
119
+ tone: "success",
120
+ message: email
121
+ ? `Check ${email} to confirm your account, then sign in.`
122
+ : "Check your email to confirm your account, then sign in.",
123
+ };
124
+ }
125
+ return {
126
+ tone: "success",
127
+ message,
128
+ };
129
+ }
130
+ function normalizeSupabaseMessage(message) {
131
+ if (/email rate limit exceeded/i.test(message)) {
132
+ return "Email sending is rate-limited for this Supabase project right now. Wait a bit, use an existing account, or disable email confirmation for local development.";
133
+ }
134
+ if (/unsupported provider/i.test(message)) {
135
+ return "This Supabase provider is not enabled on the current project.";
136
+ }
137
+ return message;
138
+ }
139
+ function isIntegrationClientRequest(request) {
140
+ return request.headers.get("x-farm-integration-client") === "1";
141
+ }
142
+ function jsonSuccess(data, headers) {
143
+ return Response.json(data, {
144
+ status: 200,
145
+ headers,
146
+ });
147
+ }
148
+ function jsonError(message, status = 400, headers) {
149
+ return Response.json({
150
+ error: normalizeSupabaseMessage(message),
151
+ }, {
152
+ status,
153
+ headers,
154
+ });
155
+ }
156
+ function renderAuthPage(input) {
157
+ const requestUrl = input.context.url;
158
+ const returnTo = getReturnTo(requestUrl.searchParams.get("returnTo"), "/dashboard");
159
+ const title = input.mode === "sign-in" ? "Sign in" : "Sign up";
160
+ if (input.mode === "sign-in" && input.defaultProvider) {
161
+ const redirectUrl = new URL(input.loginPath, requestUrl.origin);
162
+ redirectUrl.searchParams.set("provider", input.defaultProvider);
163
+ redirectUrl.searchParams.set("returnTo", returnTo);
164
+ return Response.redirect(redirectUrl, 302);
165
+ }
166
+ const providerButtons = input.providers
167
+ .map((provider) => {
168
+ const href = new URL(input.loginPath, requestUrl.origin);
169
+ href.searchParams.set("provider", provider);
170
+ href.searchParams.set("returnTo", returnTo);
171
+ return `<a class="provider-link" href="${escapeHtml(href.toString())}">
172
+ <span class="provider-mark">${escapeHtml(provider.charAt(0).toUpperCase())}</span>
173
+ <span>Continue with ${escapeHtml(formatProviderLabel(provider))}</span>
174
+ </a>`;
175
+ })
176
+ .join("");
177
+ const feedbackBlock = input.feedback
178
+ ? `<div class="notice notice-${input.feedback.tone}">${escapeHtml(input.feedback.message)}</div>`
179
+ : "";
180
+ const providerSection = providerButtons.length > 0
181
+ ? `<div class="providers">
182
+ <div class="section-divider"><span>Or continue with</span></div>
183
+ <div class="provider-stack">${providerButtons}</div>
184
+ </div>`
185
+ : "";
186
+ const signInUrl = new URL(input.signInViewPath, requestUrl.origin);
187
+ signInUrl.searchParams.set("returnTo", returnTo);
188
+ const signUpUrl = new URL(input.signUpViewPath, requestUrl.origin);
189
+ signUpUrl.searchParams.set("returnTo", returnTo);
190
+ const submitLabel = input.mode === "sign-in" ? "Sign in" : "Create account";
191
+ const returnLabel = returnTo === "/dashboard" ? "Continue to /dashboard" : `Continue to ${returnTo}`;
192
+ const html = `<!doctype html>
193
+ <html lang="en">
194
+ <head>
195
+ <meta charset="utf-8" />
196
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
197
+ <title>Supabase ${input.mode === "sign-in" ? "Sign In" : "Sign Up"}</title>
198
+ <style>
199
+ :root {
200
+ color-scheme: light;
201
+ --bg: #f8fafc;
202
+ --ink: #0f172a;
203
+ --muted: #475569;
204
+ --line: #dbe3ef;
205
+ --card: #ffffff;
206
+ --panel: #09111f;
207
+ --panel-ink: #dce8f7;
208
+ --accent: #3ecf8e;
209
+ --accent-deep: #1a8f5b;
210
+ --error-bg: #fef2f2;
211
+ --error-ink: #991b1b;
212
+ --success-bg: #ecfdf5;
213
+ --success-ink: #166534;
214
+ }
215
+ * { box-sizing: border-box; }
216
+ body {
217
+ margin: 0;
218
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
219
+ background:
220
+ radial-gradient(circle at top left, rgba(62, 207, 142, 0.22), transparent 26%),
221
+ linear-gradient(180deg, #050a14 0%, #09111f 42%, #f8fafc 42%, #f8fafc 100%);
222
+ color: var(--ink);
223
+ }
224
+ main {
225
+ min-height: 100vh;
226
+ display: grid;
227
+ place-items: center;
228
+ padding: 32px;
229
+ }
230
+ .shell {
231
+ width: min(1080px, 100%);
232
+ display: grid;
233
+ grid-template-columns: minmax(0, 1.15fr) minmax(340px, 430px);
234
+ gap: 28px;
235
+ align-items: stretch;
236
+ }
237
+ .hero {
238
+ min-height: 580px;
239
+ padding: 40px;
240
+ border-radius: 32px;
241
+ background:
242
+ radial-gradient(circle at top right, rgba(62, 207, 142, 0.2), transparent 28%),
243
+ linear-gradient(155deg, #0c1728 0%, #050a14 100%);
244
+ color: var(--panel-ink);
245
+ box-shadow: 0 28px 70px rgba(2, 6, 23, 0.32);
246
+ display: flex;
247
+ flex-direction: column;
248
+ justify-content: flex-end;
249
+ }
250
+ .badge {
251
+ display: inline-flex;
252
+ align-items: center;
253
+ gap: 10px;
254
+ padding: 10px 14px;
255
+ border-radius: 999px;
256
+ background: rgba(148, 163, 184, 0.12);
257
+ color: #dff7ea;
258
+ font-size: 13px;
259
+ font-weight: 700;
260
+ letter-spacing: 0.08em;
261
+ text-transform: uppercase;
262
+ }
263
+ .badge-mark {
264
+ width: 12px;
265
+ height: 12px;
266
+ border-radius: 4px;
267
+ background: linear-gradient(135deg, #3ecf8e 0%, #1a8f5b 100%);
268
+ transform: skew(-12deg);
269
+ }
270
+ .hero h1 {
271
+ margin: 0 0 18px;
272
+ font-size: clamp(44px, 7vw, 72px);
273
+ line-height: 0.98;
274
+ letter-spacing: -0.05em;
275
+ color: #f8fafc;
276
+ }
277
+ .hero p {
278
+ margin: 0;
279
+ max-width: 420px;
280
+ color: rgba(220, 232, 247, 0.82);
281
+ font-size: 15px;
282
+ }
283
+ .card {
284
+ background: var(--card);
285
+ border-radius: 32px;
286
+ padding: 28px;
287
+ box-shadow: 0 26px 60px rgba(15, 23, 42, 0.12);
288
+ display: flex;
289
+ flex-direction: column;
290
+ min-height: 580px;
291
+ }
292
+ .tabs {
293
+ display: grid;
294
+ grid-template-columns: repeat(2, 1fr);
295
+ gap: 8px;
296
+ padding: 6px;
297
+ border-radius: 999px;
298
+ background: #eef4fb;
299
+ margin-bottom: 24px;
300
+ }
301
+ .tab {
302
+ padding: 11px 14px;
303
+ border-radius: 999px;
304
+ text-align: center;
305
+ color: #526072;
306
+ text-decoration: none;
307
+ font-weight: 700;
308
+ }
309
+ .tab-active {
310
+ background: #ffffff;
311
+ color: #0f172a;
312
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
313
+ }
314
+ .eyebrow {
315
+ margin: 0 0 8px;
316
+ color: var(--accent-deep);
317
+ font-size: 12px;
318
+ font-weight: 800;
319
+ letter-spacing: 0.12em;
320
+ text-transform: uppercase;
321
+ }
322
+ .card h2 {
323
+ margin: 0 0 18px;
324
+ font-size: 30px;
325
+ line-height: 1.05;
326
+ letter-spacing: -0.04em;
327
+ }
328
+ .notice {
329
+ margin: 0 0 18px;
330
+ padding: 14px 16px;
331
+ border-radius: 18px;
332
+ font-weight: 600;
333
+ line-height: 1.55;
334
+ }
335
+ .notice-error {
336
+ background: var(--error-bg);
337
+ color: var(--error-ink);
338
+ }
339
+ .notice-success {
340
+ background: var(--success-bg);
341
+ color: var(--success-ink);
342
+ }
343
+ form {
344
+ display: grid;
345
+ gap: 14px;
346
+ }
347
+ label {
348
+ display: grid;
349
+ gap: 7px;
350
+ font-weight: 700;
351
+ color: #0f172a;
352
+ }
353
+ input {
354
+ width: 100%;
355
+ border: 1px solid var(--line);
356
+ border-radius: 16px;
357
+ padding: 14px 16px;
358
+ font: inherit;
359
+ color: #0f172a;
360
+ background: #fbfdff;
361
+ outline: none;
362
+ }
363
+ input:focus {
364
+ border-color: #73e1b0;
365
+ box-shadow: 0 0 0 4px rgba(62, 207, 142, 0.12);
366
+ }
367
+ .submit {
368
+ margin-top: 6px;
369
+ border: 0;
370
+ border-radius: 16px;
371
+ padding: 15px 18px;
372
+ background: linear-gradient(135deg, #3ecf8e 0%, #2ba26e 100%);
373
+ color: #03110a;
374
+ font-weight: 800;
375
+ font-size: 15px;
376
+ cursor: pointer;
377
+ }
378
+ .submit:hover {
379
+ filter: brightness(1.02);
380
+ }
381
+ .section-divider {
382
+ display: grid;
383
+ grid-template-columns: 1fr auto 1fr;
384
+ align-items: center;
385
+ gap: 14px;
386
+ margin: 18px 0 16px;
387
+ color: #94a3b8;
388
+ font-size: 12px;
389
+ font-weight: 700;
390
+ letter-spacing: 0.08em;
391
+ text-transform: uppercase;
392
+ }
393
+ .section-divider::before,
394
+ .section-divider::after {
395
+ content: "";
396
+ height: 1px;
397
+ background: var(--line);
398
+ }
399
+ .provider-stack {
400
+ display: grid;
401
+ gap: 10px;
402
+ }
403
+ .provider-link {
404
+ display: flex;
405
+ align-items: center;
406
+ gap: 12px;
407
+ padding: 14px 16px;
408
+ border-radius: 16px;
409
+ border: 1px solid var(--line);
410
+ color: var(--ink);
411
+ text-decoration: none;
412
+ font-weight: 700;
413
+ background: #ffffff;
414
+ }
415
+ .provider-link:hover {
416
+ background: #f8fafc;
417
+ }
418
+ .provider-mark {
419
+ width: 34px;
420
+ height: 34px;
421
+ border-radius: 12px;
422
+ display: grid;
423
+ place-items: center;
424
+ background: #eef4fb;
425
+ color: #0f172a;
426
+ font-size: 15px;
427
+ font-weight: 800;
428
+ }
429
+ code {
430
+ background: #eff6ff;
431
+ color: #0f172a;
432
+ border-radius: 10px;
433
+ padding: 2px 8px;
434
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
435
+ font-size: 12px;
436
+ }
437
+ @media (max-width: 920px) {
438
+ main {
439
+ padding: 20px;
440
+ }
441
+ .shell {
442
+ grid-template-columns: 1fr;
443
+ }
444
+ .hero,
445
+ .card {
446
+ min-height: unset;
447
+ }
448
+ }
449
+ </style>
450
+ </head>
451
+ <body>
452
+ <main>
453
+ <section class="shell">
454
+ <section class="hero">
455
+ <div>
456
+ <span class="badge"><span class="badge-mark"></span> Supabase Auth</span>
457
+ <h1>Secure access, server-first.</h1>
458
+ <p>${escapeHtml(returnLabel)}</p>
459
+ </div>
460
+ </section>
461
+
462
+ <section class="card">
463
+ <nav class="tabs" aria-label="Auth mode">
464
+ <a class="tab ${input.mode === "sign-in" ? "tab-active" : ""}" href="${escapeHtml(signInUrl.toString())}">Sign in</a>
465
+ <a class="tab ${input.mode === "sign-up" ? "tab-active" : ""}" href="${escapeHtml(signUpUrl.toString())}">Sign up</a>
466
+ </nav>
467
+
468
+ <p class="eyebrow">Supabase x Farm</p>
469
+ <h2>${escapeHtml(title)}</h2>
470
+ ${feedbackBlock}
471
+
472
+ <form method="post" action="${escapeHtml(input.mode === "sign-in" ? input.loginPath : input.signupPath)}">
473
+ <input type="hidden" name="${supabaseAuthFormFields.returnTo}" value="${escapeHtml(returnTo)}" />
474
+ <label>
475
+ <span>Email</span>
476
+ <input name="${supabaseAuthFormFields.email}" type="email" required autocomplete="email" />
477
+ </label>
478
+ <label>
479
+ <span>Password</span>
480
+ <input name="${supabaseAuthFormFields.password}" type="password" required autocomplete="${input.mode === "sign-in" ? "current-password" : "new-password"}" />
481
+ </label>
482
+ <button class="submit" type="submit">${escapeHtml(submitLabel)}</button>
483
+ </form>
484
+
485
+ ${providerSection}
486
+ </section>
487
+ </section>
488
+ </main>
489
+ </body>
490
+ </html>`;
491
+ return new Response(html, {
492
+ status: 200,
493
+ headers: {
494
+ "content-type": "text/html; charset=utf-8",
495
+ },
496
+ });
497
+ }
498
+ function renderCheckEmailPage(context, signInViewPath, email, returnTo) {
499
+ const requestUrl = context.url;
500
+ const signInUrl = new URL(signInViewPath, requestUrl.origin);
501
+ signInUrl.searchParams.set("returnTo", returnTo);
502
+ const html = `<!doctype html>
503
+ <html lang="en">
504
+ <head>
505
+ <meta charset="utf-8" />
506
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
507
+ <title>Check your email</title>
508
+ <style>
509
+ body {
510
+ margin: 0;
511
+ min-height: 100vh;
512
+ display: grid;
513
+ place-items: center;
514
+ padding: 24px;
515
+ background:
516
+ radial-gradient(circle at top left, rgba(62, 207, 142, 0.18), transparent 25%),
517
+ linear-gradient(180deg, #07111d 0%, #0f172a 38%, #f8fafc 38%, #f8fafc 100%);
518
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
519
+ color: #0f172a;
520
+ }
521
+ section {
522
+ width: min(520px, 100%);
523
+ border-radius: 30px;
524
+ background: #ffffff;
525
+ padding: 30px;
526
+ box-shadow: 0 26px 60px rgba(15, 23, 42, 0.16);
527
+ }
528
+ p:first-child {
529
+ margin: 0 0 10px;
530
+ color: #15803d;
531
+ font-size: 12px;
532
+ font-weight: 800;
533
+ letter-spacing: 0.12em;
534
+ text-transform: uppercase;
535
+ }
536
+ h1 {
537
+ margin: 0 0 12px;
538
+ font-size: 34px;
539
+ line-height: 1.05;
540
+ letter-spacing: -0.04em;
541
+ }
542
+ p {
543
+ margin: 0 0 16px;
544
+ color: #475569;
545
+ line-height: 1.7;
546
+ }
547
+ a {
548
+ color: #0f172a;
549
+ font-weight: 800;
550
+ text-decoration: none;
551
+ }
552
+ code {
553
+ background: #eff6ff;
554
+ color: #0f172a;
555
+ border-radius: 10px;
556
+ padding: 2px 8px;
557
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
558
+ font-size: 12px;
559
+ }
560
+ </style>
561
+ </head>
562
+ <body>
563
+ <section>
564
+ <p>Supabase Auth</p>
565
+ <h1>Check your email</h1>
566
+ <p>Supabase created the account for <strong>${escapeHtml(email)}</strong>.</p>
567
+ <p>Complete the email confirmation flow for this project, then sign in again to create a server session and reach <code>${escapeHtml(returnTo)}</code>.</p>
568
+ <a href="${escapeHtml(signInUrl.toString())}">Back to sign in</a>
569
+ </section>
570
+ </body>
571
+ </html>`;
572
+ return new Response(html, {
573
+ status: 200,
574
+ headers: {
575
+ "content-type": "text/html; charset=utf-8",
576
+ },
577
+ });
578
+ }
579
+ function createSupabaseHandler(context, env) {
580
+ const setCookies = [];
581
+ const supabase = createServerClient(env.url, env.anonKey, {
582
+ auth: {
583
+ flowType: "pkce",
584
+ detectSessionInUrl: false,
585
+ },
586
+ cookies: {
587
+ getAll() {
588
+ return parseCookieHeaderList(context.request.headers.get("cookie"));
589
+ },
590
+ setAll(cookiesToSet) {
591
+ for (const cookie of cookiesToSet) {
592
+ setCookies.push(serializeCookie(cookie.name, cookie.value, cookie.options));
593
+ }
594
+ context.req.set("supabase:set-cookies", [...setCookies]);
595
+ },
596
+ },
597
+ });
598
+ return {
599
+ supabase,
600
+ setCookies,
601
+ };
602
+ }
603
+ async function parseEmailPasswordRequest(request) {
604
+ const contentType = request.headers.get("content-type") || "";
605
+ let payload = {};
606
+ if (contentType.includes("application/json")) {
607
+ payload = (await request.json()) || {};
608
+ }
609
+ else {
610
+ const formData = await request.formData();
611
+ formData.forEach((value, key) => {
612
+ payload[key] = value;
613
+ });
614
+ }
615
+ const email = String(payload[supabaseAuthFormFields.email] || "").trim();
616
+ const password = String(payload[supabaseAuthFormFields.password] || "");
617
+ const returnTo = getReturnTo(String(payload[supabaseAuthFormFields.returnTo] || ""), "/dashboard");
618
+ if (!email || !password) {
619
+ return {
620
+ ok: false,
621
+ message: "Email and password are required.",
622
+ returnTo,
623
+ email,
624
+ };
625
+ }
626
+ return {
627
+ ok: true,
628
+ email,
629
+ password,
630
+ returnTo,
631
+ };
632
+ }
633
+ export function supabase(input = {}) {
634
+ const env = resolveEnv(input);
635
+ const pages = resolvePages(input);
636
+ const providers = input.providers || [];
637
+ const protectedMatchers = normalizeMatchers(input.protectedRoutes);
638
+ const loginPath = input.loginPath ?? "/auth/login";
639
+ const signupPath = input.signupPath ?? "/auth/signup";
640
+ const logoutPath = input.logoutPath ?? "/auth/logout";
641
+ const sessionPath = input.sessionPath ?? "/auth/session";
642
+ const signInViewPath = pages.signIn ?? loginPath;
643
+ const signUpViewPath = pages.signUp ?? signupPath;
644
+ const callbackSettings = resolveCallbackSettings({
645
+ callbackUrl: input.callbackUrl,
646
+ callbackPath: input.callbackPath,
647
+ defaultPath: "/auth/callback",
648
+ label: "Supabase integration",
649
+ }, env.appBaseUrl);
650
+ const callbackPath = callbackSettings.callbackPath;
651
+ const api = createSupabaseApi({
652
+ loginPath,
653
+ signupPath,
654
+ logoutPath,
655
+ sessionPath,
656
+ });
657
+ return defineIntegration({
658
+ category: "auth",
659
+ type: "supabase",
660
+ instance: {
661
+ url: env.url,
662
+ protectedRoutes: protectedMatchers,
663
+ providers,
664
+ pages,
665
+ },
666
+ config: integrationConfig({
667
+ label: "Supabase integration",
668
+ env: {
669
+ url: ["SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_URL"],
670
+ anonKey: [
671
+ "SUPABASE_ANON_KEY",
672
+ "NEXT_PUBLIC_SUPABASE_ANON_KEY",
673
+ "SUPABASE_PUBLISHABLE_KEY",
674
+ "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY",
675
+ "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY",
676
+ ],
677
+ appBaseUrl: "APP_BASE_URL",
678
+ },
679
+ input: env,
680
+ required: ["url", "anonKey"],
681
+ }),
682
+ api,
683
+ log: input.log,
684
+ documentNavigations: [
685
+ {
686
+ matcher: createDocumentNavigationMatchers(loginPath, signupPath, callbackPath, logoutPath, pages.signIn, pages.signUp),
687
+ },
688
+ ],
689
+ routes: [
690
+ {
691
+ path: loginPath,
692
+ methods: ["GET", "POST"],
693
+ async handler(_request, context) {
694
+ const request = context.request;
695
+ const requestUrl = context.url;
696
+ const clientRequest = isIntegrationClientRequest(request);
697
+ const provider = requestUrl.searchParams.get("provider") || input.defaultProvider;
698
+ const returnTo = getReturnTo(requestUrl.searchParams.get("returnTo"), "/dashboard");
699
+ if (request.method === "POST") {
700
+ const parsedRequest = await parseEmailPasswordRequest(request);
701
+ if (!parsedRequest.ok) {
702
+ if (clientRequest) {
703
+ return jsonError(parsedRequest.message, 400);
704
+ }
705
+ if (pages.signIn && pages.signIn !== loginPath) {
706
+ return redirectToPage(pages.signIn, context, env.appBaseUrl, {
707
+ returnTo: parsedRequest.returnTo,
708
+ error: parsedRequest.message,
709
+ });
710
+ }
711
+ return renderAuthPage({
712
+ mode: "sign-in",
713
+ context,
714
+ loginPath,
715
+ signupPath,
716
+ signInViewPath,
717
+ signUpViewPath,
718
+ providers,
719
+ defaultProvider: input.defaultProvider,
720
+ feedback: {
721
+ tone: "error",
722
+ message: parsedRequest.message,
723
+ },
724
+ });
725
+ }
726
+ const { supabase, setCookies } = createSupabaseHandler(context, env);
727
+ const { error } = await supabase.auth.signInWithPassword({
728
+ email: parsedRequest.email,
729
+ password: parsedRequest.password,
730
+ });
731
+ if (error) {
732
+ const message = normalizeSupabaseMessage(error.message);
733
+ if (clientRequest) {
734
+ return jsonError(message, 400);
735
+ }
736
+ if (pages.signIn && pages.signIn !== loginPath) {
737
+ return redirectToPage(pages.signIn, context, env.appBaseUrl, {
738
+ returnTo: parsedRequest.returnTo,
739
+ error: message,
740
+ });
741
+ }
742
+ return renderAuthPage({
743
+ mode: "sign-in",
744
+ context,
745
+ loginPath,
746
+ signupPath,
747
+ signInViewPath,
748
+ signUpViewPath,
749
+ providers,
750
+ defaultProvider: input.defaultProvider,
751
+ feedback: {
752
+ tone: "error",
753
+ message,
754
+ },
755
+ });
756
+ }
757
+ const headers = new Headers();
758
+ appendSetCookies(headers, setCookies);
759
+ const redirectTo = new URL(parsedRequest.returnTo, getOrigin(context.request, env.appBaseUrl)).toString();
760
+ if (clientRequest) {
761
+ return jsonSuccess({
762
+ redirectTo,
763
+ }, headers);
764
+ }
765
+ headers.set("location", redirectTo);
766
+ return new Response(null, {
767
+ status: 302,
768
+ headers,
769
+ });
770
+ }
771
+ if (!provider && pages.signIn && pages.signIn !== loginPath) {
772
+ return redirectToPage(pages.signIn, context, env.appBaseUrl, {
773
+ returnTo,
774
+ error: requestUrl.searchParams.get("error"),
775
+ message: requestUrl.searchParams.get("message"),
776
+ email: requestUrl.searchParams.get("email"),
777
+ });
778
+ }
779
+ if (!provider) {
780
+ return renderAuthPage({
781
+ mode: "sign-in",
782
+ context,
783
+ loginPath,
784
+ signupPath,
785
+ signInViewPath,
786
+ signUpViewPath,
787
+ providers,
788
+ defaultProvider: input.defaultProvider,
789
+ feedback: readPageFeedback(context),
790
+ });
791
+ }
792
+ const { supabase, setCookies } = createSupabaseHandler(context, env);
793
+ const { data, error } = await supabase.auth.signInWithOAuth({
794
+ provider: provider,
795
+ options: {
796
+ redirectTo: callbackSettings.getCallbackUrl(context.request, { returnTo }),
797
+ },
798
+ });
799
+ if (error || !data.url) {
800
+ const message = error?.message || "Failed to start Supabase OAuth sign-in.";
801
+ if (clientRequest) {
802
+ return jsonError(message, 500);
803
+ }
804
+ return new Response(message, {
805
+ status: 500,
806
+ });
807
+ }
808
+ const headers = new Headers();
809
+ appendSetCookies(headers, setCookies);
810
+ if (clientRequest) {
811
+ return jsonSuccess({
812
+ redirectTo: data.url,
813
+ }, headers);
814
+ }
815
+ headers.set("location", data.url);
816
+ return new Response(null, {
817
+ status: 302,
818
+ headers,
819
+ });
820
+ },
821
+ },
822
+ {
823
+ path: signupPath,
824
+ methods: ["GET", "POST"],
825
+ async handler(_request, context) {
826
+ const request = context.request;
827
+ const requestUrl = context.url;
828
+ const clientRequest = isIntegrationClientRequest(request);
829
+ const returnTo = getReturnTo(requestUrl.searchParams.get("returnTo"), "/dashboard");
830
+ if (request.method === "GET") {
831
+ if (pages.signUp && pages.signUp !== signupPath) {
832
+ return redirectToPage(pages.signUp, context, env.appBaseUrl, {
833
+ returnTo,
834
+ error: requestUrl.searchParams.get("error"),
835
+ message: requestUrl.searchParams.get("message"),
836
+ email: requestUrl.searchParams.get("email"),
837
+ });
838
+ }
839
+ return renderAuthPage({
840
+ mode: "sign-up",
841
+ context,
842
+ loginPath,
843
+ signupPath,
844
+ signInViewPath,
845
+ signUpViewPath,
846
+ providers,
847
+ feedback: readPageFeedback(context),
848
+ });
849
+ }
850
+ const parsedRequest = await parseEmailPasswordRequest(context.request);
851
+ if (!parsedRequest.ok) {
852
+ if (clientRequest) {
853
+ return jsonError(parsedRequest.message, 400);
854
+ }
855
+ if (pages.signUp && pages.signUp !== signupPath) {
856
+ return redirectToPage(pages.signUp, context, env.appBaseUrl, {
857
+ returnTo: parsedRequest.returnTo,
858
+ error: parsedRequest.message,
859
+ });
860
+ }
861
+ return renderAuthPage({
862
+ mode: "sign-up",
863
+ context,
864
+ loginPath,
865
+ signupPath,
866
+ signInViewPath,
867
+ signUpViewPath,
868
+ providers,
869
+ feedback: {
870
+ tone: "error",
871
+ message: parsedRequest.message,
872
+ },
873
+ });
874
+ }
875
+ const { supabase, setCookies } = createSupabaseHandler(context, env);
876
+ const { data, error } = await supabase.auth.signUp({
877
+ email: parsedRequest.email,
878
+ password: parsedRequest.password,
879
+ options: {
880
+ emailRedirectTo: callbackSettings.getCallbackUrl(context.request, {
881
+ returnTo: parsedRequest.returnTo,
882
+ }),
883
+ },
884
+ });
885
+ if (error) {
886
+ const message = normalizeSupabaseMessage(error.message);
887
+ if (clientRequest) {
888
+ return jsonError(message, 400);
889
+ }
890
+ if (pages.signUp && pages.signUp !== signupPath) {
891
+ return redirectToPage(pages.signUp, context, env.appBaseUrl, {
892
+ returnTo: parsedRequest.returnTo,
893
+ error: message,
894
+ });
895
+ }
896
+ return renderAuthPage({
897
+ mode: "sign-up",
898
+ context,
899
+ loginPath,
900
+ signupPath,
901
+ signInViewPath,
902
+ signUpViewPath,
903
+ providers,
904
+ feedback: {
905
+ tone: "error",
906
+ message,
907
+ },
908
+ });
909
+ }
910
+ const headers = new Headers();
911
+ appendSetCookies(headers, setCookies);
912
+ if (data.session) {
913
+ const redirectTo = new URL(parsedRequest.returnTo, getOrigin(context.request, env.appBaseUrl)).toString();
914
+ if (clientRequest) {
915
+ return jsonSuccess({
916
+ redirectTo,
917
+ }, headers);
918
+ }
919
+ headers.set("location", redirectTo);
920
+ return new Response(null, {
921
+ status: 302,
922
+ headers,
923
+ });
924
+ }
925
+ const signInRedirectTo = toAbsoluteUrl(signInViewPath, context.request, env.appBaseUrl);
926
+ signInRedirectTo.searchParams.set("returnTo", parsedRequest.returnTo);
927
+ signInRedirectTo.searchParams.set("message", "check-email");
928
+ signInRedirectTo.searchParams.set("email", parsedRequest.email);
929
+ if (clientRequest) {
930
+ return jsonSuccess({
931
+ redirectTo: signInRedirectTo.toString(),
932
+ emailConfirmationRequired: true,
933
+ email: parsedRequest.email,
934
+ message: `Check ${parsedRequest.email} to confirm your account, then sign in.`,
935
+ }, headers);
936
+ }
937
+ if (pages.signIn && pages.signIn !== loginPath) {
938
+ return redirectToPage(pages.signIn, context, env.appBaseUrl, {
939
+ returnTo: parsedRequest.returnTo,
940
+ message: "check-email",
941
+ email: parsedRequest.email,
942
+ });
943
+ }
944
+ return renderCheckEmailPage(context, signInViewPath, parsedRequest.email, parsedRequest.returnTo);
945
+ },
946
+ },
947
+ {
948
+ path: callbackPath,
949
+ methods: ["GET"],
950
+ async handler(_request, context) {
951
+ const requestUrl = context.url;
952
+ const code = requestUrl.searchParams.get("code");
953
+ const returnTo = getReturnTo(requestUrl.searchParams.get("returnTo"), "/dashboard");
954
+ if (!code) {
955
+ return new Response("Missing Supabase authorization code.", { status: 400 });
956
+ }
957
+ const { supabase, setCookies } = createSupabaseHandler(context, env);
958
+ const { error } = await supabase.auth.exchangeCodeForSession(code);
959
+ if (error) {
960
+ return new Response(error.message, { status: 500 });
961
+ }
962
+ const headers = new Headers();
963
+ appendSetCookies(headers, setCookies);
964
+ headers.set("location", new URL(returnTo, getOrigin(context.request, env.appBaseUrl)).toString());
965
+ return new Response(null, {
966
+ status: 302,
967
+ headers,
968
+ });
969
+ },
970
+ },
971
+ {
972
+ path: logoutPath,
973
+ methods: ["GET", "POST"],
974
+ async handler(_request, context) {
975
+ const request = context.request;
976
+ const requestUrl = context.url;
977
+ let returnTo = getReturnTo(requestUrl.searchParams.get("returnTo"), "/");
978
+ const clientRequest = isIntegrationClientRequest(request);
979
+ if (request.method === "POST") {
980
+ const contentType = context.request.headers.get("content-type") || "";
981
+ if (contentType.includes("application/json")) {
982
+ const payload = (await context.request.json()) || {};
983
+ returnTo = getReturnTo(payload.returnTo, returnTo);
984
+ }
985
+ }
986
+ const { supabase, setCookies } = createSupabaseHandler(context, env);
987
+ const { error } = await supabase.auth.signOut();
988
+ if (error) {
989
+ if (clientRequest) {
990
+ return jsonError(error.message, 500);
991
+ }
992
+ return new Response(error.message, { status: 500 });
993
+ }
994
+ const headers = new Headers();
995
+ appendSetCookies(headers, setCookies);
996
+ const redirectTo = new URL(returnTo, getOrigin(context.request, env.appBaseUrl)).toString();
997
+ if (clientRequest) {
998
+ return jsonSuccess({
999
+ redirectTo,
1000
+ }, headers);
1001
+ }
1002
+ headers.set("location", redirectTo);
1003
+ return new Response(null, {
1004
+ status: 302,
1005
+ headers,
1006
+ });
1007
+ },
1008
+ },
1009
+ {
1010
+ path: sessionPath,
1011
+ methods: ["GET"],
1012
+ async handler(_request, context) {
1013
+ const { supabase, setCookies } = createSupabaseHandler(context, env);
1014
+ const { data, error } = await supabase.auth.getUser();
1015
+ const headers = new Headers({
1016
+ "content-type": "application/json",
1017
+ });
1018
+ appendSetCookies(headers, setCookies);
1019
+ if (error || !data.user) {
1020
+ return new Response(JSON.stringify({ authenticated: false }), {
1021
+ status: 401,
1022
+ headers,
1023
+ });
1024
+ }
1025
+ return new Response(JSON.stringify({
1026
+ authenticated: true,
1027
+ user: data.user,
1028
+ }), {
1029
+ status: 200,
1030
+ headers,
1031
+ });
1032
+ },
1033
+ },
1034
+ ],
1035
+ middleware: protectedMatchers.length > 0
1036
+ ? [
1037
+ {
1038
+ matcher: protectedMatchers,
1039
+ async handler(_request, context) {
1040
+ const { supabase } = createSupabaseHandler(context, env);
1041
+ const { data: { session }, } = await supabase.auth.getSession();
1042
+ if (session) {
1043
+ return;
1044
+ }
1045
+ return redirectToPage(signInViewPath, context, env.appBaseUrl, {
1046
+ returnTo: `${context.url.pathname}${context.url.search}`,
1047
+ });
1048
+ },
1049
+ },
1050
+ ]
1051
+ : [],
1052
+ });
1053
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@farm.js/supabase",
3
+ "version": "0.1.0-beta.0",
4
+ "description": "Supabase integration for Farm.js",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/farming-labs/farm.js",
9
+ "directory": "packages/farm-supabase"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "type": "module",
15
+ "main": "./dist/index.js",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ },
23
+ "./client": {
24
+ "types": "./dist/client.d.ts",
25
+ "import": "./dist/client.js"
26
+ }
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "dependencies": {
32
+ "@supabase/ssr": "^0.9.0",
33
+ "@supabase/supabase-js": "^2.99.2",
34
+ "@farm.js/core": "0.1.0-beta.0",
35
+ "@farm.js/integration-utils": "0.1.0-beta.0"
36
+ },
37
+ "scripts": {
38
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
39
+ "dev": "tsc --watch",
40
+ "type-check": "tsc --noEmit",
41
+ "test": "echo 'No tests in this package'"
42
+ }
43
+ }