Repository navigation
Yes on the guest prompt moved nothing when the account was already a member (GRYT-1225) - #193
Merged
Merged
Conversation
…member (GRYT-1225) carryIdentityForward returned account_already_member when the account already had a row. So a yes after joining from a phone, or after adding the server again once signed in, moved nothing and told nobody. It now merges the guest into the account's row in one transaction. Every column naming the guest as sender, uploader, creator, reporter or moderator names the account instead. A reaction, conversation or mention both had counts once, and blocks follow in both directions. The account keeps its name, picture and roles. It gets ownership and the owner role if the guest owned the server. Either way it takes the stricter moderation state and the earlier join date. The guest's refresh tokens are revoked and its row deleted, so a second claim gets no_prior_membership. server:joined carries identityClaim when a link came with the join. After a merge the server clears its message cache, sends chat:merge_user, resends the guest's conversations to their members and revokes other sockets still on the guest. A socket that proves a different identity now drops the old one before the new one is admitted, so a refused switch stops getting member broadcasts as the guest. Co-Authored-By: Claude Opus 5 <[email protected]>
This was referenced Sep 15, 2026
Merged
This was referenced Sep 15, 2026
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.
Review-required. This changes
src/db/sqlite, so Sivert reads the whole diff before it merges. Don't merge it on green.GRYT-1225. The client half is Gryt-chat/client#587, and it needs this released first.
Gryt-chat/docs#116 documents
chat:merge_user, and the two API reference checks wait on each other. This one reads docs main, which documents 147 of the 212 socket events. The check wants 70%. The one on docs#116 reads server main, which doesn't havechat:merge_useryet. So it fails on an event the server doesn't have. Merge this first and docs#116 straight after. Server main is under the coverage floor only until docs#116 lands.What yes did before
Yes on "Were you here as a guest before?" stores the answer, drops the server session and reconnects. On that join the account signs a link with the guest's key, and
carryIdentityForwardpoints the guest's users row at the account. It's the sameserver_user_id, so name, picture, roles, messages and ownership all come along.If the account was already a member, it returned
account_already_member, logged a line and moved nothing. The client wasn't told. That happens when the account joined from a phone first. It also happens when this device added the server again after signing in, since that joins as the account before the prompt shows.What it does now
carryIdentityForwardmerges the guest into the account's row instead, in oneBEGIN IMMEDIATEtransaction (mergeGuestIntoAccountinsrc/db/sqlite/mergeGuest.ts):no_prior_membership, and the join carries on as normal.The join then tells the client what happened, with
identityClaimonserver:joined:carried,merged,no_prior_membershiporfailed. It's only there when a link came with the join.After a merge the server clears its message cache. It sends
chat:merge_userwith both ids to verified sockets, so clients can rewrite the ids on messages and reactions they already have. It also sends each of the guest's conversations to its members again. Any other socket still signed in as the guest getstoken:revokedwith the reasonidentity_merged.A socket that proves a different identity than the one it holds now drops the old one straight away, before the new one is admitted. Until now, a device that said no and was then refused, say because the server needs an invite, kept getting member broadcasts as the guest.
Look closely at
AUTHOR_COLUMNSis the list of every column that can name the guest.mergeGuest.test.tsreads the schema and fails on any column matchingserver_user_id,sender_server_idorcreated_bythat isn't listed or handled. The test can't seeaudit_log.target, which holds all kinds of ids, so that one is listed by hand.forgetIdentityinjoin.tsruns for every socket that verifies as someone else, including ones that never saw the prompt. For a move the account ends up on the sameserver_user_id, so the SFU sync should leave a call alone. For a merge or a no the id changes, and the sync drops a socket in a call a couple of seconds later. I didn't run an SFU, so the voice part is read off the code.Tests
src/db/sqlite/mergeGuest.test.tscovers messages, reactions both left on one message, attachment ownership, ownership moving from a guest owner, an account that already owns, and a second claim. It also covers conversations, thread authorship, mentions, blocks, revoked refresh tokens, mutes, a rollback when a step fails, and the schema check.carryIdentity.test.tsnow expectsmergedwhere it expectedaccount_already_member. I broke the reaction dedupe, blocks, ownership, rollback, conversation dedupe and the files column one at a time, and each one failed a test.yarn test(1326 passing),yarn test:examples,yarn build,npx eslint .andcheck-comment-lengthpass locally.Checked against the client
I ran this branch as a throwaway server with the client branch and drove both in headless Chromium, reading the DOM, the socket frames and the database. Keycloak needs credentials I can't type, so I faked three things, all uncommitted and since reverted:
getValidIdentityToken,useAccountanduseUserIdanswered for a fake account when agryt_fake_accountkey was set in localStorage.GRYT_TRUSTED_CERT_ISSUERS.65 checks passed. Among them:
member,owner. The DM was between the account and the third member, and the third member was sent it again. The phone never reloaded, and its row went from "Birch 9:49 PM hello from the guest 👍 2" to "Sivert 9:49 PM hello from the guest 👍 1".identityClaim: no_prior_membershipand changed nothing.token:revokedwithidentity_mergedand rejoined as the account without an error, in 3 runs out of 3.failedand left everything as it was.forgetIdentityline taken out it got 3.The client's
yarn e2epassed against this branch too: 13 passed, and the phone settings test skipped because it needs a server of its own.🤖 Generated with Claude Code