Something went wrong. Try again.
ATProto-based AI Assistant Chat interface.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160import { randomBytes } from 'node:crypto'import { readFileSync } from 'node:fs'import { join } from 'node:path'import { serveStatic } from '@hono/node-server/serve-static'import { Hono } from 'hono'import { sql } from 'kysely'import { type AdminRoutesDeps, adminRoutes } from './admin/routes.ts'import { SessionExpired } from './auth/pds.ts'import { type AuthDeps, accessGate, authApiRoutes, oauthRoutes, originCheck, sessionMiddleware,} from './auth/routes.ts'import { type BlobRoutesDeps, blobRoutes } from './blobs/routes.ts'import { InvalidBody } from './body.ts'import type { Config } from './config.ts'import type { Db } from './db/index.ts'import type { AppEnv } from './env.ts'import { pdsFailure } from './lex-errors.ts'import type { Logger } from './logger.ts'import { type PluginRoutesDeps, pluginRoutes } from './plugins/routes.ts'import { ModelUnavailable } from './providers/catalog.ts'import { type ProviderRoutesDeps, providerRoutes } from './providers/routes.ts'import { safeErrorMessage } from './safe-error.ts'import { SettingsStore } from './settings/store.ts'import { sharingRoutes } from './sharing/routes.ts'import type { SharingService } from './sharing/service.ts'import { InvalidCursor, InvalidRecord, InvalidStoredRecord, SpaceNotFound,} from './storage/record-store.ts'import { InvalidSpaceUri } from './storage/records.ts'import { type StorageRoutesDeps, storageRoutes } from './storage/routes.ts'import { type SyncRoutesDeps, syncRoutes } from './sync/routes.ts'import { type TurnRoutesDeps, turnRoutes } from './turns/routes.ts'import { renderIndexHtml } from './web-html.ts'
export type AppDeps = { config: Config db: Db logger: Logger /** Admin settings. Without them the defaults apply. */ settings?: SettingsStore /** Built web app to serve, in production. */ webDist?: string auth?: AuthDeps plugins?: PluginRoutesDeps storage?: StorageRoutesDeps providers?: ProviderRoutesDeps turns?: TurnRoutesDeps blobs?: BlobRoutesDeps sharing?: SharingService sync?: SyncRoutesDeps admin?: AdminRoutesDeps}
export function createApp(deps: AppDeps) { const { config, db, logger } = deps const settings = deps.settings ?? deps.auth?.settings ?? new SettingsStore(db, logger) const appName = () => settings.get('general').appName const app = new Hono<AppEnv>()
app.use('*', async (c, next) => { const start = performance.now() await next() logger.info( { method: c.req.method, path: c.req.path, status: c.res.status, ms: Math.round(performance.now() - start), }, 'request', ) })
if (deps.auth) { app.use('/api/*', sessionMiddleware(deps.auth), originCheck(config), accessGate(deps.auth)) app.route('/', oauthRoutes(deps.auth)) }
const api = new Hono<AppEnv>().get('/health', async (c) => { try { await sql`select 1`.execute(db) return c.json({ status: 'ok' as const, appName: appName() }) } catch (err) { logger.error({ err }, 'health check database query failed') return c.json({ status: 'error' as const }, 503) } })
if (deps.auth) api.route('/', authApiRoutes(deps.auth)) if (deps.plugins) api.route('/plugins', pluginRoutes(deps.plugins)) if (deps.storage) api.route('/', storageRoutes(deps.storage)) if (deps.providers) api.route('/', providerRoutes(deps.providers)) if (deps.turns) api.route('/', turnRoutes(deps.turns)) if (deps.blobs) api.route('/', blobRoutes(deps.blobs)) if (deps.sharing) api.route('/', sharingRoutes(deps.sharing)) if (deps.admin) api.route('/admin', adminRoutes(deps.admin))
app.route('/api', api) if (deps.sync) app.route('/', syncRoutes(deps.sync))
app.onError((err, c) => { if (err instanceof SessionExpired) return c.json({ error: 'SessionExpired', message: err.message }, 401) if (err instanceof InvalidRecord) return c.json({ error: 'InvalidRecord', message: err.message }, 400) if (err instanceof InvalidStoredRecord) { logger.warn({ err, path: c.req.path }, 'a stored record does not match its lexicon') return c.json({ error: 'InvalidStoredRecord', message: err.message }, 502) } if (err instanceof InvalidBody) return c.json({ error: 'InvalidRequest', message: err.message, issues: err.issues }, 400) if (err instanceof InvalidCursor) return c.json({ error: 'InvalidRequest', message: err.message }, 400) if (err instanceof InvalidSpaceUri) return c.json({ error: 'InvalidRequest', message: err.message }, 400) if (err instanceof ModelUnavailable) return c.json({ error: 'ModelUnavailable', message: err.message }, 400) if (err instanceof SpaceNotFound) return c.json({ error: 'NotFound', message: err.message }, 404) const pds = pdsFailure(err) if (pds) { logger.warn({ err, path: c.req.path }, 'a PDS call failed') return c.json({ error: pds.error, message: pds.message }, pds.status) } // The reference ties what the user sees to this log line. Details stay in the log outside development. const reference = randomBytes(4).toString('hex') logger.error({ err, path: c.req.path, reference }, 'unhandled error') const detail = config.nodeEnv === 'development' ? `: ${safeErrorMessage(err)}` : '.' return c.json( { error: 'InternalServerError', message: `Something went wrong on the server (reference ${reference})${detail}`, }, 500, ) }) app.all('/api/*', (c) => c.json({ error: 'NotFound' }, 404))
if (deps.webDist) { const raw = readFileSync(join(deps.webDist, 'index.html'), 'utf8') const index = () => renderIndexHtml(raw, appName()) app.get('/', (c) => c.html(index())) app.get('/index.html', (c) => c.html(index())) app.use('*', serveStatic({ root: deps.webDist })) app.get('*', (c) => c.html(index())) }
return app}
export type AppType = ReturnType<typeof createApp>