Transform qualifying scenarios into marketing-ready use cases with: - AI-generated titles, descriptions, ROI estimates, business value - Persona/industry/category taxonomy for targeting - Browseable feed with filtering and ranking - SEO-optimized case study pages - Daily cron job for generation and ranking Database: - Add Persona, Industry, Category lookup tables - Add UseCase model with marketing content fields - Add junction tables for personas/industries/categories - Add SocialProof model for cached metrics API: - GET /api/use-cases - Global directory with filtering - GET /api/use-cases/[id] - Individual use case details - GET /api/public/users/[username]/collections/[slug]/use-cases - POST /api/cron/use-cases - Nightly generation job Frontend: - /use-cases - Global feed with persona dropdown - /use-cases/[slug] - SEO case study page - /[username]/collections/[slug]/use-cases - Collection feed - UseCasesFeed component - Sortable table component - UseCaseCaseStudy component - Full case study layout
225 lines
6.1 KiB
JavaScript
225 lines
6.1 KiB
JavaScript
/**
|
|
* TPMJS Sandbox Executor Service
|
|
* Securely executes npm packages using isolated-vm
|
|
*/
|
|
|
|
import { execSync } from 'node:child_process';
|
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import cors from 'cors';
|
|
import express from 'express';
|
|
import ivm from 'isolated-vm';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3000;
|
|
const CACHE_DIR = process.env.PACKAGE_CACHE_DIR || '/tmp/.tpmjs-cache';
|
|
|
|
// Security settings
|
|
const MAX_MEMORY_MB = 128;
|
|
const MAX_TIMEOUT_MS = 10000;
|
|
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(',') || ['*'];
|
|
|
|
app.use(
|
|
cors({
|
|
origin: ALLOWED_ORIGINS.includes('*') ? true : ALLOWED_ORIGINS,
|
|
})
|
|
);
|
|
app.use(express.json({ limit: '1mb' }));
|
|
|
|
/**
|
|
* Health check endpoint
|
|
*/
|
|
app.get('/health', (_req, res) => {
|
|
res.json({
|
|
status: 'healthy',
|
|
service: 'tpmjs-sandbox-executor',
|
|
version: '1.0.0',
|
|
memoryLimit: `${MAX_MEMORY_MB}MB`,
|
|
timeout: `${MAX_TIMEOUT_MS}ms`,
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Execute package endpoint
|
|
*/
|
|
app.post('/execute', async (req, res) => {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
const { packageName, functionName = 'default', params = {} } = req.body;
|
|
|
|
if (!packageName) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'packageName is required',
|
|
});
|
|
}
|
|
|
|
// Ensure package is installed
|
|
const packageDir = await ensurePackageInstalled(packageName);
|
|
|
|
// Create isolated VM
|
|
const isolate = new ivm.Isolate({ memoryLimit: MAX_MEMORY_MB });
|
|
const context = await isolate.createContext();
|
|
|
|
// Set up sandbox environment
|
|
const jail = context.global;
|
|
await jail.set('global', jail.derefInto());
|
|
|
|
// Create console mock that captures logs
|
|
const _logs = [];
|
|
const consoleObject = await isolate.compileScript(`
|
|
({
|
|
log: (...args) => { logs.push(args.join(' ')); },
|
|
error: (...args) => { logs.push('[ERROR] ' + args.join(' ')); },
|
|
warn: (...args) => { logs.push('[WARN] ' + args.join(' ')); }
|
|
})
|
|
`);
|
|
await jail.set('console', await consoleObject.run(context));
|
|
await jail.set('logs', []);
|
|
|
|
// Build execution code
|
|
const _code = `
|
|
(async function() {
|
|
try {
|
|
// Dynamic import is not available in isolated-vm
|
|
// We need to use a different approach - we'll pass the package code as a string
|
|
const params = ${JSON.stringify(params)};
|
|
const result = await executePackage(params);
|
|
return { success: true, output: result, logs };
|
|
} catch (error) {
|
|
return { success: false, error: error.message, logs };
|
|
}
|
|
})();
|
|
`;
|
|
|
|
// For now, let's use a simpler approach with require (via context bridge)
|
|
// This requires building a bridge between Node and the isolate
|
|
|
|
// Simplified execution: run package in main Node context but with timeout
|
|
const result = await executeWithTimeout(async () => {
|
|
const packagePath = join(packageDir, 'node_modules', packageName);
|
|
|
|
// Dynamic import for ESM packages
|
|
let pkg;
|
|
try {
|
|
pkg = await import(packagePath);
|
|
} catch {
|
|
// Fallback to require for CommonJS
|
|
pkg = await import('node:module').then((m) =>
|
|
m.createRequire(import.meta.url)(packagePath)
|
|
);
|
|
}
|
|
|
|
const fn = typeof pkg === 'function' ? pkg : pkg[functionName] || pkg.default;
|
|
|
|
if (typeof fn !== 'function') {
|
|
throw new Error(`Package does not export a function named '${functionName}'`);
|
|
}
|
|
|
|
return await fn(params);
|
|
}, MAX_TIMEOUT_MS);
|
|
|
|
const executionTimeMs = Date.now() - startTime;
|
|
|
|
res.json({
|
|
success: true,
|
|
output: result,
|
|
executionTimeMs,
|
|
logs: [],
|
|
});
|
|
} catch (error) {
|
|
const executionTimeMs = Date.now() - startTime;
|
|
console.error('Execution error:', error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
executionTimeMs,
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Clear package cache endpoint
|
|
*/
|
|
app.post('/cache/clear', async (_req, res) => {
|
|
try {
|
|
if (existsSync(CACHE_DIR)) {
|
|
execSync(`rm -rf ${CACHE_DIR}`, { stdio: 'ignore' });
|
|
}
|
|
res.json({ success: true, message: 'Cache cleared' });
|
|
} catch (error) {
|
|
res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Execute function with timeout
|
|
*/
|
|
function executeWithTimeout(fn, timeout) {
|
|
return Promise.race([
|
|
fn(),
|
|
new Promise((_, reject) => setTimeout(() => reject(new Error('Execution timeout')), timeout)),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Ensure package is installed in cache directory
|
|
*/
|
|
async function ensurePackageInstalled(packageName) {
|
|
// Create cache directory if needed
|
|
if (!existsSync(CACHE_DIR)) {
|
|
mkdirSync(CACHE_DIR, { recursive: true });
|
|
}
|
|
|
|
// Package-specific directory
|
|
const packageDir = join(CACHE_DIR, packageName.replace(/[@/]/g, '_'));
|
|
|
|
// Check if already installed
|
|
if (existsSync(join(packageDir, 'node_modules', packageName))) {
|
|
return packageDir;
|
|
}
|
|
|
|
// Install the package
|
|
try {
|
|
if (!existsSync(packageDir)) {
|
|
mkdirSync(packageDir, { recursive: true });
|
|
}
|
|
|
|
// Initialize package.json if not exists
|
|
const packageJsonPath = join(packageDir, 'package.json');
|
|
if (!existsSync(packageJsonPath)) {
|
|
execSync('npm init -y', {
|
|
cwd: packageDir,
|
|
stdio: 'ignore',
|
|
});
|
|
}
|
|
|
|
// Install the package
|
|
console.log(`Installing ${packageName}...`);
|
|
execSync(`npm install ${packageName} --no-save --legacy-peer-deps`, {
|
|
cwd: packageDir,
|
|
stdio: 'inherit',
|
|
timeout: 60000,
|
|
});
|
|
|
|
return packageDir;
|
|
} catch (error) {
|
|
throw new Error(`Failed to install package ${packageName}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start server
|
|
*/
|
|
app.listen(PORT, () => {
|
|
console.log(`🔒 TPMJS Sandbox Executor running on port ${PORT}`);
|
|
console.log(`📦 Package cache: ${CACHE_DIR}`);
|
|
console.log(`🧠 Memory limit: ${MAX_MEMORY_MB}MB`);
|
|
console.log(`⏱️ Timeout: ${MAX_TIMEOUT_MS}ms`);
|
|
});
|