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 === '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<CommunityDoc> {
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}`);

View File

@ -464,7 +464,7 @@ async function getSpaceConfig(slug: string): Promise<SpaceAuthConfig | null> {
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,

View File

@ -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

View File

@ -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<string, number> = { 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,