diff --git a/apps/app-frontend/src/composables/skin-site-session.test.ts b/apps/app-frontend/src/composables/skin-site-session.test.ts index 7b29daf..ea341cd 100644 --- a/apps/app-frontend/src/composables/skin-site-session.test.ts +++ b/apps/app-frontend/src/composables/skin-site-session.test.ts @@ -605,3 +605,110 @@ test('skin updates are restricted to known non-Mojang players and validated resu setSkinSiteFrame(null) resetSkinSiteSession() }) + +test('hosted create auto-authenticates the first skin site player when no game account exists', async () => { + const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window') + const calls: Array<{ command: string; args: Record }> = [] + let accounts: Array<{ + account_type: string + yggdrasil?: { api_root: string } + }> = [] + const frame = { + postMessage(data: { type: string; requestId: string }) { + if (data.type === 'starlight-pack-token-request') { + receiveSkinSiteMessage( + { + origin: SKIN_SITE_ORIGIN, + source: frame, + data: { + type: 'starlight-pack-token-result', + requestId: data.requestId, + token: 'site.jwt.secret', + }, + } as MessageEvent, + frame, + ) + return + } + if (data.type === 'starlight-skin-players-request') { + receiveSkinSiteMessage( + { + origin: SKIN_SITE_ORIGIN, + source: frame, + data: { + type: 'starlight-skin-players-result', + requestId: data.requestId, + ok: true, + players: [ + { + uuid: '11111111-1111-1111-1111-111111111111', + name: 'AutoPlayer', + isMojang: false, + skinState: 'empty', + }, + ], + }, + } as MessageEvent, + frame, + ) + } + }, + } as unknown as Window + Object.defineProperty(globalThis, 'window', { + configurable: true, + value: { + __TAURI_INTERNALS__: { + async invoke(command: string, args: Record) { + calls.push({ command, args }) + if (command === 'plugin:install|hosted_has_game_account') { + return accounts.some( + (account) => + account.account_type === 'microsoft' || + (account.account_type === 'yggdrasil' && + account.yggdrasil?.api_root.replace(/\/+$/, '') === + 'https://skin.starlight.cool/yggdrasil'), + ) + } + if (command === 'plugin:auth|login_skin_site_player') { + accounts = [ + { + account_type: 'yggdrasil', + yggdrasil: { api_root: 'https://skin.starlight.cool/yggdrasil' }, + }, + ] + return null + } + if (command === 'plugin:instance|instance_get') return null + return command === 'plugin:install|hosted_create' ? 'auto-instance' : null + }, + }, + }, + }) + try { + resetSkinSiteSession() + setSkinSiteFrame(frame) + receiveSkinSiteMessage( + { + origin: SKIN_SITE_ORIGIN, + source: frame, + data: { + type: 'starlight-skin-session', + status: 'signed-in', + user: { uuid: 'site-user', username: 'Site' }, + }, + } as MessageEvent, + frame, + ) + await requestSkinSitePlayers().catch(() => {}) + assert.equal(await hostedCreate(), 'auto-instance') + assert.ok( + calls.some(({ command }) => command === 'plugin:auth|login_skin_site_player'), + 'expected automatic player authentication', + ) + } finally { + resetSkinSiteSession() + setSkinSiteFrame(null) + if (previousWindow) Object.defineProperty(globalThis, 'window', previousWindow) + else delete (globalThis as { window?: unknown }).window + } +}) diff --git a/apps/app-frontend/src/helpers/hosted-packs.ts b/apps/app-frontend/src/helpers/hosted-packs.ts index 7519a04..9904d00 100644 --- a/apps/app-frontend/src/helpers/hosted-packs.ts +++ b/apps/app-frontend/src/helpers/hosted-packs.ts @@ -2,10 +2,13 @@ import { invoke } from '@tauri-apps/api/core' import { requestSkinSiteDownloadToken, + requestSkinSitePlayers, + skinSitePlayers, skinSiteStatus, skinSiteUser, waitForSkinSiteSession, } from '../composables/skin-site-session.ts' +import { authenticateInstancePlayer } from './instance-player.ts' export const HOSTED_ACCOUNT_REQUIRED_MESSAGE = '您需要登录正版或皮肤站账号后才能操作' @@ -24,10 +27,36 @@ function startHostedPackAttempt(instanceId: string) { } export async function requireHostedGameAccount(): Promise { - const available = await invokeHosted('plugin:install|hosted_has_game_account') - if (!available) { - throw new Error(HOSTED_ACCOUNT_REQUIRED_MESSAGE) + if (await invokeHosted('plugin:install|hosted_has_game_account')) return + + // No game credential yet, but a signed-in skin site already lists the + // user's players. Authenticate the first one automatically so the user + // does not have to pick a player manually before creating an instance. + await waitForSkinSiteSession() + if (skinSiteStatus.value === 'signed-in' && skinSiteUser.value) { + if (skinSitePlayers.value.length === 0) { + await requestSkinSitePlayers().catch(() => {}) + } + const player = + skinSitePlayers.value.find((candidate) => !candidate.isMojang) ?? skinSitePlayers.value[0] + if (player) { + try { + await authenticateInstancePlayer({ + id: player.uuid, + name: player.name, + account_type: 'yggdrasil', + skin_site_user: skinSiteUser.value.uuid, + }) + } catch { + // Fall through to the login-required error below. + } + if (await invokeHosted('plugin:install|hosted_has_game_account')) { + return + } + } } + + throw new Error(HOSTED_ACCOUNT_REQUIRED_MESSAGE) } export function clearHostedSession(): Promise {