Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/team-workspace-correctness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@everything-dev/auth-plugin": patch
"api": patch
"ui": patch
---

Bind wallet invitations to their NEAR network, guard invitation status transitions, and align wallet membership limits with email invitations. Refresh workspace state after team changes and invitation acceptance, and defer membership loading until the Teams tab is opened.

Existing wallet invitations without a network must be reissued; email invitations are unaffected.
8 changes: 8 additions & 0 deletions .changeset/teams-workspaces-wallet-invitations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@everything-dev/auth-plugin": minor
"api": patch
"ui": minor
"everything-dev": minor
---

Complete organization teams and wallet invitations across the auth plugin, API, and dashboard. Team workspaces now carry feature-area context through node mutation authorization, and organization owners can invite either an email address or a NEAR account, target a team, and manage wallet-aware pending invitations. Invitees can accept email or wallet invitations from the dashboard or claim link and land in the targeted workspace.
61 changes: 61 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# City Node

A geographic node in the City Node network, owned by a node DAO and optionally backed by a staking pool.

## Language

**Node DAO Account**:
The node DAO's NEAR account — the account that stakes into the node's pool.
_Avoid_: team account, team wallet, org account, user wallet

**Staking Pool**:
The validator pool contract the node resolves for staking.
_Avoid_: validator (the metadata record), total staked (the whole pool)

**Node DAO Stake**:
The Node DAO Account's current stake in the node's Staking Pool, including compounded validator rewards.
_Avoid_: available rewards (product label for this same quantity), total staked, pool stake

**Validator Rewards**:
NEAR already compounded into Node DAO Stake. Not a separately held balance.
_Avoid_: reward balance, pending rewards

## Organization access

**Team**:
A named sub-group within an organization that shares access to the organization's feature areas.
_Avoid_: team account, team wallet, node DAO

**Active Team**:
The Team currently selected for a user's organization work.
_Avoid_: current team, team account

**Feature Area**:
A product capability that an organization can grant to a Team.
_Avoid_: permission, role

**Team Workspace**:
The organization view scoped to an Active Team and its granted feature areas.
_Avoid_: node workspace, organization account

## Community discovery

**Discovery Profile**:
A node's public community identity, including its chosen geographic location and official social channels.
_Avoid_: validator profile, node DAO account

**Community Activity**:
A published event or social update associated with a node and visible to visitors.
_Avoid_: staking activity, validator uptime

**Active Node**:
A publicly discoverable node with recent Community Activity or an upcoming published event.
_Avoid_: online node, active validator

**Node Event**:
A community gathering associated with one or more nodes, with a scheduled time and a public destination for event details or registration.
_Avoid_: blockchain event, transaction

**Social Update**:
A node-associated public post with an attributed source, original publication time, and a link to the original content.
_Avoid_: social account, imported activity
81 changes: 81 additions & 0 deletions advisor-plans/025-wallet-invitation-network.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Plan 025: Bind wallet invitations to network identity

> Follow each step and its verification gate. Do not push, merge, deploy, or modify live data. Update only this plan's status in advisor-plans/README.md when verified.

## Status
- Priority: P1
- Effort: M (about one day, including regression coverage)
- Risk: MED — invitation schema and legacy records
- Depends on: none
- Category: correctness / architecture
- Planned at: `f0b65165`, 2026-09-21
- Scope: PR #136, teams and organizations only; findings introduced by this branch.

## Drift check and conventions
Run `git diff f0b65165..HEAD -- plugins/auth/src/near-invitations.ts plugins/auth/src/auth-instance.ts plugins/auth/src/contract.ts plugins/auth/src/handlers/invitations.ts plugins/auth/src/db/schema.ts plugins/auth/src/db/migrations plugins/auth/tests/integration/near-invitations.test.ts ui/src/routes/_layout/_authenticated/_dashboard/orgs/-invite-member-form.tsx ui/src/routes/_layout/_authenticated/_dashboard/orgs/-invite-member-form.test.tsx ui/src/routes/_layout/_authenticated/_dashboard/orgs/-organization-invitations.ts ui/src/routes/_layout/_authenticated/_dashboard/orgs/-organization-invitations.test.tsx ui/src/routes/_layout/_authenticated/_dashboard/orgs/-invitation-card.tsx ui/src/routes/_layout/_authenticated/_dashboard/orgs/invites.$id.tsx` and inspect `git status --short`. Compare the excerpts below to live code. Expected predecessor-plan changes are allowed; unexplained semantic drift requires stopping and reporting.
This is a Bun workspace with TypeScript, React/TanStack Query/Router, Better Auth, Drizzle PostgreSQL and Effect/oRPC. Read AGENTS.md and CONTRIBUTING.md. Before source edits load applicable intent guidance: `bunx @tanstack/intent@latest load every-plugin#plugin-development` and `every-plugin#plugin-testing` for auth/API changes; `everything-dev#ui-integration` for UI changes (use the same command prefix). Match existing kebab-case files, semantic Tailwind, typed contracts, and no implementation comments.
CONTEXT.md defines Team as “A named sub-group within an organization that shares access to the organization's feature areas.” Active Team is session state, not a new user preference. No selected team means unrestricted areas; organization owners/admins and platform admins bypass area filtering. Preserve these decisions.

## Scope
Only these paths may change:
- `plugins/auth/src/near-invitations.ts`
- `plugins/auth/src/auth-instance.ts`
- `plugins/auth/src/contract.ts`
- `plugins/auth/src/handlers/invitations.ts`
- `plugins/auth/src/db/schema.ts`
- `plugins/auth/src/db/migrations`
- `plugins/auth/tests/integration/near-invitations.test.ts`
- `ui/src/routes/_layout/_authenticated/_dashboard/orgs/-invite-member-form.tsx`
- `ui/src/routes/_layout/_authenticated/_dashboard/orgs/-invite-member-form.test.tsx`
- `ui/src/routes/_layout/_authenticated/_dashboard/orgs/-organization-invitations.ts`
- `ui/src/routes/_layout/_authenticated/_dashboard/orgs/-organization-invitations.test.tsx`
- `ui/src/routes/_layout/_authenticated/_dashboard/orgs/-invitation-card.tsx`
- `ui/src/routes/_layout/_authenticated/_dashboard/orgs/invites.$id.tsx`
- A new scoped `.changeset/*.md` for user-visible behavior.
- This plan and its status row in `advisor-plans/README.md`.

Do not modify unrelated organizations API-key behavior, node/resource ownership, framework-wide auth, dependencies, live databases, environment secrets, or unrelated advisor plans. Work in an isolated `fix/teams-025` branch/worktree if dispatched for execution. Preserve existing changes. Use semantic commits only when requested, such as `fix(auth): bind wallet invitations to their network`.

## Commands
Run from repository root. Dependencies already exist; do not reinstall or upgrade them.
- Focused verification: `bun run --cwd plugins/auth test tests/integration/near-invitations.test.ts` — all tests pass.
- Typecheck: `bun typecheck` — exit 0.
- Lint: `bun lint` — exit 0 (warnings may be baseline).
- Whitespace: `git diff --check` — exit 0.
- Scope: `git diff --name-only` and `git status --short` — only allowed paths.

Use Vitest via `bun run`, never Bun's built-in `bun test`. Auth integration helpers use a disposable in-memory database. Do not replace their database with a development database.

## Why this matters
Invitation discovery and acceptance currently discard the network of a linked NEAR account. Authentication supports mainnet and testnet, whose named accounts are distinct identities. A matching string on the wrong network must never grant organization membership.

## Current state
`plugins/auth/src/near-invitations.ts:33`:
```ts
.select({ accountId: schema.nearAccount.accountId })
.from(schema.nearAccount)
.where(eq(schema.nearAccount.userId, userId));
```
Acceptance then calls `linked.includes(invitation.nearAccountId)`. The existing `nearAccount` table has both `accountId` and `network`, while invitations have only `nearAccountId`. `contract.ts` defines invite input and invitation output. The UI `InviteMemberValues` contains email, nearAccountId, role and teamId. Follow the existing integration test's `walletInvitation` fixture and direct linked-account fixture; these test identity matching, not SIWN cryptography.

## Design decision
Represent a wallet recipient as account ID plus network, carrying `nearNetwork` through schema, contracts, listing, resend and acceptance. New wallet invitations require a network; UI defaults to mainnet and explicitly shows the selected network, with testnet selectable. Email invitations keep network null. **Do not silently assign a network to legacy wallet invitations:** leave their network null and make them unclaimable with a specific reissue-required error. Admins may cancel/reissue them; do not delete or cancel records automatically. Existing email invitations remain valid. This avoids guessing the intended identity of old invitations.

## Steps and test plan
1. Extend the existing auth tests with two users linked to the same synthetic account name on different networks. Cover listing, claim lookup, acceptance and rejection. Add a legacy null-network invitation fixture. Run the focused command: new wrong-network assertions must fail on the old implementation; existing cases stay green.
2. Add nullable invitation `nearNetwork`, an additive migration and its Drizzle metadata. Register it with Better Auth additional fields. Use a mainnet/testnet enum in input/output validation, require network when creating wallet invitations, reject a supplied network for email invites. Match both fields in all ownership checks, and never emit legacy ambiguous records as claimable. Run the focused command: all auth cases pass, including old-email and legacy-wallet cases. Recreate the test database through existing helpers to verify migrations; never apply to live data.
3. Carry network through UI creation, resend, cards and claim details. Add semantic labels/test IDs and display the network alongside the account. Follow existing form tests. Verify `bun run --cwd ui test src/routes/_layout/_authenticated/_dashboard/orgs/-invite-member-form.test.tsx src/routes/_layout/_authenticated/_dashboard/orgs/-organization-invitations.test.tsx` exits 0.
4. Add cases for correct-network acceptance setting org/team/session, no cross-network discovery, wrong-network 403, invalid/missing new-wallet network, and clear legacy rejection. Run focused tests and all common gates.

## Maintenance
Future supported networks must extend the recipient type and all adapters together. Never derive identity network from an account suffix. Inspect both direct Better Auth creation and the oRPC entry point so one cannot create a newly ambiguous wallet invitation.


## Completion and stop rules
- [ ] Focused tests include the specified regressions and pass.
- [ ] `bun typecheck`, `bun lint`, and `git diff --check` exit 0.
- [ ] No unrelated changes, secrets, dependency changes or live data mutations.
- [ ] Update the index row to DONE only after verification; otherwise record the precise blocker.

Stop if the implementation requires out-of-scope files (apart from imports in explicitly named callers), intended identity/network semantics cannot be established, or a verification gate still fails after two reasonable repair attempts. Report pre-existing test failures separately; never label an incomplete gate as passed.

71 changes: 71 additions & 0 deletions advisor-plans/026-wallet-invitation-lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Plan 026: Make wallet invitation transitions atomic and preserve membership policy

> Follow each step and its verification gate. Do not push, merge, deploy, or modify live data. Update only this plan's status in advisor-plans/README.md when verified.

## Status
- Priority: P2
- Effort: M (about one day, including regression coverage)
- Risk: MED — transaction and session lifecycle
- Depends on: 025-wallet-invitation-network.md
- Category: correctness / architecture
- Planned at: `f0b65165`, 2026-09-21
- Scope: PR #136, teams and organizations only; findings introduced by this branch.

## Drift check and conventions
Run `git diff f0b65165..HEAD -- plugins/auth/src/near-invitations.ts plugins/auth/src/auth-instance.ts plugins/auth/src/organization-membership-policy.ts plugins/auth/tests/integration/near-invitations.test.ts plugins/auth/tests/integration/team-invitations.test.ts` and inspect `git status --short`. Compare the excerpts below to live code. Expected predecessor-plan changes are allowed; unexplained semantic drift requires stopping and reporting.
This is a Bun workspace with TypeScript, React/TanStack Query/Router, Better Auth, Drizzle PostgreSQL and Effect/oRPC. Read AGENTS.md and CONTRIBUTING.md. Before source edits load applicable intent guidance: `bunx @tanstack/intent@latest load every-plugin#plugin-development` and `every-plugin#plugin-testing` for auth/API changes; `everything-dev#ui-integration` for UI changes (use the same command prefix). Match existing kebab-case files, semantic Tailwind, typed contracts, and no implementation comments.
CONTEXT.md defines Team as “A named sub-group within an organization that shares access to the organization's feature areas.” Active Team is session state, not a new user preference. No selected team means unrestricted areas; organization owners/admins and platform admins bypass area filtering. Preserve these decisions.

## Scope
Only these paths may change:
- `plugins/auth/src/near-invitations.ts`
- `plugins/auth/src/auth-instance.ts`
- `plugins/auth/src/organization-membership-policy.ts`
- `plugins/auth/tests/integration/near-invitations.test.ts`
- `plugins/auth/tests/integration/team-invitations.test.ts`
- A new scoped `.changeset/*.md` for user-visible behavior.
- This plan and its status row in `advisor-plans/README.md`.

Do not modify unrelated organizations API-key behavior, node/resource ownership, framework-wide auth, dependencies, live databases, environment secrets, or unrelated advisor plans. Work in an isolated `fix/teams-026` branch/worktree if dispatched for execution. Preserve existing changes. Use semantic commits only when requested, such as `fix(auth): bind wallet invitations to their network`.

## Commands
Run from repository root. Dependencies already exist; do not reinstall or upgrade them.
- Focused verification: `bun run --cwd plugins/auth test tests/integration/near-invitations.test.ts tests/integration/team-invitations.test.ts` — all tests pass.
- Typecheck: `bun typecheck` — exit 0.
- Lint: `bun lint` — exit 0 (warnings may be baseline).
- Whitespace: `git diff --check` — exit 0.
- Scope: `git diff --name-only` and `git status --short` — only allowed paths.

Use Vitest via `bun run`, never Bun's built-in `bun test`. Auth integration helpers use a disposable in-memory database. Do not replace their database with a development database.

## Why this matters
Rejection can overwrite an accepted invitation because its final write does not check pending status. Wallet acceptance also writes memberships directly, skipping the ordinary Better Auth acceptance limit (100 by default). Recipient identity may vary by adapter; terminal transitions and membership policy must remain consistent.

## Current state
`near-invitations.ts:171`:
```ts
await db.update(schema.invitation)
.set({ status: "rejected" })
.where(eq(schema.invitation.id, invitation.id));
```
Acceptance at lines 97–149 uses a transaction and a conditional pending-to-accepted update, followed by team/member insertion. Session activation happens afterward at line 152. `auth-instance.ts:329` registers `nearInvitations(db)`. Better Auth's installed `organization/routes/crud-invites.mjs` checks `membershipLimit || 100` before ordinary acceptance. Do not modify node_modules. Follow the existing accepted transition's conditional `.returning()` idiom and APIError handling.

## Steps and test plan
1. Add a deterministic interleaving test: pause rejection after reading a pending invitation, commit acceptance, then release rejection. Assert rejection errors, final status is accepted, and exactly one membership remains. Also cover cancel winning before rejection and repeated rejection. Use a narrowly scoped spy/barrier restored after each test, not time sleeps. Verify the focused command exposes the original race.
2. Change rejection to a conditional pending transition with `.returning()`; treat zero rows as no longer pending. Keep ownership validation, and ensure acceptance continues to use guarded consumption. Verify all lifecycle tests pass. Review expiration timing; validate expiry again at the guarded transition where practical.
3. Introduce a small application-owned membership policy module exporting the configured limit and validation used by the wallet path. Pass the same explicit limit to Better Auth's organization plugin and the wallet plugin. Retain upstream email acceptance; do not replace or fork Better Auth's lifecycle. Within the wallet transaction, serialize competing wallet admissions for the same organization using a database-supported row lock before counting and inserting members. Verify two wallet invitations cannot exceed the cap. Do not claim global email/wallet concurrency serialization unless the upstream path joins the same lock.
4. Add parity cases using a small test-configured limit: both email and wallet acceptance reject a full organization; below-limit acceptance succeeds; missing team rolls back invitation/member writes; replay does not duplicate membership. Make test configuration derive from the same policy interface, not hardcoded alternate production logic. Verify the focused command passes.
5. Run all common gates. Document the existing separate session-update failure mode and verify retry reports an already-consumed invitation without duplicating membership. Do not introduce a distributed transaction or rewrite session storage for this scoped change.

## Maintenance and additional stop condition
Only share policy that the application owns. If enforcing the same configured limit requires unsupported Better Auth internals, stop and report the exact integration gap rather than patching dependencies. Verify the installed limit behavior against the lockfile on upgrades. Callback/hook parity beyond currently configured hooks is not a reason to build a general invitation framework.


## Completion and stop rules
- [ ] Focused tests include the specified regressions and pass.
- [ ] `bun typecheck`, `bun lint`, and `git diff --check` exit 0.
- [ ] No unrelated changes, secrets, dependency changes or live data mutations.
- [ ] Update the index row to DONE only after verification; otherwise record the precise blocker.

Stop if the implementation requires out-of-scope files (apart from imports in explicitly named callers), intended identity/network semantics cannot be established, or a verification gate still fails after two reasonable repair attempts. Report pre-existing test failures separately; never label an incomplete gate as passed.

Loading
Loading