mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
fix(core): Prevent Supabase vector store node auth client timer leaks (#37054)
This commit is contained in:
parent
c6222d4b96
commit
ad24a481c2
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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>>;
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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 },
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user