From ad24a481c2fc4c33fad6bab6aed511ed80dad689 Mon Sep 17 00:00:00 2001 From: Anne Aguirre Date: Thu, 27 Aug 2026 08:08:49 +0000 Subject: [PATCH] fix(core): Prevent Supabase vector store node auth client timer leaks (#37054) --- .../__tests__/supabase-client-options.test.ts | 32 +++++++++++++++++++ .../@n8n/agents/src/vector-stores/supabase.ts | 6 +++- .../VectorStoreSupabase.node.ts | 6 ++-- .../VectorStoreSupabaseInsert.node.ts | 4 +-- .../VectorStoreSupabaseLoad.node.ts | 4 +-- .../shared/supabase.client.test.ts | 28 ++++++++++++++++ .../vector_store/shared/supabase.test.ts | 26 +++++++++++++++ .../nodes/vector_store/shared/supabase.ts | 11 +++++++ 8 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 packages/@n8n/agents/src/__tests__/supabase-client-options.test.ts create mode 100644 packages/@n8n/nodes-langchain/nodes/vector_store/shared/supabase.client.test.ts create mode 100644 packages/@n8n/nodes-langchain/nodes/vector_store/shared/supabase.test.ts create mode 100644 packages/@n8n/nodes-langchain/nodes/vector_store/shared/supabase.ts diff --git a/packages/@n8n/agents/src/__tests__/supabase-client-options.test.ts b/packages/@n8n/agents/src/__tests__/supabase-client-options.test.ts new file mode 100644 index 00000000000..3f5a02104f4 --- /dev/null +++ b/packages/@n8n/agents/src/__tests__/supabase-client-options.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import { SupabaseVectorStore } from '../vector-stores/supabase'; + +type AuthInternals = { + autoRefreshTicker: NodeJS.Timeout | null; + initialize: () => Promise; +}; + +function getAuthInternals(client: { auth: object }): AuthInternals { + return client.auth as AuthInternals; +} + +describe('SupabaseVectorStore client options', () => { + it('does not arm a 30s token-refresh ticker', async () => { + const store = new SupabaseVectorStore('test-store', { + url: 'https://example.supabase.co', + apiKey: 'service-role-key', + tableName: 'docs', + }); + + const client = await ( + store as unknown as { + getClient(): Promise<{ auth: object }>; + } + ).getClient(); + + await getAuthInternals(client).initialize(); + + expect(getAuthInternals(client).autoRefreshTicker).toBeNull(); + }); +}); diff --git a/packages/@n8n/agents/src/vector-stores/supabase.ts b/packages/@n8n/agents/src/vector-stores/supabase.ts index 980912bf925..0987f74e97b 100644 --- a/packages/@n8n/agents/src/vector-stores/supabase.ts +++ b/packages/@n8n/agents/src/vector-stores/supabase.ts @@ -68,7 +68,11 @@ type SupabaseVectorSchema = { */ async function createSupabaseVectorClient(url: string, apiKey: string) { const { createClient } = await import('@supabase/supabase-js'); - return createClient(url, apiKey); + // supabase-js arms a 30s token-refresh interval per client; PostgREST-only + // usage does not need auth refresh or session persistence. + return createClient(url, apiKey, { + auth: { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false }, + }); } type SupabaseVectorClient = Awaited>; diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreSupabase/VectorStoreSupabase.node.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreSupabase/VectorStoreSupabase.node.ts index 18ac6d9021c..33b6504b46d 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreSupabase/VectorStoreSupabase.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreSupabase/VectorStoreSupabase.node.ts @@ -1,11 +1,11 @@ import { SupabaseVectorStore } from '@langchain/community/vectorstores/supabase'; -import { createClient } from '@supabase/supabase-js'; import { NodeOperationError, type INodeProperties } from 'n8n-workflow'; import { metadataFilterField, createVectorStoreNode } from '@n8n/ai-utilities'; import { supabaseTableNameSearch } from '../shared/methods/listSearch'; import { supabaseTableNameRLC } from '../shared/descriptions'; +import { createSupabaseClient } from '../shared/supabase'; const queryNameField: INodeProperties = { displayName: 'Query Name', @@ -72,7 +72,7 @@ export class VectorStoreSupabase extends createVectorStoreNode Promise; +}; + +function getAuthInternals(client: { auth: object }): AuthInternals { + return client.auth as AuthInternals; +} + +describe('createSupabaseClient (real supabase-js)', () => { + it('does not arm a 30s token-refresh ticker', async () => { + const client = createSupabaseClient({ + host: 'https://example.supabase.co', + serviceRole: 'test-service-role-key', + }); + + await getAuthInternals(client).initialize(); + + expect(getAuthInternals(client).autoRefreshTicker).toBeNull(); + }); +}); diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/supabase.test.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/supabase.test.ts new file mode 100644 index 00000000000..cb361fcabd2 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/supabase.test.ts @@ -0,0 +1,26 @@ +vi.mock('@supabase/supabase-js', () => ({ + createClient: vi.fn(), +})); + +import { createClient } from '@supabase/supabase-js'; + +import { createSupabaseClient } from './supabase'; + +const serverSideAuthOptions = { + auth: { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false }, +}; + +describe('createSupabaseClient', () => { + it('creates a client with server-side auth options', () => { + createSupabaseClient({ + host: 'https://example.supabase.co', + serviceRole: 'service-role-key', + }); + + expect(createClient).toHaveBeenCalledWith( + 'https://example.supabase.co', + 'service-role-key', + serverSideAuthOptions, + ); + }); +}); diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/supabase.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/supabase.ts new file mode 100644 index 00000000000..5f7412b5330 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/supabase.ts @@ -0,0 +1,11 @@ +import { createClient } from '@supabase/supabase-js'; +import type { ICredentialDataDecryptedObject } from 'n8n-workflow'; + +// supabase-js arms a 30s token-refresh interval per client that keeps the +// whole client graph reachable for the process lifetime. The vector store +// only talks to PostgREST with a service-role key. +export function createSupabaseClient(credentials: ICredentialDataDecryptedObject) { + return createClient(credentials.host as string, credentials.serviceRole as string, { + auth: { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false }, + }); +}