@neondatabase/auth 0.2.0-beta.1 → 0.4.0-beta

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 (44) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +22 -7
  3. package/codemods/migrate-auth-ui-imports.mjs +439 -0
  4. package/dist/{adapter-core-CnrOXh1T.d.mts → adapter-core-BWM7cWOp.d.mts} +465 -401
  5. package/dist/{adapter-core-CtmnMMJ7.mjs → adapter-core-Bt4M5I2g.mjs} +21 -11
  6. package/dist/auth-interface-Clz-oWq1.d.mts +8 -0
  7. package/dist/better-auth-helpers-Bkezghej.mjs +541 -0
  8. package/dist/{better-auth-react-adapter-DNi5PC5D.d.mts → better-auth-react-adapter-BDxJ65mF.d.mts} +389 -302
  9. package/dist/{better-auth-react-adapter-Dv-o6A6O.mjs → better-auth-react-adapter-aMv8WeDb.mjs} +1 -1
  10. package/dist/{index-CzsGMS7C.d.mts → index-DHryUj7e.d.mts} +0 -1
  11. package/dist/index.d.mts +94 -5
  12. package/dist/index.mjs +4 -3
  13. package/dist/{neon-auth-Cs2cWh1B.mjs → neon-auth-CS4FpK2X.mjs} +1 -1
  14. package/dist/next/index.d.mts +161 -73
  15. package/dist/next/index.mjs +5 -4
  16. package/dist/next/server/index.d.mts +25 -5
  17. package/dist/next/server/index.mjs +369 -251
  18. package/dist/react/adapters/index.d.mts +3 -3
  19. package/dist/react/adapters/index.mjs +2 -2
  20. package/dist/react/index.d.mts +6 -5
  21. package/dist/react/index.mjs +5 -5
  22. package/dist/react/ui/index.d.mts +1 -2
  23. package/dist/react/ui/index.mjs +2 -4
  24. package/dist/react/ui/server.mjs +2 -2
  25. package/dist/{supabase-adapter-DUqw2fw8.d.mts → supabase-adapter-BGwV0Vu2.d.mts} +429 -373
  26. package/dist/{supabase-adapter-BlcGPyOf.mjs → supabase-adapter-DBt4LJJd.mjs} +3 -514
  27. package/dist/types/index.d.mts +2 -2
  28. package/dist/ui/.safelist.html +1 -1
  29. package/dist/ui/css.css +2 -2
  30. package/dist/ui/theme.css +1 -1
  31. package/dist/ui-CnVnqGns.mjs +3 -0
  32. package/dist/vanilla/adapters/index.d.mts +4 -3
  33. package/dist/vanilla/adapters/index.mjs +2 -2
  34. package/dist/vanilla/index.d.mts +4 -3
  35. package/dist/vanilla/index.mjs +2 -2
  36. package/llms.txt +2 -2
  37. package/package.json +27 -21
  38. package/dist/chunk-VCZJYX65-CLnrj1o7-D6ZQkcc_.mjs +0 -543
  39. package/dist/constants-Cupc_bln.mjs +0 -28
  40. package/dist/neon-auth-BEGCfAe6.d.mts +0 -107
  41. package/dist/ui-COLWzDsu.mjs +0 -12469
  42. /package/dist/{adapters-B7YKkjaL.mjs → adapters-CUvhsAvY.mjs} +0 -0
  43. /package/dist/{index-CPnFzULh.d.mts → index-B0Pd4HOH.d.mts} +0 -0
  44. /package/dist/{index-OEBbnNdr.d.mts → index-CzpoWrv9.d.mts} +0 -0
@@ -1,4 +1,4 @@
1
- import { o as NEON_AUTH_SESSION_VERIFIER_PARAM_NAME } from "../../constants-Cupc_bln.mjs";
1
+ import { c as isAuthApiError, l as isAuthError, m as NEON_AUTH_SESSION_VERIFIER_PARAM_NAME, o as AuthApiError, r as normalizeBetterAuthError, s as AuthError } from "../../better-auth-helpers-Bkezghej.mjs";
2
2
  import { SignJWT, jwtVerify } from "jose";
3
3
  import { parseCookies, parseSetCookieHeader } from "better-auth/cookies";
4
4
  import { cookies, headers } from "next/headers";
@@ -54,6 +54,10 @@ const API_ENDPOINTS = {
54
54
  emailOtp: {
55
55
  path: "sign-in/email-otp",
56
56
  method: "POST"
57
+ },
58
+ magicLink: {
59
+ path: "sign-in/magic-link",
60
+ method: "POST"
57
61
  }
58
62
  },
59
63
  signUp: { email: {
@@ -273,7 +277,11 @@ const API_ENDPOINTS = {
273
277
  path: "email-otp/passcode",
274
278
  method: "POST"
275
279
  }
276
- }
280
+ },
281
+ magicLink: { verify: {
282
+ path: "magic-link/verify",
283
+ method: "GET"
284
+ } }
277
285
  };
278
286
 
279
287
  //#endregion
@@ -473,6 +481,35 @@ async function getSessionDataFromCookie(request, cookieName, cookieSecret) {
473
481
  return null;
474
482
  }
475
483
  }
484
+ /**
485
+ * Fetch session data from upstream using session token cookie
486
+ *
487
+ * @param sessionTokenCookie - Session token cookie string (can be Set-Cookie header or "name=value" format)
488
+ * @param baseUrl - Auth server base URL
489
+ * @returns Session data from upstream
490
+ */
491
+ async function fetchSessionWithCookie(sessionTokenCookie, baseUrl) {
492
+ let cookieName;
493
+ let cookieValue;
494
+ if (sessionTokenCookie.includes("=")) {
495
+ const [name, ...valueParts] = sessionTokenCookie.split(";")[0].trim().split("=");
496
+ cookieName = name.trim();
497
+ cookieValue = valueParts.join("=").trim();
498
+ } else throw new Error("Invalid session token cookie format");
499
+ if (!cookieName.includes("session_token")) throw new Error("session_token not found in cookie");
500
+ const response = await fetch(`${baseUrl}/get-session`, {
501
+ headers: { Cookie: `${cookieName}=${cookieValue}` },
502
+ signal: AbortSignal.timeout(3e3)
503
+ });
504
+ if (!response.ok) throw new Error(`Failed to fetch session data: ${response.status} ${response.statusText}`);
505
+ let body;
506
+ try {
507
+ body = await response.json();
508
+ } catch (error) {
509
+ throw new Error(`Failed to parse /get-session response as JSON: ${error instanceof Error ? error.message : String(error)}`);
510
+ }
511
+ return parseSessionData(body);
512
+ }
476
513
 
477
514
  //#endregion
478
515
  //#region src/server/errors.ts
@@ -506,6 +543,253 @@ async function validateSessionData(sessionDataString, cookieSecret) {
506
543
  }
507
544
  }
508
545
 
546
+ //#endregion
547
+ //#region src/server/session/minting.ts
548
+ /**
549
+ * Core minting logic - creates session_data cookie from session token
550
+ *
551
+ * @param sessionTokenCookie - Session token cookie string (format: "name=value")
552
+ * @param baseUrl - Auth server base URL
553
+ * @param cookieConfig - Cookie configuration
554
+ * @returns Set-Cookie string or null on error
555
+ */
556
+ async function mintSessionDataCookie(sessionTokenCookie, baseUrl, cookieConfig) {
557
+ try {
558
+ const sessionData = await fetchSessionWithCookie(sessionTokenCookie, baseUrl);
559
+ if (!sessionData.session) return null;
560
+ const { value: signedData, expiresAt } = await signSessionDataCookie(sessionData, cookieConfig.secret, cookieConfig.sessionDataTtl);
561
+ const maxAge = Math.floor((expiresAt.getTime() - Date.now()) / 1e3);
562
+ return serializeSetCookie({
563
+ name: NEON_AUTH_SESSION_DATA_COOKIE_NAME,
564
+ value: signedData,
565
+ path: "/",
566
+ domain: cookieConfig.domain,
567
+ httpOnly: true,
568
+ secure: true,
569
+ sameSite: cookieConfig.sameSite ?? "strict",
570
+ maxAge
571
+ });
572
+ } catch (error) {
573
+ const errorMessage = error instanceof Error ? error.message : String(error);
574
+ console.error("[mintSessionDataCookie] Failed to mint session_data cookie:", {
575
+ error: errorMessage,
576
+ ...process.env.NODE_ENV !== "production" && { stack: error instanceof Error ? error.stack : void 0 }
577
+ });
578
+ return null;
579
+ }
580
+ }
581
+ /**
582
+ * Utility A: Mint session_data cookie when session_token is updated by upstream
583
+ *
584
+ * Checks response headers for session_token in Set-Cookie, then mints session_data.
585
+ * Handles token deletion (max-age=0) by returning deletion cookie.
586
+ *
587
+ * Use case: Response handling in proxy/middleware after upstream auth calls
588
+ *
589
+ * @param responseHeaders - Response headers from upstream auth server
590
+ * @param baseUrl - Auth server base URL
591
+ * @param cookieConfig - Cookie configuration
592
+ * @returns Set-Cookie string for session_data or null if no action needed
593
+ */
594
+ async function mintSessionDataFromResponse(responseHeaders, baseUrl, cookieConfig) {
595
+ const sessionTokenCookie = responseHeaders.getSetCookie().find((cookie) => cookie.includes("session_token"));
596
+ if (!sessionTokenCookie) return null;
597
+ if (sessionTokenCookie.toLowerCase().includes("max-age=0")) return serializeSetCookie({
598
+ name: NEON_AUTH_SESSION_DATA_COOKIE_NAME,
599
+ value: "",
600
+ path: "/",
601
+ domain: cookieConfig.domain,
602
+ httpOnly: true,
603
+ secure: true,
604
+ sameSite: cookieConfig.sameSite ?? "strict",
605
+ maxAge: 0
606
+ });
607
+ return await mintSessionDataCookie(sessionTokenCookie, baseUrl, cookieConfig);
608
+ }
609
+ /**
610
+ * Utility B: Mint/refresh session_data cookie from existing session_token
611
+ *
612
+ * Extracts session_token from request cookies and mints a fresh session_data cookie.
613
+ * Use case: Cache misses, expired cookies, proactive refresh
614
+ *
615
+ * @param sessionTokenCookie - Session token cookie string (format: "name=value")
616
+ * @param baseUrl - Auth server base URL
617
+ * @param cookieConfig - Cookie configuration
618
+ * @returns Set-Cookie string for session_data or null on error
619
+ */
620
+ async function mintSessionDataFromToken(sessionTokenCookie, baseUrl, cookieConfig) {
621
+ return await mintSessionDataCookie(sessionTokenCookie, baseUrl, cookieConfig);
622
+ }
623
+
624
+ //#endregion
625
+ //#region src/server/client-factory.ts
626
+ function createAuthServerInternal(config) {
627
+ const { baseUrl, context: getContext, cookieSecret, sessionDataTtl, domain, sameSite } = config;
628
+ const effectiveSameSite = sameSite ?? "strict";
629
+ const fetchWithAuth = async (path, method, args) => {
630
+ const ctx = await getContext();
631
+ const cookies$1 = await ctx.getCookies();
632
+ const origin = await ctx.getOrigin();
633
+ const framework = ctx.getFramework();
634
+ const url = new URL(path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
635
+ const { query, fetchOptions: _fetchOptions, ...body } = args || {};
636
+ if (query && typeof query === "object") {
637
+ const queryParams = query;
638
+ for (const [key, value] of Object.entries(queryParams)) if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
639
+ }
640
+ const headers$1 = {
641
+ Cookie: cookies$1,
642
+ Origin: origin,
643
+ [NEON_AUTH_SERVER_PROXY_HEADER]: framework
644
+ };
645
+ let requestBody;
646
+ if (method === "POST") {
647
+ headers$1["Content-Type"] = "application/json";
648
+ requestBody = JSON.stringify(Object.keys(body).length > 0 ? body : {});
649
+ }
650
+ const response = await fetch(url.toString(), {
651
+ method,
652
+ headers: headers$1,
653
+ body: requestBody
654
+ });
655
+ const setCookieHeaders = response.headers.getSetCookie();
656
+ if (setCookieHeaders.length > 0) {
657
+ for (const setCookieHeader of setCookieHeaders) {
658
+ const parsedCookies = parseSetCookies(setCookieHeader);
659
+ for (const cookie of parsedCookies) {
660
+ const cookieOptions = {
661
+ ...cookie,
662
+ domain,
663
+ partitioned: void 0,
664
+ sameSite: effectiveSameSite
665
+ };
666
+ await ctx.setCookie(cookie.name, cookie.value, cookieOptions);
667
+ }
668
+ }
669
+ try {
670
+ const sessionDataCookie = await mintSessionDataFromResponse(response.headers, baseUrl, {
671
+ secret: cookieSecret,
672
+ sessionDataTtl,
673
+ domain,
674
+ sameSite
675
+ });
676
+ if (sessionDataCookie) {
677
+ const [parsedSessionData] = parseSetCookies(sessionDataCookie);
678
+ if (parsedSessionData) await ctx.setCookie(parsedSessionData.name, parsedSessionData.value, parsedSessionData);
679
+ }
680
+ } catch (error) {
681
+ console.error("[fetchWithAuth] Failed to mint session data cookie:", error);
682
+ }
683
+ }
684
+ const responseData = await response.json().catch(() => null);
685
+ if (!response.ok) {
686
+ const normalized = normalizeBetterAuthError({
687
+ status: response.status,
688
+ statusText: response.statusText,
689
+ message: responseData?.message || response.statusText,
690
+ code: responseData?.code,
691
+ body: responseData
692
+ });
693
+ return {
694
+ data: null,
695
+ error: {
696
+ message: normalized.message,
697
+ status: normalized.status ?? response.status,
698
+ statusText: response.statusText,
699
+ code: normalized.code
700
+ }
701
+ };
702
+ }
703
+ return {
704
+ data: responseData,
705
+ error: null
706
+ };
707
+ };
708
+ const baseServer = createApiProxy(API_ENDPOINTS, fetchWithAuth);
709
+ const originalGetSession = baseServer.getSession;
710
+ baseServer.getSession = async (...args) => {
711
+ const [data] = args;
712
+ if (!(data?.query?.disableCookieCache === "true")) try {
713
+ const cookiesString = await (await getContext()).getCookies();
714
+ const hasSessionToken = cookiesString.includes(NEON_AUTH_SESSION_COOKIE_NAME);
715
+ const sessionDataCookie = parseCookieValue(cookiesString, NEON_AUTH_SESSION_DATA_COOKIE_NAME);
716
+ if (sessionDataCookie && hasSessionToken) {
717
+ const result = await validateSessionData(sessionDataCookie, cookieSecret);
718
+ if (result.valid && result.payload) return {
719
+ data: result.payload,
720
+ error: null
721
+ };
722
+ }
723
+ } catch (error) {
724
+ console.error("[auth.getSession] Cookie validation error:", error);
725
+ }
726
+ return originalGetSession(...args);
727
+ };
728
+ return baseServer;
729
+ }
730
+ function isEndpointConfig(value) {
731
+ return typeof value === "object" && value !== null && "path" in value && "method" in value;
732
+ }
733
+ function createApiProxy(endpoints, fetchFn) {
734
+ return new Proxy({}, {
735
+ get(target, prop) {
736
+ if (prop in target) return target[prop];
737
+ const endpoint = endpoints[prop];
738
+ if (!endpoint) return;
739
+ if (isEndpointConfig(endpoint)) return (args) => fetchFn(endpoint.path, endpoint.method, args);
740
+ return createApiProxy(endpoint, fetchFn);
741
+ },
742
+ set(target, prop, value) {
743
+ target[prop] = value;
744
+ return true;
745
+ }
746
+ });
747
+ }
748
+
749
+ //#endregion
750
+ //#region src/next/server/adapter.ts
751
+ /**
752
+ * Creates a Next.js-specific RequestContext that reads cookies and headers
753
+ * from next/headers and handles cookie setting.
754
+ */
755
+ async function createNextRequestContext() {
756
+ const cookieStore = await cookies();
757
+ const headerStore = await headers();
758
+ return {
759
+ getCookies() {
760
+ return extractNeonAuthCookies(headerStore);
761
+ },
762
+ setCookie(name, value, options) {
763
+ cookieStore.set(name, value, options);
764
+ },
765
+ getHeader(name) {
766
+ return headerStore.get(name) ?? null;
767
+ },
768
+ getOrigin() {
769
+ return headerStore.get("origin") || headerStore.get("referer")?.split("/").slice(0, 3).join("/") || "";
770
+ },
771
+ getFramework() {
772
+ return "nextjs";
773
+ }
774
+ };
775
+ }
776
+
777
+ //#endregion
778
+ //#region src/server/config.ts
779
+ /**
780
+ * Framework-agnostic configuration types for Neon Auth
781
+ */
782
+ /**
783
+ * Validates cookie configuration meets security requirements
784
+ * @param cookies - The cookie configuration to validate
785
+ * @throws Error if secret is too short (< 32 characters)
786
+ */
787
+ function validateCookieConfig(cookies$1) {
788
+ if (!cookies$1.secret) throw new Error(ERRORS.MISSING_COOKIE_SECRET);
789
+ if (cookies$1.secret.length < 32) throw new Error(ERRORS.COOKIE_SECRET_TOO_SHORT);
790
+ if (cookies$1.sessionDataTtl !== void 0 && cookies$1.sessionDataTtl <= 0) throw new Error(ERRORS.INVALID_SESSION_DATA_TTL);
791
+ }
792
+
509
793
  //#endregion
510
794
  //#region src/server/proxy/request.ts
511
795
  const PROXY_HEADERS = [
@@ -612,8 +896,8 @@ const RESPONSE_HEADERS_ALLOWLIST = [
612
896
  * @returns New Response with proxied headers and session data cookie
613
897
  */
614
898
  const handleAuthResponse = async (response, baseUrl, cookieConfig) => {
615
- const responseHeaders = prepareResponseHeaders(response, cookieConfig.domain);
616
- const sessionDataCookie = await mintSessionData(response.headers, baseUrl, cookieConfig);
899
+ const responseHeaders = prepareResponseHeaders(response, cookieConfig);
900
+ const sessionDataCookie = await mintSessionDataFromResponse(response.headers, baseUrl, cookieConfig);
617
901
  if (sessionDataCookie) responseHeaders.append("Set-Cookie", sessionDataCookie);
618
902
  return new Response(response.body, {
619
903
  status: response.status,
@@ -621,85 +905,27 @@ const handleAuthResponse = async (response, baseUrl, cookieConfig) => {
621
905
  headers: responseHeaders
622
906
  });
623
907
  };
624
- const prepareResponseHeaders = (response, domain) => {
908
+ const prepareResponseHeaders = (response, cookieConfig) => {
625
909
  const headers$1 = new Headers();
910
+ const effectiveSameSite = cookieConfig.sameSite ?? "strict";
911
+ const { domain } = cookieConfig;
626
912
  for (const header of RESPONSE_HEADERS_ALLOWLIST) if (header === "set-cookie") {
627
913
  const cookies$1 = response.headers.getSetCookie();
628
- for (const cookieHeader of cookies$1) if (domain) {
914
+ for (const cookieHeader of cookies$1) {
629
915
  const parsedCookies = parseSetCookies(cookieHeader);
630
916
  for (const parsedCookie of parsedCookies) {
631
- parsedCookie.domain = domain;
917
+ parsedCookie.partitioned = void 0;
918
+ parsedCookie.sameSite = effectiveSameSite;
919
+ if (domain) parsedCookie.domain = domain;
632
920
  headers$1.append("Set-Cookie", serializeSetCookie(parsedCookie));
633
921
  }
634
- } else headers$1.append("Set-Cookie", cookieHeader);
922
+ }
635
923
  } else {
636
924
  const value = response.headers.get(header);
637
925
  if (value) headers$1.set(header, value);
638
926
  }
639
927
  return headers$1;
640
928
  };
641
- async function mintSessionData(headers$1, baseUrl, cookieConfig) {
642
- const { secret, sessionDataTtl, domain } = cookieConfig;
643
- const sessionToken = headers$1.getSetCookie().find((cookie) => cookie.includes("session_token"));
644
- if (!sessionToken) return null;
645
- if (sessionToken.toLowerCase().includes("max-age=0")) return serializeSetCookie({
646
- name: NEON_AUTH_SESSION_DATA_COOKIE_NAME,
647
- value: "",
648
- path: "/",
649
- domain,
650
- httpOnly: true,
651
- secure: true,
652
- sameSite: "lax",
653
- maxAge: 0
654
- });
655
- try {
656
- const sessionData = await fetchSessionWithCookie(sessionToken, baseUrl);
657
- if (sessionData.session) {
658
- const { value: signedData, expiresAt } = await signSessionDataCookie(sessionData, secret, sessionDataTtl);
659
- return serializeSetCookie({
660
- name: NEON_AUTH_SESSION_DATA_COOKIE_NAME,
661
- value: signedData,
662
- path: "/",
663
- domain,
664
- httpOnly: true,
665
- secure: true,
666
- sameSite: "lax",
667
- maxAge: Math.floor((expiresAt.getTime() - Date.now()) / 1e3)
668
- });
669
- }
670
- } catch (error) {
671
- const errorMessage = error instanceof Error ? error.message : String(error);
672
- const errorContext = {
673
- error: errorMessage,
674
- setCookieHeaderLength: sessionToken?.length || 0
675
- };
676
- if (errorMessage.includes("session_token not found")) console.warn("[mintSessionData] Session token missing in set-cookie:", errorContext);
677
- else if (errorMessage.includes("Failed to fetch session data")) console.error("[mintSessionData] Upstream /get-session request failed:", errorContext);
678
- else if (errorMessage.includes("NEON_AUTH_COOKIE_SECRET")) console.error("[mintSessionData] Cookie secret configuration error:", errorContext);
679
- else if (errorMessage.includes("Invalid date")) console.error("[mintSessionData] Date parsing error:", errorContext);
680
- else console.error("[mintSessionData] Unexpected error:", {
681
- ...errorContext,
682
- ...process.env.NODE_ENV !== "production" && { stack: error instanceof Error ? error.stack : void 0 }
683
- });
684
- }
685
- return null;
686
- }
687
- async function fetchSessionWithCookie(setCookieHeader, baseUrl) {
688
- const sessionToken = parseSetCookies(setCookieHeader).find((c) => c.name.includes("session_token"));
689
- if (!sessionToken) throw new Error("session_token not found in set-cookie header");
690
- const response = await fetch(`${baseUrl}/get-session`, {
691
- headers: { Cookie: `${sessionToken.name}=${sessionToken.value}` },
692
- signal: AbortSignal.timeout(3e3)
693
- });
694
- if (!response.ok) throw new Error(`Failed to fetch session data: ${response.status} ${response.statusText}`);
695
- let body;
696
- try {
697
- body = await response.json();
698
- } catch (error) {
699
- throw new Error(`Failed to parse /get-session response as JSON: ${error instanceof Error ? error.message : String(error)}`);
700
- }
701
- return parseSessionData(body);
702
- }
703
929
 
704
930
  //#endregion
705
931
  //#region src/server/session/cache-handler.ts
@@ -707,38 +933,68 @@ async function fetchSessionWithCookie(setCookieHeader, baseUrl) {
707
933
  * Attempts to retrieve session data from cookie cache
708
934
  * Returns Response with session data if cache hit, null otherwise
709
935
  *
936
+ * If session_data cookie is missing or invalid, attempts to mint a new one
937
+ * from the session_token cookie (reactive minting).
938
+ *
710
939
  * This is the framework-agnostic session cache optimization used by API handlers.
711
940
  *
712
941
  * @param request - Standard Web API Request object
713
- * @param cookieSecret - Secret for validating signed session cookies
942
+ * @param baseUrl - Auth server base URL for upstream calls
943
+ * @param cookieConfig - Cookie configuration (secret, TTL, domain)
714
944
  * @returns Response with session data JSON if cache hit, null if miss/disabled
715
945
  */
716
- async function trySessionCache(request, cookieSecret) {
946
+ async function trySessionCache(request, baseUrl, cookieConfig) {
717
947
  if (new URL(request.url).searchParams.get("disableCookieCache") === "true") return null;
718
- if (!(request.headers.get("cookie") || "").includes(NEON_AUTH_SESSION_COOKIE_NAME)) return null;
948
+ const cookieHeader = request.headers.get("cookie") || "";
949
+ if (!cookieHeader.includes(NEON_AUTH_SESSION_COOKIE_NAME)) return null;
950
+ const hasSessionData = parseCookies(cookieHeader).has(NEON_AUTH_SESSION_DATA_COOKIE_NAME);
951
+ const mintAndReturn = async () => {
952
+ const sessionTokenCookie = extractSessionTokenCookie(cookieHeader);
953
+ if (!sessionTokenCookie) return null;
954
+ const sessionDataCookieString = await mintSessionDataFromToken(sessionTokenCookie, baseUrl, cookieConfig);
955
+ if (!sessionDataCookieString) return null;
956
+ try {
957
+ const sessionData = await fetchSessionWithCookie(sessionTokenCookie, baseUrl);
958
+ if (!sessionData.session) return null;
959
+ const response = Response.json(sessionData);
960
+ response.headers.set("Set-Cookie", sessionDataCookieString);
961
+ return response;
962
+ } catch (error) {
963
+ console.error("[trySessionCache] Failed to fetch session after minting cookie:", error);
964
+ return null;
965
+ }
966
+ };
967
+ if (!hasSessionData) return await mintAndReturn();
719
968
  try {
720
- const sessionData = await getSessionDataFromCookie(request, NEON_AUTH_SESSION_DATA_COOKIE_NAME, cookieSecret);
969
+ const sessionData = await getSessionDataFromCookie(request, NEON_AUTH_SESSION_DATA_COOKIE_NAME, cookieConfig.secret);
721
970
  if (sessionData && sessionData.session) return Response.json(sessionData);
971
+ return await mintAndReturn();
722
972
  } catch (error) {
723
973
  const errorMessage = error instanceof Error ? error.message : String(error);
724
974
  const errorName = error instanceof Error ? error.name : "Unknown";
725
- if (errorName === "JWTExpired") console.debug("[trySessionCache] Session cookie expired (expected):", {
975
+ if (errorName === "JWTExpired") console.debug("[trySessionCache] Session cookie expired, minting new one:", {
726
976
  error: errorMessage,
727
- errorType: errorName,
728
977
  url: request.url
729
978
  });
730
- else if (errorName === "JWTInvalid" || errorName === "JWTClaimValidationFailed") console.warn("[trySessionCache] Invalid session cookie (possible tampering):", {
979
+ else if (errorName === "JWTInvalid" || errorName === "JWTClaimValidationFailed") console.warn("[trySessionCache] Invalid session cookie, minting new one:", {
731
980
  error: errorMessage,
732
- errorType: errorName,
733
981
  url: request.url
734
982
  });
735
- else console.error("[trySessionCache] Unexpected cookie validation error:", {
983
+ else console.error("[trySessionCache] Unexpected validation error:", {
736
984
  error: errorMessage,
737
- errorType: errorName,
738
985
  url: request.url
739
986
  });
987
+ return await mintAndReturn();
740
988
  }
741
- return null;
989
+ }
990
+ /**
991
+ * Extract session_token cookie value from cookie header
992
+ * @internal
993
+ */
994
+ function extractSessionTokenCookie(cookieHeader) {
995
+ const sessionToken = parseCookies(cookieHeader).get(NEON_AUTH_SESSION_COOKIE_NAME);
996
+ if (!sessionToken) return null;
997
+ return `${NEON_AUTH_SESSION_COOKIE_NAME}=${sessionToken}`;
742
998
  }
743
999
 
744
1000
  //#endregion
@@ -758,173 +1014,24 @@ async function trySessionCache(request, cookieSecret) {
758
1014
  * @returns Standard Web API Response
759
1015
  */
760
1016
  async function handleAuthProxyRequest(config) {
761
- const { request, path, baseUrl, cookieSecret, sessionDataTtl, domain } = config;
1017
+ const { request, path, baseUrl, cookieSecret, sessionDataTtl, domain, sameSite } = config;
762
1018
  if (path === API_ENDPOINTS.getSession.path && request.method === API_ENDPOINTS.getSession.method) {
763
- const cachedResponse = await trySessionCache(request, cookieSecret);
1019
+ const cachedResponse = await trySessionCache(request, baseUrl, {
1020
+ secret: cookieSecret,
1021
+ sessionDataTtl,
1022
+ domain,
1023
+ sameSite
1024
+ });
764
1025
  if (cachedResponse) return cachedResponse;
765
1026
  }
766
1027
  return await handleAuthResponse(await handleAuthRequest(baseUrl, request, path), baseUrl, {
767
1028
  secret: cookieSecret,
768
1029
  sessionDataTtl,
769
- domain
770
- });
771
- }
772
-
773
- //#endregion
774
- //#region src/server/client-factory.ts
775
- function createAuthServerInternal(config) {
776
- const { baseUrl, context: getContext, cookieSecret, sessionDataTtl, domain } = config;
777
- const fetchWithAuth = async (path, method, args) => {
778
- const ctx = await getContext();
779
- const cookies$1 = await ctx.getCookies();
780
- const origin = await ctx.getOrigin();
781
- const framework = ctx.getFramework();
782
- const url = new URL(path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
783
- const { query, fetchOptions: _fetchOptions, ...body } = args || {};
784
- if (query && typeof query === "object") {
785
- const queryParams = query;
786
- for (const [key, value] of Object.entries(queryParams)) if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
787
- }
788
- const headers$1 = {
789
- Cookie: cookies$1,
790
- Origin: origin,
791
- [NEON_AUTH_SERVER_PROXY_HEADER]: framework
792
- };
793
- let requestBody;
794
- if (method === "POST") {
795
- headers$1["Content-Type"] = "application/json";
796
- requestBody = JSON.stringify(Object.keys(body).length > 0 ? body : {});
797
- }
798
- const response = await fetch(url.toString(), {
799
- method,
800
- headers: headers$1,
801
- body: requestBody
802
- });
803
- const setCookieHeaders = response.headers.getSetCookie();
804
- if (setCookieHeaders.length > 0) {
805
- for (const setCookieHeader of setCookieHeaders) {
806
- const parsedCookies = parseSetCookies(setCookieHeader);
807
- for (const cookie of parsedCookies) {
808
- const cookieOptions = domain ? {
809
- ...cookie,
810
- domain
811
- } : cookie;
812
- await ctx.setCookie(cookie.name, cookie.value, cookieOptions);
813
- }
814
- }
815
- try {
816
- const sessionDataCookie = await mintSessionData(response.headers, baseUrl, {
817
- secret: cookieSecret,
818
- sessionDataTtl,
819
- domain
820
- });
821
- if (sessionDataCookie) {
822
- const [parsedSessionData] = parseSetCookies(sessionDataCookie);
823
- if (parsedSessionData) await ctx.setCookie(parsedSessionData.name, parsedSessionData.value, parsedSessionData);
824
- }
825
- } catch (error) {
826
- console.error("[fetchWithAuth] Failed to mint session data cookie:", error);
827
- }
828
- }
829
- const responseData = await response.json().catch(() => null);
830
- if (!response.ok) return {
831
- data: null,
832
- error: {
833
- message: responseData?.message || response.statusText,
834
- status: response.status,
835
- statusText: response.statusText
836
- }
837
- };
838
- return {
839
- data: responseData,
840
- error: null
841
- };
842
- };
843
- const baseServer = createApiProxy(API_ENDPOINTS, fetchWithAuth);
844
- const originalGetSession = baseServer.getSession;
845
- baseServer.getSession = async (...args) => {
846
- const [data] = args;
847
- if (!(data?.query?.disableCookieCache === "true")) try {
848
- const cookiesString = await (await getContext()).getCookies();
849
- const hasSessionToken = cookiesString.includes(NEON_AUTH_SESSION_COOKIE_NAME);
850
- const sessionDataCookie = parseCookieValue(cookiesString, NEON_AUTH_SESSION_DATA_COOKIE_NAME);
851
- if (sessionDataCookie && hasSessionToken) {
852
- const result = await validateSessionData(sessionDataCookie, cookieSecret);
853
- if (result.valid && result.payload) return {
854
- data: result.payload,
855
- error: null
856
- };
857
- }
858
- } catch (error) {
859
- console.error("[auth.getSession] Cookie validation error:", error);
860
- }
861
- return originalGetSession(...args);
862
- };
863
- return baseServer;
864
- }
865
- function isEndpointConfig(value) {
866
- return typeof value === "object" && value !== null && "path" in value && "method" in value;
867
- }
868
- function createApiProxy(endpoints, fetchFn) {
869
- return new Proxy({}, {
870
- get(target, prop) {
871
- if (prop in target) return target[prop];
872
- const endpoint = endpoints[prop];
873
- if (!endpoint) return;
874
- if (isEndpointConfig(endpoint)) return (args) => fetchFn(endpoint.path, endpoint.method, args);
875
- return createApiProxy(endpoint, fetchFn);
876
- },
877
- set(target, prop, value) {
878
- target[prop] = value;
879
- return true;
880
- }
1030
+ domain,
1031
+ sameSite
881
1032
  });
882
1033
  }
883
1034
 
884
- //#endregion
885
- //#region src/next/server/adapter.ts
886
- /**
887
- * Creates a Next.js-specific RequestContext that reads cookies and headers
888
- * from next/headers and handles cookie setting.
889
- */
890
- async function createNextRequestContext() {
891
- const cookieStore = await cookies();
892
- const headerStore = await headers();
893
- return {
894
- getCookies() {
895
- return extractNeonAuthCookies(headerStore);
896
- },
897
- setCookie(name, value, options) {
898
- cookieStore.set(name, value, options);
899
- },
900
- getHeader(name) {
901
- return headerStore.get(name) ?? null;
902
- },
903
- getOrigin() {
904
- return headerStore.get("origin") || headerStore.get("referer")?.split("/").slice(0, 3).join("/") || "";
905
- },
906
- getFramework() {
907
- return "nextjs";
908
- }
909
- };
910
- }
911
-
912
- //#endregion
913
- //#region src/server/config.ts
914
- /**
915
- * Framework-agnostic configuration types for Neon Auth
916
- */
917
- /**
918
- * Validates cookie configuration meets security requirements
919
- * @param cookies - The cookie configuration to validate
920
- * @throws Error if secret is too short (< 32 characters)
921
- */
922
- function validateCookieConfig(cookies$1) {
923
- if (!cookies$1.secret) throw new Error(ERRORS.MISSING_COOKIE_SECRET);
924
- if (cookies$1.secret.length < 32) throw new Error(ERRORS.COOKIE_SECRET_TOO_SHORT);
925
- if (cookies$1.sessionDataTtl !== void 0 && cookies$1.sessionDataTtl <= 0) throw new Error(ERRORS.INVALID_SESSION_DATA_TTL);
926
- }
927
-
928
1035
  //#endregion
929
1036
  //#region src/next/server/handler.ts
930
1037
  /**
@@ -964,7 +1071,8 @@ function authApiHandler(config) {
964
1071
  baseUrl,
965
1072
  cookieSecret: cookies$1.secret,
966
1073
  sessionDataTtl: cookies$1.sessionDataTtl,
967
- domain: cookies$1.domain
1074
+ domain: cookies$1.domain,
1075
+ sameSite: cookies$1.sameSite
968
1076
  });
969
1077
  };
970
1078
  return {
@@ -1003,7 +1111,7 @@ function needsSessionVerification(request) {
1003
1111
  * @param domain - Optional cookie domain
1004
1112
  * @returns Exchange result with redirect URL and cookies, or null if exchange not needed/failed
1005
1113
  */
1006
- async function exchangeOAuthToken(request, baseUrl, cookieSecret, sessionDataTtl, domain) {
1114
+ async function exchangeOAuthToken(request, baseUrl, cookieSecret, sessionDataTtl, domain, sameSite) {
1007
1115
  const url = new URL(request.url);
1008
1116
  const verifier = url.searchParams.get(NEON_AUTH_SESSION_VERIFIER_PARAM_NAME);
1009
1117
  const cookieHeader = request.headers.get("cookie");
@@ -1016,7 +1124,8 @@ async function exchangeOAuthToken(request, baseUrl, cookieSecret, sessionDataTtl
1016
1124
  }), "get-session"), baseUrl, {
1017
1125
  secret: cookieSecret,
1018
1126
  sessionDataTtl,
1019
- domain
1127
+ domain,
1128
+ sameSite
1020
1129
  });
1021
1130
  if (response.ok) {
1022
1131
  const setCookieHeaders = response.headers.getSetCookie();
@@ -1091,10 +1200,11 @@ function checkSessionRequired(pathname, skipRoutes, loginUrl, session) {
1091
1200
  * @returns Decision object indicating what action to take
1092
1201
  */
1093
1202
  async function processAuthMiddleware(config) {
1094
- const { request, pathname, skipRoutes, loginUrl, baseUrl, cookieSecret, sessionDataTtl, domain } = config;
1203
+ const { request, pathname, skipRoutes, loginUrl, baseUrl, cookieSecret, sessionDataTtl, domain, sameSite } = config;
1204
+ const effectiveSameSite = sameSite ?? "strict";
1095
1205
  if (pathname.startsWith(loginUrl)) return { action: "allow" };
1096
1206
  if (needsSessionVerification(request)) {
1097
- const exchangeResult = await exchangeOAuthToken(request, baseUrl, cookieSecret, sessionDataTtl, domain);
1207
+ const exchangeResult = await exchangeOAuthToken(request, baseUrl, cookieSecret, sessionDataTtl, domain, sameSite);
1098
1208
  if (exchangeResult !== null) return {
1099
1209
  action: "redirect_oauth",
1100
1210
  redirectUrl: exchangeResult.redirectUrl,
@@ -1108,6 +1218,7 @@ async function processAuthMiddleware(config) {
1108
1218
  session: null,
1109
1219
  user: null
1110
1220
  };
1221
+ let sessionCookies = [];
1111
1222
  if (hasSessionToken) {
1112
1223
  const sessionResponse = await handleAuthProxyRequest({
1113
1224
  request,
@@ -1115,16 +1226,19 @@ async function processAuthMiddleware(config) {
1115
1226
  baseUrl,
1116
1227
  cookieSecret,
1117
1228
  sessionDataTtl,
1118
- domain
1229
+ domain,
1230
+ sameSite
1119
1231
  });
1120
1232
  if (sessionResponse.ok) {
1121
1233
  const data = await sessionResponse.json().catch(() => null);
1122
1234
  if (data) sessionData = data;
1123
1235
  }
1236
+ sessionCookies = sessionResponse.headers.getSetCookie();
1124
1237
  }
1125
1238
  if (checkSessionRequired(pathname, skipRoutes, loginUrl, sessionData).allowed) return {
1126
1239
  action: "allow",
1127
- headers: { [NEON_AUTH_HEADER_MIDDLEWARE_NAME]: "true" }
1240
+ headers: { [NEON_AUTH_HEADER_MIDDLEWARE_NAME]: "true" },
1241
+ cookies: sessionCookies
1128
1242
  };
1129
1243
  const cookies$1 = [];
1130
1244
  if (hasStaleSessionData) cookies$1.push(serializeSetCookie({
@@ -1134,7 +1248,7 @@ async function processAuthMiddleware(config) {
1134
1248
  domain,
1135
1249
  httpOnly: true,
1136
1250
  secure: true,
1137
- sameSite: "lax",
1251
+ sameSite: effectiveSameSite,
1138
1252
  maxAge: 0
1139
1253
  }));
1140
1254
  return {
@@ -1193,13 +1307,16 @@ function neonAuthMiddleware(config) {
1193
1307
  baseUrl,
1194
1308
  cookieSecret: cookies$1.secret,
1195
1309
  sessionDataTtl: cookies$1.sessionDataTtl,
1196
- domain: cookies$1.domain
1310
+ domain: cookies$1.domain,
1311
+ sameSite: cookies$1.sameSite
1197
1312
  });
1198
1313
  switch (result.action) {
1199
1314
  case "allow": {
1200
1315
  const headers$1 = new Headers(request.headers);
1201
1316
  if (result.headers) for (const [key, value] of Object.entries(result.headers)) headers$1.set(key, value);
1202
- return NextResponse.next({ request: { headers: headers$1 } });
1317
+ const response = NextResponse.next({ request: { headers: headers$1 } });
1318
+ if (result.cookies) for (const cookie of result.cookies) response.headers.append("Set-Cookie", cookie);
1319
+ return response;
1203
1320
  }
1204
1321
  case "redirect_oauth": {
1205
1322
  const oauthHeaders = new Headers();
@@ -1319,7 +1436,8 @@ function createNeonAuth(config) {
1319
1436
  context: createNextRequestContext,
1320
1437
  cookieSecret: cookies$1.secret,
1321
1438
  sessionDataTtl: cookies$1.sessionDataTtl,
1322
- domain: cookies$1.domain
1439
+ domain: cookies$1.domain,
1440
+ sameSite: cookies$1.sameSite
1323
1441
  });
1324
1442
  /**
1325
1443
  * Creates API route handlers for Next.js
@@ -1370,4 +1488,4 @@ function createNeonAuth(config) {
1370
1488
  }
1371
1489
 
1372
1490
  //#endregion
1373
- export { createNeonAuth };
1491
+ export { AuthApiError, AuthError, createNeonAuth, isAuthApiError, isAuthError };