Commit Graph
1636 Commits
Author SHA1 Message Date
Jannis Braun ad7c86e89e fix(web): Re-peer surfaces incomplete outcome instead of false success (BUG-2) 2026-07-02 13:03:03 +02:00
Jannis Braun c6f0e6f25d fix(federation): initiator handles 409 + verifies handshake before activating (BUG-1b/BUG-2) 2026-07-02 12:56:26 +02:00
Jannis Braun edcb4cb72a fix(federation): /peer/accept returns honest 409 instead of false 200 for existing peer (BUG-1a) 2026-07-02 12:40:16 +02:00
Jannis Braun 442811e600 test(federation): reproduce handshake desync BUG-1/BUG-2 over real handshake (RED) (BUG-0)
#1 control passes (harness validated); #2 fails (200 vs expected 409 — false success);
#4 fails (s2sHealthy false — Re-peer reports success on dead peering). Turned green by the fixes.
2026-07-02 12:32:09 +02:00
Jannis Braun 085670cff3 fix(federation): handshake sourceOrigin honors PUBLIC_ORIGIN (align with S2S auth origin) 2026-07-02 12:31:02 +02:00
Jannis Braun aa7bf5b532 test(federation): real-handshake two-instance harness helpers (BUG-0) 2026-07-02 11:51:54 +02:00
Jannis Braun df1e457971 feat(federation): add needs_attention_reason 'repeer_incomplete' (BUG-2 prep) 2026-07-02 11:46:10 +02:00
Jannis Braun fd0ff4d199 fix(federation): clear federation_home_orphaned on tombstone (BUG-5) 2026-07-02 11:42:48 +02:00
Jannis Braun 6de14b281b fix(federation): friend-add returns graceful 503 instead of 500 on peer lookup failure (BUG-3)
lookupRemoteUser now maps peer HTTP failures (403/5xx, malformed body) to a
structured {ok:false,reason:'unreachable'} instead of throwing, and the
federated friend-add wraps the call in try/catch as defense-in-depth. A
desynced/unreachable peer no longer surfaces as a raw 500 on a user action.
README.md left unstaged.
2026-07-02 11:23:08 +02:00
Jannis Braun 43d1dad1d7 fix(federation): carry error body on HttpError so Reset-cleanup owns-spaces copy reaches the UI
Also narrow SanitizedPeer.needsAttentionReason to the shared union.
2026-07-02 02:19:13 +02:00
Jannis Braun 493deefc64 docs(federation): document instance-epoch self-healing (Phase 2) 2026-07-02 02:09:19 +02:00
Jannis Braun be7749b83f fix(federation): scope Reset-cleanup Remove owns-spaces detection to ownedSpaces payload 2026-07-02 02:01:50 +02:00
Jannis Braun 946255c4a1 feat(federation): admin Reset cleanup UI (Re-peer + Keep/Remove) 2026-07-02 01:57:30 +02:00
Jannis Braun 9d00cf024a feat(federation): client handler for federation_peer_reset_detected 2026-07-02 01:51:18 +02:00
Jannis Braun 290cd606c0 feat(federation): GET /reset-events admin endpoint + types 2026-07-02 01:47:58 +02:00
Jannis Braun ee52ff0c7b feat(federation): expose needsAttentionReason on peer API 2026-07-02 01:43:12 +02:00
Jannis Braun 732d146396 feat(federation): quarantine real accounts on reset heal (freeze + free-handle) 2026-07-02 01:39:26 +02:00
Jannis Braun e0a0d92fe7 feat(federation): login self-heal epoch guard (fetchPeerEpoch, fail-closed) 2026-07-02 01:33:42 +02:00
Jannis Braun 9b945ba5b7 feat(federation): freeze login for reset-orphaned federated accounts 2026-07-02 01:27:47 +02:00
Jannis Braun 954ff6e3dd Merge branch 'feat/federation-instance-epoch': federation instance-epoch split-brain self-healing (Phase 1)
Detects when a federated peer is factory-reset on the same domain (via a
persistent instance epoch), routes it to needs_attention (detection-only,
never auto-rekeys), and after an admin re-peer soft-tombstones the dead
incarnation's replicated stubs — clearing stale friendships/DMs while
preserving message history. 12 tasks + a needs_attention detection fix;
server suite 1201/1201. Phase 2 (login-hijack guard, real-account
quarantine, admin Reset-cleanup UI) deferred — see design spec.

Deployed + verified live on nova (Pi) and orbit (VM), commit d8fec00.
2026-07-02 00:57:13 +02:00
Jannis Braun d8fec00905 feat(federation): detect peer reset on needs_attention peers (§4.1)
A reset peer can reach needs_attention via the auth-failure path (HTTP up,
401/403 from a new incarnation crossing AUTH_FAILURE_THRESHOLD) without ever
passing through unreachable, so the unreachable-only recovery probe never
observes its epoch change and no reset journal is created — leaving a later
manual Re-peer with nothing to heal.

Add detectResetOnNeedsAttentionPeers() to the 15-minute health-check tick:
probe needs_attention peers with a non-null baseline (excluding those already
peer_reset_detected) and call markPeerReset on an observed epoch mismatch.
Detection only — never recovers a needs_attention peer to active; baseline
(peer_instance_id) and hmac_secret untouched.
2026-07-02 00:34:33 +02:00
Jannis Braun 769ed64314 docs(federation): document instance-epoch self-healing (Phase 1) 2026-07-02 00:20:43 +02:00
Jannis Braun 7d8c9c9d8d feat(federation): peer_reset_pending guard during limbo window 2026-07-01 22:32:40 +02:00
Jannis Braun 7ef1ded116 test(federation): lock reset admissibility for peer_reset_detected peers 2026-07-01 22:22:43 +02:00
Jannis Braun 8ae8dcfd86 feat(federation): heal on re-peer with false-positive guard
Add healResetIncarnation (federationReset.ts): fires from onPeerActivated after
an authenticated re-peer to soft-tombstone the flagged pure S2S stubs of a reset
peer's dead incarnation, clearing stale friendships/DMs so the reported bug is
fixed. Two mandatory guards: a reason gate (allow-list of 8 genuine handshake
activation reasons; excludes health_check_recovery + startup_bootstrap so their
stale baseline can never silently resolve a journal without healing) and an
epoch comparison (dead_epoch === newEpoch => false alarm, no tombstone). Uses
tombstoneUser(uid, { purgeContent: false }); real federated accounts are left
flagged + intact for Phase 2. Runs outside any transaction. Wire into
onPeerActivated before the mutation-log re-sync.
2026-07-01 22:17:14 +02:00
Jannis Braun 45e1c88bdc feat(federation): reset detection (markPeerReset) via handshake + probe 2026-07-01 22:09:43 +02:00
Jannis Braun 3b1a0b64a3 feat(federation): relay envelope populates peer epoch baseline 2026-07-01 21:58:03 +02:00
Jannis Braun 8f60e92f94 feat(federation): deterministic baseline epoch-refresh worker 2026-07-01 21:49:18 +02:00
Jannis Braun bf74aa8bb2 feat(federation): signed /api/federation/epoch endpoint + caller 2026-07-01 21:43:02 +02:00
Jannis Braun 538519fcd2 feat(federation): exchange + store peer epoch on handshake 2026-07-01 21:35:20 +02:00
Jannis Braun 7acf48d0a4 feat(federation): shared epoch types + getInstanceId() 2026-07-01 21:19:25 +02:00
Jannis Braun d8f2b1a9c7 feat(federation): instance epoch schema + minting 2026-07-01 21:11:34 +02:00
Jannis Braun 807b2f6234 refactor(web): fold final-review polish into join modal feature 2026-07-01 19:06:10 +02:00
Jannis Braun cf9bcfbf1a docs(spaces): document discovery-first Join a Space modal 2026-07-01 18:59:48 +02:00
Jannis Braun 309abd86e2 feat(web): discovery-first Join a Space modal 2026-07-01 18:56:56 +02:00
Jannis Braun fc8df54cb0 feat(web): compact ExploreSpacePreviewCard for join modal 2026-07-01 18:51:31 +02:00
Jannis Braun e840dcbe40 refactor(web): SpaceCard uses shared useSpaceJoin hook 2026-07-01 18:47:16 +02:00
Jannis Braun 6c66af7a88 feat(web): add shared useSpaceJoin hook over exploreStore 2026-07-01 18:42:45 +02:00
Jannis Braun 215750cb7a fix(install): harden prereq/DNS paths against set -euo pipefail aborts
Found by running the previously-untested Docker-not-installed path in a
clean, no-dig container:

- resolve the current user with $(id -un), not $USER: under `set -u`
  an unset $USER aborted the script with 'USER: unbound variable' right
  after Docker was installed ($USER is not guaranteed set under sudo,
  `su` without -l, cron, or docker exec)
- make the Docker-install prompt EOF-safe (read ... || yn="") so a
  non-interactive / piped stdin falls through to the [Y/n] default
  instead of tripping `set -e`
- swallow getent's exit-2 on an unresolved domain (... || true): with
  `set -o pipefail` it aborted the DNS check before the graceful
  'Could not resolve' warning, breaking installs on minimal hosts (no
  dig) that run before DNS is pointed — an explicitly supported flow
- guard the LAN-IP and disk-space pipelines the same way so a no-match
  grep / missing `ip` can't abort the summary or prereq checks
2026-07-01 18:35:20 +02:00
Jannis Braun 737402b5d2 docs(deployment): note install.sh commit injection + HTTPS reachability check 2026-07-01 18:14:38 +02:00
Jannis Braun 20ba7b42f9 feat(install): production-hardening for public self-hosting
- inject the git commit at build time so /api/instance/info advertises
  the exact source version on the public clone->install.sh path (AGPL
  \xc2\xa713), passed as --build-arg so it survives the sudo/non-sudo split
- pass INSTANCE_NAME through the container env instead of interpolating
  it into node -e source; names with quotes/spaces/$ are stored verbatim
  and can't break or inject into the program
- add a post-install, NAT-hairpin-safe HTTPS reachability check (curl
  --resolve to the local Caddy with full cert verification) and report
  an honest 'Live' vs 'Not live yet' status instead of always claiming
  success
- list ALL required ports in the summary (80/443 + voice) with explicit
  host-firewall vs router port-forwarding guidance and the auto-detected
  LAN IP as the forward target; note 7880 must NOT be forwarded
- document the non-interactive env vars (DOMAIN/ENABLE_VOICE/INSTANCE_NAME)
2026-07-01 18:14:38 +02:00
Jannis Braun f481e1fe9e license: relicense to AGPL-3.0-only with commercial dual-license
- LICENSE -> verbatim GNU AGPL-3.0; add LICENSE-COMMERCIAL.md + SECURITY.md
- CLA -> exclusive-license grant (contributors keep copyright); add README
  anti-rugpull covenant + relicense record
- NOTICE / README / CONTRIBUTING / CLAUDE.md / package.json x5 updated;
  contact routed through GitHub (no email placeholders)
- AGPL section 13 source offer: operator-configurable BACKSPACE_SOURCE_URL +
  build-injected commit; sourceCodeUrl+commit on /api/instance/info;
  SourceCodeLink on login/register/settings/desktop; docs + .env.example updated
2026-07-01 16:38:22 +02:00
Jannis Braun 16d75f2806 docs(readme): drop private-test-instance mention from status 2026-07-01 13:25:52 +02:00
Jannis Braun 3be8b36064 docs(readme): finalize for 1.0.0 public launch
- Confident 1.0.0 framing: version badge + stable status callout; drop
  "early-stage / active development" and inaccurate "full-text" wording
- Add "What makes Backspace different" section leading with the verified
  wedge (per-stream media controls + federation + complete/polished)
- Fix two overclaims to match source: no unread divider (-> mark-as-unread),
  search is LIKE-based not full-text (-> "message search")
- Surface previously-omitted, source-verified voice/media features:
  per-user & per-stream volume, connection inspector, viewer detection,
  auto-ducking, measured tile badge, device/EC/AGC controls, selective
  subscription, browser push-to-talk
- Add Idle/DND + custom status, drag-drop/paste upload, avatar/banner crop
2026-07-01 13:22:37 +02:00
Jannis Braun ac20f22c72 fix(voice): consistent state on DM-call ↔ space-channel transitions
Two mirror-image bugs from voice/DM-call transitions leaving stale state.

DM call → space channel (stuck "Connecting…"):
The last participant to leave a DM call for a space channel receives a
`dm_call_ended` echo (server empties the DM room on their `voice_join`).
The handlers called `disconnectFn()` unconditionally, tearing down the
space room they had just connected to. Route `dm_call_ended` /
`dm_call_rejected` / terminal `dm_call_undeliverable` through a new
`teardownDmCall()` that only disconnects LiveKit when not in a space
channel (`currentVoiceChannelId` null).

Space channel → DM call (still shown as "in" the voice channel):
1. Entering a DM call never cleared `currentVoiceChannelId`, so
   `VoiceChannel` mapped the DM call's live LiveKit participants onto the
   old space channel. Add `clearSpaceVoiceForDmCall()`, called in
   `connect()` when `isDm`, restoring the invariant that a DM call has no
   `currentVoiceChannelId`.
2. `dm_call_accepted` gated the caller's connect on `!isLiveKitConnected`,
   so a caller already in a space channel was never connected to the DM
   room. Gate on `wasOutgoingCall` only (connect() de-dupes same-room).

Tests: teardownDmCall.test.ts, clearSpaceVoiceForDmCall.test.ts.
Docs: docs/systems/voice.md.
2026-07-01 02:08:53 +02:00
Jannis Braun 7a3d892c6e deploy: never rsync over the host-owned Caddyfile
The Pi's Caddyfile carries extra vhost blocks (e.g. other-site.example.com) that
aren't in this repo; add it to the rsync excludes so deploys don't overwrite it.
2026-07-01 01:34:16 +02:00
Jannis Braun d2e9b92e80 docs(readme): correct project-status + bans wording
- Project status: drop the test-instances mention and the self-contradictory
  'no tagged public release yet' framing → 'early-stage and under active development'.
- Moderation: 'Bans with reason and audit trail' overstated it — there's no
  append-only audit log and unban hard-deletes the record. Reworded to
  'Bans with reason and moderator attribution (who, why, and when)' to match the code.
2026-07-01 01:28:28 +02:00
Jannis Braun bdf957514c docs: accurate read-state wording + mobile/PWA and app-store-roadmap mention
- Features list: 'read states' -> 'unread tracking (badges + unread divider)',
  which is what the code actually does (no read receipts).
- Clarify the PWA bullet (add-to-home-screen standalone app) and the
  mobile-responsive touch layout.
- Add a Mobile section: working mobile-browser/PWA today, native iOS/Android
  app-store apps planned once traction and license funding are secured.
2026-07-01 01:15:33 +02:00
Jannis Braun 3f5b2920a4 docs: fix screenshot caption overclaiming read states
The chat screenshot shows messages + typing; "read states" reads as read
receipts (which don't exist). Backspace tracks unread state only. Reworded
captions to replies/reactions/typing.
2026-07-01 01:11:47 +02:00
Jannis Braun cb16c8feac docs: add screenshot showcase to README + gallery
Replace the placeholder Screenshots section with a hero voice/video shot
and a curated 6-shot grid (chat, screen-share controls, spaces, federated
DMs, social discovery, federation admin), linking to a new
docs/screenshots.md gallery of all 14 shots grouped by feature.

Images are optimized to 1600px WebP (~1.2MB total vs ~45MB raw); raw
originals in demo-pictures/ are gitignored.
2026-07-01 01:06:08 +02:00