@farm.js/supabase 0.1.0-beta.1 → 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 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: string;
24
- anonKey: string;
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;
@@ -1 +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"}
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 supabase = createServerClient(env.url, env.anonKey, {
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
- const { data: { session }, } = await supabase.auth.getSession();
1042
- if (session) {
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
- return redirectToPage(signInViewPath, context, env.appBaseUrl, {
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.1",
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/integration-utils": "0.1.0-beta.1",
35
- "@farm.js/core": "0.1.0-beta.1"
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",