fix(spaces): default visibility to private (sovereign by default)

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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-21 15:24:00 -07:00
parent 7c29ccea41
commit ca9e91651c
4 changed files with 18 additions and 12 deletions

View File

@ -23,7 +23,7 @@ export function normalizeVisibility(v: string): SpaceVisibility {
if (v === 'public_read' || v === 'public') return 'public'; if (v === 'public_read' || v === 'public') return 'public';
if (v === 'authenticated' || v === 'permissioned') return 'permissioned'; if (v === 'authenticated' || v === 'permissioned') return 'permissioned';
if (v === 'members_only' || v === 'private') return 'private'; if (v === 'members_only' || v === 'private') return 'private';
return 'public'; return 'private'; // sovereign by default — unknown values treated as private
} }
// ── Nest Permissions & Policy ── // ── Nest Permissions & Policy ──
@ -237,7 +237,13 @@ function migrateVisibility(
slug: string, slug: string,
): Automerge.Doc<CommunityDoc> { ): Automerge.Doc<CommunityDoc> {
const v = doc.meta?.visibility as string; 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); const normalized = normalizeVisibility(v);
if (v === normalized) return doc; if (v === normalized) return doc;
console.log(`[Store] Migrating visibility ${v}${normalized} in ${slug}`); console.log(`[Store] Migrating visibility ${v}${normalized} in ${slug}`);

View File

@ -464,7 +464,7 @@ async function getSpaceConfig(slug: string): Promise<SpaceAuthConfig | null> {
if (!doc) return null; if (!doc) return null;
return { return {
spaceSlug: slug, spaceSlug: slug,
visibility: (doc.meta.visibility || "public") as SpaceVisibility, visibility: (doc.meta.visibility || "private") as SpaceVisibility,
ownerDID: doc.meta.ownerDID || undefined, ownerDID: doc.meta.ownerDID || undefined,
app: "rspace", app: "rspace",
}; };
@ -1960,12 +1960,12 @@ app.use("/:space/*", async (c, next) => {
const space = c.req.param("space"); const space = c.req.param("space");
if (!space || space === "api" || space.includes(".")) return; if (!space || space === "api" || space.includes(".")) return;
const config = await getSpaceConfig(space); const config = await getSpaceConfig(space);
const vis = config?.visibility || "public"; const vis = config?.visibility || "private";
if (vis === "public") return; if (vis === "private") return; // Shell already defaults to private
const html = await c.res.text(); const html = await c.res.text();
c.res = new Response( c.res = new Response(
html.replace( html.replace(
'data-space-visibility="public"', 'data-space-visibility="private"',
`data-space-visibility="${vis}"`, `data-space-visibility="${vis}"`,
), ),
{ status: c.res.status, headers: c.res.headers }, { status: c.res.status, headers: c.res.headers },
@ -2179,7 +2179,7 @@ app.get("/admin-data", async (c) => {
spacesList.push({ spacesList.push({
slug: data.meta.slug, slug: data.meta.slug,
name: data.meta.name, name: data.meta.name,
visibility: data.meta.visibility || "public", visibility: data.meta.visibility || "private",
createdAt: data.meta.createdAt, createdAt: data.meta.createdAt,
ownerDID: data.meta.ownerDID, ownerDID: data.meta.ownerDID,
shapeCount, shapeCount,

View File

@ -149,7 +149,7 @@ export function renderShell(opts: ShellOptions): string {
modules, modules,
theme = "dark", theme = "dark",
head = "", head = "",
spaceVisibility = "public", spaceVisibility = "private",
} = opts; } = opts;
// Auto-populate from space data when not explicitly provided // Auto-populate from space data when not explicitly provided

View File

@ -240,7 +240,7 @@ spaces.get("/", async (c) => {
if (seenSlugs.has(slug)) continue; if (seenSlugs.has(slug)) continue;
seenSlugs.add(slug); 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 // Check both claims.sub (raw userId) and did:key: format for
// compatibility — auto-provisioned spaces store ownerDID as did:key: // 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)}`) : ""; 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 // Within each group: user's own spaces first, then alphabetically
const visOrder: Record<string, number> = { private: 0, permissioned: 1, public: 2 }; const visOrder: Record<string, number> = { private: 0, permissioned: 1, public: 2 };
spacesList.sort((a, b) => { spacesList.sort((a, b) => {
const va = visOrder[a.visibility || "public"] ?? 2; const va = visOrder[a.visibility || "private"] ?? 0;
const vb = visOrder[b.visibility || "public"] ?? 2; const vb = visOrder[b.visibility || "private"] ?? 0;
if (va !== vb) return va - vb; if (va !== vb) return va - vb;
if (a.role && !b.role) return -1; if (a.role && !b.role) return -1;
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({ spacesList.push({
slug: data.meta.slug, slug: data.meta.slug,
name: data.meta.name, name: data.meta.name,
visibility: data.meta.visibility || "public", visibility: data.meta.visibility || "private",
createdAt: data.meta.createdAt, createdAt: data.meta.createdAt,
ownerDID: data.meta.ownerDID, ownerDID: data.meta.ownerDID,
shapeCount, shapeCount,