baltica 0.1.20 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,29 +51,57 @@ const client = new Client({
51
51
  port: 19132,
52
52
  });
53
53
 
54
- // Connect and get server info
55
54
  await client.connect();
55
+ ```
56
+
57
+ ### Email/Password Authentication
58
+
59
+ You can authenticate directly with your Microsoft account using email and password:
56
60
 
57
- // Listen for chat messages
58
- client.on("TextPacket", (packet) => {
59
- console.log(`Got message: ${packet.message}`);
61
+ ```typescript
62
+ const client = new Client({
63
+ address: "play.server.com",
64
+ port: 19132,
65
+ email: "your-email@outlook.com",
66
+ password: "your-password",
60
67
  });
61
68
 
62
- // Send a friendly greeting when connected
63
- client.on("connect", () => {
64
- const packet = new TextPacket();
65
- packet.message = 'Hey everyone! 👋';
66
- packet.needsTranslation = false;
67
- packet.parameters = [];
68
- packet.platformChatId = '';
69
- packet.source = client.username;
70
- packet.type = TextPacketType.Chat;
71
- packet.xuid = client.profile.xuid.toString();
72
- packet.filtered = '';
73
- client.send(packet.serialize());
69
+ await client.connect();
70
+ ```
71
+
72
+ **Important Notes:**
73
+ - This method only works with accounts that have 2FA (Two-Factor Authentication) **disabled**
74
+ - The account must have an Xbox profile and own Minecraft Bedrock Edition or launched minecraft at least once.
75
+ - User tokens are cached for ~14 days to minimize login requests (BETA)
76
+ - Tokens are stored in the `tokens` folder by default
77
+
78
+ ### Using a Proxy
79
+
80
+ Baltica supports SOCKS5 proxies for client connections:
81
+
82
+ ```typescript
83
+ const client = new Client({
84
+ address: "play.server.com",
85
+ port: 19132,
86
+ email: "your-email@outlook.com",
87
+ password: "your-password",
88
+ proxy: {
89
+ host: "proxy.example.com",
90
+ port: 1080,
91
+ userId: "proxy-username", // Optional
92
+ password: "proxy-password", // Optional
93
+ },
94
+ skinFile: `./skins/skin.png`, // If you want to load a skin from a png file
74
95
  });
96
+
97
+ await client.connect();
75
98
  ```
76
99
 
100
+ This is useful for:
101
+ - Bypassing IP restrictions
102
+ - Testing from different geographic locations
103
+ - Managing multiple bot connections
104
+
77
105
  ### Server Usage
78
106
 
79
107
  Want to create your own Minecraft server? We got you:
@@ -57,6 +57,8 @@ class Client extends shared_1.Emitter {
57
57
  this.packetCompressor = new shared_1.PacketCompressor(this);
58
58
  this.handleGamePackets();
59
59
  this.raknet.on("encapsulated", this.handleEncapsulated.bind(this));
60
+ // Wait for authentication to complete before sending network settings request
61
+ await this.waitForSessionReady();
60
62
  const request = new protocol_1.RequestNetworkSettingsPacket();
61
63
  request.protocol = types_1.ProtocolList[types_1.CurrentVersionConst];
62
64
  this.send(request);
@@ -1,3 +1,4 @@
1
1
  export * from "./worker";
2
2
  export * from "./types";
3
3
  export * from "./client";
4
+ export * from "./skin-loader";
@@ -17,3 +17,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./worker"), exports);
18
18
  __exportStar(require("./types"), exports);
19
19
  __exportStar(require("./client"), exports);
20
+ __exportStar(require("./skin-loader"), exports);
@@ -0,0 +1,38 @@
1
+ import type { SkinData } from "./types/payload";
2
+ /**
3
+ * Load a Minecraft Bedrock skin from a PNG file
4
+ * @param pngPath - Path to the PNG skin file (64x64 or 64x32)
5
+ * @returns SkinData object ready to use in client options
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { Client, loadSkinFromPNG } from 'baltica';
10
+ *
11
+ * const client = new Client({
12
+ * address: "play.example.com",
13
+ * port: 19132,
14
+ * skinData: loadSkinFromPNG('./my-skin.png')
15
+ * });
16
+ * ```
17
+ */
18
+ export declare function loadSkinFromPNG(pngPath: string): SkinData;
19
+ /**
20
+ * Load a Minecraft Bedrock skin from raw RGBA buffer
21
+ * @param buffer - Raw RGBA pixel data buffer
22
+ * @param width - Skin width (typically 64)
23
+ * @param height - Skin height (typically 64 or 32)
24
+ * @returns SkinData object ready to use in client options
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * import { Client, loadSkinFromBuffer } from 'baltica';
29
+ *
30
+ * const rgbaBuffer = Buffer.from([...]); // Your RGBA data
31
+ * const client = new Client({
32
+ * address: "play.example.com",
33
+ * port: 19132,
34
+ * skinData: loadSkinFromBuffer(rgbaBuffer, 64, 64)
35
+ * });
36
+ * ```
37
+ */
38
+ export declare function loadSkinFromBuffer(buffer: Buffer, width: number, height: number): SkinData;
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadSkinFromPNG = loadSkinFromPNG;
37
+ exports.loadSkinFromBuffer = loadSkinFromBuffer;
38
+ const fs = __importStar(require("node:fs"));
39
+ const pngjs_1 = require("pngjs");
40
+ /**
41
+ * Load a Minecraft Bedrock skin from a PNG file
42
+ * @param pngPath - Path to the PNG skin file (64x64 or 64x32)
43
+ * @returns SkinData object ready to use in client options
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * import { Client, loadSkinFromPNG } from 'baltica';
48
+ *
49
+ * const client = new Client({
50
+ * address: "play.example.com",
51
+ * port: 19132,
52
+ * skinData: loadSkinFromPNG('./my-skin.png')
53
+ * });
54
+ * ```
55
+ */
56
+ function loadSkinFromPNG(pngPath) {
57
+ // Read and parse PNG file
58
+ const pngData = fs.readFileSync(pngPath);
59
+ const png = pngjs_1.PNG.sync.read(pngData);
60
+ // Validate dimensions
61
+ if (png.width !== 64 || (png.height !== 64 && png.height !== 32)) {
62
+ throw new Error(`Invalid skin dimensions: ${png.width}x${png.height}. Expected 64x64 or 64x32`);
63
+ }
64
+ // Convert RGBA pixel data to base64
65
+ const base64Skin = png.data.toString("base64");
66
+ // Create skin resource patch for standard humanoid model
67
+ const resourcePatch = {
68
+ geometry: {
69
+ default: "geometry.humanoid.custom",
70
+ },
71
+ };
72
+ return {
73
+ AnimatedImageData: [],
74
+ ArmSize: "wide",
75
+ CapeData: "",
76
+ CapeId: "",
77
+ CapeImageHeight: 0,
78
+ CapeImageWidth: 0,
79
+ CapeOnClassicSkin: false,
80
+ PersonaPieces: [],
81
+ PersonaSkin: false,
82
+ PieceTintColors: [],
83
+ PremiumSkin: false,
84
+ SkinAnimationData: "",
85
+ SkinColor: "#0",
86
+ SkinData: base64Skin,
87
+ SkinGeometryData: "",
88
+ SkinGeometryDataEngineVersion: "",
89
+ SkinId: `custom_skin_${Date.now()}`,
90
+ SkinImageHeight: png.height,
91
+ SkinImageWidth: png.width,
92
+ SkinResourcePatch: Buffer.from(JSON.stringify(resourcePatch)).toString("base64"),
93
+ TrustedSkin: true,
94
+ };
95
+ }
96
+ /**
97
+ * Load a Minecraft Bedrock skin from raw RGBA buffer
98
+ * @param buffer - Raw RGBA pixel data buffer
99
+ * @param width - Skin width (typically 64)
100
+ * @param height - Skin height (typically 64 or 32)
101
+ * @returns SkinData object ready to use in client options
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * import { Client, loadSkinFromBuffer } from 'baltica';
106
+ *
107
+ * const rgbaBuffer = Buffer.from([...]); // Your RGBA data
108
+ * const client = new Client({
109
+ * address: "play.example.com",
110
+ * port: 19132,
111
+ * skinData: loadSkinFromBuffer(rgbaBuffer, 64, 64)
112
+ * });
113
+ * ```
114
+ */
115
+ function loadSkinFromBuffer(buffer, width, height) {
116
+ // Validate dimensions
117
+ if (width !== 64 || (height !== 64 && height !== 32)) {
118
+ throw new Error(`Invalid skin dimensions: ${width}x${height}. Expected 64x64 or 64x32`);
119
+ }
120
+ // Validate buffer size (RGBA = 4 bytes per pixel)
121
+ const expectedSize = width * height * 4;
122
+ if (buffer.length !== expectedSize) {
123
+ throw new Error(`Invalid buffer size: ${buffer.length} bytes. Expected ${expectedSize} bytes for ${width}x${height} RGBA`);
124
+ }
125
+ // Convert to base64
126
+ const base64Skin = buffer.toString("base64");
127
+ // Create skin resource patch
128
+ const resourcePatch = {
129
+ geometry: {
130
+ default: "geometry.humanoid.custom",
131
+ },
132
+ };
133
+ return {
134
+ AnimatedImageData: [],
135
+ ArmSize: "wide",
136
+ CapeData: "",
137
+ CapeId: "",
138
+ CapeImageHeight: 0,
139
+ CapeImageWidth: 0,
140
+ CapeOnClassicSkin: false,
141
+ PersonaPieces: [],
142
+ PersonaSkin: false,
143
+ PieceTintColors: [],
144
+ PremiumSkin: false,
145
+ SkinAnimationData: "",
146
+ SkinColor: "#0",
147
+ SkinData: base64Skin,
148
+ SkinGeometryData: "",
149
+ SkinGeometryDataEngineVersion: "",
150
+ SkinId: `custom_skin_${Date.now()}`,
151
+ SkinImageHeight: height,
152
+ SkinImageWidth: width,
153
+ SkinResourcePatch: Buffer.from(JSON.stringify(resourcePatch)).toString("base64"),
154
+ TrustedSkin: true,
155
+ };
156
+ }
@@ -42,6 +42,7 @@ const uuid_1345_1 = require("uuid-1345");
42
42
  const __1 = require("../");
43
43
  const types_1 = require("../../shared/types");
44
44
  const login_data_1 = require("./login-data");
45
+ const skin_loader_1 = require("../skin-loader");
45
46
  const PUBLIC_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp";
46
47
  const algorithm = "ES384";
47
48
  class ClientData {
@@ -52,6 +53,10 @@ class ClientData {
52
53
  this.client = client;
53
54
  this.payload = (0, __1.createDefaultPayload)(client);
54
55
  this.loginData = (0, login_data_1.prepareLoginData)();
56
+ // Load skin from file if skinFile is provided
57
+ if (client.options.skinFile && !client.options.skinData) {
58
+ client.options.skinData = (0, skin_loader_1.loadSkinFromPNG)(client.options.skinFile);
59
+ }
55
60
  }
56
61
  createLoginPacket() {
57
62
  const loginPacket = new protocol_1.LoginPacket();
@@ -68,7 +73,7 @@ class ClientData {
68
73
  return loginPacket;
69
74
  }
70
75
  async createClientChain(mojangKey, offline) {
71
- const { clientX509, ecdhKeyPair } = this.loginData;
76
+ const { clientX509, ecdhKeyPair, sessionTokenData } = this.loginData;
72
77
  let payload;
73
78
  let header;
74
79
  if (offline) {
@@ -96,6 +101,19 @@ class ClientData {
96
101
  identityPublicKey: mojangKey || PUBLIC_KEY,
97
102
  certificateAuthority: true,
98
103
  };
104
+ // Add session token data for PocketMine 1.21.100+ compatibility
105
+ if (sessionTokenData) {
106
+ payload.ipt = sessionTokenData.ipt;
107
+ payload.tid = sessionTokenData.tid;
108
+ payload.mid = sessionTokenData.mid;
109
+ payload.xid = sessionTokenData.xid;
110
+ payload.cpk = sessionTokenData.cpk;
111
+ payload.xname = this.client.profile.name;
112
+ }
113
+ // Add pfcd if available (PlayFab ID)
114
+ if (this.payload.pfcd) {
115
+ payload.pfcd = this.payload.pfcd;
116
+ }
99
117
  header = {
100
118
  alg: algorithm,
101
119
  x5u: clientX509,
@@ -33,6 +33,8 @@ export type ClientOptions = {
33
33
  tokensFolder: string;
34
34
  /** Skin Data for custom skins (By default we parse it from json)*/
35
35
  skinData: SkinData | undefined;
36
+ /** Path to PNG skin file (alternative to skinData) */
37
+ skinFile: string | undefined;
36
38
  /** LoginPacket data Customization */
37
39
  loginOptions: LoginPacketOptions;
38
40
  /** The View Distance of the client. */
@@ -18,6 +18,8 @@ exports.defaultClientOptions = {
18
18
  tokensFolder: "tokens",
19
19
  /** Default Value: undefined */
20
20
  skinData: undefined,
21
+ /** Default Value: undefined */
22
+ skinFile: undefined,
21
23
  loginOptions: {
22
24
  /** Default Value: Unknown */
23
25
  currentInputMode: protocol_1.InputMode.Unknown,
@@ -6,6 +6,13 @@ type LoginData = {
6
6
  clientX509: string;
7
7
  clientIdentityChain: string;
8
8
  clientUserChain: string;
9
+ sessionTokenData?: {
10
+ ipt: string;
11
+ tid: string;
12
+ mid: string;
13
+ xid: string;
14
+ cpk: string;
15
+ };
9
16
  };
10
17
  export declare const prepareLoginData: () => LoginData;
11
18
  export type { LoginData };
@@ -29,6 +29,7 @@ export type Payload = {
29
29
  PlatformOnlineId: string;
30
30
  PlatformType: number;
31
31
  PlayFabId: string;
32
+ pfcd?: string;
32
33
  PremiumSkin: boolean;
33
34
  SelfSignedId: string;
34
35
  ServerAddress: string;
@@ -6,7 +6,7 @@ const worker_1 = require("./worker");
6
6
  class WorkerClient extends raknet_1.EventEmitter {
7
7
  constructor(options) {
8
8
  super();
9
- this._options = { ...raknet_1.defaultClientOptions, ...options };
9
+ this._options = { ...(0, raknet_1.createDefaultClientOptions)(), ...options };
10
10
  this._worker = (0, worker_1.connect)(this._options);
11
11
  this.handleEvents();
12
12
  }
@@ -1,21 +1,43 @@
1
- import { live, xnet } from "@xboxreplay/xboxlive-auth";
2
- import type { AuthenticateResponse, Email } from "@xboxreplay/xboxlive-auth";
1
+ /**
2
+ * Xbox Live Authentication Module
3
+ *
4
+ * Based on the authentication flow from @xboxreplay/xboxlive-auth
5
+ * https://github.com/XboxReplay/xboxlive-auth
6
+ *
7
+ * Modified to support SOCKS5 proxies for all HTTP requests and Minecraft Authentication
8
+ */
3
9
  export interface BedrockTokens {
4
10
  chains: string[];
5
11
  xuid: string;
6
12
  gamertag: string;
7
13
  userHash: string;
14
+ xstsToken: string;
15
+ playfabXstsToken: string;
16
+ playfabUserHash: string;
17
+ }
18
+ export interface ProxyOptions {
19
+ host: string;
20
+ port: number;
21
+ userId?: string;
22
+ password?: string;
8
23
  }
9
24
  export interface AuthOptions {
10
25
  email: string;
11
26
  password: string;
12
27
  clientPublicKey: string;
13
28
  cacheDir?: string;
29
+ proxy?: ProxyOptions;
14
30
  }
15
31
  /**
16
32
  * Authenticates with Xbox Live using email/password and obtains Minecraft Bedrock tokens
17
- * Caches Xbox user token (~14 days valid) to minimize login requests
33
+ * Supports SOCKS5 proxy for all authentication requests
18
34
  */
19
35
  export declare function authenticateWithCredentials(options: AuthOptions): Promise<BedrockTokens>;
20
- export { live, xnet };
21
- export type { AuthenticateResponse, Email };
36
+ export type Email = string;
37
+ export interface AuthenticateResponse {
38
+ access_token: string;
39
+ token_type: string;
40
+ expires_in: number;
41
+ scope: string;
42
+ user_id: string;
43
+ }
@@ -1,4 +1,12 @@
1
1
  "use strict";
2
+ /**
3
+ * Xbox Live Authentication Module
4
+ *
5
+ * Based on the authentication flow from @xboxreplay/xboxlive-auth
6
+ * https://github.com/XboxReplay/xboxlive-auth
7
+ *
8
+ * Modified to support SOCKS5 proxies for all HTTP requests and Minecraft Authentication
9
+ */
2
10
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
11
  if (k2 === undefined) k2 = k;
4
12
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -33,15 +41,15 @@ var __importStar = (this && this.__importStar) || (function () {
33
41
  };
34
42
  })();
35
43
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.xnet = exports.live = void 0;
37
44
  exports.authenticateWithCredentials = authenticateWithCredentials;
38
- const xboxlive_auth_1 = require("@xboxreplay/xboxlive-auth");
39
- Object.defineProperty(exports, "live", { enumerable: true, get: function () { return xboxlive_auth_1.live; } });
40
- Object.defineProperty(exports, "xnet", { enumerable: true, get: function () { return xboxlive_auth_1.xnet; } });
41
45
  const fs = __importStar(require("node:fs"));
42
46
  const path = __importStar(require("node:path"));
43
47
  const raknet_1 = require("@sanctumterra/raknet");
48
+ const fetch_socks_1 = require("fetch-socks");
49
+ const undici_1 = require("undici");
44
50
  const MINECRAFT_BEDROCK_RELYING_PARTY = "https://multiplayer.minecraft.net/";
51
+ const PLAYFAB_RELYING_PARTY = "https://b980a380.minecraft.playfabapi.com/";
52
+ const XBOX_AUTH_CLIENT_ID = "00000000441cc96b";
45
53
  function hashString(str) {
46
54
  let hash = 0;
47
55
  for (let i = 0; i < str.length; i++) {
@@ -63,7 +71,6 @@ function loadCache(cacheFile) {
63
71
  try {
64
72
  if (fs.existsSync(cacheFile)) {
65
73
  const cached = JSON.parse(fs.readFileSync(cacheFile, "utf-8"));
66
- // Check if token is still valid (with 1 hour buffer)
67
74
  const expiresAt = new Date(cached.notAfter).getTime();
68
75
  if (Date.now() < expiresAt - 3600000) {
69
76
  return cached;
@@ -91,16 +98,47 @@ function saveCache(cacheFile, userToken, userHash, notAfter) {
91
98
  /* ignore */
92
99
  }
93
100
  }
101
+ function createProxiedFetch(proxy) {
102
+ if (!proxy) {
103
+ return fetch;
104
+ }
105
+ const dispatcher = (0, fetch_socks_1.socksDispatcher)({
106
+ type: 5,
107
+ host: proxy.host,
108
+ port: proxy.port,
109
+ userId: proxy.userId,
110
+ password: proxy.password,
111
+ });
112
+ return async (input, init) => {
113
+ const url = typeof input === "string" ? input : input.toString();
114
+ const response = await (0, undici_1.fetch)(url, {
115
+ ...init,
116
+ dispatcher,
117
+ });
118
+ return response;
119
+ };
120
+ }
94
121
  /**
95
122
  * Authenticates with Xbox Live using email/password and obtains Minecraft Bedrock tokens
96
- * Caches Xbox user token (~14 days valid) to minimize login requests
123
+ * Supports SOCKS5 proxy for all authentication requests
97
124
  */
98
125
  async function authenticateWithCredentials(options) {
99
- const { email, password, clientPublicKey, cacheDir } = options;
126
+ const { email, password, clientPublicKey, cacheDir, proxy } = options;
100
127
  const cacheFile = cacheDir ? getCacheFile(cacheDir, email) : null;
128
+ const proxiedFetch = createProxiedFetch(proxy);
129
+ // Verify proxy is working by checking our IP
130
+ if (proxy) {
131
+ try {
132
+ const ipResp = await proxiedFetch("https://api.ipify.org?format=json");
133
+ const ipData = (await ipResp.json());
134
+ raknet_1.Logger.info(`Proxy IP verified: ${ipData.ip}`);
135
+ }
136
+ catch (e) {
137
+ raknet_1.Logger.warn(`Could not verify proxy IP: ${e instanceof Error ? e.message : String(e)}`);
138
+ }
139
+ }
101
140
  let userToken;
102
141
  let userHash;
103
- // Try to use cached user token first
104
142
  const cached = cacheFile ? loadCache(cacheFile) : null;
105
143
  if (cached) {
106
144
  raknet_1.Logger.info("Using cached Xbox user token...");
@@ -108,49 +146,250 @@ async function authenticateWithCredentials(options) {
108
146
  userHash = cached.userHash;
109
147
  }
110
148
  else {
111
- // Fresh login required
112
- raknet_1.Logger.info("Authenticating with Xbox Live...");
113
- const accessToken = await freshLogin(email, password);
114
- // Exchange for Xbox user token (valid ~14 days)
115
- const userTokenResp = await xboxlive_auth_1.xnet.exchangeRpsTicketForUserToken(accessToken, "t");
149
+ raknet_1.Logger.info(`Authenticating with Xbox Live...${proxy ? ` (via proxy ${proxy.host}:${proxy.port})` : ""}`);
150
+ const accessToken = await getMicrosoftAccessToken(email, password, proxiedFetch);
151
+ const userTokenResp = await exchangeRpsTicketForUserToken(accessToken, proxiedFetch);
116
152
  userToken = userTokenResp.Token;
117
153
  userHash = userTokenResp.DisplayClaims.xui[0].uhs;
118
- // Cache the user token
119
154
  if (cacheFile) {
120
155
  saveCache(cacheFile, userToken, userHash, userTokenResp.NotAfter);
121
156
  }
122
157
  }
123
- // Get XSTS token for Minecraft Bedrock (short-lived, always fetch fresh)
124
- const xstsResp = await xboxlive_auth_1.xnet.exchangeTokenForXSTSToken(userToken, {
125
- XSTSRelyingParty: MINECRAFT_BEDROCK_RELYING_PARTY,
126
- sandboxId: "RETAIL",
127
- });
158
+ // Get XSTS token for Minecraft Bedrock
159
+ const xstsResp = await exchangeTokenForXSTSToken(userToken, MINECRAFT_BEDROCK_RELYING_PARTY, proxiedFetch);
128
160
  const xuid = xstsResp.DisplayClaims.xui[0].xid || "";
129
- // Get Minecraft Bedrock chains
130
- const chains = await getMinecraftBedrockChains(xstsResp.Token, userHash, clientPublicKey);
161
+ // Get XSTS token for Playfab (needed for session token)
162
+ const playfabXstsResp = await exchangeTokenForXSTSToken(userToken, PLAYFAB_RELYING_PARTY, proxiedFetch);
163
+ const chains = await getMinecraftBedrockChains(xstsResp.Token, userHash, clientPublicKey, proxiedFetch);
131
164
  const gamertag = extractGamertagFromChains(chains);
132
165
  raknet_1.Logger.info(`Authenticated as: ${gamertag} (${xuid})`);
133
- return { chains, xuid, gamertag, userHash };
166
+ return {
167
+ chains,
168
+ xuid,
169
+ gamertag,
170
+ userHash,
171
+ xstsToken: xstsResp.Token,
172
+ playfabXstsToken: playfabXstsResp.Token,
173
+ playfabUserHash: playfabXstsResp.DisplayClaims.xui[0].uhs,
174
+ };
134
175
  }
135
- async function freshLogin(email, password) {
136
- try {
137
- const liveToken = await xboxlive_auth_1.live.authenticateWithCredentials({
138
- email: email,
139
- password,
176
+ /**
177
+ * Get Microsoft access token using email/password via OAuth flow
178
+ * This implements the full browser-like login flow
179
+ */
180
+ async function getMicrosoftAccessToken(email, password, proxiedFetch) {
181
+ const authUrl = "https://login.live.com/oauth20_authorize.srf";
182
+ const params = new URLSearchParams({
183
+ client_id: XBOX_AUTH_CLIENT_ID,
184
+ redirect_uri: "https://login.live.com/oauth20_desktop.srf",
185
+ response_type: "token",
186
+ scope: "service::user.auth.xboxlive.com::MBI_SSL",
187
+ display: "touch",
188
+ locale: "en",
189
+ });
190
+ const preAuthResp = await proxiedFetch(`${authUrl}?${params}`, {
191
+ headers: {
192
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
193
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
194
+ "Accept-Language": "en-US,en;q=0.5",
195
+ },
196
+ });
197
+ if (!preAuthResp.ok) {
198
+ throw new Error(`Pre-auth request failed: ${preAuthResp.status}`);
199
+ }
200
+ const preAuthHtml = await preAuthResp.text();
201
+ const cookies = extractCookies(preAuthResp.headers);
202
+ const { ppft, urlPost } = extractLoginParams(preAuthHtml);
203
+ const loginBody = new URLSearchParams({
204
+ login: email,
205
+ loginfmt: email,
206
+ passwd: password,
207
+ PPFT: ppft,
208
+ PPSX: "Passpor",
209
+ NewUser: "1",
210
+ FoundMSAs: "",
211
+ fspost: "0",
212
+ i21: "0",
213
+ CookieDisclosure: "0",
214
+ IsFidoSupported: "1",
215
+ isSignupPost: "0",
216
+ isRecoveryAttemptPost: "0",
217
+ i13: "0",
218
+ i19: Math.floor(Math.random() * 100000).toString(),
219
+ });
220
+ const loginResp = await proxiedFetch(urlPost, {
221
+ method: "POST",
222
+ headers: {
223
+ "Content-Type": "application/x-www-form-urlencoded",
224
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
225
+ Cookie: cookies,
226
+ Referer: `${authUrl}?${params}`,
227
+ Origin: "https://login.live.com",
228
+ },
229
+ body: loginBody.toString(),
230
+ redirect: "manual",
231
+ });
232
+ // Check for access token in redirect
233
+ let location = loginResp.headers.get("location") || "";
234
+ // Follow redirects manually to find the access token
235
+ let attempts = 0;
236
+ while (attempts < 5 && !location.includes("access_token=")) {
237
+ if (!location) {
238
+ // Check if we got an error page
239
+ const responseText = await loginResp.text();
240
+ if (responseText.includes("sErrTxt") ||
241
+ responseText.includes("Your account or password is incorrect")) {
242
+ throw new Error("Invalid credentials");
243
+ }
244
+ if (responseText.includes("Sign in a different way") ||
245
+ responseText.includes("idA_PWD_SwitchToCredPicker")) {
246
+ throw new Error("2FA is enabled on this account. Direct login requires 2FA to be disabled.");
247
+ }
248
+ // Try to extract access token from response body (some flows embed it)
249
+ const tokenMatch = responseText.match(/access_token=([^&"']+)/);
250
+ if (tokenMatch) {
251
+ return decodeURIComponent(tokenMatch[1]);
252
+ }
253
+ throw new Error("Failed to get redirect URL from login response");
254
+ }
255
+ if (location.includes("access_token=")) {
256
+ break;
257
+ }
258
+ const redirectResp = await proxiedFetch(location, {
259
+ headers: {
260
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
261
+ Cookie: cookies,
262
+ },
263
+ redirect: "manual",
140
264
  });
141
- return liveToken.access_token;
265
+ location = redirectResp.headers.get("location") || "";
266
+ attempts++;
142
267
  }
143
- catch (error) {
144
- const err = error;
145
- if (err.attributes?.code === "INVALID_CREDENTIALS_OR_2FA_ENABLED") {
146
- throw new Error("Authentication failed: Invalid credentials or 2FA is enabled.\n" +
147
- "Direct email/password login only works with 2FA DISABLED.");
268
+ // Extract access token from URL fragment
269
+ const tokenMatch = location.match(/access_token=([^&]+)/);
270
+ if (tokenMatch) {
271
+ return decodeURIComponent(tokenMatch[1]);
272
+ }
273
+ throw new Error("Failed to obtain access token from Microsoft");
274
+ }
275
+ function extractCookies(headers) {
276
+ const setCookies = headers.get("set-cookie");
277
+ if (!setCookies)
278
+ return "";
279
+ // Parse and combine cookies
280
+ const cookies = [];
281
+ const cookieStrings = setCookies.split(/,(?=[^;]*=)/);
282
+ for (const cookieStr of cookieStrings) {
283
+ const match = cookieStr.match(/^([^=]+)=([^;]*)/);
284
+ if (match) {
285
+ cookies.push(`${match[1].trim()}=${match[2]}`);
148
286
  }
149
- throw error;
150
287
  }
288
+ return cookies.join("; ");
151
289
  }
152
- async function getMinecraftBedrockChains(xstsToken, userHash, clientPublicKey) {
153
- const response = await fetch("https://multiplayer.minecraft.net/authentication", {
290
+ function extractLoginParams(html) {
291
+ let ppft = null;
292
+ // Pattern for sFTTag with escaped quotes (JSON format)
293
+ const sFTTagMatch = html.match(/sFTTag":"<input[^>]*value=\\"([^"\\]+)\\"/);
294
+ if (sFTTagMatch) {
295
+ ppft = sFTTagMatch[1];
296
+ }
297
+ // Fallback patterns
298
+ if (!ppft) {
299
+ const ppftPatterns = [
300
+ /sFTTag:'[^']*value="([^"]+)"/,
301
+ /name="PPFT"[^>]*value="([^"]+)"/,
302
+ /value="([^"]+)"[^>]*name="PPFT"/,
303
+ /<input[^>]*name="PPFT"[^>]*value="([^"]+)"/,
304
+ /"sFT"\s*:\s*"([^"]+)"/,
305
+ /sFT:'([^']+)'/,
306
+ /"sFT":"([^"]+)"/,
307
+ ];
308
+ for (const pattern of ppftPatterns) {
309
+ const match = html.match(pattern);
310
+ if (match) {
311
+ ppft = match[1];
312
+ break;
313
+ }
314
+ }
315
+ }
316
+ if (!ppft) {
317
+ throw new Error("Failed to extract PPFT token from login page");
318
+ }
319
+ // Extract urlPost
320
+ const urlPostPatterns = [
321
+ /urlPost:\s*'([^']+)'/,
322
+ /urlPost:\s*"([^"]+)"/,
323
+ /"urlPost"\s*:\s*"([^"]+)"/,
324
+ ];
325
+ let urlPost = "https://login.live.com/ppsecure/post.srf";
326
+ for (const pattern of urlPostPatterns) {
327
+ const match = html.match(pattern);
328
+ if (match) {
329
+ urlPost = match[1];
330
+ break;
331
+ }
332
+ }
333
+ return { ppft, urlPost };
334
+ }
335
+ /**
336
+ * Exchange Microsoft access token for Xbox User Token
337
+ */
338
+ async function exchangeRpsTicketForUserToken(accessToken, proxiedFetch) {
339
+ const response = await proxiedFetch("https://user.auth.xboxlive.com/user/authenticate", {
340
+ method: "POST",
341
+ headers: {
342
+ "Content-Type": "application/json",
343
+ Accept: "application/json",
344
+ "x-xbl-contract-version": "1",
345
+ },
346
+ body: JSON.stringify({
347
+ RelyingParty: "http://auth.xboxlive.com",
348
+ TokenType: "JWT",
349
+ Properties: {
350
+ AuthMethod: "RPS",
351
+ SiteName: "user.auth.xboxlive.com",
352
+ RpsTicket: `t=${accessToken}`,
353
+ },
354
+ }),
355
+ });
356
+ if (!response.ok) {
357
+ const text = await response.text();
358
+ throw new Error(`Xbox user token exchange failed: ${response.status} - ${text}`);
359
+ }
360
+ return response.json();
361
+ }
362
+ /**
363
+ * Exchange Xbox User Token for XSTS Token
364
+ */
365
+ async function exchangeTokenForXSTSToken(userToken, relyingParty, proxiedFetch) {
366
+ const response = await proxiedFetch("https://xsts.auth.xboxlive.com/xsts/authorize", {
367
+ method: "POST",
368
+ headers: {
369
+ "Content-Type": "application/json",
370
+ Accept: "application/json",
371
+ "x-xbl-contract-version": "1",
372
+ },
373
+ body: JSON.stringify({
374
+ RelyingParty: relyingParty,
375
+ TokenType: "JWT",
376
+ Properties: {
377
+ SandboxId: "RETAIL",
378
+ UserTokens: [userToken],
379
+ },
380
+ }),
381
+ });
382
+ if (!response.ok) {
383
+ const text = await response.text();
384
+ throw new Error(`Xbox XSTS token exchange failed: ${response.status} - ${text}`);
385
+ }
386
+ return response.json();
387
+ }
388
+ /**
389
+ * Get Minecraft Bedrock authentication chains
390
+ */
391
+ async function getMinecraftBedrockChains(xstsToken, userHash, clientPublicKey, proxiedFetch) {
392
+ const response = await proxiedFetch("https://multiplayer.minecraft.net/authentication", {
154
393
  method: "POST",
155
394
  headers: {
156
395
  "Content-Type": "application/json",
@@ -162,11 +401,8 @@ async function getMinecraftBedrockChains(xstsToken, userHash, clientPublicKey) {
162
401
  if (!response.ok) {
163
402
  const text = await response.text();
164
403
  if (response.status === 401) {
165
- throw new Error("Minecraft Bedrock authentication failed (401 UNAUTHORIZED).\n" +
166
- "This usually means:\n" +
167
- " 1. The account does not have an Xbox profile (create one at xbox.com)\n" +
168
- " 2. The account does not own Minecraft Bedrock Edition\n" +
169
- " 3. The account needs to accept Xbox/Minecraft terms of service");
404
+ throw new Error("Minecraft Bedrock authentication failed (401).\n" +
405
+ "The account may not have an Xbox profile or Minecraft Bedrock Edition.");
170
406
  }
171
407
  throw new Error(`Minecraft Bedrock auth failed: ${response.status} - ${text}`);
172
408
  }
@@ -107,12 +107,42 @@ async function authenticateWithEmailPassword(client) {
107
107
  password: client.options.password,
108
108
  clientPublicKey: client.data.loginData.clientX509,
109
109
  cacheDir: client.options.tokensFolder,
110
+ proxy: client.options.proxy,
110
111
  });
111
112
  const profile = {
112
113
  name: tokens.gamertag,
113
114
  uuid: generateUUID(tokens.gamertag),
114
115
  xuid: Number(tokens.xuid) || 0,
115
116
  };
117
+ // Get Playfab session ticket
118
+ const playfabData = await getPlayfabSessionTicket(tokens.playfabUserHash, tokens.playfabXstsToken);
119
+ // Get the multiplayer session token
120
+ // First get the MC services token (mcToken)
121
+ const mcToken = await getMinecraftServicesTokenFromPlayfab(playfabData.sessionTicket);
122
+ // Then use mcToken to get the multiplayer session token
123
+ const sessionToken = await getMultiplayerSessionTokenFromMcToken(mcToken, client.data.loginData.clientX509);
124
+ client.data.loginToken = sessionToken;
125
+ // Extract pfcd from session token and store it
126
+ try {
127
+ const tokenParts = sessionToken.split(".");
128
+ if (tokenParts.length >= 2) {
129
+ const payload = JSON.parse(Buffer.from(tokenParts[1], "base64").toString());
130
+ if (payload.pfcd) {
131
+ client.data.payload.pfcd = payload.pfcd;
132
+ }
133
+ // Store session token data for client chain
134
+ client.data.loginData.sessionTokenData = {
135
+ ipt: payload.ipt,
136
+ tid: payload.prop ? JSON.parse(payload.prop).tid : undefined,
137
+ mid: payload.prop ? JSON.parse(payload.prop).mid : undefined,
138
+ xid: payload.xid,
139
+ cpk: payload.cpk,
140
+ };
141
+ }
142
+ }
143
+ catch (e) {
144
+ raknet_1.Logger.warn(`Failed to extract pfcd from session token: ${e instanceof Error ? e.message : String(e)}`);
145
+ }
116
146
  const endTime = Date.now();
117
147
  raknet_1.Logger.info(`Authentication with Xbox (email/password) took ${(endTime - startTime) / 1000}s.`);
118
148
  setupClientProfile(client, profile, tokens.chains);
@@ -154,6 +184,144 @@ async function getMultiplayerSessionToken(authflow, client) {
154
184
  throw error;
155
185
  }
156
186
  }
187
+ async function getPlayfabSessionTicket(playfabUserHash, playfabXstsToken) {
188
+ try {
189
+ const response = await fetch("https://20ca2.playfabapi.com/Client/LoginWithXbox", {
190
+ method: "POST",
191
+ headers: { "Content-Type": "application/json" },
192
+ body: JSON.stringify({
193
+ CreateAccount: true,
194
+ EncryptedRequest: null,
195
+ InfoRequestParameters: {
196
+ GetCharacterInventories: false,
197
+ GetCharacterList: false,
198
+ GetPlayerProfile: true,
199
+ GetPlayerStatistics: false,
200
+ GetTitleData: false,
201
+ GetUserAccountInfo: true,
202
+ GetUserData: false,
203
+ GetUserInventory: false,
204
+ GetUserReadOnlyData: false,
205
+ GetUserVirtualCurrency: false,
206
+ PlayerStatisticNames: null,
207
+ ProfileConstraints: null,
208
+ TitleDataKeys: null,
209
+ UserDataKeys: null,
210
+ UserReadOnlyDataKeys: null,
211
+ },
212
+ PlayerSecret: null,
213
+ TitleId: "20CA2",
214
+ XboxToken: `XBL3.0 x=${playfabUserHash};${playfabXstsToken}`,
215
+ }),
216
+ });
217
+ if (!response.ok) {
218
+ const text = await response.text();
219
+ throw new Error(`Playfab login failed: ${response.status} ${response.statusText} - ${text}`);
220
+ }
221
+ const json = (await response.json());
222
+ return {
223
+ sessionTicket: json.data.SessionTicket,
224
+ playFabId: json.data.PlayFabId,
225
+ };
226
+ }
227
+ catch (error) {
228
+ raknet_1.Logger.error(`Error while getting Playfab session ticket: ${error instanceof Error ? error.message : String(error)}`);
229
+ throw error;
230
+ }
231
+ }
232
+ async function getMinecraftServicesTokenFromPlayfab(sessionTicket) {
233
+ try {
234
+ const response = await fetch("https://authorization.franchise.minecraft-services.net/api/v1.0/session/start", {
235
+ method: "POST",
236
+ headers: { "Content-Type": "application/json" },
237
+ body: JSON.stringify({
238
+ device: {
239
+ applicationType: "MinecraftPE",
240
+ gameVersion: "1.21.130",
241
+ id: "c1681ad3-415e-30cd-abd3-3b8f51e771d1",
242
+ memory: String(8 * (1024 * 1024 * 1024)),
243
+ platform: "Windows10",
244
+ playFabTitleId: "20CA2",
245
+ storePlatform: "uwp.store",
246
+ type: "Windows10",
247
+ },
248
+ user: {
249
+ token: sessionTicket,
250
+ tokenType: "PlayFab",
251
+ },
252
+ }),
253
+ });
254
+ if (!response.ok) {
255
+ const text = await response.text();
256
+ throw new Error(`MC services token failed: ${response.status} ${response.statusText} - ${text}`);
257
+ }
258
+ const json = (await response.json());
259
+ return json.result.authorizationHeader;
260
+ }
261
+ catch (error) {
262
+ raknet_1.Logger.error(`Error while getting MC services token: ${error instanceof Error ? error.message : String(error)}`);
263
+ throw error;
264
+ }
265
+ }
266
+ async function getMultiplayerSessionTokenFromMcToken(mcToken, publicKey) {
267
+ try {
268
+ const response = await fetch("https://authorization.franchise.minecraft-services.net/api/v1.0/multiplayer/session/start", {
269
+ method: "POST",
270
+ headers: {
271
+ "Content-Type": "application/json",
272
+ Authorization: mcToken,
273
+ "Accept-Encoding": "identity",
274
+ },
275
+ body: JSON.stringify({
276
+ publicKey: publicKey,
277
+ }),
278
+ });
279
+ if (!response.ok) {
280
+ const text = await response.text();
281
+ throw new Error(`Multiplayer session start failed: ${response.status} ${response.statusText} - ${text}`);
282
+ }
283
+ const json = (await response.json());
284
+ return json.result.signedToken;
285
+ }
286
+ catch (error) {
287
+ raknet_1.Logger.error(`Error while getting Multiplayer Session Token: ${error instanceof Error ? error.message : String(error)}`);
288
+ throw error;
289
+ }
290
+ }
291
+ async function getMultiplayerSessionTokenFromXsts(sessionTicket, publicKey) {
292
+ try {
293
+ const response = await fetch("https://authorization.franchise.minecraft-services.net/api/v1.0/session/start", {
294
+ method: "POST",
295
+ headers: { "Content-Type": "application/json" },
296
+ body: JSON.stringify({
297
+ device: {
298
+ applicationType: "MinecraftPE",
299
+ gameVersion: "1.21.130",
300
+ id: "c1681ad3-415e-30cd-abd3-3b8f51e771d1",
301
+ memory: String(8 * (1024 * 1024 * 1024)),
302
+ platform: "Windows10",
303
+ playFabTitleId: "20CA2",
304
+ storePlatform: "uwp.store",
305
+ type: "Windows10",
306
+ },
307
+ user: {
308
+ token: sessionTicket,
309
+ tokenType: "PlayFab",
310
+ },
311
+ }),
312
+ });
313
+ if (!response.ok) {
314
+ const text = await response.text();
315
+ throw new Error(`Multiplayer session start failed: ${response.status} ${response.statusText} - ${text}`);
316
+ }
317
+ const json = (await response.json());
318
+ return json.result.authorizationHeader;
319
+ }
320
+ catch (error) {
321
+ raknet_1.Logger.error(`Error while getting Multiplayer Session Token: ${error instanceof Error ? error.message : String(error)}`);
322
+ throw error;
323
+ }
324
+ }
157
325
  function extractProfile(jwt) {
158
326
  if (!jwt) {
159
327
  raknet_1.Logger.error("JWT is undefined or empty");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "baltica",
3
3
  "description": "Library for Minecraft Bedrock Edition community developers.",
4
- "version": "0.1.20",
4
+ "version": "0.1.23",
5
5
  "minecraft": "1.21.130",
6
6
  "main": "dist/index.js",
7
7
  "license": "MIT",
@@ -21,18 +21,21 @@
21
21
  }
22
22
  ],
23
23
  "dependencies": {
24
- "@sanctumterra/raknet": "^1.4.8",
24
+ "@sanctumterra/raknet": "^1.4.11",
25
25
  "@serenityjs/binarystream": "^3.0.10",
26
26
  "@serenityjs/protocol": "^0.8.17",
27
- "@xboxreplay/xboxlive-auth": "^5.1.0",
27
+ "fetch-socks": "^1.3.2",
28
28
  "jose": "^5.10.0",
29
29
  "prismarine-auth": "^2.7.0",
30
+ "undici": "^7.18.2",
30
31
  "uuid-1345": "^1.0.2"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@biomejs/biome": "1.9.4",
34
- "@types/node": "^22.19.6",
35
+ "@types/node": "^22.19.7",
36
+ "@types/pngjs": "^6.0.5",
35
37
  "@types/uuid-1345": "^0.99.25",
38
+ "pngjs": "^7.0.0",
36
39
  "typescript": "^5.9.3"
37
40
  }
38
- }
41
+ }