From 9899ee3f7fb2a32e59646d6c345d72ecaa4c98eb Mon Sep 17 00:00:00 2001 From: Benjtalkshow Date: Wed, 6 Aug 2025 00:25:03 +0100 Subject: [PATCH 1/4] Add Security Middleware (Helmet, CORS) --- apps/backend/bun.lock | 3 +++ apps/backend/src/index.ts | 52 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/apps/backend/bun.lock b/apps/backend/bun.lock index 0635f46..167e10a 100644 --- a/apps/backend/bun.lock +++ b/apps/backend/bun.lock @@ -24,6 +24,9 @@ "typescript": "5.8.3", "typescript-eslint": "^8.30.0", }, + "peerDependencies": { + "typescript": "^5.8.3", + }, }, }, "packages": { diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index f9426d7..3726eac 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -1,6 +1,56 @@ import express from 'express'; +import helmet from 'helmet'; +import cors from 'cors'; + const app = express(); + +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + scriptSrc: ["'self'"], + imgSrc: ["'self'", "data:", "https:"], + }, + }, + crossOriginEmbedderPolicy: false, + hsts: { + maxAge: 31536000, + includeSubDomains: true, + preload: true + } +})); + +app.use(cors({ + origin: 'http://localhost:4200', + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'Accept'], + optionsSuccessStatus: 200 +})); + +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + app.get('/', (_, res) => res.send('Hello from Backend!' + '
' + 'The best online soroban compiler is coming...') ); -app.listen(3000, () => console.log('Server on http://localhost:3000')); + +app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { + res.status(500).json({ + error: 'Internal Server Error', + message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong' + }); +}); + +app.use((req, res) => { + res.status(404).json({ + error: 'Not Found', + message: `Route ${req.originalUrl} not found` + }); +}); + +app.listen(3000, () => { + console.log('Server on http://localhost:3000'); + console.log('CORS restricted to http://localhost:4200'); +}); \ No newline at end of file From e60ac0bb1c7fd44028be2c192b5ebb9272039cd4 Mon Sep 17 00:00:00 2001 From: Benjtalkshow Date: Thu, 7 Aug 2025 00:51:25 +0100 Subject: [PATCH 2/4] feat: create file manager utility --- apps/backend/bun.lock | 2 +- apps/backend/index.ts | 1 + apps/backend/package.json | 7 +++- apps/backend/scripts/clean.ts | 15 +++++++ apps/backend/scripts/setup.ts | 17 ++++++++ apps/backend/src/utils/fileManager.ts | 57 +++++++++++++++++++++++++++ 6 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 apps/backend/index.ts create mode 100644 apps/backend/scripts/clean.ts create mode 100644 apps/backend/scripts/setup.ts create mode 100644 apps/backend/src/utils/fileManager.ts diff --git a/apps/backend/bun.lock b/apps/backend/bun.lock index 167e10a..9773556 100644 --- a/apps/backend/bun.lock +++ b/apps/backend/bun.lock @@ -21,7 +21,7 @@ "eslint-config-prettier": "^10.1.8", "prettier": "^3.6.2", "ts-node": "^10.9.2", - "typescript": "5.8.3", + "typescript": "^5.9.2", "typescript-eslint": "^8.30.0", }, "peerDependencies": { diff --git a/apps/backend/index.ts b/apps/backend/index.ts new file mode 100644 index 0000000..f67b2c6 --- /dev/null +++ b/apps/backend/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/apps/backend/package.json b/apps/backend/package.json index 8b26c70..d2d9031 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -1,4 +1,5 @@ { + "name": "backend", "module": "index.ts", "dependencies": { @@ -22,7 +23,7 @@ "eslint-config-prettier": "^10.1.8", "prettier": "^3.6.2", "ts-node": "^10.9.2", - "typescript": "5.8.3", + "typescript": "^5.8.3", "typescript-eslint": "^8.30.0" }, "private": true, @@ -30,7 +31,9 @@ "dev": "bun run --watch src/index.ts", "build": "bun build src/index.ts --outdir ./dist", "lint": "bun eslint src/**/*.ts", - "format": "bun prettier --write 'src/**/*.ts'" + "format": "bun prettier --write 'src/**/*.ts'", + "setup": "bun run scripts/setup.ts", + "clean": "bun run scripts/clean.ts" }, "type": "module" } diff --git a/apps/backend/scripts/clean.ts b/apps/backend/scripts/clean.ts new file mode 100644 index 0000000..7e8a815 --- /dev/null +++ b/apps/backend/scripts/clean.ts @@ -0,0 +1,15 @@ +import { fileManager } from '../src/utils/fileManager'; + +async function main() { + const customPath = process.argv[2]; + + try { + await fileManager.cleanupProject(customPath); + console.log(' Cleanup completed successfully'); + } catch (error) { + console.error(' Cleanup failed:', error instanceof Error ? error.message : error); + process.exit(1); + } +} + +await main(); \ No newline at end of file diff --git a/apps/backend/scripts/setup.ts b/apps/backend/scripts/setup.ts new file mode 100644 index 0000000..ed6b364 --- /dev/null +++ b/apps/backend/scripts/setup.ts @@ -0,0 +1,17 @@ +import { fileManager } from '../src/utils/fileManager'; + +async function main() { + try { + const projectPath = await fileManager.setupProject(); + console.log('Project created at:', projectPath); + console.log('\nTo clean up later, run:'); + console.log('bun run clean'); + console.log('or to clean a specific path:'); + console.log('bun run clean -- /path/to/project'); + } catch (error) { + console.error('Setup failed:', error instanceof Error ? error.message : error); + process.exit(1); + } +} + +await main(); \ No newline at end of file diff --git a/apps/backend/src/utils/fileManager.ts b/apps/backend/src/utils/fileManager.ts new file mode 100644 index 0000000..220dce9 --- /dev/null +++ b/apps/backend/src/utils/fileManager.ts @@ -0,0 +1,57 @@ +import { promises as fs } from 'fs'; +import path from 'path'; + +const DEFAULT_CARGO_TOML = `[package] +name = "temp_project" +version = "0.1.0" +edition = "2021" + +[lib] +name = "temp_project" +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = "21.2.0" +`; + +export const fileManager = { + async setupProject(): Promise { + const tempDir = path.join(__dirname, '../../temp', `project_${Date.now()}`); + + const rustCode = `#![no_std] +use soroban_sdk::{contractimpl, Env}; + +pub struct Contract; + +#[contractimpl] +impl Contract { + pub fn hello(env: Env, name: String) -> String { + format!("Hello, {}!", name) + } +}`; + + try { + await fs.mkdir(tempDir, { recursive: true }); + const srcDir = path.join(tempDir, 'src'); + await fs.mkdir(srcDir); + await fs.writeFile(path.join(tempDir, 'Cargo.toml'), DEFAULT_CARGO_TOML); + await fs.writeFile(path.join(srcDir, 'lib.rs'), rustCode); + return tempDir; + } catch (error) { + throw new Error(`Setup failed: ${error instanceof Error ? error.message : String(error)}`); + } + }, + + async cleanupProject(projectPath?: string): Promise { + const tempDir = projectPath || path.join(__dirname, '../../temp'); + + try { + await fs.rm(tempDir, { recursive: true, force: true }); + } catch (error) { + throw new Error(`Cleanup failed: ${error instanceof Error ? error.message : String(error)}`); + } + } +}; + +export type FileManager = typeof fileManager; \ No newline at end of file From 9a3b44a2a92babdc8ed4978f49d9d90388569fcf Mon Sep 17 00:00:00 2001 From: Benjtalkshow Date: Fri, 8 Aug 2025 15:44:20 +0100 Subject: [PATCH 3/4] feat: implement command executor --- apps/backend/package.json | 4 +- apps/backend/scripts/command-executor.ts | 58 ++++++ apps/backend/src/utils/commandExecutor.ts | 206 ++++++---------------- 3 files changed, 118 insertions(+), 150 deletions(-) create mode 100644 apps/backend/scripts/command-executor.ts diff --git a/apps/backend/package.json b/apps/backend/package.json index b7186e0..ec357f1 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -1,5 +1,4 @@ { - "name": "backend", "module": "index.ts", "dependencies": { @@ -33,7 +32,8 @@ "lint": "bun eslint src/**/*.ts", "format": "bun prettier --write 'src/**/*.ts'", "setup": "bun run scripts/setup.ts", - "clean": "bun run scripts/clean.ts" + "clean": "bun run scripts/clean.ts", + "command-executor": "bun run scripts/command-executor.ts" }, "type": "module" } diff --git a/apps/backend/scripts/command-executor.ts b/apps/backend/scripts/command-executor.ts new file mode 100644 index 0000000..d947845 --- /dev/null +++ b/apps/backend/scripts/command-executor.ts @@ -0,0 +1,58 @@ +import { executeCommand } from "../src/utils/commandExecutor"; + +interface CommandError extends Error { + stderr?: string; + stdout?: string; + code?: number; +} + +async function main() { + try { + console.log('Testing command executor...\n'); + + // Test 1: Simple successful command + console.log('Test 1: Running "echo hello world"'); + const result1 = await executeCommand('echo "hello world"'); + console.log('Success:', result1, '\n'); + + // Test 2: Command with error + console.log('Test 2: Running "ls non-existent-file"'); + try { + await executeCommand('ls non-existent-file'); + } catch (error: unknown) { + const err = error as CommandError; + console.log('Error caught as expected:'); + console.log('Message:', err.message); + if (err.stderr) console.log('Stderr:', err.stderr); + console.log(''); + } + + // Test 3: Command with timeout (using macOS compatible command) + console.log('Test 3: Running "ping -c 5 127.0.0.1" with 1000ms timeout'); + try { + await executeCommand('ping -c 5 127.0.0.1', 1000); + } catch (error: unknown) { + const err = error as CommandError; + console.log('Timeout caught as expected:'); + console.log('Message:', err.message); + console.log(''); + } + + // Test 4: Successful command sequence + console.log('Test 4: Running "date && whoami"'); + const result4 = await executeCommand('date && whoami'); + console.log('Success:', result4); + + console.log('\nAll tests completed successfully!'); + + } catch (error: unknown) { + const err = error as CommandError; + console.error('\nUnexpected error in test script:'); + console.error('Message:', err.message); + if (err.stderr) console.error('Stderr:', err.stderr); + if (err.stdout) console.error('Stdout:', err.stdout); + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/apps/backend/src/utils/commandExecutor.ts b/apps/backend/src/utils/commandExecutor.ts index a113487..438f9ce 100644 --- a/apps/backend/src/utils/commandExecutor.ts +++ b/apps/backend/src/utils/commandExecutor.ts @@ -1,164 +1,74 @@ -import { spawn, ChildProcess } from 'child_process'; +import { spawn, type SpawnOptionsWithoutStdio } from 'child_process'; -/** - * Result of a command execution - */ -export interface CommandResult { - /** Exit code of the command */ - exitCode: number; - /** Standard output */ - stdout: string; - /** Standard error output */ - stderr: string; - /** Whether the command was killed due to timeout */ - timedOut: boolean; -} - -/** - * Options for command execution - */ -export interface ExecuteOptions { - /** Working directory for the command */ - cwd?: string; - /** Environment variables */ - env?: Record; - /** Timeout in milliseconds (default: 30000ms = 30s) */ - timeout?: number; -} - -/** - * Error thrown when a command exceeds the timeout limit - */ -export class CommandTimeoutError extends Error { - constructor(timeout: number) { - super(`Command exceeded time limit of ${timeout}ms`); - this.name = 'CommandTimeoutError'; - } -} +const DEFAULT_TIMEOUT = 30000; /** - * Executes a shell command with timeout enforcement - * - * @param command - The command to execute - * @param args - Arguments for the command - * @param options - Execution options including timeout - * @returns Promise that resolves to CommandResult - * @throws CommandTimeoutError if command exceeds timeout + * Executes a shell command securely with a timeout + * @param command The command to execute + * @param timeout Maximum execution time in milliseconds (default: 30000) + * @returns Promise that resolves with the command output + * @throws Error with stderr content if command fails or times out */ export async function executeCommand( command: string, - args: string[] = [], - options: ExecuteOptions = {} -): Promise { - const { cwd, env = process.env, timeout = 30000 } = options; - - return new Promise((resolve, reject) => { - // Buffer to collect stdout and stderr - let stdout = ''; - let stderr = ''; - let timedOut = false; - let childProcess: ChildProcess; + timeout: number = DEFAULT_TIMEOUT +): Promise { + // Validate inputs + if (typeof command !== 'string' || command.trim() === '') { + throw new Error('Command must be a non-empty string'); + } - try { - // Spawn the child process - childProcess = spawn(command, args, { - cwd, - env: { ...process.env, ...env }, - stdio: ['pipe', 'pipe', 'pipe'], - }); + if (typeof timeout !== 'number' || timeout <= 0) { + throw new Error('Timeout must be a positive number'); + } - // Set up timeout - const timeoutId = setTimeout(() => { - timedOut = true; + const options: SpawnOptionsWithoutStdio = { + shell: '/bin/bash', + env: { ...process.env }, + }; - // Kill the process if it's still running - if (childProcess && !childProcess.killed) { - childProcess.kill('SIGTERM'); + return new Promise((resolve, reject) => { + const child = spawn('bash', ['-c', command], options); - // Force kill after 5 seconds if SIGTERM doesn't work - setTimeout(() => { - if (childProcess && !childProcess.killed) { - childProcess.kill('SIGKILL'); - } - }, 5000); - } + let stdout = ''; + let stderr = ''; + let timeoutId: NodeJS.Timeout; - reject(new CommandTimeoutError(timeout)); + // Set up timeout + if (timeout !== Infinity) { + timeoutId = setTimeout(() => { + child.kill('SIGTERM'); + reject(new Error(`Command timed out after ${timeout}ms`)); }, timeout); + } - // Collect stdout data - if (childProcess.stdout) { - childProcess.stdout.on('data', (data: Buffer) => { - stdout += data.toString(); - }); - } - - // Collect stderr data - if (childProcess.stderr) { - childProcess.stderr.on('data', (data: Buffer) => { - stderr += data.toString(); - }); - } - - // Handle process completion - childProcess.on('close', (exitCode: number | null) => { - clearTimeout(timeoutId); - - // Don't resolve if we already timed out - if (timedOut) { - return; - } - - resolve({ - exitCode: exitCode ?? -1, - stdout: stdout.trim(), - stderr: stderr.trim(), - timedOut: false, - }); - }); - - // Handle process errors - childProcess.on('error', (error: Error) => { - clearTimeout(timeoutId); - - // Don't reject if we already timed out - if (timedOut) { - return; - } - + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + child.on('close', (code) => { + clearTimeout(timeoutId); + + if (code === 0) { + resolve(stdout.trim()); + } else { + const error = new Error( + stderr.trim() || `Command failed with exit code ${code}` + ); + (error as any).stderr = stderr.trim(); + (error as any).stdout = stdout.trim(); + (error as any).code = code; reject(error); - }); - - // Handle process being killed - childProcess.on('exit', (code: number | null, signal: string | null) => { - if (signal === 'SIGTERM' || signal === 'SIGKILL') { - clearTimeout(timeoutId); + } + }); - // This was likely our timeout kill, but check the flag to be sure - if (timedOut) { - return; // The timeout handler will reject - } - } - }); - } catch (error) { - reject(error); - } + child.on('error', (err) => { + clearTimeout(timeoutId); + reject(err); + }); }); -} - -/** - * Executes a command with a specific timeout and returns the result - * This is a convenience wrapper around executeCommand - * - * @param command - The command to execute - * @param args - Arguments for the command - * @param timeoutMs - Timeout in milliseconds - * @returns Promise that resolves to CommandResult - */ -export async function executeCommandWithTimeout( - command: string, - args: string[] = [], - timeoutMs: number = 30000 -): Promise { - return executeCommand(command, args, { timeout: timeoutMs }); -} +} \ No newline at end of file From 95145031074f797834f6638223104039c5131f47 Mon Sep 17 00:00:00 2001 From: Benjtalkshow Date: Fri, 8 Aug 2025 15:49:09 +0100 Subject: [PATCH 4/4] feat: implement command executor --- apps/backend/src/utils/commandExecutor.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/utils/commandExecutor.ts b/apps/backend/src/utils/commandExecutor.ts index 438f9ce..95dcdf5 100644 --- a/apps/backend/src/utils/commandExecutor.ts +++ b/apps/backend/src/utils/commandExecutor.ts @@ -56,9 +56,7 @@ export async function executeCommand( if (code === 0) { resolve(stdout.trim()); } else { - const error = new Error( - stderr.trim() || `Command failed with exit code ${code}` - ); + const error = new Error(stderr.trim() || `Command failed with exit code ${code}`); (error as any).stderr = stderr.trim(); (error as any).stdout = stdout.trim(); (error as any).code = code; @@ -71,4 +69,4 @@ export async function executeCommand( reject(err); }); }); -} \ No newline at end of file +}