mirror of https://github.com/buster-so/buster.git
Add secret to electric url
This commit is contained in:
parent
d93e51cb61
commit
eb288b1282
10
.env.example
10
.env.example
|
@ -12,6 +12,7 @@ SUPABASE_SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey AgCiAgICAicm9
|
|||
POSTHOG_TELEMETRY_KEY="phc_zZraCicSTfeXX5b9wWQv2rWG8QB4Z3xlotOT7gFtoNi"
|
||||
TELEMETRY_ENABLED="true"
|
||||
MAX_RECURSION="15"
|
||||
SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey AgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
|
||||
|
||||
# AI VARS
|
||||
RERANK_API_KEY="your_rerank_api_key"
|
||||
|
@ -27,3 +28,12 @@ NEXT_PUBLIC_SUPABASE_URL="http://kong:8000" # External URL for Supabase (Kong pr
|
|||
NEXT_PUBLIC_WS_URL="ws://localhost:3001"
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey AgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
|
||||
NEXT_PRIVATE_SUPABASE_SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey AgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
||||
|
||||
# TS SERVER
|
||||
SERVER_PORT=3002
|
||||
|
||||
# ELECTRIC
|
||||
ELECTRIC_PROXY_URL=http://localhost:3003
|
||||
ELECTRIC_PORT=3003
|
||||
ELECTRIC_INSECURE=false
|
||||
ELECTRIC_SECRET=my-little-buttercup-has-the-sweetest-smile
|
||||
|
|
|
@ -2,7 +2,10 @@ services:
|
|||
electric:
|
||||
image: electricsql/electric
|
||||
ports:
|
||||
- "3003:3000" # Expose Electric's HTTP API on port 3011 instead of 3000
|
||||
- "3003:3003" # Expose Electric's HTTP API on port 3003 instead of 3000
|
||||
environment:
|
||||
DATABASE_URL: "postgresql://postgres:postgres@host.docker.internal:54322/postgres?sslmode=disable"
|
||||
ELECTRIC_INSECURE: true
|
||||
ELECTRIC_INSECURE: ${ELECTRIC_INSECURE:-false}
|
||||
ELECTRIC_PROXY_URL: ${ELECTRIC_PROXY_URL:-http://localhost:3003}
|
||||
ELECTRIC_PORT: ${ELECTRIC_PORT:-3003}
|
||||
ELECTRIC_SECRET: ${ELECTRIC_SECRET:-my-little-buttercup-has-the-sweetest-smile}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
include ../.env
|
||||
export
|
||||
|
||||
dev:
|
||||
docker compose stop && docker compose up -d
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
PORT=3002
|
||||
SUPABASE_URL="http://127.0.0.1:54321"
|
||||
SUPABASE_SERVICE_ROLE_KEY=""
|
||||
ELECTRIC_URL="http://localhost:3003"
|
||||
ELECTRIC_PROXY_URL="http://localhost:3003"
|
|
@ -0,0 +1,270 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createProxiedResponse } from './electricHandler';
|
||||
|
||||
describe('createProxiedResponse', () => {
|
||||
const mockFetch = vi.fn<typeof fetch>();
|
||||
let originalEnv: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('fetch', mockFetch);
|
||||
vi.clearAllMocks();
|
||||
// Store original environment variable
|
||||
originalEnv = process.env.ELECTRIC_SECRET;
|
||||
// Set default secret for tests
|
||||
process.env.ELECTRIC_SECRET = 'test-secret';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
// Restore original environment variable
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.ELECTRIC_SECRET = originalEnv;
|
||||
} else {
|
||||
process.env.ELECTRIC_SECRET = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
it('should proxy a successful response and remove content-encoding and content-length headers', async () => {
|
||||
const testUrl = new URL('https://example.com/test');
|
||||
const mockResponseBody = 'test response body';
|
||||
|
||||
// Create mock headers that include content-encoding and content-length
|
||||
const mockHeaders = new Headers({
|
||||
'content-type': 'application/json',
|
||||
'content-encoding': 'gzip',
|
||||
'content-length': '123',
|
||||
'cache-control': 'no-cache',
|
||||
'custom-header': 'custom-value',
|
||||
});
|
||||
|
||||
const mockResponse = new Response(mockResponseBody, {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: mockHeaders,
|
||||
});
|
||||
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await createProxiedResponse(testUrl);
|
||||
|
||||
// Verify fetch was called with correct URL
|
||||
expect(mockFetch).toHaveBeenCalledOnce();
|
||||
expect(mockFetch).toHaveBeenCalledWith(testUrl);
|
||||
|
||||
// Verify response properties
|
||||
expect(await result.text()).toBe(mockResponseBody);
|
||||
expect(result.status).toBe(200);
|
||||
expect(result.statusText).toBe('OK');
|
||||
|
||||
// Verify headers were properly modified
|
||||
expect(result.headers.has('content-encoding')).toBe(false);
|
||||
expect(result.headers.has('content-length')).toBe(false);
|
||||
|
||||
// Verify other headers are preserved
|
||||
expect(result.headers.get('content-type')).toBe('application/json');
|
||||
expect(result.headers.get('cache-control')).toBe('no-cache');
|
||||
expect(result.headers.get('custom-header')).toBe('custom-value');
|
||||
});
|
||||
|
||||
it('should handle responses without content-encoding or content-length headers', async () => {
|
||||
const testUrl = new URL('https://example.com/test');
|
||||
const mockResponseBody = 'test response body';
|
||||
|
||||
const mockHeaders = new Headers({
|
||||
'content-type': 'text/plain',
|
||||
'cache-control': 'max-age=3600',
|
||||
});
|
||||
|
||||
const mockResponse = new Response(mockResponseBody, {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: mockHeaders,
|
||||
});
|
||||
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await createProxiedResponse(testUrl);
|
||||
|
||||
// Verify response properties
|
||||
expect(await result.text()).toBe(mockResponseBody);
|
||||
expect(result.status).toBe(200);
|
||||
expect(result.statusText).toBe('OK');
|
||||
|
||||
// Verify headers are preserved (nothing to remove)
|
||||
expect(result.headers.get('content-type')).toBe('text/plain');
|
||||
expect(result.headers.get('cache-control')).toBe('max-age=3600');
|
||||
expect(result.headers.has('content-encoding')).toBe(false);
|
||||
expect(result.headers.has('content-length')).toBe(false);
|
||||
});
|
||||
|
||||
it('should proxy error responses correctly', async () => {
|
||||
const testUrl = new URL('https://example.com/error');
|
||||
|
||||
const mockHeaders = new Headers({
|
||||
'content-type': 'application/json',
|
||||
'content-encoding': 'deflate',
|
||||
'content-length': '456',
|
||||
});
|
||||
|
||||
const mockResponse = new Response('{"error": "Not found"}', {
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
headers: mockHeaders,
|
||||
});
|
||||
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await createProxiedResponse(testUrl);
|
||||
|
||||
// Verify error response is properly proxied
|
||||
expect(result.status).toBe(404);
|
||||
expect(result.statusText).toBe('Not Found');
|
||||
expect(await result.text()).toBe('{"error": "Not found"}');
|
||||
|
||||
// Verify headers are still properly cleaned
|
||||
expect(result.headers.has('content-encoding')).toBe(false);
|
||||
expect(result.headers.has('content-length')).toBe(false);
|
||||
expect(result.headers.get('content-type')).toBe('application/json');
|
||||
});
|
||||
|
||||
it('should handle responses with only content-encoding header', async () => {
|
||||
const testUrl = new URL('https://example.com/test');
|
||||
|
||||
const mockHeaders = new Headers({
|
||||
'content-type': 'application/json',
|
||||
'content-encoding': 'br',
|
||||
});
|
||||
|
||||
const mockResponse = new Response('compressed data', {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: mockHeaders,
|
||||
});
|
||||
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await createProxiedResponse(testUrl);
|
||||
|
||||
// Verify only content-encoding is removed
|
||||
expect(result.headers.has('content-encoding')).toBe(false);
|
||||
expect(result.headers.has('content-length')).toBe(false); // Should be false (wasn't present)
|
||||
expect(result.headers.get('content-type')).toBe('application/json');
|
||||
});
|
||||
|
||||
it('should handle responses with only content-length header', async () => {
|
||||
const testUrl = new URL('https://example.com/test');
|
||||
|
||||
const mockHeaders = new Headers({
|
||||
'content-type': 'text/html',
|
||||
'content-length': '789',
|
||||
});
|
||||
|
||||
const mockResponse = new Response('<html></html>', {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: mockHeaders,
|
||||
});
|
||||
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await createProxiedResponse(testUrl);
|
||||
|
||||
// Verify only content-length is removed
|
||||
expect(result.headers.has('content-length')).toBe(false);
|
||||
expect(result.headers.has('content-encoding')).toBe(false); // Should be false (wasn't present)
|
||||
expect(result.headers.get('content-type')).toBe('text/html');
|
||||
});
|
||||
|
||||
it('should preserve all other headers', async () => {
|
||||
const testUrl = new URL('https://example.com/test');
|
||||
|
||||
const mockHeaders = new Headers({
|
||||
'content-type': 'application/json',
|
||||
'content-encoding': 'gzip',
|
||||
'content-length': '100',
|
||||
authorization: 'Bearer token123',
|
||||
'x-custom-header': 'custom-value',
|
||||
'cache-control': 'private, max-age=0',
|
||||
etag: '"abc123"',
|
||||
'last-modified': 'Wed, 21 Oct 2015 07:28:00 GMT',
|
||||
});
|
||||
|
||||
const mockResponse = new Response('response data', {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: mockHeaders,
|
||||
});
|
||||
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await createProxiedResponse(testUrl);
|
||||
|
||||
// Verify the problematic headers are removed
|
||||
expect(result.headers.has('content-encoding')).toBe(false);
|
||||
expect(result.headers.has('content-length')).toBe(false);
|
||||
|
||||
// Verify all other headers are preserved
|
||||
expect(result.headers.get('content-type')).toBe('application/json');
|
||||
expect(result.headers.get('authorization')).toBe('Bearer token123');
|
||||
expect(result.headers.get('x-custom-header')).toBe('custom-value');
|
||||
expect(result.headers.get('cache-control')).toBe('private, max-age=0');
|
||||
expect(result.headers.get('etag')).toBe('"abc123"');
|
||||
expect(result.headers.get('last-modified')).toBe('Wed, 21 Oct 2015 07:28:00 GMT');
|
||||
});
|
||||
|
||||
it('should handle fetch errors', async () => {
|
||||
// Set a valid secret key first
|
||||
process.env.ELECTRIC_SECRET = 'test-secret';
|
||||
|
||||
const testUrl = new URL('https://example.com/error');
|
||||
const fetchError = new Error('Network error');
|
||||
|
||||
mockFetch.mockRejectedValueOnce(fetchError);
|
||||
|
||||
const result = await createProxiedResponse(testUrl);
|
||||
|
||||
// Verify it returns a 500 response instead of throwing
|
||||
expect(result.status).toBe(500);
|
||||
expect(await result.text()).toBe('Internal Server Error');
|
||||
expect(mockFetch).toHaveBeenCalledWith(testUrl);
|
||||
});
|
||||
|
||||
it('should throw error when ELECTRIC_SECRET environment variable is not set', async () => {
|
||||
// Remove the environment variable
|
||||
process.env.ELECTRIC_SECRET = '';
|
||||
|
||||
const testUrl = new URL('https://example.com/test');
|
||||
|
||||
await expect(createProxiedResponse(testUrl)).rejects.toThrow('ELECTRIC_SECRET is not set');
|
||||
|
||||
// Verify fetch was never called since error is thrown before
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add secret key to URL params when ELECTRIC_SECRET is set', async () => {
|
||||
const secretKey = 'test-secret-key-123';
|
||||
process.env.ELECTRIC_SECRET = secretKey;
|
||||
|
||||
const testUrl = new URL('https://example.com/test');
|
||||
const mockResponseBody = 'test response';
|
||||
|
||||
const mockResponse = new Response(mockResponseBody, {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
});
|
||||
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await createProxiedResponse(testUrl);
|
||||
|
||||
// Verify fetch was called with URL that has secret parameter
|
||||
expect(mockFetch).toHaveBeenCalledOnce();
|
||||
const calledUrl = mockFetch.mock.calls[0][0] as URL;
|
||||
expect(calledUrl.searchParams.get('secret')).toBe(secretKey);
|
||||
|
||||
// Verify the response is still valid
|
||||
expect(await result.text()).toBe(mockResponseBody);
|
||||
expect(result.status).toBe(200);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
export const createProxiedResponse = async (url: URL) => {
|
||||
const secretKey = process.env.ELECTRIC_SECRET;
|
||||
|
||||
if (!secretKey) {
|
||||
throw new Error('ELECTRIC_SECRET is not set');
|
||||
}
|
||||
|
||||
url.searchParams.set('secret', secretKey);
|
||||
|
||||
const response = await fetch(url).catch((error) => {
|
||||
console.error('Error fetching from Electric:', error);
|
||||
return new Response('Internal Server Error', { status: 500 });
|
||||
});
|
||||
|
||||
// Fetch decompresses the body but doesn't remove the
|
||||
// content-encoding & content-length headers which would
|
||||
// break decoding in the browser.
|
||||
// See https://github.com/whatwg/fetch/issues/1729
|
||||
const headers = new Headers(response.headers);
|
||||
headers.delete('content-encoding');
|
||||
headers.delete('content-length');
|
||||
|
||||
// Return the proxied response
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
};
|
|
@ -1,33 +1,33 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createProxiedResponse, extractParamFromWhere, getElectricShapeUrl } from './_helpers';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { extractParamFromWhere, getElectricShapeUrl } from '.';
|
||||
|
||||
describe('getElectricShapeUrl', () => {
|
||||
process.env.ELECTRIC_URL = 'http://localhost:3000';
|
||||
const originalElectricUrl = process.env.ELECTRIC_URL;
|
||||
process.env.ELECTRIC_PROXY_URL = 'http://localhost:3000';
|
||||
const originalElectricUrl = process.env.ELECTRIC_PROXY_URL;
|
||||
|
||||
beforeEach(() => {
|
||||
// Clean up environment variable before each test
|
||||
process.env.ELECTRIC_URL = undefined;
|
||||
process.env.ELECTRIC_PROXY_URL = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment variable after each test
|
||||
if (originalElectricUrl !== undefined) {
|
||||
process.env.ELECTRIC_URL = originalElectricUrl;
|
||||
process.env.ELECTRIC_PROXY_URL = originalElectricUrl;
|
||||
} else {
|
||||
process.env.ELECTRIC_URL = undefined;
|
||||
process.env.ELECTRIC_PROXY_URL = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
it('should return default URL with /v1/shape path when no ELECTRIC_URL is set', () => {
|
||||
it('should return default URL with /v1/shape path when no ELECTRIC_PROXY_URL is set', () => {
|
||||
const requestUrl = 'http://example.com/test?table=users';
|
||||
const result = getElectricShapeUrl(requestUrl);
|
||||
|
||||
expect(result.toString()).toBe('http://localhost:3000/v1/shape?table=users');
|
||||
});
|
||||
|
||||
it('should use ELECTRIC_URL environment variable when set', () => {
|
||||
process.env.ELECTRIC_URL = 'https://electric.example.com';
|
||||
it('should use ELECTRIC_PROXY_URL environment variable when set', () => {
|
||||
process.env.ELECTRIC_PROXY_URL = 'https://electric.example.com';
|
||||
const requestUrl = 'http://example.com/test?table=users&live=true';
|
||||
const result = getElectricShapeUrl(requestUrl);
|
||||
|
||||
|
@ -138,16 +138,16 @@ describe('getElectricShapeUrl', () => {
|
|||
expect(result.searchParams.get('table')).toBe('posts');
|
||||
});
|
||||
|
||||
it('should handle ELECTRIC_URL with trailing slash', () => {
|
||||
process.env.ELECTRIC_URL = 'https://electric.example.com/';
|
||||
it('should handle ELECTRIC_PROXY_URL with trailing slash', () => {
|
||||
process.env.ELECTRIC_PROXY_URL = 'https://electric.example.com/';
|
||||
const requestUrl = 'http://example.com/test?table=users';
|
||||
const result = getElectricShapeUrl(requestUrl);
|
||||
|
||||
expect(result.toString()).toBe('https://electric.example.com/v1/shape?table=users');
|
||||
});
|
||||
|
||||
it('should handle ELECTRIC_URL with path', () => {
|
||||
process.env.ELECTRIC_URL = 'https://api.example.com/electric';
|
||||
it('should handle ELECTRIC_PROXY_URL with path', () => {
|
||||
process.env.ELECTRIC_PROXY_URL = 'https://api.example.com/electric';
|
||||
const requestUrl = 'http://example.com/test?table=users';
|
||||
const result = getElectricShapeUrl(requestUrl);
|
||||
|
||||
|
@ -155,219 +155,8 @@ describe('getElectricShapeUrl', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('createProxiedResponse', () => {
|
||||
const mockFetch = vi.fn<typeof fetch>();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('fetch', mockFetch);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should proxy a successful response and remove content-encoding and content-length headers', async () => {
|
||||
const testUrl = new URL('https://example.com/test');
|
||||
const mockResponseBody = 'test response body';
|
||||
|
||||
// Create mock headers that include content-encoding and content-length
|
||||
const mockHeaders = new Headers({
|
||||
'content-type': 'application/json',
|
||||
'content-encoding': 'gzip',
|
||||
'content-length': '123',
|
||||
'cache-control': 'no-cache',
|
||||
'custom-header': 'custom-value',
|
||||
});
|
||||
|
||||
const mockResponse = new Response(mockResponseBody, {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: mockHeaders,
|
||||
});
|
||||
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await createProxiedResponse(testUrl);
|
||||
|
||||
// Verify fetch was called with correct URL
|
||||
expect(mockFetch).toHaveBeenCalledOnce();
|
||||
expect(mockFetch).toHaveBeenCalledWith(testUrl);
|
||||
|
||||
// Verify response properties
|
||||
expect(await result.text()).toBe(mockResponseBody);
|
||||
expect(result.status).toBe(200);
|
||||
expect(result.statusText).toBe('OK');
|
||||
|
||||
// Verify headers were properly modified
|
||||
expect(result.headers.has('content-encoding')).toBe(false);
|
||||
expect(result.headers.has('content-length')).toBe(false);
|
||||
|
||||
// Verify other headers are preserved
|
||||
expect(result.headers.get('content-type')).toBe('application/json');
|
||||
expect(result.headers.get('cache-control')).toBe('no-cache');
|
||||
expect(result.headers.get('custom-header')).toBe('custom-value');
|
||||
});
|
||||
|
||||
it('should handle responses without content-encoding or content-length headers', async () => {
|
||||
const testUrl = new URL('https://example.com/test');
|
||||
const mockResponseBody = 'test response body';
|
||||
|
||||
const mockHeaders = new Headers({
|
||||
'content-type': 'text/plain',
|
||||
'cache-control': 'max-age=3600',
|
||||
});
|
||||
|
||||
const mockResponse = new Response(mockResponseBody, {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: mockHeaders,
|
||||
});
|
||||
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await createProxiedResponse(testUrl);
|
||||
|
||||
// Verify response properties
|
||||
expect(await result.text()).toBe(mockResponseBody);
|
||||
expect(result.status).toBe(200);
|
||||
expect(result.statusText).toBe('OK');
|
||||
|
||||
// Verify headers are preserved (nothing to remove)
|
||||
expect(result.headers.get('content-type')).toBe('text/plain');
|
||||
expect(result.headers.get('cache-control')).toBe('max-age=3600');
|
||||
expect(result.headers.has('content-encoding')).toBe(false);
|
||||
expect(result.headers.has('content-length')).toBe(false);
|
||||
});
|
||||
|
||||
it('should proxy error responses correctly', async () => {
|
||||
const testUrl = new URL('https://example.com/error');
|
||||
|
||||
const mockHeaders = new Headers({
|
||||
'content-type': 'application/json',
|
||||
'content-encoding': 'deflate',
|
||||
'content-length': '456',
|
||||
});
|
||||
|
||||
const mockResponse = new Response('{"error": "Not found"}', {
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
headers: mockHeaders,
|
||||
});
|
||||
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await createProxiedResponse(testUrl);
|
||||
|
||||
// Verify error response is properly proxied
|
||||
expect(result.status).toBe(404);
|
||||
expect(result.statusText).toBe('Not Found');
|
||||
expect(await result.text()).toBe('{"error": "Not found"}');
|
||||
|
||||
// Verify headers are still properly cleaned
|
||||
expect(result.headers.has('content-encoding')).toBe(false);
|
||||
expect(result.headers.has('content-length')).toBe(false);
|
||||
expect(result.headers.get('content-type')).toBe('application/json');
|
||||
});
|
||||
|
||||
it('should handle responses with only content-encoding header', async () => {
|
||||
const testUrl = new URL('https://example.com/test');
|
||||
|
||||
const mockHeaders = new Headers({
|
||||
'content-type': 'application/json',
|
||||
'content-encoding': 'br',
|
||||
});
|
||||
|
||||
const mockResponse = new Response('compressed data', {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: mockHeaders,
|
||||
});
|
||||
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await createProxiedResponse(testUrl);
|
||||
|
||||
// Verify only content-encoding is removed
|
||||
expect(result.headers.has('content-encoding')).toBe(false);
|
||||
expect(result.headers.has('content-length')).toBe(false); // Should be false (wasn't present)
|
||||
expect(result.headers.get('content-type')).toBe('application/json');
|
||||
});
|
||||
|
||||
it('should handle responses with only content-length header', async () => {
|
||||
const testUrl = new URL('https://example.com/test');
|
||||
|
||||
const mockHeaders = new Headers({
|
||||
'content-type': 'text/html',
|
||||
'content-length': '789',
|
||||
});
|
||||
|
||||
const mockResponse = new Response('<html></html>', {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: mockHeaders,
|
||||
});
|
||||
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await createProxiedResponse(testUrl);
|
||||
|
||||
// Verify only content-length is removed
|
||||
expect(result.headers.has('content-length')).toBe(false);
|
||||
expect(result.headers.has('content-encoding')).toBe(false); // Should be false (wasn't present)
|
||||
expect(result.headers.get('content-type')).toBe('text/html');
|
||||
});
|
||||
|
||||
it('should preserve all other headers', async () => {
|
||||
const testUrl = new URL('https://example.com/test');
|
||||
|
||||
const mockHeaders = new Headers({
|
||||
'content-type': 'application/json',
|
||||
'content-encoding': 'gzip',
|
||||
'content-length': '100',
|
||||
authorization: 'Bearer token123',
|
||||
'x-custom-header': 'custom-value',
|
||||
'cache-control': 'private, max-age=0',
|
||||
etag: '"abc123"',
|
||||
'last-modified': 'Wed, 21 Oct 2015 07:28:00 GMT',
|
||||
});
|
||||
|
||||
const mockResponse = new Response('response data', {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: mockHeaders,
|
||||
});
|
||||
|
||||
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await createProxiedResponse(testUrl);
|
||||
|
||||
// Verify the problematic headers are removed
|
||||
expect(result.headers.has('content-encoding')).toBe(false);
|
||||
expect(result.headers.has('content-length')).toBe(false);
|
||||
|
||||
// Verify all other headers are preserved
|
||||
expect(result.headers.get('content-type')).toBe('application/json');
|
||||
expect(result.headers.get('authorization')).toBe('Bearer token123');
|
||||
expect(result.headers.get('x-custom-header')).toBe('custom-value');
|
||||
expect(result.headers.get('cache-control')).toBe('private, max-age=0');
|
||||
expect(result.headers.get('etag')).toBe('"abc123"');
|
||||
expect(result.headers.get('last-modified')).toBe('Wed, 21 Oct 2015 07:28:00 GMT');
|
||||
});
|
||||
|
||||
it('should handle fetch errors', async () => {
|
||||
const testUrl = new URL('https://example.com/error');
|
||||
const fetchError = new Error('Network error');
|
||||
|
||||
mockFetch.mockRejectedValueOnce(fetchError);
|
||||
|
||||
await expect(createProxiedResponse(testUrl)).rejects.toThrow('Network error');
|
||||
expect(mockFetch).toHaveBeenCalledWith(testUrl);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractParamFromWhere', () => {
|
||||
it('should', () => {
|
||||
it('should handle a simple where clause', () => {
|
||||
const testClause = "where=id='420226c8-b91d-49c5-99f8-660b04cc8c01'&offset=-1";
|
||||
const url = new URL(`https://example.com/test?${testClause}`);
|
||||
const result = extractParamFromWhere(url, 'id');
|
|
@ -1,10 +1,10 @@
|
|||
export const getElectricShapeUrl = (requestUrl: string) => {
|
||||
const url = new URL(requestUrl);
|
||||
|
||||
const electricUrl = process.env.ELECTRIC_URL;
|
||||
const electricUrl = process.env.ELECTRIC_PROXY_URL;
|
||||
|
||||
if (!electricUrl) {
|
||||
throw new Error('ELECTRIC_URL is not set');
|
||||
throw new Error('ELECTRIC_PROXY_URL is not set');
|
||||
}
|
||||
|
||||
const baseUrl =
|
||||
|
@ -39,32 +39,6 @@ export const getElectricShapeUrl = (requestUrl: string) => {
|
|||
return originUrl;
|
||||
};
|
||||
|
||||
export const createProxiedResponse = async (url: URL) => {
|
||||
const response = await fetch(url);
|
||||
|
||||
// Fetch decompresses the body but doesn't remove the
|
||||
// content-encoding & content-length headers which would
|
||||
// break decoding in the browser.
|
||||
|
||||
// See https://github.com/whatwg/fetch/issues/1729
|
||||
const headers = new Headers(response.headers);
|
||||
headers.delete('content-encoding');
|
||||
headers.delete('content-length');
|
||||
|
||||
// Return the proxied response
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts a parameter value from the where clause in the URL
|
||||
* @param url - The URL object containing the where parameter
|
||||
* @param paramName - The parameter name to extract (e.g., 'chatId', 'userId')
|
||||
* @returns The parameter value or null if not found
|
||||
*/
|
||||
export const extractParamFromWhere = (url: URL, paramName: string): string | null => {
|
||||
const whereClause = url.searchParams.get('where');
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './helpers';
|
||||
export * from './electricHandler';
|
|
@ -8,9 +8,6 @@ export const requireAuth = bearerAuth({
|
|||
verifyToken: async (token, c) => {
|
||||
const { data, error } = await supabase.auth.getUser(token); //usually takes about 3 - 7ms
|
||||
|
||||
if (token.includes('Cj0g')) {
|
||||
}
|
||||
|
||||
if (error || !data.user) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ NODE_ENV=production
|
|||
SERVER_PORT=3002
|
||||
SUPABASE_URL=http://localhost:54321
|
||||
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey AgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
|
||||
ELECTRIC_URL=http://localhost:3003
|
||||
ELECTRIC_PROXY_URL=http://localhost:3003
|
||||
DATABASE_URL=http://localhost:54321
|
||||
EOF
|
||||
|
||||
|
|
Loading…
Reference in New Issue