baltica 0.1.20 → 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:
@@ -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,40 @@
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;
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;
13
25
  cacheDir?: string;
26
+ proxy?: ProxyOptions;
14
27
  }
15
28
  /**
16
29
  * Authenticates with Xbox Live using email/password and obtains Minecraft Bedrock tokens
17
- * Caches Xbox user token (~14 days valid) to minimize login requests
30
+ * Supports SOCKS5 proxy for all authentication requests
18
31
  */
19
32
  export declare function authenticateWithCredentials(options: AuthOptions): Promise<BedrockTokens>;
20
- export { live, xnet };
21
- 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,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,14 @@ 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 XBOX_AUTH_CLIENT_ID = "00000000441cc96b";
45
52
  function hashString(str) {
46
53
  let hash = 0;
47
54
  for (let i = 0; i < str.length; i++) {
@@ -63,7 +70,6 @@ function loadCache(cacheFile) {
63
70
  try {
64
71
  if (fs.existsSync(cacheFile)) {
65
72
  const cached = JSON.parse(fs.readFileSync(cacheFile, "utf-8"));
66
- // Check if token is still valid (with 1 hour buffer)
67
73
  const expiresAt = new Date(cached.notAfter).getTime();
68
74
  if (Date.now() < expiresAt - 3600000) {
69
75
  return cached;
@@ -91,16 +97,47 @@ function saveCache(cacheFile, userToken, userHash, notAfter) {
91
97
  /* ignore */
92
98
  }
93
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
+ }
94
120
  /**
95
121
  * Authenticates with Xbox Live using email/password and obtains Minecraft Bedrock tokens
96
- * Caches Xbox user token (~14 days valid) to minimize login requests
122
+ * Supports SOCKS5 proxy for all authentication requests
97
123
  */
98
124
  async function authenticateWithCredentials(options) {
99
- const { email, password, clientPublicKey, cacheDir } = options;
125
+ const { email, password, clientPublicKey, cacheDir, proxy } = options;
100
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
+ }
101
139
  let userToken;
102
140
  let userHash;
103
- // Try to use cached user token first
104
141
  const cached = cacheFile ? loadCache(cacheFile) : null;
105
142
  if (cached) {
106
143
  raknet_1.Logger.info("Using cached Xbox user token...");
@@ -108,49 +145,239 @@ async function authenticateWithCredentials(options) {
108
145
  userHash = cached.userHash;
109
146
  }
110
147
  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");
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);
116
151
  userToken = userTokenResp.Token;
117
152
  userHash = userTokenResp.DisplayClaims.xui[0].uhs;
118
- // Cache the user token
119
153
  if (cacheFile) {
120
154
  saveCache(cacheFile, userToken, userHash, userTokenResp.NotAfter);
121
155
  }
122
156
  }
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
- });
157
+ const xstsResp = await exchangeTokenForXSTSToken(userToken, MINECRAFT_BEDROCK_RELYING_PARTY, proxiedFetch);
128
158
  const xuid = xstsResp.DisplayClaims.xui[0].xid || "";
129
- // Get Minecraft Bedrock chains
130
- const chains = await getMinecraftBedrockChains(xstsResp.Token, userHash, clientPublicKey);
159
+ const chains = await getMinecraftBedrockChains(xstsResp.Token, userHash, clientPublicKey, proxiedFetch);
131
160
  const gamertag = extractGamertagFromChains(chains);
132
161
  raknet_1.Logger.info(`Authenticated as: ${gamertag} (${xuid})`);
133
162
  return { chains, xuid, gamertag, userHash };
134
163
  }
135
- async function freshLogin(email, password) {
136
- try {
137
- const liveToken = await xboxlive_auth_1.live.authenticateWithCredentials({
138
- email: email,
139
- password,
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",
140
252
  });
141
- return liveToken.access_token;
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]}`);
274
+ }
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;
301
+ }
302
+ }
303
+ }
304
+ if (!ppft) {
305
+ throw new Error("Failed to extract PPFT token from login page");
142
306
  }
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.");
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;
148
319
  }
149
- throw error;
150
320
  }
321
+ return { ppft, urlPost };
322
+ }
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();
151
375
  }
152
- async function getMinecraftBedrockChains(xstsToken, userHash, clientPublicKey) {
153
- const response = await fetch("https://multiplayer.minecraft.net/authentication", {
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", {
154
381
  method: "POST",
155
382
  headers: {
156
383
  "Content-Type": "application/json",
@@ -162,11 +389,8 @@ async function getMinecraftBedrockChains(xstsToken, userHash, clientPublicKey) {
162
389
  if (!response.ok) {
163
390
  const text = await response.text();
164
391
  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");
392
+ throw new Error("Minecraft Bedrock authentication failed (401).\n" +
393
+ "The account may not have an Xbox profile or Minecraft Bedrock Edition.");
170
394
  }
171
395
  throw new Error(`Minecraft Bedrock auth failed: ${response.status} - ${text}`);
172
396
  }
@@ -107,6 +107,7 @@ 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,
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.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.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",
35
36
  "@types/uuid-1345": "^0.99.25",
36
37
  "typescript": "^5.9.3"
37
38
  }
38
- }
39
+ }