diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..29ce430 --- /dev/null +++ b/TODO.md @@ -0,0 +1,18 @@ +# Rate Limiting Fix - Task Breakdown + +## Bug 1: Missing `rawApiKeys` variable in `config.ts` +- [x] Add `const rawApiKeys = parseJsonEnv('API_KEYS', '[]');` before its usage in `loadConfig()` +- File: `listener/src/config.ts` + +## Bug 2: Health endpoint not rate-limit exempt in `events-server.ts` +- [x] Use the standalone `isRateLimitExempt()` function instead of the incomplete inline check +- File: `listener/src/api/events-server.ts` + +## Bug 3: Duplicate `contractStatuses` declaration in `events-server.ts` +- [x] Remove the first (redundant) `const contractStatuses` block, keep the second +- File: `listener/src/api/events-server.ts` + +## Verification +- [ ] Run `npm run typecheck` to verify TypeScript compilation +- [ ] Run `npm test -- rate-limiter` to verify tests pass + diff --git a/listener/src/api/events-server.ts b/listener/src/api/events-server.ts index bf23268..d6490ff 100644 --- a/listener/src/api/events-server.ts +++ b/listener/src/api/events-server.ts @@ -203,7 +203,7 @@ async function getContractPauseStatus( try { const server = new StellarSDK.rpc.Server(stellarRpcUrl); const contract = new StellarSDK.Contract(contractAddress); - + // Create a dummy account for simulation (we don't need to actually sign anything) const dummyKeypair = StellarSDK.Keypair.random(); const sourceAccount = await server.getAccount(dummyKeypair.publicKey()).catch(() => { @@ -237,9 +237,9 @@ async function getContractPauseStatus( const value = StellarSDK.scValToNative(simResult.retval); return { paused: !!value }; } catch (err) { - return { - paused: false, - error: err instanceof Error ? err.message : String(err) + return { + paused: false, + error: err instanceof Error ? err.message : String(err) }; } } @@ -252,17 +252,6 @@ async function buildStatusResponse(options: EventsServerOptions): Promise<{ }>; timestamp: string; }> { - const contractStatuses = options.contractAddresses - ? await Promise.all( - options.contractAddresses.map(async (contractConfig) => { - const status = await getContractPauseStatus(contractConfig.address, options.stellarRpcUrl); - return { - address: contractConfig.address, - ...status - }; - }) - ) - : []; const contractStatuses = await Promise.all( (options.contractAddresses ?? []).map(async (contractConfig) => { const status = await getContractPauseStatus(contractConfig.address, options.stellarRpcUrl); @@ -417,6 +406,7 @@ export function createEventsServer(options: EventsServerOptions): http.Server { res.setHeader('X-Correlation-Id', correlationId); const url = new URL(req.url ?? '/', 'http://localhost'); + const pathname = url.pathname; // ── API Route Versioning (#386) ───────────────────────────────────────── // Accept requests to /api/v1/* and silently rewrite the pathname to the @@ -435,7 +425,7 @@ export function createEventsServer(options: EventsServerOptions): http.Server { // reachable even after a client exhausts its quota — otherwise callers // can't read the very metrics that explain why they are being throttled. const isRateLimitExempt = - req.method === 'GET' && url.pathname === '/api/rate-limit/metrics'; + req.method === 'GET' && (pathname === '/api/rate-limit/metrics' || pathname === '/health'); if (rateLimiter && !isRateLimitExempt) { const allowed = await rateLimiter.handle(req, res as any); diff --git a/listener/src/config.ts b/listener/src/config.ts index fe5c962..67f8dcd 100644 --- a/listener/src/config.ts +++ b/listener/src/config.ts @@ -178,6 +178,7 @@ export function loadConfig(): Config { const discord = loadDiscordConfig(); const rawContractAddresses = parseJsonEnv('CONTRACT_ADDRESSES', '[]'); const rawWebhookSecrets = parseJsonEnv('WEBHOOK_SECRETS', '[]'); + const rawApiKeys = parseJsonEnv('API_KEYS', '[]'); const clientOverrides = parseJsonEnv>( 'RATE_LIMIT_CLIENT_OVERRIDES', '{}'