fix: use stdio-wrapper as bin entry and preserve credentials on workflow update (v2.45.1) (#695)

Switch the npm bin entry from index.js to stdio-wrapper.js to prevent
INFO-level logs from corrupting the JSON-RPC stdio transport. Also update
both publish scripts so the fix persists across releases. Fixes #693.

Preserve node credentials during full workflow updates. AI-generated node
updates typically omit credential references, causing the n8n API to reject
the PUT. The update handler now merges credentials from the current server-side
workflow when user-provided nodes lack them. Fixes #689.

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Romuald Członkowski
2026-04-02 22:40:17 +02:00
committed by GitHub
parent 8888c63e7a
commit ca20586eda
8 changed files with 186 additions and 8 deletions

View File

@@ -16,6 +16,12 @@ import { ExecutionStatus } from '@/types/n8n-api';
vi.mock('@/services/n8n-api-client');
vi.mock('@/services/workflow-validator');
vi.mock('@/database/node-repository');
vi.mock('@/services/workflow-versioning-service', () => ({
WorkflowVersioningService: vi.fn().mockImplementation(() => ({
createBackup: vi.fn().mockResolvedValue({ versionId: 'v1', versionNumber: 1, pruned: 0 }),
getVersions: vi.fn().mockResolvedValue([]),
})),
}));
vi.mock('@/config/n8n-api', () => ({
getN8nApiConfig: vi.fn()
}));
@@ -1343,4 +1349,142 @@ describe('handlers-n8n-manager', () => {
expect(result.error).toMatch(/mode:\s*'preview'/);
});
});
describe('handleUpdateWorkflow - credential preservation', () => {
function mockCurrentWorkflow(nodes: any[]): void {
const workflow = createTestWorkflow({ id: 'wf-1', active: false, nodes });
mockApiClient.getWorkflow.mockResolvedValue(workflow);
mockApiClient.updateWorkflow.mockResolvedValue({ ...workflow, updatedAt: '2024-01-02' });
}
function getSentNodes(): any[] {
return mockApiClient.updateWorkflow.mock.calls[0][1].nodes;
}
it('should preserve credentials from current workflow when update nodes omit them', async () => {
mockCurrentWorkflow([
{
id: 'node-1', name: 'Postgres', type: 'n8n-nodes-base.postgres',
typeVersion: 2, position: [100, 100],
parameters: { operation: 'executeQuery', query: 'SELECT 1' },
credentials: { postgresApi: { id: 'cred-123', name: 'My Postgres' } },
},
{
id: 'node-2', name: 'HTTP Request', type: 'n8n-nodes-base.httpRequest',
typeVersion: 4, position: [300, 100],
parameters: { url: 'https://example.com' },
credentials: { httpBasicAuth: { id: 'cred-456', name: 'Basic Auth' } },
},
{
id: 'node-3', name: 'Set', type: 'n8n-nodes-base.set',
typeVersion: 3, position: [500, 100], parameters: {},
},
]);
await handlers.handleUpdateWorkflow(
{
id: 'wf-1',
nodes: [
{
id: 'node-1', name: 'Postgres', type: 'n8n-nodes-base.postgres',
typeVersion: 2, position: [100, 100],
parameters: { operation: 'executeQuery', query: 'SELECT * FROM users' },
},
{
id: 'node-2', name: 'HTTP Request', type: 'n8n-nodes-base.httpRequest',
typeVersion: 4, position: [300, 100],
parameters: { url: 'https://example.com/v2' },
},
{
id: 'node-3', name: 'Set', type: 'n8n-nodes-base.set',
typeVersion: 3, position: [500, 100], parameters: { mode: 'manual' },
},
],
connections: {},
},
mockRepository,
);
const sentNodes = getSentNodes();
expect(sentNodes[0].credentials).toEqual({ postgresApi: { id: 'cred-123', name: 'My Postgres' } });
expect(sentNodes[1].credentials).toEqual({ httpBasicAuth: { id: 'cred-456', name: 'Basic Auth' } });
expect(sentNodes[2].credentials).toBeUndefined();
});
it('should not overwrite user-provided credentials', async () => {
mockCurrentWorkflow([
{
id: 'node-1', name: 'Postgres', type: 'n8n-nodes-base.postgres',
typeVersion: 2, position: [100, 100], parameters: {},
credentials: { postgresApi: { id: 'cred-old', name: 'Old Postgres' } },
},
]);
await handlers.handleUpdateWorkflow(
{
id: 'wf-1',
nodes: [
{
id: 'node-1', name: 'Postgres', type: 'n8n-nodes-base.postgres',
typeVersion: 2, position: [100, 100], parameters: {},
credentials: { postgresApi: { id: 'cred-new', name: 'New Postgres' } },
},
],
connections: {},
},
mockRepository,
);
const sentNodes = getSentNodes();
expect(sentNodes[0].credentials).toEqual({ postgresApi: { id: 'cred-new', name: 'New Postgres' } });
});
it('should match nodes by name when ids differ', async () => {
mockCurrentWorkflow([
{
id: 'server-id-1', name: 'Gmail', type: 'n8n-nodes-base.gmail',
typeVersion: 2, position: [100, 100], parameters: {},
credentials: { gmailOAuth2: { id: 'cred-gmail', name: 'Gmail' } },
},
]);
await handlers.handleUpdateWorkflow(
{
id: 'wf-1',
nodes: [
{
id: 'client-id-different', name: 'Gmail', type: 'n8n-nodes-base.gmail',
typeVersion: 2, position: [100, 100],
parameters: { resource: 'message' },
},
],
connections: {},
},
mockRepository,
);
const sentNodes = getSentNodes();
expect(sentNodes[0].credentials).toEqual({ gmailOAuth2: { id: 'cred-gmail', name: 'Gmail' } });
});
it('should treat empty credentials object as missing and carry forward', async () => {
mockCurrentWorkflow([
{ id: 'node-1', name: 'Postgres', type: 'n8n-nodes-base.postgres', typeVersion: 2, position: [100, 100], parameters: {}, credentials: { postgresApi: { id: 'cred-123', name: 'My Postgres' } } },
]);
await handlers.handleUpdateWorkflow(
{
id: 'wf-1',
nodes: [
{ id: 'node-1', name: 'Postgres', type: 'n8n-nodes-base.postgres', typeVersion: 2, position: [100, 100], parameters: {}, credentials: {} },
],
connections: {},
},
mockRepository,
);
const sentNodes = getSentNodes();
expect(sentNodes[0].credentials).toEqual({ postgresApi: { id: 'cred-123', name: 'My Postgres' } });
});
});
});