From ca9e91651c5862c86c80168c321a4941305a9649 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sat, 21 Mar 2026 15:24:00 -0700 Subject: [PATCH] fix(spaces): default visibility to private (sovereign by default) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spaces with missing/undefined visibility were falling through to "public" in 7 places: normalizeVisibility fallback, migrateVisibility early return, renderShell default, getSpaceConfig, space list APIs, and the HTML injection middleware. All now default to "private". The migrateVisibility function now writes "private" to docs with missing visibility on load. Also fixed jeff and hash spaces on production (were undefined → private). Co-Authored-By: Claude Opus 4.6 --- server/community-store.ts | 10 ++++++++-- server/index.ts | 10 +++++----- server/shell.ts | 2 +- server/spaces.ts | 8 ++++---- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/server/community-store.ts b/server/community-store.ts index e7a379b..9107b43 100644 --- a/server/community-store.ts +++ b/server/community-store.ts @@ -23,7 +23,7 @@ export function normalizeVisibility(v: string): SpaceVisibility { if (v === 'public_read' || v === 'public') return 'public'; if (v === 'authenticated' || v === 'permissioned') return 'permissioned'; if (v === 'members_only' || v === 'private') return 'private'; - return 'public'; + return 'private'; // sovereign by default — unknown values treated as private } // ── Nest Permissions & Policy ── @@ -237,7 +237,13 @@ function migrateVisibility( slug: string, ): Automerge.Doc { const v = doc.meta?.visibility as string; - if (!v) return doc; + if (!v) { + // Missing visibility — default to private (sovereign by default) + console.log(`[Store] Migrating missing visibility→private in ${slug}`); + return Automerge.change(doc, `Set default visibility to private in ${slug}`, (d) => { + d.meta.visibility = 'private'; + }); + } const normalized = normalizeVisibility(v); if (v === normalized) return doc; console.log(`[Store] Migrating visibility ${v}→${normalized} in ${slug}`); diff --git a/server/index.ts b/server/index.ts index 0a8ed84..f2e825c 100644 --- a/server/index.ts +++ b/server/index.ts @@ -464,7 +464,7 @@ async function getSpaceConfig(slug: string): Promise { if (!doc) return null; return { spaceSlug: slug, - visibility: (doc.meta.visibility || "public") as SpaceVisibility, + visibility: (doc.meta.visibility || "private") as SpaceVisibility, ownerDID: doc.meta.ownerDID || undefined, app: "rspace", }; @@ -1960,12 +1960,12 @@ app.use("/:space/*", async (c, next) => { const space = c.req.param("space"); if (!space || space === "api" || space.includes(".")) return; const config = await getSpaceConfig(space); - const vis = config?.visibility || "public"; - if (vis === "public") return; + const vis = config?.visibility || "private"; + if (vis === "private") return; // Shell already defaults to private const html = await c.res.text(); c.res = new Response( html.replace( - 'data-space-visibility="public"', + 'data-space-visibility="private"', `data-space-visibility="${vis}"`, ), { status: c.res.status, headers: c.res.headers }, @@ -2179,7 +2179,7 @@ app.get("/admin-data", async (c) => { spacesList.push({ slug: data.meta.slug, name: data.meta.name, - visibility: data.meta.visibility || "public", + visibility: data.meta.visibility || "private", createdAt: data.meta.createdAt, ownerDID: data.meta.ownerDID, shapeCount, diff --git a/server/shell.ts b/server/shell.ts index 13abe2d..347e7d2 100644 --- a/server/shell.ts +++ b/server/shell.ts @@ -149,7 +149,7 @@ export function renderShell(opts: ShellOptions): string { modules, theme = "dark", head = "", - spaceVisibility = "public", + spaceVisibility = "private", } = opts; // Auto-populate from space data when not explicitly provided diff --git a/server/spaces.ts b/server/spaces.ts index a7bf471..b159c3e 100644 --- a/server/spaces.ts +++ b/server/spaces.ts @@ -240,7 +240,7 @@ spaces.get("/", async (c) => { if (seenSlugs.has(slug)) continue; seenSlugs.add(slug); - let vis = data.meta.visibility || "public"; + let vis = data.meta.visibility || "private"; // Check both claims.sub (raw userId) and did:key: format for // compatibility — auto-provisioned spaces store ownerDID as did:key: const callerDid = claims ? ((claims.did as string) || `did:key:${(claims.sub as string).slice(0, 32)}`) : ""; @@ -298,8 +298,8 @@ spaces.get("/", async (c) => { // Within each group: user's own spaces first, then alphabetically const visOrder: Record = { private: 0, permissioned: 1, public: 2 }; spacesList.sort((a, b) => { - const va = visOrder[a.visibility || "public"] ?? 2; - const vb = visOrder[b.visibility || "public"] ?? 2; + const va = visOrder[a.visibility || "private"] ?? 0; + const vb = visOrder[b.visibility || "private"] ?? 0; if (va !== vb) return va - vb; if (a.role && !b.role) return -1; if (!a.role && b.role) return 1; @@ -522,7 +522,7 @@ spaces.get("/admin", async (c) => { spacesList.push({ slug: data.meta.slug, name: data.meta.name, - visibility: data.meta.visibility || "public", + visibility: data.meta.visibility || "private", createdAt: data.meta.createdAt, ownerDID: data.meta.ownerDID, shapeCount,