Tell one person, in a kind the client knows how to render (GRYT-896) - #130
Merged
Conversation
The outdated-client reminder was a chat message. `postSystemMessage` writes a row into the messages table and broadcasts it, so a notice addressed to one person by name — "your Windows client (v1.6.24) cannot update itself" — was stored for good and shown to everybody in the channel. The whole server learned that somebody's install was broken, and there was no way to take it back. It is a directed notice now: that person's sockets only, nothing written down. `clientsInfo` already maps every socket to a `serverUserId` and `chat.ts` already filters on it, so the delivery is the same shape rather than new machinery. **The server sends a kind and a version. It does not send a sentence.** That is the security property and it is the reason for the shape. A panel in app furniture, carrying text the server chose, addressed to one person, is a phishing message with a nice border: "Your Gryt session has expired, sign in at …". Attribution and link-stripping make that harder. Sending no text makes it impossible. Every string that reaches the screen now ships in the client. It is worth nothing if a *value* can be a sentence, so each field is checked on the way out and a notice that fails is dropped rather than trimmed — a half-filled notice would render copy that does not match what happened. A version is `x.y.z` and nothing else. The cost is a client release whenever there is something new to say. Accepted: there is one kind today, and anything genuinely bespoke belongs in `postSystemMessage`, which is public, attributable and deletable — the right shape for something a server tells everybody. Join and leave stay there. `remindOutdatedWindowsClient` loses its `nickname` argument. The client renders this to the person it is about, so it says "your client"; there is nobody else in the room to disambiguate for. `formatUpdateReminder` and the installer URL go with it, to the client. One bug the tests did not catch until the rig did: the first version emitted with `io.to(clientId)`, a room lookup, and reached nobody. Every other targeted emit here uses `io.sockets.sockets.get(cid)`. The fake in the test had a `to()` method, so it passed against an implementation that sent nothing — it is shaped like the real registry now, and a notice only counts as sent if a registered socket received it. Seven checks, four mutations, each caught: widening the version pattern to any non-empty string, dropping the validity gate, dropping the per-user filter so it broadcasts, and growing a `message` field on the notice type. The last is a source check, because a field carrying prose would typecheck perfectly and hand the server back the exact thing this shape exists to take away. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Server half. Client half is Gryt-chat/client#374 — merge this one first, or a client that knows the event never gets sent one.
What was wrong
The outdated-client reminder went through
postSystemMessage, which does aninsertMessageand broadcasts. So a notice addressed to one person by name — "your Windows client (v1.6.24) cannot update itself" — was stored permanently and shown to everybody in the channel. The whole server learned somebody's install was broken, and there was no way to take it back.The shape
The server sends a kind and a version. It does not send a sentence.
That is the security property, not a style choice. A panel rendered in app furniture, carrying text the server chose, addressed to one person, is a phishing message with a nice border: "Your Gryt session has expired, sign in at …". Attribution and link-stripping make that harder. Sending no text makes it impossible. Every string that reaches a screen now ships in the client.
It is worth nothing if a value can be a sentence, so every field is checked on the way out and a failing notice is dropped rather than trimmed — a half-filled notice renders copy that does not match what happened.
Delivery is that person's sockets only.
clientsInfoalready maps every socket to aserverUserIdandchat.tsalready filters on it, so this is the same shape rather than new machinery. Nothing is written down — it is state, re-sent on the next join while the condition holds.The cost: a client release whenever there is something new to say. Accepted — there is one kind today, and anything genuinely bespoke belongs in
postSystemMessage, which is public, attributable and deletable. Join and leave stay there.A bug the tests did not catch until the rig did
The first version emitted with
io.to(clientId)— a room lookup — and reached nobody. Every other targeted emit in this codebase usesio.sockets.sockets.get(cid).The fake in the test had a
to()method on it, so it passed against an implementation that sent nothing. It is shaped like the real socket registry now, and a notice only counts as sent if a registered socket received it. Worth knowing about, because a fake shaped to match the code rather than the runtime is a test that agrees with whatever it is given.Tests
Seven, four mutations, each caught:
messagefield added toClientNoticeThat last one is a source check, because a field carrying prose would typecheck perfectly and hand the server back the exact thing this shape exists to take away.
What to look at
remindOutdatedWindowsClientlost itsnicknameargument. The client renders this to the person it is about, so it says "your client".server:verify, not on a session restore — same as before this change, but it means an already-joined client reconnecting will not see it until their session lapses. Pre-existing; flagging it rather than widening the diff.formatUpdateReminderand the installer URL are gone from here. They live in the client now.yarn test694 pass.tsc --noEmitandeslintclean.🤖 Generated with Claude Code