feat(spaces): blank canvas init + team inbox provisioning
New spaces start with an empty canvas instead of 25+ template shapes. Each space gets a {slug}@rspace.online team inbox (multi-sig ready) via the rinbox onSpaceCreate hook. Fix EncryptID auto-provision passing raw string instead of SpaceLifecycleContext to module hooks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
97bf2d7987
commit
d31e8fdca4
|
|
@ -1630,6 +1630,44 @@ routes.get("/", (c) => {
|
||||||
|
|
||||||
// ── Seed template data ──
|
// ── Seed template data ──
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a clean team inbox for a space: {space}@rspace.online
|
||||||
|
* No demo threads — just an empty mailbox ready for multi-sig approval.
|
||||||
|
*/
|
||||||
|
function initSpaceInbox(space: string, ownerDid: string) {
|
||||||
|
if (!_syncServer) return;
|
||||||
|
// Skip if space already has mailboxes
|
||||||
|
const prefix = `${space}:inbox:mailboxes:`;
|
||||||
|
const existing = _syncServer.listDocs().filter((id) => id.startsWith(prefix));
|
||||||
|
if (existing.length > 0) return;
|
||||||
|
|
||||||
|
const mbId = crypto.randomUUID();
|
||||||
|
const docId = mailboxDocId(space, mbId);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const doc = Automerge.change(Automerge.init<MailboxDoc>(), 'init space inbox', (d) => {
|
||||||
|
d.meta = { module: 'inbox', collection: 'mailboxes', version: 2, spaceSlug: space, createdAt: now };
|
||||||
|
d.mailbox = {
|
||||||
|
id: mbId, workspaceId: null, slug: space, name: `${space.charAt(0).toUpperCase() + space.slice(1)} Inbox`,
|
||||||
|
email: `${space}@rspace.online`,
|
||||||
|
description: `Team inbox for the ${space} space. Multi-sig approval can be configured via Gnosis Safe.`,
|
||||||
|
visibility: 'members', ownerDid,
|
||||||
|
safeAddress: null, safeChainId: null, approvalThreshold: 1, createdAt: now,
|
||||||
|
};
|
||||||
|
d.members = [];
|
||||||
|
d.threads = {};
|
||||||
|
d.approvals = {};
|
||||||
|
d.personalInboxes = {};
|
||||||
|
d.agentInboxes = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
_syncServer.setDoc(docId, doc);
|
||||||
|
console.log(`[Inbox] Team inbox provisioned for "${space}": ${space}@rspace.online`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed demo mailbox with sample threads (for demo/template spaces only).
|
||||||
|
*/
|
||||||
function seedTemplateInbox(space: string) {
|
function seedTemplateInbox(space: string) {
|
||||||
if (!_syncServer) return;
|
if (!_syncServer) return;
|
||||||
// Skip if space already has mailboxes
|
// Skip if space already has mailboxes
|
||||||
|
|
@ -1714,7 +1752,7 @@ export const inboxModule: RSpaceModule = {
|
||||||
if (SMTP_USER) getSmtpTransport().catch(() => {});
|
if (SMTP_USER) getSmtpTransport().catch(() => {});
|
||||||
},
|
},
|
||||||
async onSpaceCreate(ctx) {
|
async onSpaceCreate(ctx) {
|
||||||
seedTemplateInbox(ctx.spaceSlug);
|
initSpaceInbox(ctx.spaceSlug, ctx.ownerDID || 'did:unknown');
|
||||||
},
|
},
|
||||||
standaloneDomain: "rinbox.online",
|
standaloneDomain: "rinbox.online",
|
||||||
feeds: [
|
feeds: [
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import {
|
||||||
} from "./community-store";
|
} from "./community-store";
|
||||||
import type { NestPermissions, SpaceRefFilter } from "./community-store";
|
import type { NestPermissions, SpaceRefFilter } from "./community-store";
|
||||||
import { ensureDemoCommunity } from "./seed-demo";
|
import { ensureDemoCommunity } from "./seed-demo";
|
||||||
import { seedTemplateShapes, ensureTemplateSeeding } from "./seed-template";
|
import { seedTemplateShapes } from "./seed-template";
|
||||||
// Campaign demo moved to rsocials module — see modules/rsocials/campaign-data.ts
|
// Campaign demo moved to rsocials module — see modules/rsocials/campaign-data.ts
|
||||||
import type { SpaceVisibility } from "./community-store";
|
import type { SpaceVisibility } from "./community-store";
|
||||||
import {
|
import {
|
||||||
|
|
@ -3076,7 +3076,6 @@ ensureDemoCommunity().then(() => console.log("[Demo] Demo community ready")).cat
|
||||||
import { initTokenService, seedCUSDC } from "./token-service";
|
import { initTokenService, seedCUSDC } from "./token-service";
|
||||||
loadAllDocs(syncServer)
|
loadAllDocs(syncServer)
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
ensureTemplateSeeding();
|
|
||||||
// Seed all modules' demo data so /demo routes always have content
|
// Seed all modules' demo data so /demo routes always have content
|
||||||
for (const mod of getAllModules()) {
|
for (const mod of getAllModules()) {
|
||||||
if (mod.seedTemplate) {
|
if (mod.seedTemplate) {
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ import type { EncryptIDClaims } from "@encryptid/sdk/server";
|
||||||
import { getAllModules, getModule } from "../shared/module";
|
import { getAllModules, getModule } from "../shared/module";
|
||||||
import type { SpaceLifecycleContext } from "../shared/module";
|
import type { SpaceLifecycleContext } from "../shared/module";
|
||||||
import { syncServer } from "./sync-instance";
|
import { syncServer } from "./sync-instance";
|
||||||
import { seedTemplateShapes } from "./seed-template";
|
|
||||||
import { notify, notifySpaceAdmins } from "./notification-service";
|
import { notify, notifySpaceAdmins } from "./notification-service";
|
||||||
|
|
||||||
const ENCRYPTID_URL = process.env.ENCRYPTID_INTERNAL_URL || "http://encryptid:3000";
|
const ENCRYPTID_URL = process.env.ENCRYPTID_INTERNAL_URL || "http://encryptid:3000";
|
||||||
|
|
@ -169,23 +169,9 @@ export async function createSpace(opts: CreateSpaceOpts): Promise<CreateSpaceRes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seed generic template content (non-fatal)
|
// NOTE: New spaces start with a blank canvas.
|
||||||
try {
|
// Template shapes and module demo data are NOT seeded here.
|
||||||
seedTemplateShapes(slug);
|
// The rinbox onSpaceCreate hook provisions a {slug}@rspace.online mailbox.
|
||||||
} catch (e) {
|
|
||||||
console.error(`[createSpace:${source}] Template seeding failed for ${slug}:`, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed module-specific demo data (calendar events, sample tasks, etc.)
|
|
||||||
for (const mod of getAllModules()) {
|
|
||||||
if (mod.seedTemplate) {
|
|
||||||
try {
|
|
||||||
mod.seedTemplate(slug);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`[createSpace:${source}] Module ${mod.id} seedTemplate failed for ${slug}:`, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[createSpace:${source}] Created space: ${slug}`);
|
console.log(`[createSpace:${source}] Created space: ${slug}`);
|
||||||
return { ok: true, slug, name, visibility, ownerDID };
|
return { ok: true, slug, name, visibility, ownerDID };
|
||||||
|
|
|
||||||
|
|
@ -637,10 +637,17 @@ app.post('/api/register/complete', async (c) => {
|
||||||
nestPolicy: DEFAULT_USER_NEST_POLICY,
|
nestPolicy: DEFAULT_USER_NEST_POLICY,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fire module hooks
|
// Fire module hooks with proper lifecycle context
|
||||||
|
const { syncServer } = await import('../../server/sync-instance');
|
||||||
|
const lifecycleCtx = {
|
||||||
|
spaceSlug,
|
||||||
|
ownerDID: did,
|
||||||
|
enabledModules: DEFAULT_USER_MODULES,
|
||||||
|
syncServer,
|
||||||
|
};
|
||||||
for (const mod of getAllModules()) {
|
for (const mod of getAllModules()) {
|
||||||
if (mod.onSpaceCreate) {
|
if (mod.onSpaceCreate) {
|
||||||
try { await mod.onSpaceCreate(spaceSlug); } catch (e) {
|
try { await mod.onSpaceCreate(lifecycleCtx); } catch (e) {
|
||||||
console.error(`[EncryptID] Module ${mod.id} onSpaceCreate for user space:`, e);
|
console.error(`[EncryptID] Module ${mod.id} onSpaceCreate for user space:`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue