Skip to content

client: a follower tab starts against a leader hosting a different account #1322

Description

@FSM1

What

A follower tab's start(secret, accountId) resolves against a leader hosting a different account, and every later command then runs on that leader's engine.

Mechanism

EngineClient.start (packages/client/src/engineClient.ts) forwards to the live transport:

  • Leader pathLocalTransport.start posts accountId to the worker, and EngineHost.engineFor (packages/client/src/worker/engineHost.ts) refuses a second account: if (current.accountId !== id) throw refuse('alreadyStarted', 'another account holds this engine').
  • Follower pathBroadcastTransport.start() (packages/client/src/broadcastTransport.ts) takes no parameters at all. It returns leaderReady, which settles on the cb:leader beacon. Nothing compares the account the follower is starting for against the account the leader's engine actually opened.

So the guard that makes per-account durable-store namespacing meaningful (makeBrowserSeams: "two accounts signed in on one origin never share one") exists only on the leader path. Two tabs on one origin, signed into different accounts, put the follower's UI on the leader's vault — reads render the wrong account's tree, and an upload from that tab lands in the wrong account's vault.

Same origin is the stated trust boundary (packages/client/src/broadcast.ts header), so this is a cross-account correctness/containment gap rather than a cross-origin escape — but it is exactly the confusion per-account namespacing is meant to deny.

Shape of the fix

Carry the account through the follower-to-leader handshake and fail closed on a mismatch. The port adoption (cb:portHello / cb:portReady in packages/client/src/broadcast.ts) is the natural carrier: it is already private to one leadership and authenticated by the leadership token.

Two design questions have to be settled first, which is why this is not a review-fix:

  1. Liveness. A leader elected before anyone logged in has no account yet. A follower start that waits for the leader's account would hang against such a leader. The handshake needs a defined answer for "leader not started" that neither hangs nor silently accepts.
  2. Ordering. BroadcastTransport.start currently resolves without brokering a port. Making it account-checked means either brokering at start, or re-announcing the leader beacon once the leader's engine starts.

Acceptance

  • A follower whose accountId differs from the leader's live engine account has its start rejected, and issues no command against that leader.
  • A follower whose accountId matches starts exactly as today.
  • A two-tab browser test in packages/client/test/browser/ covers the mismatch, alongside the existing leadership suites.

Provenance

Raised as an outside-diff review finding on #1311 and verified against that branch; deferred there because the fix is a wire-protocol change with the two open design questions above, not a review edit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    comp:webpackages/client + apps/web — WASM host, browser seams, React UIv2-buildv2 rewrite build slice

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions