Skip to content

feat: custom profile avatars + profiles that reach share recipients#465

Merged
crs48 merged 4 commits into
mainfrom
claude/custom-avatars-profile-sharing-6e119f
Jul 11, 2026
Merged

feat: custom profile avatars + profiles that reach share recipients#465
crs48 merged 4 commits into
mainfrom
claude/custom-avatars-profile-sharing-6e119f

Conversation

@crs48

@crs48 crs48 commented Jul 10, 2026

Copy link
Copy Markdown
Owner

What

Custom avatars. Settings → Profile gains a Picture field: uploads are center-cropped, downscaled to 192px, and re-encoded through a canvas (which strips EXIF/GPS metadata) into a small inline `data:image/*` URL stored on the Profile node — one sync path for name and picture, no blob authorization needed across share boundaries. The top-left profile button, profile menu/sheet, mobile shell, and editor header show the picture when set; without one they show the deterministic DID identicon (the "default genmoji"), via a shared `SelfAvatar` component. The profile menu now shows your display name instead of "You".

Profiles in shared contexts. Share recipients used to see `did:key…` fragments and identicons for every author, because Profile nodes never reached them (the "hub-published" intent in `auth-exempt.ts` was never implemented). Now:

  • A DID's canonical Profile lives at the deterministic `profileNodeId(did)` (`profile-`, mirroring `inbox-`), so knowing a `createdBy` DID is enough to acquire the author's profile. `ProfileSettings` writes there; a one-time idempotent migration in `CommsProvider` copies legacy random-ID profiles over.
  • The hub treats profile rooms as hub-published identity: any authenticated DID may subscribe/read (`authorize.ts`, source `profile-public`); only the subject DID may write — enforced on both the node-change and Yjs write paths (`ShareAccessService`).
  • A new `useEnsureProfiles(dids)` hook acquires missing profiles on sight, wired into channel chat, threads, channel rosters, page comments, presence facepiles, and the ShareDialog People tab (which now shows names + avatars instead of raw DIDs).
  • `usePageComments` gains an optional `resolveAuthorName` — the existing `authorDisplayName` plumbing in the comment UI was always there, just never fed.

Security reflection

Sharing something with someone implies they can see who you are — so profiles (display name, handle, avatar, status) are readable by any authenticated DID on the hub, matching the "public identity" intent already documented in `auth-exempt.ts`. Per-share profile ACLs can't work anyway: a recipient needs the profiles of all authors in a doc, not just the sharer. Guard rails added:

  • Write protection: hub rejects profile-room writes from any DID other than the subject.
  • Spoof resistance: `dedupeProfiles` prefers self-authored profiles (`createdBy === did`), so a write-role grantee can't override someone's identity by injecting a Profile node into a shared doc (foreign-authored profiles still fill gaps — the devtools seeder relies on that).
  • Avatar sanitization: `safeAvatarSrc` only lets `data:image/*`, `http(s):`, and `blob:` through to ``; profiles sync from untrusted peers.
  • Metadata hygiene: canvas re-encoding strips EXIF before the picture leaves the device; anonymous hub sessions get no profile-public allowance.

Tests / verification

  • `profileNodeId` determinism + round-trip, 64k avatar field (inbox-state.test.ts)
  • Hub: subject-only writes, authenticated-read allow, anonymous deny, non-profile rooms unaffected (share-links.test.ts — full suite green)
  • `safeAvatarSrc` allow/deny matrix, spoof-preference + fallback in `dedupeProfiles`
  • Live-verified in the worktree preview: upload → 2.2 KB WebP data URL → top-left avatar shows the picture, persists across reload; identicon fallback intact.

Changeset: `@xnetjs/data` minor (new `profileNodeId`/`didFromProfileNodeId` exports; `avatar` maxLength 500 → 65536, a loosening). Hub/editor/app are private packages.

🤖 Generated with Claude Code

xNet Test added 3 commits July 10, 2026 16:12
Custom avatar upload in Settings → Profile: the picture is center-cropped,
downscaled and re-encoded client-side (stripping EXIF) into a small inline
data URL stored on the Profile node, so it travels the same sync path as the
display name. The shell's top-left profile button, profile menus, editor
header and presence facepiles show the picture when set and fall back to the
deterministic DID identicon otherwise.

Profiles now reach shared contexts. A DID's canonical Profile lives at the
deterministic profileNodeId(did) (profile-<did>, like inbox-<did>), so any
collaborator can acquire an author's profile knowing only the createdBy DID.
The hub treats profile rooms as hub-published identity: any authenticated
DID may read them, only the subject DID may write (node-change + Yjs paths).
Channel chat, threads, rosters, page comments, presence and the ShareDialog
People tab acquire missing author profiles on sight, so share recipients see
real names and pictures instead of DID fragments and identicons.

Trust guards: dedupeProfiles prefers self-authored profiles so a write-role
grantee can't override someone's identity by injecting a Profile node into a
shared doc, and avatar sources are sanitized (data:image/https/blob only)
before rendering, since profiles sync from untrusted peers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Signed-off-by: xNet Test <test@xnet.dev>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Signed-off-by: xNet Test <test@xnet.dev>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Signed-off-by: xNet Test <test@xnet.dev>
@crs48 crs48 temporarily deployed to pr-465 July 11, 2026 19:53 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #465.

github-actions Bot added a commit that referenced this pull request Jul 11, 2026
@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

Screens

✏️ Home _(SSIM 0.972)_
before after diff
before after diff

Interactions

🎬 Open a channel and post a message

Open a channel and post a message

▶ Watch MP4

🎬 Create a page and use the editor

Create a page and use the editor

▶ Watch MP4

Auto-captured by CI · run. Informational — not a blocking check.

github-actions Bot added a commit that referenced this pull request Jul 11, 2026
@crs48 crs48 temporarily deployed to pr-465 July 11, 2026 20:11 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jul 11, 2026
github-actions Bot added a commit that referenced this pull request Jul 11, 2026
@crs48 crs48 merged commit 703de66 into main Jul 11, 2026
16 of 17 checks passed
@crs48 crs48 deleted the claude/custom-avatars-profile-sharing-6e119f branch July 11, 2026 20:23
github-actions Bot added a commit that referenced this pull request Jul 11, 2026
crs48 added a commit that referenced this pull request Jul 11, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @xnetjs/data@0.11.0

### Minor Changes

- [#465](#465)
[`d9cd478`](d9cd478)
Thanks [@crs48](https://github.com/crs48)! - Profiles gain a canonical
deterministic node ID and room for inline avatar images:
- New `profileNodeId(did)` / `didFromProfileNodeId(nodeId)` helpers — a
DID's canonical Profile now lives at `profile-<did>` (same pattern as
`inboxStateNodeId`), so any collaborator who knows a DID (e.g. from
`createdBy` on shared content) can acquire the profile without a
directory lookup.
- `Profile.avatar` max length raised from 500 to 65536 so a small,
client-side-downscaled `data:image/*` avatar can live inside the Profile
node itself and reach share recipients through the same sync path as the
display name.

### Patch Changes

- Updated dependencies []:
  - @xnetjs/storage@0.11.0
  - @xnetjs/sqlite@0.11.0
  - @xnetjs/sync@0.11.0
  - @xnetjs/identity@0.11.0
  - @xnetjs/crypto@0.11.0
  - @xnetjs/core@0.11.0
## @xnetjs/react@0.11.0

### Minor Changes

- [#467](#467)
[`07b480d`](07b480d)
Thanks [@crs48](https://github.com/crs48)! - Deliver a shared chat
channel's nodes to a grantee (exploration 0298). `NodeStoreSyncProvider`
gains a subscribe-only mode (receives + applies a room but never
publishes local changes and never advances its cursor from a live
broadcast — share rooms cursor on a per-room `seq`). `SyncManager` gains
`subscribeShareRoom(room)` / `unsubscribeShareRoom(room)`, and a
`channelShareRoom(id)` helper is exported (re-exported from
`@xnetjs/react`). Together these let a client subscribe to a channel's
`xnet-channel-<id>` share room so its node, message history, and
members' profiles sync in — the transport that channel share links were
missing.

- [#470](#470)
[`e68c016`](e68c016)
Thanks [@crs48](https://github.com/crs48)! - Add
`workspaceShareRoom(id)` (re-exported from `@xnetjs/react`) so a shared
workspace (bench) node is delivered to a grantee via the same share-room
mechanism as channels (exploration 0298 Phase 2).

### Patch Changes

- Updated dependencies
[[`07b480d`](07b480d),
[`d9cd478`](d9cd478),
[`e68c016`](e68c016)]:
  - @xnetjs/runtime@0.3.0
  - @xnetjs/data@0.11.0
  - @xnetjs/data-bridge@0.11.0
  - @xnetjs/history@0.11.0
  - @xnetjs/plugins@0.11.0
  - @xnetjs/sync@0.11.0
  - @xnetjs/identity@0.11.0
  - @xnetjs/crypto@0.11.0
  - @xnetjs/core@0.11.0
## @xnetjs/runtime@0.3.0

### Minor Changes

- [#467](#467)
[`07b480d`](07b480d)
Thanks [@crs48](https://github.com/crs48)! - Deliver a shared chat
channel's nodes to a grantee (exploration 0298). `NodeStoreSyncProvider`
gains a subscribe-only mode (receives + applies a room but never
publishes local changes and never advances its cursor from a live
broadcast — share rooms cursor on a per-room `seq`). `SyncManager` gains
`subscribeShareRoom(room)` / `unsubscribeShareRoom(room)`, and a
`channelShareRoom(id)` helper is exported (re-exported from
`@xnetjs/react`). Together these let a client subscribe to a channel's
`xnet-channel-<id>` share room so its node, message history, and
members' profiles sync in — the transport that channel share links were
missing.

- [#470](#470)
[`e68c016`](e68c016)
Thanks [@crs48](https://github.com/crs48)! - Add
`workspaceShareRoom(id)` (re-exported from `@xnetjs/react`) so a shared
workspace (bench) node is delivered to a grantee via the same share-room
mechanism as channels (exploration 0298 Phase 2).

### Patch Changes

- Updated dependencies
[[`d9cd478`](d9cd478)]:
  - @xnetjs/data@0.11.0
  - @xnetjs/data-bridge@0.11.0
  - @xnetjs/history@0.11.0
  - @xnetjs/plugins@0.11.0
  - @xnetjs/storage@0.11.0
  - @xnetjs/sync@0.11.0
  - @xnetjs/identity@0.11.0
  - @xnetjs/crypto@0.11.0
  - @xnetjs/core@0.11.0
## @xnetjs/abuse@0.11.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/identity@0.11.0
  - @xnetjs/crypto@0.11.0
## @xnetjs/cli@0.1.3

### Patch Changes

- Updated dependencies
[[`07b480d`](07b480d),
[`d9cd478`](d9cd478),
[`e68c016`](e68c016)]:
  - @xnetjs/runtime@0.3.0
  - @xnetjs/data@0.11.0
  - @xnetjs/plugins@0.11.0
  - @xnetjs/sqlite@0.11.0
  - @xnetjs/sync@0.11.0
  - @xnetjs/identity@0.11.0
  - @xnetjs/crypto@0.11.0
  - @xnetjs/core@0.11.0
## @xnetjs/crypto@0.11.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/core@0.11.0
## @xnetjs/data-bridge@0.11.0

### Patch Changes

- Updated dependencies
[[`d9cd478`](d9cd478)]:
  - @xnetjs/data@0.11.0
  - @xnetjs/sqlite@0.11.0
  - @xnetjs/sync@0.11.0
  - @xnetjs/core@0.11.0
## @xnetjs/history@0.11.0

### Patch Changes

- Updated dependencies
[[`d9cd478`](d9cd478)]:
  - @xnetjs/data@0.11.0
  - @xnetjs/sync@0.11.0
  - @xnetjs/core@0.11.0
## @xnetjs/identity@0.11.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/crypto@0.11.0
  - @xnetjs/core@0.11.0
## @xnetjs/plugins@0.11.0

### Patch Changes

- Updated dependencies
[[`d9cd478`](d9cd478)]:
  - @xnetjs/data@0.11.0
  - @xnetjs/abuse@0.11.0
  - @xnetjs/core@0.11.0
## @xnetjs/storage@0.11.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/sqlite@0.11.0
  - @xnetjs/crypto@0.11.0
  - @xnetjs/core@0.11.0
## @xnetjs/sync@0.11.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/identity@0.11.0
  - @xnetjs/crypto@0.11.0
  - @xnetjs/core@0.11.0
## @xnetjs/core@0.11.0


## @xnetjs/sqlite@0.11.0


## xnet-cloud@0.0.14

### Patch Changes

- Updated dependencies []:
  - @xnetjs/crypto@0.11.0
  - @xnetjs/cloud@0.0.1
## @xnetjs/brain@0.0.15

### Patch Changes

- Updated dependencies
[[`d9cd478`](d9cd478)]:
  - @xnetjs/data@0.11.0
  - @xnetjs/vectors@0.0.1
## @xnetjs/comms@0.0.15

### Patch Changes

- Updated dependencies
[[`d9cd478`](d9cd478)]:
  - @xnetjs/data@0.11.0
  - @xnetjs/crypto@0.11.0
## @xnetjs/dashboard@0.0.15

### Patch Changes

- Updated dependencies
[[`07b480d`](07b480d),
[`d9cd478`](d9cd478),
[`e68c016`](e68c016)]:
  - @xnetjs/react@0.11.0
  - @xnetjs/data@0.11.0
  - @xnetjs/plugins@0.11.0
  - @xnetjs/social@0.0.15
## @xnetjs/labs@0.0.15

### Patch Changes

- Updated dependencies
[[`d9cd478`](d9cd478)]:
  - @xnetjs/data@0.11.0
  - @xnetjs/plugins@0.11.0
## @xnetjs/licenses@0.0.15

### Patch Changes

- Updated dependencies []:
  - @xnetjs/crypto@0.11.0
## @xnetjs/maps@0.0.15

### Patch Changes

- Updated dependencies
[[`d9cd478`](d9cd478)]:
  - @xnetjs/data@0.11.0
## @xnetjs/meetings@0.0.8

### Patch Changes

- Updated dependencies
[[`d9cd478`](d9cd478)]:
  - @xnetjs/data@0.11.0
  - @xnetjs/plugins@0.11.0
## @xnetjs/server@0.0.14

### Patch Changes

- Updated dependencies
[[`d9cd478`](d9cd478)]:
  - @xnetjs/data@0.11.0
  - @xnetjs/data-bridge@0.11.0
  - @xnetjs/identity@0.11.0
  - @xnetjs/crypto@0.11.0
## @xnetjs/social@0.0.15

### Patch Changes

- Updated dependencies
[[`d9cd478`](d9cd478)]:
  - @xnetjs/data@0.11.0
  - @xnetjs/crypto@0.11.0
## @xnetjs/unreal@0.0.15

### Patch Changes

- Updated dependencies
[[`d9cd478`](d9cd478)]:
  - @xnetjs/data@0.11.0
## xnet-desktop@0.11.0

Desktop shell release riding the @xnetjs/core 0.11.0 train.
Desktop-specific changes are not tracked here; see the core packages'
changelogs for what shipped.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant