// The hub's API: Better Auth at /api/auth, the hub's own routes at /api/v1. The website calls it through its own // origin (Next.js rewrites /api to here), so session cookies are first-party; T5DE's proxy calls it with a bearer // token. import { Elysia, t } from "elysia"; import { and, desc, eq, ilike, inArray, ne, or, sql, count, schema, type Database } from "@t5de/db"; import type { Auth } from "./auth"; import { ImvuClient, ImvuUnavailable, NoSuchImvuUser } from "./imvu"; import { Realtime } from "./realtime"; const { user, session, imvuLink, linkChallenge, audit } = schema; export type Deps = { db: Database; auth: Auth; imvu: ImvuClient; realtime?: Realtime }; /** An error shown to the person: the website displays message as is. */ export class Problem extends Error { constructor( readonly status: number, readonly slug: string, message: string, ) { super(message); } } type Person = typeof user.$inferSelect; type Link = typeof imvuLink.$inferSelect; const roles = ["user", "moderator", "admin"] as const; const isModerator = (p: Person) => p.role === "moderator" || p.role === "admin"; // Link codes: "t5de-" and eight letters and digits without look-alikes. function linkCode(): string { const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; const bytes = crypto.getRandomValues(new Uint8Array(8)); const s = [...bytes].map((b) => alphabet[b % alphabet.length]).join(""); return `t5de-${s.slice(0, 4)}-${s.slice(4)}`; } function randomID(bytes = 16): string { return Buffer.from(crypto.getRandomValues(new Uint8Array(bytes))).toString("base64url"); } const challengeLifetime = 24 * 60 * 60 * 1000; /** Allows n attempts per key in a window (IMVU lookups, which cost IMVU a request each). */ class Limiter { private hits = new Map(); allow(key: string, n: number, windowMs: number): boolean { const now = Date.now(); const recent = (this.hits.get(key) ?? []).filter((t) => now - t < windowMs); if (recent.length >= n) { this.hits.set(key, recent); return false; } recent.push(now); this.hits.set(key, recent); if (this.hits.size > 100_000) this.hits.clear(); return true; } } export function createApp({ db, auth, imvu, realtime: rt }: Deps) { const realtime = rt ?? new Realtime(async (imvuId) => { const [l] = await db.select({ userId: imvuLink.userId }).from(imvuLink).where(eq(imvuLink.imvuId, imvuId)); return l?.userId; }); const limiter = new Limiter(); const personView = (p: Person) => ({ id: p.id, username: p.displayUsername ?? p.username ?? p.name, email: p.email, role: (roles as readonly string[]).includes(p.role) ? (p.role as (typeof roles)[number]) : "user", linkCap: p.linkCap, createdAt: p.createdAt.toISOString(), banned: p.banned === true, banReason: p.banReason ?? "", }); const linkView = (l: Link) => { const live = realtime.online().find((o) => o.imvuId === l.imvuId); return { imvuId: l.imvuId, username: l.username, linkedAt: l.linkedAt.toISOString(), ageVerified: l.ageVerified, accessPass: l.accessPass, adult: l.ageVerified || l.accessPass, checkedAt: l.checkedAt.toISOString(), showBadge: l.showBadge, showPresence: l.showPresence, online: live !== undefined, outfit: live?.outfit ?? [], }; }; const audited = (actorId: string | null, action: string, target: string, detail: Record = {}) => db.insert(audit).values({ actorId, action, target, detail }); /** Who's online that viewer may see: accounts showing their presence (moderators see all). */ const visibleOnline = async (viewer: Person | undefined) => { const online = realtime.online(); if (online.length === 0) return []; const links = await db.select().from(imvuLink).where(inArray(imvuLink.imvuId, online.map((o) => o.imvuId))); const mod = viewer !== undefined && isModerator(viewer); return online.flatMap((o) => { const l = links.find((l) => l.imvuId === o.imvuId); if (!l || (!l.showPresence && !mod)) return []; return [ { imvuId: o.imvuId, username: l.username, badge: l.showBadge, since: o.since.toISOString(), outfit: o.outfit, ...(mod ? { hidden: !l.showPresence, personId: o.userId, adult: l.ageVerified || l.accessPass } : {}), }, ]; }); }; const imvuIdParam = t.Object({ imvu: t.Numeric({ minimum: 1 }) }); const ownLink = async (p: Person, imvuId: number) => { const [l] = await db.select().from(imvuLink).where(and(eq(imvuLink.imvuId, imvuId), eq(imvuLink.userId, p.id))); if (!l) throw new Problem(404, "not_found", "That account isn't linked to you."); return l; }; const imvuCall = async (p: Person, call: () => Promise): Promise => { if (!limiter.allow(`imvu:${p.id}`, 20, 60 * 60 * 1000)) throw new Problem(429, "rate_limited", "Too many lookups. Try again in a while."); try { return await call(); } catch (e) { if (e instanceof NoSuchImvuUser) throw new Problem(404, "no_such_imvu_account", "IMVU has no account by that name."); if (e instanceof ImvuUnavailable) throw new Problem(502, "imvu_unavailable", "IMVU didn't answer. Try again in a moment."); throw e; } }; const personById = async (id: string) => { const [p] = await db.select().from(user).where(eq(user.id, id)); if (!p) throw new Problem(404, "not_found", "No such person."); return p; }; const api = new Elysia({ prefix: "/api/v1" }) .error({ Problem }) .onError(({ code, error, set }) => { if (code === "Problem") { set.status = error.status; return { error: error.slug, message: error.message }; } if (code === "VALIDATION") { set.status = 400; return { error: "invalid", message: "Some of that isn't right. Check it and try again." }; } if (code === "NOT_FOUND") { set.status = 404; return { error: "not_found", message: "Not found." }; } }) .derive(async ({ request }) => { const s = await auth.api.getSession({ headers: request.headers }); const person = s?.user ? ((await db.select().from(user).where(eq(user.id, s.user.id)))[0] ?? undefined) : undefined; return { person: person && !person.banned ? person : undefined, sessionId: s?.session.id }; }) .macro({ signedIn: { resolve: ({ person }) => { if (!person) throw new Problem(401, "signed_out", "Sign in first."); return { me: person }; }, }, moderator: { resolve: ({ person }) => { if (!person) throw new Problem(401, "signed_out", "Sign in first."); if (!isModerator(person)) throw new Problem(403, "forbidden", "Only moderators can do this."); return { me: person }; }, }, }) // Who's signed in, and the home page. .get("/me", ({ me }) => ({ person: personView(me) }), { signedIn: true }) .get("/home", async ({ person }) => { const online = (await visibleOnline(person)).length; if (!person) return { onlineCount: online }; const [row] = await db.select({ n: count() }).from(imvuLink).where(eq(imvuLink.userId, person.id)); return { onlineCount: online, links: row?.n ?? 0 }; }) // Who's online. .get("/online", async ({ person }) => ({ online: await visibleOnline(person) })) .get("/online/events", ({ person, request }) => { const encoder = new TextEncoder(); let stop = () => {}; const stream = new ReadableStream({ start(controller) { const send = async () => { const online = await visibleOnline(person); try { controller.enqueue(encoder.encode(`event: online\ndata: ${JSON.stringify({ online })}\n\n`)); } catch {} }; const unsubscribe = realtime.subscribe(() => void send()); const keepAlive = setInterval(() => { try { controller.enqueue(encoder.encode(": keep-alive\n\n")); } catch {} }, 25_000); stop = () => { unsubscribe(); clearInterval(keepAlive); }; request.signal.addEventListener("abort", () => { stop(); try { controller.close(); } catch {} }); void send(); }, cancel() { stop(); }, }); return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-store", Connection: "keep-alive" }, }); }) // Your account. .get( "/account", async ({ me }) => { const links = await db.select().from(imvuLink).where(eq(imvuLink.userId, me.id)).orderBy(imvuLink.linkedAt); const challenges = await db .select() .from(linkChallenge) .where(and(eq(linkChallenge.userId, me.id), sql`${linkChallenge.createdAt} > now() - interval '1 day'`)) .orderBy(linkChallenge.createdAt); const [computers] = await db .select({ n: count() }) .from(session) .where(and(eq(session.userId, me.id), ilike(session.userAgent, "t5de-proxy%"), sql`${session.expiresAt} > now()`)); return { person: personView(me), links: links.map(linkView), challenges: challenges.map((c) => ({ imvuId: c.imvuId, username: c.username, code: c.code, createdAt: c.createdAt.toISOString(), expiresAt: new Date(c.createdAt.getTime() + challengeLifetime).toISOString(), })), computers: computers?.n ?? 0, canLink: links.length < me.linkCap, }; }, { signedIn: true }, ) .post( "/account/links", async ({ me, body, set }) => { const found = await imvuCall(me, () => imvu.byUsername(body.username)); const [existing] = await db.select().from(imvuLink).where(eq(imvuLink.imvuId, found.id)); if (existing?.userId === me.id) throw new Problem(409, "already_linked", `${found.username} is already linked.`); const code = linkCode(); const [c] = await db .insert(linkChallenge) .values({ userId: me.id, imvuId: found.id, username: found.username, code }) .onConflictDoUpdate({ target: [linkChallenge.userId, linkChallenge.imvuId], set: { code, username: found.username, createdAt: new Date() }, }) .returning(); set.status = 201; return { challenge: { imvuId: c!.imvuId, username: c!.username, code: c!.code, createdAt: c!.createdAt.toISOString(), expiresAt: new Date(c!.createdAt.getTime() + challengeLifetime).toISOString(), }, }; }, { signedIn: true, body: t.Object({ username: t.String({ minLength: 1, maxLength: 64 }) }) }, ) .post( "/account/links/:imvu/check", async ({ me, params }) => { const [c] = await db .select() .from(linkChallenge) .where( and( eq(linkChallenge.userId, me.id), eq(linkChallenge.imvuId, params.imvu), sql`${linkChallenge.createdAt} > now() - interval '1 day'`, ), ); if (!c) throw new Problem(410, "expired", "That code expired. Start again."); const found = await imvuCall(me, () => imvu.byId(params.imvu)); if (!found.tagline.toLowerCase().includes(c.code.toLowerCase())) throw new Problem( 409, "code_missing", `${found.username}'s tagline doesn't have the code yet. IMVU can take a minute to show a new tagline.`, ); const result = await db.transaction(async (tx) => { const [{ n } = { n: 0 }] = await tx .select({ n: count() }) .from(imvuLink) .where(and(eq(imvuLink.userId, me.id), ne(imvuLink.imvuId, found.id))); if (n >= me.linkCap) throw new Problem(403, "link_cap", `You can link up to ${me.linkCap} IMVU accounts. Ask a moderator if you need more.`); const [prev] = await tx.select().from(imvuLink).where(eq(imvuLink.imvuId, found.id)); const moved = prev !== undefined && prev.userId !== me.id; const values = { userId: me.id, username: found.username, ageVerified: found.ageVerified, accessPass: found.accessPass, checkedAt: new Date(), }; let link: Link; if (!prev) { [link] = (await tx.insert(imvuLink).values({ imvuId: found.id, pairwiseId: randomID(), ...values }).returning()) as [Link]; } else if (moved) { // A moved account gets a new pairwise ID and starts with nothing shown: nothing connects the new owner // to the old one. [link] = (await tx .update(imvuLink) .set({ ...values, pairwiseId: randomID(), linkedAt: new Date(), showBadge: false, showPresence: false }) .where(eq(imvuLink.imvuId, found.id)) .returning()) as [Link]; } else { [link] = (await tx.update(imvuLink).set(values).where(eq(imvuLink.imvuId, found.id)).returning()) as [Link]; } await tx.delete(linkChallenge).where(eq(linkChallenge.imvuId, found.id)); await tx.insert(audit).values({ actorId: me.id, action: "link.add", target: String(found.id), detail: moved ? { imvu: found.username, movedFrom: prev!.userId } : { imvu: found.username }, }); return { link, moved, previousOwner: prev?.userId }; }); if (result.moved) realtime.unlinked(result.previousOwner!, found.id); realtime.linked(me.id, found.id); return { link: linkView(result.link), moved: result.moved }; }, { signedIn: true, params: imvuIdParam }, ) .delete( "/account/challenges/:imvu", async ({ me, params, set }) => { await db.delete(linkChallenge).where(and(eq(linkChallenge.userId, me.id), eq(linkChallenge.imvuId, params.imvu))); set.status = 204; }, { signedIn: true, params: imvuIdParam }, ) .patch( "/account/links/:imvu", async ({ me, params, body }) => { await ownLink(me, params.imvu); const [l] = await db .update(imvuLink) .set({ ...(body.showBadge !== undefined ? { showBadge: body.showBadge } : {}), ...(body.showPresence !== undefined ? { showPresence: body.showPresence } : {}), }) .where(eq(imvuLink.imvuId, params.imvu)) .returning(); return { link: linkView(l!) }; }, { signedIn: true, params: imvuIdParam, body: t.Object({ showBadge: t.Optional(t.Boolean()), showPresence: t.Optional(t.Boolean()) }), }, ) .post( "/account/links/:imvu/recheck", async ({ me, params }) => { await ownLink(me, params.imvu); const found = await imvuCall(me, () => imvu.byId(params.imvu)); const [l] = await db .update(imvuLink) .set({ username: found.username, ageVerified: found.ageVerified, accessPass: found.accessPass, checkedAt: new Date() }) .where(eq(imvuLink.imvuId, params.imvu)) .returning(); return { link: linkView(l!) }; }, { signedIn: true, params: imvuIdParam }, ) .delete( "/account/links/:imvu", async ({ me, params, set }) => { const l = await ownLink(me, params.imvu); await db.delete(imvuLink).where(eq(imvuLink.imvuId, l.imvuId)); realtime.unlinked(me.id, l.imvuId); await audited(me.id, "link.remove", String(l.imvuId), { imvu: l.username }); set.status = 204; }, { signedIn: true, params: imvuIdParam }, ) .post( "/account/signout-computers", async ({ me, set }) => { await db.delete(session).where(and(eq(session.userId, me.id), ilike(session.userAgent, "t5de-proxy%"))); realtime.disconnect(me.id); set.status = 204; }, { signedIn: true }, ) // A proxy's sign-in code, approved by the person on the website. .post( "/device/approve", async ({ me, body, request, set }) => { const raw = body.code.toUpperCase().replace(/[\s-]/g, ""); try { // Viewing the code claims it for this person; then they approve it. await auth.api.deviceVerify({ query: { user_code: raw }, headers: request.headers }); await auth.api.deviceApprove({ body: { userCode: raw }, headers: request.headers }); } catch { throw new Problem(404, "not_found", "That code isn't right, or it expired. Check the code T5DE shows."); } await audited(me.id, "proxy.signin", ""); set.status = 204; }, { signedIn: true, body: t.Object({ code: t.String({ minLength: 4, maxLength: 32 }) }) }, ) // The console. .get( "/console", async ({ me }) => { const [counts] = await db .select({ people: sql`(select count(*) from ${user})::int`, links: sql`(select count(*) from ${imvuLink})::int`, computers: sql`(select count(*) from ${session} where ${session.userAgent} ilike 't5de-proxy%' and ${session.expiresAt} > now())::int`, }) .from(sql`(select 1) as one`); const log = await db .select({ at: audit.at, actor: user.username, action: audit.action, target: audit.target, detail: audit.detail }) .from(audit) .leftJoin(user, eq(user.id, audit.actorId)) .orderBy(desc(audit.id)) .limit(50); return { counts: counts!, connections: realtime.connections(), online: await visibleOnline(me), audit: log.map((e) => ({ ...e, at: e.at.toISOString(), actor: e.actor ?? "" })), }; }, { moderator: true }, ) .get( "/console/people", async ({ query }) => { const q = (query.q ?? "").trim(); const like = `%${q.replace(/[%_\\]/g, (c) => "\\" + c)}%`; const rows = await db .select() .from(user) .where( q ? or( ilike(user.username, like), inArray(user.id, db.select({ id: imvuLink.userId }).from(imvuLink).where(ilike(imvuLink.username, like))), ) : undefined, ) .orderBy(desc(user.createdAt)) .limit(100); return { people: rows.map(personView) }; }, { moderator: true, query: t.Object({ q: t.Optional(t.String({ maxLength: 64 })) }) }, ) .get( "/console/people/:id", async ({ params }) => { const p = await personById(params.id); const links = await db.select().from(imvuLink).where(eq(imvuLink.userId, p.id)).orderBy(imvuLink.linkedAt); const [computers] = await db .select({ n: count() }) .from(session) .where(and(eq(session.userId, p.id), ilike(session.userAgent, "t5de-proxy%"), sql`${session.expiresAt} > now()`)); return { person: personView(p), links: links.map(linkView), computers: computers?.n ?? 0 }; }, { moderator: true }, ) .post( "/console/people/:id/link-cap", async ({ me, params, body }) => { const p = await personById(params.id); const [updated] = await db.update(user).set({ linkCap: body.linkCap }).where(eq(user.id, p.id)).returning(); await audited(me.id, "person.link_cap", p.username ?? p.id, { from: p.linkCap, to: body.linkCap }); return { person: personView(updated!) }; }, { moderator: true, body: t.Object({ linkCap: t.Integer({ minimum: 0, maximum: 1000 }) }) }, ) .post( "/console/people/:id/ban", async ({ me, params, body }) => { const p = await personById(params.id); if (p.id === me.id) throw new Problem(400, "invalid", "You can't ban yourself."); if (isModerator(p) && me.role !== "admin") throw new Problem(403, "forbidden", "Only admins can ban moderators."); const [updated] = await db.update(user).set({ banned: true, banReason: body.reason }).where(eq(user.id, p.id)).returning(); await db.delete(session).where(eq(session.userId, p.id)); realtime.disconnect(p.id); await audited(me.id, "person.ban", p.username ?? p.id, { reason: body.reason }); return { person: personView(updated!) }; }, { moderator: true, body: t.Object({ reason: t.String({ minLength: 1, maxLength: 500 }) }) }, ) .post( "/console/people/:id/unban", async ({ me, params }) => { const p = await personById(params.id); const [updated] = await db.update(user).set({ banned: false, banReason: null }).where(eq(user.id, p.id)).returning(); await audited(me.id, "person.unban", p.username ?? p.id); return { person: personView(updated!) }; }, { moderator: true }, ) .post( "/console/people/:id/role", async ({ me, params, body }) => { if (me.role !== "admin") throw new Problem(403, "forbidden", "Only admins can change roles."); const p = await personById(params.id); if (p.id === me.id) throw new Problem(400, "invalid", "Another admin has to change your role."); const [updated] = await db.update(user).set({ role: body.role }).where(eq(user.id, p.id)).returning(); await audited(me.id, "person.role", p.username ?? p.id, { from: p.role, to: body.role }); return { person: personView(updated!) }; }, { moderator: true, body: t.Object({ role: t.Union([t.Literal("user"), t.Literal("moderator"), t.Literal("admin")]) }), }, ) // T5DE's proxy: the live connection. .ws("/realtime", { async beforeHandle({ person, set }) { if (!person) { set.status = 401; return { error: "signed_out", message: "Sign in first." }; } }, open(ws) { const { person } = ws.data as unknown as { person: Person }; realtime.open(ws.id, person.id, () => ws.close(4001, "signed out")); ws.send(JSON.stringify({ type: "welcome" })); }, async message(ws, message) { const reply = await realtime.message(ws.id, message); if (reply) ws.send(JSON.stringify(reply)); }, close(ws) { realtime.close(ws.id); }, }); const app = new Elysia() .all("/api/auth/*", ({ request }) => auth.handler(request)) .get("/.well-known/jwks.json", async () => auth.api.getJwks()) .get("/health", () => ({ ok: true })) .use(api); return { app, realtime }; } export type App = ReturnType["app"];