Host-provided identity: show your app's users in the chat (0.2.0) - #8
Conversation
Let a host app supply the display names and avatars for the wallets in its chat, so the widget renders ITS users instead of a .sol domain or a shortened address. Visual and scoped to one running embed: Cherry persists none of it, the wallet stays the author of every message, and the Cherry app is unaffected. The iframe PULLS, because only it knows which wallets are on screen: resolveUsers — batch (<=50 wallets), null/omitted for anyone you don't know searchUsers — @mention autocomplete over your own directory getUser — reserved for the profile view The host PUSHES what changes: setUserProfiles() (a rename is invisible to an open chat otherwise — the iframe only asks about wallets it has not resolved yet), invalidateUserProfiles(), and setIdentityToken() for the profile endpoint's Authorization header (memory only, never persisted). `userProfiles` in the config gets the first paint right with no round-trip. Registering nothing keeps the previous behaviour exactly: the bridge answers an unregistered method with METHOD_NOT_FOUND, which the iframe reads as "this host cannot answer" and stops asking, instead of waiting out a timeout per wallet. Requires "Who your users appear as" on the embed at portal.cherry.fun. Hosts that would rather answer from their backend configure a profile endpoint there instead — no host-page handlers needed, which is the better fit for mobile WebViews where the host page is a thin shim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A runnable bench for the feature: one page, one small backend, and a
checklist that says what "working" looks like.
It exercises both transports of the same contract — the page answering
resolveUsers/searchUsers over the bridge, and a profile endpoint the iframe
calls directly (the URL is configured per embed in the portal) — plus the
paths that are easy to get wrong and invisible in a unit test:
- a live rename via setUserProfiles(), which is the only way an already
open chat learns that a user changed their name;
- invalidateUserProfiles(), and what a null profile does (that person
keeps their Cherry identity and stops being asked about);
- @mention autocomplete searching the app's own directory rather than the
room's history;
- a "hostile profiles" button that answers with a bidi override, an
embedded newline, zero-width padding, 400 characters and a
`javascript:` avatar — the chat must flatten, truncate and drop them.
The demo directory maps ANY wallet to a stable fake user, so the bench is
useful in a room full of strangers; avatars are SVGs served by the example
itself, so it works offline and doubles as proof that an ordinary
cross-origin image URL is all the embed needs. A call log shows what the
iframe asked for and which transport answered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Added It covers both transports (page handlers vs. a profile endpoint the iframe calls There is also a "Serve hostile profiles" button — it answers with a bidi The demo directory maps any wallet to a stable fake user, so the bench stays Verified locally: the page renders, |
Follows the runtime fix in messaging-server: pushing a name no longer drops the avatar. Says so where an integrator actually looks — the method's JSDoc, the profile type, the README snippet and the changelog entry — including how to clear a field (mention it empty) versus forget the wallet (push null). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bench is where this gets checked by hand, so the expectation belongs in its checklist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bench previously only had the wallet-only path, which needs a browser wallet extension — awkward for a bench whose whole point is to be runnable in one command. With APP_SECRET set it now mints an embedToken server-side and mounts with it, so the chat is authenticated with no wallet in the loop and the identity overlay can be exercised immediately. DEMO_VIEWER_WALLET says which wallet to sign in as. A real backend takes that from its own session — the banner on /api/embed-token already spells this out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Caught by running the bench: picking a directory user from @mention autocomplete rendered "@Maya_Duarte Demo2ea87…" — the full address next to the name, in the message body. Not a Cherry bug. The composer attaches the picked wallet as an invisible `<address>` tag, and the readers only recognise it when the address is valid base58 (no 0, O, I, l). The demo roster was minting wallets by slicing a sha256 HEX digest, so the tag never matched its pattern and fell through as visible text — a bench that lied about how the feature behaves. The roster now base58-encodes the 32-byte digest: same shape and charset as a Solana public key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
package.json moved to 0.2.0 with host-provided identity, but every copy-paste snippet still pinned 0.1.5 — a version that predates even the last release, so an integrator following the README would load a bundle without the new handlers and wonder why nothing resolves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reported from the bench: the name shown for a chat participant was also a row in the directory list, which reads as "the overlay resolved the wrong user". It was the demo generator, not the runtime: 16 first names × 8 surnames is 128 combinations, and across ~27 shown people a duplicate is ~95% likely. Members and the searchable directory now draw from two disjoint pools (16 × 16 each), directory names are de-duplicated within the list, and a directory wallet resolves to its DIRECTORY identity — otherwise someone picked from @mention autocomplete got renamed on the next resolve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Setting a name meant pasting a wallet into one field and a name into another,
with no way to see who is even in the room. The bench now lists every wallet the
chat has asked about, one row each, with the backend's answer as the placeholder
and editable name / avatar-URL fields over it.
- Apply pushes the row through setUserProfiles(); clearing one field drops
just that field, clearing both is the same as pushing null.
- Reset forgets the edit and re-asks the backend.
- Edits live in localStorage and win inside resolveUsers, so a re-resolve or a
reload doesn't quietly undo them — a bench you have to re-fill after every
reload doesn't get used.
- Add lets you name a wallet that hasn't spoken yet.
The avatar field warns about data:/blob: URLs. The embed refuses those, and
without the note the field looks broken: the name updates while the picture
silently doesn't.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A pasted image URL stretched the grid column and pushed the inputs past the card's edge. The body column needs an explicit min-width: 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ver HTTP
Reported from the bench: "back to normal and invalidate don't work". Both were
firing correctly — the answer just never changed, because `resolveUsers` checked
hand-typed profiles BEFORE hostile mode. One edit applied earlier (a wallet
renamed to "1111") permanently swallowed the diagnostic for that row, so the
probe, its undo, and every invalidate looked dead.
Priority is now hostile → hand-typed → backend. A diagnostic must not be
silently overridden by something you set ten minutes ago.
Two more things this exposed:
- Hostile mode and the edits lived only in the page, so in HTTP mode — where
the iframe calls the profile endpoint directly and never asks the page —
every control would silently do nothing. Both are mirrored to the backend
now (POST /api/bench/state) and the resolver applies the same priority.
- The "http endpoint: answering" status counted the PAGE's own fetches to
/identity/*, so it claimed the HTTP transport was live while bridge mode was
running. Only an iframe-originated hit (it carries X-Cherry-App-Id) flips it
now, and the log labels the two apart.
The hostile buttons also show which mode is active — with no state on screen,
"is the probe still on?" was unanswerable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was no way to see the fallback the whole feature is defined against. The demo directory answers for ANY wallet, so in normal operation a host name always exists and Cherry's own identity — the `.sol` domain or shortened address — never renders. The only route to it was turning the switch off in the portal, which also stops the iframe from asking at all, so it proves less than it looks. The bench now has three explicit answer modes: normal, "I know nobody" (null for every wallet) and the hostile probe. What used to be a hostile on/off pair is one selector with the active mode visible — "is the probe still on?" had no answer on screen before. Per-person the same thing already existed and stays: clear both fields in a row and Apply pushes null for that wallet alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t it In HTTP mode the iframe calls the profile endpoint directly and never asks the page, so `resolveUsers` never runs — and the "Chat users" panel sat empty with nothing to edit, which is the wrong half of the bench to lose exactly when you're testing that transport. The endpoint now records which wallets it was asked about, along with what the directory would answer, and the page folds those into its roster. Edits and answer modes were already mirrored to the backend, so the panel is fully live either way. README gains the local-vs-stage split: locally both sides are http and a plain `http://localhost:<port>/identity` endpoint works, while a stage embed is served over https and needs the bench exposed through a tunnel — otherwise the call is blocked as mixed content and the transport looks broken for a reason that has nothing to do with the feature. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README covered the feature; the guides an integrator (or an agent) actually follows did not. - integration_instructions.md: a section that applies to all three auth modes (auth decides who may post, this decides what their row says), both transports, push semantics, and the five ways it goes wrong in practice. Mode 1's "No host-side identity" bullet was flatly false and is fixed. - skills/cherry-embed-integration: the same ground as hard rules an agent can check, a Discovery question so the feature gets raised at all, and verification steps. Its jsDelivr pin was still 0.1.5 while every other file said 0.2.0. - docs/react-native.md: why mobile wants the profile endpoint rather than page handlers -- resolveUsers is a function and the bridge config crosses as a JSON string, so it cannot travel -- and which lines to add to the example host page if a rename has to be pushed from native. - README: link the reference page and the bench, and state the sanitizing rules, since a refused avatar or a truncated name reads as a bug otherwise. Two statements no longer matched the shipped behaviour: - invalidateUserProfiles was documented as forgetting profiles. Since the invalidate fix it marks them stale: the name on screen stays while the fresh answer is in flight, which is the whole point of the change. - resolveUsers batching is a 16 ms window, not an animation frame.
develop released 0.1.7 (unread API for host pages, the chatBubble launcher,
the painted iframe ground, the bridge's sender-window check). Six conflicts,
five of them two independent additions landing in the same spot: the identity
types next to UnreadState, the users.auth/users.update mount commands next to
setVisibility, both README sections, both CHANGELOG entries, and 0.2.0 as the
minor bump on top of 0.1.7.
Marketing.tsx was the one real conflict: develop rewrote buildSnippet to
return { code, shown, total }, while this branch had only touched the CDN pin
inside it. Took develop's function and re-pinned in place.
CDN pins go back to a version that exists. This branch had moved them to
0.2.0, which is not published; develop had reverted them to 0.1.5, which the
0.1.7 release then made stale. Everything now points at 0.1.7 -- README,
integration_instructions, the skill, and the wallet-only snippet -- and the
bump belongs in the commit that publishes 0.2.0 to npm, not before it.
package.json stays 0.2.0: that is the package's own version, not a pin.
typecheck clean, 260 tests green, build clean.
Lets a host app supply the display names and avatars for the wallets in its
chat, so the widget renders its own users instead of a
.soldomain or ashortened address. Visual and scoped to one running embed: Cherry persists none
of it, the wallet stays the author of every message, and the Cherry app is
unaffected.
Surface
The iframe pulls — only it knows which wallets are on screen:
The host pushes what changes — an open chat would otherwise never notice a
rename, since the iframe only asks about wallets it hasn't resolved yet:
Plus
userProfilesin the config (right first paint, no round-trip) andgetUser, reserved for the profile view.Test bench —
example/host-identity/One page, one small backend, and a checklist of what "working" looks like. It
covers both transports (page handlers vs. a profile endpoint the iframe calls
directly), a live rename,
invalidateUserProfiles(), anullprofile fallingback to the Cherry identity, @mention autocomplete over the app's own
directory, and a "Serve hostile profiles" button that answers with a bidi
override, an embedded newline, zero-width padding, 400 characters and a
javascript:avatar — so the sanitizer can be checked by eye.Runs authenticated with no wallet extension: with
APP_SECRETset it mints anembedToken server-side, which is what makes it a one-command bench.
Verified end to end
Ran the bench against a full local stack (server + realtime + embed + traefik).
Every claim above was checked in a live chat, not just in unit tests: names and
avatars replaced, both transports answering, a live rename applying instantly,
the hostile payload rendering flattened and pictureless, and @mention
autocomplete finding directory users Cherry has never heard of — with the
picked wallet riding along invisibly, so mention routing still works.
Two bugs the live run caught, both fixed:
weren't valid base58 — the invisible wallet tag on a picked mention went
unrecognised and the address rendered as visible junk beside the name. A
bench that lies about the feature is worse than none; the roster now
base58-encodes the digest.
the avatar.
users.updatenow merges field-wise — documented here in themethod's JSDoc, the type, the README and the changelog.
Notes for review
wired only when supplied, so an unregistered method gets
METHOD_NOT_FOUNDimmediately — which the iframe reads as "this host cannot answer" and stops
asking. A handler resolving empty instead would look like "nobody is known"
and cache negatives for every wallet in the room.
setIdentityTokenkeeps the token in memory only; it is re-sent after aniframe reload alongside the profile map, exactly like the auth token.
(cherrydotfun/self-service-portal#270). Integrators who prefer answering from
their backend configure a profile endpoint there and register no handlers at
all — the better fit for mobile WebViews, where the host page is a thin shim.
Docs
integration_instructions.md: a section that applies to all three auth modes(auth decides who may post, this decides what their row says), both
transports, push semantics, and the five ways it goes wrong in practice.
Mode 1's "No host-side identity" bullet was flatly false and is fixed.
skills/cherry-embed-integration: the same ground as hard rules an agent cancheck, a Discovery question so the feature gets raised at all, and
verification steps. Its jsDelivr pin was still 0.1.5 while every other file
said 0.2.0.
docs/react-native.md: why mobile wants the profile endpoint rather than pagehandlers (
resolveUsersis a function and the bridge config crosses as a JSONstring, so it cannot travel), and which lines to add to the example host page
if a rename has to be pushed from native.
rules, since a refused avatar or a truncated name reads as a bug otherwise.
invalidateUserProfilesmarks profiles stale rather than forgetting them (thename on screen stays while the fresh answer is in flight), and
resolveUsersbatching is a 16 ms window, not an animation frame.
Checks
tsc --noEmitclean,npm run buildcleanpackage.jsonis 0.2.0 -- that is the package's own version, not a pin.Merged
develop(0.1.7: unread API, chatBubble launcher, painted iframe ground,the bridge's sender-window check). Five of the six conflicts were two independent
additions in the same spot; the real one was
Marketing.tsx, where developrewrote
buildSnippetto return{ code, shown, total }while this branch hadonly touched the pin inside it — took develop's function and re-pinned in place.
Publish to npm only after the embed half is in production: cherrydotfun/messaging-server#313
and #316 are in
develop, riding the release in cherrydotfun/messaging-server#318.Until that ships, the new methods have nothing on the other side. The portal docs
(cherrydotfun/self-service-portal#272, merged) describe 0.2.0, so the npm release
is what closes that gap.
🤖 Generated with Claude Code