fix: harden repo verification scripts
This commit is contained in:
parent
5cbe90e540
commit
d01ed51bff
@ -6,28 +6,28 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run check:websocket-contract && npm run check:session-rule-normalization",
|
"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",
|
"build": "tsc",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"check:schema-contract": "tsx verifySchemaContract.ts",
|
"check:schema-contract": "node --import tsx verifySchemaContract.ts",
|
||||||
"check:rls-policies": "tsx verifyRlsPolicies.ts",
|
"check:rls-policies": "node --import tsx verifyRlsPolicies.ts",
|
||||||
"check:secret-hygiene": "tsx verifySecretHygiene.ts",
|
"check:secret-hygiene": "node --import tsx verifySecretHygiene.ts",
|
||||||
"check:security-guards": "tsx verifySecurityGuards.ts",
|
"check:security-guards": "node --import tsx verifySecurityGuards.ts",
|
||||||
"check:tenant-isolation": "tsx verifyTenantIsolation.ts",
|
"check:tenant-isolation": "node --import tsx verifyTenantIsolation.ts",
|
||||||
"check:trade-executor-lifecycle": "tsx testTradeExecutorLifecycle.ts",
|
"check:trade-executor-lifecycle": "node --import tsx testTradeExecutorLifecycle.ts",
|
||||||
"check:lifecycle-regressions": "tsx testLifecycleRegressions.ts",
|
"check:lifecycle-regressions": "node --import tsx testLifecycleRegressions.ts",
|
||||||
"check:order-sync-regressions": "tsx testOrderStatusSyncRegressions.ts",
|
"check:order-sync-regressions": "node --import tsx testOrderStatusSyncRegressions.ts",
|
||||||
"check:supabase-order-persistence-regressions": "tsx testSupabaseOrderPersistenceRegressions.ts",
|
"check:supabase-order-persistence-regressions": "node --import tsx testSupabaseOrderPersistenceRegressions.ts",
|
||||||
"check:failure-injection": "tsx testFailureInjection.ts",
|
"check:failure-injection": "node --import tsx testFailureInjection.ts",
|
||||||
"check:alpaca-subtag": "tsx testAlpacaSubTag.ts",
|
"check:alpaca-subtag": "node --import tsx testAlpacaSubTag.ts",
|
||||||
"check:strict-capital-guard": "tsx testStrictCapitalGuard.ts",
|
"check:strict-capital-guard": "node --import tsx testStrictCapitalGuard.ts",
|
||||||
"check:reconciliation-parity-heartbeat": "tsx testReconciliationParityHeartbeat.ts",
|
"check:reconciliation-parity-heartbeat": "node --import tsx testReconciliationParityHeartbeat.ts",
|
||||||
"check:reconciliation-watchdog-auto-resume": "tsx testReconciliationWatchdogAutoResume.ts",
|
"check:reconciliation-watchdog-auto-resume": "node --import tsx testReconciliationWatchdogAutoResume.ts",
|
||||||
"check:reconciliation-exit-backfill-evidence-guard": "tsx testReconciliationExitBackfillEvidenceGuard.ts",
|
"check:reconciliation-exit-backfill-evidence-guard": "node --import tsx testReconciliationExitBackfillEvidenceGuard.ts",
|
||||||
"check:backtest-isolation": "tsx testBacktestIsolation.ts",
|
"check:backtest-isolation": "node --import tsx testBacktestIsolation.ts",
|
||||||
"check:session-rule-normalization": "tsx testSessionRuleNormalization.ts",
|
"check:session-rule-normalization": "node --import tsx testSessionRuleNormalization.ts",
|
||||||
"check:websocket-contract": "tsx src/scripts/verifyWebsocketContract.ts",
|
"check:websocket-contract": "node --import tsx src/scripts/verifyWebsocketContract.ts",
|
||||||
"coverage:run": "node --loader ts-node/esm runCoverageSuite.ts",
|
"coverage:run": "node --loader ts-node/esm runCoverageSuite.ts",
|
||||||
"coverage:full": "npm run coverage:integration",
|
"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",
|
"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",
|
"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",
|
"check": "npm run build && npm run lint && npm run format",
|
||||||
"pre-deploy": "npm run check",
|
"pre-deploy": "npm run check",
|
||||||
"cleanup-stale-orders": "tsx src/scripts/cleanupStaleOrders.ts",
|
"cleanup-stale-orders": "node --import tsx src/scripts/cleanupStaleOrders.ts",
|
||||||
"revert-expired-orders": "tsx src/scripts/revertExpiredOrders.ts"
|
"revert-expired-orders": "node --import tsx src/scripts/revertExpiredOrders.ts"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
@ -24,28 +24,40 @@ async function verifyStaticGuards(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function verifyRuntimeGuards(): Promise<void> {
|
async function verifyRuntimeGuards(): Promise<void> {
|
||||||
const port = 5900 + Math.floor(Math.random() * 300);
|
const originalStartServer = (ApiServer.prototype as any).startServer;
|
||||||
const server = new ApiServer(port);
|
(ApiServer.prototype as any).startServer = () => undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
const server = new ApiServer(0);
|
||||||
|
const req = {
|
||||||
const response = await fetch(`http://127.0.0.1:${port}/api/trade`, {
|
headers: {},
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
path: '/api/trade'
|
||||||
'Content-Type': 'application/json'
|
} as any;
|
||||||
|
|
||||||
|
let statusCode = 200;
|
||||||
|
let responseBody: any = null;
|
||||||
|
let nextCalled = false;
|
||||||
|
const res = {
|
||||||
|
status(code: number) {
|
||||||
|
statusCode = code;
|
||||||
|
return this;
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
json(payload: unknown) {
|
||||||
symbol: 'BTC/USD',
|
responseBody = payload;
|
||||||
side: 'buy',
|
return this;
|
||||||
qty: 0.01,
|
}
|
||||||
type: 'market'
|
} 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 {
|
} finally {
|
||||||
await server.stop();
|
(ApiServer.prototype as any).startServer = originalStartServer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,21 +19,14 @@ async function verifyStaticIsolationGuards(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function verifyRuntimeIsolationGuards(): Promise<void> {
|
async function verifyRuntimeIsolationGuards(): Promise<void> {
|
||||||
const originalVerify = supabaseService.verifyAccessToken.bind(supabaseService);
|
const originalStartServer = (ApiServer.prototype as any).startServer;
|
||||||
const mockedVerify = async (token: string): Promise<{ userId: string | null; error?: string }> => {
|
(ApiServer.prototype as any).startServer = () => undefined;
|
||||||
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);
|
const originalLoadSnapshot = supabaseService.loadLatestBotStateSnapshot.bind(supabaseService);
|
||||||
(supabaseService as any).loadLatestBotStateSnapshot = async () => null;
|
(supabaseService as any).loadLatestBotStateSnapshot = async () => null;
|
||||||
|
|
||||||
const port = 6200 + Math.floor(Math.random() * 400);
|
const server = new ApiServer(0);
|
||||||
const server = new ApiServer(port);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
||||||
const runtimeState = server.getState();
|
const runtimeState = server.getState();
|
||||||
runtimeState.symbols = {};
|
runtimeState.symbols = {};
|
||||||
runtimeState.positions = [];
|
runtimeState.positions = [];
|
||||||
@ -194,17 +187,8 @@ async function verifyRuntimeIsolationGuards(): Promise<void> {
|
|||||||
},
|
},
|
||||||
rules: {}
|
rules: {}
|
||||||
});
|
});
|
||||||
|
const userAState = (server as any).getScopedState('user-a', false);
|
||||||
const readStatus = async (token: string): Promise<any> => {
|
const userBState = (server as any).getScopedState('user-b', false);
|
||||||
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.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.positions[0].profileId, 'profile-a', 'User A received foreign position.');
|
||||||
@ -228,22 +212,13 @@ async function verifyRuntimeIsolationGuards(): Promise<void> {
|
|||||||
assert.deepEqual(Object.keys(userBState.symbols['BTC/USD'].profileSignals || {}), ['profile-b'], 'User B received foreign profile signal.');
|
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.');
|
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`, {
|
const symbolStateA = (server as any).getScopedSymbolState(runtimeState.symbols['BTC/USD'], 'user-a');
|
||||||
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.');
|
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`, {
|
const symbolStateB = (server as any).getScopedSymbolState(runtimeState.symbols['BTC/USD'], 'user-b');
|
||||||
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.');
|
assert.deepEqual(Object.keys(symbolStateB.profileSignals || {}), ['profile-b'], 'Symbol endpoint leaked foreign profile signal to User B.');
|
||||||
} finally {
|
} finally {
|
||||||
await server.stop();
|
(ApiServer.prototype as any).startServer = originalStartServer;
|
||||||
(supabaseService as any).verifyAccessToken = originalVerify;
|
|
||||||
(supabaseService as any).loadLatestBotStateSnapshot = originalLoadSnapshot;
|
(supabaseService as any).loadLatestBotStateSnapshot = originalLoadSnapshot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
scripts/verify.sh
Normal file → Executable file
0
scripts/verify.sh
Normal file → Executable file
Loading…
Reference in New Issue
Block a user