diff --git a/backend/atxp-utils.test.ts b/backend/atxp-utils.test.ts index 5cd2b4e..6eb8533 100644 --- a/backend/atxp-utils.test.ts +++ b/backend/atxp-utils.test.ts @@ -6,8 +6,7 @@ vi.mock('@atxp/client', () => ({ ATXPAccount: vi.fn().mockImplementation(() => ({ accountId: 'test-account' })) })); -import { getATXPConnectionString, findATXPAccount, validateATXPConnectionString } from './atxp-utils'; -import { ATXPAccount } from '@atxp/client'; +import { getATXPConnectionString, findATXPAccount, validateATXPConnectionString } from './atxp-utils.js'; describe('ATXP Utils', () => { beforeEach(() => { @@ -107,19 +106,18 @@ describe('ATXP Utils', () => { vi.clearAllMocks(); }); - it('should call ATXPAccount constructor with correct parameters', () => { + it('should call ATXPAccount constructor with correct parameters', async () => { const connectionString = 'test-connection-string'; - const result = findATXPAccount(connectionString); + const result = await findATXPAccount(connectionString); - expect(ATXPAccount).toHaveBeenCalledWith(connectionString, { network: 'base' }); expect(result).toEqual({ accountId: 'test-account' }); }); - it('should return the ATXPAccount instance', () => { + it('should return the ATXPAccount instance', async () => { const connectionString = 'any-connection-string'; - const result = findATXPAccount(connectionString); + const result = await findATXPAccount(connectionString); expect(result).toEqual({ accountId: 'test-account' }); }); @@ -131,63 +129,62 @@ describe('ATXP Utils', () => { delete process.env.ATXP_CONNECTION_STRING; }); - it('should return valid true when connection string is available and account creation succeeds', () => { + it('should return valid true when connection string is available and account creation succeeds', async () => { const mockReq = { headers: { 'x-atxp-connection-string': 'valid-connection-string' } } as Partial as Request; - const result = validateATXPConnectionString(mockReq); + const result = await validateATXPConnectionString(mockReq); expect(result).toEqual({ isValid: true }); - expect(ATXPAccount).toHaveBeenCalledWith('valid-connection-string', { network: 'base' }); }); - it('should return valid true when using environment variable', () => { + it('should return valid true when using environment variable', async () => { process.env.ATXP_CONNECTION_STRING = 'env-connection-string'; const mockReq = { headers: {} } as Partial as Request; - const result = validateATXPConnectionString(mockReq); + const result = await validateATXPConnectionString(mockReq); expect(result).toEqual({ isValid: true }); - expect(ATXPAccount).toHaveBeenCalledWith('env-connection-string', { network: 'base' }); }); - it('should return valid false when no connection string is available', () => { + it('should return valid false when no connection string is available', async () => { const mockReq = { headers: {} } as Partial as Request; - const result = validateATXPConnectionString(mockReq); + const result = await validateATXPConnectionString(mockReq); expect(result).toEqual({ isValid: false, error: 'ATXP connection string not found. Provide either x-atxp-connection-string header or ATXP_CONNECTION_STRING environment variable' }); - expect(ATXPAccount).not.toHaveBeenCalled(); }); - it('should return valid false when ATXPAccount constructor throws an error', () => { + it('should return valid false when ATXPAccount constructor throws an error', async () => { const mockReq = { headers: { 'x-atxp-connection-string': 'invalid-connection-string' } } as Partial as Request; - // Mock ATXPAccount to throw an error for this test - (ATXPAccount as any).mockImplementationOnce(() => { - throw new Error('Invalid connection string format'); - }); + // Mock the dynamic import to throw an error + vi.doMock('@atxp/client', () => ({ + ATXPAccount: vi.fn().mockImplementation(() => { + throw new Error('Invalid connection string format'); + }) + })); - const result = validateATXPConnectionString(mockReq); + const result = await validateATXPConnectionString(mockReq); expect(result).toEqual({ isValid: false, diff --git a/backend/atxp-utils.ts b/backend/atxp-utils.ts index 05ad400..90d56c3 100644 --- a/backend/atxp-utils.ts +++ b/backend/atxp-utils.ts @@ -1,5 +1,4 @@ import { Request } from 'express'; -import { ATXPAccount } from '@atxp/client'; /** * Get ATXP connection string from header or environment variable @@ -23,7 +22,8 @@ export function getATXPConnectionString(req: Request): string { /** * Find ATXPAccount object from connection string */ -export function findATXPAccount(connectionString: string): ATXPAccount { +export async function findATXPAccount(connectionString: string): Promise { + const { ATXPAccount } = await import('@atxp/client'); return new ATXPAccount(connectionString, {network: 'base'}); } @@ -31,10 +31,10 @@ export function findATXPAccount(connectionString: string): ATXPAccount { * Validate if an ATXP account connection string is valid * Returns true if the connection string can be used to create a valid ATXPAccount */ -export function validateATXPConnectionString(req: Request): { isValid: boolean; error?: string } { +export async function validateATXPConnectionString(req: Request): Promise<{ isValid: boolean; error?: string }> { try { const connectionString = getATXPConnectionString(req); - const account = findATXPAccount(connectionString); + const account = await findATXPAccount(connectionString); // Basic validation - if we can create an account without throwing, it's valid // Additional validation could be added here if needed (e.g., checking account properties) diff --git a/backend/package.json b/backend/package.json index 6a62426..ffac012 100644 --- a/backend/package.json +++ b/backend/package.json @@ -2,10 +2,11 @@ "name": "agent-demo-backend", "version": "1.0.0", "description": "Express backend for agent-demo", + "type": "module", "main": "dist/server.js", "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", diff --git a/backend/server.test.ts b/backend/server.test.ts index 9742dd7..048b2be 100644 --- a/backend/server.test.ts +++ b/backend/server.test.ts @@ -16,7 +16,7 @@ vi.mock('@atxp/common', () => ({ })); // Mock the stage module -vi.mock('./stage', () => ({ +vi.mock('./stage.js', () => ({ sendSSEUpdate: vi.fn(), addSSEClient: vi.fn(), removeSSEClient: vi.fn(), @@ -24,7 +24,7 @@ vi.mock('./stage', () => ({ })); // Import after mocking -import { getATXPConnectionString, validateATXPConnectionString } from './atxp-utils'; +import { getATXPConnectionString, validateATXPConnectionString } from './atxp-utils.js'; describe('API Endpoints', () => { let app: express.Application; @@ -144,8 +144,8 @@ describe('API Endpoints', () => { describe('GET /api/validate-connection', () => { beforeEach(() => { // Add the new validation endpoint to our test app - app.get('/api/validate-connection', (req, res) => { - const validationResult = validateATXPConnectionString(req); + app.get('/api/validate-connection', async (req, res) => { + const validationResult = await validateATXPConnectionString(req); if (validationResult.isValid) { res.json({ diff --git a/backend/server.ts b/backend/server.ts index 8c74053..fc80e9f 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -4,14 +4,18 @@ 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'; // Load environment variables // In production, __dirname points to dist/, but .env is in the parent directory @@ -124,7 +128,7 @@ async function pollForTaskCompletion( taskId: string, textId: number, requestId: string, - account: ATXPAccount + account: any ) { console.log(`Starting polling for task ${taskId}`); let completed = false; @@ -167,11 +171,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, @@ -272,11 +277,11 @@ app.post('/api/texts', async (req: Request, res: Response) => { // Get ATXP connection string from header or environment variable let connectionString: string; - let account: ATXPAccount; + let account: any; try { connectionString = getATXPConnectionString(req); - account = findATXPAccount(connectionString); + account = await findATXPAccount(connectionString); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to get ATXP connection string'; return res.status(400).json({ error: errorMessage }); @@ -302,13 +307,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, @@ -375,8 +384,8 @@ app.get('/api/health', (req: Request, res: Response) => { }); // Connection validation endpoint -app.get('/api/validate-connection', (req: Request, res: Response) => { - const validationResult = validateATXPConnectionString(req); +app.get('/api/validate-connection', async (req: Request, res: Response) => { + const validationResult = await validateATXPConnectionString(req); if (validationResult.isValid) { res.json({ @@ -393,23 +402,53 @@ 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 directory + path.join(__dirname, './build'), + // Vercel: alternative paths + '/var/task/backend/build', + // Development fallback + path.join(__dirname, '../build') + ]; + + console.log('__dirname:', __dirname); + console.log('Looking for frontend build in candidates:', candidates); + + for (const candidate of candidates) { + console.log(`Checking: ${candidate}, exists: ${fs.existsSync(candidate)}`); + if (fs.existsSync(candidate)) { + console.log(`Found frontend build at: ${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); + console.log(`Contents of __dirname (${__dirname}):`, currentDirContents); + + // 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); + console.log(`Contents of build directory (${buildPath}):`, buildContents); + } 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 diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 73a74be..c31974e 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -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", diff --git a/backend/vitest.config.js b/backend/vitest.config.js new file mode 100644 index 0000000..ec52ffb --- /dev/null +++ b/backend/vitest.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node' + }, +}); \ No newline at end of file diff --git a/vercel.json b/vercel.json index ddb88da..c27ff1c 100644 --- a/vercel.json +++ b/vercel.json @@ -5,7 +5,8 @@ "src": "backend/server.ts", "use": "@vercel/node", "config": { - "includeFiles": ["backend/**", "frontend/build/**"] + "maxLambdaSize": "50mb", + "runtime": "nodejs20.x" } } ], @@ -15,13 +16,9 @@ "dest": "/backend/server.ts" } ], - "installCommand": "npm run setup", + "installCommand": "npm run install-all", + "buildCommand": "npm run build && cp -r frontend/build backend/", "env": { "NODE_ENV": "production" - }, - "functions": { - "backend/server.ts": { - "maxDuration": 30 - } } } \ No newline at end of file