@xmcl/user
Version:
Minecraft user related functions, including Yggdrasil authenticator, player skin fetcher, and Mojang security API
709 lines (701 loc) • 23.6 kB
JavaScript
;
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