Squashed commit of the following:

commit a782b683f30d127936115d0e31a70de1d8268694
Author: Wyvest <wyvestbusiness@gmail.com>
Date:   Wed Feb 25 20:56:05 2026 -0500

    remove debug logging

commit 640950728f480f1b8c7939a720124ea581c227af
Author: Wyvest <wyvestbusiness@gmail.com>
Date:   Wed Feb 25 14:40:03 2026 -0500

    add some ms account error parsing

commit 014e3ecb26d75934798592f5be8cfbb437c92263
Merge: 83a6a1e ecfb98b
Author: Wyvest <wyvestbusiness@gmail.com>
Date:   Wed Feb 25 13:34:16 2026 -0500

    Merge branch 'main' into account-logging

commit 83a6a1e233dd4f3a3de18aabf1c7beac268d1594
Author: Wyvest <wyvestbusiness@gmail.com>
Date:   Wed Feb 25 04:05:05 2026 -0500

    add a lot of debug logging for accounts
This commit is contained in:
Wyvest 2026-02-25 20:56:21 -05:00
parent 5b566a03a3
commit e7bb7d6b70
10 changed files with 525 additions and 51 deletions

View file

@ -78,7 +78,7 @@ export type MessagePayload = { level: MessageLevel; message: string }
export type MetadataError = { type: "FetchError"; data: string } | { type: "NotModdedManifest"; data: string } | { type: "NotVanillaManifest"; data: string } | { type: "ParseError"; data: string } | { type: "NoMatchingLoader"; data: string } | { type: "NoMatchingVersion"; data: string }
export type MinecraftAuthError = { type: "PublicKeyReading"; data: string } | { type: "PKCS8Error"; data: string } | { type: "SerializeError"; data: string } | { type: "DeserializeError"; data: string } | { type: "RequestError"; data: string } | { type: "SigningError"; data: string } | { type: "HashError"; data: string } | { type: "SessionIdError"; data: string }
export type MinecraftAuthError = { type: "PublicKeyReading"; data: string } | { type: "PKCS8Error"; data: string } | { type: "SerializeError"; data: string } | { type: "DeserializeError"; data: string } | { type: "RequestError"; data: string } | { type: "SigningError"; data: string } | { type: "HashError"; data: string } | { type: "SessionIdError"; data: string } | { type: "XboxError"; data: string }
/**
* A structure of all needed Minecraft credentials for logging in and account management.
@ -313,20 +313,12 @@ export type VersionType =
*/
"old_beta"
const ARGS_MAP = { 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'core':'{"getGameVersions":[],"getScreenshots":["id"],"setDefaultUser":["uuid"],"removeUser":["uuid"],"isClusterRunning":["cluster_id"],"killProcess":["pid"],"updateClusterById":["id","request"],"writeSettings":["setting"],"getWorlds":["id"],"setDiscordRPCMessage":["message"],"removeCape":["access_token"],"getLinkedPackages":["cluster_id"],"getPackageBody":["provider","body"],"removeCluster":["id"],"syncCluster":["cluster_id"],"launchCluster":["id","uuid","search_for_java"],"getLogByName":["id","name"],"getProfileOrDefault":["name"],"getClusterById":["id"],"getLogs":["id"],"getLoadersForVersion":["mc_version"],"openMsaLogin":[],"getRunningProcessesByClusterId":["cluster_id"],"getGlobalProfile":[],"refreshAccounts":[],"getRunningProcesses":[],"readSettings":[],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getPackage":["provider","slug"],"fetchMinecraftProfile":["uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"setClusterStage":["id","stage"],"createSettingsProfile":["name"],"open":["input"],"createCluster":["options"],"updateClusterProfile":["name","profile"],"refreshAccount":["uuid"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getUsers":[],"getUser":["uuid"],"getUsersFromAuthor":["provider","author"],"getMultiplePackages":["provider","slugs"],"changeCape":["access_token","cape_uuid"],"changeSkin":["access_token","skin_url","skin_variant"],"convertUsernameUUID":["username_uuid"],"removePackage":["cluster_id","package_hash"],"searchPackages":["provider","query"],"getClusters":[],"togglePackage":["cluster_id","package_hash"],"installModpack":["modpack","cluster_id"],"getProcessLogTail":["id","max_lines"],"fetchLoggedInProfile":["access_token"],"getDefaultUser":["fallback"]}', 'debug':'{"getBuildTimestamp":[],"getType":[],"getPackageVersion":[],"openDevTools":[],"getPlatform":[],"getLocale":[],"isInDev":[],"getArch":[],"getOsVersion":[],"getGitCommitHash":[],"getFamily":[]}', 'oneclient':'{"installUpdate":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"],"extractBundleOverrides":["bundle_path","cluster_id"],"checkForUpdate":[],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"updateBundlePackages":["cluster_id"],"cacheArt":["path"],"refreshArt":["path"],"isBundleSyncing":[],"getVersions":[]}', 'events':'{"message":["event"],"ingress":["event"],"process":["event"]}' }
export type Router = { 'folders': { fromCluster: (folderName: string) => Promise<string>,
const ARGS_MAP = { 'core':'{"updateClusterById":["id","request"],"createSettingsProfile":["name"],"convertUsernameUUID":["username_uuid"],"refreshAccount":["uuid"],"killProcess":["pid"],"removeCluster":["id"],"writeSettings":["setting"],"getScreenshots":["id"],"getUsersFromAuthor":["provider","author"],"removePackage":["cluster_id","package_hash"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"changeSkin":["access_token","skin_url","skin_variant"],"getProcessLogTail":["id","max_lines"],"searchPackages":["provider","query"],"createCluster":["options"],"getPackageBody":["provider","body"],"getClusterById":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"fetchMinecraftProfile":["uuid"],"fetchLoggedInProfile":["access_token"],"getProfileOrDefault":["name"],"isClusterRunning":["cluster_id"],"getDefaultUser":["fallback"],"refreshAccounts":[],"getRunningProcesses":[],"getLogs":["id"],"openMsaLogin":[],"getClusters":[],"getLoadersForVersion":["mc_version"],"launchCluster":["id","uuid","search_for_java"],"getGlobalProfile":[],"removeUser":["uuid"],"getGameVersions":[],"setDefaultUser":["uuid"],"updateClusterProfile":["name","profile"],"setClusterStage":["id","stage"],"getUser":["uuid"],"getUsers":[],"getPackage":["provider","slug"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"syncCluster":["cluster_id"],"togglePackage":["cluster_id","package_hash"],"changeCape":["access_token","cape_uuid"],"removeCape":["access_token"],"getWorlds":["id"],"getLogByName":["id","name"],"readSettings":[],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"getLinkedPackages":["cluster_id"],"getMultiplePackages":["provider","slugs"],"getRunningProcessesByClusterId":["cluster_id"],"installModpack":["modpack","cluster_id"],"setDiscordRPCMessage":["message"],"open":["input"]}', 'oneclient':'{"refreshArt":["path"],"cacheArt":["path"],"getVersions":[],"getBundlesFor":["cluster_id"],"updateBundlePackages":["cluster_id"],"checkForUpdate":[],"installUpdate":[],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"isBundleSyncing":[],"extractBundleOverrides":["bundle_path","cluster_id"],"getClustersGroupedByMajor":[]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}', 'debug':'{"getGitCommitHash":[],"isInDev":[],"getPackageVersion":[],"getArch":[],"getOsVersion":[],"getLocale":[],"getBuildTimestamp":[],"getType":[],"getFamily":[],"openDevTools":[],"getPlatform":[]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}' }
export type Router = { 'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> },
'folders': { fromCluster: (folderName: string) => Promise<string>,
openCluster: (folderName: string) => Promise<null> },
'debug': { openDevTools: () => Promise<void>,
isInDev: () => Promise<boolean>,
getArch: () => Promise<string>,
getFamily: () => Promise<string>,
getLocale: () => Promise<string | null>,
getType: () => Promise<string>,
getPlatform: () => Promise<string>,
getOsVersion: () => Promise<string>,
getGitCommitHash: () => Promise<string>,
getBuildTimestamp: () => Promise<string>,
getPackageVersion: () => Promise<string> },
'oneclient': { getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]>,
getVersions: () => Promise<OnlineClusterManifest>,
@ -392,9 +384,17 @@ removeCape: (accessToken: string) => Promise<MojangFullPlayerProfile>,
convertUsernameUUID: (usernameUuid: string) => Promise<MowojangProfile>,
setDiscordRPCMessage: (message: string) => Promise<null>,
open: (input: string) => Promise<null> },
'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> } };
'debug': { openDevTools: () => Promise<void>,
isInDev: () => Promise<boolean>,
getArch: () => Promise<string>,
getFamily: () => Promise<string>,
getLocale: () => Promise<string | null>,
getType: () => Promise<string>,
getPlatform: () => Promise<string>,
getOsVersion: () => Promise<string>,
getGitCommitHash: () => Promise<string>,
getBuildTimestamp: () => Promise<string>,
getPackageVersion: () => Promise<string> } };
export type { InferCommandOutput }

View file

@ -1,12 +1,13 @@
import type { MinecraftCredentials } from '@/bindings.gen';
import { AccountAvatar, Overlay } from '@/components';
import { AccountAvatar, isMinecraftAuthError, MinecraftAuthErrorModal, Overlay } from '@/components';
import { bindings } from '@/main';
import { useCommandMut } from '@onelauncher/common';
import { useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
export function AddAccountModal() {
return (
<Overlay.Dialog>
<Overlay.Dialog isDismissable>
<Overlay.Title>Add Account</Overlay.Title>
<p className="text-fg-secondary">Add a new account to OneClient</p>
<AddAccountModalButton />
@ -16,9 +17,11 @@ export function AddAccountModal() {
export function AddAccountModalButton() {
const queryClient = useQueryClient();
const [authError, setAuthError] = useState<unknown>(null);
const { data: profile, isPending, mutate: login } = useCommandMut(bindings.core.openMsaLogin, {
onSuccess() {
onSuccess(data) {
setAuthError(null);
queryClient.invalidateQueries({
queryKey: ['getUsers'],
});
@ -26,12 +29,21 @@ export function AddAccountModalButton() {
queryKey: ['getDefaultUser'],
});
},
onError(error) {
console.error('[auth] AddAccountModal: openMsaLogin failed', error);
if (isMinecraftAuthError(error))
setAuthError(error);
},
});
const onClick = () => {
setAuthError(null);
login();
};
if (authError)
return <MinecraftAuthErrorModal error={authError} />;
return (
profile
? (

View file

@ -0,0 +1,184 @@
import type { MinecraftAuthErrorInfo } from './minecraft-auth-errors';
import { Overlay } from '@/components';
import { bindings } from '@/main';
import { useCommandMut } from '@onelauncher/common';
import { Button } from '@onelauncher/common/components';
import { useQueryClient } from '@tanstack/react-query';
import { AlertTriangleIcon, Copy01Icon, RefreshCw01Icon } from '@untitled-theme/icons-react';
import { useState } from 'react';
import { minecraftAuthErrors } from './minecraft-auth-errors';
/**
* Extracts an Xbox error code from a LauncherError or error string.
* Error format from backend: "Minecraft authentication error: <CODE> during MSA step <STEP>"
*/
export function extractXboxErrorCode(error: unknown): string | null {
const errorStr = typeof error === 'string'
? error
: error instanceof Error
? error.message
: typeof error === 'object' && error !== null
? JSON.stringify(error)
: String(error);
const match = errorStr.match(/Minecraft authentication error:\s*(\d+)/);
return match ? match[1] : null;
}
/**
* Checks whether an error is a Minecraft/Xbox authentication error.
*/
export function isMinecraftAuthError(error: unknown): boolean {
return extractXboxErrorCode(error) !== null;
}
function MatchedErrorContent({
matchedError,
}: {
matchedError: MinecraftAuthErrorInfo;
}) {
return (
<div className="flex flex-col gap-3 w-full text-left">
<div>
<h3 className="text-fg-primary font-semibold text-sm mb-1">What happened</h3>
<p className="text-fg-secondary text-sm">{matchedError.whatHappened}</p>
</div>
<div>
<h3 className="text-fg-primary font-semibold text-sm mb-1">How to fix it</h3>
<ol className="list-none flex flex-col gap-1.5">
{matchedError.stepsToFix.map((step, index) => (
<li className="flex items-start gap-2" key={step}>
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-brand-primary/20 text-brand-primary text-xs flex items-center justify-center font-medium mt-0.5">
{index + 1}
</span>
<span
className="text-fg-secondary text-sm [&_a]:text-brand-primary [&_a]:underline"
dangerouslySetInnerHTML={{ __html: step }}
/>
</li>
))}
</ol>
</div>
</div>
);
}
function UnknownErrorContent() {
return (
<div className="flex flex-col gap-3 w-full text-left">
<div>
<h3 className="text-fg-primary font-semibold text-sm mb-1">Unknown error</h3>
<p className="text-fg-secondary text-sm">
We don't recognize this error and can't recommend specific steps to resolve it.
</p>
<p className="text-fg-secondary text-sm mt-1">
Try visiting
{' '}
<a
className="text-brand-primary underline"
href="https://www.minecraft.net/en-us/login"
rel="noopener noreferrer"
target="_blank"
>
Minecraft Login
</a>
{' '}
and signing in, as it may prompt you with the necessary steps.
</p>
</div>
</div>
);
}
export function MinecraftAuthErrorModal({
error,
}: {
error: unknown;
}) {
const queryClient = useQueryClient();
const [copied, setCopied] = useState(false);
const errorCode = extractXboxErrorCode(error);
const matchedError = errorCode
? minecraftAuthErrors.find(e => e.errorCode === errorCode)
: null;
const { isPending: isRetrying, mutate: retryLogin } = useCommandMut(bindings.core.openMsaLogin, {
onSuccess(data) {
console.warn('[auth] MinecraftAuthErrorModal: retry openMsaLogin succeeded', data ? `user=${data.username} id=${data.id}` : 'returned null');
queryClient.invalidateQueries({ queryKey: ['getUsers'] });
queryClient.invalidateQueries({ queryKey: ['getDefaultUser'] });
},
onError(err) {
console.error('[auth] MinecraftAuthErrorModal: retry openMsaLogin failed', err);
},
});
const debugInfo = typeof error === 'string'
? error
: error instanceof Error
? error.message
: JSON.stringify(error, null, 2);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(debugInfo);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
catch {
console.error('[auth] Failed to copy to clipboard');
}
};
return (
<Overlay.Dialog isDismissable>
<div className="flex items-center gap-2">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-danger/20">
<AlertTriangleIcon className="w-4 h-4 text-danger" />
</div>
<Overlay.Title>Sign-in Error</Overlay.Title>
</div>
<p className="text-fg-secondary text-sm text-center max-w-sm">
Something went wrong while signing in with your Microsoft account.
{errorCode && (
<>
{' '}
<span className="text-fg-secondary/60 text-xs">
(Error code:
{' '}
{errorCode}
)
</span>
</>
)}
</p>
{matchedError ? <MatchedErrorContent matchedError={matchedError} /> : <UnknownErrorContent />}
<div className="flex flex-col w-full gap-2 mt-1">
<Button
color="primary"
isPending={isRetrying}
onPress={() => retryLogin()}
size="large"
slot="close"
>
<RefreshCw01Icon className="w-4 h-4" />
Sign in again
</Button>
<Button
color="secondary"
onPress={handleCopy}
size="large"
>
<Copy01Icon className="w-4 h-4" />
{copied ? 'Copied!' : 'Copy error details'}
</Button>
</div>
</Overlay.Dialog>
);
}

View file

@ -1,4 +1,4 @@
import { Overlay, RawDebugInfo, SettingsRow, SettingsSwitch, useDebugInfo } from '@/components';
import { minecraftAuthErrors, MinecraftAuthErrorModal, Overlay, RawDebugInfo, SettingsRow, SettingsSwitch, useDebugInfo } from '@/components';
import { useSettings } from '@/hooks/useSettings';
import { bindings } from '@/main';
import { Button } from '@onelauncher/common/components';
@ -10,6 +10,7 @@ import { useEffect, useState } from 'react';
export function SuperSecretDevOptions() {
const { createSetting } = useSettings();
const debugInfo = useDebugInfo();
const [previewError, setPreviewError] = useState<string | null>(null);
const [launcherDir, setLauncherDir] = useState('');
@ -48,9 +49,43 @@ export function SuperSecretDevOptions() {
<Button onClick={logRunningProcesses} size="normal">Console log processes</Button>
</div>
<div className="mt-3 border-t border-component-border pt-3">
<p className="text-fg-secondary text-xs mb-2">Auth Error Preview</p>
<div className="flex flex-wrap gap-2">
{minecraftAuthErrors.map(err => (
<Button
key={err.errorCode}
onClick={() => setPreviewError(`Minecraft authentication error: ${err.errorCode} during MSA step XstsAuthorize`)}
size="normal"
>
{err.errorCode}
</Button>
))}
<Button
onClick={() => setPreviewError('Some unknown auth error occurred')}
size="normal"
>
Unknown
</Button>
</div>
</div>
</div>
<RawDebugInfo debugInfo={debugInfo} />
{previewError && (
<Overlay
isDismissable
isOpen
onOpenChange={(open) => {
if (!open)
setPreviewError(null);
}}
>
<MinecraftAuthErrorModal error={previewError} />
</Overlay>
)}
</Overlay.Dialog>
);
}

View file

@ -4,6 +4,8 @@ export * from './BundleModListModal';
export * from './DebugInfo';
export * from './ImportSkinModal';
export * from './KillMinecraft';
export * from './minecraft-auth-errors';
export * from './MinecraftAuthErrorModal';
export * from './NoAccountPopup';
export * from './Overlay';
export * from './Popup';

View file

@ -0,0 +1,90 @@
export interface MinecraftAuthErrorInfo {
errorCode: string;
whatHappened: string;
stepsToFix: Array<string>;
}
export const minecraftAuthErrors: Array<MinecraftAuthErrorInfo> = [
{
errorCode: '2148916222',
whatHappened:
'Your Minecraft/Xbox Live account requires age verification to comply with UK regulations. You must complete this before signing in.',
stepsToFix: [
'Go to the <a href="https://www.minecraft.net/en-us/login" target="_blank" rel="noopener noreferrer">Minecraft Login</a> page and sign in',
'Follow the instructions to verify your age',
'Once verified, try signing in again',
'For additional help, visit <a href="https://support.xbox.com/en-GB/help/family-online-safety/online-safety/UK-age-verification" target="_blank" rel="noopener noreferrer">UK age verification on Xbox</a>',
],
},
{
errorCode: '2148916233',
whatHappened: `This account doesn't have an Xbox profile set up or doesn't own Minecraft.`,
stepsToFix: [
'Make sure Minecraft is purchased on this account',
'Visit <a href="https://www.minecraft.net/en-us/login" target="_blank" rel="noopener noreferrer">Minecraft Login</a> and sign in',
'Complete Xbox profile setup if prompted',
'Once finished, try signing in again',
],
},
{
errorCode: '2148916235',
whatHappened: `Xbox Live isn't available in your region, so sign-in is blocked.`,
stepsToFix: [
'Xbox services must be supported in your country before you can sign in',
'Check <a href="https://www.xbox.com/en-US/regions" target="_blank" rel="noopener noreferrer">Xbox Availability</a> for supported regions',
],
},
{
errorCode: '2148916236',
whatHappened: 'This account requires adult verification under South Korean regulations.',
stepsToFix: [
'Visit <a href="https://www.xbox.com" target="_blank" rel="noopener noreferrer">Xbox</a> and sign in',
'Complete the identity verification process',
'Once finished, try signing in again',
],
},
{
errorCode: '2148916237',
whatHappened: 'This account requires adult verification under South Korean regulations.',
stepsToFix: [
'Visit <a href="https://www.xbox.com" target="_blank" rel="noopener noreferrer">Xbox</a> and sign in',
'Complete the identity verification process',
'Once finished, try signing in again',
],
},
{
errorCode: '2148916238',
whatHappened: 'This account is underage and not linked to a Microsoft family group.',
stepsToFix: [
'Review the <a href="https://help.minecraft.net/hc/en-us/articles/4408968616077" target="_blank" rel="noopener noreferrer">Family Setup Guide</a>',
'Join or create a family group as instructed',
'Once finished, try signing in again',
],
},
{
errorCode: '2148916227',
whatHappened: 'This account was suspended for violating Xbox Community Standards.',
stepsToFix: [
'Visit <a href="https://support.xbox.com" target="_blank" rel="noopener noreferrer">Xbox Support</a> and review the enforcement details',
'Submit an appeal if one is available',
],
},
{
errorCode: '2148916229',
whatHappened: `This account is restricted and doesn't have permission to play online.`,
stepsToFix: [
'Have a guardian sign in to <a href="https://account.microsoft.com/family/" target="_blank" rel="noopener noreferrer">Microsoft Family</a>',
'Update online play permissions',
'Once finished, try signing in again',
],
},
{
errorCode: '2148916234',
whatHappened: `This account hasn't accepted Xbox's Terms of Service.`,
stepsToFix: [
'Visit <a href="https://www.xbox.com" target="_blank" rel="noopener noreferrer">Xbox</a> and sign in',
'Accept the Terms if prompted',
'Once finished, try signing in again',
],
},
];

View file

@ -1,5 +1,5 @@
import type { MinecraftCredentials } from '@/bindings.gen';
import { AccountAvatar, SkinViewer } from '@/components';
import { AccountAvatar, isMinecraftAuthError, MinecraftAuthErrorModal, Overlay, SkinViewer } from '@/components';
import { usePlayerProfile } from '@/hooks/usePlayerProfile';
import { bindings } from '@/main';
import { OnboardingNavigation } from '@/routes/onboarding/route';
@ -7,6 +7,7 @@ import { useCommandMut, useCommandSuspense } from '@onelauncher/common';
import { Button } from '@onelauncher/common/components';
import { useQueryClient } from '@tanstack/react-query';
import { createFileRoute } from '@tanstack/react-router';
import { useState } from 'react';
export const Route = createFileRoute('/onboarding/account')({
component: RouteComponent,
@ -52,16 +53,26 @@ function AccountPreview({
function RouteComponent() {
const queryClient = useQueryClient();
const { data: currentAccount } = useCommandSuspense(['getDefaultUser'], () => bindings.core.getDefaultUser(true));
const [authError, setAuthError] = useState<unknown>(null);
const { data: currentAccount } = useCommandSuspense(['getDefaultUser'], () => {
return bindings.core.getDefaultUser(true);
});
const { data: profile, isPending, mutate: login } = useCommandMut(bindings.core.openMsaLogin, {
onSuccess() {
onSuccess(data) {
setAuthError(null);
queryClient.invalidateQueries({
queryKey: ['getDefaultUser'],
});
},
onError(error) {
console.error('[auth] onboarding/account: openMsaLogin failed', error);
if (isMinecraftAuthError(error))
setAuthError(error);
},
});
const onClick = () => {
setAuthError(null);
login();
};
@ -100,6 +111,19 @@ function RouteComponent() {
)}
</div>
<OnboardingNavigation disableNext={currentAccount === null} />
{authError && (
<Overlay
isDismissable
isOpen
onOpenChange={(open) => {
if (!open)
setAuthError(null);
}}
>
<MinecraftAuthErrorModal error={authError} />
</Overlay>
)}
</>
);
}

View file

@ -321,6 +321,7 @@ pub struct ProfileUpdate {
#[taurpc::ipc_type]
pub struct TauriLauncherApiImpl;
#[allow(clippy::too_many_lines)]
#[taurpc::resolvers]
impl TauriLauncherApi for TauriLauncherApiImpl {
// MARK: Impl: clusters
@ -622,7 +623,13 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
) -> LauncherResult<Option<MinecraftCredentials>> {
use tauri::Manager;
let flow = api::credentials::begin().await?;
let flow = api::credentials::begin().await.map_err(|e| {
tracing::error!(
"[auth] open_msa_login() - credentials::begin() failed: {:?}",
e
);
e
})?;
let now = chrono::Utc::now();
@ -643,7 +650,14 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
.inner_size(800.0, 600.0)
.center()
.focused(true)
.build()?;
.build()
.map_err(|e| {
tracing::error!(
"[auth] open_msa_login() - failed to create login window: {:?}",
e
);
e
})?;
win.request_user_attention(Some(tauri::UserAttentionType::Critical))?;
@ -652,18 +666,33 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
return Ok(None);
}
if win
.url()?
let current_url = match win.url() {
Ok(u) => u,
Err(_) => {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
continue;
}
};
if current_url
.as_str()
.starts_with("https://login.live.com/oauth20_desktop.srf")
&& let Some((_, code)) = win.url()?.query_pairs().find(|x| x.0 == "code")
{
win.close()?;
let value = api::credentials::finish(&code.clone(), flow).await?;
if let Some((_, code)) = win.url()?.query_pairs().find(|x| x.0 == "code") {
win.close()?;
let value = api::credentials::finish(&code.clone(), flow)
.await
.map_err(|e| {
tracing::error!(
"[auth] open_msa_login() - credentials::finish() failed: {:?}",
e
);
e
})?;
return Ok(Some(value));
return Ok(Some(value));
}
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}

View file

@ -18,7 +18,7 @@ pub async fn start_logger() {
Core::get()
.logger_filter
.clone()
.unwrap_or_else(|| format!("{}=info", env!("CARGO_PKG_NAME"))),
.unwrap_or_else(|| format!("{}=debug", env!("CARGO_PKG_NAME"))),
)
});

View file

@ -36,18 +36,17 @@ impl CredentialsStore {
#[tracing::instrument]
pub async fn initialize() -> LauncherResult<Self> {
let path = Dirs::get_auth_file().await?;
let store = io::read_json(&path).await.ok();
let store = io::read_json::<Self>(&path).await.ok();
store.map_or_else(
|| {
Ok(Self {
users: HashMap::new(),
token: None,
default_user: None,
})
},
Ok,
)
if let Some(s) = store {
Ok(s)
} else {
Ok(Self {
users: HashMap::new(),
token: None,
default_user: None,
})
}
}
/// Save the current Minecraft credentials.
@ -143,6 +142,10 @@ impl CredentialsStore {
}),
Err(err) => {
if valid_date {
tracing::error!(
"[auth] begin() - sisu_authenticate failed (device token was fresh): {:?}",
err
);
Err(err.into())
} else {
let (key, token, current_date, _) = self.refresh(Utc::now(), false).await?;
@ -290,6 +293,7 @@ impl CredentialsStore {
_ => {}
}
tracing::error!("[auth] get_default() - token refresh failed: {:?}", err);
Err(err)
}
}
@ -397,7 +401,7 @@ pub async fn device_token(
key: &DeviceKey,
current_date: DateTime<Utc>,
) -> Result<RequestWithDate<DeviceToken>, MinecraftAuthError> {
let res = send_signed_request(
let res: SignedRequestResponse<DeviceToken> = send_signed_request(
None,
"https://device.auth.xboxlive.com/device/authenticate",
"/device/authenticate",
@ -474,7 +478,10 @@ async fn sisu_authenticate(
.headers
.get("X-SessionId")
.and_then(|x| x.to_str().ok())
.ok_or_else(|| MinecraftAuthError::SessionIdError)?
.ok_or_else(|| {
tracing::error!("[auth] sisu_authenticate() - missing X-SessionId header in response");
MinecraftAuthError::SessionIdError
})?
.to_string();
Ok((
@ -533,7 +540,15 @@ async fn oauth_token(
source,
})?;
let body =
if !status.is_success() {
tracing::error!(
"[auth] oauth_token() - non-success status={}, body={}",
status,
text
);
}
let body: OAuthToken =
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
step: MinecraftAuthStep::OAuthToken,
raw: text,
@ -581,7 +596,15 @@ async fn oauth_refresh(
source,
})?;
let body =
if !status.is_success() {
tracing::error!(
"[auth] oauth_refresh() - non-success status={}, body={}",
status,
text
);
}
let body: OAuthToken =
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
step: MinecraftAuthStep::RefreshOAuthToken,
raw: text,
@ -657,7 +680,7 @@ async fn xsts_authorize(
key: &DeviceKey,
current_date: DateTime<Utc>,
) -> Result<RequestWithDate<DeviceToken>, MinecraftAuthError> {
let res = send_signed_request(
let res: SignedRequestResponse<DeviceToken> = send_signed_request(
None,
"https://xsts.auth.xboxlive.com/xsts/authorize",
"/xsts/authorize",
@ -699,7 +722,13 @@ async fn minecraft_token(token: DeviceToken) -> Result<MinecraftToken, Minecraft
.and_then(|x| x.get(0))
.and_then(|x| x.get("uhs"))
.and_then(|x| x.as_str().map(String::from))
.ok_or_else(|| MinecraftAuthError::HashError)?;
.ok_or_else(|| {
tracing::error!(
"[auth] minecraft_token() - failed to extract UHS from display_claims: {:?}",
token.display_claims
);
MinecraftAuthError::HashError
})?;
let token = token.token;
let res = auth_retry(|| {
@ -727,6 +756,15 @@ async fn minecraft_token(token: DeviceToken) -> Result<MinecraftToken, Minecraft
source,
})?;
if status.is_success() {
} else {
tracing::error!(
"[auth] minecraft_token() - non-success status={}, body={}",
status,
text
);
}
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
step: MinecraftAuthStep::MinecraftToken,
raw: text,
@ -765,6 +803,15 @@ async fn minecraft_profile(token: &str) -> Result<MinecraftProfile, MinecraftAut
source,
})?;
if status.is_success() {
} else {
tracing::error!(
"[auth] minecraft_profile() - non-success status={}, body={}",
status,
text
);
}
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
step: MinecraftAuthStep::MinecraftProfile,
raw: text,
@ -801,6 +848,15 @@ async fn minecraft_entitlements(token: &str) -> Result<MinecraftEntitlements, Mi
source,
})?;
if status.is_success() {
} else {
tracing::error!(
"[auth] minecraft_entitlements() - non-success status={}, body={}",
status,
text
);
}
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
step: MinecraftAuthStep::MinecraftEntitlements,
raw: text,
@ -871,9 +927,25 @@ struct SignedRequestResponse<T> {
pub body: T,
}
/// Xbox error response structure returned by Xbox Live APIs on authentication failure.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct XboxErrorResponse {
/// The Xbox error code (e.g., 2148916233 for no Xbox profile).
#[serde(rename = "XErr")]
pub x_err: u64,
/// A human-readable error message from Xbox.
#[serde(rename = "Message")]
pub message: String,
/// An optional redirect URL for resolving the error.
#[serde(rename = "Redirect")]
pub redirect: Option<String>,
}
#[allow(clippy::cast_possible_wrap)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::too_many_lines)]
async fn send_signed_request<T: serde::de::DeserializeOwned>(
authorization: Option<&str>,
url: &str,
@ -961,6 +1033,25 @@ async fn send_signed_request<T: serde::de::DeserializeOwned>(
.await
.map_err(|source| MinecraftAuthError::RequestError { step, source })?;
if !status.is_success() {
tracing::error!(
"[auth] send_signed_request() - step={:?}, non-success status={}, body={}",
step,
status,
body
);
// Try to parse as an Xbox error response (contains XErr code)
if let Ok(xbox_err) = serde_json::from_str::<XboxErrorResponse>(&body) {
return Err(MinecraftAuthError::XboxError {
step,
error_code: xbox_err.x_err,
message: xbox_err.message,
redirect: xbox_err.redirect,
});
}
}
let body =
serde_json::from_str(&body).map_err(|source| MinecraftAuthError::DeserializeError {
step,
@ -1061,4 +1152,11 @@ pub enum MinecraftAuthError {
HashError,
#[error("failed to read user xbox session ID")]
SessionIdError,
#[error("Minecraft authentication error: {error_code} during MSA step {step:?}")]
XboxError {
step: MinecraftAuthStep,
error_code: u64,
message: String,
redirect: Option<String>,
},
}