{"record":{"id":"a6d05f0e17672c26","repo":"toeverything/AFFiNE","slug":"new-owner-is-not-active-member","errorCode":"new_owner_is_not_active_member","errorMessage":"Can not set a non-active member as owner.","messagePattern":"Can not set a non-active member as owner\\.","errorType":"exception","errorClass":"NewOwnerIsNotActiveMember","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/models/workspace-user.ts","lineNumber":93,"sourceCode":"    });\n    const fallbackRole = (await this.models.workspace.isTeamWorkspace(\n      workspaceId\n    ))\n      ? WorkspaceRole.Admin\n      : WorkspaceRole.Collaborator;\n\n    try {\n      await this.models.workspaceMember.setOwner(\n        workspaceId,\n        userId,\n        fallbackRole\n      );\n    } catch (error) {\n      if (\n        error instanceof Error &&\n        error.message === 'New workspace owner must be an active member.'\n      ) {\n        throw new NewOwnerIsNotActiveMember();\n      }\n      throw error;\n    }\n\n    if (oldOwner?.user && oldOwner.user.id !== userId) {\n      this.event.emit('workspace.owner.changed', {\n        workspaceId,\n        from: oldOwner.user.id,\n        to: userId,\n      });\n      this.logger.log(\n        `Transfer workspace owner of [${workspaceId}] from [${oldOwner.user.id}] to [${userId}]`\n      );\n    } else {\n      this.logger.log(`Set workspace owner of [${workspaceId}] to [${userId}]`);\n    }\n  }\n","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/models/workspace-user.ts#L75-L111","documentation":"Thrown by WorkspaceUserModel.setOwner (workspace-user.ts:93), which wraps WorkspaceMemberModel.setOwner. The inner method refuses to install a new owner when the workspace already has an owner AND the target user has no active workspace_members row (findFirst with state:'active' returns null). The wrapper catches that inner plain Error ('New workspace owner must be an active member.') and rethrows it as NewOwnerIsNotActiveMember. UserFriendlyError, code new_owner_is_not_active_member, type bad_request, HTTP 400.","triggerScenarios":"Calling transfer-ownership (setOwner) for a user who has not joined the workspace or whose invitation is still pending/revoked, when the workspace already has an active owner.","commonSituations":"Admin selects a pending invitee (not yet active) as the new owner; UI lets you pick any known user instead of only active members; a race between member removal and the ownership transfer.","solutions":["Invite the target user and have them accept first, so an active workspace_members row exists, then transfer.","If the user was already invited, transition them to Accepted (setState) to activate the membership, then call setOwner.","Choose a user who is already an active member as the new owner."],"exampleFix":"// before\nawait models.workspaceUser.setOwner(wsId, pendingUserId);\n// after\nawait models.workspaceInvitation.setState(wsId, pendingUserId, WorkspaceMemberStatus.Accepted);\nawait models.workspaceUser.setOwner(wsId, pendingUserId);","handlingStrategy":"validation","validationCode":"async function transferOwnerSafe(models, wsId: string, newOwnerId: string) {\n  const active = await models.db.workspaceMember.findFirst({\n    where: { workspaceId: wsId, userId: newOwnerId, state: 'active' },\n  });\n  if (!active) {\n    throw new Error('Target user is not an active member; invite and accept first.');\n  }\n  return models.workspaceUser.setOwner(wsId, newOwnerId);\n}","typeGuard":null,"tryCatchPattern":"import { NewOwnerIsNotActiveMember } from '../base/error/errors.gen';\n\ntry {\n  await models.workspaceUser.setOwner(wsId, newOwnerId);\n} catch (e) {\n  if (e instanceof NewOwnerIsNotActiveMember) {\n    // invite/accept the user first, then retry\n  } else throw e;\n}","preventionTips":["Ensure the target user is an active member before transferring ownership.","Restrict the 'pick new owner' UI to currently active members only.","Resolve pending invitations to Accepted before calling setOwner."],"tags":["workspace","ownership","permissions","state-machine"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}