learning_ai_invt_trdg/backend/verifyTenantIsolation.ts
Saravana Achu Mac 6fac10de9d refactor(backend): root scripts use legacySupabase client where possible
- Call loadDynamicConfig() without dead supabaseService argument (Cosmos-backed).
- Use getLegacySupabaseClient() for raw .from() queries in maintenance scripts.
- manualOverrideCloseTrades: typed imports + legacy client for lifecycle SELECT.
- verify_realtime: ESM .js imports and comment for subscribeToProfiles.
- verifyTenantIsolation: comment for singleton monkey-patch.

Made-with: Cursor
2026-04-04 20:44:24 -07:00

230 lines
9.4 KiB
TypeScript

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<void> {
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<void> {
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');