From 1c7696a29eec6696f4555c9a79ad5e8e42e6964e Mon Sep 17 00:00:00 2001 From: Oluwafemi Ajibade Date: Sun, 30 Aug 2026 13:17:55 +0000 Subject: [PATCH 1/5] fix: add ESLint config, lint script, and lint-staged gate to backend (#1278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend had no ESLint config, no lint script, and was excluded from the root lint-staged glob, so backend TypeScript was never statically linted locally or in CI. Add a flat ESLint config for backend (typescript-eslint recommended, adapted from the frontend's ruleset), a `lint` script, eslint/typescript-eslint devDependencies, a lint step in the backend CI job, and extend lint-staged to backend/** so pre-commit hooks catch backend violations. Also fix the lint violations the new rules surface in src (explicit any, unused vars, console, prefer-const) so the gate passes on the current codebase; tests keep no-explicit-any as a warning. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- .github/workflows/ci.yml | 4 ++ backend/eslint.config.mjs | 52 +++++++++++++++++++ backend/package.json | 4 ++ backend/src/controllers/stream.controller.ts | 12 ++--- backend/src/controllers/stream/cancel.ts | 7 +-- backend/src/controllers/user.controller.ts | 2 +- backend/src/controllers/webhook.controller.ts | 22 ++++---- backend/src/lib/prisma-sandbox.ts | 9 ++-- backend/src/lib/redis.ts | 4 +- .../admin-rate-limiter.middleware.ts | 2 +- backend/src/middleware/error.middleware.ts | 7 ++- .../stream-rate-limiter.middleware.ts | 6 +-- backend/src/routes/v1/admin.routes.ts | 10 ++-- backend/src/routes/v1/stream.routes.ts | 4 +- backend/src/routes/v1/streams/withdraw.ts | 13 ++--- backend/src/services/export.service.ts | 39 +++++++++----- backend/src/services/webhook.service.ts | 8 +-- backend/src/workers/soroban-event-worker.ts | 2 +- backend/src/workers/stream-runway-worker.ts | 3 +- package-lock.json | 13 +++-- package.json | 3 ++ 21 files changed, 158 insertions(+), 68 deletions(-) create mode 100644 backend/eslint.config.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eaaac563..55916dd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,6 +86,10 @@ jobs: env: DATABASE_URL: postgresql://postgres:password@127.0.0.1:5432/flowfi_test + - name: Lint + run: npm run lint + working-directory: backend + - name: Build run: npm run build working-directory: backend diff --git a/backend/eslint.config.mjs b/backend/eslint.config.mjs new file mode 100644 index 00000000..f023a008 --- /dev/null +++ b/backend/eslint.config.mjs @@ -0,0 +1,52 @@ +// ESLint flat config for the FlowFi backend (Node.js/Express + TypeScript). +// Mirrors the frontend setup (frontend/eslint.config.mjs), adapted for a +// non-React, Node-first codebase via typescript-eslint's recommended ruleset. +import { defineConfig, globalIgnores } from "eslint/config"; +import tseslint from "typescript-eslint"; + +const eslintConfig = defineConfig([ + globalIgnores([ + "node_modules/**", + "dist/**", + "coverage/**", + "src/generated/**", + "examples/**", + "src/**/*.example.ts", + ]), + ...tseslint.configs.recommended, + { + files: ["**/*.{ts,tsx}"], + rules: { + // Forbid raw console.* calls in favour of the winston-based logger + // (src/logger.ts). + "no-console": "error", + // Allow the conventional `_`-prefix for intentionally-unused + // parameters, destructured variables, and caught errors. + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + }, + }, + { + // Seed scripts legitimately print to stdout — console is fine there. + files: ["prisma/seed.ts"], + rules: { + "no-console": "off", + }, + }, + { + // Test files use `any` liberally for mocks/test doubles; keep them linted + // but surface `any` as a warning instead of blocking. + files: ["tests/**/*.ts"], + rules: { + "@typescript-eslint/no-explicit-any": "warn", + }, + }, +]); + +export default eslintConfig; diff --git a/backend/package.json b/backend/package.json index 38478593..ea3a696b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,6 +12,7 @@ "test:integration": "vitest run tests/integration", "test:integration:docker": "docker compose up -d postgres && vitest run tests/integration/stream-lifecycle.test.ts; docker compose stop postgres", "dev": "nodemon", + "lint": "eslint .", "build": "tsc", "start": "node dist/index.js", "prisma:generate": "prisma generate", @@ -42,6 +43,7 @@ "zod": "^4.4.3" }, "devDependencies": { + "@eslint/js": "^9.39.3", "@types/cors": "^2.8.19", "@types/eventsource": "^1.1.15", "@types/express": "^5.0.6", @@ -50,6 +52,7 @@ "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", "@vitest/coverage-v8": "^3.2.7", + "eslint": "^9.39.3", "eventsource": "^2.0.2", "nodemon": "^3.1.11", "prisma": "^7.4.1", @@ -57,6 +60,7 @@ "ts-node": "^10.9.2", "tsx": "^4.19.2", "typescript": "^5.9.3", + "typescript-eslint": "^8.56.1", "vitest": "^3.2.4" } } diff --git a/backend/src/controllers/stream.controller.ts b/backend/src/controllers/stream.controller.ts index 0ba2a383..23c4bb66 100644 --- a/backend/src/controllers/stream.controller.ts +++ b/backend/src/controllers/stream.controller.ts @@ -655,17 +655,17 @@ export const getUserStreamSummary = async ( const totalStreamsCreated = outgoingStreams.length; const totalStreamedOut = sumStringI128( - outgoingStreams.map((stream: any) => stream.withdrawnAmount), + outgoingStreams.map((stream) => stream.withdrawnAmount), ); const totalStreamedIn = sumStringI128( - incomingStreams.map((stream: any) => stream.withdrawnAmount), + incomingStreams.map((stream) => stream.withdrawnAmount), ); const activeOutgoingCount = outgoingStreams.filter( - (stream: any) => stream.isActive, + (stream) => stream.isActive, ).length; const activeIncomingCount = incomingStreams.filter( - (stream: any) => stream.isActive, + (stream) => stream.isActive, ).length; const truncated = @@ -765,9 +765,9 @@ export const topUpStreamHandler = async (req: Request, res: Response) => { return res .status(200) .json({ streamId, txHash, depositedAmount: newDeposited }); - } catch (error: any) { + } catch (error: unknown) { logger.error(`[topUp] stream=${streamId} error:`, error); - return res.status(400).json({ error: 'Failed to top up stream on chain', message: error.message ?? 'Unknown error' }); + return res.status(400).json({ error: 'Failed to top up stream on chain', message: error instanceof Error ? error.message : 'Unknown error' }); } }; diff --git a/backend/src/controllers/stream/cancel.ts b/backend/src/controllers/stream/cancel.ts index eb16d62a..7a269e13 100644 --- a/backend/src/controllers/stream/cancel.ts +++ b/backend/src/controllers/stream/cancel.ts @@ -1,4 +1,4 @@ -import type { Response } from 'express'; +import type { Request, Response } from 'express'; import { prisma } from '../../lib/prisma.js'; import logger from '../../logger.js'; import * as sorobanService from '../../services/sorobanService.js'; @@ -46,10 +46,11 @@ import { parseStreamId } from '../../lib/stream-id.js'; * 409: * description: Stream already cancelled or completed */ -export const cancelStreamHandler = async (req: AuthenticatedRequest, res: Response) => { +export const cancelStreamHandler = async (req: Request, res: Response) => { try { + const authReq = req as AuthenticatedRequest; const streamIdParam = req.params.streamId; - const callerAddress = req.user.publicKey; + const callerAddress = authReq.user.publicKey; const streamId = Array.isArray(streamIdParam) ? streamIdParam[0] : streamIdParam; if (!streamId) { diff --git a/backend/src/controllers/user.controller.ts b/backend/src/controllers/user.controller.ts index 439db5d8..fa82ce84 100644 --- a/backend/src/controllers/user.controller.ts +++ b/backend/src/controllers/user.controller.ts @@ -207,7 +207,7 @@ export const getCurrentUser = async ( const { publicKey } = authReq.user; // Try to get user from database - let user = await prisma.user.findUnique({ + const user = await prisma.user.findUnique({ where: { publicKey }, include: { sentStreams: { diff --git a/backend/src/controllers/webhook.controller.ts b/backend/src/controllers/webhook.controller.ts index 7054262e..63847158 100644 --- a/backend/src/controllers/webhook.controller.ts +++ b/backend/src/controllers/webhook.controller.ts @@ -33,11 +33,11 @@ export async function createWebhook( secretKey, // Only returned once on creation message: "Store the secret key securely - it will not be shown again", }); - } catch (error: any) { + } catch (error: unknown) { logger.error("[Webhook Controller] Create error:", error); - res - .status(500) - .json({ error: error.message || "Failed to create webhook" }); + res.status(500).json({ + error: error instanceof Error ? error.message : "Failed to create webhook", + }); } } @@ -55,11 +55,11 @@ export async function listWebhooks(req: Request, res: Response): Promise { // Don't expose secret keys const safeSubscriptions = subscriptions.map( - ({ secretKey, ...rest }) => rest, + ({ secretKey: _secretKey, ...rest }) => rest, ); res.json({ subscriptions: safeSubscriptions }); - } catch (error: any) { + } catch (error: unknown) { logger.error("[Webhook Controller] List error:", error); res.status(500).json({ error: "Failed to list webhooks" }); } @@ -82,7 +82,7 @@ export async function deleteWebhook( await webhookService.deleteWebhookSubscription(id, userAddress); res.status(204).send(); - } catch (error: any) { + } catch (error: unknown) { logger.error("[Webhook Controller] Delete error:", error); res.status(500).json({ error: "Failed to delete webhook" }); } @@ -105,10 +105,10 @@ export async function testWebhook(req: Request, res: Response): Promise { message: "Test webhook sent", result, }); - } catch (error: any) { + } catch (error: unknown) { logger.error("[Webhook Controller] Test error:", error); - res - .status(500) - .json({ error: error.message || "Failed to send test webhook" }); + res.status(500).json({ + error: error instanceof Error ? error.message : "Failed to send test webhook", + }); } } diff --git a/backend/src/lib/prisma-sandbox.ts b/backend/src/lib/prisma-sandbox.ts index b6e5ae49..7890bafd 100644 --- a/backend/src/lib/prisma-sandbox.ts +++ b/backend/src/lib/prisma-sandbox.ts @@ -9,11 +9,12 @@ export function getSandboxPrisma(): PrismaClient { return globalForSandboxPrisma.sandboxPrisma; } - const sandboxPrisma = new PrismaClient({ - log: process.env.NODE_ENV === 'development' + const log: Array<'query' | 'info' | 'warn' | 'error'> = + process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] - : ['error'], - } as any); + : ['error']; + + const sandboxPrisma = new PrismaClient({ log }); if (process.env.NODE_ENV !== 'production') { globalForSandboxPrisma.sandboxPrisma = sandboxPrisma; diff --git a/backend/src/lib/redis.ts b/backend/src/lib/redis.ts index ac85d54e..568fb151 100644 --- a/backend/src/lib/redis.ts +++ b/backend/src/lib/redis.ts @@ -19,7 +19,7 @@ interface CacheItem { } class MemoryCache { - private cache = new Map>(); + private cache = new Map>(); private hits = 0; private misses = 0; @@ -35,7 +35,7 @@ class MemoryCache { return null; } this.hits++; - return item.value; + return item.value as T; } set(key: string, value: T, ttlSeconds: number): void { diff --git a/backend/src/middleware/admin-rate-limiter.middleware.ts b/backend/src/middleware/admin-rate-limiter.middleware.ts index 09160f03..fc9a1eef 100644 --- a/backend/src/middleware/admin-rate-limiter.middleware.ts +++ b/backend/src/middleware/admin-rate-limiter.middleware.ts @@ -19,7 +19,7 @@ export const adminRateLimiter = rateLimit({ } return req.ip ?? 'unknown'; }, - skip: (req: Request): boolean => { + skip: (_req: Request): boolean => { // Skip rate limiting in test environment return process.env.NODE_ENV === 'test'; }, diff --git a/backend/src/middleware/error.middleware.ts b/backend/src/middleware/error.middleware.ts index af90505b..2cec4227 100644 --- a/backend/src/middleware/error.middleware.ts +++ b/backend/src/middleware/error.middleware.ts @@ -50,7 +50,12 @@ export const errorHandler = ( } // Default Error - const statusCode = (err instanceof Error && (err as any).status) || (err instanceof Error && (err as any).statusCode) || 500; + const statusError = + err instanceof Error + ? (err as Error & { status?: number; statusCode?: number }) + : null; + const statusCode = + statusError?.status || statusError?.statusCode || 500; const message = err instanceof Error ? err.message : 'Internal Server Error'; return res.status(statusCode).json({ diff --git a/backend/src/middleware/stream-rate-limiter.middleware.ts b/backend/src/middleware/stream-rate-limiter.middleware.ts index 19fe84a2..e3227309 100644 --- a/backend/src/middleware/stream-rate-limiter.middleware.ts +++ b/backend/src/middleware/stream-rate-limiter.middleware.ts @@ -1,4 +1,4 @@ -import { rateLimit } from 'express-rate-limit'; +import { rateLimit, type Options } from 'express-rate-limit'; import { type Request, type Response, type NextFunction } from 'express'; import type { AuthenticatedRequest } from '../types/auth.types.js'; import logger from '../logger.js'; @@ -44,7 +44,7 @@ export function createStreamRateLimiter( * Skip rate limiting for non-authenticated requests * to ensure we only rate limit authenticated users */ - skip: (req: Request, res: Response): boolean => { + skip: (req: Request, _res: Response): boolean => { const authReq = req as AuthenticatedRequest; if (!authReq.user?.publicKey) { logger.warn('Stream creation rate limiter skipped: no authenticated user'); @@ -52,7 +52,7 @@ export function createStreamRateLimiter( } return false; }, - handler: (req: Request, res: Response, next: NextFunction, options: any): void => { + handler: (req: Request, res: Response, _next: NextFunction, options: Options): void => { const authReq = req as AuthenticatedRequest; logger.warn( `Rate limit exceeded for wallet: ${authReq.user?.publicKey || 'unknown'}`, diff --git a/backend/src/routes/v1/admin.routes.ts b/backend/src/routes/v1/admin.routes.ts index 93b1a817..b06deb5b 100644 --- a/backend/src/routes/v1/admin.routes.ts +++ b/backend/src/routes/v1/admin.routes.ts @@ -200,8 +200,8 @@ router.get('/metrics', async (_req: Request, res: Response) => { cache.set(ADMIN_METRICS_CACHE_KEY, payload, ADMIN_METRICS_CACHE_TTL_SECONDS); res.set('X-Cache', 'MISS'); res.json(withCalculatedAt(payload)); - } catch (err) { - logger.error('Error fetching admin metrics:', err); + } catch (_err) { + logger.error('Error fetching admin metrics:', _err); res.status(500).json({ error: 'Internal server error' }); } }); @@ -221,7 +221,7 @@ router.get('/indexer/status', async (req: Request, res: Response) => { try { const status = await getIndexerStatus(); res.json(status); - } catch (err) { + } catch (_err) { res.status(500).json({ error: 'Failed to fetch indexer status' }); } }); @@ -256,7 +256,7 @@ router.post('/indexer/reset', async (req: Request, res: Response) => { try { await resetIndexer(ledger); res.json({ ok: true, lastLedger: ledger }); - } catch (err) { + } catch (_err) { res.status(500).json({ error: 'Reset failed' }); } }); @@ -287,7 +287,7 @@ router.post('/indexer/replay', async (req: Request, res: Response) => { try { const requestId = await replayFromLedger(fromLedger); res.status(202).json({ ok: true, replayingFrom: fromLedger, requestId }); - } catch (err) { + } catch (_err) { res.status(500).json({ error: 'Replay failed' }); } }); diff --git a/backend/src/routes/v1/stream.routes.ts b/backend/src/routes/v1/stream.routes.ts index cb6c39eb..d2f27f6e 100644 --- a/backend/src/routes/v1/stream.routes.ts +++ b/backend/src/routes/v1/stream.routes.ts @@ -259,7 +259,7 @@ router.post('/:streamId/resume', requireAuth, resumeStream); * 409: * description: Conflict - no claimable balance available */ -router.post('/:streamId/withdraw', requireAuth, withdrawHandler as any); +router.post('/:streamId/withdraw', requireAuth, withdrawHandler); /** * @openapi @@ -315,6 +315,6 @@ router.post('/:streamId/withdraw', requireAuth, withdrawHandler as any); * description: Stream not found */ router.post('/:streamId/top-up', requireAuth, topUpStreamHandler); -router.post('/:streamId/cancel', requireAuth, cancelStreamHandler as any); +router.post('/:streamId/cancel', requireAuth, cancelStreamHandler); export default router; diff --git a/backend/src/routes/v1/streams/withdraw.ts b/backend/src/routes/v1/streams/withdraw.ts index 5988eb05..cbc1b2e3 100644 --- a/backend/src/routes/v1/streams/withdraw.ts +++ b/backend/src/routes/v1/streams/withdraw.ts @@ -1,4 +1,4 @@ -import type { Response } from 'express'; +import type { Request, Response } from 'express'; import { prisma } from '../../../lib/prisma.js'; import logger from '../../../logger.js'; import { claimableAmountService } from '../../../services/claimable.service.js'; @@ -52,8 +52,9 @@ import { parseStreamId } from '../../../lib/stream-id.js'; * 409: * description: Conflict - no claimable balance available */ -export const withdrawHandler = async (req: AuthenticatedRequest, res: Response) => { +export const withdrawHandler = async (req: Request, res: Response) => { try { + const authReq = req as AuthenticatedRequest; const streamIdParam = Array.isArray(req.params.streamId) ? req.params.streamId[0] : req.params.streamId; @@ -87,7 +88,7 @@ export const withdrawHandler = async (req: AuthenticatedRequest, res: Response) } // Verify the caller is the stream recipient - if (stream.recipient !== req.user.publicKey) { + if (stream.recipient !== authReq.user.publicKey) { return res.status(403).json({ error: 'Forbidden', message: 'Only the stream recipient can withdraw from the stream', @@ -105,7 +106,7 @@ export const withdrawHandler = async (req: AuthenticatedRequest, res: Response) try { // Call Soroban service - const result = await sorobanWithdraw(parsedStreamId, req.user.publicKey); + const result = await sorobanWithdraw(parsedStreamId, authReq.user.publicKey); const now = BigInt(Math.floor(Date.now() / 1000)); const nextWithdrawnAmount = ( @@ -138,12 +139,12 @@ export const withdrawHandler = async (req: AuthenticatedRequest, res: Response) transactionHash: result.txHash, ledgerSequence: 0, timestamp: now, - metadata: JSON.stringify({ withdrawnBy: req.user.publicKey }), + metadata: JSON.stringify({ withdrawnBy: authReq.user.publicKey }), }, update: {}, }); - logger.info(`Stream ${parsedStreamId} withdrawn by ${req.user.publicKey}`); + logger.info(`Stream ${parsedStreamId} withdrawn by ${authReq.user.publicKey}`); return res.status(200).json({ success: true, diff --git a/backend/src/services/export.service.ts b/backend/src/services/export.service.ts index 7199f682..40362434 100644 --- a/backend/src/services/export.service.ts +++ b/backend/src/services/export.service.ts @@ -3,6 +3,7 @@ * Generates CSV/JSON exports for bookkeeping and tax filings */ import type { Response } from "express"; +import type { Prisma } from "../generated/prisma/index.js"; import { prisma } from "../lib/prisma.js"; import logger from "../logger.js"; @@ -68,12 +69,28 @@ function calculateProtocolFee(amount: string, eventType: string): string { return "0"; } +/** Structural view of the StreamEvent fields used for export. */ +interface ExportEvent { + amount: string | null; + eventType: string; + timestamp: bigint; + transactionHash: string; +} + +/** Structural view of the Stream fields used for export. */ +interface ExportStream { + streamId: bigint; + sender: string; + recipient: string; + tokenAddress: string; +} + /** * Convert database records to transaction export format */ function mapEventToTransaction( - event: any, - stream: any, + event: ExportEvent, + stream: ExportStream, userAddress: string, ): TransactionRecord { const isOutgoing = stream.sender === userAddress; @@ -136,15 +153,14 @@ export async function streamTransactionCSV( const { direction, startDate, endDate, tokenAddress } = options; // Build where clause - const where: any = { - OR: [], - }; + const or: Prisma.StreamWhereInput[] = []; + const where: Prisma.StreamWhereInput = { OR: or }; if (direction === "outgoing" || direction === "all") { - where.OR.push({ sender: userAddress }); + or.push({ sender: userAddress }); } if (direction === "incoming" || direction === "all") { - where.OR.push({ recipient: userAddress }); + or.push({ recipient: userAddress }); } if (tokenAddress) { @@ -237,15 +253,14 @@ export async function streamTransactionJSON( const { direction, startDate, endDate, tokenAddress } = options; // Build where clause - const where: any = { - OR: [], - }; + const or: Prisma.StreamWhereInput[] = []; + const where: Prisma.StreamWhereInput = { OR: or }; if (direction === "outgoing" || direction === "all") { - where.OR.push({ sender: userAddress }); + or.push({ sender: userAddress }); } if (direction === "incoming" || direction === "all") { - where.OR.push({ recipient: userAddress }); + or.push({ recipient: userAddress }); } if (tokenAddress) { diff --git a/backend/src/services/webhook.service.ts b/backend/src/services/webhook.service.ts index fe1b7443..a92a81cb 100644 --- a/backend/src/services/webhook.service.ts +++ b/backend/src/services/webhook.service.ts @@ -17,7 +17,7 @@ export interface WebhookSubscription { export interface WebhookPayload { eventType: string; - data: Record; + data: Record; timestamp: string; } @@ -238,8 +238,8 @@ export async function deliverWebhook( logger.warn( `[Webhook] Delivery failed with status ${lastStatus} (attempt ${attempt + 1}/${MAX_RETRY_ATTEMPTS})`, ); - } catch (error: any) { - lastError = error.message || "Network error"; + } catch (error: unknown) { + lastError = error instanceof Error ? error.message : "Network error"; logger.warn( `[Webhook] Delivery error: ${lastError} (attempt ${attempt + 1}/${MAX_RETRY_ATTEMPTS})`, ); @@ -304,7 +304,7 @@ async function checkConsecutiveFailures(subscriptionId: string): Promise { */ export async function dispatchWebhookEvent( eventType: string, - data: Record, + data: Record, userAddresses: string[], ): Promise { const subscriptions = await prisma.webhookSubscription.findMany({ diff --git a/backend/src/workers/soroban-event-worker.ts b/backend/src/workers/soroban-event-worker.ts index 495e2539..957891a6 100644 --- a/backend/src/workers/soroban-event-worker.ts +++ b/backend/src/workers/soroban-event-worker.ts @@ -384,7 +384,7 @@ export class SorobanEventWorker { // Use the response's final cursor if provided and no error occurred, otherwise the last valid event's ID const finalCursor = hasError ? lastCursor - : ((response as any).latestCursor || lastCursor); + : ((response as { latestCursor?: string }).latestCursor || lastCursor); await prisma.indexerState.upsert({ where: { id: INDEXER_STATE_ID }, diff --git a/backend/src/workers/stream-runway-worker.ts b/backend/src/workers/stream-runway-worker.ts index 27417ca1..68c0cd56 100644 --- a/backend/src/workers/stream-runway-worker.ts +++ b/backend/src/workers/stream-runway-worker.ts @@ -2,6 +2,7 @@ * Stream Runway & Low-Balance Alert Engine (Issue #1190) * Monitors active streams and generates proactive notifications before funds run out */ +import type { Stream } from "../generated/prisma/index.js"; import { prisma } from "../lib/prisma.js"; import logger from "../logger.js"; import { claimableAmountService } from "../services/claimable.service.js"; @@ -23,7 +24,7 @@ interface RunwayCalculation { /** * Calculate remaining runway for a stream in seconds */ -function calculateStreamRunway(stream: any, now: number): RunwayCalculation { +function calculateStreamRunway(stream: Stream, now: number): RunwayCalculation { const result = claimableAmountService.getClaimableAmount( { streamId: stream.streamId, diff --git a/package-lock.json b/package-lock.json index e4a14c96..49fffd07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "zod": "^4.4.3" }, "devDependencies": { + "@eslint/js": "^9.39.3", "@types/cors": "^2.8.19", "@types/eventsource": "^1.1.15", "@types/express": "^5.0.6", @@ -56,6 +57,7 @@ "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", "@vitest/coverage-v8": "^3.2.7", + "eslint": "^9.39.3", "eventsource": "^2.0.2", "nodemon": "^3.1.11", "prisma": "^7.4.1", @@ -63,6 +65,7 @@ "ts-node": "^10.9.2", "tsx": "^4.19.2", "typescript": "^5.9.3", + "typescript-eslint": "^8.56.1", "vitest": "^3.2.4" } }, @@ -454,7 +457,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -464,7 +467,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -498,7 +501,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -558,7 +561,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -9243,7 +9246,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.25.4", diff --git a/package.json b/package.json index c63c7fb5..e71aeef9 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,9 @@ "lint-staged": { "frontend/**/*.{ts,tsx,js,jsx}": [ "npm run lint --workspace=frontend --" + ], + "backend/**/*.{ts,tsx,js}": [ + "npm run lint --workspace=backend --" ] }, "dependencies": { From dbd0e75613c28e4305802941975450fc40debb6b Mon Sep 17 00:00:00 2001 From: Oluwafemi Ajibade Date: Sun, 30 Aug 2026 15:01:07 +0000 Subject: [PATCH 2/5] fix: satisfy backend ESLint by typing topUp catch as unknown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend ESLint gate added in this branch flags `catch (error: any)` as an error. The handler already narrows via `error instanceof Error`, so drop the `any` annotation and rely on the default `unknown` catch type. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- backend/src/controllers/stream.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/controllers/stream.controller.ts b/backend/src/controllers/stream.controller.ts index 5a5514dd..cebd2651 100644 --- a/backend/src/controllers/stream.controller.ts +++ b/backend/src/controllers/stream.controller.ts @@ -770,7 +770,7 @@ export const topUpStreamHandler = async (req: Request, res: Response) => { return res .status(200) .json({ streamId, txHash, depositedAmount: updatedStream!.depositedAmount }); - } catch (error: any) { + } catch (error) { logger.error(`[topUp] stream=${streamId} error:`, error); return res.status(400).json({ error: 'Failed to top up stream on chain', message: error instanceof Error ? error.message : 'Unknown error' }); } From 86243881457dee2414fd5efaee688cd14fa2286a Mon Sep 17 00:00:00 2001 From: Oluwafemi Ajibade Date: Tue, 1 Sep 2026 22:06:16 +0000 Subject: [PATCH 3/5] fix(backend): resolve ESLint errors from #1278 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove duplicate MemoryCache class fragment in redis.ts and use unknown instead of any - Drop unused rateLimit import in stream-rate-limiter middleware - Remove dup cancel route registration left by merge and drop as any - Fix broken ternary leftover in soroban-event-worker - Pin prisma's mysql2 dep via override and sync package-lock Generated with Codebuff 🤖 Co-Authored-By: Codebuff --- backend/src/lib/redis.ts | 7 +- .../stream-rate-limiter.middleware.ts | 2 +- backend/src/routes/v1/stream.routes.ts | 3 +- backend/src/workers/soroban-event-worker.ts | 1 - package-lock.json | 124 +----------------- package.json | 5 +- 6 files changed, 14 insertions(+), 128 deletions(-) diff --git a/backend/src/lib/redis.ts b/backend/src/lib/redis.ts index 6a820e09..e20b7494 100644 --- a/backend/src/lib/redis.ts +++ b/backend/src/lib/redis.ts @@ -24,8 +24,6 @@ interface CacheItem { createdAt: number; } -class MemoryCache { - private cache = new Map>(); interface MemoryCacheOptions { /** * Hard cap on the number of entries kept in memory. When exceeded, the @@ -43,7 +41,7 @@ const DEFAULT_MEMORY_CACHE_MAX_ITEMS = 10_000; * memory usage stays bounded regardless of key churn or sweep interval. */ export class MemoryCache { - private cache = new Map>(); + private cache = new Map>(); private hits = 0; private misses = 0; private readonly maxItems: number; @@ -72,12 +70,11 @@ export class MemoryCache { return null; } this.hits++; - return item.value as T; // Refresh LRU recency: re-insert at the end of the Map so this entry is // the last candidate for eviction when the max-size cap is hit. this.cache.delete(key); this.cache.set(key, item); - return item.value; + return item.value as T; } set(key: string, value: T, ttlSeconds: number): void { diff --git a/backend/src/middleware/stream-rate-limiter.middleware.ts b/backend/src/middleware/stream-rate-limiter.middleware.ts index 429e118c..df5d12c3 100644 --- a/backend/src/middleware/stream-rate-limiter.middleware.ts +++ b/backend/src/middleware/stream-rate-limiter.middleware.ts @@ -1,4 +1,4 @@ -import { rateLimit, type Options } from 'express-rate-limit'; +import { type Options } from 'express-rate-limit'; import { createRateLimiter } from './rate-limiter.middleware.js'; import { type Request, type Response, type NextFunction } from 'express'; import type { AuthenticatedRequest } from '../types/auth.types.js'; diff --git a/backend/src/routes/v1/stream.routes.ts b/backend/src/routes/v1/stream.routes.ts index 7a3bb104..7d6c06c2 100644 --- a/backend/src/routes/v1/stream.routes.ts +++ b/backend/src/routes/v1/stream.routes.ts @@ -656,7 +656,6 @@ router.post('/:streamId/withdraw', requireAuth, withdrawHandler); * $ref: '#/components/schemas/Error' */ router.post('/:streamId/top-up', requireAuth, topUpStreamHandler); -router.post('/:streamId/cancel', requireAuth, cancelStreamHandler); /** * @openapi @@ -719,6 +718,6 @@ router.post('/:streamId/cancel', requireAuth, cancelStreamHandler); * schema: * $ref: '#/components/schemas/Error' */ -router.post('/:streamId/cancel', requireAuth, cancelStreamHandler as any); +router.post('/:streamId/cancel', requireAuth, cancelStreamHandler); export default router; diff --git a/backend/src/workers/soroban-event-worker.ts b/backend/src/workers/soroban-event-worker.ts index c07e7653..2e91b1e3 100644 --- a/backend/src/workers/soroban-event-worker.ts +++ b/backend/src/workers/soroban-event-worker.ts @@ -396,7 +396,6 @@ export class SorobanEventWorker { const finalCursor = hasError ? lastCursor : ((response as { latestCursor?: string }).latestCursor || lastCursor); - : (response as any).latestCursor || lastCursor; await prisma.indexerState.upsert({ where: { id: INDEXER_STATE_ID }, diff --git a/package-lock.json b/package-lock.json index b2dfd6c8..3260df6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -462,7 +462,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -472,7 +472,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -506,7 +506,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.8" @@ -598,7 +598,7 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -4191,7 +4191,7 @@ "version": "19.2.5", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -5628,16 +5628,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/aws-ssl-profiles": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", - "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/axe-core": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", @@ -8269,16 +8259,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "is-property": "^1.0.2" - } - }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -9232,13 +9212,6 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "devOptional": true, - "license": "MIT" - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -9787,7 +9760,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9849,7 +9821,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9871,7 +9842,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9893,7 +9863,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9915,7 +9884,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9957,7 +9925,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9979,7 +9946,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -10001,7 +9967,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -10023,7 +9988,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -10045,7 +10009,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -10067,7 +10030,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -10153,13 +10115,6 @@ "node": ">= 12.0.0" } }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "devOptional": true, - "license": "Apache-2.0" - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -10190,22 +10145,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lru.min": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", - "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", - "devOptional": true, - "license": "MIT", - "engines": { - "bun": ">=1.0.0", - "deno": ">=1.30.0", - "node": ">=8.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wellwelwel" - } - }, "node_modules/lucide-react": { "version": "0.575.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.575.0.tgz", @@ -10239,7 +10178,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.25.4", @@ -10444,40 +10383,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/mysql2": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", - "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "aws-ssl-profiles": "^1.1.1", - "denque": "^2.1.0", - "generate-function": "^2.3.1", - "iconv-lite": "^0.7.0", - "long": "^5.2.1", - "lru.min": "^1.0.0", - "named-placeholders": "^1.1.3", - "seq-queue": "^0.0.5", - "sqlstring": "^2.3.2" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/named-placeholders": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", - "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "lru.min": "^1.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.18", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", @@ -11341,7 +11246,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -12226,12 +12130,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/seq-queue": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", - "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", - "devOptional": true - }, "node_modules/serve-static": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", @@ -12524,16 +12422,6 @@ "node": ">= 10.x" } }, - "node_modules/sqlstring": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", - "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", diff --git a/package.json b/package.json index c3c8eb15..c6e8c9a7 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,9 @@ "sharp": "^0.35.0", "deepmerge-ts": "^8.0.0", "react": "19.2.7", - "react-dom": "19.2.7" + "react-dom": "19.2.7", + "prisma": { + "mysql2": "^3.22.0" + } } } From 9f09280cc37b128e74954b012c840849351d0380 Mon Sep 17 00:00:00 2001 From: Oluwafemi Ajibade Date: Thu, 3 Sep 2026 09:11:14 +0000 Subject: [PATCH 4/5] fix: restore valid JSON in root package.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit 48f46eb (mysql2 pin) dropped a comma after the prisma override block, which broke npm ci on every CI job. Restore it. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8313a6ef..b9af72ad 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "react-dom": "19.2.7", "prisma": { "mysql2": "^3.22.0" - } + }, "mysql2": "^3.22.0" } } From daac6e02ddff92b8e072c2ddc2923e03965790b7 Mon Sep 17 00:00:00 2001 From: Oluwafemi Ajibade Date: Thu, 3 Sep 2026 09:40:27 +0000 Subject: [PATCH 5/5] fix: repair overrides block broken by merge conflict resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merge of main into fix/1278-backend-eslint jammed both mysql2 override specs (^3.22.0 and >=3.22.0) together without a comma, making package.json invalid JSON and breaking npm ci on every CI job. Keep the prisma-scoped override from the branch and main's >=3.22.0 top-level pin. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index aa006cfc..321195a9 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "prisma": { "mysql2": "^3.22.0" }, - "mysql2": "^3.22.0" "mysql2": ">=3.22.0" } }