import { createReadStream, existsSync, statSync } from "node:fs"; import { extname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import argon2 from "argon2"; import cookie from "@fastify/cookie"; import rateLimit from "@fastify/rate-limit"; import websocket from "@fastify/websocket"; import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from "fastify"; import { z } from "zod"; import type { AppConfig } from "./config.js"; import { openDatabase, type CompanionDatabase } from "./database.js"; import { JobRunner } from "./jobs.js"; import { clientAddress, CSRF_COOKIE, hashToken, randomToken, safeEqual, SESSION_COOKIE } from "./security.js"; import { collectSystemStatus } from "./status.js"; import { readNetworkActivity } from "./network-activity.js"; import { readUpdateStatus, triggerSystemUpdate } from "./update.js"; import { hotspotConfigurationSchema, networkActionSchema, readNetworkStatus, requestHotspotConfiguration, requestNetworkAction } from "./network.js"; import { collectAdbStatus, installAdbApk, launchAdbTarget, listInstalledAdbPackages, MAX_APK_BYTES } from "./adb.js"; import { CarTelemetryRecorder, type TelemetryPeriod } from "./car-telemetry.js"; import { openShell } from "./shell.js"; import { systemPowerActionSchema, triggerSystemPower } from "./system.js"; import "./types.js"; const usernameSchema = z .string() .trim() .min(3, "Username must contain at least 3 characters") .max(48, "Username must contain at most 48 characters") .regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/, "Use letters, numbers, dots, hyphens, or underscores"); const loginCredentialsSchema = z.object({ username: usernameSchema, password: z.string().min(1).max(256) }); const setupCredentialsSchema = loginCredentialsSchema.extend({ password: z .string() .min(8, "Password must contain at least 8 characters") .max(256) .regex(/[A-Z]/, "Password must contain at least one uppercase letter") .regex(/[0-9]/, "Password must contain at least one number") }); const adbPackageNameSchema = z.string().trim().max(240).regex( /^(?:[a-zA-Z][a-zA-Z0-9_]*\.)+[a-zA-Z][a-zA-Z0-9_]*$/, "Enter a valid Android package name" ); const adbComponentSchema = z.string().trim().max(240).regex( /^(?:[a-zA-Z][a-zA-Z0-9_]*\.)+[a-zA-Z][a-zA-Z0-9_]*\/(?:\.|[a-zA-Z])[a-zA-Z0-9_.$]*$/, "Enter a valid Android component" ); const adbTargetSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal("component"), value: adbComponentSchema }), z.object({ type: z.literal("package"), value: adbPackageNameSchema }) ]); const adbShortcutSchema = z.object({ name: z.string().trim().min(1).max(48), type: z.enum(["component", "package"]), value: z.string() }) .transform((value, context) => { const target = adbTargetSchema.safeParse({ type: value.type, value: value.value }); if (!target.success) { context.addIssue({ code: "custom", message: target.error.issues[0]?.message ?? "Enter a valid Android target" }); return z.NEVER; } return { name: value.name, ...target.data }; }); const clearTelemetrySchema = z.object({ mode: z.enum(["history", "all"]) }); type UserRow = { id: number; username: string; role: "admin"; password_hash: string }; type SessionUserRow = { id: number; username: string; role: "admin" }; function audit( database: CompanionDatabase, event: { actorUserId?: number; action: string; result: "success" | "failure"; ipAddress?: string; details?: Record; } ): void { database .prepare( `INSERT INTO audit_events (actor_user_id, action, result, ip_address, details_json, created_at) VALUES (?, ?, ?, ?, ?, ?)` ) .run( event.actorUserId ?? null, event.action, event.result, event.ipAddress ?? null, JSON.stringify(event.details ?? {}), new Date().toISOString() ); } function cookieOptions(config: AppConfig, httpOnly: boolean) { return { path: "/", httpOnly, sameSite: "strict" as const, secure: config.cookieSecure }; } function requireUser(request: FastifyRequest, reply: FastifyReply): boolean { if (request.authUser) return true; void reply.code(401).send({ error: { code: "AUTH_REQUIRED", message: "Sign in to continue" } }); return false; } export async function buildApp(config: AppConfig): Promise { const app = Fastify({ logger: config.nodeEnv === "test" ? false : { level: config.nodeEnv === "production" ? "info" : "debug", redact: ["req.headers.cookie", "req.headers.authorization", "req.headers.x-csrf-token"] } }); const database = openDatabase(config.databasePath); const jobRunner = new JobRunner(database, undefined, config.adb); const carTelemetry = new CarTelemetryRecorder(database, config.adb); if (config.adb.enabled) void collectAdbStatus(config.adb); carTelemetry.start(); app.decorate("database", database); app.decorate("jobRunner", jobRunner); app.decorateRequest("authUser", null); await app.register(cookie); await app.register(rateLimit, { global: false }); await app.register(websocket); app.addContentTypeParser( ["application/vnd.android.package-archive", "application/octet-stream"], { parseAs: "buffer" }, (_request, body, done) => done(null, body) ); app.addHook("onClose", async () => { carTelemetry.stop(); jobRunner.close(); database.close(); }); app.addHook("onSend", async (_request, reply) => { reply.header("X-Content-Type-Options", "nosniff"); reply.header("Referrer-Policy", "no-referrer"); reply.header("X-Frame-Options", "DENY"); reply.header( "Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'" ); }); app.addHook("onRequest", async (request) => { request.authUser = null; const token = request.cookies[SESSION_COOKIE]; if (!token) return; const now = new Date().toISOString(); const user = database .prepare( `SELECT users.id, users.username, users.role FROM sessions JOIN users ON users.id = sessions.user_id WHERE sessions.id_hash = ? AND sessions.expires_at > ?` ) .get(hashToken(token), now) as SessionUserRow | undefined; if (user) request.authUser = user; }); app.addHook("preValidation", async (request, reply) => { if (!["POST", "PUT", "PATCH", "DELETE"].includes(request.method)) return; const origin = request.headers.origin; let sameHostOrigin = false; if (origin && request.headers.host) { try { const parsedOrigin = new URL(origin); sameHostOrigin = ["http:", "https:"].includes(parsedOrigin.protocol) && parsedOrigin.host === request.headers.host; } catch { sameHostOrigin = false; } } if (origin && !sameHostOrigin && !config.allowedOrigins.has(origin)) { return reply.code(403).send({ error: { code: "ORIGIN_REJECTED", message: "Request origin is not trusted" } }); } const cookieToken = request.cookies[CSRF_COOKIE]; const headerToken = request.headers["x-csrf-token"]; if (!cookieToken || typeof headerToken !== "string" || !safeEqual(cookieToken, headerToken)) { return reply.code(403).send({ error: { code: "CSRF_REJECTED", message: "Refresh the page and try again" } }); } }); app.get("/healthz", async () => ({ status: "ok", timestamp: new Date().toISOString() })); app.get("/api/auth/state", async (request, reply) => { let csrfToken = request.cookies[CSRF_COOKIE]; if (!csrfToken) { csrfToken = randomToken(); reply.setCookie(CSRF_COOKIE, csrfToken, cookieOptions(config, false)); } const row = database.prepare("SELECT COUNT(*) AS count FROM users").get() as { count: number }; return { setupRequired: row.count === 0, user: request.authUser, csrfToken }; }); app.post( "/api/auth/setup", { config: { rateLimit: { max: 5, timeWindow: "1 minute" } } }, async (request, reply) => { const parsed = setupCredentialsSchema.safeParse(request.body); if (!parsed.success) { return reply.code(400).send({ error: { code: "INVALID_CREDENTIALS", message: parsed.error.issues[0]?.message ?? "Invalid account details" } }); } const passwordHash = await argon2.hash(parsed.data.password, { type: argon2.argon2id }); const createAdmin = database.transaction(() => { const row = database.prepare("SELECT COUNT(*) AS count FROM users").get() as { count: number }; if (row.count !== 0) return null; const result = database .prepare("INSERT INTO users (username, password_hash, role, created_at) VALUES (?, ?, 'admin', ?)") .run(parsed.data.username, passwordHash, new Date().toISOString()); return Number(result.lastInsertRowid); }); const userId = createAdmin(); if (userId === null) { audit(database, { action: "auth.setup", result: "failure", ipAddress: clientAddress(request), details: { reason: "setup_already_complete" } }); return reply.code(409).send({ error: { code: "SETUP_COMPLETE", message: "Initial setup is already complete" } }); } audit(database, { actorUserId: userId, action: "auth.setup", result: "success", ipAddress: clientAddress(request) }); return reply.code(201).send({ created: true }); } ); app.post( "/api/auth/login", { config: { rateLimit: { max: 8, timeWindow: "1 minute" } } }, async (request, reply) => { const parsed = loginCredentialsSchema.safeParse(request.body); const user = parsed.success ? (database .prepare("SELECT id, username, role, password_hash FROM users WHERE username = ? COLLATE NOCASE") .get(parsed.data.username) as UserRow | undefined) : undefined; const valid = user && parsed.success ? await argon2.verify(user.password_hash, parsed.data.password) : false; if (!user || !valid) { audit(database, { action: "auth.login", result: "failure", ipAddress: clientAddress(request), details: { reason: "invalid_credentials" } }); return reply.code(401).send({ error: { code: "LOGIN_FAILED", message: "Username or password is incorrect" } }); } database.prepare("DELETE FROM sessions WHERE expires_at <= ?").run(new Date().toISOString()); const token = randomToken(); const expiresAt = new Date(Date.now() + config.sessionTtlMs); database .prepare("INSERT INTO sessions (id_hash, user_id, expires_at, created_at) VALUES (?, ?, ?, ?)") .run(hashToken(token), user.id, expiresAt.toISOString(), new Date().toISOString()); reply.setCookie(SESSION_COOKIE, token, { ...cookieOptions(config, true), expires: expiresAt }); audit(database, { actorUserId: user.id, action: "auth.login", result: "success", ipAddress: clientAddress(request) }); return { user: { id: user.id, username: user.username, role: user.role } }; } ); app.post("/api/auth/logout", async (request, reply) => { const token = request.cookies[SESSION_COOKIE]; if (token) database.prepare("DELETE FROM sessions WHERE id_hash = ?").run(hashToken(token)); if (request.authUser) { audit(database, { actorUserId: request.authUser.id, action: "auth.logout", result: "success", ipAddress: clientAddress(request) }); } reply.clearCookie(SESSION_COOKIE, cookieOptions(config, true)); return { loggedOut: true }; }); app.get("/api/status", async (request, reply) => { if (!requireUser(request, reply)) return; return collectSystemStatus(config.adb); }); app.get("/api/jobs", async (request, reply) => { if (!requireUser(request, reply)) return; return { jobs: jobRunner.listJobs() }; }); app.get("/api/job-runs", async (request, reply) => { if (!requireUser(request, reply)) return; return { runs: jobRunner.listRuns() }; }); app.get<{ Params: { id: string } }>("/api/job-runs/:id", async (request, reply) => { if (!requireUser(request, reply)) return; const run = jobRunner.getRun(request.params.id); if (!run) return reply.code(404).send({ error: { code: "RUN_NOT_FOUND", message: "Job run not found" } }); return run; }); app.post<{ Params: { jobId: string } }>("/api/jobs/:jobId/runs", async (request, reply) => { if (!requireUser(request, reply)) return; if (!jobRunner.has(request.params.jobId)) { return reply.code(404).send({ error: { code: "JOB_NOT_FOUND", message: "Job is not available" } }); } const run = jobRunner.enqueue(request.params.jobId, request.authUser!.id); audit(database, { actorUserId: request.authUser!.id, action: "job.enqueue", result: "success", ipAddress: clientAddress(request), details: { jobId: request.params.jobId, runId: run.id } }); return reply.code(202).send(run); }); app.get("/api/network", async (request, reply) => { if (!requireUser(request, reply)) return; return readNetworkStatus(config); }); app.get("/api/network/activity", async (request, reply) => { if (!requireUser(request, reply)) return; return readNetworkActivity(config.network.activityPath); }); app.get("/api/adb/packages", async (request, reply) => { if (!requireUser(request, reply)) return; try { return { packages: await listInstalledAdbPackages(config.adb) }; } catch { return reply.code(503).send({ error: { code: "ADB_UNAVAILABLE", message: "The infotainment unit is not available over ADB" } }); } }); app.post( "/api/adb/install", { config: { rateLimit: { max: 4, timeWindow: "10 minutes" } }, bodyLimit: MAX_APK_BYTES }, async (request, reply) => { if (!requireUser(request, reply)) return; if (!Buffer.isBuffer(request.body)) { return reply.code(400).send({ error: { code: "INVALID_APK", message: "Choose a valid APK file" } }); } try { await installAdbApk(config.adb, request.body); audit(database, { actorUserId: request.authUser!.id, action: "adb.install-apk", result: "success", ipAddress: clientAddress(request), details: { bytes: request.body.byteLength } }); return reply.code(201).send({ installed: true }); } catch (error) { const invalid = error instanceof Error && error.message === "INVALID_APK"; audit(database, { actorUserId: request.authUser!.id, action: "adb.install-apk", result: "failure", ipAddress: clientAddress(request), details: { reason: invalid ? "invalid_apk" : "install_failed" } }); return reply.code(invalid ? 400 : 503).send({ error: { code: invalid ? "INVALID_APK" : "APK_INSTALL_FAILED", message: invalid ? "Choose a valid APK file" : "The APK could not be installed on the infotainment unit" } }); } } ); app.post("/api/adb/launch", { config: { rateLimit: { max: 30, timeWindow: "1 minute" } } }, async (request, reply) => { if (!requireUser(request, reply)) return; const parsed = adbTargetSchema.safeParse(request.body); if (!parsed.success) { return reply.code(400).send({ error: { code: "INVALID_ADB_TARGET", message: "Enter a valid package or activity" } }); } try { await launchAdbTarget(config.adb, parsed.data); audit(database, { actorUserId: request.authUser!.id, action: "adb.launch", result: "success", ipAddress: clientAddress(request), details: { targetType: parsed.data.type, target: parsed.data.value } }); return { launched: true }; } catch { audit(database, { actorUserId: request.authUser!.id, action: "adb.launch", result: "failure", ipAddress: clientAddress(request), details: { targetType: parsed.data.type, target: parsed.data.value } }); return reply.code(503).send({ error: { code: "ADB_LAUNCH_FAILED", message: "The target could not be opened on the infotainment unit" } }); } }); app.get("/api/adb/shortcuts", async (request, reply) => { if (!requireUser(request, reply)) return; const shortcuts = database .prepare("SELECT id, name, target_type AS type, target_value AS value, created_at AS createdAt FROM adb_shortcuts ORDER BY created_at, id") .all(); return { shortcuts }; }); app.post("/api/adb/shortcuts", async (request, reply) => { if (!requireUser(request, reply)) return; const parsed = adbShortcutSchema.safeParse(request.body); if (!parsed.success) { return reply.code(400).send({ error: { code: "INVALID_ADB_SHORTCUT", message: "Enter a name and a valid target" } }); } try { const createdAt = new Date().toISOString(); const result = database .prepare("INSERT INTO adb_shortcuts (name, target_type, target_value, created_at) VALUES (?, ?, ?, ?)") .run(parsed.data.name, parsed.data.type, parsed.data.value, createdAt); return reply.code(201).send({ id: Number(result.lastInsertRowid), ...parsed.data, createdAt }); } catch { return reply.code(409).send({ error: { code: "ADB_SHORTCUT_EXISTS", message: "A shortcut for that target already exists" } }); } }); app.delete<{ Params: { id: string } }>("/api/adb/shortcuts/:id", async (request, reply) => { if (!requireUser(request, reply)) return; const id = Number(request.params.id); if (!Number.isInteger(id) || id < 1) { return reply.code(400).send({ error: { code: "INVALID_ADB_SHORTCUT", message: "That shortcut is invalid" } }); } const result = database.prepare("DELETE FROM adb_shortcuts WHERE id = ?").run(id); if (result.changes === 0) return reply.code(404).send({ error: { code: "ADB_SHORTCUT_NOT_FOUND", message: "Shortcut not found" } }); return reply.code(204).send(); }); app.get("/api/car/telemetry", async (request, reply) => { if (!requireUser(request, reply)) return; try { return await carTelemetry.readLive(); } catch { return reply.code(503).send({ error: { code: "CAR_TELEMETRY_UNAVAILABLE", message: "Live car telemetry is unavailable over ADB" } }); } }); app.get<{ Querystring: { period?: string } }>("/api/car/history", async (request, reply) => { if (!requireUser(request, reply)) return; const parsed = z.enum(["24h", "7d", "30d", "1y", "all"]).safeParse(request.query.period ?? "24h"); if (!parsed.success) { return reply.code(400).send({ error: { code: "INVALID_HISTORY_PERIOD", message: "That history period is not available" } }); } return carTelemetry.store.history(parsed.data as TelemetryPeriod); }); app.get("/api/car/statistics", async (request, reply) => { if (!requireUser(request, reply)) return; return carTelemetry.store.overview(); }); app.post( "/api/car/history/clear", { config: { rateLimit: { max: 4, timeWindow: "10 minutes" } } }, async (request, reply) => { if (!requireUser(request, reply)) return; const parsed = clearTelemetrySchema.safeParse(request.body); if (!parsed.success) { return reply.code(400).send({ error: { code: "INVALID_CLEAR_TELEMETRY_MODE", message: "Choose which car statistics to clear" } }); } const result = carTelemetry.store.clear(parsed.data.mode); audit(database, { actorUserId: request.authUser!.id, action: "car.telemetry-clear", result: "success", ipAddress: clientAddress(request), details: result }); return result; } ); app.post( "/api/network/configuration", { config: { rateLimit: { max: 3, timeWindow: "10 minutes" } } }, async (request, reply) => { if (!requireUser(request, reply)) return; const parsed = hotspotConfigurationSchema.safeParse(request.body); if (!parsed.success) { return reply.code(400).send({ error: { code: "INVALID_HOTSPOT_CONFIGURATION", message: parsed.error.issues[0]?.message ?? "The hotspot settings are invalid" } }); } try { const configuration = await requestHotspotConfiguration(config, parsed.data, request.authUser!.id); audit(database, { actorUserId: request.authUser!.id, action: "network.configure-hotspot", result: "success", ipAddress: clientAddress(request), details: { requestId: configuration.id } }); return reply.code(202).send({ accepted: true, requestId: configuration.id }); } catch { audit(database, { actorUserId: request.authUser!.id, action: "network.configure-hotspot", result: "failure", ipAddress: clientAddress(request), details: { reason: "configuration_service_unavailable" } }); return reply.code(503).send({ error: { code: "HOTSPOT_CONFIGURATION_UNAVAILABLE", message: "The hotspot configuration service is unavailable" } }); } } ); app.post("/api/network/actions", async (request, reply) => { if (!requireUser(request, reply)) return; const parsed = z.object({ action: networkActionSchema }).safeParse(request.body); if (!parsed.success) { return reply.code(400).send({ error: { code: "INVALID_NETWORK_ACTION", message: "That network action is not available" } }); } try { const command = await requestNetworkAction(config, parsed.data.action, request.authUser!.id); audit(database, { actorUserId: request.authUser!.id, action: `network.${parsed.data.action}`, result: "success", ipAddress: clientAddress(request), details: { commandId: command.id } }); return reply.code(202).send({ accepted: true, commandId: command.id }); } catch { audit(database, { actorUserId: request.authUser!.id, action: `network.${parsed.data.action}`, result: "failure", ipAddress: clientAddress(request), details: { reason: "controller_unavailable" } }); return reply.code(503).send({ error: { code: "NETWORK_CONTROL_UNAVAILABLE", message: "Automatic network management is not configured" } }); } }); app.get("/api/system/update", async (request, reply) => { if (!requireUser(request, reply)) return; return readUpdateStatus(config); }); app.post( "/api/system/update/check", { config: { rateLimit: { max: 6, timeWindow: "10 minutes" } } }, async (request, reply) => { if (!requireUser(request, reply)) return; const current = await readUpdateStatus(config); if (!current.supported) { return reply.code(503).send({ error: { code: "UPDATES_UNAVAILABLE", message: "Install the system service before checking for updates" } }); } if (["checking", "building"].includes(current.state)) { return reply.code(409).send({ error: { code: "UPDATE_IN_PROGRESS", message: "An update task is already running" } }); } try { await triggerSystemUpdate("check"); audit(database, { actorUserId: request.authUser!.id, action: "system.update-check", result: "success", ipAddress: clientAddress(request) }); return reply.code(202).send({ queued: true }); } catch { return reply.code(503).send({ error: { code: "UPDATE_CHECK_FAILED", message: "The update check could not be started" } }); } } ); app.post( "/api/system/update", { config: { rateLimit: { max: 2, timeWindow: "10 minutes" } } }, async (request, reply) => { if (!requireUser(request, reply)) return; const current = await readUpdateStatus(config); if (!current.supported) { return reply.code(503).send({ error: { code: "UPDATES_UNAVAILABLE", message: "Install the system service before using automatic updates" } }); } if (["checking", "building"].includes(current.state)) { return reply.code(409).send({ error: { code: "UPDATE_IN_PROGRESS", message: "An update is already running" } }); } try { await triggerSystemUpdate("install"); audit(database, { actorUserId: request.authUser!.id, action: "system.update", result: "success", ipAddress: clientAddress(request), details: { state: "queued" } }); return reply.code(202).send({ queued: true }); } catch { audit(database, { actorUserId: request.authUser!.id, action: "system.update", result: "failure", ipAddress: clientAddress(request), details: { reason: "update_service_unavailable" } }); return reply.code(503).send({ error: { code: "UPDATE_START_FAILED", message: "The update service could not be started" } }); } } ); app.post( "/api/system/power", { config: { rateLimit: { max: 3, timeWindow: "10 minutes" } } }, async (request, reply) => { if (!requireUser(request, reply)) return; const parsed = z.object({ action: systemPowerActionSchema }).safeParse(request.body); if (!parsed.success) { return reply.code(400).send({ error: { code: "INVALID_POWER_ACTION", message: "That power action is not available" } }); } try { await triggerSystemPower(parsed.data.action); audit(database, { actorUserId: request.authUser!.id, action: `system.${parsed.data.action}`, result: "success", ipAddress: clientAddress(request) }); return reply.code(202).send({ queued: true, action: parsed.data.action }); } catch { audit(database, { actorUserId: request.authUser!.id, action: `system.${parsed.data.action}`, result: "failure", ipAddress: clientAddress(request) }); return reply.code(503).send({ error: { code: "POWER_ACTION_FAILED", message: "The Pi could not schedule that power action" } }); } } ); app.get("/api/system/shell", { websocket: true }, (socket, request) => { openShell(socket, request, config); }); app.get("/api/events", async (request, reply) => { if (!requireUser(request, reply)) return; reply.hijack(); reply.raw.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", "X-Accel-Buffering": "no" }); let closed = false; const sendStatus = async () => { if (closed) return; try { const status = await collectSystemStatus(config.adb); reply.raw.write(`event: status\ndata: ${JSON.stringify(status)}\n\n`); } catch { reply.raw.write(`event: collection-error\ndata: {"code":"STATUS_COLLECTION_FAILED"}\n\n`); } }; await sendStatus(); const interval = setInterval(() => void sendStatus(), 10_000); const stopJobEvents = jobRunner.onRun((run) => { if (!closed) reply.raw.write(`event: job-run\ndata: ${JSON.stringify(run)}\n\n`); }); request.raw.on("close", () => { closed = true; clearInterval(interval); stopJobEvents(); }); }); const webRoot = resolve(fileURLToPath(new URL("../../web/dist", import.meta.url))); if (config.nodeEnv === "production" && existsSync(webRoot)) { app.get("/", async (_request, reply) => { reply.type("text/html; charset=utf-8"); return reply.send(createReadStream(resolve(webRoot, "index.html"))); }); const publicFiles: Record = { "/manifest.webmanifest": { file: "manifest.webmanifest", type: "application/manifest+json; charset=utf-8", cacheControl: "no-cache" }, "/service-worker.js": { file: "service-worker.js", type: "text/javascript; charset=utf-8", cacheControl: "no-cache" }, "/icon.svg": { file: "icon.svg", type: "image/svg+xml", cacheControl: "public, max-age=86400" }, "/pwa-192x192.png": { file: "pwa-192x192.png", type: "image/png", cacheControl: "public, max-age=86400" }, "/pwa-512x512.png": { file: "pwa-512x512.png", type: "image/png", cacheControl: "public, max-age=86400" }, "/pwa-maskable-512x512.png": { file: "pwa-maskable-512x512.png", type: "image/png", cacheControl: "public, max-age=86400" }, "/apple-touch-icon.png": { file: "apple-touch-icon.png", type: "image/png", cacheControl: "public, max-age=86400" } }; for (const [route, asset] of Object.entries(publicFiles)) { app.get(route, async (_request, reply) => { const assetPath = resolve(webRoot, asset.file); if (!existsSync(assetPath) || !statSync(assetPath).isFile()) { return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Asset not found" } }); } reply.type(asset.type); reply.header("Cache-Control", asset.cacheControl); return reply.send(createReadStream(assetPath)); }); } app.get<{ Params: { file: string } }>("/assets/:file", async (request, reply) => { if (!/^[a-zA-Z0-9._-]+$/.test(request.params.file)) { return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Asset not found" } }); } const assetPath = resolve(webRoot, "assets", request.params.file); if (!existsSync(assetPath) || !statSync(assetPath).isFile()) { return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Asset not found" } }); } const contentTypes: Record = { ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".map": "application/json; charset=utf-8", ".png": "image/png", ".svg": "image/svg+xml", ".woff2": "font/woff2" }; reply.type(contentTypes[extname(assetPath)] ?? "application/octet-stream"); reply.header("Cache-Control", "public, max-age=31536000, immutable"); return reply.send(createReadStream(assetPath)); }); } return app; }