diff --git a/packages/core/supabase-js/src/SupabaseClient.ts b/packages/core/supabase-js/src/SupabaseClient.ts index 3c2125ce5..71c8b30d3 100644 --- a/packages/core/supabase-js/src/SupabaseClient.ts +++ b/packages/core/supabase-js/src/SupabaseClient.ts @@ -156,6 +156,14 @@ export default class SupabaseClient< accessToken: this._getAccessToken.bind(this), ...settings.realtime, }) + if (this.accessToken) { + setTimeout(() => { + this.accessToken?.() + ?.then((token) => this.realtime.setAuth(token)) + .catch((e) => console.warn('Failed to set initial Realtime auth token:', e)) + }, 0) + } + this.rest = new PostgrestClient(new URL('rest/v1', baseUrl).href, { headers: this.headers, schema: settings.db.schema, diff --git a/packages/core/supabase-js/test/integration.test.ts b/packages/core/supabase-js/test/integration.test.ts index d9553405b..387849d74 100644 --- a/packages/core/supabase-js/test/integration.test.ts +++ b/packages/core/supabase-js/test/integration.test.ts @@ -1,12 +1,13 @@ +import { assert } from 'console' import { createClient, RealtimeChannel, SupabaseClient } from '../src/index' - +import { sign } from 'jsonwebtoken' // These tests assume that a local Supabase server is already running // Start a local Supabase instance with 'supabase start' before running these tests // Default local dev credentials from Supabase CLI const SUPABASE_URL = 'http://127.0.0.1:54321' const ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' - +const JWT_SECRET = 'super-secret-jwt-token-with-at-least-32-characters-long' // For Node.js < 22, we need to provide a WebSocket implementation // Node.js 22+ has native WebSocket support let wsTransport: any = undefined @@ -292,7 +293,7 @@ describe('Supabase Integration Tests', () => { channel .on('broadcast', { event: '*' }, (payload) => (receivedMessage = payload)) - .subscribe((status) => { + .subscribe((status, err) => { if (status == 'SUBSCRIBED') subscribed = true }) @@ -358,3 +359,31 @@ describe('Storage API', () => { expect(deleteError).toBeNull() }) }) + +describe('Custom JWT', () => { + describe('Realtime', () => { + test('will connect with a properly signed jwt token', async () => { + const jwtToken = sign({ sub: '1234567890' }, JWT_SECRET, { expiresIn: '1h' }) + const supabaseWithCustomJwt = createClient(SUPABASE_URL, ANON_KEY, { + accessToken: () => Promise.resolve(jwtToken), + }) + await new Promise((resolve) => setTimeout(resolve, 100)) + expect(supabaseWithCustomJwt.realtime.accessTokenValue).toBe(jwtToken) + let subscribed = false + let attempts = 0 + supabaseWithCustomJwt.channel('test-channel').subscribe((status) => { + if (status == 'SUBSCRIBED') subscribed = true + }) + + // Wait for subscription + while (!subscribed) { + if (attempts > 50) throw new Error('Timeout waiting for subscription') + await new Promise((resolve) => setTimeout(resolve, 100)) + attempts++ + } + + expect(subscribed).toBe(true) + // + }, 10000) + }) +}) diff --git a/packages/core/supabase-js/test/unit/SupabaseClient.test.ts b/packages/core/supabase-js/test/unit/SupabaseClient.test.ts index 6f7304114..edaf33c88 100644 --- a/packages/core/supabase-js/test/unit/SupabaseClient.test.ts +++ b/packages/core/supabase-js/test/unit/SupabaseClient.test.ts @@ -259,6 +259,61 @@ describe('SupabaseClient', () => { }) describe('Realtime Authentication', () => { + test('should automatically call setAuth() when accessToken option is provided', async () => { + const customToken = 'custom-jwt-token' + const customAccessTokenFn = jest.fn().mockResolvedValue(customToken) + const client = createClient(URL, KEY, { accessToken: customAccessTokenFn }) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect((client.realtime as any).accessTokenValue).toBe(customToken) + expect(customAccessTokenFn).toHaveBeenCalled() + }) + + test('should automatically populate token in channels when using custom JWT', async () => { + const customToken = 'custom-channel-token' + const customAccessTokenFn = jest.fn().mockResolvedValue(customToken) + const client = createClient(URL, KEY, { accessToken: customAccessTokenFn }) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + const channel = client.channel('test-channel') + channel.subscribe() + + expect((channel as any).joinPush.payload.access_token).toBe(customToken) + expect((client.realtime as any).accessTokenValue).toBe(customToken) + }) + + test('should handle errors gracefully when accessToken callback fails', async () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}) + const error = new Error('Token fetch failed') + const failingAccessTokenFn = jest.fn().mockRejectedValue(error) + + const client = createClient(URL, KEY, { accessToken: failingAccessTokenFn }) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'Failed to set initial Realtime auth token:', + error + ) + expect(client).toBeDefined() + expect(client.realtime).toBeDefined() + + consoleWarnSpy.mockRestore() + }) + + test('should not call setAuth() automatically in normal mode', async () => { + const client = createClient(URL, KEY) + const setAuthSpy = jest.spyOn(client.realtime, 'setAuth') + + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(setAuthSpy).not.toHaveBeenCalled() + + setAuthSpy.mockRestore() + }) + test('should provide access token to realtime client', async () => { const expectedToken = 'test-jwt-token' const client = createClient(URL, KEY) diff --git a/supabase/.branches/_current_branch b/supabase/.branches/_current_branch new file mode 100644 index 000000000..88d050b19 --- /dev/null +++ b/supabase/.branches/_current_branch @@ -0,0 +1 @@ +main \ No newline at end of file diff --git a/supabase/.temp/cli-latest b/supabase/.temp/cli-latest new file mode 100644 index 000000000..11335d2f8 --- /dev/null +++ b/supabase/.temp/cli-latest @@ -0,0 +1 @@ +v2.54.11 \ No newline at end of file