Skip to content

fix(web): send the selected project/org on API calls, not just page loads - #30

Open
marcorivm wants to merge 1 commit into
feat/budget-metering-accuracyfrom
fix/project-scope-headers
Open

fix(web): send the selected project/org on API calls, not just page loads#30
marcorivm wants to merge 1 commit into
feat/budget-metering-accuracyfrom
fix/project-scope-headers

Conversation

@marcorivm

Copy link
Copy Markdown
Member

Fixes two bugs found in local testing. 3 files, ~60 lines. Stacked on #28.

The bug that matters

After switching project, creating an agent put it in the default project.

proxy.ts turns the scope cookies into X-Project-Id / X-Organization-Id — but its matcher deliberately excludes v1:

"/((?!_next/static|_next/image|favicon.ico|v1|api|.*\\.).*)"

So the middleware never runs on API calls. Page requests carried the selection; every client-side call to /v1/* did not. resolveProjectId, seeing no header, silently fell back to findUserDefaultProject — so writes landed in the wrong project while the page around them looked correct. That's the worst shape for this kind of bug: no error, no visible inconsistency until you go looking for the agent.

This is a hole in #23's design. I assumed the proxy covered API calls; it doesn't.

getProjectId() / getOrganizationId() in api-fetch.ts exist for exactly this purpose and were undefined stubs (the EE build aliases the module away). They now read the cookies, and apiFetch attaches the headers. An explicit header from the caller still wins, so projects.list({ organizationId }) keeps its account-route override.

Guarded on typeof documentgetProjectId is called from queryKeys' scope() during render, which also runs server-side, where scope must come from the request headers the proxy already set.

The bug that was visible

The switcher reverted to the default until a reload. Both switchers read the cookie in a mount effect, which doesn't re-run when they write a new one — router.refresh() re-renders server components but leaves client state alone. They now hold the pending selection, so the trigger updates immediately.

A comment I had backwards

With getProjectId() returning a real value, queryKeys' scope() finally re-keys per project, so cached agents/secrets/policy can't bleed across a switch. The queryClient.clear() calls in both switchers are now belt-and-braces rather than the only thing preventing it — I corrected the comments, which claimed the opposite.

Verification

pnpm run check                    # 9/9
pnpm --filter @onecli/web test    # 13 passed
pnpm --filter @onecli/web build   # compiled successfully

Manual: switch project → trigger updates immediately; create an agent → lands in the selected project; reload → still selected.

Not fixed here

The /v1/approvals/pending 401 is unrelated and not a code bug — see the PR discussion. The org switcher hiding is expected when you belong to only one org.

…oads

Found by testing: after switching project, creating an agent put it in the
DEFAULT project, and the switcher reverted to the default until a reload.

Root cause: `proxy.ts` turns the scope cookies into X-Project-Id /
X-Organization-Id, but its matcher deliberately EXCLUDES `v1`, so the
middleware never runs on API calls. Page requests carried the selection;
every client-side call to /v1/* did not. The API's `resolveProjectId`, seeing
no header, silently fell back to the caller's default project — so writes
landed in the wrong place while the page around them looked correct.

`getProjectId()` / `getOrganizationId()` in api-fetch.ts existed for exactly
this and were `undefined` stubs (the EE build aliases the module). They now
read the cookies, and `apiFetch` attaches the headers. An explicit header from
the caller still wins, so `projects.list({ organizationId })` keeps its
account-route override.

Guarded on `document`: `getProjectId` is called from `queryKeys`' `scope()`
during render, which also runs server-side, where the answer must come from
the request headers the proxy already set.

Second bug, same symptom: both switchers read the cookie in a MOUNT effect,
which does not re-run when they write a new one — `router.refresh()` re-renders
server components but leaves client state alone. They now hold the pending
selection so the trigger updates immediately.

Side effect worth noting: with `getProjectId()` returning a real value,
`queryKeys`' `scope()` finally re-keys per project, so cached agents/secrets/
policy can no longer bleed across a switch. The `queryClient.clear()` in the
switchers is now belt-and-braces rather than the only thing preventing it —
comments corrected, since they claimed the opposite.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant