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:
Ajax Davis 2026-01-09 01:15:47 +10:00
parent 346a3f6da0
commit 300083aff9
11 changed files with 357 additions and 357 deletions

View file

@ -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
[![Deploy with Vercel](https://vercel.com/button)](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)

View 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': '*' },
});
}
}

View file

@ -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',

View file

@ -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',
},
});
}

View file

@ -1,11 +0,0 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

View file

@ -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>
);
}

View file

@ -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,
};
}
}

View file

@ -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;

View file

@ -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"
}
}

View file

@ -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"]
}

View file

@ -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/(.*)",