feat(web): project cookie transport and list/create data layer - #23
Open
marcorivm wants to merge 1 commit into
Open
feat(web): project cookie transport and list/create data layer#23marcorivm wants to merge 1 commit into
marcorivm wants to merge 1 commit into
Conversation
Slice 3a of docs/project-lifecycle.md — no visible UI, just what the switcher needs to exist. Split from the UI so the security-relevant part (a new source of project scope on every request) can be reviewed on its own. Flat editions have no transport for a switcher: proxy.ts strips the /p/<id> prefix when !CAPS.orgScopedUI, so the URL carries no project scope and the only sources are the /app-connect query bridge and the default-project fallback. DEFAULT_PROJECT_COOKIE is that transport — the proxy turns it into the X-Project-Id header the API already understands. Precedence is path > query > cookie. The cookie is last so an explicit /p/<id> or ?projectId= always beats a stale selection, and on flat editions it is the only source there is. The cookie is a HINT, never authority. resolveProjectId still validates the id against the caller's memberships and canAccessProjectAsUser before trusting it, so a forged or stale value resolves to no project rather than to someone else's. Applying it on every edition is therefore safe: it only ever SETS a header that is independently re-validated. Named and shaped to sit beside upstream's DEFAULT_ORG_COOKIE, which lands in this same file in v1.45.0 — one definition each so writer and reader cannot drift. projects.list() takes no org parameter: the org comes from the scope the proxy already sets. Upstream widens apiGet with an X-Organization-Id override in v1.45.0 for its account-route picker; client.ts is deliberately left untouched here since it is one of that release's conflict files. No tests: apps/web has no vitest harness on 1.44.0, and adding one would collide with the config upstream ships in v1.45.0.
marcorivm
force-pushed
the
feat/projects-switch-transport
branch
from
August 8, 2026 19:34
0442d6e to
6a3c163
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Slice 3a of #20, stacked on #22. 5 files, ~90 lines. No visible UI.
Split from the switcher UI deliberately: this adds a new source of project scope on every request, which deserves reviewing on its own rather than buried under components.
The problem it solves
proxy.tsstrips the/p/<id>prefix when!CAPS.orgScopedUI— so onossandonprem-slimthe URL carries no project scope at all, and the only sources are the/app-connectquery bridge and thefindUserDefaultProjectfallback. A switcher on those editions has nothing to ride on.DEFAULT_PROJECT_COOKIEis that transport; the proxy turns it into theX-Project-Idheader the API already understands.Why this is safe
The cookie is a hint, never authority.
resolveProjectIdalready validates the id against the caller's active memberships andcanAccessProjectAsUserbefore trusting it — so a forged or stale cookie resolves to no project, not to someone else's. That's why applying it on every edition is fine: it only ever sets a header that is independently re-validated downstream.Precedence is path → query → cookie, cookie last:
An explicit
/p/<id>or?projectId=must always beat a stale selection. On flat editions the first two never match, so the cookie is the only source — which is the point.SameSite=Lax, noSecureflag: self-hosted installs are routinely plain HTTP on a private network, and aSecurecookie would silently never be set there.Upstream alignment
Named and shaped to sit beside upstream's
DEFAULT_ORG_COOKIE, which lands in this same file in v1.45.0 — one definition each so writer and reader can't drift, per upstream's own comment on theirs. When the v1.45.0 adoption lands, the two live together and slice 4 (org switching) inherits the pattern.projects.list()takes no org parameter — the org comes from the scope the proxy already sets. Upstream widensapiGetwith anX-Organization-Idoverride in v1.45.0 for its account-route picker;client.tsis deliberately left untouched here, since it's one of that release's six conflict files and churning it would make the adoption harder.What's here
navigation.ts—DEFAULT_PROJECT_COOKIE+ read/write helpersproxy.ts— cookie →x-project-idapi/projects.ts—list(),create(name)api/keys.ts—queryKeys.projects.list()hooks/use-projects.ts—useProjectsList,useCreateProject(invalidates the list on success)Tests
None, and that's a deliberate gap.
apps/webhas no vitest harness on the 1.44.0 base — upstream shipsapps/web/vitest.config.tsin v1.45.0 (flagged as a "take" in the sync review). Adding one here would collide with that adoption. The cookie helpers are pure functions and are the obvious first thing to cover once the harness arrives.pnpm run checkis clean (lint + types + format).