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>
.env on Netcup sets COMPOSE_FILE=docker-compose.yml:docker-compose.sablier-support.yml
but the file was never created, causing docker compose commands to fail
without explicit -f flag. Adds the missing override with Sablier labels for
encryptid services (matching the existing sablier-encryptid.yml pattern).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B's full booking UI + admin dashboard + cancel page bundles are
now built. Bumping cache version forces Cloudflare + browser to fetch
the new code instead of the Phase A stub.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- rename old rSchedule (cron/automations) → rMinders
- native port of schedule-jeffemmett as new rSchedule (Calendly-style booking)
All 8 phases complete: scaffold, public booking UI, availability engine,
Google Calendar OAuth+sync, admin UI, emails, in-process cron, cross-space
invitations + timezone shift.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lib/availability.ts (getAvailableSlots) was imported by mod.ts but never
committed — caused module load to fail on server, leaving renderShell
callers with undefined 'modules' and producing 500s on every rschedule
route. Plus small updates to booking/admin/cancel components and mod.ts
wiring.
Schema for the new Calendly-style rSchedule port plus two new shared
canvas components (folk-app-canvas, folk-widget) that replace per-rApp
tab-based nav with a unified widget-on-canvas surface.
Complete the rename started in dda7760 (which removed rschedule/ but
left callers unmigrated and the rminders/ dir uncommitted). Updates
vite.config.ts build entries, API base fetches in folk-comment-pin,
folk-rapp widget map, module-display meta, calendar reminder-drop
route, docs comment-panel, e2e fixtures, shell/landing/mcp-server
references, and backlog/ONTOLOGY docs.
Fixes vite build failure: "Could not resolve entry module
modules/rschedule/components/folk-schedule-app.ts".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>