1
0

Switch to official launcher auth (#1118)

* Switch to official launcher auth

* add debug info

* Fix build
This commit is contained in:
Geometrically
2024-04-15 13:58:20 -07:00
committed by GitHub
parent 76447019c0
commit 2877919639
65 changed files with 1674 additions and 5349 deletions

View File

@@ -1,132 +0,0 @@
//! Authentication flow interface
use crate::{
hydra::{self, init::DeviceLoginSuccess},
launcher::auth as inner,
State,
};
use chrono::Utc;
use crate::state::AuthTask;
pub use inner::Credentials;
/// Authenticate a user with Hydra - part 1
/// This begins the authentication flow quasi-synchronously, returning a URL
/// This can be used in conjunction with 'authenticate_await_complete_flow'
/// to call authenticate and call the flow from the frontend.
/// Visit the URL in a browser, then call and await 'authenticate_await_complete_flow'.
pub async fn authenticate_begin_flow() -> crate::Result<DeviceLoginSuccess> {
let url = AuthTask::begin_auth().await?;
Ok(url)
}
/// Authenticate a user with Hydra - part 2
/// This completes the authentication flow quasi-synchronously, returning the credentials
/// This can be used in conjunction with 'authenticate_begin_flow'
/// to call authenticate and call the flow from the frontend.
pub async fn authenticate_await_complete_flow() -> crate::Result<Credentials> {
let credentials = AuthTask::await_auth_completion().await?;
Ok(credentials)
}
/// Cancels the active authentication flow
pub async fn cancel_flow() -> crate::Result<()> {
AuthTask::cancel().await
}
/// Refresh some credentials using Hydra, if needed
/// This is the primary desired way to get credentials, as it will also refresh them.
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn refresh(user: uuid::Uuid) -> crate::Result<Credentials> {
let state = State::get().await?;
let mut users = state.users.write().await;
let mut credentials = users.get(user).ok_or_else(|| {
crate::ErrorKind::OtherError(
"You are not logged in with a Minecraft account!".to_string(),
)
.as_error()
})?;
let offline = *state.offline.read().await;
if !offline {
let fetch_semaphore: &crate::util::fetch::FetchSemaphore =
&state.fetch_semaphore;
if Utc::now() > credentials.expires
&& inner::refresh_credentials(&mut credentials, fetch_semaphore)
.await
.is_err()
{
users.remove(credentials.id).await?;
return Err(crate::ErrorKind::OtherError(
"Please re-authenticate with your Minecraft account!"
.to_string(),
)
.as_error());
}
// Update player info from bearer token
let player_info =
hydra::stages::player_info::fetch_info(&credentials.access_token)
.await
.map_err(|_err| {
crate::ErrorKind::HydraError(
"No Minecraft account for your profile. Please try again or contact support in our Discord for help!".to_string(),
)
})?;
credentials.username = player_info.name;
users.insert(&credentials).await?;
}
Ok(credentials)
}
/// Remove a user account from the database
#[tracing::instrument]
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
let state = State::get().await?;
let mut users = state.users.write().await;
if state.settings.read().await.default_user == Some(user) {
let mut settings = state.settings.write().await;
settings.default_user = users.0.values().next().map(|it| it.id);
}
users.remove(user).await?;
Ok(())
}
/// Check if a user exists in Theseus
#[tracing::instrument]
pub async fn has_user(user: uuid::Uuid) -> crate::Result<bool> {
let state = State::get().await?;
let users = state.users.read().await;
Ok(users.contains(user))
}
/// Get a copy of the list of all user credentials
#[tracing::instrument]
pub async fn users() -> crate::Result<Vec<Credentials>> {
let state = State::get().await?;
let users = state.users.read().await;
Ok(users.0.values().cloned().collect())
}
/// Get a specific user by user ID
/// Prefer to use 'refresh' instead of this function
#[tracing::instrument]
pub async fn get_user(user: uuid::Uuid) -> crate::Result<Credentials> {
let state = State::get().await?;
let users = state.users.read().await;
let user = users.get(user).ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Tried to get nonexistent user with ID {user}"
))
.as_error()
})?;
Ok(user)
}

View File

@@ -1,84 +0,0 @@
//! Main authentication flow for Hydra
use serde::Deserialize;
use crate::prelude::Credentials;
use super::stages::{
bearer_token, player_info, poll_response, xbl_signin, xsts_token,
};
#[derive(Debug, Deserialize)]
pub struct OauthFailure {
pub error: String,
}
pub struct SuccessfulLogin {
pub name: String,
pub icon: String,
pub token: String,
pub refresh_token: String,
pub expires_after: i64,
}
pub async fn wait_finish(device_code: String) -> crate::Result<Credentials> {
// Loop, polling for response from Microsoft
let oauth = poll_response::poll_response(device_code).await?;
// Get xbl token from oauth token
let xbl_token = xbl_signin::login_xbl(&oauth.access_token).await?;
// Get xsts token from xbl token
let xsts_response = xsts_token::fetch_token(&xbl_token.token).await?;
match xsts_response {
xsts_token::XSTSResponse::Unauthorized(err) => {
Err(crate::ErrorKind::HydraError(format!(
"Error getting XBox Live token: {}",
err
))
.as_error())
}
xsts_token::XSTSResponse::Success { token: xsts_token } => {
// Get xsts bearer token from xsts token
let (bearer_token, expires_in) =
bearer_token::fetch_bearer(&xsts_token, &xbl_token.uhs)
.await
.map_err(|err| {
crate::ErrorKind::HydraError(format!(
"Error getting bearer token: {}",
err
))
})?;
// Get player info from bearer token
let player_info = player_info::fetch_info(&bearer_token).await.map_err(|_err| {
crate::ErrorKind::HydraError("No Minecraft account for profile. Make sure you own the game and have set a username through the official Minecraft launcher."
.to_string())
})?;
// Create credentials
let credentials = Credentials::new(
uuid::Uuid::parse_str(&player_info.id)?, // get uuid from player_info.id which is a String
player_info.name,
bearer_token,
oauth.refresh_token,
chrono::Utc::now() + chrono::Duration::seconds(expires_in),
);
// Put credentials into state
let state = crate::State::get().await?;
{
let mut users = state.users.write().await;
users.insert(&credentials).await?;
}
if state.settings.read().await.default_user.is_none() {
let mut settings = state.settings.write().await;
settings.default_user = Some(credentials.id);
}
Ok(credentials)
}
}
}

View File

@@ -1,47 +0,0 @@
//! Login route for Hydra, redirects to the Microsoft login page before going to the redirect route
use serde::{Deserialize, Serialize};
use crate::{hydra::MicrosoftError, util::fetch::REQWEST_CLIENT};
use super::{stages::auth_retry, MICROSOFT_CLIENT_ID};
#[derive(Serialize, Deserialize, Debug)]
pub struct DeviceLoginSuccess {
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
pub expires_in: u64,
pub interval: u64,
pub message: String,
}
pub async fn init() -> crate::Result<DeviceLoginSuccess> {
// Get the initial URL
// Get device code
// Define the parameters
// urlencoding::encode("XboxLive.signin offline_access"));
let resp = auth_retry(|| REQWEST_CLIENT.get("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode")
.header("Content-Length", "0")
.query(&[
("client_id", MICROSOFT_CLIENT_ID),
(
"scope",
"XboxLive.signin XboxLive.offline_access profile openid email",
),
])
.send()
).await?;
match resp.status() {
reqwest::StatusCode::OK => Ok(resp.json().await?),
_ => {
let microsoft_error = resp.json::<MicrosoftError>().await?;
Err(crate::ErrorKind::HydraError(format!(
"Error from Microsoft: {:?}",
microsoft_error.error_description
))
.into())
}
}
}

View File

@@ -1,15 +0,0 @@
pub mod complete;
pub mod init;
pub mod refresh;
pub(crate) mod stages;
use serde::Deserialize;
const MICROSOFT_CLIENT_ID: &str = "c4502edb-87c6-40cb-b595-64a280cf8906";
#[derive(Deserialize)]
pub struct MicrosoftError {
pub error: String,
pub error_description: String,
pub error_codes: Vec<u64>,
}

View File

@@ -1,69 +0,0 @@
use std::collections::HashMap;
use reqwest::StatusCode;
use serde::Deserialize;
use crate::{
hydra::{MicrosoftError, MICROSOFT_CLIENT_ID},
util::fetch::REQWEST_CLIENT,
};
use super::stages::auth_retry;
#[derive(Debug, Deserialize)]
pub struct OauthSuccess {
pub token_type: String,
pub scope: String,
pub expires_in: i64,
pub access_token: String,
pub refresh_token: String,
}
pub async fn refresh(refresh_token: String) -> crate::Result<OauthSuccess> {
let mut params = HashMap::new();
params.insert("grant_type", "refresh_token");
params.insert("client_id", MICROSOFT_CLIENT_ID);
params.insert("refresh_token", &refresh_token);
params.insert(
"redirect_uri",
"https://login.microsoftonline.com/common/oauth2/nativeclient",
);
// Poll the URL in a loop until we are successful.
// On an authorization_pending response, wait 5 seconds and try again.
let resp =
auth_retry(|| {
REQWEST_CLIENT
.post("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
.header("Content-Type", "application/x-www-form-urlencoded")
.form(&params)
.send()
})
.await?;
match resp.status() {
StatusCode::OK => {
let oauth = resp.json::<OauthSuccess>().await.map_err(|err| {
crate::ErrorKind::HydraError(format!(
"Could not decipher successful response: {}",
err
))
})?;
Ok(oauth)
}
_ => {
let failure =
resp.json::<MicrosoftError>().await.map_err(|err| {
crate::ErrorKind::HydraError(format!(
"Could not decipher failure response: {}",
err
))
})?;
Err(crate::ErrorKind::HydraError(format!(
"Error refreshing token: {}",
failure.error
))
.as_error())
}
}
}

View File

@@ -1,42 +0,0 @@
use serde::Deserialize;
use serde_json::json;
use super::auth_retry;
const MCSERVICES_AUTH_URL: &str =
"https://api.minecraftservices.com/authentication/login_with_xbox";
#[derive(Deserialize)]
pub struct BearerTokenResponse {
access_token: String,
expires_in: i64,
}
#[tracing::instrument]
pub async fn fetch_bearer(
token: &str,
uhs: &str,
) -> crate::Result<(String, i64)> {
let body = auth_retry(|| {
let client = reqwest::Client::new();
client
.post(MCSERVICES_AUTH_URL)
.header("Accept", "application/json")
.json(&json!({
"identityToken": format!("XBL3.0 x={};{}", uhs, token),
}))
.send()
})
.await?
.text()
.await?;
serde_json::from_str::<BearerTokenResponse>(&body)
.map(|x| (x.access_token, x.expires_in))
.map_err(|_| {
crate::ErrorKind::HydraError(format!(
"Response didn't contain valid bearer token. body: {body}"
))
.into()
})
}

View File

@@ -1,37 +0,0 @@
//! MSA authentication stages
use futures::Future;
use reqwest::Response;
const RETRY_COUNT: usize = 9; // Does command 3 times
const RETRY_WAIT: std::time::Duration = std::time::Duration::from_secs(2);
pub mod bearer_token;
pub mod player_info;
pub mod poll_response;
pub mod xbl_signin;
pub mod xsts_token;
#[tracing::instrument(skip(reqwest_request))]
pub async fn auth_retry<F>(
reqwest_request: impl Fn() -> F,
) -> crate::Result<reqwest::Response>
where
F: Future<Output = Result<Response, reqwest::Error>>,
{
let mut resp = reqwest_request().await?;
for i in 0..RETRY_COUNT {
if resp.status().is_success() {
break;
}
tracing::debug!(
"Request failed with status code {}, retrying...",
resp.status()
);
if i < RETRY_COUNT - 1 {
tokio::time::sleep(RETRY_WAIT).await;
}
resp = reqwest_request().await?;
}
Ok(resp)
}

View File

@@ -1,46 +0,0 @@
//! Fetch player info for display
use serde::Deserialize;
use crate::util::fetch::REQWEST_CLIENT;
use super::auth_retry;
const PROFILE_URL: &str = "https://api.minecraftservices.com/minecraft/profile";
#[derive(Deserialize)]
pub struct PlayerInfo {
pub id: String,
pub name: String,
}
impl Default for PlayerInfo {
fn default() -> Self {
Self {
id: "606e2ff0ed7748429d6ce1d3321c7838".to_string(),
name: String::from("Unknown"),
}
}
}
#[tracing::instrument]
pub async fn fetch_info(token: &str) -> crate::Result<PlayerInfo> {
auth_retry(|| {
REQWEST_CLIENT
.get("https://api.minecraftservices.com/entitlements/mcstore")
.bearer_auth(token)
.send()
})
.await?;
let response = auth_retry(|| {
REQWEST_CLIENT
.get(PROFILE_URL)
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
.send()
})
.await?;
let resp = response.error_for_status()?.json().await?;
Ok(resp)
}

View File

@@ -1,99 +0,0 @@
use std::collections::HashMap;
use reqwest::StatusCode;
use serde::Deserialize;
use crate::{
hydra::{MicrosoftError, MICROSOFT_CLIENT_ID},
util::fetch::REQWEST_CLIENT,
};
use super::auth_retry;
#[derive(Debug, Deserialize)]
pub struct OauthSuccess {
pub token_type: String,
pub scope: String,
pub expires_in: i64,
pub access_token: String,
pub refresh_token: String,
}
#[tracing::instrument]
pub async fn poll_response(device_code: String) -> crate::Result<OauthSuccess> {
let mut params = HashMap::new();
params.insert("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
params.insert("client_id", MICROSOFT_CLIENT_ID);
params.insert("device_code", &device_code);
params.insert(
"scope",
"XboxLive.signin XboxLive.offline_access profile openid email",
);
// Poll the URL in a loop until we are successful.
// On an authorization_pending response, wait 5 seconds and try again.
loop {
let resp = auth_retry(|| {
REQWEST_CLIENT
.post(
"https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
)
.form(&params)
.send()
})
.await?;
match resp.status() {
StatusCode::OK => {
let oauth =
resp.json::<OauthSuccess>().await.map_err(|err| {
crate::ErrorKind::HydraError(format!(
"Could not decipher successful response: {}",
err
))
})?;
return Ok(oauth);
}
_ => {
let failure =
resp.json::<MicrosoftError>().await.map_err(|err| {
crate::ErrorKind::HydraError(format!(
"Could not decipher failure response: {}",
err
))
})?;
match failure.error.as_str() {
"authorization_pending" => {
tokio::time::sleep(std::time::Duration::from_secs(2))
.await;
}
"authorization_declined" => {
return Err(crate::ErrorKind::HydraError(
"Authorization declined".to_string(),
)
.as_error());
}
"expired_token" => {
return Err(crate::ErrorKind::HydraError(
"Device code expired".to_string(),
)
.as_error());
}
"bad_verification_code" => {
return Err(crate::ErrorKind::HydraError(
"Invalid device code".to_string(),
)
.as_error());
}
_ => {
return Err(crate::ErrorKind::HydraError(format!(
"Unknown error: {}",
failure.error
))
.as_error());
}
}
}
}
}
}

View File

@@ -1,59 +0,0 @@
use serde_json::json;
use crate::util::fetch::REQWEST_CLIENT;
use super::auth_retry;
const XBL_AUTH_URL: &str = "https://user.auth.xboxlive.com/user/authenticate";
// Deserialization
pub struct XBLLogin {
pub token: String,
pub uhs: String,
}
// Impl
#[tracing::instrument]
pub async fn login_xbl(token: &str) -> crate::Result<XBLLogin> {
let response = auth_retry(|| {
REQWEST_CLIENT
.post(XBL_AUTH_URL)
.header(reqwest::header::ACCEPT, "application/json")
.json(&json!({
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": format!("d={token}")
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}))
.send()
})
.await?;
let body = response.text().await?;
let json = serde_json::from_str::<serde_json::Value>(&body)?;
let token = Some(&json)
.and_then(|it| it.get("Token")?.as_str().map(String::from))
.ok_or(crate::ErrorKind::HydraError(
"XBL response didn't contain valid token".to_string(),
))?;
let uhs = Some(&json)
.and_then(|it| {
it.get("DisplayClaims")?
.get("xui")?
.get(0)?
.get("uhs")?
.as_str()
.map(String::from)
})
.ok_or(
crate::ErrorKind::HydraError(
"XBL response didn't contain valid user hash".to_string(),
)
.as_error(),
)?;
Ok(XBLLogin { token, uhs })
}

View File

@@ -1,62 +0,0 @@
use serde_json::json;
use crate::util::fetch::REQWEST_CLIENT;
use super::auth_retry;
const XSTS_AUTH_URL: &str = "https://xsts.auth.xboxlive.com/xsts/authorize";
pub enum XSTSResponse {
Unauthorized(String),
Success { token: String },
}
#[tracing::instrument]
pub async fn fetch_token(token: &str) -> crate::Result<XSTSResponse> {
let resp = auth_retry(|| {
REQWEST_CLIENT
.post(XSTS_AUTH_URL)
.header(reqwest::header::ACCEPT, "application/json")
.json(&json!({
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [
token
]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}))
.send()
})
.await?;
let status = resp.status();
let body = resp.text().await?;
let json = serde_json::from_str::<serde_json::Value>(&body)?;
if status.is_success() {
Ok(json
.get("Token")
.and_then(|x| x.as_str().map(String::from))
.map(|it| XSTSResponse::Success { token: it })
.unwrap_or(XSTSResponse::Unauthorized(
"XSTS response didn't contain valid token!".to_string(),
)))
} else {
Ok(XSTSResponse::Unauthorized(
#[allow(clippy::unreadable_literal)]
match json.get("XErr").and_then(|x| x.as_i64()) {
Some(2148916238) => {
String::from("This Microsoft account is underage and is not linked to a family.")
},
Some(2148916235) => {
String::from("XBOX Live/Minecraft is not available in your country.")
},
Some(2148916233) => String::from("This account does not have a valid XBOX Live profile. Please buy Minecraft and try again!"),
Some(2148916236) | Some(2148916237) => String::from("This account needs adult verification on Xbox page."),
_ => String::from("Unknown error code"),
},
))
}
}

View File

@@ -140,8 +140,14 @@ pub async fn get_output_by_filename(
let logs_folder = DirectoryInfo::profile_logs_dir(profile_subpath).await?;
let path = logs_folder.join(file_name);
let credentials: Vec<Credentials> =
state.users.read().await.clone().0.into_values().collect();
let credentials: Vec<Credentials> = state
.users
.read()
.await
.users
.clone()
.into_values()
.collect();
// Load .gz file into String
if let Some(ext) = path.extension() {
@@ -296,8 +302,14 @@ pub async fn get_generic_live_log_cursor(
let output = String::from_utf8_lossy(&buffer).to_string(); // Convert to String
let cursor = cursor + bytes_read as u64; // Update cursor
let credentials: Vec<Credentials> =
state.users.read().await.clone().0.into_values().collect();
let credentials: Vec<Credentials> = state
.users
.read()
.await
.users
.clone()
.into_values()
.collect();
let output = CensoredString::censor(output, &credentials);
Ok(LatestLogCursor {
cursor,

View File

@@ -0,0 +1,76 @@
//! Authentication flow interface
use crate::state::{Credentials, MinecraftLoginFlow};
use crate::State;
#[tracing::instrument]
pub async fn begin_login() -> crate::Result<MinecraftLoginFlow> {
let state = State::get().await?;
let mut users = state.users.write().await;
users.login_begin().await
}
#[tracing::instrument]
pub async fn finish_login(
code: &str,
flow: MinecraftLoginFlow,
) -> crate::Result<Credentials> {
let state = State::get().await?;
let mut users = state.users.write().await;
users.login_finish(code, flow).await
}
#[tracing::instrument]
pub async fn get_default_user() -> crate::Result<Option<uuid::Uuid>> {
let state = State::get().await?;
let users = state.users.read().await;
Ok(users.default_user)
}
#[tracing::instrument]
pub async fn set_default_user(user: uuid::Uuid) -> crate::Result<()> {
let user = get_user(user).await?;
let state = State::get().await?;
let mut users = state.users.write().await;
users.default_user = Some(user.id);
users.save().await?;
Ok(())
}
/// Remove a user account from the database
#[tracing::instrument]
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
let state = State::get().await?;
let mut users = state.users.write().await;
users.remove(user).await?;
Ok(())
}
/// Get a copy of the list of all user credentials
#[tracing::instrument]
pub async fn users() -> crate::Result<Vec<Credentials>> {
let state = State::get().await?;
let users = state.users.read().await;
Ok(users.users.values().cloned().collect())
}
/// Get a specific user by user ID
/// Prefer to use 'refresh' instead of this function
#[tracing::instrument]
pub async fn get_user(user: uuid::Uuid) -> crate::Result<Credentials> {
let state = State::get().await?;
let users = state.users.read().await;
let user = users
.users
.get(&user)
.ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Tried to get nonexistent user with ID {user}"
))
.as_error()
})?
.clone();
Ok(user)
}

View File

@@ -1,10 +1,9 @@
//! API for interacting with Theseus
pub mod auth;
pub mod handler;
pub mod hydra;
pub mod jre;
pub mod logs;
pub mod metadata;
pub mod minecraft_auth;
pub mod mr_auth;
pub mod pack;
pub mod process;
@@ -15,19 +14,19 @@ pub mod tags;
pub mod data {
pub use crate::state::{
DirectoryInfo, Hooks, JavaSettings, LinkedData, MemorySettings,
ModLoader, ModrinthCredentials, ModrinthCredentialsResult,
ModrinthProject, ModrinthTeamMember, ModrinthUser, ModrinthVersion,
ProfileMetadata, ProjectMetadata, Settings, Theme, WindowSize,
Credentials, DirectoryInfo, Hooks, JavaSettings, LinkedData,
MemorySettings, ModLoader, ModrinthCredentials,
ModrinthCredentialsResult, ModrinthProject, ModrinthTeamMember,
ModrinthUser, ModrinthVersion, ProfileMetadata, ProjectMetadata,
Settings, Theme, WindowSize,
};
}
pub mod prelude {
pub use crate::{
auth::{self, Credentials},
data::*,
event::CommandPayload,
jre, metadata, pack, process,
jre, metadata, minecraft_auth, pack, process,
profile::{self, create, Profile},
settings,
state::JavaGlobals,

View File

@@ -8,12 +8,13 @@ use crate::pack::install_from::{
EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
};
use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId};
use crate::state::{InnerProjectPathUnix, ProjectMetadata, SideType};
use crate::state::{
Credentials, InnerProjectPathUnix, ProjectMetadata, SideType,
};
use crate::util::fetch;
use crate::util::io::{self, IOError};
use crate::{
auth::{self, refresh},
event::{emit::emit_profile, ProfilePayloadType},
state::MinecraftChild,
};
@@ -745,20 +746,16 @@ pub async fn run(
let state = State::get().await?;
// Get default account and refresh credentials (preferred way to log in)
let default_account = state.settings.read().await.default_user;
let credentials = if let Some(default_account) = default_account {
refresh(default_account).await?
} else {
// If no default account, try to use a logged in account
let users = auth::users().await?;
let last_account = users.first();
if let Some(last_account) = last_account {
refresh(last_account.id).await?
} else {
return Err(crate::ErrorKind::NoCredentialsError.as_error());
}
let default_account = {
let mut write = state.users.write().await;
write
.get_default_credential()
.await?
.ok_or_else(|| crate::ErrorKind::NoCredentialsError.as_error())?
};
run_credentials(path, &credentials).await
run_credentials(path, &default_account).await
}
/// Run Minecraft using a profile, and credentials for authentication
@@ -767,7 +764,7 @@ pub async fn run(
#[theseus_macros::debug_pin]
pub async fn run_credentials(
path: &ProfilePathId,
credentials: &auth::Credentials,
credentials: &Credentials,
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
let state = State::get().await?;
let settings = state.settings.read().await;