feat(rsocials): add Listmonk newsletter page + legacy domain redirect

- Add /newsletter-list route embedding Listmonk via iframe
- Add LISTMONK_URL env var to docker-compose
- Add Traefik redirect: social.jeffemmett.com → demo.rspace.online/rsocials
- Add backlog task for linked wallets security hardening (TASK-HIGH.5)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-09 18:15:05 -07:00
parent bc810d34e4
commit 0e9d00d2ac
3 changed files with 88 additions and 0 deletions

View File

@ -0,0 +1,42 @@
---
id: TASK-HIGH.5
title: Link External Wallets to EncryptID + Security Hardening
status: Done
assignee: []
created_date: '2026-03-10 01:07'
updated_date: '2026-03-10 01:08'
labels: []
dependencies: []
parent_task_id: TASK-HIGH
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Implemented EIP-6963 wallet discovery, SIWE ownership verification, server-side AES-256-GCM encrypted storage, and Safe owner addition flow. Full security audit addressed 16 findings across Critical, High, Medium, Low, and Informational categories.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 EIP-6963 provider discovery for browser wallets
- [x] #2 SIWE (Sign-In with Ethereum) ownership verification
- [x] #3 Server-side AES-256-GCM encryption at rest for linked wallet data
- [x] #4 Safe add-owner-proposal with threshold validation
- [x] #5 Security: real encryption replaces Base64 (C-1)
- [x] #6 Security: XSS-safe token name escaping (H-1)
- [x] #7 Security: salted address hashes (H-2)
- [x] #8 Security: rate limiting on nonce endpoint (H-3)
- [x] #9 Security: sender verified against JWT (H-4)
- [x] #10 Security: icon URI sanitization (M-1)
- [x] #11 Security: threshold bounds checking (M-2)
- [x] #12 Security: SSRF prevention via address validation (M-3)
- [x] #13 Security: no cleartext sessionStorage cache (M-4)
- [x] #14 Security: low-severity hardening (L-1 through L-7)
- [x] #15 Security: headers and EIP-712 fixes (I-1, I-9)
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented across 5 commits (c789481, d861c0a, 45f5cea, 92fde65, bc810d3). New files: eip6963.ts, external-signer.ts, linked-wallets.ts. Modified: server.ts, db.ts, session.ts, schema.sql, mod.ts, folk-wallet-viewer.ts. Full security audit: 16 findings (1C, 4H, 4M, 7L, 9I) — all actionable items resolved.
<!-- SECTION:NOTES:END -->

View File

@ -48,6 +48,7 @@ services:
- INFISICAL_AI_CLIENT_SECRET=${INFISICAL_AI_CLIENT_SECRET} - INFISICAL_AI_CLIENT_SECRET=${INFISICAL_AI_CLIENT_SECRET}
- INFISICAL_AI_PROJECT_SLUG=claude-ops - INFISICAL_AI_PROJECT_SLUG=claude-ops
- INFISICAL_AI_SECRET_PATH=/ai - INFISICAL_AI_SECRET_PATH=/ai
- LISTMONK_URL=https://newsletter.cosmolocal.world
depends_on: depends_on:
rspace-db: rspace-db:
condition: service_healthy condition: service_healthy
@ -151,6 +152,15 @@ services:
- "traefik.http.routers.rspace-rsocials.entrypoints=web" - "traefik.http.routers.rspace-rsocials.entrypoints=web"
- "traefik.http.routers.rspace-rsocials.priority=120" - "traefik.http.routers.rspace-rsocials.priority=120"
- "traefik.http.routers.rspace-rsocials.service=rspace-online" - "traefik.http.routers.rspace-rsocials.service=rspace-online"
# ── Legacy redirect: social.jeffemmett.com → demo.rspace.online/rsocials ──
- "traefik.http.routers.rspace-social-redirect.rule=Host(`social.jeffemmett.com`)"
- "traefik.http.routers.rspace-social-redirect.entrypoints=web"
- "traefik.http.routers.rspace-social-redirect.priority=130"
- "traefik.http.middlewares.social-redirect.redirectregex.regex=^https?://social\\.jeffemmett\\.com(.*)"
- "traefik.http.middlewares.social-redirect.redirectregex.replacement=https://demo.rspace.online/rsocials$${1}"
- "traefik.http.middlewares.social-redirect.redirectregex.permanent=true"
- "traefik.http.routers.rspace-social-redirect.middlewares=social-redirect"
- "traefik.http.routers.rspace-social-redirect.service=rspace-online"
# Service configuration # Service configuration
- "traefik.http.services.rspace-online.loadbalancer.server.port=3000" - "traefik.http.services.rspace-online.loadbalancer.server.port=3000"
- "traefik.docker.network=traefik-public" - "traefik.docker.network=traefik-public"

View File

@ -576,6 +576,10 @@ function renderDemoFeedHTML(): string {
// The /scheduler route renders a full-page iframe shell. // The /scheduler route renders a full-page iframe shell.
const POSTIZ_URL = process.env.POSTIZ_URL || "https://demo.rsocials.online"; const POSTIZ_URL = process.env.POSTIZ_URL || "https://demo.rsocials.online";
// ── Listmonk newsletter — embedded via iframe ──
// Listmonk admin at newsletter.cosmolocal.world (not behind CF Access).
const LISTMONK_URL = process.env.LISTMONK_URL || "https://newsletter.cosmolocal.world";
routes.get("/scheduler", (c) => { routes.get("/scheduler", (c) => {
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = (c.get("effectiveSpace" as any) as string) || space; const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
@ -590,6 +594,19 @@ routes.get("/scheduler", (c) => {
})); }));
}); });
routes.get("/newsletter-list", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderExternalAppShell({
title: `Newsletter List — rSocials | rSpace`,
moduleId: "rsocials",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
appName: "Listmonk",
appUrl: LISTMONK_URL,
}));
});
routes.get("/feed", (c) => { routes.get("/feed", (c) => {
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const dataSpace = (c.get("effectiveSpace" as any) as string) || space; const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
@ -670,6 +687,13 @@ routes.get("/", (c) => {
<p>Compose and preview tweet threads with live card preview</p> <p>Compose and preview tweet threads with live card preview</p>
</div> </div>
</a> </a>
<a href="${base}/newsletter-list">
<span class="nav-icon">📧</span>
<div class="nav-body">
<h3>Newsletter List</h3>
<p>Manage newsletter subscribers and send campaigns via Listmonk</p>
</div>
</a>
</nav> </nav>
</div>`, </div>`,
})); }));
@ -740,5 +764,17 @@ export const socialsModule: RSpaceModule = {
{ icon: "🖼️", title: "Share Images", text: "Auto-generate a branded share image of your thread for cross-posting." }, { icon: "🖼️", title: "Share Images", text: "Auto-generate a branded share image of your thread for cross-posting." },
], ],
}, },
{
path: "newsletter-list",
title: "Newsletter List",
icon: "📧",
tagline: "rSocials Tool",
description: "Manage newsletter subscribers and send email campaigns via the embedded Listmonk interface.",
features: [
{ icon: "👥", title: "Subscriber Management", text: "View, import, and manage newsletter subscribers across multiple mailing lists." },
{ icon: "📨", title: "Email Campaigns", text: "Compose and send email campaigns with templates and scheduling." },
{ icon: "📊", title: "Analytics", text: "Track open rates, click-throughs, and subscriber engagement." },
],
},
], ],
}; };