Exosphere is a set of small, modular, self-hostable community tools built on the AT Protocol. app.exosphere.site
at main 114 lines 3.3 kB view raw
1import type { Context } from "hono"; 2import { Hono } from "hono"; 3import { z } from "zod"; 4import { and, eq } from "../../db/drizzle.ts"; 5import { getDb } from "../../db/index.ts"; 6import { sphereModules, type spheres } from "../../db/schema/index.ts"; 7import { requireAuth, type AuthEnv } from "../../auth/index.ts"; 8import { putPdsRecord } from "../../pds.ts"; 9import { enableModuleSchema } from "../schemas.ts"; 10import { findSphere, getEnabledModules, formatModules } from "./helpers.ts"; 11 12const SPHERE_COLLECTION = "site.exosphere.sphere.profile" as const; 13 14type Sphere = typeof spheres.$inferSelect; 15 16/** Sync the sphere declaration (including modules) to the owner's PDS. */ 17async function syncSpherePds(c: Context<AuthEnv>, sphere: Sphere) { 18 const modules = getEnabledModules(sphere.id).map((m) => m.moduleName); 19 await putPdsRecord(c.var.session, SPHERE_COLLECTION, "self", { 20 name: sphere.name, 21 description: sphere.description ?? undefined, 22 visibility: sphere.visibility, 23 modules, 24 createdAt: sphere.createdAt, 25 }); 26} 27 28export function createModuleRoutes(availableModules: string[]) { 29 const app = new Hono<AuthEnv>(); 30 31 // List enabled modules for a sphere 32 app.get("/:handle/modules", (c) => { 33 const sphere = findSphere(c.req.param("handle")); 34 if (!sphere) { 35 return c.json({ error: "Sphere not found" }, 404); 36 } 37 38 return c.json({ 39 modules: formatModules(getEnabledModules(sphere.id)), 40 available: availableModules, 41 }); 42 }); 43 44 // Enable module 45 app.post("/:handle/modules", requireAuth, async (c) => { 46 const sphere = findSphere(c.req.param("handle")); 47 if (!sphere) { 48 return c.json({ error: "Sphere not found" }, 404); 49 } 50 51 // Only the owner can manage modules — changes are synced to PDS which requires the owner's session 52 if (c.var.did !== sphere.ownerDid) { 53 return c.json({ error: "Forbidden" }, 403); 54 } 55 56 const body = await c.req.json(); 57 const parsed = enableModuleSchema.safeParse(body); 58 if (!parsed.success) { 59 return c.json({ error: z.flattenError(parsed.error) }, 400); 60 } 61 62 const moduleName = parsed.data.module; 63 if (!availableModules.includes(moduleName)) { 64 return c.json( 65 { 66 error: `Unknown module: ${moduleName}. Available: ${availableModules.join(", ")}`, 67 }, 68 400, 69 ); 70 } 71 72 getDb() 73 .insert(sphereModules) 74 .values({ sphereId: sphere.id, moduleName }) 75 .onConflictDoNothing() 76 .run(); 77 78 await syncSpherePds(c, sphere); 79 80 return c.json({ 81 modules: formatModules(getEnabledModules(sphere.id)), 82 }); 83 }); 84 85 // Disable module 86 app.delete("/:handle/modules/:moduleName", requireAuth, async (c) => { 87 const sphere = findSphere(c.req.param("handle")); 88 if (!sphere) { 89 return c.json({ error: "Sphere not found" }, 404); 90 } 91 92 if (c.var.did !== sphere.ownerDid) { 93 return c.json({ error: "Forbidden" }, 403); 94 } 95 96 getDb() 97 .delete(sphereModules) 98 .where( 99 and( 100 eq(sphereModules.sphereId, sphere.id), 101 eq(sphereModules.moduleName, c.req.param("moduleName")), 102 ), 103 ) 104 .run(); 105 106 await syncSpherePds(c, sphere); 107 108 return c.json({ 109 modules: formatModules(getEnabledModules(sphere.id)), 110 }); 111 }); 112 113 return app; 114}