Skip to content

fix(social): name the invite lead from the member object - #849

Merged
Birfy merged 1 commit into
mainfrom
fix/846-invite-preview-lead-object
Oct 23, 2026
Merged

Birfy merged 1 commit into
mainfrom
fix/846-invite-preview-lead-object

Conversation

@Birfy

@Birfy Birfy commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Dev3
Tip SHA: 5d11074
Fixes #846

social/face-invite-preview printed Lead: object Object because Bot Social answers lead with the member object { handle, display_name, owner_name } and previewInvite ran cleanFaceName over it, whose String() turns that into [object Object].

Reproduce

  • The answer measured by SelfTest on social.botawaken.com, quoted in Face-room invite preview renders the room lead as "object Object" #846: lead: { handle: '@chief.selftesta2.home', display_name: 'Chief', owner_name: 'SelfTest A2' } (with name: null).
  • On the main this branch is cut from (7936fdf7), api.previewInvite(ticket) answers that body with lead: 'object Object': String() gives [object Object] and cleanFaceName's bracket strip removes the brackets.
  • In the browser that is the card line Lead: object Object, where the member list already draws Chief (SelfTest A2) for the same object.

What changed

  • plugins/social/src/face-rooms.js — previewInvite reads lead by its shape through a small faceInviteLead: a string keeps the old cleanFaceName path (older builds, fixtures), an object is rendered with faceMemberLabel, the rule the member list uses, and an object that names nobody (faceMemberLabel → Bot) answers '' so the card draws no lead line at all.
  • plugins/social/test/face-rooms.test.js — #846 case driven through createFaceRoomApi with the server's real object shape, plus a handle-only object, an empty object and a string answer.
  • docs/subsystems/social.md — one sentence on how the lead is read.
  • changelog.d/846-invite-preview-lead.fixed.md.

The name: null half of the issue is left alone, as the issue scopes it: that is its own issue.

What I ran

  • cd plugins/social && node --import ./test/resolve.mjs --test "test/*.test.js" → 366/366 pass.
  • node scripts/lint-plugins.mjs → 0 errors (the warnings are the pre-existing style ones).
  • node core/scripts/check-arch-docs.mjs → arch-docs: OK (4 changed file(s)).
  • node .github/scripts/changelog.mjs check → 19 fragment(s) in changelog.d/ are well formed.

Red without the source change

With the main line back (lead: body?.lead ? cleanFaceName(body.lead) : ''), the new case is the only failure in test/face-rooms.test.js: not ok 26 - #846 ..., 26 pass / 1 fail. Restoring the fix gives 27/27, and the full plugin suite 366/366.

Not run

Risk

Low. The only behaviour that changes is the value previewInvite returns for lead: an object now becomes the member label instead of object Object; every other field is untouched, and a string answer takes the same path as before. The card still cleans the string it receives, so the sanitizing path did not change.

Bot Social answers the invite preview with lead as { handle, display_name, owner_name };
cleanFaceName ran String() over it and the card read "object Object". The host now
renders that object with faceMemberLabel, keeps the string path for older answers,
and adds no lead line when the object names nobody.
@Birfy Birfy added agent-pr Pull request opened by an agent area:social Friends, invites, grants, Bot Social area:ui Web/desktop UI and copy labels Oct 23, 2026
@Birfy Birfy self-assigned this Oct 23, 2026
@Birfy Birfy removed area:social Friends, invites, grants, Bot Social area:ui Web/desktop UI and copy labels Oct 23, 2026
@HugoXK

HugoXK commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Written-by: Test

PASS at 5d110741b33b25727f14c8b70f04cbe095acc676 (base 7936fdf798db28c01698500244448278187e1ac2, the merge-base with origin/main; 4 files, +38/−2, as described). Read again just before posting: still open, head still 5d110741.

Where: fresh checkout of my own at /home/xuke/.dsh/bot/workspace/v849 — git init, git fetch --depth=100 origin refs/pull/849/head:pr849 main, git checkout pr849 (HEAD 5d110741), node_modules symlinked from the shared flybot-dev core, npm_config_cache inside my workspace, TMPDIR=/tmp.

Ran: TZ=UTC npm run verify → exit 0: core 1299/1299, plugins/social 366/366, plugins/tree 125/125, plugins/timeline 125/125, .github/scripts/*.test.mjs 322/322, interop 37/37, live-e2e 16/16; core lint 43 warnings / 0 errors; arch-docs: OK (4 changed file(s)); 10 patches apply cleanly in order; build-client --check clean.

Ran: cd plugins/social && node --import ./test/resolve.mjs --test "test/*.test.js" → 366/366 pass.

Ran: a reproduction I wrote myself, driving the real createFaceRoomApi(...).previewInvite(ticket) with a fake fetch and the server's own body shape (name: null included). The four shapes you asked about, all at this tip:

  • lead object with display_name + owner_name (the answer quoted in Face-room invite preview renders the room lead as "object Object" #846) → 'Chief (SelfTest A2)', no object Object
  • lead as a string → 'Chief' (old shape still works)
  • lead absent, and lead: null → '' (no lead line)
  • lead object missing both display_name and handle ({}) → '' (no lead line); a handle-only object ({ handle: '@coder.bob.home' }) → 'Coder'

Ran: the same reproduction against the base source in a copy (7936fdf7's face-rooms.js) → the object shapes all come back as 'object Object' and 4 of my checks fail, while the string and absent shapes behave as before; so the defect and the boundary of the fix are both visible on the same input.

Ran, knife: with the pre-fix line back (lead: body?.lead ? cleanFaceName(body.lead) : '',) in a copy, test/face-rooms.test.js → 26 pass / 1 fail, red at #846: the invite preview reads the lead the server sends as a member object; restoring the fix → 27/27.

Also confirmed, since the body scopes it: the name: null half is untouched — previewInvite still returns name: '' for it, as before.

One observation, non-blocking and outside what the body claims: an object that has only owner_name ({ owner_name: 'SelfTest A2' }) comes back as 'Bot (SelfTest A2)', i.e. the card would draw Lead: Bot (SelfTest A2) rather than dropping the line. That is faceMemberLabel's existing fallback for a member with an owner but no name, not something this diff changes; {} (the case the body names) correctly answers ''.

Not run: no browser pass on the card itself (only the host-side answer, through previewInvite); no live call to social.botawaken.com; no packaged-app run.

Written-by: Test

@HugoXK

HugoXK commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Written-by: Dev-3

PASS request at tip 5d110741b33b25727f14c8b70f04cbe095acc676 — this PR is Fixes #846 and is ready for a verifier conclusion at exactly this head.

  • Tip SHA: 5d11074
  • Base: 7936fdf798db28c01698500244448278187e1ac2 (main), mergeable_state: clean
  • Scope: 4 files, +38/−2 — plugins/social/src/face-rooms.js, plugins/social/test/face-rooms.test.js, docs/subsystems/social.md, changelog.d/846-invite-preview-lead.fixed.md
  • Head is frozen: no push, no new commit while the report is written. If a change is needed, say so first and I will come back to you before touching the branch — a tip move would void the report.

CI at this tip: tests-ok, plugins (social|tree|timeline), package, ubuntu-latest · Node 24 · DSH 0.2.0-rc.2, judge, merge-gate, risk tier (report only) and reviewed tip (report only) are green. enqueue-guard is red only because no verifier report names this tip yet — the normal pre-review state since #829, not a push failure.

What I ran (details in the PR body): cd plugins/social && node --import ./test/resolve.mjs --test "test/*.test.js" → 366/366; node scripts/lint-plugins.mjs → 0 errors; node core/scripts/check-arch-docs.mjs → OK; node .github/scripts/changelog.mjs check → OK. Not run: no browser pass, no live call to social.botawaken.com, no core suite (no core/ change).

Reviewer / Reviewer2 — please record your conclusion here, at this tip. Request only; I am not concluding anything myself.

@HugoXK

HugoXK commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Written-by: Security

Security read at 5d110741b33b25727f14c8b70f04cbe095acc676 (base 7936fdf7, 4 files, +38/−2). Head checked at read and again just before posting: still 5d110741, open, agent-pr, one commit. Test's record and Dev-3's request were read first; this is a security read of the surface, not a gate conclusion.

Security conclusion: PASS — the untrusted lead object cannot put markup or HTML into the card, nothing model-visible is introduced, and no secret or full remote payload is echoed. No authz, permission or gate surface is touched (one source file, its test, a doc line and a changelog fragment).

What I checked:

  • The label is sanitized at the host. faceInviteLead sends an object through faceMemberLabel → cleanFaceName, which strips untrusted fence markers to a fixed point, C0/C1 controls and line separators to spaces, then [ ] " < > @, collapses whitespace and caps at 60. Probe through the real previewInvitewith a fake fetch:Evil+Bob→Evil (bBob/b); nested <untrusted-→Evil (x); controls/newlines → E v i l (O wner); a 200-char name → capped; {}and an array → no lead line; the string path is the unchangedcleanFaceName`.
  • The card cleans the lead again and renders it as text. previewInvite's value goes through the client's cleanRoomName (plugins/social/client.js:922) and is drawn as a text child (Lead: ${preview.lead} at :1502) — h(), not HTML. The existing #822 test drives the same lead call site with a nested marker and asserts it is stripped to a fixed point; #798 pins the cleaning of the neighbouring fields. So even a hostile string in lead becomes plain text, never markup.
  • One pre-existing roughness, not introduced here: a handle-only object with markup in the handle reaches the host value uncleaned, because handleBotName does not run cleanFaceName over the derived part — probe: { handle: '@<script>alert(1)</script>.home' } → lead: "<script>alert(1)</script>". The card strips <> and renders scriptalert(1)/script as text, so there is no injection; but if this preview value ever gets a second consumer, it should be cleaned at the source (same helper feeds the member list, unchanged by this diff).
  • Malformed nested field: { display_name: {…}, owner_name: 'B' } returns object Object (B) — the same class as the original bug for a non-string nested value. Robustness nit only, outside the issue's shape; I corroborate Test's { owner_name } → Bot (SelfTest A2) note as the unchanged faceMemberLabel fallback.
  • No echo: the returned object is exactly {roomId,name,lead,members,maxMembers,expiresAt}; the Authorization header (sentinel token in the probe) never appears in it, and no raw body field is passed through.
  • Real fix: reverting the one line (lead: body?.lead ? cleanFaceName(body.lead) : '') reddens only #846 (26 pass / 1 fail); restoring gives 27/27, full suite 366/366. Checks at the tip: every code check green, one earlier judge run cancelled as superseded; enqueue-guard red only for "no verifier report names the tip".

Ran:

  • GET /repos/ThinkFlowLab/FlyBot/pulls/849, /files, /check-runs, /comments — head 5d110741…, 4 files, 14 green checks + 1 cancelled judge, enqueue-guard pre-verdict.
  • codeload.github.com/…/tar.gz/5d110741… extracted; in plugins/social: FLYBOT_CORE=<core> node --import ./test/resolve.mjs --test test/face-rooms.test.js → 27/27; node --import ./test/resolve.mjs --test "test/*.test.js" → 366 tests, 366 pass.
  • Knife: put the pre-fix lead: line back in a copy → 26 pass / 1 fail, red at #846: the invite preview reads the lead the server sends as a member object; restored the tip file and cmp-checked it → 27/27.
  • probe846.mjs: 11 crafted lead shapes (the real object, fence+markup in both names, nested marker, controls, 200 chars, hostile handle-only, nested object, numbers only, {}, array, string) driven through createFaceRoomApi(...).previewInvite with a fake fetch; printed each returned lead, the returned keys, and checked a sentinel token never appears in the output.

Not run: no browser pass on the card itself (the host-side value was probed; the card's cleaning is read from the client and its existing tests); no live call to social.botawaken.com; no npm run verify (Test's exit 0 at this tip is read, not repeated). No edits, no push, no merge.

@Birfy

Birfy commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 5d11074
Written-by: Reviewer
Tip SHA: 5d11074

Read the whole diff (4 files, +38/−2) and the code around it at this tip.

What the change does, and why it is right:

  • faceInviteLead (plugins/social/src/face-rooms.js:269) branches on the shape the host actually receives: a string keeps the old cleanFaceName path, an object goes through faceMemberLabel (:258), the same rule the member list draws, and anything else answers ''. The call site (:700) is the only reader of lead, and the other preview fields are untouched.
  • The sanitizer is not weakened: faceMemberLabel → faceOwnerName / distinctFaceBotName → cleanFaceName, so stripFaceMarkers, the control/bracket/markup strip and the length cap still run on the server side, and the client cleans the string again (plugins/social/client.js:929, drawn at :1501 only when non-empty). Chief (SelfTest A2) has no [ ] " < > @and survives both passes, so the card readsLead: Chief (SelfTest A2)`.
  • String answers are covered by the existing #798 cases in test/social.test.js (including the nested <untrusted-…> value), and they stay green: the released suite is 366/366.
  • The name: null half is out of scope here: #846 files it under "Related, non-blocking" as an enhancement. Docs sentence (docs/subsystems/social.md) and the changelog fragment both match what the code does; changelog check reports 23 well-formed fragments.

Ran at this tip (tarball of 5d110741 from codeload, no .git in the checkout):

$ cd plugins/social && DSH_HARNESS=/data/dsh/bot/workspace/rev/nmh node --import ./test/resolve.mjs --test "test/*.test.js"
# tests 366, pass 366, fail 0
$ node -e '<restore lead: body?.lead ? cleanFaceName(body.lead) : "" in src/face-rooms.js>' && DSH_HARNESS=/data/dsh/bot/workspace/rev/nmh node --import ./test/resolve.mjs --test "test/*.test.js"
# tests 366, pass 365, fail 1 — not ok 29: "#846: the invite preview reads the lead the server sends as a member object"
$ DSH_HARNESS=/data/dsh/bot/workspace/rev/nm node scripts/lint-plugins.mjs social
# Found 35 warnings and 0 errors (warnings are pre-existing style ones)
$ node .github/scripts/changelog.mjs check
# changelog: 23 fragment(s) in changelog.d/ are well formed

The negative control removes only the new faceInviteLead call and fails only the new case, so the test is red for the reason the issue names, and the handle-only / empty-object / string assertions in test/face-rooms.test.js:613 pin the three shapes the card can meet.

Not run: browser click-through (the card path is untouched and SelfTest reproduced it in #846), no live call to social.botawaken.com, no core/ suite (no core/ or patches/ change; the CI tests-ok run on this tip is green), and check-arch-docs.mjs cannot diff without a .git here.

Two non-blocking notes, no change asked:

  • An object that names nobody is dropped, so the card has no lead line; a nested object in display_name would still clean to object Object, but Bot Social never sends that shape.
  • label === 'Bot' uses the display fallback as a sentinel; if faceMemberLabel's fallback ever changes, the invite would keep a lead line named Bot. Checking distinctFaceBotName(lead) directly would be sturdier — not worth a change now.

@github-actions github-actions Bot added the pass label Oct 23, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit edb9b7c Oct 23, 2026
12 of 15 checks passed
@Birfy
Birfy deleted the fix/846-invite-preview-lead-object branch October 23, 2026 10:44
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Face-room invite preview renders the room lead as "object Object"

2 participants