221 lines
8.3 KiB
JavaScript
221 lines
8.3 KiB
JavaScript
import { parse, simpleTraverse, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
|
|
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { dirname, parse as parsePath } from 'node:path';
|
|
import { isCredentialTypeClass, isNodeTypeClass, findClassProperty, getStringLiteralValue, findArrayLiteralProperty, extractCredentialInfoFromArray, findSimilarStrings, } from './ast-utils.js';
|
|
/**
|
|
* Checks if the given childPath is contained within the parentPath. Resolves
|
|
* the paths before comparing them, so that relative paths are also supported.
|
|
*/
|
|
export function isContainedWithin(parentPath, childPath) {
|
|
parentPath = path.resolve(parentPath);
|
|
childPath = path.resolve(childPath);
|
|
if (parentPath === childPath) {
|
|
return true;
|
|
}
|
|
return childPath.startsWith(parentPath + path.sep);
|
|
}
|
|
/**
|
|
* Joins the given paths to the parentPath, ensuring that the resulting path
|
|
* is still contained within the parentPath. If not, it throws an error to
|
|
* prevent path traversal vulnerabilities.
|
|
*
|
|
* @throws {UnexpectedError} If the resulting path is not contained within the parentPath.
|
|
*/
|
|
export function safeJoinPath(parentPath, ...paths) {
|
|
const candidate = path.join(parentPath, ...paths);
|
|
if (!isContainedWithin(parentPath, candidate)) {
|
|
throw new Error(`Path traversal detected, refusing to join paths: ${parentPath} and ${JSON.stringify(paths)}`);
|
|
}
|
|
return candidate;
|
|
}
|
|
export function findPackageJson(startPath) {
|
|
let currentDir = path.dirname(startPath);
|
|
while (parsePath(currentDir).dir !== parsePath(currentDir).root) {
|
|
const testPath = safeJoinPath(currentDir, 'package.json');
|
|
if (fileExistsWithCaseSync(testPath)) {
|
|
return testPath;
|
|
}
|
|
currentDir = dirname(currentDir);
|
|
}
|
|
return null;
|
|
}
|
|
function isValidPackageJson(obj) {
|
|
return typeof obj === 'object' && obj !== null;
|
|
}
|
|
function readPackageJsonN8n(packageJsonPath) {
|
|
try {
|
|
const content = readFileSync(packageJsonPath, 'utf8');
|
|
const parsed = JSON.parse(content);
|
|
if (isValidPackageJson(parsed)) {
|
|
return parsed.n8n ?? {};
|
|
}
|
|
return {};
|
|
}
|
|
catch {
|
|
return {};
|
|
}
|
|
}
|
|
function resolveN8nFilePaths(packageJsonPath, filePaths) {
|
|
const packageDir = dirname(packageJsonPath);
|
|
const resolvedFiles = [];
|
|
for (const filePath of filePaths) {
|
|
const sourcePath = filePath.replace(/^dist\//, '').replace(/\.js$/, '.ts');
|
|
const fullSourcePath = safeJoinPath(packageDir, sourcePath);
|
|
if (existsSync(fullSourcePath)) {
|
|
resolvedFiles.push(fullSourcePath);
|
|
}
|
|
}
|
|
return resolvedFiles;
|
|
}
|
|
export function readPackageJsonCredentials(packageJsonPath) {
|
|
const n8nConfig = readPackageJsonN8n(packageJsonPath);
|
|
const credentialPaths = n8nConfig.credentials ?? [];
|
|
const credentialFiles = resolveN8nFilePaths(packageJsonPath, credentialPaths);
|
|
const credentialNames = [];
|
|
for (const credentialFile of credentialFiles) {
|
|
try {
|
|
const credentialName = extractCredentialNameFromFile(credentialFile);
|
|
if (credentialName) {
|
|
credentialNames.push(credentialName);
|
|
}
|
|
}
|
|
catch {
|
|
// Silently continue if file can't be parsed
|
|
}
|
|
}
|
|
return new Set(credentialNames);
|
|
}
|
|
export function extractCredentialNameFromFile(credentialFilePath) {
|
|
try {
|
|
const sourceCode = readFileSync(credentialFilePath, 'utf8');
|
|
const ast = parse(sourceCode, {
|
|
jsx: false,
|
|
range: true,
|
|
});
|
|
let credentialName = null;
|
|
simpleTraverse(ast, {
|
|
enter(node) {
|
|
if (node.type === AST_NODE_TYPES.ClassDeclaration && isCredentialTypeClass(node)) {
|
|
const nameProperty = findClassProperty(node, 'name');
|
|
if (nameProperty) {
|
|
const nameValue = getStringLiteralValue(nameProperty.value);
|
|
if (nameValue) {
|
|
credentialName = nameValue;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
});
|
|
return credentialName;
|
|
}
|
|
catch {
|
|
return null;
|
|
}
|
|
}
|
|
export function validateIconPath(iconPath, baseDir) {
|
|
const isFile = iconPath.startsWith('file:');
|
|
const relativePath = iconPath.replace(/^file:/, '');
|
|
const isSvg = relativePath.endsWith('.svg');
|
|
// Should not use safeJoinPath here because iconPath can be outside of the node class folder
|
|
const fullPath = path.join(baseDir, relativePath);
|
|
const exists = fileExistsWithCaseSync(fullPath);
|
|
return {
|
|
isValid: isFile && isSvg && exists,
|
|
isFile,
|
|
isSvg,
|
|
exists,
|
|
};
|
|
}
|
|
export function readPackageJsonNodes(packageJsonPath) {
|
|
const n8nConfig = readPackageJsonN8n(packageJsonPath);
|
|
const nodePaths = n8nConfig.nodes ?? [];
|
|
return resolveN8nFilePaths(packageJsonPath, nodePaths);
|
|
}
|
|
export function areAllCredentialUsagesTestedByNodes(credentialName, packageDir) {
|
|
const packageJsonPath = safeJoinPath(packageDir, 'package.json');
|
|
if (!existsSync(packageJsonPath)) {
|
|
return false;
|
|
}
|
|
const nodeFiles = readPackageJsonNodes(packageJsonPath);
|
|
let hasAnyCredentialUsage = false;
|
|
for (const nodeFile of nodeFiles) {
|
|
const result = checkCredentialUsageInFile(nodeFile, credentialName);
|
|
if (result.hasUsage) {
|
|
hasAnyCredentialUsage = true;
|
|
if (!result.allTestedBy) {
|
|
return false; // Found usage without testedBy
|
|
}
|
|
}
|
|
}
|
|
return hasAnyCredentialUsage;
|
|
}
|
|
function checkCredentialUsageInFile(nodeFile, credentialName) {
|
|
try {
|
|
const sourceCode = readFileSync(nodeFile, 'utf8');
|
|
const ast = parse(sourceCode, { jsx: false, range: true });
|
|
let hasUsage = false;
|
|
let allTestedBy = true;
|
|
simpleTraverse(ast, {
|
|
enter(node) {
|
|
if (node.type === AST_NODE_TYPES.ClassDeclaration && isNodeTypeClass(node)) {
|
|
const descriptionProperty = findClassProperty(node, 'description');
|
|
if (!descriptionProperty?.value ||
|
|
descriptionProperty.value.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
return;
|
|
}
|
|
const credentialsArray = findArrayLiteralProperty(descriptionProperty.value, 'credentials');
|
|
if (!credentialsArray) {
|
|
return;
|
|
}
|
|
for (const element of credentialsArray.elements) {
|
|
const credentialInfo = extractCredentialInfoFromArray(element);
|
|
if (credentialInfo?.name === credentialName) {
|
|
hasUsage = true;
|
|
if (!credentialInfo.testedBy) {
|
|
allTestedBy = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
});
|
|
return { hasUsage, allTestedBy };
|
|
}
|
|
catch {
|
|
return { hasUsage: false, allTestedBy: true };
|
|
}
|
|
}
|
|
function fileExistsWithCaseSync(filePath) {
|
|
try {
|
|
const dir = path.dirname(filePath);
|
|
const file = path.basename(filePath);
|
|
const files = new Set(readdirSync(dir));
|
|
return files.has(file);
|
|
}
|
|
catch {
|
|
return false;
|
|
}
|
|
}
|
|
export function findSimilarSvgFiles(targetPath, baseDir) {
|
|
try {
|
|
const targetFileName = path.basename(targetPath, path.extname(targetPath));
|
|
const targetDir = path.dirname(targetPath);
|
|
// Should not use safeJoinPath here because iconPath can be outside of the node class folder
|
|
const searchDir = path.join(baseDir, targetDir);
|
|
if (!existsSync(searchDir)) {
|
|
return [];
|
|
}
|
|
const files = readdirSync(searchDir);
|
|
const svgFileNames = files
|
|
.filter((file) => file.endsWith('.svg'))
|
|
.map((file) => path.basename(file, '.svg'));
|
|
const candidateNames = new Set(svgFileNames);
|
|
const similarNames = findSimilarStrings(targetFileName, candidateNames);
|
|
return similarNames.map((name) => path.join(targetDir, `${name}.svg`));
|
|
}
|
|
catch {
|
|
return [];
|
|
}
|
|
}
|
|
//# sourceMappingURL=file-utils.js.map
|