Repository navigation
fix(social): keep the owner name and home when a room-only identity is promoted - #867
Conversation
…s promoted
Promoting a room-only Bot Social identity to a person grant rewrote the member
record from scratch: `register(bot, { forInvite: true })` called memberFields with
its defaults, and the `bot.grants` handler wrote the literal { bot_id, handle }. Both
dropped the owner-level ownerName and home that the room-share envelope reads, so the
next group invitation said "a friend" with an empty inviterHome until a restart.
Both sites now carry the stored values forward and fill either from one whoami only
when the room-only record never had it; memberFields' defaults are unchanged.
Fixes #863
|
Verdict: PASS at 974874e Ran: codeload tarball of this tip; in Tip: head 974874e, base fdecbfa, What I checked
Non-blocking
Not reviewed (out of scope): |
Written-by: Dev3 Tip SHA: 0d1f5db Part of #852 — this PR does **not** close it. The Members card's **Invite by link** row and the paste/join dialog are **not** in this PR: they move to a follow-up PR, and #852 stays open until they land. Fixes #877 — the wire half of the umbrella. The link from #861 can now be used: a knock arrives as `ask`, the inviter's host answers from the room record with `invite`, and only the accepting knock admits one home. Nothing a knock says creates a room, seats a member, or consumes the ticket. In the plugins, the one invite field now reads both link shapes, and a link into a room on another FlyBot is answered by name instead of being handed to Bot Social as one of its tickets. ## Reproduce - On `fdecbfa0` the knock is dropped: `parseRoomShare` answers `undefined` for `{ "v": 1, "op": "ask", "ticket": "AbCdEfGhIjKlMnOpQrStUv" }`, because `ask` is not an op and every payload must carry a `roomId` an invitee cannot know. - Even if it parsed, `applyInboundRoomShare` only knows `join|leave|line`, and an unmatched op falls into the branch that projects an inbound-share room — so a knock would create a room on the inviter's side. - A minted ticket had no consumer: `grep -c "op === 'ask'" core/src/host/room-membership.js` answers `0` on that base. - Plugin side: `parseFaceInviteLink('https://social.botawaken.com/r/@chief.alice.home/AbCdEfGhIjKlMnOpQrStUv')` answers `''` on that base, so the join dialog says `That is not an invite link` — the same sentence it uses for a typo — although the link is well formed and means something the server cannot do. ## What changed - `core/src/shared/room-share.js`: ops `ask` / `invite`; fields `ticket`, `accept`, `lead`, `expiresAt`. `ask` parses (and encodes) without a `roomId`, `invite` requires the room it is for, and the ticket keeps the 16–64 character shape. - `core/src/host/room-membership.js`: - `answerLocalInvite` answers the knock from the room record. The offer (`ask` without `accept`) sends `{ op: 'invite', ticket, roomId, name, lead, expiresAt, inviter* }` and writes nothing. Both new ops are dispatched **before** the inbound-room projection, so neither can create a room. - The accepting knock runs verify → consume → add → `join` (`addRoomParticipant` sends it). `acceptedBy` is written once, at accept. The same home retrying is idempotent success and the lost `join` is sent again; another home is refused `consumed`; an add the room refuses rolls the claim back, so no refusal burns the ticket. - The asker is `event.fromHandle`, the transport's sender — nothing in the payload can name it. Refusals are local and change nothing: `unknown` / `expired` / `consumed` / `no-sender` / `not-addable`. `rt.onLocalInviteOffer` is the seam the dialog will draw from (no UI yet). - The four non-blocking items from #861's review: an expired ticket resolves no room; `acceptedBy` is the one name; `mintRoomInvite` builds the URL before persisting (a handle a link cannot carry leaves no ticket behind); the no-identity refusal says `This machine has no Bot Social identity yet`. - `docs/subsystems/core-host.md` (the four #861 follow-ups) and `docs/subsystems/social.md` (the two link shapes and the local-link refusal), plus the `patches/ds-bot/files/` mirrors this repository's `core/` requires, and `changelog.d/852-local-room-invites.added.md`. - `plugins/social/src/face-rooms.js` + `social.js` + `client.js`: `parseInviteLink` reads both shapes (`{ kind, ticket, handle? }`); `parseFaceInviteLink` answers only the face-room ticket. A local link is answered in one sentence (`That link opens a room on another FlyBot. It does not join a face-to-face room.`) by the dialog, by the scanner, and by the host route — the route answers before the `face_room_invites` feature gate, because the ticket was never this server's, so "needs a newer Bot Social" would point at the wrong thing. ## What I ran - Full core suite at this tip → **1305/1305**, 0 fail (`TMPDIR` outside `/tmp`, which is a full 128M tmpfs here). - `core/test/remote-room.test.js` → **50/50**, including the three `#852` cases. - Full `plugins/social` suite at this tip → **382/382**, 0 fail (`face-rooms` 29/29). - `node scripts/ds-bot-patches.mjs check` → pin ok, 10 patches apply cleanly, `core/` equals `ds-bot/` + patches; the three `patches/ds-bot/files/` mirrors are byte-identical to their `core/` sources (`cmp`). - `node core/scripts/lint.mjs` → 0 errors; `node scripts/lint-plugins.mjs` → ok. - Rebased onto `1d81580e` (current `main`: #867 and #870 included). Line diff of this tip against `main` for `plugins/social/test/social.test.js` is **+42/−0** — every test main already has is still there, including #870's, #848's and #847's. ## Red without the source change Nine knives, each cut on this tip: | removed | red | | --- | --- | | the `ask` / `invite` dispatch before the room projection | `#852` test 3 — a knock projects a room | | the `consumed` check | `#852` test 3 — a second home takes the seat | | the claim rollback on a refused add | `#852` test 3 — a refusal burns the ticket | | resolving by ticket string only | `#852` test 1 — an expired ticket opens a room | | persisting before the URL is built | `#852` test 1 — a refused mint leaves a ticket | | `Sign in first` instead of the identity message | `#852` test 2 — the refusal names the wrong problem | | the dialog's local-link branch | `#852: a link into a room on another FlyBot is answered in one sentence and calls nobody` — only that one | | the host route's local-link refusal | `#852: the host refuses a local room link before the feature gate, and sends nothing` — only that one | | the two-segment shape in `parseInviteLink` | `#852: two link shapes in one field` — only that one | ## Not run (and why) - The Members card's **Invite by link** row and the paste/join dialog: a follow-up PR, where the face preview card moves into a core component first. Nothing in this PR is waiting on them; #852 stays open. - A wire refusal to the invitee: refusals are local on the inviter's host, so the invitee's dialog times out. Sending `refused` back belongs with the dialog, which does not exist yet. - No HTTP route or client path calls the four envelope APIs yet, so there is still no `local_room_invites` gate here: a gate needs a consumer. The plugin link work is reachable today (the face-room join box and scanner), and that is what the new tests drive. ## Risk - The claim is written in memory before the seat is added and rolled back if the add throws; a process crash between those two statements persists neither (the first `save` is the add), so the ticket survives. Once the seat exists the retry is the idempotent path. - The ticket stays a bearer secret in `state.rooms` for ten minutes, as in #861. - `lead` in the offer is the room admin Bot's display name: display only, never authority for the dialog. - One sentence now lives twice, in the host (`face-rooms.js`) and in the client bundle (`client.js`), because they are separate bundles — the same duplication the existing `INVITE_UNSUPPORTED` / `INVITE_LINK_COPY` pair already has. A test pins both.
Fixes #863
What changed
Promoting a room-only Bot Social identity to a person grant rewrote the member record from scratch, dropping the owner-level
ownerNameandhomethatshareInviterreads:register(bot, { forInvite: true })calledmemberFields(existing)with no overrides, and its defaults areownerName: ''/home: home?.handle;bot.grantshandler wrote the literal{ bot_id, handle }, dropping both.Both sites now carry the stored values forward and fill either one from a single
whoamionly when the room-only record never had it — the same fallbackregisterAll's backfill uses.memberFields' defaults are unchanged, andregisterOncestill passes both explicitly. OnlyroomOnlyis dropped.Docs:
docs/subsystems/social.md(invariant 4). Changelog:changelog.d/863-promotion-keeps-name.fixed.md.Verification at the tip
Tip SHA: 974874e
Ran:
plugins/social378/378 (node --import ./test/resolve.mjs --test "test/*.test.js"; basefdecbfa0is 376); lintcore/scripts/lint.mjs social0 errors (43 pre-existing warnings); oxlint on the two touched sources 0 errors;changelog.mjs check28 fragments well formed;testing-layers.mjs332 tests / 14 workflows classified.Not run (and why):
arch-docsneedsorigin/mainto diff and this machine has nogit; the owned subsystem doc is updated.Evidence (withdrawing the change reds a test)
The two new cases are separate on purpose, and the
whoamimock answers something else than the record so a passingwhoamifallback cannot be mistaken for preservation:memberFields(existing)without overrides atregister(theforInvitepromotion)#863: promotion fills a name the room-only record never had…red; the grant case stays green (377/378){ bot_id, handle }at thebot.grantshandler#863: a person grant promotes a room-only identity without dropping its name and homered; theregistercase stays green (377/378)One knife never reds both. The stored values win over
whoamiin the grant case (home: '@alice.home',ownerName: 'Alice'whilewhoamianswersWhoami Name/@whoami.home), so a regression that silently refills fromwhoamiis caught too.Acceptance 3 (
shareInviteranswers the same name and a non-empty home after promotion) is covered by the record these cases assert plus the#848envelope cases incore/test/remote-room.test.js; the plugin does not compute the envelope.Risk
Low: two promotion sites, values preserved instead of dropped, one
whoamiread at promotion (shared with the in-flight one when there is one). The startup backfill case is unchanged and still green.