baltica 0.1.19 → 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
  /**
@@ -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
  };
@@ -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;
@@ -41,22 +146,6 @@ async function authenticateWithCredentials(options) {
41
146
  throw new Error("Authentication failed: Invalid credentials or 2FA is enabled.\n" +
42
147
  "Direct email/password login only works with 2FA DISABLED.");
43
148
  }
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]}`);
58
- }
59
- }
60
149
  throw error;
61
150
  }
62
151
  }
@@ -72,6 +161,13 @@ async function getMinecraftBedrockChains(xstsToken, userHash, clientPublicKey) {
72
161
  });
73
162
  if (!response.ok) {
74
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
+ }
75
171
  throw new Error(`Minecraft Bedrock auth failed: ${response.status} - ${text}`);
76
172
  }
77
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,
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.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
  }