UNPKG

@xmcl/user

Version:

Minecraft user related functions, including Yggdrasil authenticator, player skin fetcher, and Mojang security API

709 lines (701 loc) 23.6 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // index.ts var user_exports = {}; __export(user_exports, { MicrosoftAuthenticator: () => MicrosoftAuthenticator, MojangClient: () => MojangClient, MojangError: () => MojangError, NameAvailability: () => NameAvailability, ProfileNotFoundError: () => ProfileNotFoundError, SetNameError: () => SetNameError, SetSkinError: () => SetSkinError, UnauthorizedError: () => UnauthorizedError, YggdrasilClient: () => YggdrasilClient, YggdrasilError: () => YggdrasilError, YggdrasilThirdPartyClient: () => YggdrasilThirdPartyClient, getOfflineUUID: () => getOfflineUUID, getTextureType: () => getTextureType, isTextureSlim: () => isTextureSlim, newToken: () => newToken, offline: () => offline }); module.exports = __toCommonJS(user_exports); // offline.ts var import_uuid = require("uuid"); // ../user-offline-uuid/index.ts var import_crypto = require("crypto"); function getOfflineUUID(username) { const md5Bytes = (0, import_crypto.createHash)("md5").update(`OfflinePlayer:${username}`).digest(); md5Bytes[6] &= 15; md5Bytes[6] |= 48; md5Bytes[8] &= 63; md5Bytes[8] |= 128; return md5Bytes.toString("hex").replace(/(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/, "$1-$2-$3-$4-$5"); } // offline.ts function newToken() { return (0, import_uuid.v4)().replace(/-/g, ""); } function offline(username, uuid) { const id = uuid || getOfflineUUID(username); const prof = { id, name: username }; return { accessToken: newToken(), clientToken: newToken(), selectedProfile: prof, availableProfiles: [prof], user: { id, username } }; } // mojang.ts var import_undici = require("undici"); var NameAvailability = /* @__PURE__ */ ((NameAvailability2) => { NameAvailability2["DUPLICATE"] = "DUPLICATE"; NameAvailability2["AVAILABLE"] = "AVAILABLE"; NameAvailability2["NOT_ALLOWED"] = "NOT_ALLOWED"; return NameAvailability2; })(NameAvailability || {}); var SetNameError = class extends Error { path; errorType; error; details; errorMessage; developerMessage; constructor(message, err) { super(message); this.name = "SetNameError"; this.path = err.path; this.errorType = err.errorType; this.error = err.error; this.details = err.details; this.errorMessage = err.errorMessage; this.developerMessage = err.developerMessage; } }; var SetSkinError = class extends Error { path; errorType; error; details; errorMessage; developerMessage; constructor(message, err) { super(message); this.name = "SetSkinError"; this.path = err.path; this.errorType = err.errorType; this.error = err.error; this.details = err.details; this.errorMessage = err.errorMessage; this.developerMessage = err.developerMessage; } }; var MojangError = class extends Error { path; errorMessage; developerMessage; constructor(err) { super(err.errorMessage); this.path = err.path; this.errorMessage = err.errorMessage; this.developerMessage = err.developerMessage; Object.assign(this, err); } }; var UnauthorizedError = class extends MojangError { name = "UnauthorizedError"; constructor(err) { super(err); } }; var ProfileNotFoundError = class extends MojangError { name = "ProfileNotFoundError"; constructor(err) { super(err); } }; var MojangClient = class { fetch; constructor(options) { this.fetch = (options == null ? void 0 : options.fetch) || import_undici.fetch; } async setName(name, token, signal) { const resp = await this.fetch(`https://api.minecraftservices.com/minecraft/profile/name/${name}`, { method: "PUT", headers: { Authorization: `Bearer ${token}` }, signal }); switch (resp.status) { case 200: return await resp.json(); case 400: throw new SetNameError("Name is unavailable (Either taken or has not become available)", await resp.json()); case 403: throw new SetNameError("Name is unavailable (Either taken or has not become available)", await resp.json()); case 401: throw new SetNameError("Unauthorized (Bearer token expired or is not correct)", await resp.json()); case 429: throw new SetNameError("Too many requests sent", await resp.json()); case 500: throw new SetNameError("Timed out (API lagged out and could not respond)", await resp.json()); } throw new SetNameError("Unknown error", await resp.json()); } async getNameChangeInformation(token) { const resp = await this.fetch("https://api.minecraftservices.com/minecraft/profile/namechange", { method: "GET", headers: { Authorization: `Bearer ${token}` } }); return await resp.json(); } async checkNameAvailability(name, token, signal) { const resp = await this.fetch(`https://api.minecraftservices.com/minecraft/profile/name/${name}/available`, { method: "GET", headers: { Authorization: `Bearer ${token}` }, signal }); const result = await resp.json(); return result.status; } async getProfile(token, signal) { var _a; const resp = await this.fetch("https://api.minecraftservices.com/minecraft/profile", { method: "GET", headers: { Authorization: `Bearer ${token}` }, signal }); if (((_a = resp.headers.get("content-type")) == null ? void 0 : _a.toLocaleLowerCase()) !== "application/json") { throw new Error(await resp.text()); } const json = await resp.json(); if (resp.ok) { return json; } else if (json.error === "NOT_FOUND") { throw new ProfileNotFoundError(json); } else if (resp.status === 401) { throw new UnauthorizedError(json); } throw Object.assign(new Error("Unknown Error"), json); } async setSkin(fileName, skin, variant, token, signal) { const body = typeof skin === "string" ? JSON.stringify({ url: skin, variant }) : getSkinFormData(skin, fileName, variant); const headers = { Authorization: `Bearer ${token}` }; if (typeof body === "string") { headers["Content-Type"] = "application/json"; } const resp = await this.fetch("https://api.minecraftservices.com/minecraft/profile/skins", { method: "POST", headers, body, signal }); const profileResponse = await resp.json(); if (resp.status === 401) { throw new UnauthorizedError(await resp.json()); } if ("error" in profileResponse || "errorMessage" in profileResponse) { throw new SetSkinError(`Fail to set skin ${profileResponse.errorMessage}`, profileResponse); } return profileResponse; } async resetSkin(token, signal) { const resp = await this.fetch("https://api.minecraftservices.com/minecraft/profile/skins/active", { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, signal }); if (resp.status === 401) { throw new UnauthorizedError(await resp.json()); } } async hideCape(token, signal) { const resp = await this.fetch("https://api.minecraftservices.com/minecraft/profile/capes/active", { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, signal }); if (resp.status === 401) { throw new UnauthorizedError(await resp.json()); } } async showCape(capeId, token, signal) { const resp = await this.fetch("https://api.minecraftservices.com/minecraft/profile/capes/active", { method: "PUT", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, body: JSON.stringify({ capeId }), signal }); if (resp.status === 401) { throw new UnauthorizedError(await resp.json()); } if (resp.status === 400) { throw new Error(); } const profile = await resp.json(); return profile; } async verifySecurityLocation(token, signal) { const resp = await this.fetch("https://api.mojang.com/user/security/location", { method: "GET", headers: { Authorization: `Bearer ${token}` }, signal }); if (resp.status === 204) { return true; } return false; } async getSecurityChallenges(token) { const resp = await this.fetch("https://api.mojang.com/user/security/challenges", { method: "GET", headers: { Authorization: `Bearer ${token}` } }); if (resp.status === 401) { throw new UnauthorizedError(await resp.json()); } return await resp.json(); } async submitSecurityChallenges(answers, token) { const resp = await this.fetch("https://api.mojang.com/user/security/location", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, body: JSON.stringify(answers) }); if (resp.status === 204) { return; } if (resp.status === 401) { throw new UnauthorizedError(await resp.json()); } throw new Error(); } /** * Return the owner ship list of the player with those token. */ async checkGameOwnership(token, signal) { var _a; const mcResponse = await this.fetch("https://api.minecraftservices.com/entitlements/mcstore", { headers: { Authorization: `Bearer ${token}` }, signal }); if (mcResponse.status === 401) { throw new UnauthorizedError(await mcResponse.text()); } if (!mcResponse.ok || ((_a = mcResponse.headers.get("content-type")) == null ? void 0 : _a.toLocaleLowerCase()) !== "application/json") { throw new Error(await mcResponse.text()); } const result = await mcResponse.json(); return result; } }; function getSkinFormData(buf, fileName, variant) { const form = new import_undici.FormData(); form.append("variant", variant); const file = new File([buf], fileName, { type: "image/png" }); form.append("file", file); return form; } // yggdrasil.ts var YggdrasilError = class extends Error { constructor(statusCode, message, o) { super(message); this.statusCode = statusCode; this.name = "YggdrasilError"; this.error = o == null ? void 0 : o.error; this.errorMessage = o == null ? void 0 : o.errorMessage; this.cause = o == null ? void 0 : o.cause; } error; errorMessage; cause; }; var YggdrasilClient = class { /** * Create client for official-like api endpoint * @param api The official-like api endpoint */ constructor(api, options) { this.api = api; this.headers = (options == null ? void 0 : options.headers) ?? {}; this.fetch = (options == null ? void 0 : options.fetch) || fetch; this.FormData = (options == null ? void 0 : options.FormData) || FormData; this.File = (options == null ? void 0 : options.File) || File; } headers; fetch; FormData; File; async validate(accessToken, clientToken, signal) { const response = await this.fetch(this.api + "/validate", { method: "POST", body: JSON.stringify({ accessToken, clientToken }), headers: { ...this.headers, "content-type": "application/json; charset=utf-8" }, signal }); return response.ok; } async invalidate(accessToken, clientToken, signal) { return await this.fetch(this.api + "/invalidate", { method: "POST", body: JSON.stringify({ accessToken, clientToken }), headers: { ...this.headers, "content-type": "application/json; charset=utf-8" }, signal }).then((s) => s.ok); } async login({ username, password, clientToken, requestUser }, signal) { var _a; const response = await this.fetch(this.api + "/authenticate", { method: "POST", body: JSON.stringify({ agent: { name: "Minecraft", version: 1 }, requestUser: typeof requestUser === "boolean" ? requestUser : false, username, password, clientToken }), headers: { ...this.headers, "content-type": "application/json; charset=utf-8" }, signal }); if (response.status >= 400) { const body = await response.text(); throw new YggdrasilError(response.status, response.status + ":" + body, ((_a = response.headers.get("content-type")) == null ? void 0 : _a.startsWith("application/json")) ? JSON.parse(body) : void 0); } const authentication = await response.json(); return authentication; } async refresh({ accessToken, requestUser, clientToken }, signal) { var _a; const response = await this.fetch(this.api + "/refresh", { method: "POST", body: JSON.stringify({ accessToken, clientToken, requestUser: typeof requestUser === "boolean" ? requestUser : false }), headers: { ...this.headers, "content-type": "application/json; charset=utf-8" }, signal }); if (response.status >= 400) { const body = await response.text(); throw new YggdrasilError(response.status, response.status + ":" + body, ((_a = response.headers.get("content-type")) == null ? void 0 : _a.startsWith("application/json")) ? JSON.parse(body) : void 0); } const authentication = await response.json(); return authentication; } }; function isTextureSlim(o) { return o.metadata ? o.metadata.model === "slim" : false; } function getTextureType(o) { return isTextureSlim(o) ? "slim" : "steve"; } var YggdrasilThirdPartyClient = class extends YggdrasilClient { profileApi; textureApi; /** * Create thirdparty (authlib-injector) style client * @param api The api url following https://github.com/yushijinhun/authlib-injector/wiki/Yggdrasil-%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83 * @param clientToken * @param dispatcher */ constructor(api, options) { super(api + "/authserver", options); this.profileApi = api + "/sessionserver/session/minecraft/profile/${uuid}"; this.textureApi = api + "/api/user/profile/${uuid}/${type}"; } async lookup(uuid, unsigned = true, signal) { var _a; const url = new URL(this.profileApi.replace("${uuid}", uuid)); url.searchParams.append("unsigned", unsigned ? "true" : "false"); const response = await this.fetch(url.toString(), { method: "GET", headers: this.headers, signal }); if (response.status !== 200) { const body = await response.text(); throw new YggdrasilError(response.status, response.status + ":" + body, ((_a = response.headers.get("content-type")) == null ? void 0 : _a.startsWith("application/json")) ? JSON.parse(body) : void 0); } const o = await response.json(); if (o.properties && o.properties instanceof Array) { const properties = o.properties; const to = {}; for (const prop of properties) { to[prop.name] = prop.value; } o.properties = to; } return o; } async setTexture(options, signal) { var _a, _b; const url = new URL(this.textureApi.replace("${uuid}", options.uuid).replace("${type}", options.type)); const requestOptions = { headers: { ...this.headers, Authorization: `Bearer ${options.accessToken}` }, signal }; if (!options.texture) { requestOptions.method = "DELETE"; } else if ("data" in options.texture) { requestOptions.method = "PUT"; const form = new this.FormData(); form.append("model", ((_a = options.texture.metadata) == null ? void 0 : _a.model) || "steve"); form.append("file", new this.File([options.texture.data], "Steve.png", { type: "image/png" })); requestOptions.body = form; } else if ("url" in options.texture) { requestOptions.method = "POST"; url.searchParams.append("model", ((_b = options.texture.metadata) == null ? void 0 : _b.model) || ""); url.searchParams.append("url", options.texture.url); } else { throw new TypeError("Illegal Option Format!"); } const response = await this.fetch(url.toString(), requestOptions); if (response.status === 401) { if (response.headers.get("content-type") === "application/json") { const body = await response.json(); throw new YggdrasilError(response.status, response.status.toString(), { error: body.error ?? "Unauthorized", errorMessage: body.errorMessage ?? "Unauthorized" }); } else { const body = await response.text(); throw new YggdrasilError(response.status, response.status + ":" + body, { error: "Unauthorized", errorMessage: "Unauthorized: " + body }); } } if (response.status >= 400) { const body = await response.text(); throw new YggdrasilError(response.status, response.status + ":" + body, { error: "SetSkinFailed", errorMessage: "Fail to set skin " + body }); } } }; // microsoft.ts var MicrosoftAuthenticator = class { fetch; constructor(options) { this.fetch = options.fetch || fetch; } /** * Authenticate with xbox live by ms oauth access token * @param oauthAccessToken The oauth access token */ async authenticateXboxLive(oauthAccessToken, signal) { const xblResponse = await this.fetch("https://user.auth.xboxlive.com/user/authenticate", { method: "POST", body: JSON.stringify({ Properties: { AuthMethod: "RPS", SiteName: "user.auth.xboxlive.com", RpsTicket: `d=${oauthAccessToken}` }, RelyingParty: "http://auth.xboxlive.com", TokenType: "JWT" }), headers: { "Content-Type": "application/json" }, signal }); if (xblResponse.status !== 200) { throw new Error(`Failed to authenticate with xbox live, status code: ${xblResponse.status}: ${await xblResponse.text()}}`); } const result = await xblResponse.json(); return result; } /** * Authorize the xbox live. It will get the xsts token in response. * @param xblResponseToken The {@link XBoxResponse.Token} */ async authorizeXboxLive(xblResponseToken, relyingParty = "rp://api.minecraftservices.com/", signal) { const xstsResponse = await this.fetch("https://xsts.auth.xboxlive.com/xsts/authorize", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ Properties: { SandboxId: "RETAIL", UserTokens: [xblResponseToken] }, RelyingParty: relyingParty, TokenType: "JWT" }), signal }); if (xstsResponse.status !== 200) { const errText = await xstsResponse.text(); let errObj = {}; try { errObj = JSON.parse(errText); } catch (e) { } throw Object.assign(new Error(`Failed to authorize with xbox live, status code: ${xstsResponse.status}: ${errText}}`), errObj); } const result = await xstsResponse.json(); return result; } /** * Get xbox user profile, including **username** and **avatar**. * * You can find the parameters from the {@link XBoxResponse}. * * @param xuid The `xuid` in a {@link XBoxResponse.DisplayClaims} * @param uhs The `uhs` in a {@link XBoxResponse.DisplayClaims} * @param xstsToken The {@link XBoxResponse.Token} * @returns The user game profile. */ async getXboxGameProfile(xuid, uhs, xstsToken, signal) { const url = new URL(`https://profile.xboxlive.com/users/xuid(${xuid})/profile/settings`); url.searchParams.append("settings", ["PublicGamerpic", "Gamertag"].join(",")); const response = await this.fetch(url.toString(), { headers: { "x-xbl-contract-version": "2", "content-type": "application/json", Authorization: `XBL3.0 x=${uhs};${xstsToken}` }, signal }); if (response.status !== 200) { throw new Error(`Failed to get xbox game profile, status code: ${response.status}: ${await response.text()}}`); } const result = await response.json(); return result; } /** * Acquire both Minecraft and xbox token and xbox game profile. * You can use the xbox token to login Minecraft by {@link loginMinecraftWithXBox}. * * This method is the composition of calling * - {@link authenticateXboxLive} * - {@link authorizeXboxLive} to `rp://api.minecraftservices.com/` * - {@link authorizeXboxLive} to `http://xboxlive.com` * - {@link getXboxGameProfile} * * You can call them individually if you want a more detailed control. * * @param oauthAccessToken The microsoft access token * @param signal The abort signal * @returns The object contain xstsResponse (minecraft xbox token) and xbox game profile */ async acquireXBoxToken(oauthAccessToken, signal) { const xblResponse = await this.authenticateXboxLive(oauthAccessToken, signal); const minecraftXstsResponse = await this.authorizeXboxLive(xblResponse.Token, "rp://api.minecraftservices.com/", signal); const xstsResponse = await this.authorizeXboxLive(xblResponse.Token, "http://xboxlive.com", signal); return { minecraftXstsResponse, liveXstsResponse: xstsResponse }; } /** * This will return the response with Minecraft access token! * * This access token allows us to launch the game, but, we haven't actually checked if the account owns the game. Everything until here works with a normal Microsoft account! * * @param uhs uhs from {@link XBoxResponse} of {@link acquireXBoxToken} * @param xstsToken You need to get this token from {@link acquireXBoxToken} */ async loginMinecraftWithXBox(uhs, xstsToken, signal) { const mcResponse = await this.fetch("https://api.minecraftservices.com/authentication/login_with_xbox", { method: "POST", body: JSON.stringify({ identityToken: `XBL3.0 x=${uhs};${xstsToken}` }), headers: { "content-type": "application/json" }, signal }); if (mcResponse.status !== 200) { throw new Error(`Failed to login minecraft with xbox, status code: ${mcResponse.status}: ${await mcResponse.text()}}`); } const result = await mcResponse.json(); return result; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { MicrosoftAuthenticator, MojangClient, MojangError, NameAvailability, ProfileNotFoundError, SetNameError, SetSkinError, UnauthorizedError, YggdrasilClient, YggdrasilError, YggdrasilThirdPartyClient, getOfflineUUID, getTextureType, isTextureSlim, newToken, offline }); //# sourceMappingURL=index.js.map