Repository navigation
feat: share sessions with specific users (AGE-2577) - #1122
Conversation
🦋 Changeset detectedLatest commit: d9267e3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 190b368. Configure here.
|
How does user sharing work in opensource In open source mode - either allow unvalidated users for sharing or restrict to only creator or tenant. |
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- Use "access" naming only: SessionAccessGrant -> SessionAccess, nesting the shared subject as `subject` so request and response use the same shape - Drop display_name/email from access entries; names come from GET /users - Rename UserDirectory -> IUserStore / userStore (TrueFoundryUserStore, EmptyUserStore) - Use the existing callerAccessToken instead of moving requireUserCredential - Avoid "SFY" in new code (PeekUser/PeekUsersResponse); require user email - Cap subjects per request at 50 - Describe the caller-visible contract only: no creator-only wording, 403 "User does not have permission.", simpler subject/user id descriptions - Revoke: keep per-subject DELETE; 404 says the subject has no access Co-authored-by: Cursor <cursoragent@cursor.com>
…-2577)
- Paths: /api/v1/sessions/{session_id}/grants for list, create and revoke
- Revoke takes subject_type and subject_id as required query params
(DELETE /grants?subject_type=user&subject_id=...), so new identifying
fields can be added later without changing the route
- Rename wire schemas and Fern methods: SessionGrant, SessionGrantSubject,
ListSessionGrantsResponse, CreateSessionGrantsRequest,
DeleteSessionGrantsRequestQuery, DeleteSessionGrantResponse;
list_grants / create_grants / delete_grant
Co-authored-by: Cursor <cursoragent@cursor.com>
- Replace POST /grants (create) and DELETE /grants with one
POST /api/v1/sessions/{session_id}/grants taking { grant, revoke }
(up to 50 each, at least one subject). Both lists apply atomically in
one transaction; granting an existing subject or revoking one without
a grant is a no-op. Fern: sessions.update_grants, returns 200 with
every grant on the session.
- 422 when a subject to grant is the creator or unknown, or when a
subject is in both lists; nothing is written.
- Store revoke takes a batch of subjects in one DELETE.
- GET /api/v1/users accepts ids=u1,u2,... (up to 100) so the UI can show
names and emails for a session's grants; cannot be combined with
query or page_token.
- peekUsers: id lookups no longer send limit/offset; searches still do.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…PI tests (AGE-2577) - IAccessControlEntryStore -> IAccessControlStore (accessControlStore dep), PostgresAccessControlStore / SqliteAccessControlStore in access-control-store/, contract suite renamed to match - Drop low-value GET /users tests (default page size, empty store, near-duplicate ids rejections) Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
25826cd to
6c0cf64
Compare

Summary
First of two stacked PRs for AGE-2577: lets a session creator share a session with specific users.
This PR only adds things. Grants are stored but don't give read access yet; the follow-up PR (stacked on this branch) makes
canReadSessionhonour them. No existing behaviour changes here:PATCH { shared }, thesession.sharedcolumn,ISessionStore, session listing, subscribe, downloads and schedules are untouched.Changes
access_controltable (Postgres and SQLite migrations20261027_000001_access_control). It has one row per (tenant, session, subject_type, subject_id) and storesgranted_by_subjectandcreated_at. It referencessession(session_id)withON DELETE CASCADE. Onlysubject_type = 'user'is written for now.IAccessControlStorewith Postgres and SQLite implementations (list, grant, revoke), plus a shared contract suite.INSERT ... ON CONFLICT DO NOTHING, so re-granting is a no-op.DELETEfor a batch of subjects; subjects without a grant are ignored./api/v1/sessions/{session_id}/grants: list, plus one update endpoint that grants and revokes together. Only the session creator may call it. The update runs both writes in one transaction, so the sessions router takeswithTransactionagain. See New APIs below.IUserStorewith generic{ id, display_name, email }users:TrueFoundryUserStore, which calls ServiceFoundryGET v1/subjects/peekwith the caller's token. Searches always sendlimit. Id lookups send no paging and are chunked 50 ids per call, to keep URLs short.EmptyUserStore, so sharing with users is rejected there.GET /api/v1/users(newUserstag): search with token pagination, or look up specific users withidsso the UI can show names and emails for a session's grants.OpenAPI and SDK are not hand-edited; CI regenerates them.
New APIs
All endpoints need an authenticated caller. The
/grantsendpoints are restricted to the session creator (others get 403). Errors use{ "error": { "message": "..." } }./api/v1/sessions/{session_id}/grantssessions.list_grantsListSessionGrantsResponse/api/v1/sessions/{session_id}/grantssessions.update_grantsListSessionGrantsResponse/api/v1/users?query=&limit=&page_token=or?ids=users.listListUsersResponseUpdate request (grant and revoke in one call)
{ "grant": [{ "subject_type": "user", "subject_id": "u1" }], "revoke": [{ "subject_type": "user", "subject_id": "u2" }] }usersubjects in this PR.List / update response
{ "data": [{ "subject": { "subject_type": "user", "subject_id": "u1" }, "granted_by_subject": { "subject_id": "owner", "subject_type": "user", "subject_display_name": "Owner" }, "created_at": "2026-10-22T08:30:00.000Z" }] }Users response
{ "data": [{ "id": "u1", "display_name": "Alice", "email": "alice@example.com" }], "pagination": { "limit": 25, "next_page_token": "…" } }querymatches email or display name,limitdefaults to 25 (max 100), andpage_tokenpages through results.ids=u1,u2,…(up to 100) returns just those users, for showing names and emails of a session's grants. Unknown or inactive ids are omitted. It can't be combined withqueryorpage_token.Not in this PR: read access from these grants, and the
tenant(everyone) subject. Grants carry only ids; resolve names and emails withGET /users?ids=.How was this tested?
test:store:sqlite,test:store:postgres), including batch revoke.GET /users: search paging, plusidslookup (omits unknown ids, limit of 100, can't combine withquery/page_token);TrueFoundryUserStore;peekUsers: search sendslimit/offset, id lookup doesn't, 424 on a malformed body.GET /sessions/{id}. The stacked PR flips it.tsc --noEmit(src and both test configs), ESLint on changed files, full unit suite (734 tests).Checklist
pnpm build,pnpm test,pnpm typecheck,pnpm lint:ci, andpnpm format:checkpass locallypackages/trueforge-sdk,python/trueforge_sdk,.github/fern/openapi/openapi.json,docs/openapi.json) — fork PRs omit SDK regen; maintainers regenerate after merge.env.exampleupdated if configuration or behavior changedNote
Medium Risk
Adds authorization-adjacent APIs and grant storage with creator-only enforcement, but grants do not yet affect session read access; user validation depends on ServiceFoundry in TrueFoundry mode.
Overview
Adds per-user session sharing as stored grants plus HTTP APIs to manage them, without changing who can read a session today.
New
access_controlpersistence (Postgres/SQLite) records explicitusergrants per session, with list/grant/revoke viaGET/POST /api/v1/sessions/{session_id}/grants. Only the session creator may list or update grants; updates apply grant and revoke lists atomically (up to 50 each), validate users againstIUserStore, and return the full grant list.GET /api/v1/userssupports search oridslookup so clients can resolve share targets. In TrueFoundry mode this calls ServiceFoundryv1/subjects/peek; standalone/OIDC use an empty store so grants to users are rejected. OpenAPI and SDK clients gainsessions.listGrants/updateGrantsandusers.list.Grants are persisted only—
canReadSessionand existingsharedbehavior are unchanged; a test documents that grantees still get 403 on session reads until a follow-up PR.Reviewed by Cursor Bugbot for commit d9267e3. Bugbot is set up for automated code reviews on this repo. Configure here.