diff --git a/packages/core/realtime-js/src/lib/transformers.ts b/packages/core/realtime-js/src/lib/transformers.ts index ba5a19972..6c3901c3f 100644 --- a/packages/core/realtime-js/src/lib/transformers.ts +++ b/packages/core/realtime-js/src/lib/transformers.ts @@ -251,8 +251,24 @@ export const toTimestampString = (value: RecordValue): RecordValue => { } export const httpEndpointURL = (socketUrl: string): string => { - let url = socketUrl - url = url.replace(/^ws/i, 'http') - url = url.replace(/(\/socket\/websocket|\/socket|\/websocket)\/?$/i, '') - return url.replace(/\/+$/, '') + '/api/broadcast' + // Convert ws/wss protocol to http/https using URL object for security + const wsUrl = new URL(socketUrl) + + wsUrl.protocol = wsUrl.protocol.replace(/^ws/i, 'http') + + // Remove WebSocket-specific path suffixes (handle multiple trailing slashes) + wsUrl.pathname = wsUrl.pathname + .replace(/\/+$/, '') // First remove all trailing slashes + .replace(/\/socket\/websocket$/i, '') + .replace(/\/socket$/i, '') + .replace(/\/websocket$/i, '') + + // Ensure pathname doesn't end with slash before appending + if (wsUrl.pathname === '' || wsUrl.pathname === '/') { + wsUrl.pathname = '/api/broadcast' + } else { + wsUrl.pathname = wsUrl.pathname + '/api/broadcast' + } + + return wsUrl.href } diff --git a/packages/core/realtime-js/test/transformers.test.ts b/packages/core/realtime-js/test/transformers.test.ts index f2b40a9b5..22e148373 100644 --- a/packages/core/realtime-js/test/transformers.test.ts +++ b/packages/core/realtime-js/test/transformers.test.ts @@ -5,6 +5,7 @@ import { convertCell, convertChangeData, convertColumn, + httpEndpointURL, toArray, toJson, toTimestampString, @@ -150,3 +151,74 @@ test('toArray with non-array strings', () => { assert.strictEqual(toArray('missing_closing', 'text'), 'missing_closing') assert.strictEqual(toArray('missing_opening}', 'text'), 'missing_opening}') }) + +test('httpEndpointURL', () => { + // Test basic ws to http conversion + assert.strictEqual( + httpEndpointURL('ws://example.com/socket/websocket'), + 'http://example.com/api/broadcast' + ) + + // Test wss to https conversion + assert.strictEqual( + httpEndpointURL('wss://example.com/socket/websocket'), + 'https://example.com/api/broadcast' + ) + + // Test with /socket path + assert.strictEqual(httpEndpointURL('ws://example.com/socket'), 'http://example.com/api/broadcast') + + // Test with /websocket path + assert.strictEqual( + httpEndpointURL('ws://example.com/websocket'), + 'http://example.com/api/broadcast' + ) + + // Test with trailing slash + assert.strictEqual( + httpEndpointURL('ws://example.com/socket/websocket/'), + 'http://example.com/api/broadcast' + ) + + // Test with port number + assert.strictEqual( + httpEndpointURL('ws://example.com:8080/socket/websocket'), + 'http://example.com:8080/api/broadcast' + ) + + // Test with path prefix + assert.strictEqual( + httpEndpointURL('ws://example.com/prefix/socket/websocket'), + 'http://example.com/prefix/api/broadcast' + ) + + // Test with query parameters + assert.strictEqual( + httpEndpointURL('ws://example.com/socket/websocket?apikey=test'), + 'http://example.com/api/broadcast?apikey=test' + ) + + // Test already http protocol (should remain unchanged) + assert.strictEqual( + httpEndpointURL('http://example.com/socket/websocket'), + 'http://example.com/api/broadcast' + ) + + // Test already https protocol (should remain unchanged) + assert.strictEqual( + httpEndpointURL('https://example.com/socket/websocket'), + 'https://example.com/api/broadcast' + ) + + // Test with multiple trailing slashes + assert.strictEqual( + httpEndpointURL('ws://example.com/socket/websocket///'), + 'http://example.com/api/broadcast' + ) + + // Test with no websocket-specific paths + assert.strictEqual( + httpEndpointURL('ws://example.com/some/path'), + 'http://example.com/some/path/api/broadcast' + ) +}) diff --git a/packages/core/storage-js/src/packages/StorageBucketApi.ts b/packages/core/storage-js/src/packages/StorageBucketApi.ts index a2b7157fc..1d28673c5 100644 --- a/packages/core/storage-js/src/packages/StorageBucketApi.ts +++ b/packages/core/storage-js/src/packages/StorageBucketApi.ts @@ -28,7 +28,12 @@ export default class StorageBucketApi { } } - this.url = baseUrl.href.replace(/\/$/, '') + // Remove trailing slash from pathname using URL object + if (baseUrl.pathname.endsWith('/') && baseUrl.pathname.length > 1) { + baseUrl.pathname = baseUrl.pathname.slice(0, -1) + } + + this.url = baseUrl.href this.headers = { ...DEFAULT_HEADERS, ...headers } this.fetch = resolveFetch(fetch) } diff --git a/packages/core/supabase-js/src/SupabaseClient.ts b/packages/core/supabase-js/src/SupabaseClient.ts index 511d999c7..78ef58ec5 100644 --- a/packages/core/supabase-js/src/SupabaseClient.ts +++ b/packages/core/supabase-js/src/SupabaseClient.ts @@ -111,7 +111,8 @@ export default class SupabaseClient< if (!supabaseKey) throw new Error('supabaseKey is required.') this.realtimeUrl = new URL('realtime/v1', baseUrl) - this.realtimeUrl.protocol = this.realtimeUrl.protocol.replace('http', 'ws') + this.realtimeUrl.protocol = this.realtimeUrl.protocol.replace(/^http/i, 'ws') + this.authUrl = new URL('auth/v1', baseUrl) this.storageUrl = new URL('storage/v1', baseUrl) this.functionsUrl = new URL('functions/v1', baseUrl)