baltica 0.1.18 → 0.1.20

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.
@@ -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
  /**
@@ -35,16 +35,16 @@ class Client extends shared_1.Emitter {
35
35
  ? new worker_1.WorkerClient({
36
36
  address: this.options.address,
37
37
  port: this.options.port,
38
+ proxy: this.options.proxy,
38
39
  })
39
40
  : new raknet_1.Client({
40
41
  address: this.options.address,
41
42
  port: this.options.port,
43
+ proxy: this.options.proxy,
42
44
  });
43
45
  /** Create ClientData to store and handle auth data */
44
46
  this.data = new types_2.ClientData(this);
45
- const time = Date.now();
46
- /** Session event gets mojang (minecraft) auth session */
47
- /** NOTE! This takes like 30-100ms for offline mode which kinda feels slow but not really */
47
+ this.raknet.on("disconnect", () => this.cleanup());
48
48
  this.once("session", () => {
49
49
  this.sessionReady = true;
50
50
  });
@@ -182,11 +182,23 @@ class Client extends shared_1.Emitter {
182
182
  }
183
183
  });
184
184
  this.on("DisconnectPacket", (packet) => {
185
- this.status = raknet_1.ConnectionStatus.Disconnected;
186
- this.raknet.disconnect();
187
- console.log(packet.message);
185
+ this.disconnect(packet.message.message ?? undefined);
188
186
  });
189
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
+ }
190
202
  startEncryption(iv) {
191
203
  this.packetEncryptor = new shared_1.PacketEncryptor(this, iv);
192
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
  };
@@ -47,6 +47,17 @@ export type ClientOptions = {
47
47
  email?: string;
48
48
  /** Password for direct email/password authentication (requires 2FA disabled) */
49
49
  password?: string;
50
+ /** SOCKS5 Proxy for the client to use. */
51
+ proxy?: {
52
+ /** Proxy Host */
53
+ host: string;
54
+ /** Proxy Port */
55
+ port: number;
56
+ /** Proxy Username */
57
+ userId?: string;
58
+ /** Proxy Password */
59
+ password?: string;
60
+ };
50
61
  };
51
62
  /** Default Client Options */
52
63
  export declare const defaultClientOptions: ClientOptions;
@@ -1,4 +1,4 @@
1
- import { authenticate as xboxAuthenticate, live, xnet } from "@xboxreplay/xboxlive-auth";
1
+ import { live, xnet } from "@xboxreplay/xboxlive-auth";
2
2
  import type { AuthenticateResponse, Email } from "@xboxreplay/xboxlive-auth";
3
3
  export interface BedrockTokens {
4
4
  chains: string[];
@@ -10,11 +10,12 @@ export interface AuthOptions {
10
10
  email: string;
11
11
  password: string;
12
12
  clientPublicKey: string;
13
+ cacheDir?: string;
13
14
  }
14
15
  /**
15
16
  * Authenticates with Xbox Live using email/password and obtains Minecraft Bedrock tokens
16
- * NOTE: Only works if 2FA is DISABLED on the Microsoft account
17
+ * Caches Xbox user token (~14 days valid) to minimize login requests
17
18
  */
18
19
  export declare function authenticateWithCredentials(options: AuthOptions): Promise<BedrockTokens>;
19
- export { xboxAuthenticate as authenticate, live, xnet };
20
+ export { live, xnet };
20
21
  export type { AuthenticateResponse, Email };
@@ -1,39 +1,144 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.xnet = exports.live = exports.authenticate = void 0;
36
+ exports.xnet = exports.live = void 0;
4
37
  exports.authenticateWithCredentials = authenticateWithCredentials;
5
38
  const xboxlive_auth_1 = require("@xboxreplay/xboxlive-auth");
6
- Object.defineProperty(exports, "authenticate", { enumerable: true, get: function () { return xboxlive_auth_1.authenticate; } });
7
39
  Object.defineProperty(exports, "live", { enumerable: true, get: function () { return xboxlive_auth_1.live; } });
8
40
  Object.defineProperty(exports, "xnet", { enumerable: true, get: function () { return xboxlive_auth_1.xnet; } });
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
9
43
  const raknet_1 = require("@sanctumterra/raknet");
10
44
  const MINECRAFT_BEDROCK_RELYING_PARTY = "https://multiplayer.minecraft.net/";
45
+ function hashString(str) {
46
+ let hash = 0;
47
+ for (let i = 0; i < str.length; i++) {
48
+ const char = str.charCodeAt(i);
49
+ hash = (hash << 5) - hash + char;
50
+ hash = hash & hash;
51
+ }
52
+ return Math.abs(hash).toString(16).slice(0, 6);
53
+ }
54
+ function ensureDir(dir) {
55
+ if (!fs.existsSync(dir)) {
56
+ fs.mkdirSync(dir, { recursive: true });
57
+ }
58
+ }
59
+ function getCacheFile(cacheDir, email) {
60
+ return path.join(cacheDir, `${hashString(email)}_xbl-user-cache.json`);
61
+ }
62
+ function loadCache(cacheFile) {
63
+ try {
64
+ if (fs.existsSync(cacheFile)) {
65
+ const cached = JSON.parse(fs.readFileSync(cacheFile, "utf-8"));
66
+ // Check if token is still valid (with 1 hour buffer)
67
+ const expiresAt = new Date(cached.notAfter).getTime();
68
+ if (Date.now() < expiresAt - 3600000) {
69
+ return cached;
70
+ }
71
+ raknet_1.Logger.info("Cached user token expired");
72
+ }
73
+ }
74
+ catch {
75
+ /* ignore */
76
+ }
77
+ return null;
78
+ }
79
+ function saveCache(cacheFile, userToken, userHash, notAfter) {
80
+ try {
81
+ ensureDir(path.dirname(cacheFile));
82
+ const data = {
83
+ userToken,
84
+ userHash,
85
+ notAfter,
86
+ obtainedOn: Date.now(),
87
+ };
88
+ fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2));
89
+ }
90
+ catch {
91
+ /* ignore */
92
+ }
93
+ }
11
94
  /**
12
95
  * Authenticates with Xbox Live using email/password and obtains Minecraft Bedrock tokens
13
- * NOTE: Only works if 2FA is DISABLED on the Microsoft account
96
+ * Caches Xbox user token (~14 days valid) to minimize login requests
14
97
  */
15
98
  async function authenticateWithCredentials(options) {
16
- const { email, password, clientPublicKey } = options;
17
- raknet_1.Logger.info("Authenticating with Xbox Live...");
99
+ const { email, password, clientPublicKey, cacheDir } = options;
100
+ const cacheFile = cacheDir ? getCacheFile(cacheDir, email) : null;
101
+ let userToken;
102
+ let userHash;
103
+ // Try to use cached user token first
104
+ const cached = cacheFile ? loadCache(cacheFile) : null;
105
+ if (cached) {
106
+ raknet_1.Logger.info("Using cached Xbox user token...");
107
+ userToken = cached.userToken;
108
+ userHash = cached.userHash;
109
+ }
110
+ 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");
116
+ userToken = userTokenResp.Token;
117
+ userHash = userTokenResp.DisplayClaims.xui[0].uhs;
118
+ // Cache the user token
119
+ if (cacheFile) {
120
+ saveCache(cacheFile, userToken, userHash, userTokenResp.NotAfter);
121
+ }
122
+ }
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
+ });
128
+ const xuid = xstsResp.DisplayClaims.xui[0].xid || "";
129
+ // Get Minecraft Bedrock chains
130
+ const chains = await getMinecraftBedrockChains(xstsResp.Token, userHash, clientPublicKey);
131
+ const gamertag = extractGamertagFromChains(chains);
132
+ raknet_1.Logger.info(`Authenticated as: ${gamertag} (${xuid})`);
133
+ return { chains, xuid, gamertag, userHash };
134
+ }
135
+ async function freshLogin(email, password) {
18
136
  try {
19
137
  const liveToken = await xboxlive_auth_1.live.authenticateWithCredentials({
20
138
  email: email,
21
139
  password,
22
140
  });
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",
30
- });
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 };
141
+ return liveToken.access_token;
37
142
  }
38
143
  catch (error) {
39
144
  const err = error;
@@ -56,6 +161,13 @@ async function getMinecraftBedrockChains(xstsToken, userHash, clientPublicKey) {
56
161
  });
57
162
  if (!response.ok) {
58
163
  const text = await response.text();
164
+ 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");
170
+ }
59
171
  throw new Error(`Minecraft Bedrock auth failed: ${response.status} - ${text}`);
60
172
  }
61
173
  const data = (await response.json());
@@ -106,6 +106,7 @@ 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,
109
110
  });
110
111
  const profile = {
111
112
  name: tokens.gamertag,
@@ -120,6 +121,7 @@ async function authenticateWithEmailPassword(client) {
120
121
  }
121
122
  catch (error) {
122
123
  raknet_1.Logger.error(`Email/password authentication failed: ${error instanceof Error ? error.message : String(error)}`);
124
+ raknet_1.Logger.warn("Make sure you have an xbox profile crated!");
123
125
  throw error;
124
126
  }
125
127
  }
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.18",
4
+ "version": "0.1.20",
5
5
  "minecraft": "1.21.130",
6
6
  "main": "dist/index.js",
7
7
  "license": "MIT",
@@ -21,9 +21,9 @@
21
21
  }
22
22
  ],
23
23
  "dependencies": {
24
- "@sanctumterra/raknet": "^1.4.7",
24
+ "@sanctumterra/raknet": "^1.4.8",
25
25
  "@serenityjs/binarystream": "^3.0.10",
26
- "@serenityjs/protocol": "^0.8.14",
26
+ "@serenityjs/protocol": "^0.8.17",
27
27
  "@xboxreplay/xboxlive-auth": "^5.1.0",
28
28
  "jose": "^5.10.0",
29
29
  "prismarine-auth": "^2.7.0",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "devDependencies": {
33
33
  "@biomejs/biome": "1.9.4",
34
- "@types/node": "^22.17.1",
34
+ "@types/node": "^22.19.6",
35
35
  "@types/uuid-1345": "^0.99.25",
36
- "typescript": "^5.9.2"
36
+ "typescript": "^5.9.3"
37
37
  }
38
38
  }