baltica 0.1.19 → 0.1.22

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,56 @@ 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
+ },
74
94
  });
95
+
96
+ await client.connect();
75
97
  ```
76
98
 
99
+ This is useful for:
100
+ - Bypassing IP restrictions
101
+ - Testing from different geographic locations
102
+ - Managing multiple bot connections
103
+
77
104
  ### Server Usage
78
105
 
79
106
  Want to create your own Minecraft server? We got you:
@@ -43,6 +43,7 @@ export declare class Client extends Emitter<ClientEvents> {
43
43
  startGameData: StartGamePacket;
44
44
  /** Whether we should continue after sending Login (Proxy use) */
45
45
  cancelPastLogin: boolean;
46
+ private disconnectReason?;
46
47
  constructor(options: Partial<ClientOptions>);
47
48
  /** Connect to the server and start sending/receiving packets. */
48
49
  connect(): Promise<[StartGamePacket]>;
@@ -53,6 +54,8 @@ export declare class Client extends Emitter<ClientEvents> {
53
54
  processPacket(buffer: Buffer): void;
54
55
  /** Do not call this, leaving it public incase someone needs to override this for some reason. */
55
56
  handleGamePackets(): void;
57
+ disconnect(reason?: string): void;
58
+ private cleanup;
56
59
  startEncryption(iv: Buffer): void;
57
60
  private waitForSessionReady;
58
61
  /**
@@ -44,9 +44,7 @@ class Client extends shared_1.Emitter {
44
44
  });
45
45
  /** Create ClientData to store and handle auth data */
46
46
  this.data = new types_2.ClientData(this);
47
- const time = Date.now();
48
- /** Session event gets mojang (minecraft) auth session */
49
- /** NOTE! This takes like 30-100ms for offline mode which kinda feels slow but not really */
47
+ this.raknet.on("disconnect", () => this.cleanup());
50
48
  this.once("session", () => {
51
49
  this.sessionReady = true;
52
50
  });
@@ -184,11 +182,23 @@ class Client extends shared_1.Emitter {
184
182
  }
185
183
  });
186
184
  this.on("DisconnectPacket", (packet) => {
187
- this.status = raknet_1.ConnectionStatus.Disconnected;
188
- this.raknet.disconnect();
189
- console.log(packet.message);
185
+ this.disconnect(packet.message.message ?? undefined);
190
186
  });
191
187
  }
188
+ disconnect(reason) {
189
+ if (this.status === raknet_1.ConnectionStatus.Disconnected)
190
+ return;
191
+ this.disconnectReason = reason;
192
+ this.raknet.disconnect();
193
+ }
194
+ cleanup() {
195
+ this.status = raknet_1.ConnectionStatus.Disconnected;
196
+ this._encryptionEnabled = false;
197
+ this._compressionEnabled = false;
198
+ this.sessionReady = false;
199
+ this.emit("disconnect", this.disconnectReason);
200
+ this.disconnectReason = undefined;
201
+ }
192
202
  startEncryption(iv) {
193
203
  this.packetEncryptor = new shared_1.PacketEncryptor(this, iv);
194
204
  this._encryptionEnabled = true;
@@ -7,6 +7,7 @@ type ClientEvents = {
7
7
  } & {
8
8
  packet: [packet: InstanceType<(typeof Protocol)[PacketNames]>];
9
9
  connect: [];
10
+ disconnect: [reason?: string];
10
11
  } & {
11
12
  [K in `${number}`]: [buffer: Buffer];
12
13
  };
@@ -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,20 +1,40 @@
1
- import { authenticate as xboxAuthenticate, 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;
8
14
  }
15
+ export interface ProxyOptions {
16
+ host: string;
17
+ port: number;
18
+ userId?: string;
19
+ password?: string;
20
+ }
9
21
  export interface AuthOptions {
10
22
  email: string;
11
23
  password: string;
12
24
  clientPublicKey: string;
25
+ cacheDir?: string;
26
+ proxy?: ProxyOptions;
13
27
  }
14
28
  /**
15
29
  * Authenticates with Xbox Live using email/password and obtains Minecraft Bedrock tokens
16
- * NOTE: Only works if 2FA is DISABLED on the Microsoft account
30
+ * Supports SOCKS5 proxy for all authentication requests
17
31
  */
18
32
  export declare function authenticateWithCredentials(options: AuthOptions): Promise<BedrockTokens>;
19
- export { xboxAuthenticate as authenticate, live, xnet };
20
- export type { AuthenticateResponse, Email };
33
+ export type Email = string;
34
+ export interface AuthenticateResponse {
35
+ access_token: string;
36
+ token_type: string;
37
+ expires_in: number;
38
+ scope: string;
39
+ user_id: string;
40
+ }
@@ -1,67 +1,383 @@
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
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
2
43
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.xnet = exports.live = exports.authenticate = void 0;
4
44
  exports.authenticateWithCredentials = authenticateWithCredentials;
5
- const xboxlive_auth_1 = require("@xboxreplay/xboxlive-auth");
6
- Object.defineProperty(exports, "authenticate", { enumerable: true, get: function () { return xboxlive_auth_1.authenticate; } });
7
- Object.defineProperty(exports, "live", { enumerable: true, get: function () { return xboxlive_auth_1.live; } });
8
- Object.defineProperty(exports, "xnet", { enumerable: true, get: function () { return xboxlive_auth_1.xnet; } });
45
+ const fs = __importStar(require("node:fs"));
46
+ const path = __importStar(require("node:path"));
9
47
  const raknet_1 = require("@sanctumterra/raknet");
48
+ const fetch_socks_1 = require("fetch-socks");
49
+ const undici_1 = require("undici");
10
50
  const MINECRAFT_BEDROCK_RELYING_PARTY = "https://multiplayer.minecraft.net/";
51
+ const XBOX_AUTH_CLIENT_ID = "00000000441cc96b";
52
+ function hashString(str) {
53
+ let hash = 0;
54
+ for (let i = 0; i < str.length; i++) {
55
+ const char = str.charCodeAt(i);
56
+ hash = (hash << 5) - hash + char;
57
+ hash = hash & hash;
58
+ }
59
+ return Math.abs(hash).toString(16).slice(0, 6);
60
+ }
61
+ function ensureDir(dir) {
62
+ if (!fs.existsSync(dir)) {
63
+ fs.mkdirSync(dir, { recursive: true });
64
+ }
65
+ }
66
+ function getCacheFile(cacheDir, email) {
67
+ return path.join(cacheDir, `${hashString(email)}_xbl-user-cache.json`);
68
+ }
69
+ function loadCache(cacheFile) {
70
+ try {
71
+ if (fs.existsSync(cacheFile)) {
72
+ const cached = JSON.parse(fs.readFileSync(cacheFile, "utf-8"));
73
+ const expiresAt = new Date(cached.notAfter).getTime();
74
+ if (Date.now() < expiresAt - 3600000) {
75
+ return cached;
76
+ }
77
+ raknet_1.Logger.info("Cached user token expired");
78
+ }
79
+ }
80
+ catch {
81
+ /* ignore */
82
+ }
83
+ return null;
84
+ }
85
+ function saveCache(cacheFile, userToken, userHash, notAfter) {
86
+ try {
87
+ ensureDir(path.dirname(cacheFile));
88
+ const data = {
89
+ userToken,
90
+ userHash,
91
+ notAfter,
92
+ obtainedOn: Date.now(),
93
+ };
94
+ fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2));
95
+ }
96
+ catch {
97
+ /* ignore */
98
+ }
99
+ }
100
+ function createProxiedFetch(proxy) {
101
+ if (!proxy) {
102
+ return fetch;
103
+ }
104
+ const dispatcher = (0, fetch_socks_1.socksDispatcher)({
105
+ type: 5,
106
+ host: proxy.host,
107
+ port: proxy.port,
108
+ userId: proxy.userId,
109
+ password: proxy.password,
110
+ });
111
+ return async (input, init) => {
112
+ const url = typeof input === "string" ? input : input.toString();
113
+ const response = await (0, undici_1.fetch)(url, {
114
+ ...init,
115
+ dispatcher,
116
+ });
117
+ return response;
118
+ };
119
+ }
11
120
  /**
12
121
  * Authenticates with Xbox Live using email/password and obtains Minecraft Bedrock tokens
13
- * NOTE: Only works if 2FA is DISABLED on the Microsoft account
122
+ * Supports SOCKS5 proxy for all authentication requests
14
123
  */
15
124
  async function authenticateWithCredentials(options) {
16
- const { email, password, clientPublicKey } = options;
17
- raknet_1.Logger.info("Authenticating with Xbox Live...");
18
- try {
19
- const liveToken = await xboxlive_auth_1.live.authenticateWithCredentials({
20
- email: email,
21
- password,
22
- });
23
- // Exchange for Xbox user token
24
- const userTokenResp = await xboxlive_auth_1.xnet.exchangeRpsTicketForUserToken(liveToken.access_token, "t");
25
- const userHash = userTokenResp.DisplayClaims.xui[0].uhs;
26
- // Get XSTS token for Minecraft Bedrock
27
- const xstsResp = await xboxlive_auth_1.xnet.exchangeTokenForXSTSToken(userTokenResp.Token, {
28
- XSTSRelyingParty: MINECRAFT_BEDROCK_RELYING_PARTY,
29
- sandboxId: "RETAIL",
125
+ const { email, password, clientPublicKey, cacheDir, proxy } = options;
126
+ const cacheFile = cacheDir ? getCacheFile(cacheDir, email) : null;
127
+ const proxiedFetch = createProxiedFetch(proxy);
128
+ // Verify proxy is working by checking our IP
129
+ if (proxy) {
130
+ try {
131
+ const ipResp = await proxiedFetch("https://api.ipify.org?format=json");
132
+ const ipData = (await ipResp.json());
133
+ raknet_1.Logger.info(`Proxy IP verified: ${ipData.ip}`);
134
+ }
135
+ catch (e) {
136
+ raknet_1.Logger.warn(`Could not verify proxy IP: ${e instanceof Error ? e.message : String(e)}`);
137
+ }
138
+ }
139
+ let userToken;
140
+ let userHash;
141
+ const cached = cacheFile ? loadCache(cacheFile) : null;
142
+ if (cached) {
143
+ raknet_1.Logger.info("Using cached Xbox user token...");
144
+ userToken = cached.userToken;
145
+ userHash = cached.userHash;
146
+ }
147
+ else {
148
+ raknet_1.Logger.info(`Authenticating with Xbox Live...${proxy ? ` (via proxy ${proxy.host}:${proxy.port})` : ""}`);
149
+ const accessToken = await getMicrosoftAccessToken(email, password, proxiedFetch);
150
+ const userTokenResp = await exchangeRpsTicketForUserToken(accessToken, proxiedFetch);
151
+ userToken = userTokenResp.Token;
152
+ userHash = userTokenResp.DisplayClaims.xui[0].uhs;
153
+ if (cacheFile) {
154
+ saveCache(cacheFile, userToken, userHash, userTokenResp.NotAfter);
155
+ }
156
+ }
157
+ const xstsResp = await exchangeTokenForXSTSToken(userToken, MINECRAFT_BEDROCK_RELYING_PARTY, proxiedFetch);
158
+ const xuid = xstsResp.DisplayClaims.xui[0].xid || "";
159
+ const chains = await getMinecraftBedrockChains(xstsResp.Token, userHash, clientPublicKey, proxiedFetch);
160
+ const gamertag = extractGamertagFromChains(chains);
161
+ raknet_1.Logger.info(`Authenticated as: ${gamertag} (${xuid})`);
162
+ return { chains, xuid, gamertag, userHash };
163
+ }
164
+ /**
165
+ * Get Microsoft access token using email/password via OAuth flow
166
+ * This implements the full browser-like login flow
167
+ */
168
+ async function getMicrosoftAccessToken(email, password, proxiedFetch) {
169
+ const authUrl = "https://login.live.com/oauth20_authorize.srf";
170
+ const params = new URLSearchParams({
171
+ client_id: XBOX_AUTH_CLIENT_ID,
172
+ redirect_uri: "https://login.live.com/oauth20_desktop.srf",
173
+ response_type: "token",
174
+ scope: "service::user.auth.xboxlive.com::MBI_SSL",
175
+ display: "touch",
176
+ locale: "en",
177
+ });
178
+ const preAuthResp = await proxiedFetch(`${authUrl}?${params}`, {
179
+ headers: {
180
+ "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",
181
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
182
+ "Accept-Language": "en-US,en;q=0.5",
183
+ },
184
+ });
185
+ if (!preAuthResp.ok) {
186
+ throw new Error(`Pre-auth request failed: ${preAuthResp.status}`);
187
+ }
188
+ const preAuthHtml = await preAuthResp.text();
189
+ const cookies = extractCookies(preAuthResp.headers);
190
+ const { ppft, urlPost } = extractLoginParams(preAuthHtml);
191
+ const loginBody = new URLSearchParams({
192
+ login: email,
193
+ loginfmt: email,
194
+ passwd: password,
195
+ PPFT: ppft,
196
+ PPSX: "Passpor",
197
+ NewUser: "1",
198
+ FoundMSAs: "",
199
+ fspost: "0",
200
+ i21: "0",
201
+ CookieDisclosure: "0",
202
+ IsFidoSupported: "1",
203
+ isSignupPost: "0",
204
+ isRecoveryAttemptPost: "0",
205
+ i13: "0",
206
+ i19: Math.floor(Math.random() * 100000).toString(),
207
+ });
208
+ const loginResp = await proxiedFetch(urlPost, {
209
+ method: "POST",
210
+ headers: {
211
+ "Content-Type": "application/x-www-form-urlencoded",
212
+ "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",
213
+ Cookie: cookies,
214
+ Referer: `${authUrl}?${params}`,
215
+ Origin: "https://login.live.com",
216
+ },
217
+ body: loginBody.toString(),
218
+ redirect: "manual",
219
+ });
220
+ // Check for access token in redirect
221
+ let location = loginResp.headers.get("location") || "";
222
+ // Follow redirects manually to find the access token
223
+ let attempts = 0;
224
+ while (attempts < 5 && !location.includes("access_token=")) {
225
+ if (!location) {
226
+ // Check if we got an error page
227
+ const responseText = await loginResp.text();
228
+ if (responseText.includes("sErrTxt") ||
229
+ responseText.includes("Your account or password is incorrect")) {
230
+ throw new Error("Invalid credentials");
231
+ }
232
+ if (responseText.includes("Sign in a different way") ||
233
+ responseText.includes("idA_PWD_SwitchToCredPicker")) {
234
+ throw new Error("2FA is enabled on this account. Direct login requires 2FA to be disabled.");
235
+ }
236
+ // Try to extract access token from response body (some flows embed it)
237
+ const tokenMatch = responseText.match(/access_token=([^&"']+)/);
238
+ if (tokenMatch) {
239
+ return decodeURIComponent(tokenMatch[1]);
240
+ }
241
+ throw new Error("Failed to get redirect URL from login response");
242
+ }
243
+ if (location.includes("access_token=")) {
244
+ break;
245
+ }
246
+ const redirectResp = await proxiedFetch(location, {
247
+ headers: {
248
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
249
+ Cookie: cookies,
250
+ },
251
+ redirect: "manual",
30
252
  });
31
- const xuid = xstsResp.DisplayClaims.xui[0].xid || "";
32
- // Get Minecraft Bedrock chains using the client's public key
33
- const chains = await getMinecraftBedrockChains(xstsResp.Token, userHash, clientPublicKey);
34
- const gamertag = extractGamertagFromChains(chains);
35
- raknet_1.Logger.info(`Authenticated as: ${gamertag} (${xuid})`);
36
- return { chains, xuid, gamertag, userHash };
37
- }
38
- catch (error) {
39
- const err = error;
40
- if (err.attributes?.code === "INVALID_CREDENTIALS_OR_2FA_ENABLED") {
41
- throw new Error("Authentication failed: Invalid credentials or 2FA is enabled.\n" +
42
- "Direct email/password login only works with 2FA DISABLED.");
253
+ location = redirectResp.headers.get("location") || "";
254
+ attempts++;
255
+ }
256
+ // Extract access token from URL fragment
257
+ const tokenMatch = location.match(/access_token=([^&]+)/);
258
+ if (tokenMatch) {
259
+ return decodeURIComponent(tokenMatch[1]);
260
+ }
261
+ throw new Error("Failed to obtain access token from Microsoft");
262
+ }
263
+ function extractCookies(headers) {
264
+ const setCookies = headers.get("set-cookie");
265
+ if (!setCookies)
266
+ return "";
267
+ // Parse and combine cookies
268
+ const cookies = [];
269
+ const cookieStrings = setCookies.split(/,(?=[^;]*=)/);
270
+ for (const cookieStr of cookieStrings) {
271
+ const match = cookieStr.match(/^([^=]+)=([^;]*)/);
272
+ if (match) {
273
+ cookies.push(`${match[1].trim()}=${match[2]}`);
43
274
  }
44
- // Check for Xbox Live specific errors
45
- const xErr = err.data?.attributes?.extra?.body?.XErr;
46
- if (xErr) {
47
- const xboxErrors = {
48
- 2148916233: "No Xbox profile exists for this account. Create one at https://xbox.com/live",
49
- 2148916227: "Account banned by Xbox for violating Community Standards.",
50
- 2148916229: "Account restricted - guardian permission required. Visit https://account.microsoft.com/family/",
51
- 2148916234: "Must accept Xbox Terms of Service. Login at https://xbox.com",
52
- 2148916235: "Account region not authorized by Xbox.",
53
- 2148916236: "Account requires age verification. Login at https://login.live.com",
54
- 2148916238: "Account under 18 must be added to a family by an adult.",
55
- };
56
- if (xboxErrors[xErr]) {
57
- throw new Error(`Xbox Live error: ${xboxErrors[xErr]}`);
275
+ }
276
+ return cookies.join("; ");
277
+ }
278
+ function extractLoginParams(html) {
279
+ let ppft = null;
280
+ // Pattern for sFTTag with escaped quotes (JSON format)
281
+ const sFTTagMatch = html.match(/sFTTag":"<input[^>]*value=\\"([^"\\]+)\\"/);
282
+ if (sFTTagMatch) {
283
+ ppft = sFTTagMatch[1];
284
+ }
285
+ // Fallback patterns
286
+ if (!ppft) {
287
+ const ppftPatterns = [
288
+ /sFTTag:'[^']*value="([^"]+)"/,
289
+ /name="PPFT"[^>]*value="([^"]+)"/,
290
+ /value="([^"]+)"[^>]*name="PPFT"/,
291
+ /<input[^>]*name="PPFT"[^>]*value="([^"]+)"/,
292
+ /"sFT"\s*:\s*"([^"]+)"/,
293
+ /sFT:'([^']+)'/,
294
+ /"sFT":"([^"]+)"/,
295
+ ];
296
+ for (const pattern of ppftPatterns) {
297
+ const match = html.match(pattern);
298
+ if (match) {
299
+ ppft = match[1];
300
+ break;
58
301
  }
59
302
  }
60
- throw error;
61
303
  }
304
+ if (!ppft) {
305
+ throw new Error("Failed to extract PPFT token from login page");
306
+ }
307
+ // Extract urlPost
308
+ const urlPostPatterns = [
309
+ /urlPost:\s*'([^']+)'/,
310
+ /urlPost:\s*"([^"]+)"/,
311
+ /"urlPost"\s*:\s*"([^"]+)"/,
312
+ ];
313
+ let urlPost = "https://login.live.com/ppsecure/post.srf";
314
+ for (const pattern of urlPostPatterns) {
315
+ const match = html.match(pattern);
316
+ if (match) {
317
+ urlPost = match[1];
318
+ break;
319
+ }
320
+ }
321
+ return { ppft, urlPost };
62
322
  }
63
- async function getMinecraftBedrockChains(xstsToken, userHash, clientPublicKey) {
64
- const response = await fetch("https://multiplayer.minecraft.net/authentication", {
323
+ /**
324
+ * Exchange Microsoft access token for Xbox User Token
325
+ */
326
+ async function exchangeRpsTicketForUserToken(accessToken, proxiedFetch) {
327
+ const response = await proxiedFetch("https://user.auth.xboxlive.com/user/authenticate", {
328
+ method: "POST",
329
+ headers: {
330
+ "Content-Type": "application/json",
331
+ Accept: "application/json",
332
+ "x-xbl-contract-version": "1",
333
+ },
334
+ body: JSON.stringify({
335
+ RelyingParty: "http://auth.xboxlive.com",
336
+ TokenType: "JWT",
337
+ Properties: {
338
+ AuthMethod: "RPS",
339
+ SiteName: "user.auth.xboxlive.com",
340
+ RpsTicket: `t=${accessToken}`,
341
+ },
342
+ }),
343
+ });
344
+ if (!response.ok) {
345
+ const text = await response.text();
346
+ throw new Error(`Xbox user token exchange failed: ${response.status} - ${text}`);
347
+ }
348
+ return response.json();
349
+ }
350
+ /**
351
+ * Exchange Xbox User Token for XSTS Token
352
+ */
353
+ async function exchangeTokenForXSTSToken(userToken, relyingParty, proxiedFetch) {
354
+ const response = await proxiedFetch("https://xsts.auth.xboxlive.com/xsts/authorize", {
355
+ method: "POST",
356
+ headers: {
357
+ "Content-Type": "application/json",
358
+ Accept: "application/json",
359
+ "x-xbl-contract-version": "1",
360
+ },
361
+ body: JSON.stringify({
362
+ RelyingParty: relyingParty,
363
+ TokenType: "JWT",
364
+ Properties: {
365
+ SandboxId: "RETAIL",
366
+ UserTokens: [userToken],
367
+ },
368
+ }),
369
+ });
370
+ if (!response.ok) {
371
+ const text = await response.text();
372
+ throw new Error(`Xbox XSTS token exchange failed: ${response.status} - ${text}`);
373
+ }
374
+ return response.json();
375
+ }
376
+ /**
377
+ * Get Minecraft Bedrock authentication chains
378
+ */
379
+ async function getMinecraftBedrockChains(xstsToken, userHash, clientPublicKey, proxiedFetch) {
380
+ const response = await proxiedFetch("https://multiplayer.minecraft.net/authentication", {
65
381
  method: "POST",
66
382
  headers: {
67
383
  "Content-Type": "application/json",
@@ -72,6 +388,10 @@ async function getMinecraftBedrockChains(xstsToken, userHash, clientPublicKey) {
72
388
  });
73
389
  if (!response.ok) {
74
390
  const text = await response.text();
391
+ if (response.status === 401) {
392
+ throw new Error("Minecraft Bedrock authentication failed (401).\n" +
393
+ "The account may not have an Xbox profile or Minecraft Bedrock Edition.");
394
+ }
75
395
  throw new Error(`Minecraft Bedrock auth failed: ${response.status} - ${text}`);
76
396
  }
77
397
  const data = (await response.json());
@@ -106,6 +106,8 @@ async function authenticateWithEmailPassword(client) {
106
106
  email: client.options.email,
107
107
  password: client.options.password,
108
108
  clientPublicKey: client.data.loginData.clientX509,
109
+ cacheDir: client.options.tokensFolder,
110
+ proxy: client.options.proxy,
109
111
  });
110
112
  const profile = {
111
113
  name: tokens.gamertag,
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.19",
4
+ "version": "0.1.22",
5
5
  "minecraft": "1.21.130",
6
6
  "main": "dist/index.js",
7
7
  "license": "MIT",
@@ -21,18 +21,19 @@
21
21
  }
22
22
  ],
23
23
  "dependencies": {
24
- "@sanctumterra/raknet": "^1.4.7",
24
+ "@sanctumterra/raknet": "^1.4.11",
25
25
  "@serenityjs/binarystream": "^3.0.10",
26
- "@serenityjs/protocol": "^0.8.14",
27
- "@xboxreplay/xboxlive-auth": "^5.1.0",
26
+ "@serenityjs/protocol": "^0.8.17",
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.17.1",
35
+ "@types/node": "^22.19.7",
35
36
  "@types/uuid-1345": "^0.99.25",
36
- "typescript": "^5.9.2"
37
+ "typescript": "^5.9.3"
37
38
  }
38
- }
39
+ }