fix(core): Prevent Supabase vector store node auth client timer leaks (#37054)

This commit is contained in:
Anne Aguirre 2026-08-27 08:08:49 +00:00 committed by GitHub
parent c6222d4b96
commit ad24a481c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 109 additions and 8 deletions

View File

@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest';
import { SupabaseVectorStore } from '../vector-stores/supabase';
type AuthInternals = {
autoRefreshTicker: NodeJS.Timeout | null;
initialize: () => Promise<unknown>;
};
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();
});
});

View File

@ -68,7 +68,11 @@ type SupabaseVectorSchema = {
*/
async function createSupabaseVectorClient(url: string, apiKey: string) {
const { createClient } = await import('@supabase/supabase-js');
return createClient<SupabaseVectorSchema>(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<SupabaseVectorSchema>(url, apiKey, {
auth: { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false },
});
}
type SupabaseVectorClient = Awaited<ReturnType<typeof createSupabaseVectorClient>>;

View File

@ -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<SupabaseVectorSto
queryName: string;
};
const credentials = await context.getCredentials('supabaseApi');
const client = createClient(credentials.host as string, credentials.serviceRole as string);
const client = createSupabaseClient(credentials);
return await SupabaseVectorStore.fromExistingIndex(embeddings, {
client,
@ -89,7 +89,7 @@ export class VectorStoreSupabase extends createVectorStoreNode<SupabaseVectorSto
queryName: string;
};
const credentials = await context.getCredentials('supabaseApi');
const client = createClient(credentials.host as string, credentials.serviceRole as string);
const client = createSupabaseClient(credentials);
try {
await SupabaseVectorStore.fromDocuments(documents, embeddings, {

View File

@ -1,7 +1,6 @@
import { SupabaseVectorStore } from '@langchain/community/vectorstores/supabase';
import type { Document } from '@langchain/core/documents';
import type { Embeddings } from '@langchain/core/embeddings';
import { createClient } from '@supabase/supabase-js';
import {
type IExecuteFunctions,
type INodeType,
@ -14,6 +13,7 @@ import { processDocuments, type N8nJsonLoader } from '@n8n/ai-utilities';
import { supabaseTableNameSearch } from '../shared/methods/listSearch';
import { supabaseTableNameRLC } from '../shared/descriptions';
import { createSupabaseClient } from '../shared/supabase';
// This node is deprecated. Use VectorStoreSupabase instead.
export class VectorStoreSupabaseInsert implements INodeType {
@ -109,7 +109,7 @@ export class VectorStoreSupabaseInsert implements INodeType {
NodeConnectionTypes.AiEmbedding,
0,
)) as Embeddings;
const client = createClient(credentials.host as string, credentials.serviceRole as string);
const client = createSupabaseClient(credentials);
const { processedDocuments, serializedDocuments } = await processDocuments(
documentInput,

View File

@ -1,7 +1,6 @@
import type { SupabaseLibArgs } from '@langchain/community/vectorstores/supabase';
import { SupabaseVectorStore } from '@langchain/community/vectorstores/supabase';
import type { Embeddings } from '@langchain/core/embeddings';
import { createClient } from '@supabase/supabase-js';
import {
type INodeType,
type INodeTypeDescription,
@ -14,6 +13,7 @@ import { logWrapper, getMetadataFiltersValues, metadataFilterField } from '@n8n/
import { supabaseTableNameSearch } from '../shared/methods/listSearch';
import { supabaseTableNameRLC } from '../shared/descriptions';
import { createSupabaseClient } from '../shared/supabase';
// This node is deprecated. Use VectorStoreSupabase instead.
export class VectorStoreSupabaseLoad implements INodeType {
@ -95,7 +95,7 @@ export class VectorStoreSupabaseLoad implements INodeType {
0,
)) as Embeddings;
const client = createClient(credentials.host as string, credentials.serviceRole as string);
const client = createSupabaseClient(credentials);
const config: SupabaseLibArgs = {
client,
tableName,

View File

@ -0,0 +1,28 @@
/**
* Unmocked: supabase.test.ts stubs `@supabase/supabase-js` and can only
* assert call shape. This file constructs a real client and checks that
* no 30s GoTrue auto-refresh ticker is armed.
*/
import { createSupabaseClient } from './supabase';
type AuthInternals = {
autoRefreshTicker: NodeJS.Timeout | null;
initialize: () => Promise<unknown>;
};
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();
});
});

View File

@ -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,
);
});
});

View File

@ -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 },
});
}