A third party ATProto appview

Refactor: Improve type safety and error handling

This commit enhances type safety across the codebase by using more specific types and improves error handling by catching and logging errors more effectively. It also removes unused imports and code, and refactors API request logic for better maintainability.

Co-authored-by: dollspacegay <dollspacegay@gmail.com>

+1 -1
client/src/components/firehose-status.tsx
··· 1 1 import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 2 2 import { Button } from '@/components/ui/button'; 3 - import { CheckCircle2, XCircle } from 'lucide-react'; 3 + import { XCircle } from 'lucide-react'; 4 4 5 5 interface FirehoseStatusProps { 6 6 connected: boolean;
+1 -1
client/src/components/logs-panel.tsx
··· 8 8 timestamp: string; 9 9 level: 'INFO' | 'SUCCESS' | 'WARNING' | 'ERROR' | 'EVENT'; 10 10 message: string; 11 - metadata?: Record<string, any>; 11 + metadata?: Record<string, unknown>; 12 12 } 13 13 14 14 const getLevelColor = (level: LogEntry['level']) => {
-8
client/src/components/osprey-status.tsx
··· 319 319 </Card> 320 320 ); 321 321 } 322 - 323 - function formatUptime(seconds: number): string { 324 - if (seconds < 60) return `${seconds}s`; 325 - if (seconds < 3600) return `${Math.floor(seconds / 60)}m`; 326 - if (seconds < 86400) 327 - return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`; 328 - return `${Math.floor(seconds / 86400)}d ${Math.floor((seconds % 86400) / 3600)}h`; 329 - }
+4 -2
client/src/components/pds-fetcher-status.tsx
··· 27 27 const fetchStats = async () => { 28 28 try { 29 29 setLoading(true); 30 - const response = await api.get('/api/admin/pds-fetcher/stats'); 31 - setStats((response as any).stats); 30 + const response = await api.get<{ stats: PDSFetcherStats }>( 31 + '/api/admin/pds-fetcher/stats' 32 + ); 33 + setStats(response.stats); 32 34 } catch (error) { 33 35 console.error('Failed to fetch PDS fetcher stats:', error); 34 36 } finally {
-1
client/src/components/sidebar.tsx
··· 3 3 Activity, 4 4 Database, 5 5 Terminal, 6 - Settings, 7 6 FileText, 8 7 Zap, 9 8 BookOpen,
+4 -13
client/src/hooks/use-toast.ts
··· 12 12 action?: ToastActionElement; 13 13 }; 14 14 15 - const actionTypes = { 16 - ADD_TOAST: 'ADD_TOAST', 17 - UPDATE_TOAST: 'UPDATE_TOAST', 18 - DISMISS_TOAST: 'DISMISS_TOAST', 19 - REMOVE_TOAST: 'REMOVE_TOAST', 20 - } as const; 21 - 22 15 let count = 0; 23 16 24 17 function genId() { 25 18 count = (count + 1) % Number.MAX_SAFE_INTEGER; 26 19 return count.toString(); 27 20 } 28 - 29 - type ActionType = typeof actionTypes; 30 21 31 22 type Action = 32 23 | { 33 - type: ActionType['ADD_TOAST']; 24 + type: 'ADD_TOAST'; 34 25 toast: ToasterToast; 35 26 } 36 27 | { 37 - type: ActionType['UPDATE_TOAST']; 28 + type: 'UPDATE_TOAST'; 38 29 toast: Partial<ToasterToast>; 39 30 } 40 31 | { 41 - type: ActionType['DISMISS_TOAST']; 32 + type: 'DISMISS_TOAST'; 42 33 toastId?: ToasterToast['id']; 43 34 } 44 35 | { 45 - type: ActionType['REMOVE_TOAST']; 36 + type: 'REMOVE_TOAST'; 46 37 toastId?: ToasterToast['id']; 47 38 }; 48 39
+14 -10
client/src/lib/api.ts
··· 69 69 fetchCSRFToken(); 70 70 71 71 // --- Main API Request Logic --- 72 - const request = async ( 72 + const request = async <T = unknown>( 73 73 method: 'GET' | 'POST' | 'PUT' | 'DELETE', 74 74 url: string, 75 - body?: any, 75 + body?: unknown, 76 76 retryCount = 0 77 - ): Promise<any> => { 77 + ): Promise<T> => { 78 78 const csrf = await fetchCSRFToken(); 79 79 80 80 const headers: Record<string, string> = { ··· 114 114 await refreshCSRFToken(); 115 115 return request(method, url, body, retryCount + 1); 116 116 } 117 - } catch (e) { 117 + } catch { 118 118 // If we can't parse the error, continue with normal error handling 119 119 } 120 120 } ··· 124 124 queryClient.invalidateQueries({ queryKey: ['/api/auth/session'] }); 125 125 } 126 126 127 - const error: any = new Error(`HTTP error! status: ${response.status}`); 127 + const error = new Error( 128 + `HTTP error! status: ${response.status}` 129 + ) as Error & { data?: unknown }; 128 130 try { 129 131 error.data = await response.json(); 130 - } catch (e) { 132 + } catch { 131 133 error.data = { message: 'Could not parse error response.' }; 132 134 } 133 135 throw error; ··· 141 143 }; 142 144 143 145 const api = { 144 - get: <T>(url: string): Promise<T> => request('GET', url), 145 - post: <T>(url: string, body: any): Promise<T> => request('POST', url, body), 146 - put: <T>(url: string, body: any): Promise<T> => request('PUT', url, body), 147 - delete: <T>(url: string): Promise<T> => request('DELETE', url), 146 + get: <T = unknown>(url: string): Promise<T> => request<T>('GET', url), 147 + post: <T = unknown>(url: string, body: unknown): Promise<T> => 148 + request<T>('POST', url, body), 149 + put: <T = unknown>(url: string, body: unknown): Promise<T> => 150 + request<T>('PUT', url, body), 151 + delete: <T = unknown>(url: string): Promise<T> => request<T>('DELETE', url), 148 152 // Expose refresh function for manual CSRF token refresh if needed 149 153 refreshCSRFToken, 150 154 };
+8 -6
client/src/lib/queryClient.ts
··· 10 10 try { 11 11 const data = await api.get<T>(url); 12 12 return data; 13 - } catch (error: any) { 14 - if (options?.on401 === 'returnNull' && error.response?.status === 401) { 13 + } catch (error: unknown) { 14 + const err = error as { response?: { status?: number } }; 15 + if (options?.on401 === 'returnNull' && err.response?.status === 401) { 15 16 return null as T; 16 17 } 17 18 // Re-throw other errors to be handled by React Query ··· 26 27 refetchInterval: false, 27 28 refetchOnWindowFocus: false, 28 29 staleTime: Infinity, 29 - retry: (failureCount, error: any) => { 30 + retry: (failureCount, error: unknown) => { 31 + const err = error as { response?: { status?: number } }; 30 32 if ( 31 - error.response?.status === 401 || 32 - error.response?.status === 403 || 33 - error.response?.status === 404 33 + err.response?.status === 401 || 34 + err.response?.status === 403 || 35 + err.response?.status === 404 34 36 ) { 35 37 return false; 36 38 }
+20 -24
client/src/pages/admin-moderation.tsx
··· 28 28 AlertCircle, 29 29 LogIn, 30 30 LogOut, 31 - RefreshCw, 32 - Zap, 33 31 } from 'lucide-react'; 34 32 import { api } from '@/lib/api'; 35 33 import { PDSFetcherStatus } from '@/components/pds-fetcher-status'; ··· 51 49 description: string; 52 50 } 53 51 52 + interface MetricsData { 53 + firehoseStatus?: { 54 + connected: boolean; 55 + }; 56 + eventCounts?: { 57 + '#commit': number; 58 + '#identity': number; 59 + '#account': number; 60 + }; 61 + errorRate?: number; 62 + } 63 + 54 64 export default function AdminControlPanel() { 55 65 const { toast } = useToast(); 56 66 const queryClient = useQueryClient(); ··· 99 109 } 100 110 // Redirect to PDS for OAuth authorization 101 111 window.location.href = data.authUrl; 102 - } catch (error) { 112 + } catch { 103 113 toast({ 104 114 title: 'Login Failed', 105 115 description: 'Invalid authentication URL returned from server', ··· 129 139 }); 130 140 131 141 // Fetch firehose status 132 - const { data: metrics } = useQuery({ 142 + const { data: metrics } = useQuery<MetricsData>({ 133 143 queryKey: ['/api/metrics'], 134 144 refetchInterval: 5000, 135 145 }); ··· 137 147 // Update firehose stats when metrics change 138 148 useEffect(() => { 139 149 if (metrics) { 140 - const metricsData = metrics as any; 141 - setFirehoseConnected(metricsData.firehoseStatus?.connected || false); 150 + setFirehoseConnected(metrics.firehoseStatus?.connected || false); 142 151 setFirehoseStats({ 143 - commits: metricsData.eventCounts?.['#commit'] || 0, 144 - identity: metricsData.eventCounts?.['#identity'] || 0, 145 - account: metricsData.eventCounts?.['#account'] || 0, 146 - errorRate: metricsData.errorRate || 0, 152 + commits: metrics.eventCounts?.['#commit'] || 0, 153 + identity: metrics.eventCounts?.['#identity'] || 0, 154 + account: metrics.eventCounts?.['#account'] || 0, 155 + errorRate: metrics.errorRate || 0, 147 156 }); 148 157 } 149 158 }, [metrics]); ··· 156 165 title: 'Reconnect Initiated', 157 166 description: 'Attempting to reconnect to firehose...', 158 167 }); 159 - } catch (error) { 168 + } catch { 160 169 toast({ 161 170 title: 'Reconnect Failed', 162 171 description: 'Failed to reconnect to firehose', ··· 245 254 const handleSearch = () => { 246 255 if (searchQuery) { 247 256 refetchLabels(); 248 - } 249 - }; 250 - 251 - const getLabelColor = (reason: string) => { 252 - switch (reason) { 253 - case 'legal': 254 - return 'destructive'; 255 - case 'safety': 256 - return 'default'; 257 - case 'quality': 258 - return 'secondary'; 259 - default: 260 - return 'outline'; 261 257 } 262 258 }; 263 259
+12 -7
client/src/pages/dashboard.tsx
··· 13 13 import AdminControlPanel from '@/pages/admin-moderation'; 14 14 import UserPanel from '@/pages/user-panel'; 15 15 import LoginPage from '@/pages/login'; 16 - import { useLocation, useSearch } from 'wouter'; 17 - import { useQuery, useQueryClient } from '@tanstack/react-query'; 16 + import { useLocation } from 'wouter'; 17 + import { useQuery } from '@tanstack/react-query'; 18 18 import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 19 19 import { ShieldAlert } from 'lucide-react'; 20 20 import { api } from '@/lib/api'; ··· 49 49 } 50 50 51 51 export default function Dashboard() { 52 - const [location, setLocation] = useLocation(); 53 - const queryClient = useQueryClient(); 52 + const [location] = useLocation(); 54 53 55 54 // SECURITY: We now use HttpOnly cookies for authentication 56 55 // No need to handle tokens in URL or localStorage ··· 79 78 lastUpdate: new Date().toISOString(), 80 79 }); 81 80 82 - const [events, setEvents] = useState<any[]>([]); 81 + interface EventData { 82 + type: string; 83 + timestamp: string; 84 + [key: string]: unknown; 85 + } 86 + 87 + const [events, setEvents] = useState<EventData[]>([]); 83 88 84 89 // Initial metrics fetch (SSE stream will update in real-time after connection) 85 90 useEffect(() => { ··· 97 102 98 103 // Real-time event stream via Server-Sent Events (SSE) 99 104 useEffect(() => { 100 - const recentEvents: any[] = []; 105 + const recentEvents: EventData[] = []; 101 106 let eventSource: EventSource | null = null; 102 107 103 108 // Fetch initial events from API 104 109 const fetchInitialEvents = async () => { 105 110 try { 106 - const data = await api.get<any[]>('/api/events/recent'); 111 + const data = await api.get<EventData[]>('/api/events/recent'); 107 112 recentEvents.push(...data); 108 113 setEvents([...data.slice(0, 10)]); 109 114 } catch (error) {
+1 -1
client/src/pages/login.tsx
··· 17 17 // Sanitize user input to prevent XSS attacks 18 18 function sanitizeText(text: string): string { 19 19 return text 20 - .replace(/[<>\"']/g, '') // Remove potentially dangerous characters 20 + .replace(/[<>"']/g, '') // Remove potentially dangerous characters 21 21 .substring(0, 500); // Limit length to prevent abuse 22 22 } 23 23
-1
client/src/pages/user-panel.tsx
··· 11 11 import { Label } from '@/components/ui/label'; 12 12 import { Input } from '@/components/ui/input'; 13 13 import { Switch } from '@/components/ui/switch'; 14 - import { Badge } from '@/components/ui/badge'; 15 14 import { useToast } from '@/hooks/use-toast'; 16 15 import { 17 16 User,
+1 -1
microcosm-bridge/constellation-client/src/api-client.ts
··· 120 120 } else { 121 121 throw new Error('JSON response missing total field'); 122 122 } 123 - } catch (jsonError) { 123 + } catch { 124 124 // Fall back to plain text number (old API format) 125 125 count = parseInt(text.trim(), 10); 126 126 }
+2 -2
microcosm-bridge/constellation-client/src/enricher.ts
··· 5 5 * Includes Redis caching to minimize API calls and improve performance. 6 6 */ 7 7 8 - import { ConstellationAPIClient, LinksCounts } from './api-client.js'; 8 + import { ConstellationAPIClient } from './api-client.js'; 9 9 import Redis from 'ioredis'; 10 10 11 11 interface PostStats { ··· 94 94 /** 95 95 * Set value in cache 96 96 */ 97 - private async setInCache(key: string, value: any): Promise<void> { 97 + private async setInCache(key: string, value: unknown): Promise<void> { 98 98 if (!this.cache || !this.cacheEnabled) { 99 99 return; 100 100 }
+1 -1
microcosm-bridge/constellation-client/src/health.ts
··· 87 87 try { 88 88 const healthy = await this.apiClient.healthCheck(); 89 89 res.status(healthy ? 200 : 503).send(healthy ? 'ready' : 'not ready'); 90 - } catch (error) { 90 + } catch { 91 91 res.status(503).send('not ready'); 92 92 } 93 93 });
+1 -1
osprey-bridge/firehose-to-kafka/src/adapters/base-adapter.ts
··· 30 30 export interface AdapterEvent { 31 31 type: 'commit' | 'identity' | 'account'; 32 32 did: string; 33 - data: any; 33 + data: unknown; 34 34 seq?: string; 35 35 }
+4 -4
osprey-bridge/firehose-to-kafka/src/adapters/firehose-adapter.ts
··· 5 5 6 6 // Make WebSocket available globally for @skyware/firehose in Node.js environment 7 7 if (typeof globalThis.WebSocket === 'undefined') { 8 - (global as any).WebSocket = WebSocket; 8 + (global as typeof WebSocket & { prototype: WebSocket }).WebSocket = WebSocket; 9 9 } 10 10 11 11 export interface FirehoseAdapterConfig { ··· 39 39 const savedCursor = this.loadCursor(); 40 40 41 41 // Create firehose client 42 - const firehoseOptions: any = { 42 + const firehoseOptions: { relay: string; cursor?: string } = { 43 43 relay: this.config.url, 44 44 }; 45 45 ··· 64 64 data: { 65 65 repo: commit.repo, 66 66 ops: commit.ops.map((op) => { 67 - const baseOp: any = { 67 + const baseOp: Record<string, unknown> = { 68 68 action: op.action, 69 69 path: op.path, 70 70 }; ··· 113 113 console.log(`[${this.getName()}] Disconnected from firehose`); 114 114 }); 115 115 116 - this.firehose.on('error', (error: any) => { 116 + this.firehose.on('error', (error: unknown) => { 117 117 console.error(`[${this.getName()}] Firehose error:`, error); 118 118 }); 119 119
+6 -4
osprey-bridge/firehose-to-kafka/src/adapters/redis-adapter.ts
··· 154 154 console.log( 155 155 `[${this.getName()}] Created consumer group: ${this.consumerGroup}` 156 156 ); 157 - } catch (error: any) { 158 - if (error.message.includes('BUSYGROUP')) { 157 + } catch (error: unknown) { 158 + const err = error as Error; 159 + if (err.message.includes('BUSYGROUP')) { 159 160 console.log( 160 161 `[${this.getName()}] Consumer group already exists: ${this.consumerGroup}` 161 162 ); ··· 276 277 } 277 278 278 279 return events; 279 - } catch (error: any) { 280 + } catch (error: unknown) { 281 + const err = error as Error; 280 282 // Handle READONLY errors specifically 281 - if (error.message && error.message.includes('READONLY')) { 283 + if (err.message && err.message.includes('READONLY')) { 282 284 console.error( 283 285 `[${this.getName()}] READONLY error - Redis is configured as a read-only replica. XREADGROUP requires write access. Please connect to the master Redis instance.` 284 286 );
+7 -3
osprey-bridge/firehose-to-kafka/src/event-enricher.ts
··· 12 12 action: 'create' | 'update' | 'delete'; 13 13 path: string; 14 14 cid?: string; 15 - record?: any; 15 + record?: unknown; 16 16 }>; 17 17 18 18 // Enriched metadata ··· 51 51 this.enrichWithHandles = options.enrichWithHandles ?? true; 52 52 } 53 53 54 - async enrich(event: any): Promise<EnrichedEvent> { 54 + async enrich(event: { 55 + type: string; 56 + seq?: string; 57 + data?: Record<string, unknown>; 58 + }): Promise<EnrichedEvent> { 55 59 const enriched: EnrichedEvent = { 56 60 type: event.type, 57 61 seq: event.seq, ··· 93 97 const author = result.rows[0]; 94 98 95 99 // Filter based on enrichment settings 96 - const enrichedAuthor: any = {}; 100 + const enrichedAuthor: Record<string, unknown> = {}; 97 101 98 102 if (this.enrichWithHandles && author.handle) { 99 103 enrichedAuthor.handle = author.handle;
+2 -2
osprey-bridge/firehose-to-kafka/src/health.ts
··· 76 76 77 77 const statusCode = health.status === 'healthy' ? 200 : 503; 78 78 res.status(statusCode).json(health); 79 - } catch (error) { 79 + } catch { 80 80 res.status(503).json({ 81 81 status: 'unhealthy', 82 82 error: 'Failed to get health status', ··· 95 95 .status(503) 96 96 .json({ ready: false, timestamp: new Date().toISOString() }); 97 97 } 98 - } catch (error) { 98 + } catch { 99 99 res.status(503).json({ 100 100 ready: false, 101 101 error: 'Failed to get readiness status',
+2 -2
osprey-bridge/firehose-to-kafka/src/kafka-producer.ts
··· 42 42 console.log('[KAFKA] Connected successfully'); 43 43 } 44 44 45 - async publishEvent(event: any): Promise<void> { 45 + async publishEvent(event: Record<string, unknown>): Promise<void> { 46 46 if (!this.isConnected) { 47 47 throw new Error('Kafka producer not connected'); 48 48 } ··· 65 65 } 66 66 } 67 67 68 - async publishBatch(events: any[]): Promise<void> { 68 + async publishBatch(events: Record<string, unknown>[]): Promise<void> { 69 69 if (!this.isConnected) { 70 70 throw new Error('Kafka producer not connected'); 71 71 }
+1 -1
osprey-bridge/label-effector/src/kafka-consumer.ts
··· 138 138 } 139 139 } 140 140 141 - private isValidLabel(data: any): boolean { 141 + private isValidLabel(data: unknown): boolean { 142 142 return ( 143 143 typeof data === 'object' && 144 144 typeof data.ver === 'number' &&
+1 -1
server/db.ts
··· 95 95 96 96 // For backwards compatibility, export a pool variable (though the actual pool is internal to drizzle) 97 97 // This is used by some legacy code that checks pool status 98 - export const pool = db as any; 98 + export const pool = db as unknown; 99 99 100 100 // Export main db connection 101 101 export { db };
+27 -19
server/index.ts
··· 5 5 import { setupVite, serveStatic, log } from './vite'; 6 6 import { logCollector } from './services/log-collector'; 7 7 import { cacheService } from './services/cache'; 8 - import { spawn } from 'child_process'; 9 8 10 9 const app = express(); 11 10 ··· 67 66 const bodyBuffer = Buffer.concat(chunks); 68 67 69 68 // Replicate the 'verify' functionality to store the raw body 70 - (req as any).rawBody = bodyBuffer; 69 + (req as Request & { rawBody?: Buffer }).rawBody = bodyBuffer; 71 70 72 71 if (bodyBuffer.length === 0) { 73 72 req.body = {}; ··· 175 174 app.use((req, res, next) => { 176 175 const start = Date.now(); 177 176 const path = req.path; 178 - let capturedJsonResponse: Record<string, any> | undefined = undefined; 177 + let capturedJsonResponse: Record<string, unknown> | undefined = undefined; 179 178 180 179 const originalResJson = res.json; 181 180 res.json = function (bodyJson, ...args) { ··· 211 210 212 211 const server = await registerRoutes(app); 213 212 214 - app.use((err: any, _req: Request, res: Response, _next: NextFunction) => { 215 - const status = err.status || err.statusCode || 500; 216 - const message = err.message || 'Internal Server Error'; 213 + app.use( 214 + ( 215 + err: Error & { status?: number; statusCode?: number }, 216 + _req: Request, 217 + res: Response, 218 + _next: NextFunction 219 + ) => { 220 + const status = err.status || err.statusCode || 500; 221 + const message = err.message || 'Internal Server Error'; 217 222 218 - // Log error for debugging 219 - console.error('[ERROR]', err); 220 - logCollector.error('Request error', { 221 - error: message, 222 - status, 223 - stack: err.stack, 224 - }); 223 + // Log error for debugging 224 + console.error('[ERROR]', err); 225 + logCollector.error('Request error', { 226 + error: message, 227 + status, 228 + stack: err.stack, 229 + }); 225 230 226 - res.status(status).json({ message }); 227 - // DO NOT throw after sending response - this would crash the server 228 - }); 231 + res.status(status).json({ message }); 232 + // DO NOT throw after sending response - this would crash the server 233 + } 234 + ); 229 235 230 236 // importantly only setup vite in development and after 231 237 // setting up all the other routes so the catch-all route ··· 273 279 }); 274 280 275 281 // Initialize data pruning service (if enabled) 276 - import('./services/data-pruning').then(({ dataPruningService }) => { 277 - // Service auto-initializes in its constructor 278 - }); 282 + import('./services/data-pruning').then( 283 + ({ dataPruningService: _dataPruningService }) => { 284 + // Service auto-initializes in its constructor 285 + } 286 + ); 279 287 280 288 // TypeScript backfill service is PERMANENTLY DISABLED 281 289 // Backfill functionality has been moved to Python (python-firehose/backfill_service.py)
+56 -40
server/routes.ts
··· 38 38 authLimiter, 39 39 oauthLimiter, 40 40 writeLimiter, 41 - searchLimiter, 42 41 apiLimiter, 43 42 xrpcLimiter, 44 43 adminLimiter, ··· 69 68 70 69 const originalSend = res.send; 71 70 72 - res.send = function (data: any) { 71 + res.send = function (data: unknown) { 73 72 const duration = Date.now() - startTime; 74 73 const success = res.statusCode >= 200 && res.statusCode < 400; 75 74 metricsService.recordEndpointRequest(req.path, duration, success); ··· 120 119 `); 121 120 122 121 if (stats.rows.length > 0) { 123 - const row: any = stats.rows[0]; 122 + const row = stats.rows[0] as { 123 + users?: string; 124 + posts?: string; 125 + likes?: string; 126 + reposts?: string; 127 + follows?: string; 128 + blocks?: string; 129 + }; 124 130 await Promise.all([ 125 131 redisQueue.incrementRecordCount('users', parseInt(row.users || '0')), 126 132 redisQueue.incrementRecordCount('posts', parseInt(row.posts || '0')), ··· 199 205 const { eventProcessor } = await import('./services/event-processor'); 200 206 const PARALLEL_PIPELINES = 5; // Run 5 concurrent consumer loops per worker 201 207 202 - const processEvent = async (event: any) => { 208 + interface RedisEvent { 209 + type: string; 210 + data: unknown; 211 + } 212 + const processEvent = async (event: RedisEvent) => { 203 213 let success = false; 204 214 try { 205 215 if (event.type === 'commit') { ··· 210 220 await eventProcessor.processAccount(event.data); 211 221 } 212 222 success = true; 213 - } catch (error: any) { 214 - if (error?.code === '23505' || error?.code === '23503') { 223 + } catch (error: unknown) { 224 + const err = error as { code?: string }; 225 + if (err?.code === '23505' || err?.code === '23503') { 215 226 // Duplicate key or foreign key violation - treat as success 216 227 success = true; 217 228 } else { ··· 337 348 ); 338 349 res.setHeader('Pragma', 'no-cache'); 339 350 res.send(didDoc); 340 - } catch (error) { 351 + } catch { 341 352 // If DID document doesn't exist, return a basic one based on APPVIEW_DID 342 353 const appviewDid = process.env.APPVIEW_DID; 343 354 if (!appviewDid) { ··· 345 356 } 346 357 const domain = appviewDid.replace('did:web:', ''); 347 358 const verificationKey = process.env.APPVIEW_ATPROTO_PUBKEY_MULTIBASE; 348 - const basicDidDoc: any = { 359 + const basicDidDoc: Record<string, unknown> = { 349 360 '@context': [ 350 361 'https://www.w3.org/ns/did/v1', 351 362 'https://w3id.org/security/multikey/v1', ··· 795 806 await storage.deleteSession(req.session.sessionId); 796 807 } 797 808 res.json({ success: true }); 798 - } catch (error) { 809 + } catch { 799 810 res.status(500).json({ error: 'Failed to logout' }); 800 811 } 801 812 } ··· 820 831 const isAdmin = await adminAuthService.isAdmin(session.userDid); 821 832 822 833 res.json({ session, user, isAdmin }); 823 - } catch (error) { 834 + } catch { 824 835 res.status(500).json({ error: 'Failed to get session' }); 825 836 } 826 837 }); ··· 1555 1566 console.log(`[API] Deleted like ${uri} for ${session.userDid}`); 1556 1567 1557 1568 res.json({ success: true }); 1558 - } catch (error) { 1569 + } catch { 1559 1570 res.status(400).json({ error: 'Failed to delete like' }); 1560 1571 } 1561 1572 } ··· 1719 1730 console.log(`[API] Deleted follow ${uri} for ${session.userDid}`); 1720 1731 1721 1732 res.json({ success: true }); 1722 - } catch (error) { 1733 + } catch { 1723 1734 res.status(400).json({ error: 'Failed to delete follow' }); 1724 1735 } 1725 1736 } ··· 1747 1758 } 1748 1759 1749 1760 res.json({ settings }); 1750 - } catch (error) { 1761 + } catch { 1751 1762 res.status(500).json({ error: 'Failed to get settings' }); 1752 1763 } 1753 1764 }); ··· 1874 1885 }); 1875 1886 1876 1887 res.json({ settings: updated }); 1877 - } catch (error) { 1888 + } catch { 1878 1889 res.status(400).json({ error: 'Failed to unblock keyword' }); 1879 1890 } 1880 1891 } ··· 1960 1971 }); 1961 1972 1962 1973 res.json({ settings: updated }); 1963 - } catch (error) { 1974 + } catch { 1964 1975 res.status(400).json({ error: 'Failed to unmute user' }); 1965 1976 } 1966 1977 } ··· 2058 2069 2059 2070 await labelService.removeLabel(uri); 2060 2071 res.json({ success: true }); 2061 - } catch (error) { 2072 + } catch { 2062 2073 res.status(400).json({ error: 'Failed to remove label' }); 2063 2074 } 2064 2075 } ··· 2068 2079 try { 2069 2080 const definitions = await labelService.getAllLabelDefinitions(); 2070 2081 res.json({ definitions }); 2071 - } catch (error) { 2082 + } catch { 2072 2083 res.status(500).json({ error: 'Failed to get label definitions' }); 2073 2084 } 2074 2085 }); ··· 2118 2129 const labels = await labelService.queryLabels(params); 2119 2130 2120 2131 res.json({ labels }); 2121 - } catch (error) { 2132 + } catch { 2122 2133 res.status(400).json({ error: 'Failed to query labels' }); 2123 2134 } 2124 2135 }); ··· 2368 2379 ); 2369 2380 const policy = instanceModerationService.getPublicPolicy(); 2370 2381 res.json(policy); 2371 - } catch (error) { 2382 + } catch { 2372 2383 res.status(500).json({ error: 'Failed to retrieve instance policy' }); 2373 2384 } 2374 2385 }); ··· 2381 2392 ); 2382 2393 const stats = await instanceModerationService.getStatistics(); 2383 2394 res.json(stats); 2384 - } catch (error) { 2395 + } catch { 2385 2396 res 2386 2397 .status(500) 2387 2398 .json({ error: 'Failed to retrieve moderation statistics' }); ··· 2464 2475 }, 2465 2476 timestamp: new Date().toISOString(), 2466 2477 }); 2467 - } catch (error) { 2478 + } catch { 2468 2479 res.status(500).json({ error: 'Failed to check Osprey status' }); 2469 2480 } 2470 2481 }); ··· 2482 2493 database: metrics, 2483 2494 connectionPool: poolStatus, 2484 2495 }); 2485 - } catch (error: any) { 2496 + } catch (error: unknown) { 2497 + const err = error as Error; 2486 2498 res.status(500).json({ 2487 2499 error: 'Health check failed', 2488 - message: error.message, 2500 + message: err.message, 2489 2501 }); 2490 2502 } 2491 2503 }); ··· 3271 3283 res.status(200).json({ 3272 3284 version: '0.0.1', 3273 3285 }); 3274 - } catch (error) { 3286 + } catch { 3275 3287 res.status(503).json({ 3276 3288 error: 'Service unavailable', 3277 3289 }); ··· 3297 3309 inviteCodeRequired: false, 3298 3310 phoneVerificationRequired: false, 3299 3311 }); 3300 - } catch (error) { 3312 + } catch { 3301 3313 res.status(500).json({ error: 'Failed to describe server' }); 3302 3314 } 3303 3315 }); ··· 3966 3978 const xrpcRoutes: Array<{ method: string; path: string }> = []; 3967 3979 3968 3980 // Access the Express router stack to find all XRPC routes 3969 - app._router.stack.forEach((middleware: any) => { 3970 - if (middleware.route) { 3971 - const path = middleware.route.path; 3972 - if (path.startsWith('/xrpc/')) { 3973 - const methods = Object.keys(middleware.route.methods); 3974 - methods.forEach((method) => { 3975 - xrpcRoutes.push({ 3976 - method: method.toUpperCase(), 3977 - path, 3981 + app._router.stack.forEach( 3982 + (middleware: { 3983 + route?: { path: string; methods: Record<string, boolean> }; 3984 + }) => { 3985 + if (middleware.route) { 3986 + const path = middleware.route.path; 3987 + if (path.startsWith('/xrpc/')) { 3988 + const methods = Object.keys(middleware.route.methods); 3989 + methods.forEach((method) => { 3990 + xrpcRoutes.push({ 3991 + method: method.toUpperCase(), 3992 + path, 3993 + }); 3978 3994 }); 3979 - }); 3995 + } 3980 3996 } 3981 3997 } 3982 - }); 3998 + ); 3983 3999 3984 4000 // Build endpoint list from discovered routes 3985 4001 const endpoints = xrpcRoutes.map((route) => { ··· 4068 4084 ); 4069 4085 4070 4086 // Subscribe to Redis event broadcasts (works on ALL workers) 4071 - const eventHandler = (event: any) => { 4087 + const eventHandler = (event: unknown) => { 4072 4088 if (res.writable) { 4073 4089 try { 4074 4090 res.write( ··· 4176 4192 const stats = contentFilter.getFilterStats(recentPosts, settings || null); 4177 4193 4178 4194 res.json({ stats }); 4179 - } catch (error) { 4195 + } catch { 4180 4196 res.status(500).json({ error: 'Failed to get filter stats' }); 4181 4197 } 4182 4198 }); ··· 4349 4365 } 4350 4366 4351 4367 // Subscribe to firehose events for this connection 4352 - const firehoseEventHandler = (event: any) => { 4368 + const firehoseEventHandler = (event: unknown) => { 4353 4369 if (ws.readyState === WebSocket.OPEN) { 4354 4370 try { 4355 4371 ws.send( ··· 4442 4458 }); 4443 4459 4444 4460 // Listen for label events from label service and broadcast to all connected clients 4445 - const broadcastLabelToClients = (label: any, eventId: number) => { 4461 + const broadcastLabelToClients = (label: unknown, eventId: number) => { 4446 4462 const message = JSON.stringify({ 4447 4463 seq: eventId, 4448 4464 labels: [
+29 -19
server/storage.ts
··· 91 91 type VideoJob, 92 92 type InsertVideoJob, 93 93 type FirehoseCursor, 94 - type InsertFirehoseCursor, 95 94 type Bookmark, 96 - insertBookmarkSchema, 97 95 type Quote, 98 96 type InsertQuote, 99 97 type Verification, ··· 111 109 type GenericRecord, 112 110 type InsertGenericRecord, 113 111 } from '@shared/schema'; 114 - import { db, pool, type DbConnection } from './db'; 112 + import { db, type DbConnection } from './db'; 115 113 import { eq, desc, and, or, sql, inArray, isNull } from 'drizzle-orm'; 116 114 import { encryptionService } from './services/encryption'; 117 115 import { sanitizeObject } from './utils/sanitize'; ··· 381 379 deleteCorruptedSessions(): Promise<number>; 382 380 383 381 // OAuth state operations 384 - saveOAuthState(state: string, stateData: any, expiresAt: Date): Promise<void>; 385 - getOAuthState(state: string): Promise<any | undefined>; 382 + saveOAuthState( 383 + state: string, 384 + stateData: unknown, 385 + expiresAt: Date 386 + ): Promise<void>; 387 + getOAuthState(state: string): Promise<unknown | undefined>; 386 388 deleteOAuthState(state: string): Promise<void>; 387 389 deleteExpiredOAuthStates(): Promise<void>; 388 390 ··· 667 669 668 670 export class DatabaseStorage implements IStorage { 669 671 private db: DbConnection; 670 - private statsCache: { data: any; timestamp: number } | null = null; 672 + private statsCache: { data: unknown; timestamp: number } | null = null; 671 673 private readonly STATS_CACHE_TTL = 60000; 672 674 private statsQueryInProgress = false; 673 675 private backgroundRefreshInterval: NodeJS.Timeout | null = null; ··· 1062 1064 feedType?: string 1063 1065 ): Promise<{ items: FeedItem[]; cursor?: string }> { 1064 1066 // Build all conditions to apply them together with AND 1065 - const conditions: any[] = [eq(feedItems.originatorDid, actorDid)]; 1067 + import type { SQL } from 'drizzle-orm'; 1068 + const conditions: SQL[] = [eq(feedItems.originatorDid, actorDid)]; 1066 1069 1067 1070 // Apply feed type filtering 1068 1071 if (feedType === 'posts_no_replies') { ··· 2393 2396 ? await encryptionService.decrypt(session.refreshToken) 2394 2397 : null, 2395 2398 }; 2396 - } catch (error) { 2399 + } catch { 2397 2400 // Decryption failed (corrupted data) - delete the session 2398 2401 console.error( 2399 2402 `[STORAGE] Failed to decrypt session ${id}, deleting corrupted session` ··· 2420 2423 ? await encryptionService.decrypt(session.refreshToken) 2421 2424 : null, 2422 2425 }); 2423 - } catch (error) { 2426 + } catch { 2424 2427 // Decryption failed - delete corrupted session 2425 2428 console.error( 2426 2429 `[STORAGE] Failed to decrypt session ${session.id}, deleting corrupted session` ··· 2438 2441 > 2439 2442 ): Promise<Session | undefined> { 2440 2443 // Encrypt tokens if provided 2441 - const updateData: any = {}; 2444 + const updateData: Partial< 2445 + Pick<InsertSession, 'accessToken' | 'refreshToken' | 'expiresAt'> 2446 + > = {}; 2442 2447 if (data.accessToken) { 2443 2448 updateData.accessToken = await encryptionService.encrypt( 2444 2449 data.accessToken ··· 2494 2499 if (session.refreshToken) { 2495 2500 await encryptionService.decrypt(session.refreshToken); 2496 2501 } 2497 - } catch (error) { 2502 + } catch { 2498 2503 // Decryption failed - delete this corrupted session 2499 2504 console.log(`[STORAGE] Deleting corrupted session ${session.id}`); 2500 2505 await this.db.delete(sessions).where(eq(sessions.id, session.id)); ··· 2507 2512 2508 2513 async saveOAuthState( 2509 2514 state: string, 2510 - stateData: any, 2515 + stateData: unknown, 2511 2516 expiresAt: Date 2512 2517 ): Promise<void> { 2513 2518 const { oauthStates } = await import('@shared/schema'); ··· 3304 3309 limit = 50, 3305 3310 cursor?: string 3306 3311 ): Promise<{ starterPacks: StarterPack[]; cursor?: string }> { 3307 - const conditions: any[] = []; 3312 + import type { SQL } from 'drizzle-orm'; 3313 + const conditions: SQL[] = []; 3308 3314 if (cursor) { 3309 3315 conditions.push(sql`${starterPacks.indexedAt} < ${new Date(cursor)}`); 3310 3316 } ··· 3327 3333 limit = 50, 3328 3334 cursor?: string 3329 3335 ): Promise<{ starterPacks: StarterPack[]; cursor?: string }> { 3330 - const conditions: any[] = [eq(starterPacks.creatorDid, creatorDid)]; 3336 + import type { SQL } from 'drizzle-orm'; 3337 + const conditions: SQL[] = [eq(starterPacks.creatorDid, creatorDid)]; 3331 3338 if (cursor) { 3332 3339 conditions.push(sql`${starterPacks.indexedAt} < ${new Date(cursor)}`); 3333 3340 } ··· 3350 3357 limit = 25, 3351 3358 cursor?: string 3352 3359 ): Promise<{ starterPacks: StarterPack[]; cursor?: string }> { 3353 - const conditions: any[] = [ 3360 + import type { SQL } from 'drizzle-orm'; 3361 + const conditions: SQL[] = [ 3354 3362 sql`${starterPacks.name} ILIKE ${'%' + q + '%'}`, 3355 3363 ]; 3356 3364 if (cursor) { ··· 3650 3658 timestamp: Date.now(), 3651 3659 }; 3652 3660 } 3653 - } catch (error) { 3661 + } catch { 3654 3662 // Silent failure - cache will just be stale 3655 3663 } finally { 3656 3664 this.statsQueryInProgress = false; ··· 3726 3734 const result = await Promise.race([statsPromise, timeoutPromise]); 3727 3735 3728 3736 const stats: Record<string, number> = {}; 3729 - result.rows.forEach((row: any) => { 3730 - stats[row.relname] = Number(row.count || 0); 3731 - }); 3737 + result.rows.forEach( 3738 + (row: { relname: string; count: string | number }) => { 3739 + stats[row.relname] = Number(row.count || 0); 3740 + } 3741 + ); 3732 3742 3733 3743 const data = { 3734 3744 totalUsers: stats.users || 0,