@farm.js/supabase 0.1.0-beta.10 → 0.1.0-beta.100
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/dist/index.d.ts +29 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +97 -18
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
|
+
import { createServerClient } from "@supabase/ssr";
|
|
1
2
|
import { type FarmIntegrationHandlerContext, type FarmIntegrationLogger } from "@farm.js/core";
|
|
2
3
|
export interface SupabaseIntegrationPages {
|
|
3
4
|
signIn?: string;
|
|
4
5
|
signUp?: string;
|
|
5
6
|
}
|
|
7
|
+
export type SupabaseIntegrationClient = ReturnType<typeof createServerClient>;
|
|
8
|
+
export type SupabaseIntegrationClientOptions = Parameters<typeof createServerClient>[2];
|
|
9
|
+
export interface SupabaseIntegrationInstanceContext {
|
|
10
|
+
request: Request;
|
|
11
|
+
url?: string;
|
|
12
|
+
anonKey?: string;
|
|
13
|
+
createClient: typeof createServerClient;
|
|
14
|
+
options: SupabaseIntegrationClientOptions;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Creates a request-scoped Supabase client using Farm's cookie adapter.
|
|
18
|
+
*
|
|
19
|
+
* A factory is used instead of a shared client because Supabase SSR clients
|
|
20
|
+
* are bound to the cookies of the current request.
|
|
21
|
+
*/
|
|
22
|
+
export type SupabaseIntegrationInstance = (context: SupabaseIntegrationInstanceContext) => SupabaseIntegrationClient;
|
|
6
23
|
export interface SupabaseIntegrationInput {
|
|
24
|
+
instance?: SupabaseIntegrationInstance;
|
|
7
25
|
url?: string;
|
|
8
26
|
anonKey?: string;
|
|
9
27
|
appBaseUrl?: string;
|
|
@@ -17,11 +35,19 @@ export interface SupabaseIntegrationInput {
|
|
|
17
35
|
providers?: string[];
|
|
18
36
|
defaultProvider?: string;
|
|
19
37
|
pages?: SupabaseIntegrationPages;
|
|
38
|
+
/**
|
|
39
|
+
* Additional origins allowed to submit the sign-in, sign-up, and sign-out
|
|
40
|
+
* routes, using the same pattern syntax as `serverActions.allowedOrigins`
|
|
41
|
+
* (`https://app.example.com`, `example.com`, `*.example.com`). The app's own
|
|
42
|
+
* origin is always trusted; set this only when a different trusted origin
|
|
43
|
+
* posts these forms.
|
|
44
|
+
*/
|
|
45
|
+
allowedOrigins?: string[];
|
|
20
46
|
log?: FarmIntegrationLogger;
|
|
21
47
|
}
|
|
22
48
|
interface ResolvedSupabaseEnv {
|
|
23
|
-
url
|
|
24
|
-
anonKey
|
|
49
|
+
url?: string;
|
|
50
|
+
anonKey?: string;
|
|
25
51
|
appBaseUrl?: string;
|
|
26
52
|
}
|
|
27
53
|
interface ResolvedSupabasePages {
|
|
@@ -32,7 +58,7 @@ export declare function supabase(input?: SupabaseIntegrationInput): import("@far
|
|
|
32
58
|
category: "auth";
|
|
33
59
|
type: string;
|
|
34
60
|
instance: {
|
|
35
|
-
url: string;
|
|
61
|
+
url: string | undefined;
|
|
36
62
|
protectedRoutes: string[];
|
|
37
63
|
providers: string[];
|
|
38
64
|
pages: ResolvedSupabasePages;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAsB,MAAM,eAAe,CAAC;AACvE,OAAO,EAGL,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,EAC3B,MAAM,eAAe,CAAC;AAsCvB,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,yBAAyB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9E,MAAM,MAAM,gCAAgC,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC;AAExF,MAAM,WAAW,kCAAkC;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,kBAAkB,CAAC;IACxC,OAAO,EAAE,gCAAgC,CAAC;CAC3C;AAED;;;;;GAKG;AACH,MAAM,MAAM,2BAA2B,GAAG,CACxC,OAAO,EAAE,kCAAkC,KACxC,yBAAyB,CAAC;AAE/B,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,2BAA2B,CAAC;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;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,GAAG,CAAC,EAAE,qBAAqB,CAAC;CAC7B;AAED,UAAU,mBAAmB;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,qBAAqB;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAuuBD,wBAAgB,QAAQ,CAAC,KAAK,GAAE,wBAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BAuG7B,OAAO,WAAW,6BAA6B;;;;0BA2czC,OAAO,WAAW,6BAA6B;;GAgDpF"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServerClient } from "@supabase/ssr";
|
|
2
|
-
import { defineIntegration, } from "@farm.js/core";
|
|
2
|
+
import { defineIntegration, forwardIntegrationSetCookies, } from "@farm.js/core";
|
|
3
|
+
import { describeIntegrationOriginRejection, resolveIntegrationAllowedOrigins, validateIntegrationRequestOrigin, } from "@farm.js/core/integrations";
|
|
3
4
|
import { api as clientApi } from "@farm.js/core/client";
|
|
4
5
|
import { createPathInferredClientApi, createDocumentNavigationMatchers, escapeHtml, getOrigin, getReturnTo, integrationConfig, normalizeMatchers, parseCookieHeaderList, resolveAppPath, resolveCallbackSettings, toAbsoluteUrl, withSearchParams, } from "@farm.js/integration-utils";
|
|
5
6
|
import { supabaseAuthFormFields, } from "./client.js";
|
|
@@ -40,8 +41,8 @@ function resolveEnv(input) {
|
|
|
40
41
|
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY ??
|
|
41
42
|
"";
|
|
42
43
|
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).");
|
|
44
|
+
if (!input.instance && (!url || !anonKey)) {
|
|
45
|
+
throw new Error("Supabase integration requires an instance factory or SUPABASE_URL (or NEXT_PUBLIC_SUPABASE_URL) and SUPABASE_ANON_KEY (or a Supabase publishable key env).");
|
|
45
46
|
}
|
|
46
47
|
return {
|
|
47
48
|
url,
|
|
@@ -576,9 +577,9 @@ function renderCheckEmailPage(context, signInViewPath, email, returnTo) {
|
|
|
576
577
|
},
|
|
577
578
|
});
|
|
578
579
|
}
|
|
579
|
-
function createSupabaseHandler(context, env) {
|
|
580
|
+
function createSupabaseHandler(context, env, instance) {
|
|
580
581
|
const setCookies = [];
|
|
581
|
-
const
|
|
582
|
+
const options = {
|
|
582
583
|
auth: {
|
|
583
584
|
flowType: "pkce",
|
|
584
585
|
detectSessionInUrl: false,
|
|
@@ -591,10 +592,18 @@ function createSupabaseHandler(context, env) {
|
|
|
591
592
|
for (const cookie of cookiesToSet) {
|
|
592
593
|
setCookies.push(serializeCookie(cookie.name, cookie.value, cookie.options));
|
|
593
594
|
}
|
|
594
|
-
context.req.set("supabase:set-cookies", [...setCookies]);
|
|
595
595
|
},
|
|
596
596
|
},
|
|
597
|
-
}
|
|
597
|
+
};
|
|
598
|
+
const supabase = instance
|
|
599
|
+
? instance({
|
|
600
|
+
request: context.request,
|
|
601
|
+
url: env.url,
|
|
602
|
+
anonKey: env.anonKey,
|
|
603
|
+
createClient: createServerClient,
|
|
604
|
+
options,
|
|
605
|
+
})
|
|
606
|
+
: createServerClient(env.url, env.anonKey, options);
|
|
598
607
|
return {
|
|
599
608
|
supabase,
|
|
600
609
|
setCookies,
|
|
@@ -648,12 +657,33 @@ export function supabase(input = {}) {
|
|
|
648
657
|
label: "Supabase integration",
|
|
649
658
|
}, env.appBaseUrl);
|
|
650
659
|
const callbackPath = callbackSettings.callbackPath;
|
|
660
|
+
const allowedOrigins = resolveIntegrationAllowedOrigins(input.allowedOrigins, "supabase.allowedOrigins");
|
|
651
661
|
const api = createSupabaseApi({
|
|
652
662
|
loginPath,
|
|
653
663
|
signupPath,
|
|
654
664
|
logoutPath,
|
|
655
665
|
sessionPath,
|
|
656
666
|
});
|
|
667
|
+
/**
|
|
668
|
+
* Reject a credential or sign-out request that did not come from this app.
|
|
669
|
+
* Returns the response to send, or null when the request may proceed.
|
|
670
|
+
*/
|
|
671
|
+
function rejectForeignOrigin(request, clientRequest, { requireOriginMetadata }) {
|
|
672
|
+
const result = validateIntegrationRequestOrigin(request, {
|
|
673
|
+
allowedOrigins,
|
|
674
|
+
requireOriginMetadata,
|
|
675
|
+
});
|
|
676
|
+
if (result.ok) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
const message = describeIntegrationOriginRejection(result.reason);
|
|
680
|
+
return clientRequest
|
|
681
|
+
? jsonError(message, 403)
|
|
682
|
+
: new Response(message, {
|
|
683
|
+
status: 403,
|
|
684
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
685
|
+
});
|
|
686
|
+
}
|
|
657
687
|
return defineIntegration({
|
|
658
688
|
category: "auth",
|
|
659
689
|
type: "supabase",
|
|
@@ -677,7 +707,7 @@ export function supabase(input = {}) {
|
|
|
677
707
|
appBaseUrl: "APP_BASE_URL",
|
|
678
708
|
},
|
|
679
709
|
input: env,
|
|
680
|
-
required: ["url", "anonKey"],
|
|
710
|
+
required: input.instance ? [] : ["url", "anonKey"],
|
|
681
711
|
}),
|
|
682
712
|
api,
|
|
683
713
|
log: input.log,
|
|
@@ -697,6 +727,12 @@ export function supabase(input = {}) {
|
|
|
697
727
|
const provider = requestUrl.searchParams.get("provider") || input.defaultProvider;
|
|
698
728
|
const returnTo = getReturnTo(requestUrl.searchParams.get("returnTo"), "/dashboard");
|
|
699
729
|
if (request.method === "POST") {
|
|
730
|
+
const rejected = rejectForeignOrigin(request, clientRequest, {
|
|
731
|
+
requireOriginMetadata: true,
|
|
732
|
+
});
|
|
733
|
+
if (rejected) {
|
|
734
|
+
return rejected;
|
|
735
|
+
}
|
|
700
736
|
const parsedRequest = await parseEmailPasswordRequest(request);
|
|
701
737
|
if (!parsedRequest.ok) {
|
|
702
738
|
if (clientRequest) {
|
|
@@ -723,7 +759,7 @@ export function supabase(input = {}) {
|
|
|
723
759
|
},
|
|
724
760
|
});
|
|
725
761
|
}
|
|
726
|
-
const { supabase, setCookies } = createSupabaseHandler(context, env);
|
|
762
|
+
const { supabase, setCookies } = createSupabaseHandler(context, env, input.instance);
|
|
727
763
|
const { error } = await supabase.auth.signInWithPassword({
|
|
728
764
|
email: parsedRequest.email,
|
|
729
765
|
password: parsedRequest.password,
|
|
@@ -789,7 +825,7 @@ export function supabase(input = {}) {
|
|
|
789
825
|
feedback: readPageFeedback(context),
|
|
790
826
|
});
|
|
791
827
|
}
|
|
792
|
-
const { supabase, setCookies } = createSupabaseHandler(context, env);
|
|
828
|
+
const { supabase, setCookies } = createSupabaseHandler(context, env, input.instance);
|
|
793
829
|
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
794
830
|
provider: provider,
|
|
795
831
|
options: {
|
|
@@ -847,6 +883,12 @@ export function supabase(input = {}) {
|
|
|
847
883
|
feedback: readPageFeedback(context),
|
|
848
884
|
});
|
|
849
885
|
}
|
|
886
|
+
const rejected = rejectForeignOrigin(request, clientRequest, {
|
|
887
|
+
requireOriginMetadata: true,
|
|
888
|
+
});
|
|
889
|
+
if (rejected) {
|
|
890
|
+
return rejected;
|
|
891
|
+
}
|
|
850
892
|
const parsedRequest = await parseEmailPasswordRequest(context.request);
|
|
851
893
|
if (!parsedRequest.ok) {
|
|
852
894
|
if (clientRequest) {
|
|
@@ -872,7 +914,7 @@ export function supabase(input = {}) {
|
|
|
872
914
|
},
|
|
873
915
|
});
|
|
874
916
|
}
|
|
875
|
-
const { supabase, setCookies } = createSupabaseHandler(context, env);
|
|
917
|
+
const { supabase, setCookies } = createSupabaseHandler(context, env, input.instance);
|
|
876
918
|
const { data, error } = await supabase.auth.signUp({
|
|
877
919
|
email: parsedRequest.email,
|
|
878
920
|
password: parsedRequest.password,
|
|
@@ -954,7 +996,7 @@ export function supabase(input = {}) {
|
|
|
954
996
|
if (!code) {
|
|
955
997
|
return new Response("Missing Supabase authorization code.", { status: 400 });
|
|
956
998
|
}
|
|
957
|
-
const { supabase, setCookies } = createSupabaseHandler(context, env);
|
|
999
|
+
const { supabase, setCookies } = createSupabaseHandler(context, env, input.instance);
|
|
958
1000
|
const { error } = await supabase.auth.exchangeCodeForSession(code);
|
|
959
1001
|
if (error) {
|
|
960
1002
|
return new Response(error.message, { status: 500 });
|
|
@@ -976,6 +1018,16 @@ export function supabase(input = {}) {
|
|
|
976
1018
|
const requestUrl = context.url;
|
|
977
1019
|
let returnTo = getReturnTo(requestUrl.searchParams.get("returnTo"), "/");
|
|
978
1020
|
const clientRequest = isIntegrationClientRequest(request);
|
|
1021
|
+
// A POST carries an Origin in every supported browser, so it is held
|
|
1022
|
+
// to the strict check. A GET sign-out is also reachable as a plain
|
|
1023
|
+
// link or bookmark, where no origin metadata exists at all; those
|
|
1024
|
+
// stay allowed and only an explicitly cross-site GET is rejected.
|
|
1025
|
+
const rejected = rejectForeignOrigin(request, clientRequest, {
|
|
1026
|
+
requireOriginMetadata: request.method === "POST",
|
|
1027
|
+
});
|
|
1028
|
+
if (rejected) {
|
|
1029
|
+
return rejected;
|
|
1030
|
+
}
|
|
979
1031
|
if (request.method === "POST") {
|
|
980
1032
|
const contentType = context.request.headers.get("content-type") || "";
|
|
981
1033
|
if (contentType.includes("application/json")) {
|
|
@@ -983,7 +1035,7 @@ export function supabase(input = {}) {
|
|
|
983
1035
|
returnTo = getReturnTo(payload.returnTo, returnTo);
|
|
984
1036
|
}
|
|
985
1037
|
}
|
|
986
|
-
const { supabase, setCookies } = createSupabaseHandler(context, env);
|
|
1038
|
+
const { supabase, setCookies } = createSupabaseHandler(context, env, input.instance);
|
|
987
1039
|
const { error } = await supabase.auth.signOut();
|
|
988
1040
|
if (error) {
|
|
989
1041
|
if (clientRequest) {
|
|
@@ -1010,7 +1062,7 @@ export function supabase(input = {}) {
|
|
|
1010
1062
|
path: sessionPath,
|
|
1011
1063
|
methods: ["GET"],
|
|
1012
1064
|
async handler(_request, context) {
|
|
1013
|
-
const { supabase, setCookies } = createSupabaseHandler(context, env);
|
|
1065
|
+
const { supabase, setCookies } = createSupabaseHandler(context, env, input.instance);
|
|
1014
1066
|
const { data, error } = await supabase.auth.getUser();
|
|
1015
1067
|
const headers = new Headers({
|
|
1016
1068
|
"content-type": "application/json",
|
|
@@ -1037,14 +1089,41 @@ export function supabase(input = {}) {
|
|
|
1037
1089
|
{
|
|
1038
1090
|
matcher: protectedMatchers,
|
|
1039
1091
|
async handler(_request, context) {
|
|
1040
|
-
const { supabase } = createSupabaseHandler(context, env);
|
|
1041
|
-
|
|
1042
|
-
|
|
1092
|
+
const { supabase, setCookies } = createSupabaseHandler(context, env, input.instance);
|
|
1093
|
+
// getSession() reads the cookie without verifying the token,
|
|
1094
|
+
// so a forged cookie would pass this gate. getUser() checks
|
|
1095
|
+
// the token against Supabase Auth before the route is served.
|
|
1096
|
+
const { data, error } = await supabase.auth.getUser();
|
|
1097
|
+
if (!error && data?.user) {
|
|
1098
|
+
// getUser() verifies the token with Supabase Auth, and while
|
|
1099
|
+
// doing so can rotate the session server-side (e.g. when the
|
|
1100
|
+
// access token is near expiry), capturing the refreshed
|
|
1101
|
+
// Set-Cookie in setCookies. This branch returns void so the
|
|
1102
|
+
// protected route still renders, which means those cookies
|
|
1103
|
+
// cannot ride on this branch's response — hand them to the
|
|
1104
|
+
// runtime via forwardIntegrationSetCookies so it forwards
|
|
1105
|
+
// them on the downstream response, mirroring the failure
|
|
1106
|
+
// branch's appendSetCookies on its redirect. Without this the
|
|
1107
|
+
// rotated cookie is dropped, the browser keeps the stale
|
|
1108
|
+
// refresh token, and the next protected request logs the user
|
|
1109
|
+
// out via refresh-token reuse detection.
|
|
1110
|
+
if (setCookies.length > 0) {
|
|
1111
|
+
forwardIntegrationSetCookies(context, setCookies);
|
|
1112
|
+
}
|
|
1043
1113
|
return;
|
|
1044
1114
|
}
|
|
1045
|
-
|
|
1115
|
+
const redirect = redirectToPage(signInViewPath, context, env.appBaseUrl, {
|
|
1046
1116
|
returnTo: `${context.url.pathname}${context.url.search}`,
|
|
1047
1117
|
});
|
|
1118
|
+
if (setCookies.length === 0) {
|
|
1119
|
+
return redirect;
|
|
1120
|
+
}
|
|
1121
|
+
// A failed validation can clear or rotate auth cookies; keep
|
|
1122
|
+
// those directives on the redirect so the browser drops the
|
|
1123
|
+
// stale session instead of replaying it on the next request.
|
|
1124
|
+
const headers = new Headers(redirect.headers);
|
|
1125
|
+
appendSetCookies(headers, setCookies);
|
|
1126
|
+
return new Response(null, { status: redirect.status, headers });
|
|
1048
1127
|
},
|
|
1049
1128
|
},
|
|
1050
1129
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farm.js/supabase",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.100",
|
|
4
4
|
"description": "Supabase integration for Farm.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@supabase/ssr": "^0.9.0",
|
|
33
33
|
"@supabase/supabase-js": "^2.99.2",
|
|
34
|
-
"@farm.js/core": "0.1.0-beta.
|
|
35
|
-
"@farm.js/integration-utils": "0.1.0-beta.
|
|
34
|
+
"@farm.js/core": "0.1.0-beta.100",
|
|
35
|
+
"@farm.js/integration-utils": "0.1.0-beta.100"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
|