Skip to main content

Work Members & Ownership

The member and ownership services implement role-based access control (RBAC) for works. Together, WorkMemberService and WorkOwnershipService manage who can access a work and what they can do.

Sources:

  • packages/agent/src/services/work-member.service.ts
  • packages/agent/src/services/work-ownership.service.ts

Role Hierarchy

Ever Works uses a four-level role hierarchy:

RoleLevelPermissions
OWNER4Full control: delete work, manage members, edit, view
MANAGER3Manage members (invite, update roles, remove), edit, view
EDITOR2Edit content (generate items, update settings, manage repos), view
VIEWER1Read-only access to all work data

The work creator always has OWNER level access, even without an explicit membership record. This is enforced at the service level, not via database records.

WorkOwnershipService

This service provides the authorization layer used by all other work services.

Access Check Methods

MethodMinimum RoleUsed By
ensureAccess(workId, userId, minimumRole?)ConfigurableBase method
ensureCanView(workId, userId)VIEWERQuery operations
ensureCanEdit(workId, userId)EDITORGeneration, updates
ensureCanManageMembers(workId, userId)MANAGERMember management
ensureIsOwner(workId, userId)OWNERWork deletion

WorkAccessResult

Every access check returns a detailed result:

interface WorkAccessResult {
work: Work; // The work entity
member: WorkMember | null; // Membership record (null for creators)
role: WorkMemberRole; // Resolved role
isCreator: boolean; // Whether user created the work
}

How Access Is Determined

  1. Find work -- Queries workRepository.findById(). Throws NotFoundException if not found.
  2. Check creator -- If work.userId === userId, the user is the creator and gets OWNER role.
  3. Check membership -- For non-creators, queries workMemberRepository.findMember().
  4. Verify role level -- If a minimumRole is specified, checks that the user's role meets or exceeds it.
private roleIsAtLeast(role: WorkMemberRole, minimumRole: WorkMemberRole): boolean {
const roleHierarchy: Record<WorkMemberRole, number> = {
[WorkMemberRole.OWNER]: 4,
[WorkMemberRole.MANAGER]: 3,
[WorkMemberRole.EDITOR]: 2,
[WorkMemberRole.VIEWER]: 1,
};
return roleHierarchy[role] >= roleHierarchy[minimumRole];
}

Non-Throwing Helpers

// Returns true/false without throwing
const canAccess = await ownershipService.hasAccess(workId, userId);

// Returns the role or null
const role = await ownershipService.getUserRole(workId, userId);

WorkMemberService

This service manages explicit membership records for works.

Listing Members

const members = await memberService.listMembers(workId, userId);

Requires Viewer access. Returns an array of WorkMemberDto:

interface WorkMemberDto {
id: string;
userId: string;
username: string;
email: string;
avatar?: string;
role: WorkMemberRole;
invitedBy?: {
id: string;
username: string;
};
createdAt: string;
}

Inviting Members

const result = await memberService.inviteMember(workId, userId, {
role: WorkMemberRole.EDITOR
});

Requires Manager access. The invitation flow:

  1. Validate role -- Only VIEWER, EDITOR, and MANAGER can be assigned. OWNER is reserved for the creator.
  2. Find invitee -- Looks up the user by email. Throws NotFoundException if not found.
  3. Prevent self-invite -- Cannot add the work creator as a member.
  4. Prevent duplicates -- Checks for existing membership.
  5. Create membership -- Adds the member with the specified role and records the inviter.
  6. Return result -- Includes the member DTO, invitee user, inviter user, and work for downstream use (e.g., sending invitation emails).
interface InviteMemberResult {
member: WorkMemberDto;
invitee: User;
inviter: User;
work: Work;
}

Updating Member Roles

const updated = await memberService.updateMemberRole(workId, userId, memberId, {
role: WorkMemberRole.MANAGER
});

Requires Manager access. Validates the new role is assignable (viewer, editor, or manager).

Removing Members

await memberService.removeMember(workId, userId, memberId);

Requires Manager access. Verifies the member belongs to the specified work before removal.

Leaving a Work

await memberService.leaveWork(workId, userId);

Allows a member to remove themselves. The work creator cannot leave -- this prevents orphaned works.

Getting Member Details

const member = await memberService.getMember(workId, userId, memberId);

Requires Viewer access. Returns a single WorkMemberDto.

Getting Owner Info

const owner = await memberService.getWorkOwnerInfo(workId, userId);
// { id, username, email, avatar }

Returns the work creator's basic profile information.

Access Patterns Across Services

Here is how the role system integrates across the agent services:

ServiceOperationRequired Role
WorkLifecycleServicecreateWorkAuthenticated user
WorkLifecycleServiceupdateWorkEditor
WorkLifecycleServicesyncFromDataRepositoryEditor
WorkLifecycleServicedeleteWorkOwner
WorkGenerationServicegenerateItemsEditor
WorkGenerationServicesubmitItemEditor
WorkGenerationServiceremoveItemEditor
WorkQueryServicegetWorksAuthenticated
WorkQueryServicegetWorkViewer
WorkQueryServiceworkItemsViewer
WorkQueryServiceupdateWebsiteSettingsEditor
WorkScheduleServicegetScheduleViewer
WorkScheduleServiceupdateScheduleEditor
WorkScheduleServicecancelScheduleEditor
WorkMemberServicelistMembersViewer
WorkMemberServiceinviteMemberManager
WorkMemberServiceupdateMemberRoleManager
WorkMemberServiceremoveMemberManager

Error Responses

ConditionExceptionMessage
Work not foundNotFoundExceptionWork with id 'X' not found
No access (not creator, not member)ForbiddenExceptionYou do not have permission to access this work
Insufficient roleForbiddenExceptionYou do not have the required permission level for this action
User not found (invite)NotFoundExceptionUser with email 'X' not found
Duplicate memberBadRequestExceptionUser is already a member of this work
Invalid role assignmentBadRequestExceptionInvalid role. Members can only be assigned: viewer, editor, or manager
Creator trying to leaveBadRequestExceptionWork creator cannot leave the work