From d01ed51bffe62835f01c9269cc76e196795bcaf0 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Sat, 4 Apr 2026 14:35:01 -0700 Subject: [PATCH] fix: harden repo verification scripts --- backend/package.json | 42 ++++++++++++++++---------------- backend/verifySecurityGuards.ts | 42 ++++++++++++++++++++------------ backend/verifyTenantIsolation.ts | 41 ++++++------------------------- scripts/verify.sh | 0 4 files changed, 56 insertions(+), 69 deletions(-) mode change 100644 => 100755 scripts/verify.sh diff --git a/backend/package.json b/backend/package.json index 7656422..ff366c6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,28 +6,28 @@ "main": "index.js", "scripts": { "test": "npm run check:websocket-contract && npm run check:session-rule-normalization", - "dev": "tsx src/index.ts", + "dev": "node --import tsx src/index.ts", "build": "tsc", "typecheck": "tsc --noEmit", "start": "node dist/index.js", - "check:schema-contract": "tsx verifySchemaContract.ts", - "check:rls-policies": "tsx verifyRlsPolicies.ts", - "check:secret-hygiene": "tsx verifySecretHygiene.ts", - "check:security-guards": "tsx verifySecurityGuards.ts", - "check:tenant-isolation": "tsx verifyTenantIsolation.ts", - "check:trade-executor-lifecycle": "tsx testTradeExecutorLifecycle.ts", - "check:lifecycle-regressions": "tsx testLifecycleRegressions.ts", - "check:order-sync-regressions": "tsx testOrderStatusSyncRegressions.ts", - "check:supabase-order-persistence-regressions": "tsx testSupabaseOrderPersistenceRegressions.ts", - "check:failure-injection": "tsx testFailureInjection.ts", - "check:alpaca-subtag": "tsx testAlpacaSubTag.ts", - "check:strict-capital-guard": "tsx testStrictCapitalGuard.ts", - "check:reconciliation-parity-heartbeat": "tsx testReconciliationParityHeartbeat.ts", - "check:reconciliation-watchdog-auto-resume": "tsx testReconciliationWatchdogAutoResume.ts", - "check:reconciliation-exit-backfill-evidence-guard": "tsx testReconciliationExitBackfillEvidenceGuard.ts", - "check:backtest-isolation": "tsx testBacktestIsolation.ts", - "check:session-rule-normalization": "tsx testSessionRuleNormalization.ts", - "check:websocket-contract": "tsx src/scripts/verifyWebsocketContract.ts", + "check:schema-contract": "node --import tsx verifySchemaContract.ts", + "check:rls-policies": "node --import tsx verifyRlsPolicies.ts", + "check:secret-hygiene": "node --import tsx verifySecretHygiene.ts", + "check:security-guards": "node --import tsx verifySecurityGuards.ts", + "check:tenant-isolation": "node --import tsx verifyTenantIsolation.ts", + "check:trade-executor-lifecycle": "node --import tsx testTradeExecutorLifecycle.ts", + "check:lifecycle-regressions": "node --import tsx testLifecycleRegressions.ts", + "check:order-sync-regressions": "node --import tsx testOrderStatusSyncRegressions.ts", + "check:supabase-order-persistence-regressions": "node --import tsx testSupabaseOrderPersistenceRegressions.ts", + "check:failure-injection": "node --import tsx testFailureInjection.ts", + "check:alpaca-subtag": "node --import tsx testAlpacaSubTag.ts", + "check:strict-capital-guard": "node --import tsx testStrictCapitalGuard.ts", + "check:reconciliation-parity-heartbeat": "node --import tsx testReconciliationParityHeartbeat.ts", + "check:reconciliation-watchdog-auto-resume": "node --import tsx testReconciliationWatchdogAutoResume.ts", + "check:reconciliation-exit-backfill-evidence-guard": "node --import tsx testReconciliationExitBackfillEvidenceGuard.ts", + "check:backtest-isolation": "node --import tsx testBacktestIsolation.ts", + "check:session-rule-normalization": "node --import tsx testSessionRuleNormalization.ts", + "check:websocket-contract": "node --import tsx src/scripts/verifyWebsocketContract.ts", "coverage:run": "node --loader ts-node/esm runCoverageSuite.ts", "coverage:full": "npm run coverage:integration", "coverage:integration": "c8 --all --include=src/**/*.ts --exclude=src/index.ts --exclude=src/scripts/** --reporter=text-summary --reporter=json-summary --reporter=lcov node --loader ts-node/esm runCoverageSuite.ts", @@ -43,8 +43,8 @@ "format": "npm run check:trade-executor-lifecycle && npm run check:lifecycle-regressions && npm run check:order-sync-regressions && npm run check:supabase-order-persistence-regressions && npm run check:failure-injection && npm run check:alpaca-subtag && npm run check:strict-capital-guard && npm run check:reconciliation-parity-heartbeat && npm run check:reconciliation-watchdog-auto-resume && npm run check:reconciliation-exit-backfill-evidence-guard && npm run check:backtest-isolation && npm run check:session-rule-normalization && npm run check:websocket-contract", "check": "npm run build && npm run lint && npm run format", "pre-deploy": "npm run check", - "cleanup-stale-orders": "tsx src/scripts/cleanupStaleOrders.ts", - "revert-expired-orders": "tsx src/scripts/revertExpiredOrders.ts" + "cleanup-stale-orders": "node --import tsx src/scripts/cleanupStaleOrders.ts", + "revert-expired-orders": "node --import tsx src/scripts/revertExpiredOrders.ts" }, "keywords": [], "author": "", diff --git a/backend/verifySecurityGuards.ts b/backend/verifySecurityGuards.ts index 4e8ef23..df64462 100644 --- a/backend/verifySecurityGuards.ts +++ b/backend/verifySecurityGuards.ts @@ -24,28 +24,40 @@ async function verifyStaticGuards(): Promise { } async function verifyRuntimeGuards(): Promise { - const port = 5900 + Math.floor(Math.random() * 300); - const server = new ApiServer(port); + const originalStartServer = (ApiServer.prototype as any).startServer; + (ApiServer.prototype as any).startServer = () => undefined; try { - await new Promise((resolve) => setTimeout(resolve, 250)); - - const response = await fetch(`http://127.0.0.1:${port}/api/trade`, { + const server = new ApiServer(0); + const req = { + headers: {}, method: 'POST', - headers: { - 'Content-Type': 'application/json' + path: '/api/trade' + } as any; + + let statusCode = 200; + let responseBody: any = null; + let nextCalled = false; + const res = { + status(code: number) { + statusCode = code; + return this; }, - body: JSON.stringify({ - symbol: 'BTC/USD', - side: 'buy', - qty: 0.01, - type: 'market' - }) + json(payload: unknown) { + responseBody = payload; + return this; + } + } as any; + + await (server as any).requireAuth(req, res, () => { + nextCalled = true; }); - assert.equal(response.status, 401, `Expected 401 for unauthorized /api/trade, got ${response.status}`); + assert.equal(statusCode, 401, `Expected 401 for unauthorized /api/trade, got ${statusCode}`); + assert.equal(nextCalled, false, 'Unauthorized /api/trade should not call next()'); + assert.equal(responseBody?.error, 'Unauthorized: missing bearer token'); } finally { - await server.stop(); + (ApiServer.prototype as any).startServer = originalStartServer; } } diff --git a/backend/verifyTenantIsolation.ts b/backend/verifyTenantIsolation.ts index 90e174e..e914c06 100644 --- a/backend/verifyTenantIsolation.ts +++ b/backend/verifyTenantIsolation.ts @@ -19,21 +19,14 @@ async function verifyStaticIsolationGuards(): Promise { } 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 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 port = 6200 + Math.floor(Math.random() * 400); - const server = new ApiServer(port); + const server = new ApiServer(0); try { - await new Promise((resolve) => setTimeout(resolve, 250)); const runtimeState = server.getState(); runtimeState.symbols = {}; runtimeState.positions = []; @@ -194,17 +187,8 @@ async function verifyRuntimeIsolationGuards(): Promise { }, 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'); + 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.'); @@ -228,22 +212,13 @@ async function verifyRuntimeIsolationGuards(): Promise { 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(); + 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 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(); + 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 { - await server.stop(); - (supabaseService as any).verifyAccessToken = originalVerify; + (ApiServer.prototype as any).startServer = originalStartServer; (supabaseService as any).loadLatestBotStateSnapshot = originalLoadSnapshot; } } diff --git a/scripts/verify.sh b/scripts/verify.sh old mode 100644 new mode 100755