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'; // Direct supabaseService: monkey-patches loadLatestBotStateSnapshot on the singleton for isolation checks. 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 originalStartServer = (ApiServer.prototype as any).startServer; (ApiServer.prototype as any).startServer = () => undefined; const originalLoadSnapshot = supabaseService.loadLatestBotStateSnapshot.bind(supabaseService); (supabaseService as any).loadLatestBotStateSnapshot = async () => null; const server = new ApiServer(0); try { 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 userAState = (server as any).getScopedState('user-a', false); const userBState = (server as any).getScopedState('user-b', false); 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 symbolStateA = (server as any).getScopedSymbolState(runtimeState.symbols['BTC/USD'], 'user-a'); assert.deepEqual(Object.keys(symbolStateA.profileSignals || {}), ['profile-a'], 'Symbol endpoint leaked foreign profile signal to User A.'); const symbolStateB = (server as any).getScopedSymbolState(runtimeState.symbols['BTC/USD'], 'user-b'); assert.deepEqual(Object.keys(symbolStateB.profileSignals || {}), ['profile-b'], 'Symbol endpoint leaked foreign profile signal to User B.'); } finally { (ApiServer.prototype as any).startServer = originalStartServer; (supabaseService as any).loadLatestBotStateSnapshot = originalLoadSnapshot; } } await verifyStaticIsolationGuards(); await verifyRuntimeIsolationGuards(); console.log('[tenant-isolation] OK: tenant-scoped runtime state guards passed');