feat(federation): foundations, identity & join flow (Milestones 1–2)#173
Conversation
Review findings & fixes — commit
|
Implement client federation across Milestones 1 (Foundations & Identity) and 2 (Spaces & Membership), against accordserver PR #38. SDK (packages/accordkit) — #168, #153: - Add qualified-ID helpers (localPart/domainOf/isRemoteId/qualify), mirroring the server's federation/mapping.rs. - Tolerate qualified IDs in snowflake timestamp decoding. - Add the optional `origin` (home domain) field to user, member, message, space, channel, role, and emoji, with isRemote/homeDomain convenience getters. Keep full qualified IDs as cache keys so a remote `123@b.example` never collides with a local `123`. - Add `client.federation.joinSpace(domain, spaceId)`. Identity rendering — #154, #155: - New shared RemoteOriginBadge wired into the message author line, member list, profile popout, and space header. - Resolve remote avatars against the home server's CDN; disable local-only moderation actions for remote members. Join flow — #156, #169: - joinFederatedSpace helper + a "Federated" tab in the add-server dialog and a daccord://federate/<spaceId>@<domain> deep link. Live replica updates — #157, #158: carried by existing gateway handlers (keyed by string IDs), which apply qualified replica content unchanged. Tests: accordkit qualified-ID/snowflake/origin/endpoint coverage; app helper + deep-link parser coverage. flutter analyze clean; SDK and app suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ess tests - `_parseFederate`: inline `_hostFromAuthority` null-check so the validated-but-unused `host` binding doesn't mislead readers into thinking `authority` (full authority incl. port) should have been replaced by the stripped hostname in the result. - Add two missing avatar-resolution tests (server-relative path for remote user, space-scoped bare-hash override for remote member) that exercised an untested branch of `_userAvatarUrl`/`accordMemberAvatarUrl`. - Add `lastIndexOf` edge-case tests for `parseFederatedAddress` and the `daccord://federate/` deep link so qualified space IDs (containing an embedded `@domain`) are confirmed to split on the final `@`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YHXe8ANc18KmzhtQyZ3Py9
Phase 3 of client federation: render and react to remote-homed messages
in shared spaces. M1–M2 already made every cache key on the full
qualified ID, so remote message create/edit/delete, reactions, and typing
flow through the existing gateway handlers unchanged. The gap this closes
is qualified-self recognition.
A remote home re-qualifies the local user's own actions to our domain
before fanning them back (`me` -> `me@a.example`), so a bare
`== currentUserId` stopped recognising them — self-notifying, self-chiming,
double-counting our own reactions, and showing "you are typing" on
ourselves on remote-homed channels/messages.
- accordkit: add `isSameUser(actorId, localId, {localDomain})` — matches a
bare local id or our own id qualified to our home domain, while a remote
actor that merely shares our bare snowflake stays distinct (#159).
- AccordServer.homeDomain + ref `watchHomeDomain()`: expose our federation
home domain (the base URL host) to event handlers and the message pane.
- Event handler: route message author/mention, reaction ownership, and
typing skip-self through qualified-aware self checks (#160–#162).
- Message pane: own-message and mention highlighting recognise our
qualified id on federated messages (#159).
Remote author identity, avatars (home CDN), and the origin badge already
resolve via the qualified author_id -> member/user cache -> replica-fetch
path and the M1–M2 RemoteOriginBadge.
Tests: isSameUser (self vs. colliding-snowflake); message-cache dedup/
update/delete keyed by qualified id; federated reaction aggregation
(remote actor not-self, own-echo merges). flutter analyze + suites run in
CI (no Dart toolchain in this environment).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 4 of client federation: open and render direct messages with users on other servers. Open by qualified handle (#163): - A qualified recipient (`<snowflake>@<domain>`) now routes through the single `recipient_id` field so the server takes its cross-server DM path (deterministic home + replica mirror); bare ids keep the recipients-list same-server form. Extracted as `dmCreateBody`. - New "Message remote user" entry in the DM list opens a dialog that validates a qualified handle (`isValidRemoteHandle`) before opening. - The member popout's existing "Direct Message" already passes a remote member's qualified id, so messaging a visible remote member just works. - `openAccordDirectMessage` now surfaces the server's rejection reason (federation disabled, recipient not qualified, peer untrusted, blocked) instead of a generic failure. Origin-aware rendering (#164): - DM list tiles and the conversation header show the participant's home-domain avatar (resolved against their home CDN) and a RemoteOriginBadge for cross-server DMs. The server stores our own DM participant with a bare id, so the 1:1 "other" participant resolves cleanly in both the home and replica channel cases. - Send/receive use the (qualified or bare) channel id unchanged. Tests: dmCreateBody routing + isValidRemoteHandle validation; accordkit cross-server DM channel parse (qualified channel id + remote recipient). Remote avatar/origin helpers were already covered. flutter analyze + suites run in CI (no Dart toolchain in this environment). Known limitation (pre-existing, parity with local DMs): the lightweight DM dialog isn't a live gateway view — incoming messages and reactions/typing aren't shown there for any DM, local or federated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
c2281a1 to
b48def2
Compare
Federated entities carry an `origin`/`@domain` minted by a peer server, which the client turned directly into CDN URLs (and, for pasted server URLs, the auth-bearing base URL). Harden that trust boundary: - Add isValidHost / isLoopbackOrLinkLocalHost to accordkit. - cdnBaseForDomain now rejects unsafe origins (invalid hostnames, smuggled userinfo/path, loopback and link-local incl. cloud metadata) so a malicious origin can't redirect avatar/emoji fetches to internal or attacker-controlled hosts. - Reject off-home absolute avatar/emoji URLs from remote entities, and centralize emoji resolution in accordEmojiUrl. - Tighten server URL/deep-link host validation to a strict allowlist so `host@evil.com`-style links can't aim the base URL at another host. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Audit follow-through focused on consistency, DRY, and clean organization: - Add shared helpers: watchAccordClient(), list upsertById/removeById, and RestResult listOrLog/dataOrLog; adopt across controllers and views. - Give AccordMemberAvatar an initialStyle param and route the remaining hand-rolled CircleAvatar sites (voice, forum, message row, etc.) through it. - Extract the shared admin-tab body skeleton into AdminListScaffold. - Move non-Riverpod helpers into the services/ convention: event handler -> events/services, sound + notification -> notifications/services, federation_join -> server/services. - Rename the auth-state model accord_auth.dart -> accord_auth_state.dart to stop colliding with the AccordAuth repository. - Enforce always_use_package_imports; fix the lone relative import and drop a dead commented-out class. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The space tile menu only offered "Leave server"/"Leave & delete data", both of which hit the server over REST and so fail for a server you can no longer reach, leaving no way to clear it from the rail. Add a "Remove server" entry that calls AccordAuth.removeAccount() — purely local cleanup (disposes the client, clears saved credentials, rail entry, tabs, unread state and cached spaces), so it works even when the server is unreachable. Confirms first and notes nothing is deleted server-side. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements client-side federation for Milestone 1 (Foundations & Identity) and Milestone 2 (Spaces & Membership), against the server's peer-to-peer federation work in accordserver#38 (branch
claude/federated-servers-xlydlj).Closes #168, #153, #154, #155, #156, #157, #158, #169.
Why
The Accord server now speaks peer-to-peer federation — remote entities carry qualified IDs (
<snowflake>@<domain>) and anoriginfield, spaces can be joined cross-server, and the home server fans member/channel/space/message changes out to replicas as ordinary gateway events. The SDK and UI had zero federation support, so nothing in the client could consume a federated space. This PR adds the plumbing and UI so a user can join a space homed on another Accord server and see its content render correctly and stay live.What changed
SDK (
packages/accordkit) — #168, #153utils/qualified_id.dart):localPart,domainOf,isRemoteId,qualify— mirroring the server'sfederation/mapping.rs.decodeTimestampMsstrips@domainso snowflake-derived timestamps still parse for qualified IDs.originfield on user, member, message, space, channel, role, emoji (parse + round-trip), withisRemote/homeDomainconvenience getters. Cache/equality keys keep the full qualified ID, so a remote123@b.examplenever collides with a local123.client.federation.joinSpace(domain, spaceId)→POST /federation/spaces/join.Identity rendering — #154, #155
RemoteOriginBadge(globe glyph +@domain) on the message author line, member list, profile popout, and space header.https://<origin>/cdn, bare snowflake), with absolute-URL passthrough; local-only moderation (kick/ban/timeout/roles/nickname) is disabled for remote members.Join flow — #156, #169
joinFederatedSpacehelper that calls the SDK and hydrates the returned replica into the rail.spaceId@domain) and adaccord://federate/<spaceId>@<domain>deep link.Live replica updates — #157, #158
No new event-handling code: the existing gateway handlers apply member join/leave, channel create/update/delete, and space metadata keyed by string IDs, so qualified replica content rides them unchanged once the space is in the list.
Testing
dart analyzeclean; full suite green incl. new qualified-ID, snowflake@domain,originround-trip, and join-endpoint tests.flutter analyze --no-fatal-infos→ 0 errors; suite green incl. new member-display/remote-avatar and deep-link/address-parser tests.Reviewer notes
set_space_federation_enabledin the DB layer but no route wires it andUpdateSpacelacks the field, so the client toggle would no-op today. Will be added optimistically againstspaces.updateonce the server exposes it.@riverpod/codegen-annotated files were added, so no*.g.dartchanges are included.🤖 Generated with Claude Code