159 lines
6.2 KiB
TypeScript
159 lines
6.2 KiB
TypeScript
/**
|
|
* Docs module — collaborative documentation via Docmost.
|
|
*
|
|
* Wraps the Docmost instance as an external app embedded in the rSpace shell.
|
|
*/
|
|
|
|
import { Hono } from "hono";
|
|
import * as Automerge from "@automerge/automerge";
|
|
import { renderShell, renderExternalAppShell } from "../../server/shell";
|
|
import { getModuleInfoList } from "../../shared/module";
|
|
import type { RSpaceModule } from "../../shared/module";
|
|
import { verifyToken, extractToken } from "../../server/auth";
|
|
import type { SyncServer } from '../../server/local-first/sync-server';
|
|
import { docsSchema, docsDocId } from './schemas';
|
|
import type { DocsDoc, LinkedDocument } from './schemas';
|
|
|
|
let _syncServer: SyncServer | null = null;
|
|
|
|
const routes = new Hono();
|
|
|
|
const DOCMOST_URL = "https://docs.cosmolocal.world";
|
|
|
|
// ── Local-first helpers ──
|
|
|
|
function ensureDocsDoc(space: string): DocsDoc {
|
|
const docId = docsDocId(space);
|
|
let doc = _syncServer!.getDoc<DocsDoc>(docId);
|
|
if (!doc) {
|
|
doc = Automerge.change(Automerge.init<DocsDoc>(), 'init docs registry', (d) => {
|
|
const init = docsSchema.init();
|
|
Object.assign(d, init);
|
|
d.meta.spaceSlug = space;
|
|
});
|
|
_syncServer!.setDoc(docId, doc);
|
|
}
|
|
return doc;
|
|
}
|
|
|
|
// ── CRUD: Document Registry ──
|
|
|
|
routes.get("/api/registry", (c) => {
|
|
if (!_syncServer) return c.json({ documents: [] });
|
|
const space = c.req.param("space") || "demo";
|
|
const doc = ensureDocsDoc(space);
|
|
return c.json({ documents: Object.values(doc.linkedDocuments || {}) });
|
|
});
|
|
|
|
routes.post("/api/registry", async (c) => {
|
|
const token = extractToken(c.req.raw.headers);
|
|
if (!token) return c.json({ error: "Authentication required" }, 401);
|
|
let claims;
|
|
try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
|
|
if (!_syncServer) return c.json({ error: "Not initialized" }, 503);
|
|
const space = c.req.param("space") || "demo";
|
|
const { url, title } = await c.req.json();
|
|
if (!url || !title) return c.json({ error: "url and title required" }, 400);
|
|
const id = crypto.randomUUID();
|
|
const docId = docsDocId(space);
|
|
ensureDocsDoc(space);
|
|
_syncServer.changeDoc<DocsDoc>(docId, `register document ${id}`, (d) => {
|
|
d.linkedDocuments[id] = { id, url, title, addedBy: claims.sub || null, addedAt: Date.now() };
|
|
});
|
|
const updated = _syncServer.getDoc<DocsDoc>(docId)!;
|
|
return c.json(updated.linkedDocuments[id], 201);
|
|
});
|
|
|
|
routes.delete("/api/registry/:docRefId", async (c) => {
|
|
const token = extractToken(c.req.raw.headers);
|
|
if (!token) return c.json({ error: "Authentication required" }, 401);
|
|
try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
|
|
if (!_syncServer) return c.json({ error: "Not initialized" }, 503);
|
|
const space = c.req.param("space") || "demo";
|
|
const docRefId = c.req.param("docRefId");
|
|
const docId = docsDocId(space);
|
|
const doc = ensureDocsDoc(space);
|
|
if (!doc.linkedDocuments[docRefId]) return c.json({ error: "Not found" }, 404);
|
|
_syncServer.changeDoc<DocsDoc>(docId, `unregister document ${docRefId}`, (d) => { delete d.linkedDocuments[docRefId]; });
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
routes.get("/api/health", (c) => {
|
|
return c.json({ ok: true, module: "rdocs" });
|
|
});
|
|
|
|
routes.get("/", (c) => {
|
|
const space = c.req.param("space") || "demo";
|
|
const dataSpace = c.get("effectiveSpace") || space;
|
|
const view = c.req.query("view");
|
|
|
|
if (view === "demo") {
|
|
return c.html(renderShell({
|
|
title: `${space} — Docs | rSpace`,
|
|
moduleId: "rdocs",
|
|
spaceSlug: space,
|
|
modules: getModuleInfoList(),
|
|
theme: "dark",
|
|
body: `<div style="max-width:640px;margin:0 auto;padding:3rem 1rem;text-align:center">
|
|
<div style="font-size:3rem;margin-bottom:1rem">📝</div>
|
|
<h2 style="font-size:1.5rem;margin-bottom:0.75rem;background:linear-gradient(135deg,#14b8a6,#22d3ee);-webkit-background-clip:text;-webkit-text-fill-color:transparent">rDocs</h2>
|
|
<p style="color:#94a3b8;margin-bottom:2rem;line-height:1.6">Collaborative documentation powered by Docmost. Create wikis, knowledge bases, and shared documents for your community.</p>
|
|
<a href="?" class="rapp-nav__btn--app-toggle" style="display:inline-block;padding:10px 24px;font-size:0.9rem">Open Docmost</a>
|
|
</div>`,
|
|
}));
|
|
}
|
|
|
|
// Default: show the external app directly
|
|
return c.html(renderExternalAppShell({
|
|
title: `${space} — Docmost | rSpace`,
|
|
moduleId: "rdocs",
|
|
spaceSlug: space,
|
|
modules: getModuleInfoList(),
|
|
appUrl: DOCMOST_URL,
|
|
appName: "Docmost",
|
|
theme: "dark",
|
|
}));
|
|
});
|
|
|
|
function renderDocsLanding(): string {
|
|
return `<div style="max-width:640px;margin:0 auto;padding:3rem 1rem;text-align:center">
|
|
<div style="font-size:3rem;margin-bottom:1rem">📝</div>
|
|
<h2 style="font-size:1.5rem;margin-bottom:0.75rem;background:linear-gradient(135deg,#14b8a6,#22d3ee);-webkit-background-clip:text;-webkit-text-fill-color:transparent">rDocs</h2>
|
|
<p style="color:#94a3b8;margin-bottom:2rem;line-height:1.6">Collaborative documentation powered by Docmost. Create wikis, knowledge bases, and shared documents for your community.</p>
|
|
</div>`;
|
|
}
|
|
|
|
// ── MI Integration ──
|
|
|
|
export function getLinkedDocsForMI(space: string, limit = 5): { id: string; title: string; url: string; addedAt: number }[] {
|
|
if (!_syncServer) return [];
|
|
const docId = docsDocId(space);
|
|
const doc = _syncServer.getDoc<DocsDoc>(docId);
|
|
if (!doc) return [];
|
|
return Object.values(doc.linkedDocuments)
|
|
.sort((a, b) => b.addedAt - a.addedAt)
|
|
.slice(0, limit)
|
|
.map((d) => ({ id: d.id, title: d.title, url: d.url, addedAt: d.addedAt }));
|
|
}
|
|
|
|
export const docsModule: RSpaceModule = {
|
|
id: "rdocs",
|
|
name: "rDocs",
|
|
icon: "📝",
|
|
description: "Collaborative documentation and knowledge base",
|
|
scoping: { defaultScope: 'global', userConfigurable: true },
|
|
docSchemas: [{ pattern: '{space}:docs:links', description: 'Linked Docmost documents per space', init: docsSchema.init }],
|
|
routes,
|
|
landingPage: renderDocsLanding,
|
|
externalApp: { url: DOCMOST_URL, name: "Docmost" },
|
|
async onInit(ctx) { _syncServer = ctx.syncServer; },
|
|
feeds: [
|
|
{ id: "documents", name: "Documents", kind: "data", description: "Collaborative documents and wiki pages" },
|
|
],
|
|
acceptsFeeds: ["data"],
|
|
outputPaths: [
|
|
{ path: "documents", name: "Documents", icon: "📝", description: "Collaborative documents" },
|
|
{ path: "wikis", name: "Wikis", icon: "📖", description: "Knowledge base wikis" },
|
|
],
|
|
};
|