153 lines
3.8 KiB
TypeScript
153 lines
3.8 KiB
TypeScript
/**
|
|
* Space registry — CRUD for rSpace spaces.
|
|
*
|
|
* Spaces are stored as Automerge CRDT documents (extending the existing
|
|
* community-store pattern). This module provides Hono routes for listing,
|
|
* creating, and managing spaces.
|
|
*/
|
|
|
|
import { Hono } from "hono";
|
|
import {
|
|
communityExists,
|
|
createCommunity,
|
|
loadCommunity,
|
|
getDocumentData,
|
|
listCommunities,
|
|
} from "./community-store";
|
|
import type { SpaceVisibility } from "./community-store";
|
|
import {
|
|
verifyEncryptIDToken,
|
|
extractToken,
|
|
} from "@encryptid/sdk/server";
|
|
import type { EncryptIDClaims } from "@encryptid/sdk/server";
|
|
import { getAllModules } from "../shared/module";
|
|
|
|
const spaces = new Hono();
|
|
|
|
// ── List spaces (public + user's own/member spaces) ──
|
|
|
|
spaces.get("/", async (c) => {
|
|
const slugs = await listCommunities();
|
|
|
|
// Check if user is authenticated
|
|
const token = extractToken(c.req.raw.headers);
|
|
let claims: EncryptIDClaims | null = null;
|
|
if (token) {
|
|
try {
|
|
claims = await verifyEncryptIDToken(token);
|
|
} catch {
|
|
// Invalid token — treat as unauthenticated
|
|
}
|
|
}
|
|
|
|
const spacesList = [];
|
|
for (const slug of slugs) {
|
|
await loadCommunity(slug);
|
|
const data = getDocumentData(slug);
|
|
if (data?.meta) {
|
|
const vis = data.meta.visibility || "public_read";
|
|
const isOwner = !!(claims && data.meta.ownerDID === claims.sub);
|
|
const memberEntry = claims ? data.members?.[claims.sub] : undefined;
|
|
const isMember = !!memberEntry;
|
|
|
|
// Include if: public/public_read OR user is owner OR user is member
|
|
if (vis === "public" || vis === "public_read" || isOwner || isMember) {
|
|
spacesList.push({
|
|
slug: data.meta.slug,
|
|
name: data.meta.name,
|
|
visibility: vis,
|
|
createdAt: data.meta.createdAt,
|
|
role: isOwner ? "owner" : memberEntry?.role || undefined,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return c.json({ spaces: spacesList });
|
|
});
|
|
|
|
// ── Create a new space (requires auth) ──
|
|
|
|
spaces.post("/", async (c) => {
|
|
const token = extractToken(c.req.raw.headers);
|
|
if (!token) {
|
|
return c.json({ error: "Authentication required" }, 401);
|
|
}
|
|
|
|
let claims: EncryptIDClaims;
|
|
try {
|
|
claims = await verifyEncryptIDToken(token);
|
|
} catch {
|
|
return c.json({ error: "Invalid or expired token" }, 401);
|
|
}
|
|
|
|
const body = await c.req.json<{
|
|
name?: string;
|
|
slug?: string;
|
|
visibility?: SpaceVisibility;
|
|
}>();
|
|
|
|
const { name, slug, visibility = "public_read" } = body;
|
|
|
|
if (!name || !slug) {
|
|
return c.json({ error: "Name and slug are required" }, 400);
|
|
}
|
|
|
|
if (!/^[a-z0-9-]+$/.test(slug)) {
|
|
return c.json({ error: "Slug must contain only lowercase letters, numbers, and hyphens" }, 400);
|
|
}
|
|
|
|
const validVisibilities: SpaceVisibility[] = ["public", "public_read", "authenticated", "members_only"];
|
|
if (!validVisibilities.includes(visibility)) {
|
|
return c.json({ error: `Invalid visibility. Must be one of: ${validVisibilities.join(", ")}` }, 400);
|
|
}
|
|
|
|
if (await communityExists(slug)) {
|
|
return c.json({ error: "Space already exists" }, 409);
|
|
}
|
|
|
|
await createCommunity(name, slug, claims.sub, visibility);
|
|
|
|
// Notify all modules about the new space
|
|
for (const mod of getAllModules()) {
|
|
if (mod.onSpaceCreate) {
|
|
try {
|
|
await mod.onSpaceCreate(slug);
|
|
} catch (e) {
|
|
console.error(`[Spaces] Module ${mod.id} onSpaceCreate failed:`, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
return c.json({
|
|
slug,
|
|
name,
|
|
visibility,
|
|
ownerDID: claims.sub,
|
|
url: `/${slug}/canvas`,
|
|
}, 201);
|
|
});
|
|
|
|
// ── Get space info ──
|
|
|
|
spaces.get("/:slug", async (c) => {
|
|
const slug = c.req.param("slug");
|
|
await loadCommunity(slug);
|
|
const data = getDocumentData(slug);
|
|
|
|
if (!data) {
|
|
return c.json({ error: "Space not found" }, 404);
|
|
}
|
|
|
|
return c.json({
|
|
slug: data.meta.slug,
|
|
name: data.meta.name,
|
|
visibility: data.meta.visibility,
|
|
createdAt: data.meta.createdAt,
|
|
ownerDID: data.meta.ownerDID,
|
|
memberCount: Object.keys(data.members || {}).length,
|
|
});
|
|
});
|
|
|
|
export { spaces };
|