import { createDiscordRoutes, type CommandConfig } from "./discord/routes"; import { createLiveDiscord } from "./sharing/live-discord"; import { createReminderRoutes } from "./reminders/routes"; import { errorDetails } from "./ops/monitor"; import { createAccountRoutes } from "./account/routes"; import { createHabitRoutes } from "./habits/routes"; import { ApiError } from "./habits/service"; import { Hono } from "hono"; import { sql } from "drizzle-orm"; import { createAuth, type AppDatabase, type AuthEnv } from "./auth"; import type { AuthConfig } from "./auth/config"; import type { FetchDiscord } from "./auth/discord"; import { createSharingRoutes, type DiscordFetch } from "./sharing/routes"; import type { DiscordSharingConfig } from "./sharing/config"; export function createApi(db: AppDatabase, config: AuthConfig, request?: FetchDiscord, now?: () => number, discordRequest?: DiscordFetch, sharingConfig: DiscordSharingConfig = { token: "", channelId: "" }, operations: { healthy?: () => boolean; log?: (event: Record) => void; discord?: CommandConfig } = {}) { const app = new Hono(); app.use("*", async (c, next) => { const requestId = crypto.randomUUID(); c.set("requestId", requestId); c.header("X-Request-ID", requestId); await next(); }); const auth = createAuth(db, config, request, now); const liveSharing = createLiveDiscord(db, auth, sharingConfig, now ?? Date.now, discordRequest); const sharing = createSharingRoutes(db, auth, sharingConfig, now ?? Date.now, discordRequest); app.use("*", async (c, next) => { await next(); if (!["GET", "HEAD", "OPTIONS"].includes(c.req.method) && /^\/api\/(habits|me|account)(\/|$)/.test(c.req.path) && c.res.status < 400 && c.get("user")) liveSharing.changed(c.get("user").id); }); app.get("/api/health", c => { c.header("Cache-Control", "no-store"); db.get(sql`SELECT 1`); if (operations.healthy && !operations.healthy()) return c.json({ status: "degraded" }, 503); return c.json({ status: "ok" }); }); app.route("/api/discord/interactions", createDiscordRoutes(db, { ...(operations.discord ?? { publicKey: "", applicationId: config.clientId, origin: config.origin }), remindersAvailable: Boolean(sharingConfig.token) }, now ?? Date.now, liveSharing.changed, { progress: { sharing, live: liveSharing } })); app.route("/api/sharing/live", liveSharing.routes); app.route("/api/auth", auth.routes); app.route("/api/reminders", createReminderRoutes(db, auth, Boolean(sharingConfig.token), now ?? Date.now)); app.route("/api/account", createAccountRoutes(db, auth, now ?? Date.now)); app.get("/api/me", auth.requireAuth, c => c.json(c.get("user"))); app.route("/api", createHabitRoutes(db, auth, now ?? Date.now)); app.route("/api/sharing", sharing); app.notFound(c => c.json({ error: "Not found" }, 404)); app.onError((_error, c) => { c.header("Cache-Control", "no-store"); if (_error instanceof ApiError) return c.json({ error: _error.message }, _error.status); const requestId = c.get("requestId"); const event = { event: "request_failed", timestamp: new Date().toISOString(), requestId, method: c.req.method, route: c.req.routePath, ...errorDetails(_error) }; (operations.log ?? (event => console.error(JSON.stringify(event))))(event); return c.json({ error: "Internal server error", requestId }, 500); }); return Object.assign(app, { stopLiveSharing: liveSharing.stop, flushLiveSharing: liveSharing.flush }); }