Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 19 additions & 22 deletions backend/atxp-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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' });
});
Expand All @@ -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<Request> 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<Request> 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<Request> 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<Request> 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,
Expand Down
8 changes: 4 additions & 4 deletions backend/atxp-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Request } from 'express';
import { ATXPAccount } from '@atxp/client';

/**
* Get ATXP connection string from header or environment variable
Expand All @@ -23,18 +22,19 @@ 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<any> {
const { ATXPAccount } = await import('@atxp/client');
return new ATXPAccount(connectionString, {network: 'base'});
}

/**
* 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)
Expand Down
3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 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 Expand Up @@ -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({
Expand Down
97 changes: 68 additions & 29 deletions backend/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 });
Expand All @@ -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,
Expand Down Expand Up @@ -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({
Expand All @@ -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
Expand Down
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'
},
});
Loading