Fix app install file upload (#18593)

remove wrong file path based file selection

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
This commit is contained in:
martmull
2026-03-13 12:06:14 +01:00
committed by GitHub
parent 0641e07ca6
commit f3e0c12ce6
8 changed files with 251 additions and 82 deletions
@@ -2,8 +2,9 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { promises as fs } from 'fs';
import { join, relative } from 'path';
import { resolve } from 'path';
import { Manifest } from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
@@ -24,23 +25,6 @@ import { ApplicationSyncService } from 'src/engine/core-modules/application/appl
import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
const FILE_FOLDER_MAPPING: Record<string, FileFolder> = {
'package.json': FileFolder.Dependencies,
'yarn.lock': FileFolder.Dependencies,
};
const FILE_FOLDER_PATTERN_MAPPING: Array<{
pattern: RegExp;
folder: FileFolder;
}> = [
{ pattern: /\.function\.mjs$/, folder: FileFolder.BuiltLogicFunction },
{
pattern: /\.front-component\.mjs$/,
folder: FileFolder.BuiltFrontComponent,
},
{ pattern: /^public\//, folder: FileFolder.PublicAsset },
];
@Injectable()
export class ApplicationInstallService {
private readonly logger = new Logger(ApplicationInstallService.name);
@@ -123,6 +107,7 @@ export class ApplicationInstallService {
await this.writeFilesToStorage(
resolvedPackage.extractedDir,
resolvedPackage.manifest,
universalIdentifier,
params.workspaceId,
);
@@ -155,15 +140,32 @@ export class ApplicationInstallService {
private async writeFilesToStorage(
extractedDir: string,
manifest: Manifest,
applicationUniversalIdentifier: string,
workspaceId: string,
): Promise<void> {
const files = await this.collectFiles(extractedDir);
const filesToWrite = this.buildFileList(manifest);
for (const filePath of files) {
const relativePath = relative(extractedDir, filePath);
const fileFolder = this.resolveFileFolder(relativePath);
const content = await fs.readFile(filePath);
for (const { relativePath, fileFolder } of filesToWrite) {
const absolutePath = resolve(extractedDir, relativePath);
if (!absolutePath.startsWith(extractedDir)) {
throw new ApplicationException(
`Path traversal detected for file: ${relativePath}`,
ApplicationExceptionCode.INVALID_INPUT,
);
}
let content: Buffer;
try {
content = await fs.readFile(absolutePath);
} catch {
throw new ApplicationException(
`File not found in package: ${relativePath}`,
ApplicationExceptionCode.PACKAGE_RESOLUTION_FAILED,
);
}
await this.fileStorageService.writeFile({
sourceFile: content,
@@ -177,47 +179,38 @@ export class ApplicationInstallService {
}
}
private resolveFileFolder(relativePath: string): FileFolder {
const exact = FILE_FOLDER_MAPPING[relativePath];
private buildFileList(
manifest: Manifest,
): Array<{ relativePath: string; fileFolder: FileFolder }> {
const files: Array<{ relativePath: string; fileFolder: FileFolder }> = [];
if (isDefined(exact)) {
return exact;
files.push(
{ relativePath: 'package.json', fileFolder: FileFolder.Dependencies },
{ relativePath: 'manifest.json', fileFolder: FileFolder.Source },
);
for (const logicFunction of manifest.logicFunctions ?? []) {
files.push({
relativePath: logicFunction.builtHandlerPath,
fileFolder: FileFolder.BuiltLogicFunction,
});
}
for (const { pattern, folder } of FILE_FOLDER_PATTERN_MAPPING) {
if (pattern.test(relativePath)) {
return folder;
}
for (const frontComponent of manifest.frontComponents ?? []) {
files.push({
relativePath: frontComponent.builtComponentPath,
fileFolder: FileFolder.BuiltFrontComponent,
});
}
return FileFolder.Source;
}
private async collectFiles(dir: string): Promise<string[]> {
const result: string[] = [];
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.name === 'node_modules' || entry.name === '.yarn') {
continue;
}
if (entry.isSymbolicLink()) {
continue;
}
if (entry.isDirectory()) {
const subFiles = await this.collectFiles(fullPath);
result.push(...subFiles);
} else {
result.push(fullPath);
}
for (const publicAsset of manifest.publicAssets ?? []) {
files.push({
relativePath: publicAsset.filePath,
fileFolder: FileFolder.PublicAsset,
});
}
return result;
return files;
}
private async ensureApplicationExists(params: {
@@ -1,7 +1,8 @@
import { Injectable } from '@nestjs/common';
import crypto from 'crypto';
import { join } from 'path';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -239,7 +240,16 @@ export class LogicFunctionResourceService {
workspaceId: string;
inMemoryFolderPath: string;
}) {
await Promise.all([
const yarnLockExists = await this.fileStorageService.checkFileExists({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'yarn.lock',
});
const promises = [];
promises.push(
this.fileStorageService.downloadFile({
workspaceId,
applicationUniversalIdentifier,
@@ -247,14 +257,35 @@ export class LogicFunctionResourceService {
resourcePath: 'package.json',
localPath: join(inMemoryFolderPath, 'package.json'),
}),
this.fileStorageService.downloadFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'yarn.lock',
localPath: join(inMemoryFolderPath, 'yarn.lock'),
}),
]);
);
if (yarnLockExists) {
promises.push(
this.fileStorageService.downloadFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'yarn.lock',
localPath: join(inMemoryFolderPath, 'yarn.lock'),
}),
);
} else {
const yarnLockPath = join(inMemoryFolderPath, 'yarn.lock');
promises.push(
fs.mkdir(dirname(yarnLockPath), { recursive: true }).then(() =>
fs.writeFile(
yarnLockPath,
`# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
`,
'utf-8',
),
),
);
}
await Promise.all(promises);
}
async getBuiltCode({