import { asc, eq } from "drizzle-orm"; import { isLauncherIconName, isLauncherTintName } from "@/shared/launcher"; import { launcherApps } from "@/worker/db/schema"; import type { AppContext } from "@/worker/hono"; import { isSafeHttpUrl } from "@/worker/services/url"; // Sorts by name for stable ordering across page loads. The `isSafeHttpUrl` // re-filter on read is defense-in-depth: admin writes already validate, // but a manually-edited DB row cannot crash render or smuggle a // `javascript:` href onto the page. Unknown icon/tint names coerce to // null and the tile renders with default fallback. export const handleListLauncherApps = async (c: AppContext): Promise => { const rows = await c.var.db .select({ id: launcherApps.id, name: launcherApps.name, url: launcherApps.url, icon: launcherApps.icon, tint: launcherApps.tint, }) .from(launcherApps) .where(eq(launcherApps.enabled, true)) .orderBy(asc(launcherApps.name)); const apps = rows .filter((row) => isSafeHttpUrl(row.url)) .map((row) => ({ id: row.id, name: row.name, url: row.url, icon: isLauncherIconName(row.icon) ? row.icon : null, tint: isLauncherTintName(row.tint) ? row.tint : null, })); return c.json(apps); };