UNPKG

mc-java-core-333

Version:

A better library starting minecraft game NW.js and Electron.js

420 lines 17.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); /** * This code is distributed under the CC-BY-NC 4.0 license: * https://creativecommons.org/licenses/by-nc/4.0/ * * Original author: Luuxis * Fork author: Benjas333 */ const node_buffer_1 = require("node:buffer"); const crypto_1 = __importDefault(require("crypto")); /** * Utility function to fetch and convert an image to base64. * @param url A URL to an image. * @returns A string of the image in base64 format. */ async function getBase64(url) { const response = await fetch(url); const arrayBuffer = await response.arrayBuffer(); const buffer = node_buffer_1.Buffer.from(arrayBuffer); return buffer.toString('base64'); } /** * Utility function to convert known xsts error codes to human-readable strings. * @param errorCode An error code returned by the xsts endpoint. * @returns A string with the error description. */ async function knownTokenErrors(errorCode) { switch (errorCode) { case 2148916227: return 'ENFORCEMENT_BAN'; case 2148916229: return 'ACCOUNT_PARENTALLY_RESTRICTED'; case 2148916233: return 'The user does not currently have an Xbox profile - https://signup.live.com/signup - ACCOUNT_CREATION_REQUIRED'; case 2148916234: return 'ACCOUNT_TERMS_OF_USE_NOT_ACCEPTED'; case 2148916235: return 'ACCOUNT_COUNTRY_NOT_AUTHORIZED'; case 2148916236: return 'ACCOUNT_AGE_VERIFICATION_REQUIRED'; case 2148916237: return 'ACCOUNT_UNDER_CURFEW'; case 2148916238: return 'The account date of birth is under 18 years and cannot proceed unless the account is added to a Family by an adult - ACCOUNT_CHILD_NOT_IN_FAMILY'; // User is under 18 case 2148916239: return 'ACCOUNT_CSV_TRANSITION_REQUIRED'; case 2148916240: return 'ACCOUNT_MAINTENANCE_REQUIRED'; case 2148916243: return 'ACCOUNT_NAME_CHANGE_REQUIRED'; case 2148916242: return 'CONTENT_ISOLATION (Verify SCID / Sandbox)'; case 2148916255: return 'EXPIRED_SERVICE_TOKEN'; case 2148916258: return 'EXPIRED_USER_TOKEN'; case 2148916257: return 'EXPIRED_TITLE_TOKEN'; case 2148916256: return 'EXPIRED_DEVICE_TOKEN'; case 2148916259: return 'INVALID_DEVICE_TOKEN'; case 2148916260: return 'INVALID_TITLE_TOKEN'; case 2148916261: return 'INVALID_USER_TOKEN'; default: return `Unknown error code (${errorCode})`; } } class Microsoft { /** * Creates a Microsoft auth instance. * @param client_id Your Microsoft OAuth client ID (default: '00000000402b5328' if none provided). * @param doIncludeXboxAccount Whether to include the Xbox account data (xuid, gamertag, and ageGroup) in the response (default: true). */ constructor(client_id, doIncludeXboxAccount = true) { this.client_id = client_id || '00000000402b5328'; this.doIncludeXboxAccount = doIncludeXboxAccount; if (!!process?.versions?.electron) { this.type = 'electron'; } else if (!!process?.versions?.nw) { this.type = 'nwjs'; } else { this.type = 'terminal'; } } /** * Opens a GUI (Electron or NW.js) or uses terminal approach to fetch an OAuth2 code, * and then retrieves user information from Microsoft if successful. * @param type The environment to open the OAuth window. Defaults to the auto-detected type. * @param url The full OAuth2 authorization URL. If not provided, a default is used. * @param doRemoveCookies Whether to remove login cookies before opening the OAuth window (default: true). * @returns An object with user data on success, or false if canceled. */ async getAuth(type, url, doRemoveCookies = true) { url = url || `https://login.live.com/oauth20_authorize.srf?client_id=${this.client_id}&response_type=code&redirect_uri=https://login.live.com/oauth20_desktop.srf&scope=XboxLive.signin%20offline_access&cobrandid=8058f65d-ce06-4c30-9559-473c9275a65d&prompt=select_account`; type = type || this.type; // Dynamically require different GUI modules depending on environment let usercode; switch (type) { case "electron": usercode = await (require('./GUI/Electron.js'))(url, doRemoveCookies); break; case "nwjs": usercode = await (require('./GUI/NW.js'))(url, doRemoveCookies); break; case "terminal": usercode = await (require('./GUI/Terminal.js'))(url); break; default: break; } // Exchange the code for an OAuth2 token, then retrieve account data return !usercode || usercode === "cancel" ? false : await this.exchangeCodeForToken(usercode); } /** * Exchanges an OAuth2 authorization code for an access token, then retrieves account information. * @param code The OAuth2 authorization code returned by Microsoft. * @returns The authenticated user data or an error object. */ async exchangeCodeForToken(code) { try { const response = await fetch('https://login.live.com/oauth20_token.srf', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `client_id=${this.client_id}&code=${code}&grant_type=authorization_code&redirect_uri=https://login.live.com/oauth20_desktop.srf` }); const oauth2 = await response.json(); if (oauth2.error) return { error: oauth2.error, errorType: 'oauth2', ...oauth2 }; return await this.getAccount(oauth2); } catch (error) { return { error: error.message || error, errorType: 'network' }; } } /** * Refreshes the user's session if the token has expired or is about to expire. * Otherwise, simply fetches the user's profile. * * @param acc A previously obtained AuthResponse object. * @returns Updated AuthResponse (with new token if needed) or an error object. */ async refresh(acc) { const timeStamp = Math.floor(Date.now() / 1000); // If the token is still valid for at least 2 more hours, just re-fetch the profile if (timeStamp < (acc?.meta?.access_token_expires_in - 7200)) { const profile = await this.getProfile(acc); if ('error' in profile) { // If there's an error, return it directly return profile; } acc.profile = { skins: profile.skins, capes: profile.capes }; return acc; } // Otherwise, refresh the token try { const response = await fetch('https://login.live.com/oauth20_token.srf', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `grant_type=refresh_token&client_id=${this.client_id}&refresh_token=${acc.refresh_token}` }); const oauth2 = await response.json(); if (oauth2.error) { return { error: oauth2.error, errorType: 'oauth2 refresh', ...oauth2 }; } // Retrieve account data with the new tokens return this.getAccount(oauth2); } catch (err) { return { error: err.message || err, errorType: 'network' }; } } /** * Retrieves and assembles the full account details (Xbox Live, XSTS, Minecraft). * @param oauth2 The token object returned by the Microsoft OAuth endpoint. * @returns A fully populated AuthResponse object or an error. */ async getAccount(oauth2, doIncludeXboxAccount = this.doIncludeXboxAccount) { // 1. Authenticate with Xbox Live const xbox_live = await this.fetchJSON("https://user.auth.xboxlive.com/user/authenticate", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ Properties: { AuthMethod: "RPS", SiteName: "user.auth.xboxlive.com", RpsTicket: "d=" + oauth2.access_token }, RelyingParty: "http://auth.xboxlive.com", TokenType: "JWT" }), }); if (xbox_live.error) return { ...xbox_live, errorType: "Xbox Live Authentication" }; // 2. Authorize with XSTS for Minecraft services const xsts = await this.fetchJSON("https://xsts.auth.xboxlive.com/xsts/authorize", { method: "POST", headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ Properties: { SandboxId: "RETAIL", UserTokens: [xbox_live.Token] }, RelyingParty: "rp://api.minecraftservices.com/", TokenType: "JWT" }) }); if (xsts.error || xsts.XErr) return { ...xsts, error: xsts.error || xsts.XErr, errorType: "xsts - Minecraft API", errorMessage: xsts.XErr ? await knownTokenErrors(xsts.XErr) : 'No XErr code provided.' }; // 5. Login with Xbox token to get a Minecraft token const mcLogin = await this.fetchJSON("https://api.minecraftservices.com/authentication/login_with_xbox", { method: "POST", headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ identityToken: `XBL3.0 x=${xbox_live.DisplayClaims.xui[0].uhs};${xsts.Token}` }) }); if (mcLogin.error) return { ...mcLogin, errorType: "Minecraft Login" }; // 6. Check if the account has purchased Minecraft const hasGame = await this.fetchJSON("https://api.minecraftservices.com/entitlements/mcstore", { method: "GET", headers: { Authorization: `Bearer ${mcLogin.access_token}` } }); if (hasGame.error) return { ...hasGame, errorType: "Minecraft Entitlements" }; if (!hasGame.items?.find((i) => i.name === "product_minecraft" || i.name === "game_minecraft")) return { error: "You don't own the game", errorType: "game" }; // 7. Fetch the user profile (skins, capes, etc.) const profile = await this.getProfile(mcLogin); if ('error' in profile) return profile; let response = { access_token: mcLogin.access_token, client_token: crypto_1.default.randomBytes(16).toString('hex'), uuid: profile.id, name: profile.name, refresh_token: oauth2.refresh_token, user_properties: '{}', meta: { type: "Xbox", access_token_expires_in: mcLogin.expires_in + Math.floor(Date.now() / 1000), demo: false }, xboxAccount: null, profile: { skins: profile.skins, capes: profile.capes } }; if (doIncludeXboxAccount) { response.xboxAccount = await this.getXboxAccount(xbox_live.Token); } return response; } /** * Fetches the Minecraft profile (including skins and capes) for a given access token, * then converts each skin/cape URL to base64. * * @param mcLogin An object containing `access_token` to call the Minecraft profile API. * @returns The user's Minecraft profile or an error object. */ async getProfile(mcLogin) { const profile = await this.fetchJSON('https://api.minecraftservices.com/minecraft/profile', { method: 'GET', headers: { Authorization: `Bearer ${mcLogin.access_token}` } }); if (profile.error) return { ...profile, errorType: "Minecraft Profile" }; // Convert each skin and cape to base64 if (Array.isArray(profile.skins)) { for (const skin of profile.skins) { if (skin.url) skin.base64 = `data:image/png;base64,${await getBase64(skin.url)}`; } } if (Array.isArray(profile.capes)) { for (const cape of profile.capes) { if (cape.url) cape.base64 = `data:image/png;base64,${await getBase64(cape.url)}`; } } return { id: profile.id, name: profile.name, skins: profile.skins || [], capes: profile.capes || [] }; } /** * Retrieves the Xbox account details (xuid, gamertag, ageGroup) for a given access token. * @param accessToken An access token from the Microsoft OAuth endpoint. * @returns An object with the user's Xbox account details or an error object. */ async getXboxAccount(accessToken) { // 3. Authorize for the standard Xbox Live realm (useful for xuid/gamertag) let xboxAccount = await this.fetchJSON("https://xsts.auth.xboxlive.com/xsts/authorize", { method: "POST", headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ Properties: { SandboxId: "RETAIL", UserTokens: [accessToken] }, RelyingParty: "http://xboxlive.com", TokenType: "JWT" }) }); if (xboxAccount.error) return { ...xboxAccount, errorType: "Get Xbox Account" }; return { xuid: xboxAccount.DisplayClaims.xui[0].xid, gamertag: xboxAccount.DisplayClaims.xui[0].gtg, ageGroup: xboxAccount.DisplayClaims.xui[0].agg, }; } /** * A helper method to perform fetch and parse JSON. * @param url The endpoint URL. * @param options Fetch options (method, headers, body, etc.). * @param maxRetries Maximum number of retry attempts (default: 5). * @returns The parsed JSON or an object with an error field if something goes wrong. */ async fetchJSON(url, options, maxRetries = 5) { let attempt = 0; let last_error; while (attempt < maxRetries) { try { const response = await fetch(url, options); if (!response.ok && response.status !== 429) { const errorText = await response.text(); throw new Error(`HTTP error: ${response.status}: ${errorText}`); } if (response.status === 429) { if (attempt >= maxRetries) { const body = await response.text().catch(() => ''); throw new Error(`HTTP 429: rate limit exceeded after ${maxRetries} retries: ${body}`); } const retry_after = response.headers.get('Retry-After'); let delay_sec = retry_after ? parseInt(retry_after, 10) : Math.pow(2, attempt); if (isNaN(delay_sec)) { const then = new Date(retry_after).getTime(); if (then) { delay_sec = Math.max(0, Math.floor((then - Date.now()) / 1000)); } else { delay_sec = Math.pow(2, attempt); } } if (delay_sec <= 0) { delay_sec = Math.pow(2, attempt); } await new Promise(resolve => setTimeout(resolve, delay_sec * 1000)); attempt++; continue; } const content_type = response.headers.get('content-type') || ''; if (!content_type.includes('application/json')) { const body = await response.text(); throw new Error(`Unexpected response (${content_type}): ${body}`); } const data = await response.json(); return data; } catch (err) { last_error = err; if (attempt >= maxRetries || (err.message && !err.message.includes('429'))) break; attempt++; } } return { error: last_error.message || last_error }; } } exports.default = Microsoft; //# sourceMappingURL=Microsoft.js.map