diff --git a/app/lib/persistence/useChatHistory.ts b/app/lib/persistence/useChatHistory.ts index b8b5c833..3077ca44 100644 --- a/app/lib/persistence/useChatHistory.ts +++ b/app/lib/persistence/useChatHistory.ts @@ -69,7 +69,7 @@ export function useChatHistory() { const summary = snapshot.summary; const rewindId = searchParams.get('rewindTo'); - let startingIdx = 0; + let startingIdx = -1; const endingIdx = rewindId ? storedMessages.messages.findIndex((m) => m.id === rewindId) + 1 : storedMessages.messages.length; @@ -80,13 +80,13 @@ export function useChatHistory() { } if (snapshotIndex > 0 && storedMessages.messages[snapshotIndex].id == rewindId) { - startingIdx = 0; + startingIdx = -1; } let filteredMessages = storedMessages.messages.slice(startingIdx + 1, endingIdx); let archivedMessages: Message[] = []; - if (startingIdx > 0) { + if (startingIdx >= 0) { archivedMessages = storedMessages.messages.slice(0, startingIdx + 1); } diff --git a/app/routes/api.git-proxy.$.ts b/app/routes/api.git-proxy.$.ts index 9e6cb3b1..45230520 100644 --- a/app/routes/api.git-proxy.$.ts +++ b/app/routes/api.git-proxy.$.ts @@ -1,6 +1,47 @@ import { json } from '@remix-run/cloudflare'; import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/cloudflare'; +// Allowed headers to forward to the target server +const ALLOW_HEADERS = [ + 'accept-encoding', + 'accept-language', + 'accept', + 'access-control-allow-origin', + 'authorization', + 'cache-control', + 'connection', + 'content-length', + 'content-type', + 'dnt', + 'pragma', + 'range', + 'referer', + 'user-agent', + 'x-authorization', + 'x-http-method-override', + 'x-requested-with', +]; + +// Headers to expose from the target server's response +const EXPOSE_HEADERS = [ + 'accept-ranges', + 'age', + 'cache-control', + 'content-length', + 'content-language', + 'content-type', + 'date', + 'etag', + 'expires', + 'last-modified', + 'pragma', + 'server', + 'transfer-encoding', + 'vary', + 'x-github-request-id', + 'x-redirected-url', +]; + // Handle all HTTP methods export async function action({ request, params }: ActionFunctionArgs) { return handleProxyRequest(request, params['*']); @@ -16,50 +57,117 @@ async function handleProxyRequest(request: Request, path: string | undefined) { return json({ error: 'Invalid proxy URL format' }, { status: 400 }); } - const url = new URL(request.url); - - // Reconstruct the target URL - const targetURL = `https://${path}${url.search}`; - - // Forward the request to the target URL - const response = await fetch(targetURL, { - method: request.method, - headers: { - ...Object.fromEntries(request.headers), - - // Override host header with the target host - host: new URL(targetURL).host, - }, - body: ['GET', 'HEAD'].includes(request.method) ? null : await request.arrayBuffer(), - }); - - // Create response with CORS headers - const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': '*', - }; - - // Handle preflight requests + // Handle CORS preflight request if (request.method === 'OPTIONS') { return new Response(null, { - headers: corsHeaders, - status: 204, + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', + 'Access-Control-Allow-Headers': ALLOW_HEADERS.join(', '), + 'Access-Control-Expose-Headers': EXPOSE_HEADERS.join(', '), + 'Access-Control-Max-Age': '86400', + }, }); } - // Forward the response with CORS headers - const responseHeaders = new Headers(response.headers); - Object.entries(corsHeaders).forEach(([key, value]) => { - responseHeaders.set(key, value); - }); + // Extract domain and remaining path + const parts = path.match(/([^\/]+)\/?(.*)/); + if (!parts) { + return json({ error: 'Invalid path format' }, { status: 400 }); + } + + const domain = parts[1]; + const remainingPath = parts[2] || ''; + + // Reconstruct the target URL with query parameters + const url = new URL(request.url); + const targetURL = `https://${domain}/${remainingPath}${url.search}`; + + console.log('Target URL:', targetURL); + + // Filter and prepare headers + const headers = new Headers(); + + // Only forward allowed headers + for (const header of ALLOW_HEADERS) { + if (request.headers.has(header)) { + headers.set(header, request.headers.get(header)!); + } + } + + // Set the host header + headers.set('Host', domain); + + // Set Git user agent if not already present + if (!headers.has('user-agent') || !headers.get('user-agent')?.startsWith('git/')) { + headers.set('User-Agent', 'git/@isomorphic-git/cors-proxy'); + } + + console.log('Request headers:', Object.fromEntries(headers.entries())); + + // Prepare fetch options + const fetchOptions: RequestInit = { + method: request.method, + headers, + redirect: 'follow', + }; + + // Add body and duplex option for non-GET/HEAD requests + if (!['GET', 'HEAD'].includes(request.method)) { + fetchOptions.body = request.body; + fetchOptions.duplex = 'half'; // This fixes the "duplex option is required when sending a body" error + } + + // Forward the request to the target URL + const response = await fetch(targetURL, fetchOptions); + + console.log('Response status:', response.status); + + // Create response headers + const responseHeaders = new Headers(); + + // Add CORS headers + responseHeaders.set('Access-Control-Allow-Origin', '*'); + responseHeaders.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); + responseHeaders.set('Access-Control-Allow-Headers', ALLOW_HEADERS.join(', ')); + responseHeaders.set('Access-Control-Expose-Headers', EXPOSE_HEADERS.join(', ')); + + // Copy exposed headers from the target response + for (const header of EXPOSE_HEADERS) { + // Skip content-length as we'll use the original response's content-length + if (header === 'content-length') { + continue; + } + + if (response.headers.has(header)) { + responseHeaders.set(header, response.headers.get(header)!); + } + } + + // If the response was redirected, add the x-redirected-url header + if (response.redirected) { + responseHeaders.set('x-redirected-url', response.url); + } + + console.log('Response headers:', Object.fromEntries(responseHeaders.entries())); + + // Return the response with the target's body stream piped directly return new Response(response.body, { status: response.status, + statusText: response.statusText, headers: responseHeaders, }); } catch (error) { - console.error('Git proxy error:', error); - return json({ error: 'Proxy error' }, { status: 500 }); + console.error('Proxy error:', error); + return json( + { + error: 'Proxy error', + message: error instanceof Error ? error.message : 'Unknown error', + url: path ? `https://${path}` : 'Invalid URL', + }, + { status: 500 }, + ); } }