|
|
| import express, { Request, Response, NextFunction } from 'express'; |
| import cors from 'cors'; |
| import { Piece } from '@activepieces/pieces-framework'; |
| import { spawn } from 'child_process'; |
| import fs from 'fs'; |
| import path from 'path'; |
|
|
| const app = express(); |
| const PORT = process.env.PORT || 3003; |
|
|
| |
| |
| |
|
|
| const PIECE_ENGINE_API_KEY = process.env.PIECE_ENGINE_API_KEY || ''; |
| const REQUIRE_AUTH = process.env.REQUIRE_AUTH === 'true' || PIECE_ENGINE_API_KEY.length > 0; |
|
|
| |
| |
| |
| |
| function authenticateRequest(req: Request, res: Response, next: NextFunction): void { |
| |
| if (!REQUIRE_AUTH) { |
| next(); |
| return; |
| } |
|
|
| |
| const apiKey = req.headers['x-api-key'] as string; |
|
|
| |
| const authHeader = req.headers['authorization'] as string; |
| const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null; |
|
|
| const token = apiKey || bearerToken; |
|
|
| if (!token) { |
| res.status(401).json({ |
| success: false, |
| error: 'Authentication required. Provide X-API-Key header or Authorization: Bearer <key>.' |
| }); |
| return; |
| } |
|
|
| if (token !== PIECE_ENGINE_API_KEY) { |
| res.status(403).json({ |
| success: false, |
| error: 'Invalid API key' |
| }); |
| return; |
| } |
|
|
| next(); |
| } |
|
|
| |
| |
| const VALID_PACKAGE_NAME_REGEX = /^(?:@([a-z0-9-~][a-z0-9-._~]*)\/)?([a-z0-9-~][a-z0-9-._~]*)$/; |
|
|
| |
| |
| |
| |
| |
| function isValidPackageName(packageName: string): boolean { |
| if (!packageName || typeof packageName !== 'string') { |
| return false; |
| } |
|
|
| |
| if (packageName.length > 214) { |
| return false; |
| } |
|
|
| |
| return VALID_PACKAGE_NAME_REGEX.test(packageName); |
| } |
|
|
| |
| |
| |
| |
| |
| function safeNpmInstall(packageName: string): Promise<void> { |
| return new Promise((resolve, reject) => { |
| |
| const process = spawn('npm', ['install', packageName, '--save'], { |
| stdio: 'inherit', |
| shell: false |
| }); |
|
|
| process.on('close', (code) => { |
| if (code === 0) { |
| resolve(); |
| } else { |
| reject(new Error(`npm install exited with code ${code}`)); |
| } |
| }); |
|
|
| process.on('error', (err) => { |
| reject(new Error(`Failed to spawn npm process: ${err.message}`)); |
| }); |
| }); |
| } |
|
|
| app.use(express.json()); |
| app.use(cors()); |
|
|
| |
| const pieces: Record<string, any> = {}; |
|
|
| |
| const loadPiece = async (pieceName: string): Promise<Piece | null> => { |
| try { |
| console.log(`Attempting to load piece: ${pieceName}`); |
|
|
| |
| if (!isValidPackageName(pieceName)) { |
| console.error(`Invalid package name: ${pieceName}`); |
| return null; |
| } |
|
|
| |
| let module; |
| try { |
| module = await import(pieceName); |
| } catch (importErr) { |
| console.log(`Module ${pieceName} not found. Attempting dynamic install...`); |
| |
| await safeNpmInstall(pieceName); |
| module = await import(pieceName); |
| } |
|
|
| const piece = module.piece || module.default?.piece || module.default; |
|
|
| if (piece && typeof piece === 'object' && piece.displayName) { |
| pieces[pieceName] = piece; |
| return piece; |
| } |
| console.warn(`Module ${pieceName} loaded but no Piece export found.`); |
| return null; |
| } catch (e: any) { |
| console.error(`Failed to load/install piece ${pieceName}:`, e.message); |
| return null; |
| } |
| }; |
|
|
| |
| const bootstrap = async () => { |
| try { |
| const packageJsonPath = path.join(process.cwd(), 'package.json'); |
| const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); |
| const deps = packageJson.dependencies || {}; |
|
|
| const pieceNames = Object.keys(deps).filter(d => d.startsWith('@activepieces/piece-')); |
|
|
| console.log(`Found ${pieceNames.length} pieces to bootstrap...`); |
|
|
| for (const name of pieceNames) { |
| await loadPiece(name); |
| } |
|
|
| console.log(`Bootstrap complete. ${Object.keys(pieces).length} pieces ready.`); |
| } catch (err) { |
| console.error('Bootstrap failed:', err); |
| } |
| }; |
|
|
| app.get('/health', (req: Request, res: Response) => { |
| res.json({ |
| status: 'ok', |
| pieces_loaded: Object.keys(pieces).length, |
| loaded_names: Object.keys(pieces) |
| }); |
| }); |
|
|
| |
| app.get('/pieces', (req: Request, res: Response) => { |
| const metadata = Object.entries(pieces).map(([name, p]) => { |
| const actions = typeof p.actions === 'function' ? p.actions() : (p as any).actions || {}; |
| const triggers = typeof p.triggers === 'function' ? p.triggers() : (p as any).triggers || {}; |
|
|
| return { |
| name: name, |
| displayName: p.displayName, |
| logoUrl: p.logoUrl, |
| version: (p as any).version || '0.0.0', |
| actions: Object.keys(actions), |
| triggers: Object.keys(triggers), |
| }; |
| }); |
| res.json(metadata); |
| }); |
|
|
| |
| |
| app.get('/pieces/:name', authenticateRequest, async (req: Request, res: Response) => { |
| const name = encodeURIComponent(req.params.name); |
| |
| |
|
|
| let piece = pieces[req.params.name]; |
| if (!piece) { |
| piece = await loadPiece(req.params.name) || null; |
| } |
|
|
| if (!piece) { |
| return res.status(404).json({ error: 'Piece not found' }); |
| } |
|
|
| const actions = typeof piece.actions === 'function' ? piece.actions() : (piece as any).actions || {}; |
| const triggers = typeof piece.triggers === 'function' ? piece.triggers() : (piece as any).triggers || {}; |
|
|
| res.json({ |
| name: req.params.name, |
| displayName: piece.displayName, |
| logoUrl: piece.logoUrl, |
| authors: piece.authors, |
| actions: actions, |
| triggers: triggers, |
| auth: piece.auth |
| }); |
| }); |
|
|
| |
| |
| app.post('/execute/action', authenticateRequest, async (req: Request, res: Response) => { |
| try { |
| const { pieceName, actionName, props, auth } = req.body; |
|
|
| let piece = pieces[pieceName]; |
| if (!piece) { |
| piece = await loadPiece(pieceName) || null; |
| } |
|
|
| if (!piece) { |
| return res.status(404).json({ error: `Piece ${pieceName} not found` }); |
| } |
|
|
| const actions = typeof piece.actions === 'function' ? piece.actions() : (piece as any).actions || {}; |
| const action = actions[actionName]; |
|
|
| if (!action) { |
| return res.status(404).json({ error: `Action ${actionName} not found in piece ${pieceName}` }); |
| } |
|
|
| |
| const context = { |
| propsValue: props || {}, |
| auth: auth || {}, |
| store: { |
| put: async () => { }, |
| get: async () => null, |
| delete: async () => { }, |
| } as any, |
| webhookUrl: '', |
| files: { |
| write: async () => '', |
| } as any, |
| serverUrl: '', |
| project: { id: 'atom-project' } as any, |
| flow: { id: 'atom-flow' } as any |
| }; |
|
|
| const result = await action.run(context); |
| res.json({ success: true, output: result }); |
| } catch (error: any) { |
| console.error('Execution error:', error); |
| res.status(500).json({ success: false, error: error.message }); |
| } |
| }); |
|
|
| |
| |
| app.post('/sys/install', authenticateRequest, async (req: Request, res: Response) => { |
| const { packageName } = req.body; |
|
|
| |
| if (!packageName || !isValidPackageName(packageName)) { |
| return res.status(400).json({ |
| success: false, |
| error: 'Invalid package name. Package names must follow npm naming conventions.' |
| }); |
| } |
|
|
| try { |
| console.log(`Installing ${packageName}...`); |
| |
| await safeNpmInstall(packageName); |
|
|
| const piece = await loadPiece(packageName); |
| if (piece) { |
| res.json({ success: true, message: `Installed and loaded ${packageName}` }); |
| } else { |
| res.json({ success: false, error: 'Package installed but no piece export found' }); |
| } |
| } catch (e: any) { |
| res.status(500).json({ success: false, error: e.message }); |
| } |
| }); |
|
|
| app.listen(PORT, async () => { |
| console.log(`Atom Piece Engine running on port ${PORT}`); |
| await bootstrap(); |
| }); |
|
|
|
|