Catalog of all rApp mini-canvas flow-node types proposed for promotion
to AppletDefinition so the main rSpace canvas can render them. 53
proposed applets across rsocials (planner + workflow), rminders,
rflows, rgov, rtime, rnetwork, rchoices. Review doc — no code changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract the rSpace main canvas wheel + touch interaction model into a
single shared controller (shared/canvas-interaction.ts). Wheel defaults
to pan, Ctrl/Cmd+wheel or trackpad pinch (ctrlKey) zooms at cursor,
two-finger touch pans + pinch-zooms at gesture center.
Migrate 8 rApp mini-canvases to use it, replacing 8 slightly-different
hand-rolled wheel handlers:
- rsocials campaign-planner (the bug report — was zoom-only)
- rsocials campaign-workflow
- rminders automation canvas (was zoom-only)
- rtime timebank weave
- rflows canvas
- rgov circuit
- rnetwork CRM graph (was zoom-only)
- lib/applet-circuit-canvas (was zoom-only)
Fixes the "wheel = zoom" regression everywhere and gives each rApp
canvas two-finger touch pan + pinch for free. Pointer-based pan and
marquee selection remain per-rApp because they depend on per-rApp
hit-testing. Bump asset cache versions for all affected routes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Align /rsocials/campaign canvas with main rspace canvas behavior:
two-finger scroll now pans; Ctrl/Cmd+wheel or trackpad pinch
(ctrlKey) zooms at cursor. Previously any wheel event zoomed.
Sibling folk-campaign-workflow.ts already used this pattern.
Bump asset cache to v=4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Outbound: forward per-node platformSettings + title through
createPost/createThread via new PostizIntegrationRef.settings/title
merged on top of defaultSettingsFor. Record postizSyncedHash on the
node after each successful send.
Inbound: postizReconcile pulls remote content/publishDate onto queued
and draft nodes when the local hash matches postizSyncedHash, so
Postiz-side edits surface in the planner without clobbering pending
local edits. Published posts always take remote content as final.
Drift→Resync: inline editor shows a yellow "Local edits not yet on
Postiz" row when client SHA-1 (normalized content+title+schedule+tags+
platformSettings) drifts from postizSyncedHash. New
/resync-postiz route deletes the Postiz-side post (via new deletePost
helper, 404-tolerant) then re-creates with current content. Rejected
once published.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend campaign planner with platform-aware post composition: optional
title (YouTube/TikTok/Reddit/Pinterest), mediaUrls attachments, and
per-platform settings overrides merged into Postiz __type. New
platform-specs.ts library encodes platform constraints. Bump asset
cache version to v=3.
Campaigns dashboard now sends Authorization headers on flow fetch/create
and surfaces 401/403 with a clear message instead of silently failing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Unify space invite emails across three code paths (new-user identity
invite, existing-user invite, add-by-username, plus notification-service
fallback). Template mentions the space name + rSpace, lists shared
collaboration tools, and shows a large gradient CTA button. Pass
spaceName through from server/spaces.ts so the display uses the
friendly name (e.g. "Crypto Commons") instead of the slug.
Also fix a bug in notification-service where actionUrl starting with
http would be double-prefixed with https://{slug}.rspace.online.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fal.ai was returning "invalid images found in the input, failed to
read/process" because it couldn't reliably fetch the staged image URL
through Cloudflare/Traefik. Encode the resized JPEG inline so the fetch
stays inside the fal.ai request and doesn't depend on our origin being
reachable from their crawler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tapping inline "browse" fired both the browse handler and the bubbled
drop-area handler, causing mobile Safari to cancel and reopen the file
picker so the first selection was silently dropped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a non-HttpOnly `rspace_hint` cookie scoped to .rspace.online carrying
only the public username (+ optional displayName). No JWT, session, or
credential crosses origins — WebAuthn RP ID rspace.online still drives
the actual ceremony, so moving between space subdomains requires a single
passkey tap instead of re-registering an account.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mailcow's JSON API returns HTTP 200 with a generic "Task completed" message
even when a write silently no-ops — e.g. wrong payload shape (array-wrapped
instead of {items, attr}), missing authsource, or any per-item ACL/validation
failure. The previous client only checked res.ok, so those failures looked
like successes and reconciler loops kept running without noticing.
parseMailcowResponse now:
- requires HTTP 2xx (unchanged)
- reads the body (array or object) and throws on type=danger/error
- requires a specific per-item success marker (alias_added, mailbox_modified,
etc.) — the generic "Task completed" fallback indicates every item was
silently skipped and raises a "likely silent no-op" error.
Applied to createAlias / updateAlias / deleteAlias / createMailbox /
deleteMailbox. Adds updateMailbox for password+attribute edits with the
correct {items, attr} shape and authsource gate pre-filled so new callers
can't hit the silent-swallow footgun.
- Drop beforeunload leave broadcast — was flashing users offline during
intra-space page nav between rApps
- Heartbeat 10s → 5s; GC stale threshold 15s → 12s
- Presence payload now carries userId; overlay keys peer map by userId
so reconnects with a new WS peerId map back to the same row
- uniquePeers() prefers entries with module/context metadata so the panel
shows which rApp a peer is in, even alongside cursor-only awareness
Postiz's POST /public/v1/posts response shape doesn't reliably include the
new post's id (varies by version). When parsing fails, query listPosts in a
narrow window right after create and match by integration+content to recover
the real id. Without this, reconcile can't find queued posts back in Postiz.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier version only skipped when postizPostId was set. If a send attempt
failed (no postizPostId but postizStatus='failed'), the sweep retried every
60s and hammered Postiz's throttler. Retries now require the user to clear
postizStatus — same gate as a fresh node.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Postiz's /public/v1/posts expects a nested envelope:
{ type, date, shortLink, tags, posts: [{ integration:{id}, value:[{content}], settings:{__type} }] }
The earlier shim's flat {content, integrationIds, type, scheduledAt} was
rejected with "All posts must have an integration id" (the field is named
integration, not integrationIds; it's a nested object, not an array of strings).
Also: Postiz treats PostItem.value[] as thread segments, so createThread now
sends ONE request with multiple value entries rather than N grouped posts.
Callers now pass full {id, identifier} tuples so settings.__type is set
correctly (otherwise Postiz can't route to the provider handler).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Postiz's Nest backend expects Authorization: <apiKey> (no "Bearer"
prefix) and the public API is reverse-proxied at
<hostname>/api/public/v1/*. Our client was hitting /public/v1/*
directly and using Bearer, which 401'd.
Tolerate both config.url shapes (with or without /api) so older
module settings that point at the root host keep working.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-post "Schedule to Postiz" button on Post nodes (rSpace → Postiz), a 60s
server sweep that auto-pushes Scheduled posts within a 10min lead window,
and a Postiz → rSpace reconcile poll that flips postizStatus to
'published' (or 'failed') and records publishedAt + releaseURL.
- schemas: PostNodeData gains postizPostId, postizIntegrationId,
postizStatus, postizError, postizSentAt, postizCheckedAt,
postizReleaseURL, publishedAt.
- postiz-client: listPosts(startDate, endDate) for reconciliation.
- mod.ts: sendCampaignNodeToPostiz() helper, POST /api/campaign/flows/
:flowId/nodes/:nodeId/send-postiz, postizSweep() + postizReconcile()
wired into onInit via startPostizScheduler.
- folk-campaign-planner: button + status badge in Post inspector,
timeline/table dot colors reflect postizStatus (queued=purple,
published=green, failed=red). Timeline bucket stays by scheduledAt.
- campaign-planner.css: postiz state pill styles.
- Bump campaign-planner JS/CSS to ?v=2 to bust CF cache.
Mock-Postiz smoke against the client lib passes 20/20 assertions.
Live Postiz round-trip pending deploy to Netcup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Retries of /api/register/start previously generated a fresh random user.id
each time, so the authenticator (iCloud Keychain, Windows Hello, 1Password,
etc.) stored a brand-new passkey per attempt. Users who hit the failing
registration flow ended up with three or four orphan passkeys in their
password manager for every successful one.
WebAuthn spec: a create() ceremony with the same (rpId, user.id) overwrites
the existing passkey. Deriving user.id as SHA-256(salt + username) means
repeated start calls for the same username produce the same user.id and the
authenticator overwrites in place.
Salt chain: USER_ID_SALT → JWT_SECRET → fallback constant. No new env var
needed in prod — JWT_SECRET is already set.
- rsocials: collapse dead "posts" outputPath into "threads" entry (💬 Posts)
so the sub-nav shows one button linking to the thread gallery + builder
- spaces GET /:slug/modules: redact settingsSchema fields of type 'password'
to '********' for non-owner callers; PATCH preserves existing password
when the sentinel is sent back unchanged
Closes the leak where unauth'd GET returned Listmonk/Postiz credentials in plaintext.
Friend hit a 500 on /api/register/complete because the Postgres unique
constraint fired after the WebAuthn ceremony — they'd already burned a
passkey creation by the time the server refused. Pre-check the username in
/api/register/start so the join page shows "Username is already taken"
before the browser prompts. Also catch the 23505 duplicate-key error in
/api/register/complete as a race-condition safety net.
The /join and OIDC accept pages were calling /api/register/complete without
the public key extracted from the WebAuthn attestation. storeCredential writes
a credentials row with a NOT NULL public_key column, so the request crashed
with an unhandled exception and Hono replied "Internal Server Error" as plain
text — the client JSON.parse choked on it.
- Extract credential.response.getPublicKey() on the client and include it in
the completion payload (both /join and /oidc/accept flows).
- Add app.onError to return JSON 500s on /api/* routes so any future crashes
produce parseable error bodies instead of cascading into confusing "Unexpected
token 'I'" errors in the browser.
The /api/internal/* routes (user lookups, space invites, fund claims, agent
mailbox creds) were publicly reachable via auth.rspace.online because Traefik
forwards everything under the encryptid host rule. They were designed for
service-to-service calls over the Docker network only.
Add a Hono middleware that rejects /api/internal/* with 404 when the request
arrives through the public edge (X-Forwarded-For / X-Real-IP set by Traefik)
unless it carries a valid X-Internal-Key matching INTERNAL_API_KEY. Direct
container-to-container fetches don't set forwarded headers so existing
callers keep working untouched.
- rApp dropdown no longer shows a "Manage rApps" panel for disabled modules.
Enable/disable lives in Edit Space → Modules only.
- /api/internal/user-email/:userId now resolves by id OR did (real did🔑z6Mk…
or legacy synthetic did🔑<32-char id prefix>), so Edit Space members list
shows display names instead of raw DIDs.
- /api/spaces/:slug/invites admin check now accepts ownerDID stored as either
raw userId or did🔑. Routes the list through a new internal endpoint on
encryptid so Automerge-owned spaces without a space_members row still work.
- /join page registration payload was missing userId, causing every invite
claim to fail with "Missing required fields: userId, credential, username".
Same fix applied to the OIDC accept page flow.
Space-agent and mailbox emails set From to {slug}-agent@rspace.online but
the envelope MAIL FROM was noreply@rmail.online. Rspamd DKIM-signs with
the envelope domain (rmail.online), which fails DMARC alignment against
the rspace.online From header — Gmail was quarantining invites.
Set envelope.from to match the From header so rspamd signs with
rspace.online's DKIM key and SPF/DKIM align. Verified via test send:
DKIM_SIGNED{rspace.online:s=dkim}, FROM_EQ_ENVFROM, no DMARC:Quarantine.
Covers: space invite emails (identity + existing-user paths), agent
notifications, rinbox MI agent blasts, mailbox approval sends.
No longer needed — sidecar lifecycle is delegated to Sablier via HTTP API
(see ee251fd). Removes an unnecessary Docker socket exposure from the
main app container.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the custom Docker Engine API implementation in sidecar-manager.ts
with HTTP calls to Sablier's blocking strategy endpoint. Sablier owns the
Docker socket, handles start + readiness + session-TTL idle stop.
- Drops ~80 lines of Docker API plumbing and the idle-watcher interval
- Public API (ensureSidecar/markSidecarUsed/isSidecarRunning/startIdleWatcher)
unchanged — callers in server/index.ts untouched
- SABLIER_URL defaults to http://sablier:10000 (reachable once sablier is
attached to rspace-online_rspace-internal; dev-ops change separate)
- SIDECAR_SESSION_DURATION env (default 5m) matches previous idle timeout
- Graceful no-op when Sablier unreachable (local dev)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copying the sablier-encryptid.yml labels would immediately apply
traefik.enable=false to the live encryptid container, breaking auth
before Sablier has a middleware route in place. Reduced to empty
services:{} with a comment explaining what's needed to activate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>