refactor(executor): switch vercel template from Next.js to Deno runtime
- Replace Next.js app router with pure Deno API functions - Use vercel-deno@3.0.0 community runtime for native HTTP imports - Deno natively supports importing from esm.sh URLs - Simplified template: just api/health.ts and api/execute-tool.ts - Remove unnecessary React/Next.js dependencies - Update README with new architecture documentation This matches the Railway executor's Deno-based approach but runs on the user's own Vercel account. The Deno runtime enables dynamic imports from esm.sh without any special setup.
This commit is contained in:
parent
346a3f6da0
commit
300083aff9
11 changed files with 357 additions and 357 deletions
|
|
@ -1,62 +1,77 @@
|
|||
# TPMJS Executor Template
|
||||
# TPMJS Executor for Vercel
|
||||
|
||||
Deploy your own executor for running TPMJS tools on Vercel.
|
||||
Deploy your own TPMJS tool executor using the **Deno runtime** on Vercel for secure, isolated code execution.
|
||||
|
||||
## Features
|
||||
|
||||
- **Native HTTP Imports**: Deno natively supports importing from esm.sh
|
||||
- **Full Control**: Your infrastructure, your environment variables
|
||||
- **Privacy**: No data passes through TPMJS servers
|
||||
- **One-Click Deploy**: Deploy to Vercel in minutes
|
||||
|
||||
## One-Click Deploy
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/tpmjs/tpmjs/tree/main/templates/vercel-executor&project-name=tpmjs-executor&repository-name=tpmjs-executor)
|
||||
|
||||
## What is an Executor?
|
||||
## How It Works
|
||||
|
||||
An executor is a service that runs TPMJS tools. By default, TPMJS uses a shared executor, but you can deploy your own for:
|
||||
This executor uses the [Vercel Deno Runtime](https://github.com/vercel-community/deno) to:
|
||||
|
||||
- **Full control**: Run tools on your own infrastructure
|
||||
- **Custom environment**: Inject your own environment variables and secrets
|
||||
- **Privacy**: Keep tool execution data on your own servers
|
||||
- **Performance**: Deploy in regions closest to your users
|
||||
1. Receive tool execution requests via POST `/api/execute-tool`
|
||||
2. Dynamically import the npm package from esm.sh (Deno natively supports HTTP imports!)
|
||||
3. Execute the tool with your parameters
|
||||
4. Return the result
|
||||
|
||||
This provides the same execution model as the Railway executor, but deployed to your own Vercel account.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### POST /api/execute-tool
|
||||
|
||||
Execute a TPMJS tool.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"packageName": "@tpmjs/hello",
|
||||
"name": "helloWorld",
|
||||
"version": "latest",
|
||||
"params": { "name": "World" },
|
||||
"env": { "MY_SECRET": "value" }
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"output": "Hello, World!",
|
||||
"executionTimeMs": 123
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/health
|
||||
|
||||
Check executor health status.
|
||||
|
||||
```bash
|
||||
curl https://your-executor.vercel.app/api/health
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "1.0.0",
|
||||
"info": {
|
||||
"runtime": "vercel-serverless",
|
||||
"runtime": "deno",
|
||||
"httpImports": true,
|
||||
"timestamp": "2024-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/execute-tool
|
||||
|
||||
Execute a TPMJS tool.
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-executor.vercel.app/api/execute-tool \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer your-api-key" \
|
||||
-d '{
|
||||
"packageName": "@anthropic-ai/tpmjs-hello",
|
||||
"name": "helloWorld",
|
||||
"version": "latest",
|
||||
"params": { "name": "World" }
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"output": "Hello, World!",
|
||||
"executionTimeMs": 234
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
|
@ -65,46 +80,64 @@ Check executor health status.
|
|||
|----------|----------|-------------|
|
||||
| `EXECUTOR_API_KEY` | No | API key for authentication. If set, requests must include `Authorization: Bearer <key>` header. |
|
||||
|
||||
Add custom environment variables for your tools (e.g., `MY_API_KEY`, `DATABASE_URL`) in your Vercel project settings.
|
||||
|
||||
### Setting Up API Key Authentication
|
||||
|
||||
1. Go to your Vercel project settings
|
||||
2. Add an environment variable: `EXECUTOR_API_KEY` with a secure random value
|
||||
3. When configuring your executor in TPMJS, enter this key in the "API Key" field
|
||||
|
||||
## How It Works
|
||||
## Connecting to TPMJS
|
||||
|
||||
1. TPMJS sends a request to your executor with package name, tool name, and parameters
|
||||
2. The executor dynamically imports the package from [esm.sh](https://esm.sh)
|
||||
3. The tool's `execute()` function is called with the provided parameters
|
||||
4. The result is returned to TPMJS
|
||||
1. Go to your TPMJS collection or agent settings
|
||||
2. In "Executor Configuration", select "Custom Executor"
|
||||
3. Enter your executor URL: `https://your-executor.vercel.app`
|
||||
4. Enter your API key (if configured)
|
||||
5. Click "Verify Connection" to test
|
||||
|
||||
## Local Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
# Install Vercel CLI
|
||||
npm install -g vercel
|
||||
|
||||
# Login to Vercel
|
||||
vercel login
|
||||
|
||||
# Run development server
|
||||
npm run dev
|
||||
vercel dev
|
||||
|
||||
# Test the health endpoint
|
||||
curl http://localhost:3000/api/health
|
||||
|
||||
# Test tool execution
|
||||
curl -X POST http://localhost:3000/api/execute-tool \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"packageName":"@anthropic-ai/tpmjs-hello","name":"helloWorld","params":{"name":"Test"}}'
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
**Note:** Local development requires the Vercel CLI. Run `vercel login` first.
|
||||
|
||||
- Always set `EXECUTOR_API_KEY` in production to prevent unauthorized access
|
||||
- The executor runs tools in a serverless environment with limited capabilities
|
||||
- Environment variables injected via `env` are available only during execution
|
||||
- Tools are imported from esm.sh, a trusted CDN for npm packages
|
||||
## Security
|
||||
|
||||
- Set `EXECUTOR_API_KEY` to require authentication for all requests
|
||||
- Tools are loaded dynamically from esm.sh
|
||||
- Each request gets fresh environment variable injection
|
||||
- CORS headers allow cross-origin requests (configurable)
|
||||
|
||||
## How It Compares to TPMJS Default Executor
|
||||
|
||||
| Feature | TPMJS Default (Railway) | Your Vercel Executor |
|
||||
|---------|------------------------|---------------------|
|
||||
| Runtime | Deno on Railway | Deno on Vercel |
|
||||
| Cost | Free (TPMJS hosted) | Your Vercel usage |
|
||||
| Env Vars | Stored in TPMJS | Stored in your Vercel project |
|
||||
| Privacy | Requests go through TPMJS | Direct to your executor |
|
||||
| Control | Managed by TPMJS | Fully yours |
|
||||
|
||||
## Pricing
|
||||
|
||||
Vercel's free tier includes generous limits for serverless functions. See [Vercel Pricing](https://vercel.com/pricing) for details.
|
||||
|
||||
## Support
|
||||
|
||||
- [TPMJS Documentation](https://tpmjs.com/docs)
|
||||
- [Executor Documentation](https://tpmjs.com/docs/executors)
|
||||
- [TPMJS Custom Executors Documentation](https://tpmjs.com/docs/executors)
|
||||
- [TPMJS Custom Executor Tutorial](https://tpmjs.com/docs/tutorials/custom-executor)
|
||||
- [Vercel Deno Runtime](https://github.com/vercel-community/deno)
|
||||
- [GitHub Issues](https://github.com/tpmjs/tpmjs/issues)
|
||||
|
|
|
|||
256
templates/vercel-executor/api/execute-tool.ts
Normal file
256
templates/vercel-executor/api/execute-tool.ts
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
/**
|
||||
* Tool Execution Endpoint (Deno Runtime)
|
||||
*
|
||||
* POST /api/execute-tool
|
||||
* Execute a TPMJS tool with parameters
|
||||
*
|
||||
* Uses Deno's native HTTP import support to load tools from esm.sh
|
||||
*/
|
||||
|
||||
// @ts-ignore - Deno global
|
||||
declare const Deno: {
|
||||
env: {
|
||||
get(key: string): string | undefined;
|
||||
set(key: string, value: string): void;
|
||||
};
|
||||
};
|
||||
|
||||
interface ExecuteToolRequest {
|
||||
packageName: string;
|
||||
name: string;
|
||||
version?: string;
|
||||
importUrl?: string;
|
||||
params: Record<string, unknown>;
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface ExecuteToolResponse {
|
||||
success: boolean;
|
||||
output?: unknown;
|
||||
error?: string;
|
||||
executionTimeMs: number;
|
||||
}
|
||||
|
||||
// Simple in-memory cache for tool modules
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Tool types are dynamic
|
||||
const moduleCache = new Map<string, { module: any; expiresAt: number }>();
|
||||
const CACHE_TTL_MS = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
export default async function handler(req: Request): Promise<Response> {
|
||||
// Handle CORS preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Check authorization if EXECUTOR_API_KEY is set
|
||||
const apiKey = Deno.env.get('EXECUTOR_API_KEY');
|
||||
if (apiKey) {
|
||||
const authHeader = req.headers.get('Authorization');
|
||||
if (!authHeader || authHeader !== `Bearer ${apiKey}`) {
|
||||
return Response.json(
|
||||
{ success: false, error: 'Unauthorized' },
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
let packageName = 'unknown';
|
||||
let toolName = 'unknown';
|
||||
|
||||
try {
|
||||
const body: ExecuteToolRequest = await req.json();
|
||||
packageName = body.packageName || 'unknown';
|
||||
toolName = body.name || 'unknown';
|
||||
|
||||
const { version, importUrl, params, env } = body;
|
||||
|
||||
if (!body.packageName || !body.name) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Missing required fields: packageName, name',
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedVersion = version || 'latest';
|
||||
const cacheKey = `${packageName}@${resolvedVersion}::${toolName}`;
|
||||
|
||||
// Inject environment variables before loading/executing tool
|
||||
if (env && typeof env === 'object') {
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
Deno.env.set(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Tool types are dynamic
|
||||
let toolModule: any;
|
||||
const cachedEntry = moduleCache.get(cacheKey);
|
||||
const now = Date.now();
|
||||
|
||||
if (cachedEntry && now < cachedEntry.expiresAt) {
|
||||
toolModule = cachedEntry.module;
|
||||
} else {
|
||||
// Dynamic import from esm.sh - Deno supports this natively!
|
||||
const url = importUrl || `https://esm.sh/${packageName}@${resolvedVersion}`;
|
||||
const module = await import(url);
|
||||
|
||||
let rawExport = module[toolName];
|
||||
|
||||
if (!rawExport) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Export "${toolName}" not found in module`,
|
||||
availableExports: Object.keys(module),
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
},
|
||||
{
|
||||
status: 404,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Handle factory functions (tools that need to be called to create the tool instance)
|
||||
if (typeof rawExport === 'function' && !rawExport.execute) {
|
||||
let factoryResult = null;
|
||||
|
||||
// Strategy 1: Try calling with no arguments
|
||||
try {
|
||||
factoryResult = rawExport();
|
||||
if (factoryResult?.execute) {
|
||||
rawExport = factoryResult;
|
||||
}
|
||||
} catch {
|
||||
// Try other strategies
|
||||
}
|
||||
|
||||
// Strategy 2: Try with env config object
|
||||
if (!factoryResult?.execute && env) {
|
||||
const configVariations = [
|
||||
{ ...env },
|
||||
// Extract API key if present
|
||||
(() => {
|
||||
const apiKeyEntry = Object.entries(env).find(([key]) =>
|
||||
key.toUpperCase().includes('API_KEY')
|
||||
);
|
||||
return apiKeyEntry ? { apiKey: apiKeyEntry[1] } : null;
|
||||
})(),
|
||||
].filter(Boolean);
|
||||
|
||||
for (const config of configVariations) {
|
||||
try {
|
||||
factoryResult = rawExport(config);
|
||||
if (factoryResult?.execute) {
|
||||
rawExport = factoryResult;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Try next config
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Try with first env value (single-arg pattern)
|
||||
if (!factoryResult?.execute && env) {
|
||||
const firstValue = Object.values(env)[0];
|
||||
if (firstValue) {
|
||||
try {
|
||||
factoryResult = rawExport(firstValue);
|
||||
if (factoryResult?.execute) {
|
||||
rawExport = factoryResult;
|
||||
}
|
||||
} catch {
|
||||
// Factory failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!rawExport?.execute) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Tool "${toolName}" is a factory function but couldn't be initialized`,
|
||||
hint: 'This tool may require specific configuration. Check package documentation.',
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
toolModule = rawExport;
|
||||
|
||||
if (!toolModule.execute) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Tool missing execute function',
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Cache the module
|
||||
moduleCache.set(cacheKey, {
|
||||
module: toolModule,
|
||||
expiresAt: now + CACHE_TTL_MS,
|
||||
});
|
||||
}
|
||||
|
||||
// Execute the tool with AI SDK execution context
|
||||
const abortController = new AbortController();
|
||||
const executionContext = {
|
||||
abortSignal: abortController.signal,
|
||||
messages: [],
|
||||
toolCallId: `exec_${Date.now()}`,
|
||||
};
|
||||
|
||||
const result = await toolModule.execute(params || {}, executionContext);
|
||||
|
||||
const response: ExecuteToolResponse = {
|
||||
success: true,
|
||||
output: result,
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
};
|
||||
|
||||
return Response.json(response, {
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
});
|
||||
} catch (error) {
|
||||
const response: ExecuteToolResponse = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
};
|
||||
|
||||
return Response.json(response, {
|
||||
status: 500,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +1,28 @@
|
|||
/**
|
||||
* Health Check Endpoint
|
||||
* Health Check Endpoint (Deno Runtime)
|
||||
*
|
||||
* GET /api/health
|
||||
* Returns the health status of the executor
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
interface HealthResponse {
|
||||
status: 'ok' | 'degraded' | 'error';
|
||||
version?: string;
|
||||
info?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export async function GET(): Promise<NextResponse<HealthResponse>> {
|
||||
return NextResponse.json({
|
||||
export default function handler(_req: Request): Response {
|
||||
const response: HealthResponse = {
|
||||
status: 'ok',
|
||||
version: '1.0.0',
|
||||
info: {
|
||||
runtime: 'vercel-serverless',
|
||||
runtime: 'deno',
|
||||
httpImports: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handle OPTIONS for CORS preflight
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return new NextResponse(null, {
|
||||
status: 200,
|
||||
return Response.json(response, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
/**
|
||||
* Execute Tool Endpoint
|
||||
*
|
||||
* POST /api/execute-tool
|
||||
* Executes a TPMJS tool with the provided parameters
|
||||
*/
|
||||
|
||||
import { type ExecuteToolRequest, executeTool } from '@/lib/executor';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const maxDuration = 60; // 60 seconds max
|
||||
|
||||
/**
|
||||
* Verify API key if configured
|
||||
*/
|
||||
function verifyApiKey(request: NextRequest): boolean {
|
||||
const apiKey = process.env.EXECUTOR_API_KEY;
|
||||
|
||||
// If no API key is configured, allow all requests
|
||||
if (!apiKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Expect "Bearer <api-key>" format
|
||||
const [type, token] = authHeader.split(' ');
|
||||
if (type !== 'Bearer' || token !== apiKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the request body
|
||||
*/
|
||||
function validateRequest(body: unknown): body is ExecuteToolRequest {
|
||||
if (!body || typeof body !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const req = body as Record<string, unknown>;
|
||||
|
||||
return (
|
||||
typeof req.packageName === 'string' &&
|
||||
req.packageName.length > 0 &&
|
||||
typeof req.name === 'string' &&
|
||||
req.name.length > 0 &&
|
||||
typeof req.params === 'object' &&
|
||||
req.params !== null
|
||||
);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest): Promise<NextResponse> {
|
||||
// Verify API key if configured
|
||||
if (!verifyApiKey(request)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized', executionTimeMs: 0 },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Validate request
|
||||
if (!validateRequest(body)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Invalid request. Required: packageName, name, params',
|
||||
executionTimeMs: 0,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Execute the tool
|
||||
const result = await executeTool(body);
|
||||
|
||||
return NextResponse.json(result, {
|
||||
status: result.success ? 200 : 500,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
executionTimeMs: 0,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle OPTIONS for CORS preflight
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return new NextResponse(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
export default function Home() {
|
||||
return (
|
||||
<div style={{ padding: '2rem', fontFamily: 'system-ui, sans-serif' }}>
|
||||
<h1>TPMJS Executor</h1>
|
||||
<p>This is a TPMJS tool executor service.</p>
|
||||
<h2>Endpoints</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<code>GET /api/health</code> - Health check
|
||||
</li>
|
||||
<li>
|
||||
<code>POST /api/execute-tool</code> - Execute a tool
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<a href="https://tpmjs.com/docs/executors">Documentation</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
/**
|
||||
* TPMJS Executor - Core Execution Logic
|
||||
*
|
||||
* This module handles dynamic import and execution of TPMJS tools.
|
||||
* Tools are loaded from esm.sh and executed in a serverless environment.
|
||||
*/
|
||||
|
||||
export interface ExecuteToolRequest {
|
||||
/** NPM package name, e.g., "@tpmjs/hello" */
|
||||
packageName: string;
|
||||
/** Tool name within the package, e.g., "helloWorld" */
|
||||
name: string;
|
||||
/** Package version, e.g., "1.0.0" or "latest" */
|
||||
version?: string;
|
||||
/** Direct esm.sh URL override for the package */
|
||||
importUrl?: string;
|
||||
/** Tool parameters to pass to execute() */
|
||||
params: Record<string, unknown>;
|
||||
/** Environment variables to inject during execution */
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ExecuteToolResponse {
|
||||
/** Whether the execution succeeded */
|
||||
success: boolean;
|
||||
/** Tool output on success */
|
||||
output?: unknown;
|
||||
/** Error message on failure */
|
||||
error?: string;
|
||||
/** Execution duration in milliseconds */
|
||||
executionTimeMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the esm.sh URL for a package
|
||||
*/
|
||||
function buildEsmUrl(packageName: string, version?: string): string {
|
||||
const resolvedVersion = version || 'latest';
|
||||
return `https://esm.sh/${packageName}@${resolvedVersion}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a TPMJS tool
|
||||
*
|
||||
* @param request - Execution request with package name, tool name, and parameters
|
||||
* @returns Execution result
|
||||
*/
|
||||
export async function executeTool(request: ExecuteToolRequest): Promise<ExecuteToolResponse> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Build the import URL
|
||||
const importUrl = request.importUrl || buildEsmUrl(request.packageName, request.version);
|
||||
|
||||
// Dynamically import the package from esm.sh
|
||||
const module = await import(/* webpackIgnore: true */ importUrl);
|
||||
|
||||
// Get the tool export
|
||||
// Try the specific tool name first, then fall back to default export
|
||||
const tool = module[request.name] || module.default;
|
||||
|
||||
if (!tool) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Tool '${request.name}' not found in package '${request.packageName}'`,
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure the tool has an execute function
|
||||
if (typeof tool.execute !== 'function') {
|
||||
return {
|
||||
success: false,
|
||||
error: `Tool '${request.name}' does not have an execute() function`,
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
|
||||
// Inject environment variables if provided
|
||||
if (request.env) {
|
||||
for (const [key, value] of Object.entries(request.env)) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the tool
|
||||
const output = await tool.execute(request.params);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Enable experimental features for better serverless performance
|
||||
experimental: {
|
||||
// Allow dynamic imports from esm.sh
|
||||
serverActions: true,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
|
@ -2,20 +2,11 @@
|
|||
"name": "tpmjs-executor",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "TPMJS Tool Executor - Deploy your own executor for running TPMJS tools",
|
||||
"description": "TPMJS Tool Executor - Deploy your own executor using Deno on Vercel",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^15.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"dev": "vercel dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
"vercel": "^39.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"framework": "nextjs",
|
||||
"functions": {
|
||||
"api/**/*.ts": {
|
||||
"runtime": "vercel-deno@3.0.0"
|
||||
}
|
||||
},
|
||||
"headers": [
|
||||
{
|
||||
"source": "/api/(.*)",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue