mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-31 00:37:10 +02:00
fix: Fallback to cli command if http request failed in npm-utils for community packages (#19413)
This commit is contained in:
parent
16e4c7e16e
commit
2bde2e66cd
|
|
@ -1,7 +1,27 @@
|
|||
import { UnexpectedError } from 'n8n-workflow';
|
||||
import nock from 'nock';
|
||||
|
||||
import { verifyIntegrity, isVersionExists } from '../npm-utils';
|
||||
const mockAsyncExec = jest.fn();
|
||||
|
||||
jest.mock('node:child_process', () => ({
|
||||
...jest.requireActual('node:child_process'),
|
||||
execFile: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('node:util', () => {
|
||||
const actual = jest.requireActual('node:util');
|
||||
return {
|
||||
...actual,
|
||||
promisify: jest.fn((fn) => {
|
||||
if (fn === require('node:child_process').execFile) {
|
||||
return mockAsyncExec;
|
||||
}
|
||||
return actual.promisify(fn);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
import { verifyIntegrity, checkIfVersionExistsOrThrow } from '../npm-utils';
|
||||
|
||||
describe('verifyIntegrity', () => {
|
||||
const registryUrl = 'https://registry.npmjs.org';
|
||||
|
|
@ -9,8 +29,14 @@ describe('verifyIntegrity', () => {
|
|||
const version = '1.0.0';
|
||||
const integrity = 'sha512-hash==';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockAsyncExec.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should verify integrity successfully', async () => {
|
||||
|
|
@ -39,57 +65,36 @@ describe('verifyIntegrity', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should throw error if metadata request fails', async () => {
|
||||
it('should throw error if metadata request fails and CLI fallback also fails', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.reply(500);
|
||||
|
||||
await expect(verifyIntegrity(packageName, version, registryUrl, integrity)).rejects.toThrow();
|
||||
mockAsyncExec.mockRejectedValue(new Error('CLI command failed'));
|
||||
|
||||
await expect(verifyIntegrity(packageName, version, registryUrl, integrity)).rejects.toThrow(
|
||||
UnexpectedError,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw UnexpectedError and preserve original error as cause', async () => {
|
||||
const integrity = 'sha512-somerandomhash==';
|
||||
|
||||
it('should throw UnexpectedError and preserve original error as cause when CLI fallback fails', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('Network failure');
|
||||
|
||||
try {
|
||||
await verifyIntegrity(packageName, version, registryUrl, integrity);
|
||||
throw new Error('Expected error was not thrown');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(UnexpectedError);
|
||||
expect(error.message).toBe('Checksum verification failed');
|
||||
expect(error.cause).toBeDefined();
|
||||
expect(error.cause.message).toContain('Network failure');
|
||||
}
|
||||
mockAsyncExec.mockRejectedValue(new Error('CLI command failed'));
|
||||
|
||||
await expect(verifyIntegrity(packageName, version, registryUrl, integrity)).rejects.toThrow(
|
||||
new UnexpectedError('Checksum verification failed'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return generic message for DNS getaddrinfo errors', async () => {
|
||||
const integrity = 'sha512-somerandomhash==';
|
||||
|
||||
it('should return generic message for DNS getaddrinfo errors when CLI fallback fails', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('getaddrinfo ENOTFOUND internal.registry.local');
|
||||
|
||||
try {
|
||||
await verifyIntegrity(packageName, version, registryUrl, integrity);
|
||||
throw new Error('Expected error was not thrown');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(UnexpectedError);
|
||||
expect(error.message).toBe(
|
||||
'Checksum verification failed. Please check your network connection and try again.',
|
||||
);
|
||||
expect(error.cause).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should return generic message for DNS ENOTFOUND errors', async () => {
|
||||
const integrity = 'sha512-somerandomhash==';
|
||||
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('ENOTFOUND some.internal.registry');
|
||||
mockAsyncExec.mockRejectedValue(new Error('getaddrinfo ENOTFOUND registry.npmjs.org'));
|
||||
|
||||
await expect(verifyIntegrity(packageName, version, registryUrl, integrity)).rejects.toThrow(
|
||||
new UnexpectedError(
|
||||
|
|
@ -97,15 +102,153 @@ describe('verifyIntegrity', () => {
|
|||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return generic message for DNS ENOTFOUND errors when CLI fallback fails', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('ENOTFOUND some.internal.registry');
|
||||
|
||||
mockAsyncExec.mockRejectedValue(new Error('ENOTFOUND registry.npmjs.org'));
|
||||
|
||||
await expect(verifyIntegrity(packageName, version, registryUrl, integrity)).rejects.toThrow(
|
||||
new UnexpectedError(
|
||||
'Checksum verification failed. Please check your network connection and try again.',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
describe('CLI fallback functionality', () => {
|
||||
it('should fallback to npm CLI when HTTP request fails and succeed', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('Network failure');
|
||||
|
||||
mockAsyncExec.mockResolvedValue({
|
||||
stdout: JSON.stringify(integrity),
|
||||
stderr: '',
|
||||
});
|
||||
|
||||
await expect(
|
||||
verifyIntegrity(packageName, version, registryUrl, integrity),
|
||||
).resolves.not.toThrow();
|
||||
|
||||
expect(mockAsyncExec).toHaveBeenCalledTimes(1);
|
||||
expect(mockAsyncExec).toHaveBeenCalledWith('npm', [
|
||||
'view',
|
||||
`${packageName}@${version}`,
|
||||
'dist.integrity',
|
||||
`--registry=${registryUrl}`,
|
||||
'--json',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fallback to npm CLI and throw error when integrity does not match', async () => {
|
||||
const wrongIntegrity = 'sha512-wronghash==';
|
||||
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('Network failure');
|
||||
|
||||
mockAsyncExec.mockResolvedValue({
|
||||
stdout: JSON.stringify(wrongIntegrity),
|
||||
stderr: '',
|
||||
});
|
||||
|
||||
await expect(verifyIntegrity(packageName, version, registryUrl, integrity)).rejects.toThrow(
|
||||
new UnexpectedError('Checksum verification failed'),
|
||||
);
|
||||
|
||||
expect(mockAsyncExec).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle special characters in package name and version safely', async () => {
|
||||
const specialPackageName = 'test-package; rm -rf /';
|
||||
const specialVersion = '1.0.0 && echo "hacked"';
|
||||
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(specialPackageName)}/${specialVersion}`)
|
||||
.replyWithError('Network failure');
|
||||
|
||||
mockAsyncExec.mockResolvedValue({
|
||||
stdout: JSON.stringify(integrity),
|
||||
stderr: '',
|
||||
});
|
||||
|
||||
await verifyIntegrity(specialPackageName, specialVersion, registryUrl, integrity);
|
||||
|
||||
expect(mockAsyncExec).toHaveBeenCalledTimes(1);
|
||||
expect(mockAsyncExec).toHaveBeenCalledWith('npm', [
|
||||
'view',
|
||||
`${specialPackageName}@${specialVersion}`,
|
||||
'dist.integrity',
|
||||
`--registry=${registryUrl}`,
|
||||
'--json',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle DNS errors in CLI fallback', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('Network failure');
|
||||
|
||||
mockAsyncExec.mockRejectedValue(new Error('getaddrinfo ENOTFOUND registry.npmjs.org'));
|
||||
|
||||
await expect(verifyIntegrity(packageName, version, registryUrl, integrity)).rejects.toThrow(
|
||||
new UnexpectedError(
|
||||
'Checksum verification failed. Please check your network connection and try again.',
|
||||
),
|
||||
);
|
||||
|
||||
expect(mockAsyncExec).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle npm errors in CLI fallback', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('Network failure');
|
||||
|
||||
mockAsyncExec.mockRejectedValue(
|
||||
new Error('npm ERR! 404 Not Found - GET https://registry.npmjs.org/nonexistent-package'),
|
||||
);
|
||||
|
||||
await expect(verifyIntegrity(packageName, version, registryUrl, integrity)).rejects.toThrow(
|
||||
new UnexpectedError(
|
||||
'Checksum verification failed. Please check your network connection and try again.',
|
||||
),
|
||||
);
|
||||
|
||||
expect(mockAsyncExec).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle generic CLI errors', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('Network failure');
|
||||
|
||||
mockAsyncExec.mockRejectedValue(new Error('Some other error'));
|
||||
|
||||
await expect(verifyIntegrity(packageName, version, registryUrl, integrity)).rejects.toThrow(
|
||||
new UnexpectedError('Checksum verification failed'),
|
||||
);
|
||||
|
||||
expect(mockAsyncExec).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isVersionExists', () => {
|
||||
describe('checkIfVersionExistsOrThrow', () => {
|
||||
const registryUrl = 'https://registry.npmjs.org';
|
||||
const packageName = 'test-package';
|
||||
const version = '1.0.0';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockAsyncExec.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return true when package version exists', async () => {
|
||||
|
|
@ -116,92 +259,232 @@ describe('isVersionExists', () => {
|
|||
version,
|
||||
});
|
||||
|
||||
const result = await isVersionExists(packageName, version, registryUrl);
|
||||
const result = await checkIfVersionExistsOrThrow(packageName, version, registryUrl);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw UnexpectedError when package version does not exist (404)', async () => {
|
||||
it('should throw UnexpectedError when package version does not exist (404) and CLI fallback also fails', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.reply(404);
|
||||
|
||||
await expect(isVersionExists(packageName, version, registryUrl)).rejects.toThrow(
|
||||
UnexpectedError,
|
||||
mockAsyncExec.mockRejectedValue(new Error('E404 Not Found'));
|
||||
|
||||
await expect(checkIfVersionExistsOrThrow(packageName, version, registryUrl)).rejects.toThrow(
|
||||
new UnexpectedError('Package version does not exist'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw UnexpectedError with proper message on 404', async () => {
|
||||
it('should throw UnexpectedError with proper message on 404 when CLI fallback fails', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.reply(404);
|
||||
|
||||
try {
|
||||
await isVersionExists(packageName, version, registryUrl);
|
||||
throw new Error('Expected error was not thrown');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(UnexpectedError);
|
||||
expect(error.message).toBe('Package version does not exist');
|
||||
expect(error.cause).toBeDefined();
|
||||
}
|
||||
mockAsyncExec.mockRejectedValue(new Error('Some error'));
|
||||
|
||||
await expect(checkIfVersionExistsOrThrow(packageName, version, registryUrl)).rejects.toThrow(
|
||||
new UnexpectedError('Failed to check package version existence'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw UnexpectedError for network failures', async () => {
|
||||
it('should throw UnexpectedError for network failures when CLI fallback fails', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('Network failure');
|
||||
|
||||
try {
|
||||
await isVersionExists(packageName, version, registryUrl);
|
||||
throw new Error('Expected error was not thrown');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(UnexpectedError);
|
||||
expect(error.message).toBe('Failed to check package version existence');
|
||||
expect(error.cause).toBeDefined();
|
||||
expect(error.cause.message).toContain('Network failure');
|
||||
}
|
||||
mockAsyncExec.mockRejectedValue(new Error('CLI network failure'));
|
||||
|
||||
await expect(checkIfVersionExistsOrThrow(packageName, version, registryUrl)).rejects.toThrow(
|
||||
new UnexpectedError('Failed to check package version existence'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw UnexpectedError for server errors (500)', async () => {
|
||||
it('should throw UnexpectedError for server errors (500) when CLI fallback fails', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.reply(500);
|
||||
|
||||
await expect(isVersionExists(packageName, version, registryUrl)).rejects.toThrow(
|
||||
mockAsyncExec.mockRejectedValue(new Error('CLI error'));
|
||||
|
||||
await expect(checkIfVersionExistsOrThrow(packageName, version, registryUrl)).rejects.toThrow(
|
||||
UnexpectedError,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return generic message for DNS getaddrinfo errors', async () => {
|
||||
it('should return generic message for DNS getaddrinfo errors when CLI fallback fails', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('getaddrinfo ENOTFOUND internal.registry.local');
|
||||
|
||||
try {
|
||||
await isVersionExists(packageName, version, registryUrl);
|
||||
throw new Error('Expected error was not thrown');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(UnexpectedError);
|
||||
expect(error.message).toBe(
|
||||
mockAsyncExec.mockRejectedValue(new Error('getaddrinfo ENOTFOUND registry.npmjs.org'));
|
||||
|
||||
await expect(checkIfVersionExistsOrThrow(packageName, version, registryUrl)).rejects.toThrow(
|
||||
new UnexpectedError(
|
||||
'The community nodes service is temporarily unreachable. Please try again later.',
|
||||
);
|
||||
expect(error.cause).toBeUndefined();
|
||||
}
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return generic message for DNS ENOTFOUND errors', async () => {
|
||||
it('should return generic message for DNS ENOTFOUND errors when CLI fallback fails', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('ENOTFOUND some.internal.registry');
|
||||
|
||||
try {
|
||||
await isVersionExists(packageName, version, registryUrl);
|
||||
throw new Error('Expected error was not thrown');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(UnexpectedError);
|
||||
expect(error.message).toBe(
|
||||
mockAsyncExec.mockRejectedValue(new Error('ENOTFOUND registry.npmjs.org'));
|
||||
|
||||
await expect(checkIfVersionExistsOrThrow(packageName, version, registryUrl)).rejects.toThrow(
|
||||
new UnexpectedError(
|
||||
'The community nodes service is temporarily unreachable. Please try again later.',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
describe('CLI fallback functionality', () => {
|
||||
it('should fallback to npm CLI when HTTP request fails and return true', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('Network failure');
|
||||
|
||||
mockAsyncExec.mockResolvedValue({
|
||||
stdout: JSON.stringify(version),
|
||||
stderr: '',
|
||||
});
|
||||
|
||||
const result = await checkIfVersionExistsOrThrow(packageName, version, registryUrl);
|
||||
expect(result).toBe(true);
|
||||
expect(mockAsyncExec).toHaveBeenCalledTimes(1);
|
||||
expect(mockAsyncExec).toHaveBeenCalledWith('npm', [
|
||||
'view',
|
||||
`${packageName}@${version}`,
|
||||
'version',
|
||||
`--registry=${registryUrl}`,
|
||||
'--json',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fallback to npm CLI and throw error when version does not match', async () => {
|
||||
const differentVersion = '2.0.0';
|
||||
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('Network failure');
|
||||
|
||||
mockAsyncExec.mockResolvedValue({
|
||||
stdout: JSON.stringify(differentVersion),
|
||||
stderr: '',
|
||||
});
|
||||
|
||||
await expect(checkIfVersionExistsOrThrow(packageName, version, registryUrl)).rejects.toThrow(
|
||||
new UnexpectedError('Failed to check package version existence'),
|
||||
);
|
||||
expect(error.cause).toBeUndefined();
|
||||
}
|
||||
expect(mockAsyncExec).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle special characters in package name and version safely for checkIfVersionExistsOrThrow', async () => {
|
||||
const specialPackageName = 'test-package; rm -rf /';
|
||||
const specialVersion = '1.0.0 && echo "hacked"';
|
||||
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(specialPackageName)}/${specialVersion}`)
|
||||
.replyWithError('Network failure');
|
||||
|
||||
mockAsyncExec.mockResolvedValue({
|
||||
stdout: JSON.stringify(specialVersion),
|
||||
stderr: '',
|
||||
});
|
||||
|
||||
const result = await checkIfVersionExistsOrThrow(
|
||||
specialPackageName,
|
||||
specialVersion,
|
||||
registryUrl,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
expect(mockAsyncExec).toHaveBeenCalledTimes(1);
|
||||
expect(mockAsyncExec).toHaveBeenCalledWith('npm', [
|
||||
'view',
|
||||
`${specialPackageName}@${specialVersion}`,
|
||||
'version',
|
||||
`--registry=${registryUrl}`,
|
||||
'--json',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle 404 errors in CLI fallback', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('Network failure');
|
||||
|
||||
mockAsyncExec.mockRejectedValue(
|
||||
new Error('E404 Not Found - GET https://registry.npmjs.org/nonexistent-package'),
|
||||
);
|
||||
|
||||
await expect(checkIfVersionExistsOrThrow(packageName, version, registryUrl)).rejects.toThrow(
|
||||
new UnexpectedError('Package version does not exist'),
|
||||
);
|
||||
|
||||
expect(mockAsyncExec).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle DNS errors in CLI fallback for checkIfVersionExistsOrThrow', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('Network failure');
|
||||
|
||||
mockAsyncExec.mockRejectedValue(new Error('getaddrinfo ENOTFOUND registry.npmjs.org'));
|
||||
|
||||
await expect(checkIfVersionExistsOrThrow(packageName, version, registryUrl)).rejects.toThrow(
|
||||
new UnexpectedError(
|
||||
'The community nodes service is temporarily unreachable. Please try again later.',
|
||||
),
|
||||
);
|
||||
|
||||
expect(mockAsyncExec).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle npm errors in CLI fallback for checkIfVersionExistsOrThrow', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('Network failure');
|
||||
|
||||
mockAsyncExec.mockRejectedValue(new Error('npm ERR! 500 Internal Server Error'));
|
||||
|
||||
await expect(checkIfVersionExistsOrThrow(packageName, version, registryUrl)).rejects.toThrow(
|
||||
new UnexpectedError(
|
||||
'The community nodes service is temporarily unreachable. Please try again later.',
|
||||
),
|
||||
);
|
||||
|
||||
expect(mockAsyncExec).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle generic CLI errors for checkIfVersionExistsOrThrow', async () => {
|
||||
nock(registryUrl)
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.replyWithError('Network failure');
|
||||
|
||||
mockAsyncExec.mockRejectedValue(new Error('Some other error'));
|
||||
|
||||
await expect(checkIfVersionExistsOrThrow(packageName, version, registryUrl)).rejects.toThrow(
|
||||
new UnexpectedError('Failed to check package version existence'),
|
||||
);
|
||||
|
||||
expect(mockAsyncExec).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Helper functions', () => {
|
||||
it('should sanitize registry URL by removing trailing slashes', async () => {
|
||||
const registryWithSlashes = 'https://registry.npmjs.org///';
|
||||
|
||||
nock('https://registry.npmjs.org')
|
||||
.get(`/${encodeURIComponent(packageName)}/${version}`)
|
||||
.reply(200, {
|
||||
name: packageName,
|
||||
version,
|
||||
});
|
||||
|
||||
const result = await checkIfVersionExistsOrThrow(packageName, version, registryWithSlashes);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { CommunityPackagesConfig } from './community-packages.config';
|
|||
import type { CommunityPackages } from './community-packages.types';
|
||||
import { InstalledPackages } from './installed-packages.entity';
|
||||
import { InstalledPackagesRepository } from './installed-packages.repository';
|
||||
import { isVersionExists, verifyIntegrity } from './npm-utils';
|
||||
import { checkIfVersionExistsOrThrow, verifyIntegrity } from './npm-utils';
|
||||
|
||||
const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
||||
const NPM_COMMON_ARGS = ['--audit=false', '--fund=false'];
|
||||
|
|
@ -398,7 +398,7 @@ export class CommunityPackagesService {
|
|||
await verifyIntegrity(packageName, packageVersion, this.getNpmRegistry(), options.checksum);
|
||||
}
|
||||
|
||||
await isVersionExists(packageName, packageVersion, this.getNpmRegistry());
|
||||
await checkIfVersionExistsOrThrow(packageName, packageVersion, this.getNpmRegistry());
|
||||
|
||||
try {
|
||||
await this.downloadPackage(packageName, packageVersion);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,30 @@
|
|||
import axios from 'axios';
|
||||
import { UnexpectedError } from 'n8n-workflow';
|
||||
import { jsonParse, UnexpectedError } from 'n8n-workflow';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const asyncExecFile = promisify(execFile);
|
||||
|
||||
const REQUEST_TIMEOUT = 30000;
|
||||
|
||||
function isDnsError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return message.includes('getaddrinfo') || message.includes('ENOTFOUND');
|
||||
}
|
||||
|
||||
const REQUEST_TIMEOUT = 30000;
|
||||
function isNpmError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return (
|
||||
message.includes('npm ERR!') ||
|
||||
message.includes('E404') ||
|
||||
message.includes('404 Not Found') ||
|
||||
message.includes('ENOTFOUND')
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeRegistryUrl(registryUrl: string): string {
|
||||
return registryUrl.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
export async function verifyIntegrity(
|
||||
packageName: string,
|
||||
|
|
@ -14,50 +32,86 @@ export async function verifyIntegrity(
|
|||
registryUrl: string,
|
||||
expectedIntegrity: string,
|
||||
) {
|
||||
const timeoutOption = { timeout: REQUEST_TIMEOUT };
|
||||
const url = `${sanitizeRegistryUrl(registryUrl)}/${encodeURIComponent(packageName)}`;
|
||||
|
||||
try {
|
||||
const url = `${registryUrl.replace(/\/+$/, '')}/${encodeURIComponent(packageName)}`;
|
||||
const metadata = await axios.get<{ dist: { integrity: string } }>(
|
||||
`${url}/${version}`,
|
||||
timeoutOption,
|
||||
);
|
||||
const metadata = await axios.get<{ dist: { integrity?: string } }>(`${url}/${version}`, {
|
||||
timeout: REQUEST_TIMEOUT,
|
||||
});
|
||||
|
||||
if (metadata?.data?.dist?.integrity !== expectedIntegrity) {
|
||||
const integrity = metadata?.data?.dist?.integrity;
|
||||
if (integrity !== expectedIntegrity) {
|
||||
throw new UnexpectedError('Checksum verification failed. Package integrity does not match.');
|
||||
}
|
||||
return;
|
||||
} catch (error) {
|
||||
if (isDnsError(error)) {
|
||||
throw new UnexpectedError(
|
||||
'Checksum verification failed. Please check your network connection and try again.',
|
||||
);
|
||||
try {
|
||||
const { stdout } = await asyncExecFile('npm', [
|
||||
'view',
|
||||
`${packageName}@${version}`,
|
||||
'dist.integrity',
|
||||
`--registry=${sanitizeRegistryUrl(registryUrl)}`,
|
||||
'--json',
|
||||
]);
|
||||
|
||||
const integrity = jsonParse(stdout);
|
||||
if (integrity !== expectedIntegrity) {
|
||||
throw new UnexpectedError(
|
||||
'Checksum verification failed. Package integrity does not match.',
|
||||
);
|
||||
}
|
||||
return;
|
||||
} catch (cliError) {
|
||||
if (isDnsError(cliError) || isNpmError(cliError)) {
|
||||
throw new UnexpectedError(
|
||||
'Checksum verification failed. Please check your network connection and try again.',
|
||||
);
|
||||
}
|
||||
throw new UnexpectedError('Checksum verification failed');
|
||||
}
|
||||
throw new UnexpectedError('Checksum verification failed', { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
export async function isVersionExists(
|
||||
export async function checkIfVersionExistsOrThrow(
|
||||
packageName: string,
|
||||
version: string,
|
||||
registryUrl: string,
|
||||
): Promise<boolean> {
|
||||
const timeoutOption = { timeout: REQUEST_TIMEOUT };
|
||||
): Promise<true> {
|
||||
const url = `${sanitizeRegistryUrl(registryUrl)}/${encodeURIComponent(packageName)}`;
|
||||
|
||||
try {
|
||||
const url = `${registryUrl.replace(/\/+$/, '')}/${encodeURIComponent(packageName)}`;
|
||||
await axios.get(`${url}/${version}`, timeoutOption);
|
||||
await axios.get(`${url}/${version}`, { timeout: REQUEST_TIMEOUT });
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||
throw new UnexpectedError('Package version does not exist', {
|
||||
cause: error,
|
||||
});
|
||||
try {
|
||||
const { stdout } = await asyncExecFile('npm', [
|
||||
'view',
|
||||
`${packageName}@${version}`,
|
||||
'version',
|
||||
`--registry=${sanitizeRegistryUrl(registryUrl)}`,
|
||||
'--json',
|
||||
]);
|
||||
|
||||
const versionInfo = jsonParse(stdout);
|
||||
if (versionInfo === version) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new UnexpectedError('Failed to check package version existence');
|
||||
} catch (cliError) {
|
||||
const message = cliError instanceof Error ? cliError.message : String(cliError);
|
||||
|
||||
if (message.includes('E404') || message.includes('404 Not Found')) {
|
||||
throw new UnexpectedError('Package version does not exist');
|
||||
}
|
||||
|
||||
if (isDnsError(cliError) || isNpmError(cliError)) {
|
||||
throw new UnexpectedError(
|
||||
'The community nodes service is temporarily unreachable. Please try again later.',
|
||||
);
|
||||
}
|
||||
|
||||
throw new UnexpectedError('Failed to check package version existence');
|
||||
}
|
||||
if (isDnsError(error)) {
|
||||
throw new UnexpectedError(
|
||||
'The community nodes service is temporarily unreachable. Please try again later.',
|
||||
);
|
||||
}
|
||||
throw new UnexpectedError('Failed to check package version existence', { cause: error });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user