Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140import { randomUUID } from "node:crypto";import type { HttpBindings } from "@hono/node-server";import { serveStatic } from "@hono/node-server/serve-static";import { type DeviceDTO, HealthResponse } from "@pneumo/protocol";import { Hono } from "hono";import { bodyLimit } from "hono/body-limit";import type { Logger } from "pino";import packageJson from "../package.json" with { type: "json" };import { createAuthRoutes } from "./auth/routes.js";import type { Config } from "./config.js";import type { DeviceRegistry } from "./devices/registry.js";import { createDeviceRoutes } from "./devices/routes.js";import type { FileRegistry } from "./files/registry.js";import { createFileRoutes } from "./files/routes.js";import { errorResponse, handleError } from "./http/errors.js";import { securityHeaders, staticCacheControl } from "./http/headers.js";import type { PushIndex } from "./index-store/index.js";import type { InstanceState } from "./instance/state.js";import { createRequestLogger } from "./log.js";import { createPairingRoutes } from "./pairing/routes.js";import { createPushRoutes } from "./pushes/routes.js";import type { Storage } from "./storage/types.js";import type { UploadMode } from "./storage/upload-mode.js";
export interface Clock { /** Returns Unix epoch milliseconds. */ now(): number;}
export const systemClock: Clock = { now: () => Date.now() };
export interface AppDependencies { config: Config; storage: Storage; clock: Clock; logger: Logger; instanceState: InstanceState; devices: DeviceRegistry; pushes: PushIndex; files: FileRegistry; uploadMode: UploadMode; vapidPublicKey: string;}
export type AppEnv = { Bindings: HttpBindings; Variables: { logger: Logger; requestId: string; device: DeviceDTO; staticPath?: string; };};
export function createApp(deps: AppDependencies) { const app = new Hono<AppEnv>(); app.onError(handleError); app.notFound((c) => errorResponse(c, "not_found"));
app.use("*", async (c, next) => { const requestId = randomUUID(); const logger = createRequestLogger(deps.logger, requestId); c.set("requestId", requestId); c.set("logger", logger); c.header("X-Request-Id", requestId); const startedAt = deps.clock.now(); await next(); logger.info( { method: c.req.method, path: c.req.path, status: c.res.status, ms: Math.max(0, deps.clock.now() - startedAt), }, "Request completed", ); }); app.use( "*", securityHeaders( deps.config.PUBLIC_URL, deps.uploadMode === "direct" ? deps.storage.bucketOrigin : undefined, ), ); const limitJson = bodyLimit({ maxSize: 128 * 1024, onError: (c) => errorResponse(c, "payload_too_large"), }); app.use("*", async (c, next) => { if (c.req.method === "PUT" && /^\/api\/files\/[^/]+\/content$/u.test(c.req.path)) { return next(); } const mediaType = c.req.header("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase(); if (mediaType === "application/json" || mediaType?.endsWith("+json")) { return limitJson(c, next); } await next(); });
app.get("/api/health", (c) => c.json( HealthResponse.parse({ ok: true, version: packageJson.version, initialized: deps.instanceState.initialized, }), ), ); app.route("/api", createAuthRoutes(deps)); app.route("/api", createDeviceRoutes(deps)); app.route("/api", createPairingRoutes(deps)); app.route("/api", createFileRoutes(deps)); app.route("/api", createPushRoutes(deps)); app.all("/api", (c) => errorResponse(c, "not_found")); app.all("/api/*", (c) => errorResponse(c, "not_found"));
app.use("*", async (c, next) => { await next(); const path = c.get("staticPath"); if (path) c.header("Cache-Control", staticCacheControl(path)); }); app.use( "*", serveStatic({ root: deps.config.WEB_DIST_DIR, onFound: (path, c) => c.set("staticPath", path), }), ); app.get( "*", serveStatic({ root: deps.config.WEB_DIST_DIR, path: "index.html", onFound: (path, c) => c.set("staticPath", path), }), );
return app;}