Files
FrenoCorp/server/trpc/index.ts
Michael Freno 754fce269f fix: implement critical security remediation for authentication and authorization
- Add Clerk token verification to tRPC context (server/trpc/index.ts)
- Remove client-controlled authorId/reviewedById from revisions router
- Require JWT_SECRET environment variable, remove hardcoded fallback
- Add table name validation to prevent SQL injection in backup logic
- Fix TRPCContext type to use better-sqlite3 instead of LibSQL
- Update revisions router tests to use proper tRPC v11+ API
- Add resetInMemoryState function for test isolation

Security fixes address:
- Critical: Authentication bypass via missing token verification
- Critical: User impersonation via client-controlled IDs
- High: Insecure WebSocket defaults with hardcoded secrets
- High: SQL injection vulnerability in backup logic

All tests passing (24/24).
2026-04-25 08:24:45 -04:00

71 lines
2.2 KiB
TypeScript

import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { projectRouter } from './project-router';
import { revisionsRouter } from './revisions-router';
import type { TRPCContext } from './types';
import type { TRPCError } from '@trpc/server';
import { t } from './router';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import { projects, characters, scenes, characterRelationships, sceneCharacters } from '../../src/db/schema';
import { verifyToken } from '@clerk/backend';
// App router combining all routers
export const appRouter = t.router({
project: projectRouter,
revisions: revisionsRouter,
} as const);
export type AppRouter = typeof appRouter;
// Database instance (shared for now - should come from config)
let dbInstance: ReturnType<typeof drizzle> | null = null;
function getDb() {
if (dbInstance) return dbInstance;
const sqlite = new Database('./data/frenocorp.db');
dbInstance = drizzle(sqlite);
return dbInstance;
}
// Create tRPC HTTP server
export function createTRPCServer(port: number = 8080) {
const server = createHTTPServer({
router: appRouter,
createContext: async ({ req }): Promise<TRPCContext> => {
const authHeader = req.headers.authorization;
let userId: number | undefined = undefined;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7);
try {
const clerkSecretKey = process.env.CLERK_SECRET_KEY;
if (!clerkSecretKey) {
console.warn('CLERK_SECRET_KEY not set, skipping token verification');
} else {
const payload = await verifyToken(token, { secretKey: clerkSecretKey });
userId = payload.sub ? parseInt(payload.sub, 10) : undefined;
}
} catch (error) {
console.error('Failed to verify Clerk token:', error);
}
}
return {
userId,
db: getDb(),
};
},
onError: ({ error, path }: { error: TRPCError; path: string | undefined }) => {
console.error(`tRPC error on ${path}:`, error.message);
},
});
server.listen(port, () => {
console.log(`tRPC server listening on port ${port}`);
});
return server;
}
export default appRouter;