From 344f7c99d395d4aab43c321ff1e6076b1d3ee4f8 Mon Sep 17 00:00:00 2001 From: Ryan Tamulevicz Date: Sun, 19 Oct 2025 09:30:46 -0400 Subject: [PATCH 1/4] feat(astro): add Astro integration with cookie handling and fetch client setup --- package.json | 13 ++ src/astro/index.ts | 323 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 src/astro/index.ts diff --git a/package.json b/package.json index 9dfee16..d79f3ab 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,18 @@ "default": "./dist/commonjs/react-start/index.js" } }, + "./astro": { + "import": { + "@convex-dev/component-source": "./src/astro/index.ts", + "types": "./dist/esm/astro/index.d.ts", + "default": "./dist/esm/astro/index.js" + }, + "require": { + "@convex-dev/component-source": "./src/astro/index.ts", + "types": "./dist/commonjs/astro/index.d.ts", + "default": "./dist/commonjs/astro/index.js" + } + }, "./utils": { "import": { "@convex-dev/component-source": "./src/utils/index.ts", @@ -168,6 +180,7 @@ "@types/react": "19.1.6", "@types/react-dom": "19.1.6", "@types/semver": "^7.7.0", + "astro": "^5.14.5", "chokidar-cli": "^3.0.0", "concurrently": "^9.2.0", "convex-test": "^0.0.33", diff --git a/src/astro/index.ts b/src/astro/index.ts new file mode 100644 index 0000000..2235838 --- /dev/null +++ b/src/astro/index.ts @@ -0,0 +1,323 @@ +import { betterFetch } from "@better-fetch/fetch"; +import type { betterAuth } from "better-auth"; +import { createCookieGetter } from "better-auth/cookies"; +import { ConvexHttpClient } from "convex/browser"; +import type { + FunctionReference, + FunctionReturnType, + GenericActionCtx, + GenericDataModel, +} from "convex/server"; +import type { APIContext, AstroCookies } from "astro"; +import { type CreateAuth, getStaticAuth } from "../client"; +import { JWT_COOKIE_NAME } from "../plugins/convex"; + +type CookieReader = (name: string) => string | undefined; +type CookieSource = AstroCookies | CookieReader | undefined; +type AstroRequestContext = + | Pick + | { + request: Request; + cookies?: CookieSource; + }; +type CookieInput = + | CookieSource + | AstroRequestContext + | Request + | Headers + | string + | { + request?: Request; + headers?: Headers | string; + cookies?: CookieSource | string; + cookie?: string; + }; + +const safeDecode = (value: string) => { + try { + return decodeURIComponent(value); + } catch { + return value; + } +}; + +const cookieReaderFromString = (cookieHeader: string): CookieReader => { + if (!cookieHeader) { + return () => undefined; + } + const pairs = cookieHeader.split(/;\s*/).filter(Boolean); + const store = new Map(); + for (const pair of pairs) { + const separatorIndex = pair.indexOf("="); + if (separatorIndex === -1) { + continue; + } + const key = safeDecode(pair.slice(0, separatorIndex).trim()); + const value = safeDecode(pair.slice(separatorIndex + 1)); + if (!store.has(key)) { + store.set(key, value); + } + } + if (store.size === 0) { + return () => undefined; + } + return (name) => { + if (!name) { + return undefined; + } + return store.get(name); + }; +}; + +const isAstroCookies = (source: unknown): source is AstroCookies => { + if (!source || typeof source !== "object") { + return false; + } + return ( + "get" in source && + typeof (source as { get?: unknown }).get === "function" && + "has" in source && + typeof (source as { has?: unknown }).has === "function" && + "merge" in source && + typeof (source as { merge?: unknown }).merge === "function" + ); +}; + +const isHeadersLike = (source: unknown): source is Headers => { + return typeof Headers !== "undefined" && source instanceof Headers; +}; + +const isRequestLike = (source: unknown): source is Request => { + if (typeof Request !== "undefined" && source instanceof Request) { + return true; + } + if (!source || typeof source !== "object") { + return false; + } + return ( + "headers" in source && + typeof (source as { headers?: unknown }).headers !== "undefined" && + "method" in source + ); +}; + +const normalizeCookieSource = (source: CookieInput): CookieSource => { + if (!source) { + return undefined; + } + if (typeof source === "function") { + return source; + } + if (isAstroCookies(source)) { + return source; + } + if (isRequestLike(source)) { + return normalizeCookieSource((source as Request).headers); + } + if (isHeadersLike(source)) { + const cookieHeader = (source as Headers).get("cookie") ?? ""; + return cookieHeader ? cookieReaderFromString(cookieHeader) : undefined; + } + if (typeof source === "string") { + return cookieReaderFromString(source); + } + if (typeof source === "object") { + if ("cookies" in source && source.cookies) { + const normalized = normalizeCookieSource(source.cookies as CookieInput); + if (normalized) { + return normalized; + } + } + if ("request" in source && source.request) { + const normalized = normalizeCookieSource(source.request as CookieInput); + if (normalized) { + return normalized; + } + } + if ("headers" in source && source.headers) { + const normalized = normalizeCookieSource(source.headers as CookieInput); + if (normalized) { + return normalized; + } + } + if ("cookie" in source && typeof source.cookie === "string") { + const normalized = normalizeCookieSource(source.cookie); + if (normalized) { + return normalized; + } + } + } + return undefined; +}; + +const createCookieReader = (source: CookieInput): CookieReader => { + const normalized = normalizeCookieSource(source); + if (typeof normalized === "function") { + return normalized; + } + if (normalized && typeof normalized.get === "function") { + return (name) => { + const cookie = normalized.get(name) as { value?: string } | undefined; + return typeof cookie?.value === "string" ? cookie.value : undefined; + }; + } + return () => undefined; +}; + +export const getCookieName = ( + createAuth: CreateAuth +) => { + const createCookie = createCookieGetter(getStaticAuth(createAuth).options); + const cookie = createCookie(JWT_COOKIE_NAME); + return cookie.name; +}; + +export const getToken = ( + createAuth: CreateAuth, + cookies?: CookieInput +) => { + const sessionCookieName = getCookieName(createAuth); + const readCookie = createCookieReader(cookies); + const token = readCookie(sessionCookieName); + + if (!token) { + const isSecure = sessionCookieName.startsWith("__Secure-"); + const insecureCookieName = sessionCookieName.replace("__Secure-", ""); + const secureCookieName = isSecure + ? sessionCookieName + : `__Secure-${insecureCookieName}`; + const secureToken = readCookie(secureCookieName); + const insecureToken = readCookie(insecureCookieName); + + if (isSecure && insecureToken) { + console.warn( + `Looking for secure cookie ${sessionCookieName} but found insecure cookie ${sessionCookieName.replace("__Secure-", "")}` + ); + } + if (!isSecure && secureToken) { + console.warn( + `Looking for insecure cookie ${sessionCookieName} but found secure cookie ${secureCookieName}` + ); + } + } + + return token; +}; + +export const setupFetchClient = async ( + createAuth: CreateAuth, + cookies?: CookieInput, + opts?: { convexUrl?: string } +) => { + const readCookie = createCookieReader(cookies); + const createClient = () => { + const convexUrl = opts?.convexUrl ?? process.env.VITE_CONVEX_URL; + if (!convexUrl) { + throw new Error("VITE_CONVEX_URL is not set"); + } + const sessionCookieName = getCookieName(createAuth); + const token = readCookie(sessionCookieName); + const client = new ConvexHttpClient(convexUrl); + if (token) { + client.setAuth(token); + } + return client; + }; + return { + fetchQuery< + Query extends FunctionReference<"query">, + FuncRef extends FunctionReference, + >( + query: Query, + args: FuncRef["_args"] + ): Promise> { + return createClient().query(query, args); + }, + fetchMutation< + Mutation extends FunctionReference<"mutation">, + FuncRef extends FunctionReference, + >( + mutation: Mutation, + args: FuncRef["_args"] + ): Promise> { + return createClient().mutation(mutation, args); + }, + fetchAction< + Action extends FunctionReference<"action">, + FuncRef extends FunctionReference, + >( + action: Action, + args: FuncRef["_args"] + ): Promise> { + return createClient().action(action, args); + }, + }; +}; + +export const fetchSession = async < + T extends (ctx: GenericActionCtx) => ReturnType, +>( + request: Request, + opts?: { + convexSiteUrl?: string; + verbose?: boolean; + } +) => { + type Session = ReturnType["$Infer"]["Session"]; + + if (!request) { + throw new Error("No request found"); + } + const convexSiteUrl = opts?.convexSiteUrl ?? process.env.VITE_CONVEX_SITE_URL; + if (!convexSiteUrl) { + throw new Error("VITE_CONVEX_SITE_URL is not set"); + } + const { data: session } = await betterFetch( + "/api/auth/get-session", + { + baseURL: convexSiteUrl, + headers: { + cookie: request.headers.get("cookie") ?? "", + }, + } + ); + return { + session, + }; +}; + +export const getAuth = async ( + context: AstroRequestContext, + createAuth: CreateAuth, + opts?: { convexSiteUrl?: string } +) => { + const { request } = context; + if (!request) { + throw new Error("No request found"); + } + const readCookie = createCookieReader(context); + const sessionCookieName = getCookieName(createAuth); + const token = readCookie(sessionCookieName); + const { session } = await fetchSession(request, opts); + return { + userId: session?.user.id, + token, + }; +}; + +const handler = (request: Request, opts?: { convexSiteUrl?: string }) => { + const requestUrl = new URL(request.url); + const convexSiteUrl = opts?.convexSiteUrl ?? process.env.VITE_CONVEX_SITE_URL; + if (!convexSiteUrl) { + throw new Error("VITE_CONVEX_SITE_URL is not set"); + } + const nextUrl = `${convexSiteUrl}${requestUrl.pathname}${requestUrl.search}`; + const forwardRequest = new Request(nextUrl, request); + forwardRequest.headers.set("accept-encoding", "application/json"); + return fetch(forwardRequest, { method: request.method, redirect: "manual" }); +}; + +export const astroHandler = + (opts?: { convexSiteUrl?: string }) => + async ({ request }: Pick) => + handler(request, opts); From 0d5a64be5e64555b10f1a4ebf24de994411651a3 Mon Sep 17 00:00:00 2001 From: Ryan Tamulevicz Date: Sun, 19 Oct 2025 09:42:43 -0400 Subject: [PATCH 2/4] refactor(astro): streamline cookie handling and improve type definitions in index.ts --- src/astro/index.ts | 193 +++++++++++++++------------------------------ 1 file changed, 63 insertions(+), 130 deletions(-) diff --git a/src/astro/index.ts b/src/astro/index.ts index 2235838..f64b993 100644 --- a/src/astro/index.ts +++ b/src/astro/index.ts @@ -1,6 +1,7 @@ import { betterFetch } from "@better-fetch/fetch"; -import type { betterAuth } from "better-auth"; +import { betterAuth } from "better-auth"; import { createCookieGetter } from "better-auth/cookies"; +import type { APIContext, AstroCookies } from "astro"; import { ConvexHttpClient } from "convex/browser"; import type { FunctionReference, @@ -8,30 +9,20 @@ import type { GenericActionCtx, GenericDataModel, } from "convex/server"; -import type { APIContext, AstroCookies } from "astro"; -import { type CreateAuth, getStaticAuth } from "../client"; import { JWT_COOKIE_NAME } from "../plugins/convex"; +import { type CreateAuth, getStaticAuth } from "../client"; -type CookieReader = (name: string) => string | undefined; -type CookieSource = AstroCookies | CookieReader | undefined; type AstroRequestContext = | Pick - | { - request: Request; - cookies?: CookieSource; - }; -type CookieInput = - | CookieSource + | { request: Request; cookies?: AstroCookies }; + +type CookieSource = | AstroRequestContext + | AstroCookies | Request | Headers | string - | { - request?: Request; - headers?: Headers | string; - cookies?: CookieSource | string; - cookie?: string; - }; + | undefined; const safeDecode = (value: string) => { try { @@ -41,127 +32,74 @@ const safeDecode = (value: string) => { } }; -const cookieReaderFromString = (cookieHeader: string): CookieReader => { +const isAstroCookies = (value: unknown): value is AstroCookies => + !!value && + typeof value === "object" && + "get" in value && + typeof (value as { get?: unknown }).get === "function"; + +const readCookieFromHeader = ( + cookieHeader: string | null | undefined, + name: string +) => { if (!cookieHeader) { - return () => undefined; + return undefined; } - const pairs = cookieHeader.split(/;\s*/).filter(Boolean); - const store = new Map(); - for (const pair of pairs) { + for (const pair of cookieHeader.split(/;\s*/)) { + if (!pair) { + continue; + } const separatorIndex = pair.indexOf("="); if (separatorIndex === -1) { continue; } const key = safeDecode(pair.slice(0, separatorIndex).trim()); - const value = safeDecode(pair.slice(separatorIndex + 1)); - if (!store.has(key)) { - store.set(key, value); - } - } - if (store.size === 0) { - return () => undefined; - } - return (name) => { - if (!name) { - return undefined; + if (key !== name) { + continue; } - return store.get(name); - }; -}; - -const isAstroCookies = (source: unknown): source is AstroCookies => { - if (!source || typeof source !== "object") { - return false; - } - return ( - "get" in source && - typeof (source as { get?: unknown }).get === "function" && - "has" in source && - typeof (source as { has?: unknown }).has === "function" && - "merge" in source && - typeof (source as { merge?: unknown }).merge === "function" - ); -}; - -const isHeadersLike = (source: unknown): source is Headers => { - return typeof Headers !== "undefined" && source instanceof Headers; -}; - -const isRequestLike = (source: unknown): source is Request => { - if (typeof Request !== "undefined" && source instanceof Request) { - return true; + const rawValue = pair.slice(separatorIndex + 1); + return safeDecode(rawValue); } - if (!source || typeof source !== "object") { - return false; - } - return ( - "headers" in source && - typeof (source as { headers?: unknown }).headers !== "undefined" && - "method" in source - ); + return undefined; }; -const normalizeCookieSource = (source: CookieInput): CookieSource => { +const readCookie = (source: CookieSource, name: string): string | undefined => { if (!source) { return undefined; } - if (typeof source === "function") { - return source; - } - if (isAstroCookies(source)) { - return source; + if (typeof source === "string") { + return readCookieFromHeader(source, name); } - if (isRequestLike(source)) { - return normalizeCookieSource((source as Request).headers); + if (source instanceof Headers) { + return readCookieFromHeader(source.get("cookie"), name); } - if (isHeadersLike(source)) { - const cookieHeader = (source as Headers).get("cookie") ?? ""; - return cookieHeader ? cookieReaderFromString(cookieHeader) : undefined; + if (source instanceof Request) { + return readCookie(source.headers, name); } - if (typeof source === "string") { - return cookieReaderFromString(source); + if (isAstroCookies(source)) { + const cookie = source.get(name); + return typeof cookie?.value === "string" ? cookie.value : undefined; } - if (typeof source === "object") { - if ("cookies" in source && source.cookies) { - const normalized = normalizeCookieSource(source.cookies as CookieInput); - if (normalized) { - return normalized; - } - } - if ("request" in source && source.request) { - const normalized = normalizeCookieSource(source.request as CookieInput); - if (normalized) { - return normalized; - } - } - if ("headers" in source && source.headers) { - const normalized = normalizeCookieSource(source.headers as CookieInput); - if (normalized) { - return normalized; - } + if ( + typeof source === "object" && + source !== null && + "cookies" in source && + source.cookies + ) { + const fromStore = readCookie(source.cookies, name); + if (fromStore) { + return fromStore; } - if ("cookie" in source && typeof source.cookie === "string") { - const normalized = normalizeCookieSource(source.cookie); - if (normalized) { - return normalized; - } - } - } - return undefined; -}; - -const createCookieReader = (source: CookieInput): CookieReader => { - const normalized = normalizeCookieSource(source); - if (typeof normalized === "function") { - return normalized; } - if (normalized && typeof normalized.get === "function") { - return (name) => { - const cookie = normalized.get(name) as { value?: string } | undefined; - return typeof cookie?.value === "string" ? cookie.value : undefined; - }; + if ( + typeof source === "object" && + source !== null && + "request" in source && + source.request + ) { + return readCookie(source.request, name); } - return () => undefined; + return undefined; }; export const getCookieName = ( @@ -174,11 +112,10 @@ export const getCookieName = ( export const getToken = ( createAuth: CreateAuth, - cookies?: CookieInput + cookies?: CookieSource ) => { const sessionCookieName = getCookieName(createAuth); - const readCookie = createCookieReader(cookies); - const token = readCookie(sessionCookieName); + const token = readCookie(cookies, sessionCookieName); if (!token) { const isSecure = sessionCookieName.startsWith("__Secure-"); @@ -186,12 +123,12 @@ export const getToken = ( const secureCookieName = isSecure ? sessionCookieName : `__Secure-${insecureCookieName}`; - const secureToken = readCookie(secureCookieName); - const insecureToken = readCookie(insecureCookieName); + const secureToken = readCookie(cookies, secureCookieName); + const insecureToken = readCookie(cookies, insecureCookieName); if (isSecure && insecureToken) { console.warn( - `Looking for secure cookie ${sessionCookieName} but found insecure cookie ${sessionCookieName.replace("__Secure-", "")}` + `Looking for secure cookie ${sessionCookieName} but found insecure cookie ${insecureCookieName}` ); } if (!isSecure && secureToken) { @@ -206,18 +143,16 @@ export const getToken = ( export const setupFetchClient = async ( createAuth: CreateAuth, - cookies?: CookieInput, + cookies?: CookieSource, opts?: { convexUrl?: string } ) => { - const readCookie = createCookieReader(cookies); const createClient = () => { const convexUrl = opts?.convexUrl ?? process.env.VITE_CONVEX_URL; if (!convexUrl) { throw new Error("VITE_CONVEX_URL is not set"); } - const sessionCookieName = getCookieName(createAuth); - const token = readCookie(sessionCookieName); const client = new ConvexHttpClient(convexUrl); + const token = getToken(createAuth, cookies); if (token) { client.setAuth(token); } @@ -295,9 +230,7 @@ export const getAuth = async ( if (!request) { throw new Error("No request found"); } - const readCookie = createCookieReader(context); - const sessionCookieName = getCookieName(createAuth); - const token = readCookie(sessionCookieName); + const token = getToken(createAuth, context); const { session } = await fetchSession(request, opts); return { userId: session?.user.id, From 5244bb2c8ec846189c752ca59dbe1b99f6755dad Mon Sep 17 00:00:00 2001 From: Ryan Tamulevicz Date: Wed, 22 Oct 2025 07:13:27 -0400 Subject: [PATCH 3/4] Revert "refactor(astro): streamline cookie handling and improve type definitions in index.ts" This reverts commit 0d5a64be5e64555b10f1a4ebf24de994411651a3. --- src/astro/index.ts | 193 ++++++++++++++++++++++++++++++--------------- 1 file changed, 130 insertions(+), 63 deletions(-) diff --git a/src/astro/index.ts b/src/astro/index.ts index f64b993..2235838 100644 --- a/src/astro/index.ts +++ b/src/astro/index.ts @@ -1,7 +1,6 @@ import { betterFetch } from "@better-fetch/fetch"; -import { betterAuth } from "better-auth"; +import type { betterAuth } from "better-auth"; import { createCookieGetter } from "better-auth/cookies"; -import type { APIContext, AstroCookies } from "astro"; import { ConvexHttpClient } from "convex/browser"; import type { FunctionReference, @@ -9,20 +8,30 @@ import type { GenericActionCtx, GenericDataModel, } from "convex/server"; -import { JWT_COOKIE_NAME } from "../plugins/convex"; +import type { APIContext, AstroCookies } from "astro"; import { type CreateAuth, getStaticAuth } from "../client"; +import { JWT_COOKIE_NAME } from "../plugins/convex"; +type CookieReader = (name: string) => string | undefined; +type CookieSource = AstroCookies | CookieReader | undefined; type AstroRequestContext = | Pick - | { request: Request; cookies?: AstroCookies }; - -type CookieSource = + | { + request: Request; + cookies?: CookieSource; + }; +type CookieInput = + | CookieSource | AstroRequestContext - | AstroCookies | Request | Headers | string - | undefined; + | { + request?: Request; + headers?: Headers | string; + cookies?: CookieSource | string; + cookie?: string; + }; const safeDecode = (value: string) => { try { @@ -32,76 +41,129 @@ const safeDecode = (value: string) => { } }; -const isAstroCookies = (value: unknown): value is AstroCookies => - !!value && - typeof value === "object" && - "get" in value && - typeof (value as { get?: unknown }).get === "function"; - -const readCookieFromHeader = ( - cookieHeader: string | null | undefined, - name: string -) => { +const cookieReaderFromString = (cookieHeader: string): CookieReader => { if (!cookieHeader) { - return undefined; + return () => undefined; } - for (const pair of cookieHeader.split(/;\s*/)) { - if (!pair) { - continue; - } + const pairs = cookieHeader.split(/;\s*/).filter(Boolean); + const store = new Map(); + for (const pair of pairs) { const separatorIndex = pair.indexOf("="); if (separatorIndex === -1) { continue; } const key = safeDecode(pair.slice(0, separatorIndex).trim()); - if (key !== name) { - continue; + const value = safeDecode(pair.slice(separatorIndex + 1)); + if (!store.has(key)) { + store.set(key, value); } - const rawValue = pair.slice(separatorIndex + 1); - return safeDecode(rawValue); } - return undefined; + if (store.size === 0) { + return () => undefined; + } + return (name) => { + if (!name) { + return undefined; + } + return store.get(name); + }; +}; + +const isAstroCookies = (source: unknown): source is AstroCookies => { + if (!source || typeof source !== "object") { + return false; + } + return ( + "get" in source && + typeof (source as { get?: unknown }).get === "function" && + "has" in source && + typeof (source as { has?: unknown }).has === "function" && + "merge" in source && + typeof (source as { merge?: unknown }).merge === "function" + ); +}; + +const isHeadersLike = (source: unknown): source is Headers => { + return typeof Headers !== "undefined" && source instanceof Headers; +}; + +const isRequestLike = (source: unknown): source is Request => { + if (typeof Request !== "undefined" && source instanceof Request) { + return true; + } + if (!source || typeof source !== "object") { + return false; + } + return ( + "headers" in source && + typeof (source as { headers?: unknown }).headers !== "undefined" && + "method" in source + ); }; -const readCookie = (source: CookieSource, name: string): string | undefined => { +const normalizeCookieSource = (source: CookieInput): CookieSource => { if (!source) { return undefined; } - if (typeof source === "string") { - return readCookieFromHeader(source, name); + if (typeof source === "function") { + return source; } - if (source instanceof Headers) { - return readCookieFromHeader(source.get("cookie"), name); + if (isAstroCookies(source)) { + return source; } - if (source instanceof Request) { - return readCookie(source.headers, name); + if (isRequestLike(source)) { + return normalizeCookieSource((source as Request).headers); } - if (isAstroCookies(source)) { - const cookie = source.get(name); - return typeof cookie?.value === "string" ? cookie.value : undefined; + if (isHeadersLike(source)) { + const cookieHeader = (source as Headers).get("cookie") ?? ""; + return cookieHeader ? cookieReaderFromString(cookieHeader) : undefined; } - if ( - typeof source === "object" && - source !== null && - "cookies" in source && - source.cookies - ) { - const fromStore = readCookie(source.cookies, name); - if (fromStore) { - return fromStore; - } + if (typeof source === "string") { + return cookieReaderFromString(source); } - if ( - typeof source === "object" && - source !== null && - "request" in source && - source.request - ) { - return readCookie(source.request, name); + if (typeof source === "object") { + if ("cookies" in source && source.cookies) { + const normalized = normalizeCookieSource(source.cookies as CookieInput); + if (normalized) { + return normalized; + } + } + if ("request" in source && source.request) { + const normalized = normalizeCookieSource(source.request as CookieInput); + if (normalized) { + return normalized; + } + } + if ("headers" in source && source.headers) { + const normalized = normalizeCookieSource(source.headers as CookieInput); + if (normalized) { + return normalized; + } + } + if ("cookie" in source && typeof source.cookie === "string") { + const normalized = normalizeCookieSource(source.cookie); + if (normalized) { + return normalized; + } + } } return undefined; }; +const createCookieReader = (source: CookieInput): CookieReader => { + const normalized = normalizeCookieSource(source); + if (typeof normalized === "function") { + return normalized; + } + if (normalized && typeof normalized.get === "function") { + return (name) => { + const cookie = normalized.get(name) as { value?: string } | undefined; + return typeof cookie?.value === "string" ? cookie.value : undefined; + }; + } + return () => undefined; +}; + export const getCookieName = ( createAuth: CreateAuth ) => { @@ -112,10 +174,11 @@ export const getCookieName = ( export const getToken = ( createAuth: CreateAuth, - cookies?: CookieSource + cookies?: CookieInput ) => { const sessionCookieName = getCookieName(createAuth); - const token = readCookie(cookies, sessionCookieName); + const readCookie = createCookieReader(cookies); + const token = readCookie(sessionCookieName); if (!token) { const isSecure = sessionCookieName.startsWith("__Secure-"); @@ -123,12 +186,12 @@ export const getToken = ( const secureCookieName = isSecure ? sessionCookieName : `__Secure-${insecureCookieName}`; - const secureToken = readCookie(cookies, secureCookieName); - const insecureToken = readCookie(cookies, insecureCookieName); + const secureToken = readCookie(secureCookieName); + const insecureToken = readCookie(insecureCookieName); if (isSecure && insecureToken) { console.warn( - `Looking for secure cookie ${sessionCookieName} but found insecure cookie ${insecureCookieName}` + `Looking for secure cookie ${sessionCookieName} but found insecure cookie ${sessionCookieName.replace("__Secure-", "")}` ); } if (!isSecure && secureToken) { @@ -143,16 +206,18 @@ export const getToken = ( export const setupFetchClient = async ( createAuth: CreateAuth, - cookies?: CookieSource, + cookies?: CookieInput, opts?: { convexUrl?: string } ) => { + const readCookie = createCookieReader(cookies); const createClient = () => { const convexUrl = opts?.convexUrl ?? process.env.VITE_CONVEX_URL; if (!convexUrl) { throw new Error("VITE_CONVEX_URL is not set"); } + const sessionCookieName = getCookieName(createAuth); + const token = readCookie(sessionCookieName); const client = new ConvexHttpClient(convexUrl); - const token = getToken(createAuth, cookies); if (token) { client.setAuth(token); } @@ -230,7 +295,9 @@ export const getAuth = async ( if (!request) { throw new Error("No request found"); } - const token = getToken(createAuth, context); + const readCookie = createCookieReader(context); + const sessionCookieName = getCookieName(createAuth); + const token = readCookie(sessionCookieName); const { session } = await fetchSession(request, opts); return { userId: session?.user.id, From 669098c707e49885c2fd8580ec012fe00281f182 Mon Sep 17 00:00:00 2001 From: Ryan Tamulevicz Date: Wed, 22 Oct 2025 07:13:38 -0400 Subject: [PATCH 4/4] Revert "feat(astro): add Astro integration with cookie handling and fetch client setup" This reverts commit 344f7c99d395d4aab43c321ff1e6076b1d3ee4f8. --- package.json | 13 -- src/astro/index.ts | 323 --------------------------------------------- 2 files changed, 336 deletions(-) delete mode 100644 src/astro/index.ts diff --git a/package.json b/package.json index d79f3ab..9dfee16 100644 --- a/package.json +++ b/package.json @@ -125,18 +125,6 @@ "default": "./dist/commonjs/react-start/index.js" } }, - "./astro": { - "import": { - "@convex-dev/component-source": "./src/astro/index.ts", - "types": "./dist/esm/astro/index.d.ts", - "default": "./dist/esm/astro/index.js" - }, - "require": { - "@convex-dev/component-source": "./src/astro/index.ts", - "types": "./dist/commonjs/astro/index.d.ts", - "default": "./dist/commonjs/astro/index.js" - } - }, "./utils": { "import": { "@convex-dev/component-source": "./src/utils/index.ts", @@ -180,7 +168,6 @@ "@types/react": "19.1.6", "@types/react-dom": "19.1.6", "@types/semver": "^7.7.0", - "astro": "^5.14.5", "chokidar-cli": "^3.0.0", "concurrently": "^9.2.0", "convex-test": "^0.0.33", diff --git a/src/astro/index.ts b/src/astro/index.ts deleted file mode 100644 index 2235838..0000000 --- a/src/astro/index.ts +++ /dev/null @@ -1,323 +0,0 @@ -import { betterFetch } from "@better-fetch/fetch"; -import type { betterAuth } from "better-auth"; -import { createCookieGetter } from "better-auth/cookies"; -import { ConvexHttpClient } from "convex/browser"; -import type { - FunctionReference, - FunctionReturnType, - GenericActionCtx, - GenericDataModel, -} from "convex/server"; -import type { APIContext, AstroCookies } from "astro"; -import { type CreateAuth, getStaticAuth } from "../client"; -import { JWT_COOKIE_NAME } from "../plugins/convex"; - -type CookieReader = (name: string) => string | undefined; -type CookieSource = AstroCookies | CookieReader | undefined; -type AstroRequestContext = - | Pick - | { - request: Request; - cookies?: CookieSource; - }; -type CookieInput = - | CookieSource - | AstroRequestContext - | Request - | Headers - | string - | { - request?: Request; - headers?: Headers | string; - cookies?: CookieSource | string; - cookie?: string; - }; - -const safeDecode = (value: string) => { - try { - return decodeURIComponent(value); - } catch { - return value; - } -}; - -const cookieReaderFromString = (cookieHeader: string): CookieReader => { - if (!cookieHeader) { - return () => undefined; - } - const pairs = cookieHeader.split(/;\s*/).filter(Boolean); - const store = new Map(); - for (const pair of pairs) { - const separatorIndex = pair.indexOf("="); - if (separatorIndex === -1) { - continue; - } - const key = safeDecode(pair.slice(0, separatorIndex).trim()); - const value = safeDecode(pair.slice(separatorIndex + 1)); - if (!store.has(key)) { - store.set(key, value); - } - } - if (store.size === 0) { - return () => undefined; - } - return (name) => { - if (!name) { - return undefined; - } - return store.get(name); - }; -}; - -const isAstroCookies = (source: unknown): source is AstroCookies => { - if (!source || typeof source !== "object") { - return false; - } - return ( - "get" in source && - typeof (source as { get?: unknown }).get === "function" && - "has" in source && - typeof (source as { has?: unknown }).has === "function" && - "merge" in source && - typeof (source as { merge?: unknown }).merge === "function" - ); -}; - -const isHeadersLike = (source: unknown): source is Headers => { - return typeof Headers !== "undefined" && source instanceof Headers; -}; - -const isRequestLike = (source: unknown): source is Request => { - if (typeof Request !== "undefined" && source instanceof Request) { - return true; - } - if (!source || typeof source !== "object") { - return false; - } - return ( - "headers" in source && - typeof (source as { headers?: unknown }).headers !== "undefined" && - "method" in source - ); -}; - -const normalizeCookieSource = (source: CookieInput): CookieSource => { - if (!source) { - return undefined; - } - if (typeof source === "function") { - return source; - } - if (isAstroCookies(source)) { - return source; - } - if (isRequestLike(source)) { - return normalizeCookieSource((source as Request).headers); - } - if (isHeadersLike(source)) { - const cookieHeader = (source as Headers).get("cookie") ?? ""; - return cookieHeader ? cookieReaderFromString(cookieHeader) : undefined; - } - if (typeof source === "string") { - return cookieReaderFromString(source); - } - if (typeof source === "object") { - if ("cookies" in source && source.cookies) { - const normalized = normalizeCookieSource(source.cookies as CookieInput); - if (normalized) { - return normalized; - } - } - if ("request" in source && source.request) { - const normalized = normalizeCookieSource(source.request as CookieInput); - if (normalized) { - return normalized; - } - } - if ("headers" in source && source.headers) { - const normalized = normalizeCookieSource(source.headers as CookieInput); - if (normalized) { - return normalized; - } - } - if ("cookie" in source && typeof source.cookie === "string") { - const normalized = normalizeCookieSource(source.cookie); - if (normalized) { - return normalized; - } - } - } - return undefined; -}; - -const createCookieReader = (source: CookieInput): CookieReader => { - const normalized = normalizeCookieSource(source); - if (typeof normalized === "function") { - return normalized; - } - if (normalized && typeof normalized.get === "function") { - return (name) => { - const cookie = normalized.get(name) as { value?: string } | undefined; - return typeof cookie?.value === "string" ? cookie.value : undefined; - }; - } - return () => undefined; -}; - -export const getCookieName = ( - createAuth: CreateAuth -) => { - const createCookie = createCookieGetter(getStaticAuth(createAuth).options); - const cookie = createCookie(JWT_COOKIE_NAME); - return cookie.name; -}; - -export const getToken = ( - createAuth: CreateAuth, - cookies?: CookieInput -) => { - const sessionCookieName = getCookieName(createAuth); - const readCookie = createCookieReader(cookies); - const token = readCookie(sessionCookieName); - - if (!token) { - const isSecure = sessionCookieName.startsWith("__Secure-"); - const insecureCookieName = sessionCookieName.replace("__Secure-", ""); - const secureCookieName = isSecure - ? sessionCookieName - : `__Secure-${insecureCookieName}`; - const secureToken = readCookie(secureCookieName); - const insecureToken = readCookie(insecureCookieName); - - if (isSecure && insecureToken) { - console.warn( - `Looking for secure cookie ${sessionCookieName} but found insecure cookie ${sessionCookieName.replace("__Secure-", "")}` - ); - } - if (!isSecure && secureToken) { - console.warn( - `Looking for insecure cookie ${sessionCookieName} but found secure cookie ${secureCookieName}` - ); - } - } - - return token; -}; - -export const setupFetchClient = async ( - createAuth: CreateAuth, - cookies?: CookieInput, - opts?: { convexUrl?: string } -) => { - const readCookie = createCookieReader(cookies); - const createClient = () => { - const convexUrl = opts?.convexUrl ?? process.env.VITE_CONVEX_URL; - if (!convexUrl) { - throw new Error("VITE_CONVEX_URL is not set"); - } - const sessionCookieName = getCookieName(createAuth); - const token = readCookie(sessionCookieName); - const client = new ConvexHttpClient(convexUrl); - if (token) { - client.setAuth(token); - } - return client; - }; - return { - fetchQuery< - Query extends FunctionReference<"query">, - FuncRef extends FunctionReference, - >( - query: Query, - args: FuncRef["_args"] - ): Promise> { - return createClient().query(query, args); - }, - fetchMutation< - Mutation extends FunctionReference<"mutation">, - FuncRef extends FunctionReference, - >( - mutation: Mutation, - args: FuncRef["_args"] - ): Promise> { - return createClient().mutation(mutation, args); - }, - fetchAction< - Action extends FunctionReference<"action">, - FuncRef extends FunctionReference, - >( - action: Action, - args: FuncRef["_args"] - ): Promise> { - return createClient().action(action, args); - }, - }; -}; - -export const fetchSession = async < - T extends (ctx: GenericActionCtx) => ReturnType, ->( - request: Request, - opts?: { - convexSiteUrl?: string; - verbose?: boolean; - } -) => { - type Session = ReturnType["$Infer"]["Session"]; - - if (!request) { - throw new Error("No request found"); - } - const convexSiteUrl = opts?.convexSiteUrl ?? process.env.VITE_CONVEX_SITE_URL; - if (!convexSiteUrl) { - throw new Error("VITE_CONVEX_SITE_URL is not set"); - } - const { data: session } = await betterFetch( - "/api/auth/get-session", - { - baseURL: convexSiteUrl, - headers: { - cookie: request.headers.get("cookie") ?? "", - }, - } - ); - return { - session, - }; -}; - -export const getAuth = async ( - context: AstroRequestContext, - createAuth: CreateAuth, - opts?: { convexSiteUrl?: string } -) => { - const { request } = context; - if (!request) { - throw new Error("No request found"); - } - const readCookie = createCookieReader(context); - const sessionCookieName = getCookieName(createAuth); - const token = readCookie(sessionCookieName); - const { session } = await fetchSession(request, opts); - return { - userId: session?.user.id, - token, - }; -}; - -const handler = (request: Request, opts?: { convexSiteUrl?: string }) => { - const requestUrl = new URL(request.url); - const convexSiteUrl = opts?.convexSiteUrl ?? process.env.VITE_CONVEX_SITE_URL; - if (!convexSiteUrl) { - throw new Error("VITE_CONVEX_SITE_URL is not set"); - } - const nextUrl = `${convexSiteUrl}${requestUrl.pathname}${requestUrl.search}`; - const forwardRequest = new Request(nextUrl, request); - forwardRequest.headers.set("accept-encoding", "application/json"); - return fetch(forwardRequest, { method: request.method, redirect: "manual" }); -}; - -export const astroHandler = - (opts?: { convexSiteUrl?: string }) => - async ({ request }: Pick) => - handler(request, opts);