diff --git a/modules/rsocials/mod.ts b/modules/rsocials/mod.ts index 2d468d6c..e79754fb 100644 --- a/modules/rsocials/mod.ts +++ b/modules/rsocials/mod.ts @@ -2892,7 +2892,7 @@ export const socialsModule: RSpaceModule = { acceptsFeeds: ["data", "trust"], outputPaths: [ { path: "campaigns", name: "Campaigns", icon: "📢", description: "Social media campaigns" }, - { path: "posts", name: "Posts", icon: "📱", description: "Social feed posts across platforms" }, + { path: "threads", name: "Posts", icon: "💬", description: "Draft posts and tweet-thread builder with live preview" }, ], subPageInfos: [ { diff --git a/server/spaces.ts b/server/spaces.ts index 94d7c75a..aafb66a3 100644 --- a/server/spaces.ts +++ b/server/spaces.ts @@ -365,12 +365,36 @@ spaces.get("/:slug/modules", async (c) => { const doc = getDocumentData(slug); if (!doc?.meta) return c.json({ error: "Space not found" }, 404); + // Caller identity: owner sees raw secrets, everyone else gets password fields redacted + let isOwner = false; + const token = extractToken(c.req.raw.headers); + if (token) { + try { + const claims = await verifyToken(token); + if (doc.meta.ownerDID && claims.sub === doc.meta.ownerDID) isOwner = true; + } catch { /* invalid token → treat as anonymous */ } + } + const allModules = getAllModules(); const enabled = doc.meta.enabledModules; // null = all const overrides = doc.meta.moduleScopeOverrides || {}; const savedSettings = doc.meta.moduleSettings || {}; + const redactSettings = (modId: string, settings: Record) => { + if (isOwner) return settings; + const mod = getModule(modId); + const passwordKeys = new Set( + (mod?.settingsSchema ?? []).filter(f => f.type === 'password').map(f => f.key), + ); + if (passwordKeys.size === 0) return settings; + const redacted: Record = {}; + for (const [k, v] of Object.entries(settings)) { + redacted[k] = passwordKeys.has(k) && typeof v === 'string' && v.length > 0 ? '********' : v; + } + return redacted; + }; + const modules = allModules.map(mod => ({ id: mod.id, name: mod.name, @@ -382,7 +406,7 @@ spaces.get("/:slug/modules", async (c) => { currentScope: overrides[mod.id] || mod.scoping.defaultScope, }, ...(mod.settingsSchema ? { settingsSchema: mod.settingsSchema } : {}), - ...(savedSettings[mod.id] ? { settings: savedSettings[mod.id] } : {}), + ...(savedSettings[mod.id] ? { settings: redactSettings(mod.id, savedSettings[mod.id]) } : {}), })); return c.json({ modules, enabledModules: enabled }); @@ -453,10 +477,17 @@ spaces.patch("/:slug/modules", async (c) => { if (!mod) return c.json({ error: `Unknown module: ${modId}` }, 400); if (!mod.settingsSchema) return c.json({ error: `Module ${modId} has no settings schema` }, 400); const validKeys = new Set(mod.settingsSchema.map(f => f.key)); + const passwordKeys = new Set(mod.settingsSchema.filter(f => f.type === 'password').map(f => f.key)); for (const key of Object.keys(settings)) { if (!validKeys.has(key)) return c.json({ error: `Unknown setting '${key}' for module ${modId}` }, 400); } - merged[modId] = { ...(existing[modId] || {}), ...settings }; + // Preserve existing value when password fields come back as the redaction sentinel + const incoming: Record = {}; + for (const [k, v] of Object.entries(settings)) { + if (passwordKeys.has(k) && v === '********') continue; + incoming[k] = v; + } + merged[modId] = { ...(existing[modId] || {}), ...incoming }; } updates.moduleSettings = merged; }