import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { ApiServer } from './src/services/apiServer.js'; import { supabaseService } from './src/services/SupabaseService.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = __dirname; async function verifyStaticIsolationGuards(): Promise { const apiServerSource = fs.readFileSync(path.join(repoRoot, 'src/services/apiServer.ts'), 'utf8'); assert(!/this\.io\.emit\(/.test(apiServerSource), 'Global websocket broadcasts detected (this.io.emit).'); assert(!/socket\.emit\('state',\s*this\.state\)/.test(apiServerSource), 'Global runtime state push detected on websocket connect.'); assert(/private\s+getScopedState\s*\(/.test(apiServerSource), 'Scoped state projector missing.'); assert(/private\s+emitToConnectedUsers/.test(apiServerSource), 'Tenant-scoped websocket emitter missing.'); } async function verifyRuntimeIsolationGuards(): Promise { const originalVerify = supabaseService.verifyAccessToken.bind(supabaseService); const mockedVerify = async (token: string): Promise<{ userId: string | null; error?: string }> => { if (token === 'token-user-a') return { userId: 'user-a' }; if (token === 'token-user-b') return { userId: 'user-b' }; return { userId: null, error: 'invalid token' }; }; (supabaseService as any).verifyAccessToken = mockedVerify; const originalLoadSnapshot = supabaseService.loadLatestBotStateSnapshot.bind(supabaseService); (supabaseService as any).loadLatestBotStateSnapshot = async () => null; const port = 6200 + Math.floor(Math.random() * 400); const server = new ApiServer(port); try { await new Promise((resolve) => setTimeout(resolve, 250)); const runtimeState = server.getState(); runtimeState.symbols = {}; runtimeState.positions = []; runtimeState.orders = []; runtimeState.history = []; runtimeState.alerts = []; server.registerManualTrader('profile-a', { getUserId: () => 'user-a' } as any); server.registerManualTrader('profile-b', { getUserId: () => 'user-b' } as any); server.updatePositions([ { id: 'pos-a', symbol: 'BTC/USD', side: 'BUY', size: 1, entryPrice: 100, currentPrice: 101, stopLoss: 90, takeProfit: 110, unrealizedPnl: 1, unrealizedPnlPercent: 1, marketValue: 101, userId: 'user-a', profileId: 'profile-a', profileName: 'Profile A', tradeId: 'TRD-A-1' } ], 'profile-a'); server.updatePositions([ { id: 'pos-b', symbol: 'BTC/USD', side: 'BUY', size: 2, entryPrice: 100, currentPrice: 102, stopLoss: 90, takeProfit: 110, unrealizedPnl: 4, unrealizedPnlPercent: 2, marketValue: 204, userId: 'user-b', profileId: 'profile-b', profileName: 'Profile B', tradeId: 'TRD-B-1' } ], 'profile-b'); server.updateOrders([ { id: 'ord-a', symbol: 'BTC/USD', type: 'Market', side: 'BUY', qty: 1, price: 100, status: 'filled', timestamp: Date.now(), profileId: 'profile-a', userId: 'user-a', trade_id: 'TRD-A-1', action: 'ENTRY', source: 'BOT' } ], 'profile-a'); server.updateOrders([ { id: 'ord-b', symbol: 'BTC/USD', type: 'Market', side: 'BUY', qty: 2, price: 100, status: 'filled', timestamp: Date.now(), profileId: 'profile-b', userId: 'user-b', trade_id: 'TRD-B-1', action: 'ENTRY', source: 'BOT' } ], 'profile-b'); server.addHistory({ symbol: 'BTC/USD', side: 'BUY', entryPrice: 100, exitPrice: 105, size: 1, pnl: 5, pnlPercent: 5, reason: 'target', timestamp: Date.now(), userId: 'user-a', profileId: 'profile-a', trade_id: 'TRD-A-1', source: 'BOT' }); server.addHistory({ symbol: 'BTC/USD', side: 'BUY', entryPrice: 100, exitPrice: 95, size: 1, pnl: -5, pnlPercent: -5, reason: 'stop', timestamp: Date.now(), userId: 'user-b', profileId: 'profile-b', trade_id: 'TRD-B-1', source: 'BOT' }); server.addAlert('info', 'BTC/USD', 'alert-a', { userId: 'user-a', profileId: 'profile-a' }); server.addAlert('info', 'BTC/USD', 'alert-b', { userId: 'user-b', profileId: 'profile-b' }); server.updateSymbol('BTC/USD', { price: 101, change24h: 0.5, changeToday: 0.2, session: 'NY', volatility: 'Low', signal: 'BUY', profileSignals: { 'profile-a': { profileName: 'Profile A', signal: 'BUY', passed: true, reason: 'ok', rules: {} }, 'profile-b': { profileName: 'Profile B', signal: 'SELL', passed: true, reason: 'ok', rules: {} } }, activePosition: { side: 'BUY', entryPrice: 100, size: 2, stopLoss: 90, takeProfit: 110, userId: 'user-b', profileId: 'profile-b', tradeId: 'TRD-B-1', profileName: 'Profile B' }, indicators: { ema20_1h: 99 }, rules: {} }); const readStatus = async (token: string): Promise => { const response = await fetch(`http://127.0.0.1:${port}/api/state`, { headers: { Authorization: `Bearer ${token}` } }); assert.equal(response.status, 200, `Expected /api/state 200 for token ${token}`); return await response.json(); }; const userAState = await readStatus('token-user-a'); const userBState = await readStatus('token-user-b'); assert.equal(userAState.positions.length, 1, 'User A should only receive one owned position.'); assert.equal(userAState.positions[0].profileId, 'profile-a', 'User A received foreign position.'); assert.equal(userAState.orders.length, 1, 'User A should only receive one owned order.'); assert.equal(userAState.orders[0].profileId, 'profile-a', 'User A received foreign order.'); assert.equal(userAState.history.length, 1, 'User A should only receive one owned history row.'); assert.equal(userAState.history[0].profileId, 'profile-a', 'User A received foreign history row.'); assert.equal(userAState.alerts.length, 1, 'User A should only receive one owned alert.'); assert.equal(userAState.alerts[0].profileId, 'profile-a', 'User A received foreign alert.'); assert.deepEqual(Object.keys(userAState.symbols['BTC/USD'].profileSignals || {}), ['profile-a'], 'User A received foreign profile signal.'); assert.equal(userAState.symbols['BTC/USD'].activePosition, null, 'User A should not receive User B active symbol position.'); assert.equal(userBState.positions.length, 1, 'User B should only receive one owned position.'); assert.equal(userBState.positions[0].profileId, 'profile-b', 'User B received foreign position.'); assert.equal(userBState.orders.length, 1, 'User B should only receive one owned order.'); assert.equal(userBState.orders[0].profileId, 'profile-b', 'User B received foreign order.'); assert.equal(userBState.history.length, 1, 'User B should only receive one owned history row.'); assert.equal(userBState.history[0].profileId, 'profile-b', 'User B received foreign history row.'); assert.equal(userBState.alerts.length, 1, 'User B should only receive one owned alert.'); assert.equal(userBState.alerts[0].profileId, 'profile-b', 'User B received foreign alert.'); assert.deepEqual(Object.keys(userBState.symbols['BTC/USD'].profileSignals || {}), ['profile-b'], 'User B received foreign profile signal.'); assert.equal(userBState.symbols['BTC/USD'].activePosition?.profileId, 'profile-b', 'User B should receive owned active symbol position.'); const symbolResponseA = await fetch(`http://127.0.0.1:${port}/api/symbol/BTC%2FUSD`, { headers: { Authorization: 'Bearer token-user-a' } }); assert.equal(symbolResponseA.status, 200, 'User A symbol endpoint should be reachable.'); const symbolStateA = await symbolResponseA.json(); assert.deepEqual(Object.keys(symbolStateA.profileSignals || {}), ['profile-a'], 'Symbol endpoint leaked foreign profile signal to User A.'); const symbolResponseB = await fetch(`http://127.0.0.1:${port}/api/symbol/BTC%2FUSD`, { headers: { Authorization: 'Bearer token-user-b' } }); assert.equal(symbolResponseB.status, 200, 'User B symbol endpoint should be reachable.'); const symbolStateB = await symbolResponseB.json(); assert.deepEqual(Object.keys(symbolStateB.profileSignals || {}), ['profile-b'], 'Symbol endpoint leaked foreign profile signal to User B.'); } finally { await server.stop(); (supabaseService as any).verifyAccessToken = originalVerify; (supabaseService as any).loadLatestBotStateSnapshot = originalLoadSnapshot; } } await verifyStaticIsolationGuards(); await verifyRuntimeIsolationGuards(); console.log('[tenant-isolation] OK: tenant-scoped runtime state guards passed');