diff --git a/docker-compose.yml b/docker-compose.yml index 6edf913..db616da 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,6 +58,67 @@ services: - "traefik.http.routers.rspace-canvas.rule=HostRegexp(`{subdomain:[a-z0-9-]+}.rspace.online`) && !Host(`rspace.online`) && !Host(`www.rspace.online`) && !Host(`auth.rspace.online`)" - "traefik.http.routers.rspace-canvas.entrypoints=web" - "traefik.http.routers.rspace-canvas.priority=100" + # ── Standalone domain routing (priority 120) ── + - "traefik.http.routers.rspace-rbooks.rule=Host(`rbooks.online`)" + - "traefik.http.routers.rspace-rbooks.entrypoints=web" + - "traefik.http.routers.rspace-rbooks.priority=120" + - "traefik.http.routers.rspace-rbooks.service=rspace-online" + - "traefik.http.routers.rspace-rpubs.rule=Host(`rpubs.online`)" + - "traefik.http.routers.rspace-rpubs.entrypoints=web" + - "traefik.http.routers.rspace-rpubs.priority=120" + - "traefik.http.routers.rspace-rpubs.service=rspace-online" + - "traefik.http.routers.rspace-rchoices.rule=Host(`rchoices.online`)" + - "traefik.http.routers.rspace-rchoices.entrypoints=web" + - "traefik.http.routers.rspace-rchoices.priority=120" + - "traefik.http.routers.rspace-rchoices.service=rspace-online" + - "traefik.http.routers.rspace-rfunds.rule=Host(`rfunds.online`)" + - "traefik.http.routers.rspace-rfunds.entrypoints=web" + - "traefik.http.routers.rspace-rfunds.priority=120" + - "traefik.http.routers.rspace-rfunds.service=rspace-online" + - "traefik.http.routers.rspace-rforum.rule=Host(`rforum.online`)" + - "traefik.http.routers.rspace-rforum.entrypoints=web" + - "traefik.http.routers.rspace-rforum.priority=120" + - "traefik.http.routers.rspace-rforum.service=rspace-online" + - "traefik.http.routers.rspace-rvote.rule=Host(`rvote.online`)" + - "traefik.http.routers.rspace-rvote.entrypoints=web" + - "traefik.http.routers.rspace-rvote.priority=120" + - "traefik.http.routers.rspace-rvote.service=rspace-online" + - "traefik.http.routers.rspace-rnotes.rule=Host(`rnotes.online`)" + - "traefik.http.routers.rspace-rnotes.entrypoints=web" + - "traefik.http.routers.rspace-rnotes.priority=120" + - "traefik.http.routers.rspace-rnotes.service=rspace-online" + - "traefik.http.routers.rspace-rwork.rule=Host(`rwork.online`)" + - "traefik.http.routers.rspace-rwork.entrypoints=web" + - "traefik.http.routers.rspace-rwork.priority=120" + - "traefik.http.routers.rspace-rwork.service=rspace-online" + - "traefik.http.routers.rspace-rcal.rule=Host(`rcal.online`)" + - "traefik.http.routers.rspace-rcal.entrypoints=web" + - "traefik.http.routers.rspace-rcal.priority=120" + - "traefik.http.routers.rspace-rcal.service=rspace-online" + - "traefik.http.routers.rspace-rtrips.rule=Host(`rtrips.online`)" + - "traefik.http.routers.rspace-rtrips.entrypoints=web" + - "traefik.http.routers.rspace-rtrips.priority=120" + - "traefik.http.routers.rspace-rtrips.service=rspace-online" + - "traefik.http.routers.rspace-rwallet.rule=Host(`rwallet.online`)" + - "traefik.http.routers.rspace-rwallet.entrypoints=web" + - "traefik.http.routers.rspace-rwallet.priority=120" + - "traefik.http.routers.rspace-rwallet.service=rspace-online" + - "traefik.http.routers.rspace-rdata.rule=Host(`rdata.online`)" + - "traefik.http.routers.rspace-rdata.entrypoints=web" + - "traefik.http.routers.rspace-rdata.priority=120" + - "traefik.http.routers.rspace-rdata.service=rspace-online" + - "traefik.http.routers.rspace-rnetwork.rule=Host(`rnetwork.online`)" + - "traefik.http.routers.rspace-rnetwork.entrypoints=web" + - "traefik.http.routers.rspace-rnetwork.priority=120" + - "traefik.http.routers.rspace-rnetwork.service=rspace-online" + - "traefik.http.routers.rspace-rtube.rule=Host(`rtube.online`)" + - "traefik.http.routers.rspace-rtube.entrypoints=web" + - "traefik.http.routers.rspace-rtube.priority=120" + - "traefik.http.routers.rspace-rtube.service=rspace-online" + - "traefik.http.routers.rspace-rmaps.rule=Host(`rmaps.online`)" + - "traefik.http.routers.rspace-rmaps.entrypoints=web" + - "traefik.http.routers.rspace-rmaps.priority=120" + - "traefik.http.routers.rspace-rmaps.service=rspace-online" # Service configuration - "traefik.http.services.rspace-online.loadbalancer.server.port=3000" - "traefik.docker.network=traefik-public" diff --git a/server/index.ts b/server/index.ts index cdd9026..593089a 100644 --- a/server/index.ts +++ b/server/index.ts @@ -420,6 +420,21 @@ async function serveStatic(path: string): Promise { return null; } +// ── Standalone domain → module lookup ── +const domainToModule = new Map(); +for (const mod of getAllModules()) { + if (mod.standaloneDomain) { + domainToModule.set(mod.standaloneDomain, mod.id); + } +} +// Domains we keep on their own containers (do NOT rewrite) +const keepStandalone = new Set([ + "rcart.online", + "rfiles.online", + "swag.mycofi.earth", + "providers.mycofi.earth", +]); + // ── Bun.serve: WebSocket + fetch delegation ── const server = Bun.serve({ port: PORT, @@ -427,8 +442,52 @@ const server = Bun.serve({ async fetch(req, server) { const url = new URL(req.url); const host = req.headers.get("host"); + const hostClean = host?.split(":")[0] || ""; const subdomain = getSubdomain(host); + // ── Standalone domain rewrite ── + const standaloneModuleId = domainToModule.get(hostClean); + if (standaloneModuleId && !keepStandalone.has(hostClean)) { + // WebSocket upgrade for standalone domains + if (url.pathname.startsWith("/ws/")) { + const communitySlug = url.pathname.split("/")[2]; + if (communitySlug) { + const spaceConfig = await getSpaceConfig(communitySlug); + const claims = await authenticateWSUpgrade(req); + let readOnly = false; + if (spaceConfig) { + const vis = spaceConfig.visibility; + if (vis === "authenticated" || vis === "members_only") { + if (!claims) return new Response("Authentication required", { status: 401 }); + } else if (vis === "public_read") { + readOnly = !claims; + } + } + const peerId = generatePeerId(); + const mode = url.searchParams.get("mode") === "json" ? "json" : "automerge"; + const upgraded = server.upgrade(req, { + data: { communitySlug, peerId, claims, readOnly, mode } as WSData, + }); + if (upgraded) return undefined; + } + return new Response("WebSocket upgrade failed", { status: 400 }); + } + + // Serve static assets from dist (shell.js, shell.css, etc.) + const assetPath = url.pathname.slice(1); + if (assetPath.includes(".") && !url.pathname.startsWith("/api/")) { + const staticResponse = await serveStatic(assetPath); + if (staticResponse) return staticResponse; + } + + // Rewrite: / → /demo/{moduleId}, /foo → /demo/{moduleId}/foo + const suffix = url.pathname === "/" ? "" : url.pathname; + const rewrittenPath = `/demo/${standaloneModuleId}${suffix}`; + const rewrittenUrl = new URL(rewrittenPath + url.search, `http://localhost:${PORT}`); + const rewrittenReq = new Request(rewrittenUrl, req); + return app.fetch(rewrittenReq); + } + // ── WebSocket upgrade ── if (url.pathname.startsWith("/ws/")) { const communitySlug = url.pathname.split("/")[2];