Skip to content

feat: teams and workspaces with wallet invitations - #136

Merged
elliotBraem merged 14 commits into
mainfrom
feat/teams-and-workspaces
Sep 22, 2026
Merged

elliotBraem merged 14 commits into
mainfrom
feat/teams-and-workspaces

Conversation

@itexpert120

Copy link
Copy Markdown
Collaborator

Summary

  • Add team context to the auth plugin: active team on the session, feature-area metadata on teams, and setActiveTeam / listUserTeams endpoints (migration 0003_teams_workspaces).
  • Gate node, validator and tenant-node mutations in the API by the active team's node-operations area via a new requireTeamArea middleware. Org owners, org admins and platform admins bypass it, and members with no active team keep their existing access.
  • Let organization owners invite an email address or a NEAR account, optionally targeting a team. Wallet invitations are accepted or rejected via acceptNearInvitation / rejectNearInvitation and are discovered independently of email verification.
  • Email invitation links now point to /orgs/invites/:id. Better Auth's own accept is blocked for wallet invitations.
  • Add dashboard UI: Teams tab with area grants, a unified invite form, wallet-aware invitation cards, and a sidebar team switcher. Navigation hides ungranted areas, and direct navigation to them redirects to /dashboard?restricted=<area>.
  • Enable Better Auth teams in the UI auth client and add CONTEXT.md glossary entries, a walkthrough doc and a changeset.

Testing

  • API unit tests (team-auth) and integration tests (team-gating): 13 files, 116 tests passed.
  • UI suite: 63 files, 354 tests passed.
  • Auth wallet invitation integration suite (10 tests) and utils suite (31 tests) passed.
  • Playwright team-workspace.spec.ts passed against the local dev stack. It covers team creation and area grants, email and wallet invites, accepting each, restricted navigation, 403 on the node mutation probe for a Finance team, and owner/admin bypass.
  • bun typecheck: all 8 targets passed. bun lint: passed with warnings.
  • Not verified: external wallet SIWN signature flow. The NEAR sandbox setup timed out in this environment, so its 4 SIWN tests did not run.
  • Not green: the host remote-runtime browser suite fails its navigation check because the deployed remote UI has no Skill link. It targets the deployed UI, not this change.
  • Not green: a broad auth-suite run timed out in the session-revocation test. It passed when rerun with --maxWorkers=1.
  • Not tested: real mailbox delivery. Email was preview-only locally.

itexpert120 and others added 6 commits September 21, 2026 18:46
…itation near account (#82)

- migration: session.active_team_id, team.metadata, invitation.near_account_id
- Better Auth additionalFields for team metadata and invitation nearAccountId
- disable default team creation and allow removing the last team
- getContext reports organization.teams (id, name, areas) and a
  membership-validated organization.activeTeamId
- setActiveTeam and listUserTeams plugin routes; team routes expose areas
- toORPCError maps Better Auth APIError statusCode instead of the string
  status, so auth errors no longer surface as 500s

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- FEATURE_AREAS vocabulary (node-operations, finance, things, stake)
  exported from api/feature-areas for API and UI
- requireTeam and requireTeamArea middlewares with bypass for platform
  admins and organization owners/admins; no active team keeps pre-teams
  access
- node and validator mutation routes require node-operations

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- create, rename and delete teams; grant feature areas via checkboxes
  from the shared vocabulary; add/remove organization members
- management controls for organization owners/admins, read-only view
  for members
- auth plugin listTeamMembers allows any member of the team's
  organization (Better Auth restricts it to team members)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- sidebar team switcher lists the user's teams in the active
  organization with an all-areas option; switching refreshes the session
  and workspace queries
- navigation items carry an optional feature area; the sidebar shows only
  the active team's areas (owners/admins, platform admins and users
  without an active team see everything)
- dashboard route guard redirects to the workspace overview with a
  restricted-area notice; header shows the team being operated as
- switching organizations clears the active team
- Better Auth organization client enables teams
- browser regression spec for switch, nav filtering and guard redirect

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- inviteMember passes an optional teamId through; invitation outputs
  include teamId
- accepting a team invitation joins the team and sets it active (Better
  Auth built-in); acceptance page refreshes the team workspace
- invite form gains an optional team picker; pending invitations show the
  targeted team
- invitation emails link to /orgs/invites/:id instead of the missing
  /accept-invitation/:id route

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Invite members by NEAR account id as well as email, optionally targeting a team
- Add near-invitations Better Auth plugin with accept/reject wallet endpoints
- List pending wallet invitations independently of email verification
- Fix createHeaders dropping cookies when given a native Headers instance
- Update invite form, invitation card, and claim-link page in the dashboard
- Add integration, unit, and browser regression tests plus walkthrough docs
Comment thread api/src/lib/team-auth.ts Outdated
Comment thread ui/src/components/layout/app-sidebar.tsx Outdated
Comment thread ui/src/lib/team-workspace.ts Outdated
Comment thread ui/package.json Outdated
itexpert120 and others added 3 commits September 21, 2026 23:56
The shell no longer threads the active team through layout props, and the UI keeps feature areas locally so it does not depend on the api package.

Co-authored-by: Cursor <cursoragent@cursor.com>
Wallet invites record their network and follow the same membership limit as email invites. The workspace refreshes after team changes and invitation acceptance, and team members load when the Teams tab opens.

Co-authored-by: Cursor <cursoragent@cursor.com>

@elliotBraem elliotBraem left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also browser regession failures



  1) [dev] › tests/regression/browser/specs/team-workspace.spec.ts:84:3 › team workspace › team membership, rename and deletion refresh the mounted workspace 

    Error: expect(locator).toHaveCount(expected) failed

    Locator:  getByTestId('team-switcher')
    Expected: 0
    Received: 1
    Timeout:  5000ms

    Call log:
      - Expect "toHaveCount" getByTestId('team-switcher') with timeout 5000ms
      - waiting for getByTestId('team-switcher')
        14 × locator resolved to 1 element
           - unexpected value "1"


      137 |     await page.getByTestId(`teams-tab-delete-${team.id}`).click();
      138 |     await expect(page.getByTestId("workspace-active-team")).toHaveCount(0);
    > 139 |     await expect(page.getByTestId("team-switcher")).toHaveCount(0);
          |                                                     ^
      140 |     await expect(page.getByTestId(`teams-tab-team-${team.id}`)).toHaveCount(0);
      141 |     expectNoHydrationFailure(pageErrors);
      142 |   });
        at /home/runner/work/citynode.app/citynode.app/tests/regression/browser/specs/team-workspace.spec.ts:139:53

    Error Context: test-results/team-workspace-team-worksp-e2feb-fresh-the-mounted-workspace-dev/error-context.md

    Retry #1 ───────────────────────────────────────────────────────────────────────────────────────

    Error: expect(locator).toHaveCount(expected) failed

    Locator:  getByTestId('team-switcher')
    Expected: 0
    Received: 1
    Timeout:  5000ms

    Call log:
      - Expect "toHaveCount" getByTestId('team-switcher') with timeout 5000ms
      - waiting for getByTestId('team-switcher')
        14 × locator resolved to 1 element
           - unexpected value "1"


      137 |     await page.getByTestId(`teams-tab-delete-${team.id}`).click();
      138 |     await expect(page.getByTestId("workspace-active-team")).toHaveCount(0);
    > 139 |     await expect(page.getByTestId("team-switcher")).toHaveCount(0);
          |                                                     ^
      140 |     await expect(page.getByTestId(`teams-tab-team-${team.id}`)).toHaveCount(0);
      141 |     expectNoHydrationFailure(pageErrors);
      142 |   });
        at /home/runner/work/citynode.app/citynode.app/tests/regression/browser/specs/team-workspace.spec.ts:139:53

    Error Context: test-results/team-workspace-team-worksp-e2feb-fresh-the-mounted-workspace-dev-retry1/error-context.md

  1 failed
    [dev] › tests/reg

Comment thread api/src/lib/team-auth.ts Outdated
@@ -0,0 +1,67 @@
import type { DecoratedMiddleware } from "@orpc/server";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think this can be removed in some way? Or part of it absorbed. See packages/everything-dev contract.ts

and auth.ts

@elliotBraem

Copy link
Copy Markdown

There are trace logs still in code too, should be cleaned up once all passes

itexpert120 and others added 5 commits September 22, 2026 12:34
- Replace api/src/lib/team-auth.ts with createRequireTeamArea in team-access-policy.ts, dropping the unused requireTeam middleware
- Update api/src/index.ts to build the node-operations gate from the new export
- Clear the caller's active team server-side before deleteTeam when it matches the deleted team, instead of racing a client-side setActiveTeam call
- Simplify useOrganizationTeams' deleteTeam mutation to call the API directly and drop the now-unneeded session/active-team dance
- Update unit, integration, UI, and regression tests to match
Merge 0003_teams_workspaces and 0004_peaceful_meggan (the two
unreleased migrations this branch introduced) into a single
0003_teams_workspaces migration via drizzle-kit generate, so the
feature ships as one migration instead of two small follow-ups.
Also syncs bun.lock now that ui no longer depends on the api
workspace.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Better Auth's removeTeam endpoint blocks deleting a team that is still
the caller's active team, resolving that check from the request's
better-auth.session_data cookie — a short-lived cache of the session as
of its last refresh, independent of any database writes made earlier in
the same request. The previous fix (auth.api.setActiveTeam, then
removeTeam) cleared the session row but never touched that cookie, so
removeTeam kept seeing the pre-clear activeTeamId and rejected the
deletion with FORBIDDEN — reproduced against the real dev stack, not
just the pglite test harness, which never hit this cookie-cache path.

Clear the active team directly in the database, then strip the stale
session_data cookie from the headers passed to removeTeam so its own
session lookup falls back to a fresh database read via the session
token instead of the cached cookie payload.

Verified by running tests/regression/browser/specs/team-workspace.spec.ts
against a live dev stack (previously failing 2/2, now passing).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@elliotBraem
elliotBraem merged commit 86907b9 into main Sep 22, 2026
7 checks passed
@elliotBraem
elliotBraem deleted the feat/teams-and-workspaces branch September 22, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants