Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
1e96b2c
Fix Vercel deployment: remove builds property conflict (#16)
robdimarco-atxp Sep 10, 2025
708461c
Fix Vercel functions directory path error
robdimarco-atxp Sep 10, 2025
ac22392
Fix Vercel missing public directory error
robdimarco-atxp Sep 10, 2025
60d9148
Fix ES Module compatibility by upgrading Vercel Node.js runtime
robdimarco-atxp Sep 10, 2025
cf81fbd
Upgrade Vercel runtime to Node.js 20.x
robdimarco-atxp Sep 10, 2025
e13c6a7
Convert ATXP client imports to dynamic imports to fix Vercel ES modul…
robdimarco-atxp Sep 10, 2025
f594958
Fix tests to handle async function changes
robdimarco-atxp Sep 10, 2025
9bf9653
Use Function constructor for dynamic imports to avoid TypeScript requ…
robdimarco-atxp Sep 10, 2025
d22d6b1
Convert backend from CommonJS to ESM
robdimarco-atxp Sep 10, 2025
4237bc4
Fix Vercel frontend build deployment
robdimarco-atxp Sep 10, 2025
4e3ac3e
Copy frontend build to backend directory for Vercel deployment
robdimarco-atxp Sep 10, 2025
ab61df4
Fix CORS policy for production deployments
robdimarco-atxp Sep 10, 2025
c710bbc
Fix SSE endpoint URL for production deployments
robdimarco-atxp Sep 10, 2025
f51151f
Improve production environment detection for SSE endpoint
robdimarco-atxp Sep 10, 2025
a9aec57
Simplify SSE endpoint detection using hostname-based same-origin check
robdimarco-atxp Sep 10, 2025
dbaa460
Force frontend cache bust and debug deployment
robdimarco-atxp Sep 10, 2025
cc10873
Simplify Vercel configuration to fix frontend build handling
robdimarco-atxp Sep 10, 2025
465becd
Fix Vercel runtime specification format
robdimarco-atxp Sep 10, 2025
89ddd20
Upgrade Vercel runtime to Node.js 22.x
robdimarco-atxp Sep 10, 2025
ec70198
Fix Vercel Node.js 22 runtime configuration
robdimarco-atxp Sep 10, 2025
dff650c
Simplify Vercel configuration to use rewrites instead of functions
robdimarco-atxp Sep 10, 2025
7625880
Add outputDirectory configuration to fix Vercel build
robdimarco-atxp Sep 10, 2025
c033788
Fix Vercel rewrite path for serverless function
robdimarco-atxp Sep 10, 2025
3161ee1
Convert to Vercel serverless function architecture
robdimarco-atxp Sep 10, 2025
e1afe5c
Fix Vercel output directory configuration
robdimarco-atxp Sep 10, 2025
e149c2a
Update build process to copy frontend to backend/public
robdimarco-atxp Sep 10, 2025
554ca78
Fix Vercel serverless function ES module compatibility
robdimarco-atxp Sep 10, 2025
793f654
Convert Vercel API entry point to pure ES module
robdimarco-atxp Sep 10, 2025
135bed2
Remove incorrectly checked in files
robdimarco-atxp Sep 10, 2025
ef3f94d
Take out some debug logs
robdimarco-atxp Sep 10, 2025
6d5fc8c
Revert findATXPAccount and validateATXPConnectionString to synchronou…
robdimarco-atxp Sep 10, 2025
593cf97
Add back test assertions for ATXPAccount constructor parameters
robdimarco-atxp Sep 10, 2025
bd39522
Remove some other unneeded changes
robdimarco-atxp Sep 10, 2025
702bb10
console.log cleanup
robdimarco-atxp Sep 10, 2025
9575d14
use NODE_ENV to determine where to connect for progress updates
robdimarco-atxp Sep 10, 2025
16f7df5
Fix frontend SSE connection logic to use NODE_ENV instead of hostname…
robdimarco-atxp Sep 10, 2025
da6496a
Improve server.ts typing by using ATXPAccount instead of any
robdimarco-atxp Sep 10, 2025
1cd7ec2
Ran npm install and picked up engine
robdimarco-atxp Sep 10, 2025
6c4186f
Minor formatting fix
robdimarco-atxp Sep 10, 2025
74e91ae
Remove debug statements
robdimarco-atxp Sep 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Vercel serverless function entry point (ES Module)
// Import the compiled Express app
const { default: app } = await import('../backend/dist/server.js');

// Export the Express app for Vercel
export default app;
9 changes: 6 additions & 3 deletions backend/atxp-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ vi.mock('@atxp/client', () => ({
ATXPAccount: vi.fn().mockImplementation(() => ({ accountId: 'test-account' }))
}));

import { getATXPConnectionString, findATXPAccount, validateATXPConnectionString } from './atxp-utils';
import { getATXPConnectionString, findATXPAccount, validateATXPConnectionString } from './atxp-utils.js';
import { ATXPAccount } from '@atxp/client';

describe('ATXP Utils', () => {
beforeEach(() => {
// Clear environment variables before each test
delete process.env.ATXP_CONNECTION_STRING;
// Reset mocks
vi.clearAllMocks();
});

describe('getATXPConnectionString', () => {
Expand Down Expand Up @@ -121,6 +123,7 @@ describe('ATXP Utils', () => {

const result = findATXPAccount(connectionString);

expect(ATXPAccount).toHaveBeenCalledWith(connectionString, { network: 'base' });
expect(result).toEqual({ accountId: 'test-account' });
});
});
Expand Down Expand Up @@ -182,8 +185,8 @@ describe('ATXP Utils', () => {
}
} as Partial<Request> as Request;

// Mock ATXPAccount to throw an error for this test
(ATXPAccount as any).mockImplementationOnce(() => {
// Mock ATXPAccount to throw an error
vi.mocked(ATXPAccount).mockImplementationOnce(() => {
throw new Error('Invalid connection string format');
});

Expand Down
3 changes: 3 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
"name": "agent-demo-backend",
"version": "1.0.0",
"description": "Express backend for agent-demo",
"type": "module",
"main": "dist/server.js",
"engines": {
"node": "22.x"
},
"scripts": {
"start": "NODE_ENV=production node dist/server.js",
"dev": "nodemon --exec ts-node server.ts",
"dev": "nodemon --exec node --loader ts-node/esm server.ts",
"build": "tsc",
"build:worker": "echo 'Cloudflare Workers will build directly from TypeScript source'",
"test": "vitest run",
Expand Down
4 changes: 2 additions & 2 deletions backend/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ vi.mock('@atxp/common', () => ({
}));

// Mock the stage module
vi.mock('./stage', () => ({
vi.mock('./stage.js', () => ({
sendSSEUpdate: vi.fn(),
addSSEClient: vi.fn(),
removeSSEClient: vi.fn(),
sendStageUpdate: vi.fn(),
}));

// Import after mocking
import { getATXPConnectionString, validateATXPConnectionString } from './atxp-utils';
import { getATXPConnectionString, validateATXPConnectionString } from './atxp-utils.js';

describe('API Endpoints', () => {
let app: express.Application;
Expand Down
128 changes: 96 additions & 32 deletions backend/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ import bodyParser from 'body-parser';
import path from 'path';
import fs from 'fs';
import dotenv from 'dotenv';
import { sendSSEUpdate, addSSEClient, removeSSEClient, sendStageUpdate, sendPaymentUpdate } from './stage';
import { fileURLToPath } from 'url';
import { dirname } from 'path';

// Import the ATXP client SDK
import { atxpClient, ATXPAccount } from '@atxp/client';
import { ConsoleLogger, LogLevel } from '@atxp/common';
// ESM __dirname polyfill
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
import { sendSSEUpdate, addSSEClient, removeSSEClient, sendStageUpdate, sendPaymentUpdate } from './stage.js';

// ATXP client SDK imports (will be dynamically imported due to ES module compatibility)

// Import ATXP utility functions
import { getATXPConnectionString, findATXPAccount, validateATXPConnectionString } from './atxp-utils';
import { getATXPConnectionString, findATXPAccount, validateATXPConnectionString } from './atxp-utils.js';
import type { ATXPAccount } from '@atxp/client';

// Load environment variables
// In production, __dirname points to dist/, but .env is in the parent directory
Expand All @@ -26,12 +31,34 @@ const PORT = process.env.PORT || 3001;
const FRONTEND_PORT = process.env.FRONTEND_PORT || 3000;

// Set up CORS and body parsing middleware
app.use(cors({
origin: [`http://localhost:${FRONTEND_PORT}`, `http://localhost:${PORT}`],
const corsOptions = {
origin: (origin: string | undefined, callback: (error: Error | null, allow?: boolean) => void) => {
// Allow requests with no origin (mobile apps, curl, etc.)
if (!origin) return callback(null, true);

// In development, allow localhost
if (process.env.NODE_ENV !== 'production') {
const allowedOrigins = [`http://localhost:${FRONTEND_PORT}`, `http://localhost:${PORT}`];
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
}

// In production, allow any origin since we're serving both API and frontend from same domain
// This is safe because in production, the frontend is served by the same Express server
if (process.env.NODE_ENV === 'production') {
return callback(null, true);
}

// For development, reject unknown origins
callback(new Error('Not allowed by CORS'), false);
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'Cache-Control', 'x-atxp-connection-string']
}));
};

app.use(cors(corsOptions));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

Expand Down Expand Up @@ -84,8 +111,9 @@ const filestoreService = {

// Handle OPTIONS for SSE endpoint
app.options('/api/progress', (req: Request, res: Response) => {
const origin = req.headers.origin || req.headers.host || `http://localhost:${FRONTEND_PORT}`;
res.writeHead(200, {
'Access-Control-Allow-Origin': `http://localhost:${FRONTEND_PORT}`,
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers': 'Cache-Control, Content-Type, x-atxp-connection-string',
'Access-Control-Allow-Methods': 'GET, OPTIONS'
Expand All @@ -95,11 +123,12 @@ app.options('/api/progress', (req: Request, res: Response) => {

// SSE endpoint for progress updates
app.get('/api/progress', (req: Request, res: Response) => {
const origin = req.headers.origin || req.headers.host || `http://localhost:${FRONTEND_PORT}`;
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': `http://localhost:${FRONTEND_PORT}`,
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers': 'Cache-Control, Content-Type, x-atxp-connection-string',
'Access-Control-Allow-Methods': 'GET, OPTIONS'
Expand Down Expand Up @@ -167,11 +196,12 @@ async function pollForTaskCompletion(
// Send stage update for file storage
sendStageUpdate(requestId, 'storing-file', 'Storing image in ATXP Filestore...', 'in-progress');

// Create filestore client
const filestoreClient = await atxpClient({
// Create filestore client with dynamic import
const { atxpClient: filestoreAtxpClient } = await import('@atxp/client');
const filestoreClient = await filestoreAtxpClient({
mcpServer: filestoreService.mcpServer,
account: account,
onPayment: async ({ payment }) => {
onPayment: async ({ payment }: { payment: any }) => {
console.log('Payment made to filestore:', payment);
sendPaymentUpdate({
accountId: payment.accountId,
Expand Down Expand Up @@ -302,13 +332,17 @@ app.post('/api/texts', async (req: Request, res: Response) => {
// Send stage update for client creation
sendStageUpdate(requestId, 'creating-clients', 'Initializing ATXP clients...', 'in-progress');

// Dynamically import ATXP modules
const { atxpClient } = await import('@atxp/client');
const { ConsoleLogger, LogLevel } = await import('@atxp/common');

// Create a client using the `atxpClient` function for the ATXP Image MCP Server
const imageClient = await atxpClient({
mcpServer: imageService.mcpServer,
account: account,
allowedAuthorizationServers: [`http://localhost:${PORT}`, 'https://auth.atxp.ai', 'https://atxp-accounts-staging.onrender.com/'],
logger: new ConsoleLogger({level: LogLevel.DEBUG}),
onPayment: async ({ payment }) => {
onPayment: async ({ payment }: { payment: any }) => {
console.log('Payment made to image service:', payment);
sendPaymentUpdate({
accountId: payment.accountId,
Expand Down Expand Up @@ -393,23 +427,47 @@ app.get('/api/validate-connection', (req: Request, res: Response) => {

// Helper to resolve static path for frontend build
function getStaticPath() {
// Try ./frontend/build first (works when running from project root in development)
let candidate = path.join(__dirname, './frontend/build');
if (fs.existsSync(candidate)) {
return candidate;
}
// Try ../frontend/build (works when running from backend/ directory)
candidate = path.join(__dirname, '../frontend/build');
if (fs.existsSync(candidate)) {
return candidate;
const candidates = [
// Development: running from project root
path.join(__dirname, './frontend/build'),
// Development: running from backend/ directory
path.join(__dirname, '../frontend/build'),
// Production: running from backend/dist/
path.join(__dirname, '../../frontend/build'),
// Vercel: frontend build copied to backend/public directory
path.join(__dirname, './public'),
path.join(__dirname, '../public'),
// Vercel: alternative paths
'/var/task/backend/public',
// Development fallback
path.join(__dirname, '../build')
];

for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
// Try ../../frontend/build (works when running from backend/dist/ in production)
candidate = path.join(__dirname, '../../frontend/build');
if (fs.existsSync(candidate)) {
return candidate;

// List contents of current directory for debugging
try {
const currentDirContents = fs.readdirSync(__dirname);

// Also check if build directory exists but is empty
const buildPath = path.join(__dirname, './build');
if (fs.existsSync(buildPath)) {
try {
const buildContents = fs.readdirSync(buildPath);
} catch (error) {
console.log('Could not read build directory contents:', error);
}
}
} catch (error) {
console.log('Could not read __dirname contents:', error);
}
// Fallback: throw error
throw new Error('No frontend build directory found. Make sure to run "npm run build" first.');

// Fallback: throw error with more debugging info
throw new Error(`No frontend build directory found. __dirname: ${__dirname}. Checked paths: ${candidates.join(', ')}`);
}

// Serve static files in production
Expand All @@ -428,6 +486,12 @@ if (process.env.NODE_ENV === 'production') {
});
}

app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// For Vercel serverless deployment, export the app
export default app;

// For local development, start the server
if (process.env.NODE_ENV !== 'production' || process.env.VERCEL !== '1') {
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
}
6 changes: 4 additions & 2 deletions backend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"module": "ESNext",
"lib": ["ES2020", "dom"],
"outDir": "./dist",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
"sourceMap": true,
"moduleResolution": "node"
},
"include": [
"server.ts",
Expand Down
8 changes: 8 additions & 0 deletions backend/vitest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
globals: true,
environment: 'node'
},
});
20 changes: 17 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,23 @@ function App(): JSX.Element {
}

console.log('Setting up SSE connection...');
// Use direct backend URL since SSE doesn't work well through CRA proxy
const backendPort = process.env.REACT_APP_BACKEND_PORT || '3001';
const eventSource = new EventSource(`http://localhost:${backendPort}/api/progress`);

// Use NODE_ENV to determine if we're in development mode with separate servers
// In development, we typically run frontend and backend on separate ports
// In production, they're served from the same domain
const isDevelopment = process.env.NODE_ENV === 'development';

let sseUrl: string;
if (isDevelopment) {
// Development: use direct backend URL since CRA proxy doesn't handle SSE well
const backendPort = process.env.REACT_APP_BACKEND_PORT || '3001';
sseUrl = `http://localhost:${backendPort}/api/progress`;
} else {
// Production/deployed: use relative URL (same origin)
sseUrl = '/api/progress';
}

const eventSource = new EventSource(sseUrl);
eventSourceRef.current = eventSource;

console.log('EventSource created, readyState:', eventSource.readyState);
Expand Down
29 changes: 7 additions & 22 deletions vercel.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,12 @@
{
"version": 2,
"builds": [
"installCommand": "npm run install-all",
"buildCommand": "npm run build && cp -r frontend/build backend/public",
"outputDirectory": "backend",
"rewrites": [
{
"src": "backend/server.ts",
"use": "@vercel/node",
"config": {
"includeFiles": ["backend/**", "frontend/build/**"]
}
"source": "/(.*)",
"destination": "/api"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "/backend/server.ts"
}
],
"installCommand": "npm run setup",
"env": {
"NODE_ENV": "production"
},
"functions": {
"backend/server.ts": {
"maxDuration": 30
}
}
]
}