Download apps/api/src/app.ts from Ryukijano/conjugate: direct link, hf CLI and curl.
- Browser
- Download file 5.99 kB
-
https://huggingface.co/spaces/Ryukijano/conjugate/resolve/main/apps/api/src/app.ts
- Command line
-
hf download hf://spaces/Ryukijano/conjugate/apps/api/src/app.ts
-
curl -L -o app.ts https://huggingface.co/spaces/Ryukijano/conjugate/resolve/main/apps/api/src/app.ts
5.99 kB
| import Fastify, { LogController, type FastifyInstance } from 'fastify'; | |
| import rateLimit from '@fastify/rate-limit'; | |
| import fastifyStatic from '@fastify/static'; | |
| import { access, stat } from 'node:fs/promises'; | |
| import { resolve, sep } from 'node:path'; | |
| import { posix } from 'node:path'; | |
| import { ApiErrorSchema, CatalogSchema, RunRequestSchema, RunResultSchema } from '@her2/shared'; | |
| import { buildCatalog, CLAUDE_MODEL } from './evidence.js'; | |
| import { ApiFailure } from './errors.js'; | |
| import { runReview, type RunOptions } from './run.js'; | |
| import { registerResearchRoutes } from './research-routes.js'; | |
| import { registerChatRoutes } from './chat-routes.js'; | |
| import { registerTeamRoutes } from './team-routes.js'; | |
| import { registerModelRoutes } from './models-routes.js'; | |
| import { liveRetrievalEnabled, registerLiveRoutes, sharedLiveRetriever } from './live-routes.js'; | |
| import type { ChatOptions } from './chat.js'; | |
| import type { McpInferenceOptions } from './model-mcp.js'; | |
| export interface AppOptions extends RunOptions, ChatOptions { | |
| inference?: McpInferenceOptions; | |
| staticRoot?: string; | |
| rateLimitMax?: number; | |
| rateLimitWindowMs?: number; | |
| } | |
| function errorBody(code: string, message: string) { return ApiErrorSchema.parse({ error: { code, message } }); } | |
| function apiPath(url: string) { | |
| let path: string; | |
| try { path = posix.normalize(decodeURIComponent(url.split('?')[0] ?? '/').replaceAll('\\', '/')); } catch { return true; } | |
| return path === '/api' || path.startsWith('/api/'); | |
| } | |
| export async function createApp(options: AppOptions = {}): Promise<FastifyInstance> { | |
| const app = Fastify({ logger: false, logController: new LogController({ disableRequestLogging: true }), bodyLimit: 16_384, requestTimeout: 65_000, trustProxy: false }); | |
| const configured = Boolean(options.claude?.apiKey?.trim()); | |
| app.addHook('onSend', async (_request, reply, payload) => { | |
| reply.header('cache-control', 'no-store'); | |
| reply.header('x-content-type-options', 'nosniff'); | |
| reply.header('referrer-policy', 'no-referrer'); | |
| return payload; | |
| }); | |
| await app.register(rateLimit, { | |
| global: true, max: Math.min(30, Math.max(1, options.rateLimitMax ?? 30)), | |
| timeWindow: Math.min(60_000, Math.max(1, options.rateLimitWindowMs ?? 60_000)), | |
| errorResponseBuilder: () => Object.assign(new Error('Rate limit exceeded.'), { statusCode: 429 }) | |
| }); | |
| app.setErrorHandler((error, _request, reply) => { | |
| // Errors may carry request/model/credential strings. Never serialize them. | |
| if (error instanceof ApiFailure) return reply.code(error.status).send(errorBody(error.code, error.message)); | |
| const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined; | |
| const status = error && typeof error === 'object' && 'statusCode' in error ? error.statusCode : undefined; | |
| if (code === 'FST_ERR_CTP_BODY_TOO_LARGE') return reply.code(413).send(errorBody('BODY_TOO_LARGE', 'Request body exceeds the allowed size.')); | |
| if (status === 429) return reply.code(429).send(errorBody('RATE_LIMITED', 'Request rate limit exceeded. Try again later.')); | |
| if (status === 415) return reply.code(415).send(errorBody('UNSUPPORTED_MEDIA_TYPE', 'Use application/json for API requests.')); | |
| if (status === 400) return reply.code(400).send(errorBody('INVALID_REQUEST', 'Malformed JSON or invalid request.')); | |
| return reply.code(500).send(errorBody('INTERNAL_ERROR', 'The request could not be completed safely. No clinical result was produced.')); | |
| }); | |
| app.setNotFoundHandler((_request, reply) => reply.code(404).send(errorBody('NOT_FOUND', 'Route not found.'))); | |
| app.get('/api/health', async () => ({ status: 'ok', claude_configured: configured, model: CLAUDE_MODEL })); | |
| app.get('/api/catalog', async () => CatalogSchema.parse(buildCatalog(configured))); | |
| app.post('/api/runs', async (request, reply) => { | |
| const parsed = RunRequestSchema.safeParse(request.body); | |
| if (!parsed.success) throw new ApiFailure('INVALID_REQUEST', 400); | |
| const result = await runReview(parsed.data, options); | |
| RunResultSchema.parse(result); | |
| return reply.code(200).send(result); | |
| }); | |
| const live = options.live !== undefined ? options.live : liveRetrievalEnabled() ? sharedLiveRetriever() : null; | |
| registerResearchRoutes(app, options); | |
| registerChatRoutes(app, { ...options, live }); | |
| registerTeamRoutes(app, { ...options, live }); | |
| registerModelRoutes(app, options.inference); | |
| registerLiveRoutes(app, { enabled: Boolean(live), ...(live ? { retriever: live } : {}) }); | |
| if (options.staticRoot) { | |
| const root = resolve(options.staticRoot); | |
| let exists = false; | |
| try { await access(resolve(root, 'index.html')); exists = true; } catch { /* API-only if no web build exists. */ } | |
| if (exists) { | |
| // No automatic wildcard: explicitly prevent ANY /api route from static | |
| // handling, even if a file at that path happens to exist in the build. | |
| await app.register(fastifyStatic, { root, serve: false }); | |
| app.get('/*', async (request, reply) => { | |
| if (apiPath(request.url)) return reply.code(404).send(errorBody('NOT_FOUND', 'API route not found.')); | |
| let decoded: string; | |
| try { decoded = decodeURIComponent(request.url.split('?')[0] ?? '/'); } catch { return reply.code(400).send(errorBody('INVALID_REQUEST', 'Invalid path.')); } | |
| const target = resolve(root, `.${decoded}`); | |
| if (!target.startsWith(`${root}${sep}`) && target !== root) return reply.code(404).send(errorBody('NOT_FOUND', 'Route not found.')); | |
| let fileExists = false; | |
| try { fileExists = (await stat(target)).isFile(); } catch { /* Known SPA routes may use index.html. */ } | |
| if (fileExists) return reply.sendFile(decoded.replace(/^\/+/, '')); | |
| if (decoded.startsWith('/assets/') || decoded.includes('.')) return reply.code(404).send(errorBody('NOT_FOUND', 'File not found.')); | |
| return reply.sendFile('index.html'); | |
| }); | |
| } | |
| } | |
| await app.ready(); | |
| return app; | |
| } | |