mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-08 00:54:38 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Which companies a person belongs to is an authorization fact the server owns, and the UI caches the answer under a single `["companies"]` key > - That cache entry carries no account identity, and `main.tsx` sets `staleTime: 30_000` for every query, so for thirty seconds after a sign-in the previous account's list is served with no request at all > - The invite landing page reads that list to decide whether the person is already a member of the inviting company > - A list that arrives with no loading state and no error therefore looks authoritative while describing somebody else > - This pull request makes the page trust only a list it fetched itself, for the account signed in now > - The benefit is that a membership decision stops depending on cache freshness, which nothing in the app guarantees ## Linked Issues or Issue Description No public issue exists. Refs #11380, #11382. The problem follows. **What happened?** `InviteLanding` read the shared `["companies"]` cache entry as proof of membership in two places: - The post-sign-in redirect called `fetchQuery(companiesListQueryOptions)`, which returns the cached entry without a request while it is inside the app-wide `staleTime`. - An effect cleared the pending invite token whenever the cached list contained the invited company. Neither checked that the list belonged to the account signed in now. A second account signing in on a warm tab, or a session that lapses server-side, is enough to reach both. **Expected behavior** The page decides membership from a company list fetched for the current session. **Steps to reproduce** 1. On a self-hosted instance in `authenticated` mode, sign in as account A, which belongs to company X. 2. Within thirty seconds, open an invite link for company X and sign in as account B, which does not belong to it. 3. The page reads A's cached list, finds company X, and treats B as already a member. **Paperclip version or commit** `master` at `2a4b4bc63`. ## What Changed - `ui/src/pages/InviteLanding.tsx` — the membership query sets `staleTime: 0` so it revalidates on mount, and the verdict is withheld until that fetch lands, keyed on `isFetchedAfterMount`. The token-clearing effect and the "already a member" branch both read through that gate. - `ui/src/pages/InviteLanding.tsx` — the post-sign-in path cancels anything still in flight for the previous session, then forces a fetch for the new one with `staleTime: 0`. - `ui/src/pages/Auth.tsx` — sign-in resets the companies query instead of invalidating it. Invalidation leaves the previous account's list readable, and its fetch running, until the refetch returns. - `ui/src/pages/InviteLanding.test.tsx` — coverage for the warm-cache case, the token-clearing effect, and the `local_trusted` exemption. ### `local_trusted` is exempt Those instances have no accounts, so the shared list is the only identity there is. `membershipIsAccountScoped` is false there and the gate stays open. ### Rebased onto the account-keyed cache #11488 landed while this was open and keys the company list by account, so the page can no longer reach another account's list at all. Two things changed here as a result: - The post-sign-in read now calls `fetchCompanyListForCurrentAccount`, which replaces the `cancelQueries` plus forced `fetchQuery` this PR originally carried. The helper is strictly stronger: it detaches the in-flight `/companies` request inside the query function, and it resolves the account identity past the session invalidation immediately above rather than trusting the session entry still in the cache. - The observer reads through `useCompanyListQuery`. The mount-scoped `isFetchedAfterMount` gate is **kept**, not removed. Its purpose has narrowed — cross-account leakage is now structurally impossible, so what remains is holding the verdict until this page has a list rather than acting on a pending one. It is still load-bearing: disabling it fails two tests here. Removing a defense in the same change that rebases onto a new foundation is the wrong order; that is a follow-up once the keying has proven itself. `Auth.tsx` can safely reset, because it navigates away on success and `InviteLanding` mounts fresh afterward. Measurements of exactly when that rewind does and does not bite are in [#11380](https://github.com/paperclipai/paperclip/pull/11380#issuecomment-5300984911). ## Verification - `InviteLanding.test.tsx`, `Auth.test.tsx`, `companies-query.test.ts`, `CompanyContext.test.tsx` together: **51 passed**, run twice. - `pnpm tsc -b`: clean. - `InviteLanding.test.tsx` and `Auth.test.tsx` together: 21 passed. Both failures are pre-existing and unrelated. Each reproduces on a tree that does not contain this change, in files this change does not touch: | Failure | Why it fails | | --- | --- | | `IssueProperties.test.tsx` | Timezone-dependent: expects `4:08 PM`, gets `9:08 AM` | | `StatusCards/format.test.ts` | Time-of-day dependent: "only counts updates started today" breaks near midnight | **Not done:** no manual two-account run in a browser. The path needs two accounts on an `authenticated` instance, which a local dev instance cannot exercise. ## Risks Low. The failure direction is a membership verdict withheld for one extra round trip, which resolves itself; the direction it removes is one account's membership granted to another, which does not. It adds one request per invite-page mount, on a query key the app already uses. **This does not close the class.** The shared list is still unscoped for every other consumer. #11380 clears it on sign-out and #11382 handles the onboarding draft gate; all three are needed, because an account can change without passing through any one of those paths. ## Model Used Claude Opus 5 (`claude-opus-5`), through Claude Code. Extended thinking enabled. Tool use enabled: file read and edit, shell for typecheck and test runs. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>