Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m59s Details

This commit is contained in:
Jeff Emmett 2026-04-17 10:18:01 -04:00
commit 609bae45f5
2 changed files with 34 additions and 3 deletions

View File

@ -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: [
{

View File

@ -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<string, string | boolean>) => {
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<string, string | boolean> = {};
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<string, string | boolean> = {};
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;
}