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