profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM
app.ts 3,849 bytes
1 import cors from '@fastify/cors'
2 import multipart from '@fastify/multipart'
3 import fastifyStatic from '@fastify/static'
4 import Fastify, { type FastifyInstance } from 'fastify'
5 import { createLazyInterviewLlm, createLazySttProvider } from './providers/factory'
6 import type { InterviewLlm, SttProvider } from './providers/types'
7 import { registerAudioRoutes } from './routes/audio'
8 import { registerBlockerRoutes } from './routes/blockers'
9 import { registerConfigRoutes } from './routes/config'
10 import { registerExportRoutes } from './routes/export'
11 import { registerFsRoutes } from './routes/fs'
12 import { registerGenerateRoutes } from './routes/generate'
13 import { registerSessionRoutes } from './routes/sessions'
14 import { createDefaultSessionStore, SessionStore } from './store/sessionStore'
15
16 export interface AppDeps {
17 store: SessionStore
18 llm: InterviewLlm
19 stt: SttProvider
20 demoLocked: boolean
21 // Overridable so tests never write the real .env of this repository.
22 envPath?: string
23 env?: NodeJS.ProcessEnv
24 /**
25 * Public deployment mode: the app is served to strangers on the internet
26 * rather than to the one person sitting at the machine it runs on.
27 *
28 * Everything VoiceTask does with the local disk assumes the second case, so
29 * this mode takes those powers away: no folder browsing, no writing to a path
30 * the browser chose, no editing the provider keys, and no CORS reflection.
31 */
32 publicDemo: boolean
33 /** Sandbox every session writes into. Required when publicDemo is set. */
34 sandboxRoot?: string
35 /** Built client to serve from the same origin as the API. */
36 clientDir?: string
37 }
38
39 export function buildApp(deps: Partial<AppDeps> = {}): FastifyInstance {
40 const resolved: AppDeps = {
41 store: deps.store ?? createDefaultSessionStore(),
42 llm: deps.llm ?? createLazyInterviewLlm(),
43 stt: deps.stt ?? createLazySttProvider(),
44 demoLocked: deps.demoLocked ?? false,
45 envPath: deps.envPath,
46 env: deps.env,
47 publicDemo: deps.publicDemo ?? false,
48 sandboxRoot: deps.sandboxRoot,
49 clientDir: deps.clientDir,
50 }
51
52 if (resolved.publicDemo && !resolved.sandboxRoot) {
53 throw new Error('publicDemo needs a sandboxRoot; refusing to let the browser pick a directory')
54 }
55
56 const app = Fastify({ logger: false })
57 // A public instance is served from one origin and needs no cross-origin
58 // access at all; reflecting every Origin back would let any page on the web
59 // drive it from a visitor's browser.
60 void app.register(cors, { origin: resolved.publicDemo ? false : true })
61 void app.register(multipart, { limits: { fileSize: 25 * 1024 * 1024 } })
62
63 app.get('/api/health', async () => ({ ok: true }))
64 registerSessionRoutes(app, resolved)
65 registerAudioRoutes(app, resolved)
66 registerGenerateRoutes(app, resolved)
67 registerBlockerRoutes(app, resolved)
68 // Browsing and creating folders is a local convenience. On a public host it
69 // is an unauthenticated read of the container filesystem, so it is left out.
70 if (!resolved.publicDemo) registerFsRoutes(app)
71 registerExportRoutes(app, resolved)
72 registerConfigRoutes(app, {
73 demoLocked: resolved.demoLocked,
74 envPath: resolved.envPath,
75 env: resolved.env,
76 publicDemo: resolved.publicDemo,
77 })
78
79 if (resolved.clientDir) registerClient(app, resolved.clientDir)
80
81 return app
82 }
83
84 /**
85 * Serve the built client beside the API so both live on one origin. Unknown
86 * non-API paths fall back to index.html, which is what a single-page app needs
87 * for a deep link to survive a reload.
88 */
89 function registerClient(app: FastifyInstance, root: string): void {
90 void app.register(fastifyStatic, { root, wildcard: false })
91 app.setNotFoundHandler((request, reply) => {
92 if (request.url.startsWith('/api/')) return reply.code(404).send({ error: 'not found' })
93 return reply.sendFile('index.html')
94 })
95 }
96