@loontail/minecraft-kit 0.6.0 → 0.6.1

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/dist/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var process2 = require('process');
3
4
  var os = require('os');
4
5
  var lruCache = require('lru-cache');
5
6
  var path = require('path');
@@ -15,12 +16,324 @@ var child_process = require('child_process');
15
16
 
16
17
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
17
18
 
19
+ var process2__default = /*#__PURE__*/_interopDefault(process2);
18
20
  var os__default = /*#__PURE__*/_interopDefault(os);
19
21
  var path__default = /*#__PURE__*/_interopDefault(path);
20
22
  var yauzl__default = /*#__PURE__*/_interopDefault(yauzl);
21
23
  var crypto2__default = /*#__PURE__*/_interopDefault(crypto2);
22
24
  var fs__default = /*#__PURE__*/_interopDefault(fs);
23
25
 
26
+ // src/types/errors.ts
27
+ var MinecraftKitErrorCodes = {
28
+ NETWORK_TIMEOUT: "NETWORK_TIMEOUT",
29
+ NETWORK_HTTP_ERROR: "NETWORK_HTTP_ERROR",
30
+ NETWORK_ABORTED: "NETWORK_ABORTED",
31
+ INTEGRITY_HASH_MISMATCH: "INTEGRITY_HASH_MISMATCH",
32
+ INTEGRITY_SIZE_MISMATCH: "INTEGRITY_SIZE_MISMATCH",
33
+ MANIFEST_INVALID: "MANIFEST_INVALID",
34
+ MANIFEST_NOT_FOUND: "MANIFEST_NOT_FOUND",
35
+ METADATA_PARSE_ERROR: "METADATA_PARSE_ERROR",
36
+ FILESYSTEM_PATH_TRAVERSAL: "FILESYSTEM_PATH_TRAVERSAL",
37
+ FILESYSTEM_WRITE_ERROR: "FILESYSTEM_WRITE_ERROR",
38
+ FILESYSTEM_READ_ERROR: "FILESYSTEM_READ_ERROR",
39
+ ARCHIVE_INVALID: "ARCHIVE_INVALID",
40
+ ARCHIVE_TOO_LARGE: "ARCHIVE_TOO_LARGE",
41
+ ARCHIVE_ENTRY_REJECTED: "ARCHIVE_ENTRY_REJECTED",
42
+ RUNTIME_NOT_FOUND: "RUNTIME_NOT_FOUND",
43
+ RUNTIME_UNSUPPORTED_PLATFORM: "RUNTIME_UNSUPPORTED_PLATFORM",
44
+ FORGE_PROCESSOR_FAILED: "FORGE_PROCESSOR_FAILED",
45
+ FORGE_INSTALLER_INVALID: "FORGE_INSTALLER_INVALID",
46
+ LAUNCH_JAVA_NOT_FOUND: "LAUNCH_JAVA_NOT_FOUND",
47
+ LAUNCH_PROCESS_FAILED: "LAUNCH_PROCESS_FAILED",
48
+ LAUNCH_ABORTED: "LAUNCH_ABORTED",
49
+ VERIFICATION_FAILED: "VERIFICATION_FAILED",
50
+ INVALID_INPUT: "INVALID_INPUT",
51
+ NOT_IMPLEMENTED: "NOT_IMPLEMENTED",
52
+ UNSUPPORTED_VERSION: "UNSUPPORTED_VERSION",
53
+ LZMA_DECODE_ERROR: "LZMA_DECODE_ERROR",
54
+ AUTH_DEVICE_CODE_EXPIRED: "AUTH_DEVICE_CODE_EXPIRED",
55
+ AUTH_DEVICE_CODE_DECLINED: "AUTH_DEVICE_CODE_DECLINED",
56
+ AUTH_DEVICE_CODE_FAILED: "AUTH_DEVICE_CODE_FAILED",
57
+ AUTH_REFRESH_FAILED: "AUTH_REFRESH_FAILED",
58
+ AUTH_XBOX_FAILED: "AUTH_XBOX_FAILED",
59
+ AUTH_XSTS_FAILED: "AUTH_XSTS_FAILED",
60
+ AUTH_MINECRAFT_FAILED: "AUTH_MINECRAFT_FAILED",
61
+ AUTH_NO_GAME_OWNERSHIP: "AUTH_NO_GAME_OWNERSHIP",
62
+ AUTH_MISSING_CLIENT_ID: "AUTH_MISSING_CLIENT_ID",
63
+ AUTH_CANCELLED: "AUTH_CANCELLED"
64
+ };
65
+
66
+ // src/core/errors.ts
67
+ var MinecraftKitError = class extends Error {
68
+ name = "MinecraftKitError";
69
+ /** Stable discriminator. */
70
+ code;
71
+ /** Structured context; safe to serialize. */
72
+ context;
73
+ constructor(code, message, options = {}) {
74
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
75
+ Object.setPrototypeOf(this, new.target.prototype);
76
+ this.code = code;
77
+ this.context = Object.freeze({ ...options.context ?? {} });
78
+ }
79
+ /** JSON-friendly representation. */
80
+ toJSON() {
81
+ return {
82
+ name: this.name,
83
+ code: this.code,
84
+ message: this.message,
85
+ context: this.context,
86
+ cause: this.cause instanceof Error ? { name: this.cause.name, message: this.cause.message } : this.cause
87
+ };
88
+ }
89
+ };
90
+ var isMinecraftKitError = (e) => {
91
+ return e instanceof MinecraftKitError;
92
+ };
93
+ var isErrorCode = (e, code) => {
94
+ return isMinecraftKitError(e) && e.code === code;
95
+ };
96
+
97
+ // src/types/auth.ts
98
+ var AuthModes = {
99
+ /** Offline-mode play with a chosen username and synthetic UUID. */
100
+ OFFLINE: "offline",
101
+ /** Pre-authenticated session — caller provides the access token and identity. */
102
+ ONLINE: "online"
103
+ };
104
+
105
+ // src/auth/microsoft.ts
106
+ var TENANT = "consumers";
107
+ var DEVICE_CODE_URL = `https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/devicecode`;
108
+ var TOKEN_URL = `https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/token`;
109
+ var SCOPE = "XboxLive.signin offline_access";
110
+ var startDeviceCode = async (input) => {
111
+ const body = new URLSearchParams({ client_id: input.clientId, scope: SCOPE });
112
+ const response = await input.http.request(DEVICE_CODE_URL, {
113
+ method: "POST",
114
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
115
+ body: body.toString(),
116
+ // We need to read the body of 400/401 responses to surface Microsoft's actual
117
+ // `error_description` rather than reporting "HTTP 400" — the most common cause is
118
+ // an app registration that doesn't allow personal MSA accounts or hasn't enabled
119
+ // public client flows.
120
+ acceptNonOk: true,
121
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
122
+ });
123
+ if (response.status < 200 || response.status >= 300) {
124
+ const err = await response.json().catch(() => ({}));
125
+ throw new MinecraftKitError(
126
+ MinecraftKitErrorCodes.AUTH_DEVICE_CODE_FAILED,
127
+ explainDeviceCodeError(err, input.clientId),
128
+ {
129
+ context: {
130
+ httpStatus: response.status,
131
+ microsoftError: err.error,
132
+ clientId: input.clientId
133
+ }
134
+ }
135
+ );
136
+ }
137
+ const data = await response.json();
138
+ const now = Date.now();
139
+ const state = {
140
+ deviceCode: data.device_code,
141
+ userCode: data.user_code,
142
+ verificationUri: data.verification_uri,
143
+ message: data.message,
144
+ expiresIn: data.expires_in,
145
+ interval: data.interval,
146
+ clientId: input.clientId,
147
+ expiresAt: now + data.expires_in * 1e3
148
+ };
149
+ const prompt = {
150
+ userCode: data.user_code,
151
+ verificationUri: data.verification_uri,
152
+ message: data.message,
153
+ expiresIn: data.expires_in,
154
+ interval: data.interval
155
+ };
156
+ return { prompt, state };
157
+ };
158
+ var pollDeviceCode = async (input) => {
159
+ let intervalSec = input.state.interval;
160
+ while (true) {
161
+ if (input.signal?.aborted) {
162
+ throw new MinecraftKitError(
163
+ MinecraftKitErrorCodes.AUTH_CANCELLED,
164
+ "Device-code polling aborted.",
165
+ {
166
+ context: { reason: input.signal.reason }
167
+ }
168
+ );
169
+ }
170
+ if (Date.now() >= input.state.expiresAt) {
171
+ throw new MinecraftKitError(
172
+ MinecraftKitErrorCodes.AUTH_DEVICE_CODE_EXPIRED,
173
+ "Device code expired before the user signed in."
174
+ );
175
+ }
176
+ const delayMs = intervalSec * 1e3;
177
+ input.onTick?.({ nextDelayMs: delayMs, expiresAt: input.state.expiresAt });
178
+ await wait(delayMs, input.signal);
179
+ const body = new URLSearchParams({
180
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
181
+ client_id: input.state.clientId,
182
+ device_code: input.state.deviceCode
183
+ });
184
+ const response = await input.http.request(TOKEN_URL, {
185
+ method: "POST",
186
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
187
+ body: body.toString(),
188
+ acceptNonOk: true
189
+ });
190
+ if (response.status >= 200 && response.status < 300) {
191
+ const ok = await response.json();
192
+ if (!ok.refresh_token) {
193
+ throw new MinecraftKitError(
194
+ MinecraftKitErrorCodes.AUTH_DEVICE_CODE_FAILED,
195
+ "Microsoft did not return a refresh token. Make sure `offline_access` is in the requested scopes."
196
+ );
197
+ }
198
+ return {
199
+ accessToken: ok.access_token,
200
+ refreshToken: ok.refresh_token,
201
+ expiresIn: ok.expires_in
202
+ };
203
+ }
204
+ const err = await response.json().catch(() => ({}));
205
+ switch (err.error) {
206
+ case "authorization_pending":
207
+ continue;
208
+ case "slow_down":
209
+ intervalSec += 5;
210
+ continue;
211
+ case "authorization_declined":
212
+ throw new MinecraftKitError(
213
+ MinecraftKitErrorCodes.AUTH_DEVICE_CODE_DECLINED,
214
+ "The user declined the sign-in request."
215
+ );
216
+ case "expired_token":
217
+ throw new MinecraftKitError(
218
+ MinecraftKitErrorCodes.AUTH_DEVICE_CODE_EXPIRED,
219
+ "Device code expired before the user signed in."
220
+ );
221
+ default:
222
+ throw new MinecraftKitError(
223
+ MinecraftKitErrorCodes.AUTH_DEVICE_CODE_FAILED,
224
+ `Microsoft device-code exchange failed: ${err.error ?? "unknown_error"}${err.error_description ? ` \u2014 ${err.error_description}` : ""}`,
225
+ { context: { httpStatus: response.status, microsoftError: err.error } }
226
+ );
227
+ }
228
+ }
229
+ };
230
+ var refreshMicrosoftToken = async (input) => {
231
+ const body = new URLSearchParams({
232
+ grant_type: "refresh_token",
233
+ client_id: input.clientId,
234
+ refresh_token: input.refreshToken,
235
+ scope: SCOPE
236
+ });
237
+ const response = await input.http.request(TOKEN_URL, {
238
+ method: "POST",
239
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
240
+ body: body.toString(),
241
+ acceptNonOk: true,
242
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
243
+ });
244
+ if (response.status < 200 || response.status >= 300) {
245
+ const err = await response.json().catch(() => ({}));
246
+ throw new MinecraftKitError(
247
+ MinecraftKitErrorCodes.AUTH_REFRESH_FAILED,
248
+ `Microsoft refused to refresh the token: ${err.error ?? "unknown_error"}${err.error_description ? ` \u2014 ${err.error_description}` : ""}`,
249
+ { context: { httpStatus: response.status, microsoftError: err.error } }
250
+ );
251
+ }
252
+ const ok = await response.json();
253
+ return {
254
+ accessToken: ok.access_token,
255
+ refreshToken: ok.refresh_token ?? input.refreshToken,
256
+ expiresIn: ok.expires_in
257
+ };
258
+ };
259
+ var explainDeviceCodeError = (err, clientId) => {
260
+ const desc = err.error_description ?? "";
261
+ const ms = desc ? ` \u2014 ${desc}` : "";
262
+ if (/AADSTS700016/i.test(desc) || /not found in the directory/i.test(desc)) {
263
+ return `Microsoft cannot see app ${clientId} from the consumers tenant. Fix: Azure portal \u2192 your app \u2192 Authentication \u2192 "Supported account types" \u2192 choose "Personal Microsoft accounts only" or "Multitenant + personal accounts" \u2192 Save. Wait ~30s for propagation.${ms}`;
264
+ }
265
+ if (/AADSTS7000218/i.test(desc) || /must either be a confidential client/i.test(desc)) {
266
+ return `Microsoft rejected the client_id (${clientId}): "Allow public client flows" is OFF. Fix: Azure portal \u2192 your app \u2192 Authentication \u2192 bottom of the page \u2192 toggle "Allow public client flows" to Yes \u2192 Save.${ms}`;
267
+ }
268
+ if (/AADSTS50059/i.test(desc) || /tenant identifier/i.test(desc)) {
269
+ return `Microsoft Entra cannot route the request \u2014 the app's "Supported account types" excludes consumers. Fix: Azure portal \u2192 Authentication \u2192 set Supported account types to include personal MSA \u2192 Save.${ms}`;
270
+ }
271
+ switch (err.error) {
272
+ case "unauthorized_client":
273
+ return `Microsoft rejected the client_id (${clientId}). Likely cause: "Supported account types" excludes personal Microsoft accounts, OR "Allow public client flows" is disabled. Fix both in Azure portal \u2192 your app \u2192 Authentication.${ms}`;
274
+ case "invalid_client":
275
+ return `Microsoft does not recognise client_id ${clientId}. Make sure you pasted the Application (client) ID \u2014 not the Object ID or Tenant ID \u2014 and that the app exists.${ms}`;
276
+ case "invalid_request":
277
+ return `Microsoft rejected the device-code request as malformed.${ms}`;
278
+ case "invalid_scope":
279
+ return `Microsoft refused the requested scope (XboxLive.signin offline_access). Make sure the app is configured for Microsoft account sign-in.${ms}`;
280
+ default:
281
+ return `Microsoft device-code request failed: ${err.error ?? "unknown_error"}${ms}`;
282
+ }
283
+ };
284
+ var wait = (ms, signal) => {
285
+ return new Promise((resolve, reject) => {
286
+ if (signal?.aborted) {
287
+ reject(
288
+ new MinecraftKitError(
289
+ MinecraftKitErrorCodes.AUTH_CANCELLED,
290
+ "Device-code polling aborted.",
291
+ {
292
+ context: { reason: signal.reason }
293
+ }
294
+ )
295
+ );
296
+ return;
297
+ }
298
+ const timer = setTimeout(() => {
299
+ signal?.removeEventListener("abort", onAbort);
300
+ resolve();
301
+ }, ms);
302
+ const onAbort = () => {
303
+ clearTimeout(timer);
304
+ reject(
305
+ new MinecraftKitError(
306
+ MinecraftKitErrorCodes.AUTH_CANCELLED,
307
+ "Device-code polling aborted.",
308
+ {
309
+ context: { reason: signal?.reason }
310
+ }
311
+ )
312
+ );
313
+ };
314
+ signal?.addEventListener("abort", onAbort, { once: true });
315
+ });
316
+ };
317
+
318
+ // src/core/json.ts
319
+ var parseJsonStrict = (text, options) => {
320
+ try {
321
+ return JSON.parse(text);
322
+ } catch (cause) {
323
+ throw new MinecraftKitError(options.code, options.message, {
324
+ cause,
325
+ ...options.context !== void 0 ? { context: options.context } : {}
326
+ });
327
+ }
328
+ };
329
+ var parseJsonOrUndefined = (text) => {
330
+ try {
331
+ return JSON.parse(text);
332
+ } catch {
333
+ return void 0;
334
+ }
335
+ };
336
+
24
337
  // src/types/logger.ts
25
338
  var LogLevels = {
26
339
  DEBUG: "debug",
@@ -44,12 +357,361 @@ var consoleLogger = {
44
357
  }
45
358
  }
46
359
  };
47
- function pickConsole(level) {
360
+ var scopedLogger = (base, scope, baseFields) => {
361
+ if (base === silentLogger) return silentLogger;
362
+ return {
363
+ log(level, message, fields) {
364
+ const merged = baseFields !== void 0 || fields !== void 0 ? { ...baseFields, ...fields } : void 0;
365
+ if (merged !== void 0) {
366
+ base.log(level, `[${scope}] ${message}`, merged);
367
+ } else {
368
+ base.log(level, `[${scope}] ${message}`);
369
+ }
370
+ }
371
+ };
372
+ };
373
+ var pickConsole = (level) => {
48
374
  if (level === LogLevels.ERROR) return console.error.bind(console);
49
375
  if (level === LogLevels.WARN) return console.warn.bind(console);
50
376
  if (level === LogLevels.INFO) return console.info.bind(console);
51
377
  return console.debug.bind(console);
52
- }
378
+ };
379
+
380
+ // src/auth/debug.ts
381
+ var DEBUG_ENV_VAR = "MINECRAFT_KIT_AUTH_DEBUG";
382
+ var authDebug = (message) => {
383
+ if (process2__default.default.env[DEBUG_ENV_VAR]) {
384
+ process2__default.default.stderr.write(`[auth] ${message}
385
+ `);
386
+ }
387
+ };
388
+
389
+ // src/auth/minecraft.ts
390
+ var MC_LOGIN_URL = "https://api.minecraftservices.com/authentication/login_with_xbox";
391
+ var MC_PROFILE_URL = "https://api.minecraftservices.com/minecraft/profile";
392
+ var loginWithXbox = async (input) => {
393
+ const body = JSON.stringify({
394
+ identityToken: `XBL3.0 x=${input.userHash};${input.xstsToken}`
395
+ });
396
+ authDebug(
397
+ `login_with_xbox POST \u2014 userHashLen=${input.userHash.length}, xstsTokenLen=${input.xstsToken.length}`
398
+ );
399
+ const response = await input.http.request(MC_LOGIN_URL, {
400
+ method: "POST",
401
+ headers: {
402
+ "content-type": "application/json",
403
+ accept: "application/json",
404
+ // Mojang sometimes rejects unknown user-agents on the auth endpoints. Override the
405
+ // library default with a UA that matches what real Minecraft launchers send, so we
406
+ // don't trip an "anomalous client" filter.
407
+ "user-agent": "Minecraft Launcher/2.0 (minecraft-kit)"
408
+ },
409
+ body,
410
+ acceptNonOk: true,
411
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
412
+ });
413
+ if (response.status < 200 || response.status >= 300) {
414
+ const rawBody = await response.text().catch(() => "");
415
+ const detail = rawBody.slice(0, 400);
416
+ authDebug(`login_with_xbox failed status=${response.status} body=${detail}`);
417
+ if (response.status === 403) {
418
+ if (/invalid app registration/i.test(detail)) {
419
+ throw new MinecraftKitError(
420
+ MinecraftKitErrorCodes.AUTH_MINECRAFT_FAILED,
421
+ `Mojang has not approved this Azure AD application id for the Minecraft API. The OAuth + Xbox/XSTS exchange all succeeded, but api.minecraftservices.com only accepts client_ids that are on its allow-list. Apply at https://aka.ms/mce-reviewappid (Application ID, contact email, purpose) \u2014 approval typically takes a few days. Raw response: ${detail}`,
422
+ { context: { httpStatus: 403, body: detail, reason: "invalid_app_registration" } }
423
+ );
424
+ }
425
+ throw new MinecraftKitError(
426
+ MinecraftKitErrorCodes.AUTH_NO_GAME_OWNERSHIP,
427
+ `Mojang refused login_with_xbox (HTTP 403). The Xbox/Microsoft exchange succeeded, but api.minecraftservices.com declined to issue a Minecraft token. Most common causes: (1) you signed in to the browser with a DIFFERENT Microsoft account than the one owning Java Edition \u2014 re-check the email on https://www.minecraft.net/profile and make sure it matches what you typed at the device-code page; (2) this account never used Xbox services before \u2014 open https://www.xbox.com once with this account, then retry; (3) transient Mojang 5xx/403, just retry in 60s. Raw response: ${detail || "<empty>"}`,
428
+ { context: { httpStatus: 403, body: detail } }
429
+ );
430
+ }
431
+ throw new MinecraftKitError(
432
+ MinecraftKitErrorCodes.AUTH_MINECRAFT_FAILED,
433
+ `Minecraft sign-in failed with HTTP ${response.status}. Response: ${detail || "<empty>"}`,
434
+ { context: { httpStatus: response.status, body: detail } }
435
+ );
436
+ }
437
+ const parsed = await response.json();
438
+ if (!parsed.access_token) {
439
+ throw new MinecraftKitError(
440
+ MinecraftKitErrorCodes.AUTH_MINECRAFT_FAILED,
441
+ "Minecraft sign-in returned no access token."
442
+ );
443
+ }
444
+ return { accessToken: parsed.access_token, expiresIn: parsed.expires_in };
445
+ };
446
+ var fetchMinecraftProfile = async (input) => {
447
+ const response = await input.http.request(MC_PROFILE_URL, {
448
+ headers: {
449
+ authorization: `Bearer ${input.accessToken}`,
450
+ accept: "application/json",
451
+ "user-agent": "Minecraft Launcher/2.0 (minecraft-kit)"
452
+ },
453
+ acceptNonOk: true,
454
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
455
+ });
456
+ if (response.status === 404) {
457
+ throw new MinecraftKitError(
458
+ MinecraftKitErrorCodes.AUTH_NO_GAME_OWNERSHIP,
459
+ "This Microsoft account does not own Minecraft: Java Edition.",
460
+ { context: { httpStatus: 404 } }
461
+ );
462
+ }
463
+ if (response.status < 200 || response.status >= 300) {
464
+ throw new MinecraftKitError(
465
+ MinecraftKitErrorCodes.AUTH_MINECRAFT_FAILED,
466
+ `Failed to load Minecraft profile (HTTP ${response.status}).`,
467
+ { context: { httpStatus: response.status } }
468
+ );
469
+ }
470
+ const parsed = await response.json();
471
+ if (parsed.errorMessage || !parsed.id || !parsed.name) {
472
+ throw new MinecraftKitError(
473
+ MinecraftKitErrorCodes.AUTH_MINECRAFT_FAILED,
474
+ parsed.errorMessage ?? "Minecraft profile response was malformed."
475
+ );
476
+ }
477
+ return { uuid: dashUuid(parsed.id), username: parsed.name };
478
+ };
479
+ var extractXuid = (accessToken) => {
480
+ const parts = accessToken.split(".");
481
+ const payload = parts[1];
482
+ if (typeof payload !== "string") return "";
483
+ const json = Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString(
484
+ "utf8"
485
+ );
486
+ const parsed = parseJsonOrUndefined(json);
487
+ return typeof parsed?.xuid === "string" ? parsed.xuid : "";
488
+ };
489
+ var dashUuid = (raw) => {
490
+ if (raw.includes("-")) return raw;
491
+ if (raw.length !== 32) return raw;
492
+ return `${raw.slice(0, 8)}-${raw.slice(8, 12)}-${raw.slice(12, 16)}-${raw.slice(
493
+ 16,
494
+ 20
495
+ )}-${raw.slice(20)}`;
496
+ };
497
+
498
+ // src/auth/xbox.ts
499
+ var XBL_URL = "https://user.auth.xboxlive.com/user/authenticate";
500
+ var XSTS_URL = "https://xsts.auth.xboxlive.com/xsts/authorize";
501
+ var authenticateXbl = async (input) => {
502
+ const body = JSON.stringify({
503
+ Properties: {
504
+ AuthMethod: "RPS",
505
+ SiteName: "user.auth.xboxlive.com",
506
+ RpsTicket: `d=${input.accessToken}`
507
+ },
508
+ RelyingParty: "http://auth.xboxlive.com",
509
+ TokenType: "JWT"
510
+ });
511
+ let response;
512
+ try {
513
+ response = await input.http.request(XBL_URL, {
514
+ method: "POST",
515
+ headers: { "content-type": "application/json", accept: "application/json" },
516
+ body,
517
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
518
+ });
519
+ } catch (cause) {
520
+ throw new MinecraftKitError(
521
+ MinecraftKitErrorCodes.AUTH_XBOX_FAILED,
522
+ "Xbox Live authentication failed.",
523
+ {
524
+ cause
525
+ }
526
+ );
527
+ }
528
+ const parsed = await response.json();
529
+ const userHash = parsed.DisplayClaims?.xui?.[0]?.uhs;
530
+ if (!parsed.Token || !userHash) {
531
+ throw new MinecraftKitError(
532
+ MinecraftKitErrorCodes.AUTH_XBOX_FAILED,
533
+ "Xbox Live authentication returned an incomplete response."
534
+ );
535
+ }
536
+ input.logger?.log("debug", `XBL ok \u2014 tokenLen=${parsed.Token.length}, userHash=${userHash}`);
537
+ return { token: parsed.Token, userHash };
538
+ };
539
+ var authenticateXsts = async (input) => {
540
+ const body = JSON.stringify({
541
+ Properties: { SandboxId: "RETAIL", UserTokens: [input.xblToken] },
542
+ RelyingParty: "rp://api.minecraftservices.com/",
543
+ TokenType: "JWT"
544
+ });
545
+ const response = await input.http.request(XSTS_URL, {
546
+ method: "POST",
547
+ headers: { "content-type": "application/json", accept: "application/json" },
548
+ body,
549
+ acceptNonOk: true,
550
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
551
+ });
552
+ if (response.status === 401) {
553
+ const err = await response.json().catch(() => ({}));
554
+ throw new MinecraftKitError(MinecraftKitErrorCodes.AUTH_XSTS_FAILED, explainXErr(err.XErr), {
555
+ context: { xerr: err.XErr ?? null, message: err.Message ?? null }
556
+ });
557
+ }
558
+ if (response.status < 200 || response.status >= 300) {
559
+ throw new MinecraftKitError(
560
+ MinecraftKitErrorCodes.AUTH_XSTS_FAILED,
561
+ `XSTS authorization failed with HTTP ${response.status}.`,
562
+ { context: { httpStatus: response.status } }
563
+ );
564
+ }
565
+ const parsed = await response.json();
566
+ const userHash = parsed.DisplayClaims?.xui?.[0]?.uhs;
567
+ if (!parsed.Token || !userHash) {
568
+ throw new MinecraftKitError(
569
+ MinecraftKitErrorCodes.AUTH_XSTS_FAILED,
570
+ "XSTS authorization returned an incomplete response."
571
+ );
572
+ }
573
+ input.logger?.log("debug", `XSTS ok \u2014 tokenLen=${parsed.Token.length}, userHash=${userHash}`);
574
+ return { token: parsed.Token, userHash };
575
+ };
576
+ var explainXErr = (xerr) => {
577
+ switch (xerr) {
578
+ case 2148916227:
579
+ return "This account has been banned from Xbox.";
580
+ case 2148916233:
581
+ return "This Microsoft account has no Xbox profile yet \u2014 sign in once at https://minecraft.net to create one.";
582
+ case 2148916235:
583
+ return "Xbox Live is not available in this account's country/region.";
584
+ case 2148916236:
585
+ case 2148916237:
586
+ return "This account needs an adult to verify it. Sign in on Xbox to complete the prompt.";
587
+ case 2148916238:
588
+ return "This is a child account. Add it to a Microsoft family group with an adult to continue.";
589
+ default:
590
+ return xerr ? `XSTS authorization failed (XErr ${xerr}).` : "XSTS authorization failed.";
591
+ }
592
+ };
593
+
594
+ // src/auth/index.ts
595
+ var CLIENT_ID_ENV_VAR = "MINECRAFT_KIT_MSA_CLIENT_ID";
596
+ var MojangAuthApi = class {
597
+ constructor(http) {
598
+ this.http = http;
599
+ }
600
+ http;
601
+ /**
602
+ * Run the full device-code flow end-to-end. The caller supplies an `onPrompt` callback to
603
+ * render the URL + code to the user; this method polls until they finish.
604
+ */
605
+ async login(options) {
606
+ const clientId = resolveClientId(options.clientId);
607
+ const { prompt, state } = await startDeviceCode({
608
+ http: this.http,
609
+ clientId,
610
+ ...options.signal !== void 0 ? { signal: options.signal } : {}
611
+ });
612
+ await options.onPrompt(prompt);
613
+ const msToken = await pollDeviceCode({
614
+ http: this.http,
615
+ state,
616
+ ...options.signal !== void 0 ? { signal: options.signal } : {},
617
+ ...options.onPoll !== void 0 ? { onTick: options.onPoll } : {}
618
+ });
619
+ return this.completeMicrosoftToken(msToken, clientId, options.signal);
620
+ }
621
+ /** Refresh a previously obtained session. The Microsoft refresh token may be rotated. */
622
+ async refresh(refreshToken, options = {}) {
623
+ const clientId = resolveClientId(options.clientId);
624
+ const msToken = await refreshMicrosoftToken({
625
+ http: this.http,
626
+ refreshToken,
627
+ clientId,
628
+ ...options.signal !== void 0 ? { signal: options.signal } : {}
629
+ });
630
+ return this.completeMicrosoftToken(msToken, clientId, options.signal);
631
+ }
632
+ /**
633
+ * Lower-level device-code surface — exposed so callers can decouple "show prompt" from
634
+ * "block on poll" (e.g. a GUI that lets the user dismiss the modal). Most callers should
635
+ * use {@link login} instead.
636
+ */
637
+ deviceCode = {
638
+ start: (options = {}) => {
639
+ const clientId = resolveClientId(options.clientId);
640
+ return startDeviceCode({
641
+ http: this.http,
642
+ clientId,
643
+ ...options.signal !== void 0 ? { signal: options.signal } : {}
644
+ });
645
+ },
646
+ poll: async (state, options = {}) => {
647
+ const msToken = await pollDeviceCode({
648
+ http: this.http,
649
+ state,
650
+ ...options.signal !== void 0 ? { signal: options.signal } : {},
651
+ ...options.onTick !== void 0 ? { onTick: options.onTick } : {}
652
+ });
653
+ return this.completeMicrosoftToken(msToken, state.clientId, options.signal);
654
+ }
655
+ };
656
+ /** Steps 2 → 5: given a Microsoft access token, finish the flow. */
657
+ async completeMicrosoftToken(msToken, clientId, signal) {
658
+ const signalOpt = signal !== void 0 ? { signal } : {};
659
+ const xbl = await authenticateXbl({
660
+ http: this.http,
661
+ accessToken: msToken.accessToken,
662
+ ...signalOpt
663
+ });
664
+ const xsts = await authenticateXsts({
665
+ http: this.http,
666
+ xblToken: xbl.token,
667
+ ...signalOpt
668
+ });
669
+ const mc = await loginWithXbox({
670
+ http: this.http,
671
+ xstsToken: xsts.token,
672
+ userHash: xsts.userHash,
673
+ ...signalOpt
674
+ });
675
+ const profile = await fetchMinecraftProfile({
676
+ http: this.http,
677
+ accessToken: mc.accessToken,
678
+ ...signalOpt
679
+ });
680
+ return {
681
+ minecraft: {
682
+ username: profile.username,
683
+ uuid: profile.uuid,
684
+ accessToken: mc.accessToken,
685
+ expiresAt: Date.now() + mc.expiresIn * 1e3,
686
+ xuid: extractXuid(mc.accessToken)
687
+ },
688
+ microsoft: {
689
+ refreshToken: msToken.refreshToken,
690
+ clientId
691
+ }
692
+ };
693
+ }
694
+ };
695
+ var toOnlineAuth = (session) => {
696
+ return {
697
+ mode: AuthModes.ONLINE,
698
+ username: session.minecraft.username,
699
+ uuid: session.minecraft.uuid,
700
+ accessToken: session.minecraft.accessToken,
701
+ userType: "msa",
702
+ clientId: session.microsoft.clientId,
703
+ xuid: session.minecraft.xuid
704
+ };
705
+ };
706
+ var resolveClientId = (explicit) => {
707
+ if (typeof explicit === "string" && explicit.trim().length > 0) return explicit.trim();
708
+ const fromEnv = process.env[CLIENT_ID_ENV_VAR];
709
+ if (typeof fromEnv === "string" && fromEnv.trim().length > 0) return fromEnv.trim();
710
+ throw new MinecraftKitError(
711
+ MinecraftKitErrorCodes.AUTH_MISSING_CLIENT_ID,
712
+ `No Azure AD client id supplied. Pass \`clientId\` explicitly or set ${CLIENT_ID_ENV_VAR}. Register an Azure AD application in the 'Personal Microsoft accounts' audience with XboxLive.signin + offline_access scopes to obtain one.`
713
+ );
714
+ };
53
715
 
54
716
  // src/constants/platform.ts
55
717
  var NODE_PLATFORM_TO_MOJANG_OS = {
@@ -68,39 +730,8 @@ var RUNTIME_PLATFORM_KEYS = {
68
730
  linux: { x64: "linux", x86: "linux-i386", arm64: "linux" }
69
731
  };
70
732
 
71
- // src/core/errors.ts
72
- var MinecraftKitError = class extends Error {
73
- name = "MinecraftKitError";
74
- /** Stable discriminator. */
75
- code;
76
- /** Structured context; safe to serialize. */
77
- context;
78
- constructor(code, message, options = {}) {
79
- super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
80
- Object.setPrototypeOf(this, new.target.prototype);
81
- this.code = code;
82
- this.context = Object.freeze({ ...options.context ?? {} });
83
- }
84
- /** JSON-friendly representation. */
85
- toJSON() {
86
- return {
87
- name: this.name,
88
- code: this.code,
89
- message: this.message,
90
- context: this.context,
91
- cause: this.cause instanceof Error ? { name: this.cause.name, message: this.cause.message } : this.cause
92
- };
93
- }
94
- };
95
- function isMinecraftKitError(e) {
96
- return e instanceof MinecraftKitError;
97
- }
98
- function isErrorCode(e, code) {
99
- return isMinecraftKitError(e) && e.code === code;
100
- }
101
-
102
733
  // src/core/system.ts
103
- function detectSystem(input = {}) {
734
+ var detectSystem = (input = {}) => {
104
735
  const platform = input.platform ?? process.platform;
105
736
  const arch = input.arch ?? process.arch;
106
737
  const osVersion = input.osVersion ?? os__default.default.release();
@@ -108,13 +739,13 @@ function detectSystem(input = {}) {
108
739
  const mojangArch = NODE_ARCH_TO_MOJANG_ARCH[arch];
109
740
  if (mojangOs === void 0 || mojangArch === void 0) {
110
741
  throw new MinecraftKitError(
111
- "RUNTIME_UNSUPPORTED_PLATFORM",
742
+ MinecraftKitErrorCodes.RUNTIME_UNSUPPORTED_PLATFORM,
112
743
  `Unsupported platform/arch combination: ${platform}/${arch}`,
113
744
  { context: { platform, arch: String(arch) } }
114
745
  );
115
746
  }
116
747
  return { os: mojangOs, arch: mojangArch, osVersion };
117
- }
748
+ };
118
749
 
119
750
  // src/constants/defaults.ts
120
751
  var HTTP_TIMEOUT_MS = 3e4;
@@ -135,7 +766,7 @@ var MAX_PROCESSOR_STDERR_LINES = 20;
135
766
  var SPAWNER_MAX_LINE_BYTES = 64 * 1024;
136
767
 
137
768
  // src/http/cache.ts
138
- function createMemoryCache(options = {}) {
769
+ var createMemoryCache = (options = {}) => {
139
770
  const cache = new lruCache.LRUCache({
140
771
  max: options.maxEntries ?? CACHE_MAX_ENTRIES,
141
772
  ttl: options.ttlMs ?? CACHE_TTL_MS
@@ -160,7 +791,7 @@ function createMemoryCache(options = {}) {
160
791
  cache.clear();
161
792
  }
162
793
  };
163
- }
794
+ };
164
795
 
165
796
  // src/http/client.ts
166
797
  var TIMEOUT_REASON = /* @__PURE__ */ Symbol("http-timeout");
@@ -178,39 +809,60 @@ var FetchHttpClient = class {
178
809
  }
179
810
  const timer = setTimeout(() => controller.abort(TIMEOUT_REASON), timeoutMs);
180
811
  let response;
812
+ const method = options.method ?? "GET";
181
813
  try {
182
- response = await fetch(url, {
183
- method: "GET",
814
+ const init = {
815
+ method,
184
816
  headers: { "user-agent": USER_AGENT, ...options.headers ?? {} },
185
817
  signal: controller.signal,
186
818
  redirect: "follow"
187
- });
819
+ };
820
+ if (method !== "GET" && options.body !== void 0) {
821
+ init.body = options.body;
822
+ }
823
+ response = await fetch(url, init);
188
824
  } catch (cause) {
189
825
  clearTimeout(timer);
190
826
  options.signal?.removeEventListener("abort", onParentAbort);
191
827
  if (controller.signal.reason === TIMEOUT_REASON) {
192
- throw new MinecraftKitError("NETWORK_TIMEOUT", `Request timed out: ${url}`, {
193
- cause,
194
- context: { url, timeoutMs }
195
- });
828
+ throw new MinecraftKitError(
829
+ MinecraftKitErrorCodes.NETWORK_TIMEOUT,
830
+ `Request timed out: ${url}`,
831
+ {
832
+ cause,
833
+ context: { url, timeoutMs }
834
+ }
835
+ );
196
836
  }
197
837
  if (options.signal?.aborted) {
198
- throw new MinecraftKitError("NETWORK_ABORTED", `Request aborted: ${url}`, {
838
+ throw new MinecraftKitError(
839
+ MinecraftKitErrorCodes.NETWORK_ABORTED,
840
+ `Request aborted: ${url}`,
841
+ {
842
+ cause,
843
+ context: { url }
844
+ }
845
+ );
846
+ }
847
+ throw new MinecraftKitError(
848
+ MinecraftKitErrorCodes.NETWORK_HTTP_ERROR,
849
+ `Network request failed: ${url}`,
850
+ {
199
851
  cause,
200
852
  context: { url }
201
- });
202
- }
203
- throw new MinecraftKitError("NETWORK_HTTP_ERROR", `Network request failed: ${url}`, {
204
- cause,
205
- context: { url }
206
- });
853
+ }
854
+ );
207
855
  }
208
856
  clearTimeout(timer);
209
857
  options.signal?.removeEventListener("abort", onParentAbort);
210
- if (!response.ok) {
211
- throw new MinecraftKitError("NETWORK_HTTP_ERROR", `HTTP ${response.status} for ${url}`, {
212
- context: { url, httpStatus: response.status }
213
- });
858
+ if (!response.ok && options.acceptNonOk !== true) {
859
+ throw new MinecraftKitError(
860
+ MinecraftKitErrorCodes.NETWORK_HTTP_ERROR,
861
+ `HTTP ${response.status} for ${url}`,
862
+ {
863
+ context: { url, httpStatus: response.status }
864
+ }
865
+ );
214
866
  }
215
867
  return new FetchHttpResponse(response, url);
216
868
  }
@@ -334,6 +986,17 @@ var InstallActionKinds = {
334
986
  WRITE_VERSION_JSON: "write-version-json",
335
987
  WRITE_LOGGING_CONFIG: "write-logging-config"
336
988
  };
989
+ var DownloadCategories = {
990
+ CLIENT_JAR: "client-jar",
991
+ LIBRARY: "library",
992
+ ASSET_INDEX: "asset-index",
993
+ ASSET: "asset",
994
+ LOGGING_CONFIG: "logging-config",
995
+ FABRIC_LIBRARY: "fabric-library",
996
+ FORGE_LIBRARY: "forge-library",
997
+ RUNTIME_FILE: "runtime-file",
998
+ FORGE_INSTALLER: "forge-installer"
999
+ };
337
1000
 
338
1001
  // src/types/loader.ts
339
1002
  var Loaders = {
@@ -387,7 +1050,7 @@ var ApiEndpoints = {
387
1050
  };
388
1051
 
389
1052
  // src/http/metadata.ts
390
- async function fetchJson(http, cache, input) {
1053
+ var fetchJson = async (http, cache, input) => {
391
1054
  const key = input.cacheKey ?? `json:${input.url}`;
392
1055
  const cached = cache.get(key);
393
1056
  if (cached !== void 0) {
@@ -399,8 +1062,8 @@ async function fetchJson(http, cache, input) {
399
1062
  const value = await response.json();
400
1063
  cache.set(key, value, input.ttlMs ?? CACHE_TTL_MS);
401
1064
  return value;
402
- }
403
- async function fetchText(http, cache, input) {
1065
+ };
1066
+ var fetchText = async (http, cache, input) => {
404
1067
  const key = input.cacheKey ?? `text:${input.url}`;
405
1068
  const cached = cache.get(key);
406
1069
  if (cached !== void 0) {
@@ -412,10 +1075,10 @@ async function fetchText(http, cache, input) {
412
1075
  const text = await response.text();
413
1076
  cache.set(key, text, input.ttlMs ?? CACHE_TTL_MS);
414
1077
  return text;
415
- }
1078
+ };
416
1079
 
417
1080
  // src/install/assets.ts
418
- async function planAssetDownloads(input) {
1081
+ var planAssetDownloads = async (input) => {
419
1082
  const indexUrl = input.assetIndex.url;
420
1083
  const indexPath = targetPaths.assetIndex(input.directory, input.assetIndex.id);
421
1084
  const indexDocument = await fetchJson(input.http, input.cache, {
@@ -430,7 +1093,7 @@ async function planAssetDownloads(input) {
430
1093
  target: indexPath,
431
1094
  expectedSha1: input.assetIndex.sha1,
432
1095
  expectedSize: input.assetIndex.size,
433
- category: "asset-index"
1096
+ category: DownloadCategories.ASSET_INDEX
434
1097
  }
435
1098
  ];
436
1099
  const seen = /* @__PURE__ */ new Set();
@@ -443,11 +1106,11 @@ async function planAssetDownloads(input) {
443
1106
  target: targetPaths.assetObject(input.directory, entry.hash),
444
1107
  expectedSha1: entry.hash,
445
1108
  expectedSize: entry.size,
446
- category: "asset"
1109
+ category: DownloadCategories.ASSET
447
1110
  });
448
1111
  }
449
1112
  return { actions, indexDocument };
450
- }
1113
+ };
451
1114
 
452
1115
  // src/constants/maven.ts
453
1116
  var DEFAULT_LIBRARY_REPOSITORY = "https://libraries.minecraft.net/";
@@ -455,21 +1118,25 @@ var FABRIC_MAVEN_BASE = "https://maven.fabricmc.net/";
455
1118
  var FORGE_MAVEN_BASE = "https://maven.minecraftforge.net/";
456
1119
 
457
1120
  // src/core/maven.ts
458
- function parseMavenCoordinate(input) {
1121
+ var parseMavenCoordinate = (input) => {
459
1122
  const trimmed = input.startsWith("[") && input.endsWith("]") ? input.slice(1, -1) : input;
460
1123
  const atIndex = trimmed.indexOf("@");
461
1124
  const extension = atIndex === -1 ? "jar" : trimmed.slice(atIndex + 1);
462
1125
  const body = atIndex === -1 ? trimmed : trimmed.slice(0, atIndex);
463
1126
  const parts = body.split(":");
464
1127
  if (parts.length < 3 || parts.length > 4) {
465
- throw new MinecraftKitError("INVALID_INPUT", `Invalid Maven coordinate: ${input}`, {
466
- context: { input }
467
- });
1128
+ throw new MinecraftKitError(
1129
+ MinecraftKitErrorCodes.INVALID_INPUT,
1130
+ `Invalid Maven coordinate: ${input}`,
1131
+ {
1132
+ context: { input }
1133
+ }
1134
+ );
468
1135
  }
469
1136
  const [group, artifact, version, classifier] = parts;
470
1137
  if (!group || !artifact || !version) {
471
1138
  throw new MinecraftKitError(
472
- "INVALID_INPUT",
1139
+ MinecraftKitErrorCodes.INVALID_INPUT,
473
1140
  `Invalid Maven coordinate (missing component): ${input}`,
474
1141
  { context: { input } }
475
1142
  );
@@ -478,19 +1145,19 @@ function parseMavenCoordinate(input) {
478
1145
  return { group, artifact, version, extension };
479
1146
  }
480
1147
  return { group, artifact, version, classifier, extension };
481
- }
482
- function mavenRelativePath(coord) {
1148
+ };
1149
+ var mavenRelativePath = (coord) => {
483
1150
  const groupPath = coord.group.replaceAll(".", "/");
484
1151
  const classifierSegment = coord.classifier === void 0 ? "" : `-${coord.classifier}`;
485
1152
  const filename = `${coord.artifact}-${coord.version}${classifierSegment}.${coord.extension}`;
486
1153
  return `${groupPath}/${coord.artifact}/${coord.version}/${filename}`;
487
- }
488
- function mavenRelativePathFor(input) {
1154
+ };
1155
+ var mavenRelativePathFor = (input) => {
489
1156
  return mavenRelativePath(parseMavenCoordinate(input));
490
- }
1157
+ };
491
1158
 
492
1159
  // src/core/rules.ts
493
- function evaluateRules(rules, context) {
1160
+ var evaluateRules = (rules, context) => {
494
1161
  if (!rules || rules.length === 0) {
495
1162
  return true;
496
1163
  }
@@ -501,8 +1168,8 @@ function evaluateRules(rules, context) {
501
1168
  }
502
1169
  }
503
1170
  return allowed;
504
- }
505
- function matchesRule(rule, context) {
1171
+ };
1172
+ var matchesRule = (rule, context) => {
506
1173
  if (rule.os !== void 0) {
507
1174
  if (rule.os.name !== void 0 && rule.os.name !== context.system.os) {
508
1175
  return false;
@@ -530,21 +1197,21 @@ function matchesRule(rule, context) {
530
1197
  }
531
1198
  }
532
1199
  return true;
533
- }
534
- function normalizeArch(arch) {
1200
+ };
1201
+ var normalizeArch = (arch) => {
535
1202
  return arch === "ia32" ? "x86" : arch;
536
- }
537
- function resolveArchPlaceholder(template, archDigit2) {
1203
+ };
1204
+ var resolveArchPlaceholder = (template, archDigit2) => {
538
1205
  return template.replaceAll("${arch}", archDigit2);
539
- }
540
- function archDigit(arch) {
1206
+ };
1207
+ var archDigit = (arch) => {
541
1208
  if (arch === "x86") return "32";
542
1209
  if (arch === "x64") return "64";
543
1210
  return "64";
544
- }
1211
+ };
545
1212
 
546
1213
  // src/install/libraries.ts
547
- function planLibraryDownloads(input) {
1214
+ var planLibraryDownloads = (input) => {
548
1215
  const downloads = [];
549
1216
  const nativeExtractions = [];
550
1217
  const classpathFiles = [];
@@ -598,8 +1265,8 @@ function planLibraryDownloads(input) {
598
1265
  }
599
1266
  }
600
1267
  return { downloads, nativeExtractions, classpathFiles };
601
- }
602
- function pickPrimaryArtifact(library) {
1268
+ };
1269
+ var pickPrimaryArtifact = (library) => {
603
1270
  if (library.downloads?.artifact) {
604
1271
  return artifactFromDownload(library.downloads.artifact);
605
1272
  }
@@ -610,8 +1277,8 @@ function pickPrimaryArtifact(library) {
610
1277
  return mavenArtifactFromCoord(library.name, DEFAULT_LIBRARY_REPOSITORY);
611
1278
  }
612
1279
  return null;
613
- }
614
- function pickNative(library, system) {
1280
+ };
1281
+ var pickNative = (library, system) => {
615
1282
  if (!library.natives) return null;
616
1283
  const classifierTemplate = library.natives[system.os];
617
1284
  if (!classifierTemplate) return null;
@@ -626,22 +1293,26 @@ function pickNative(library, system) {
626
1293
  return mavenArtifactFromCoord(withClassifier, library.url ?? DEFAULT_LIBRARY_REPOSITORY);
627
1294
  }
628
1295
  return null;
629
- }
630
- function artifactFromDownload(artifact) {
1296
+ };
1297
+ var artifactFromDownload = (artifact) => {
631
1298
  return {
632
1299
  relativePath: artifact.path,
633
1300
  url: artifact.url,
634
1301
  sha1: artifact.sha1,
635
1302
  size: artifact.size
636
1303
  };
637
- }
638
- function mavenArtifactFromCoord(coord, baseUrl) {
1304
+ };
1305
+ var mavenArtifactFromCoord = (coord, baseUrl) => {
639
1306
  const relativePath = mavenRelativePathFor(coord);
640
1307
  const normalizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
641
1308
  if (!relativePath) {
642
- throw new MinecraftKitError("MANIFEST_INVALID", `Invalid library coordinate: ${coord}`, {
643
- context: { input: coord }
644
- });
1309
+ throw new MinecraftKitError(
1310
+ MinecraftKitErrorCodes.MANIFEST_INVALID,
1311
+ `Invalid library coordinate: ${coord}`,
1312
+ {
1313
+ context: { input: coord }
1314
+ }
1315
+ );
645
1316
  }
646
1317
  return {
647
1318
  relativePath,
@@ -649,10 +1320,10 @@ function mavenArtifactFromCoord(coord, baseUrl) {
649
1320
  sha1: void 0,
650
1321
  size: void 0
651
1322
  };
652
- }
1323
+ };
653
1324
 
654
1325
  // src/install/fabric-install.ts
655
- function planFabricInstall(input) {
1326
+ var planFabricInstall = (input) => {
656
1327
  const versionId = input.loader.profile.id;
657
1328
  const versionJsonPath = targetPaths.versionJson(input.directory, versionId);
658
1329
  const versionJson = {
@@ -666,7 +1337,7 @@ function planFabricInstall(input) {
666
1337
  directory: input.directory,
667
1338
  system: input.system,
668
1339
  versionId: input.minecraft.version,
669
- category: "fabric-library"
1340
+ category: DownloadCategories.FABRIC_LIBRARY
670
1341
  });
671
1342
  return {
672
1343
  versionJson,
@@ -674,7 +1345,7 @@ function planFabricInstall(input) {
674
1345
  classpathFiles: plan.classpathFiles,
675
1346
  versionId
676
1347
  };
677
- }
1348
+ };
678
1349
 
679
1350
  // src/constants/limits.ts
680
1351
  var EXTRACTION_MAX_FILE_SIZE = 256 * 1024 * 1024;
@@ -682,42 +1353,42 @@ var EXTRACTION_MAX_TOTAL_SIZE = 2 * 1024 * 1024 * 1024;
682
1353
  var EXTRACTION_MAX_COMPRESSION_RATIO = 200;
683
1354
  var EXTRACTION_MAX_ENTRY_COUNT = 1e5;
684
1355
  var FORGE_INSTALLER_MAX_SIZE = 256 * 1024 * 1024;
685
- async function ensureDir(directory) {
1356
+ var ensureDir = async (directory) => {
686
1357
  try {
687
1358
  await fs__default.default.mkdir(directory, { recursive: true });
688
1359
  } catch (cause) {
689
1360
  throw new MinecraftKitError(
690
- "FILESYSTEM_WRITE_ERROR",
1361
+ MinecraftKitErrorCodes.FILESYSTEM_WRITE_ERROR,
691
1362
  `Failed to create directory: ${directory}`,
692
1363
  { cause, context: { filePath: directory } }
693
1364
  );
694
1365
  }
695
- }
696
- async function fileExists(filePath) {
1366
+ };
1367
+ var fileExists = async (filePath) => {
697
1368
  try {
698
1369
  const stat = await fs__default.default.stat(filePath);
699
1370
  return stat.isFile();
700
1371
  } catch {
701
1372
  return false;
702
1373
  }
703
- }
704
- async function dirExists(filePath) {
1374
+ };
1375
+ var dirExists = async (filePath) => {
705
1376
  try {
706
1377
  const stat = await fs__default.default.stat(filePath);
707
1378
  return stat.isDirectory();
708
1379
  } catch {
709
1380
  return false;
710
1381
  }
711
- }
712
- async function fileSize(filePath) {
1382
+ };
1383
+ var fileSize = async (filePath) => {
713
1384
  try {
714
1385
  const stat = await fs__default.default.stat(filePath);
715
1386
  return stat.size;
716
1387
  } catch {
717
1388
  return -1;
718
1389
  }
719
- }
720
- async function atomicWrite(target, data) {
1390
+ };
1391
+ var atomicWrite = async (target, data) => {
721
1392
  await ensureDir(path__default.default.dirname(target));
722
1393
  const tmp = `${target}.${crypto2__default.default.randomBytes(4).toString("hex")}.tmp`;
723
1394
  try {
@@ -732,31 +1403,39 @@ async function atomicWrite(target, data) {
732
1403
  await fs__default.default.unlink(tmp);
733
1404
  } catch {
734
1405
  }
735
- throw new MinecraftKitError("FILESYSTEM_WRITE_ERROR", `Failed to write file: ${target}`, {
736
- cause,
737
- context: { filePath: target }
738
- });
1406
+ throw new MinecraftKitError(
1407
+ MinecraftKitErrorCodes.FILESYSTEM_WRITE_ERROR,
1408
+ `Failed to write file: ${target}`,
1409
+ {
1410
+ cause,
1411
+ context: { filePath: target }
1412
+ }
1413
+ );
739
1414
  }
740
- }
741
- async function readText(filePath) {
1415
+ };
1416
+ var readText = async (filePath) => {
742
1417
  try {
743
1418
  return await fs__default.default.readFile(filePath, "utf8");
744
1419
  } catch (cause) {
745
- throw new MinecraftKitError("FILESYSTEM_READ_ERROR", `Failed to read file: ${filePath}`, {
746
- cause,
747
- context: { filePath }
748
- });
1420
+ throw new MinecraftKitError(
1421
+ MinecraftKitErrorCodes.FILESYSTEM_READ_ERROR,
1422
+ `Failed to read file: ${filePath}`,
1423
+ {
1424
+ cause,
1425
+ context: { filePath }
1426
+ }
1427
+ );
749
1428
  }
750
- }
751
- async function listChildDirectories(directory) {
1429
+ };
1430
+ var listChildDirectories = async (directory) => {
752
1431
  try {
753
1432
  const entries = await fs__default.default.readdir(directory, { withFileTypes: true });
754
1433
  return entries.filter((e) => e.isDirectory()).map((e) => e.name);
755
1434
  } catch {
756
1435
  return [];
757
1436
  }
758
- }
759
- async function chmodExecutable(filePath) {
1437
+ };
1438
+ var chmodExecutable = async (filePath) => {
760
1439
  if (process.platform === "win32") {
761
1440
  return;
762
1441
  }
@@ -764,35 +1443,43 @@ async function chmodExecutable(filePath) {
764
1443
  await fs__default.default.chmod(filePath, 493);
765
1444
  } catch {
766
1445
  }
767
- }
768
- function assertWithinRoot(root, child) {
1446
+ };
1447
+ var assertWithinRoot = (root, child) => {
769
1448
  const normalizedRoot = path__default.default.resolve(root);
770
1449
  const normalizedChild = path__default.default.resolve(root, child);
771
1450
  const sep = path__default.default.sep;
772
1451
  if (normalizedChild !== normalizedRoot && !normalizedChild.startsWith(normalizedRoot + sep)) {
773
- throw new MinecraftKitError("FILESYSTEM_PATH_TRAVERSAL", `Path escapes root: ${child}`, {
774
- context: { filePath: child, rootDirectory: root }
775
- });
1452
+ throw new MinecraftKitError(
1453
+ MinecraftKitErrorCodes.FILESYSTEM_PATH_TRAVERSAL,
1454
+ `Path escapes root: ${child}`,
1455
+ {
1456
+ context: { filePath: child, rootDirectory: root }
1457
+ }
1458
+ );
776
1459
  }
777
- }
1460
+ };
778
1461
 
779
1462
  // src/core/archive.ts
780
- function openZip(filePath) {
1463
+ var openZip = (filePath) => {
781
1464
  return new Promise((resolve, reject) => {
782
1465
  yauzl__default.default.open(filePath, { lazyEntries: true, autoClose: false }, (err, zipFile) => {
783
1466
  if (err || !zipFile) {
784
1467
  reject(
785
- new MinecraftKitError("ARCHIVE_INVALID", `Failed to open archive: ${filePath}`, {
786
- cause: err,
787
- context: { filePath }
788
- })
1468
+ new MinecraftKitError(
1469
+ MinecraftKitErrorCodes.ARCHIVE_INVALID,
1470
+ `Failed to open archive: ${filePath}`,
1471
+ {
1472
+ cause: err,
1473
+ context: { filePath }
1474
+ }
1475
+ )
789
1476
  );
790
1477
  return;
791
1478
  }
792
1479
  resolve(new ZipReader(zipFile, filePath));
793
1480
  });
794
1481
  });
795
- }
1482
+ };
796
1483
  var ZipReader = class {
797
1484
  constructor(file, filePath) {
798
1485
  this.file = file;
@@ -810,7 +1497,7 @@ var ZipReader = class {
810
1497
  count++;
811
1498
  if (count > EXTRACTION_MAX_ENTRY_COUNT) {
812
1499
  throw new MinecraftKitError(
813
- "ARCHIVE_TOO_LARGE",
1500
+ MinecraftKitErrorCodes.ARCHIVE_TOO_LARGE,
814
1501
  `Archive contains too many entries: ${this.filePath}`,
815
1502
  { context: { filePath: this.filePath } }
816
1503
  );
@@ -842,10 +1529,14 @@ var ZipReader = class {
842
1529
  const onError = (err) => {
843
1530
  cleanup();
844
1531
  reject(
845
- new MinecraftKitError("ARCHIVE_INVALID", "Failed to read archive entry", {
846
- cause: err,
847
- context: { filePath: this.filePath }
848
- })
1532
+ new MinecraftKitError(
1533
+ MinecraftKitErrorCodes.ARCHIVE_INVALID,
1534
+ "Failed to read archive entry",
1535
+ {
1536
+ cause: err,
1537
+ context: { filePath: this.filePath }
1538
+ }
1539
+ )
849
1540
  );
850
1541
  };
851
1542
  const cleanup = () => {
@@ -870,14 +1561,14 @@ var ZipReader = class {
870
1561
  readBuffer: async () => {
871
1562
  if (entry.uncompressedSize > EXTRACTION_MAX_FILE_SIZE) {
872
1563
  throw new MinecraftKitError(
873
- "ARCHIVE_TOO_LARGE",
1564
+ MinecraftKitErrorCodes.ARCHIVE_TOO_LARGE,
874
1565
  `Archive entry exceeds size cap: ${name}`,
875
1566
  { context: { filePath: this.filePath, entryName: name, size: entry.uncompressedSize } }
876
1567
  );
877
1568
  }
878
1569
  if (entry.compressedSize > 0 && entry.uncompressedSize / entry.compressedSize > EXTRACTION_MAX_COMPRESSION_RATIO) {
879
1570
  throw new MinecraftKitError(
880
- "ARCHIVE_TOO_LARGE",
1571
+ MinecraftKitErrorCodes.ARCHIVE_TOO_LARGE,
881
1572
  `Archive entry exceeds compression-ratio cap: ${name}`,
882
1573
  { context: { filePath: this.filePath, entryName: name } }
883
1574
  );
@@ -893,13 +1584,13 @@ var ZipReader = class {
893
1584
  };
894
1585
  }
895
1586
  };
896
- function openStream(file, entry, archivePath) {
1587
+ var openStream = (file, entry, archivePath) => {
897
1588
  return new Promise((resolve, reject) => {
898
1589
  file.openReadStream(entry, (err, stream) => {
899
1590
  if (err || !stream) {
900
1591
  reject(
901
1592
  new MinecraftKitError(
902
- "ARCHIVE_INVALID",
1593
+ MinecraftKitErrorCodes.ARCHIVE_INVALID,
903
1594
  `Failed to open archive entry: ${entry.fileName}`,
904
1595
  { cause: err, context: { filePath: archivePath, entryName: entry.fileName } }
905
1596
  )
@@ -909,8 +1600,8 @@ function openStream(file, entry, archivePath) {
909
1600
  resolve(stream);
910
1601
  });
911
1602
  });
912
- }
913
- async function extractAllToDir(zipPath, targetDir, options = {}) {
1603
+ };
1604
+ var extractAllToDir = async (zipPath, targetDir, options = {}) => {
914
1605
  const exclude = options.excludePrefixes ?? ["META-INF/"];
915
1606
  let fileCount = 0;
916
1607
  let totalSize = 0;
@@ -926,14 +1617,14 @@ async function extractAllToDir(zipPath, targetDir, options = {}) {
926
1617
  totalSize += entry.uncompressedSize;
927
1618
  if (totalSize > EXTRACTION_MAX_TOTAL_SIZE) {
928
1619
  throw new MinecraftKitError(
929
- "ARCHIVE_TOO_LARGE",
1620
+ MinecraftKitErrorCodes.ARCHIVE_TOO_LARGE,
930
1621
  `Archive total size cap exceeded: ${zipPath}`,
931
1622
  { context: { filePath: zipPath } }
932
1623
  );
933
1624
  }
934
1625
  if (entry.uncompressedSize > EXTRACTION_MAX_FILE_SIZE) {
935
1626
  throw new MinecraftKitError(
936
- "ARCHIVE_TOO_LARGE",
1627
+ MinecraftKitErrorCodes.ARCHIVE_TOO_LARGE,
937
1628
  `Archive entry exceeds size cap: ${entry.name}`,
938
1629
  { context: { filePath: zipPath, entryName: entry.name } }
939
1630
  );
@@ -950,9 +1641,9 @@ async function extractAllToDir(zipPath, targetDir, options = {}) {
950
1641
  reader.close();
951
1642
  }
952
1643
  return { fileCount };
953
- }
1644
+ };
954
1645
  var RESERVED_NAME = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\..*)?$/i;
955
- function assertSafeEntryName(name) {
1646
+ var assertSafeEntryName = (name) => {
956
1647
  if (!name) {
957
1648
  throw rejectEntry(name, "empty entry name");
958
1649
  }
@@ -974,15 +1665,15 @@ function assertSafeEntryName(name) {
974
1665
  throw rejectEntry(name, "trailing dot or whitespace");
975
1666
  }
976
1667
  }
977
- }
978
- function rejectEntry(name, reason) {
1668
+ };
1669
+ var rejectEntry = (name, reason) => {
979
1670
  return new MinecraftKitError(
980
- "ARCHIVE_ENTRY_REJECTED",
1671
+ MinecraftKitErrorCodes.ARCHIVE_ENTRY_REJECTED,
981
1672
  `Archive entry rejected (${reason}): ${name}`,
982
1673
  { context: { entryName: name, reason } }
983
1674
  );
984
- }
985
- async function readEntryBuffer(zipPath, entryName) {
1675
+ };
1676
+ var readEntryBuffer = async (zipPath, entryName) => {
986
1677
  const reader = await openZip(zipPath);
987
1678
  try {
988
1679
  const entry = await reader.findEntry(entryName);
@@ -991,19 +1682,23 @@ async function readEntryBuffer(zipPath, entryName) {
991
1682
  } finally {
992
1683
  reader.close();
993
1684
  }
994
- }
995
- async function extractSingleEntry(zipPath, entryName, destination) {
1685
+ };
1686
+ var extractSingleEntry = async (zipPath, entryName, destination) => {
996
1687
  const buffer = await readEntryBuffer(zipPath, entryName);
997
1688
  if (!buffer) {
998
- throw new MinecraftKitError("ARCHIVE_INVALID", `Archive entry not found: ${entryName}`, {
999
- context: { filePath: zipPath, entryName }
1000
- });
1689
+ throw new MinecraftKitError(
1690
+ MinecraftKitErrorCodes.ARCHIVE_INVALID,
1691
+ `Archive entry not found: ${entryName}`,
1692
+ {
1693
+ context: { filePath: zipPath, entryName }
1694
+ }
1695
+ );
1001
1696
  }
1002
1697
  await atomicWrite(destination, buffer);
1003
- }
1698
+ };
1004
1699
  var MANIFEST_LINE_CONTINUATION = /\r?\n[ \t]/g;
1005
1700
  var MANIFEST_MAIN_CLASS = /^Main-Class:\s*(.+)$/i;
1006
- async function readJarMainClass(zipPath) {
1701
+ var readJarMainClass = async (zipPath) => {
1007
1702
  const buf = await readEntryBuffer(zipPath, "META-INF/MANIFEST.MF");
1008
1703
  if (!buf) return void 0;
1009
1704
  const text = buf.toString("utf8").replaceAll(MANIFEST_LINE_CONTINUATION, "");
@@ -1012,10 +1707,10 @@ async function readJarMainClass(zipPath) {
1012
1707
  if (match?.[1]) return match[1].trim();
1013
1708
  }
1014
1709
  return void 0;
1015
- }
1710
+ };
1016
1711
 
1017
1712
  // src/core/collections.ts
1018
- function dedupeBy(values, key) {
1713
+ var dedupeBy = (values, key) => {
1019
1714
  const seen = /* @__PURE__ */ new Set();
1020
1715
  const result = [];
1021
1716
  for (const value of values) {
@@ -1025,13 +1720,25 @@ function dedupeBy(values, key) {
1025
1720
  result.push(value);
1026
1721
  }
1027
1722
  return result;
1028
- }
1029
- function dedupe(values) {
1723
+ };
1724
+ var dedupe = (values) => {
1030
1725
  return dedupeBy(values, (v) => v);
1031
- }
1726
+ };
1727
+
1728
+ // src/core/abort.ts
1729
+ var assertNotAborted = (signal, message) => {
1730
+ if (signal?.aborted) {
1731
+ throw new MinecraftKitError(MinecraftKitErrorCodes.LAUNCH_ABORTED, message);
1732
+ }
1733
+ };
1734
+ var checkpoint = async (sources, message = "Operation aborted by signal") => {
1735
+ assertNotAborted(sources.signal, message);
1736
+ await sources.pauseController?.waitWhilePaused();
1737
+ assertNotAborted(sources.signal, message);
1738
+ };
1032
1739
 
1033
1740
  // src/core/retry.ts
1034
- function abortableSleep(ms, signal) {
1741
+ var abortableSleep = (ms, signal) => {
1035
1742
  return new Promise((resolve, reject) => {
1036
1743
  if (signal?.aborted) {
1037
1744
  reject(toAbortError(signal));
@@ -1047,13 +1754,13 @@ function abortableSleep(ms, signal) {
1047
1754
  };
1048
1755
  signal?.addEventListener("abort", onAbort, { once: true });
1049
1756
  });
1050
- }
1051
- function toAbortError(signal) {
1052
- return new MinecraftKitError("NETWORK_ABORTED", "Operation aborted", {
1757
+ };
1758
+ var toAbortError = (signal) => {
1759
+ return new MinecraftKitError(MinecraftKitErrorCodes.NETWORK_ABORTED, "Operation aborted", {
1053
1760
  context: { reason: signal?.reason }
1054
1761
  });
1055
- }
1056
- async function withRetry(op, isRetryable, options = {}) {
1762
+ };
1763
+ var withRetry = async (op, isRetryable, options = {}) => {
1057
1764
  const max = options.maxAttempts ?? HTTP_RETRY_MAX;
1058
1765
  const base = options.baseMs ?? HTTP_RETRY_BACKOFF_BASE_MS;
1059
1766
  const cap = options.capMs ?? HTTP_RETRY_BACKOFF_CAP_MS;
@@ -1078,8 +1785,8 @@ async function withRetry(op, isRetryable, options = {}) {
1078
1785
  }
1079
1786
  }
1080
1787
  throw lastError ?? new Error("withRetry exhausted attempts");
1081
- }
1082
- function isHttpRetryable(error) {
1788
+ };
1789
+ var isHttpRetryable = (error) => {
1083
1790
  if (!isMinecraftKitError(error)) {
1084
1791
  return false;
1085
1792
  }
@@ -1092,15 +1799,45 @@ function isHttpRetryable(error) {
1092
1799
  return status === 0;
1093
1800
  }
1094
1801
  return false;
1095
- }
1802
+ };
1803
+
1804
+ // src/types/events.ts
1805
+ var EventTypes = {
1806
+ INSTALL_PHASE_CHANGED: "install:phase-changed",
1807
+ DOWNLOAD_STARTED: "download:started",
1808
+ DOWNLOAD_PROGRESS: "download:progress",
1809
+ DOWNLOAD_SKIPPED: "download:skipped",
1810
+ DOWNLOAD_COMPLETED: "download:completed",
1811
+ DOWNLOAD_FAILED: "download:failed",
1812
+ INTEGRITY_VERIFIED: "integrity:verified",
1813
+ INTEGRITY_MISMATCH: "integrity:mismatch",
1814
+ ARCHIVE_EXTRACTED: "archive:extracted",
1815
+ FORGE_PROCESSOR_STARTED: "forge:processor-started",
1816
+ FORGE_PROCESSOR_COMPLETED: "forge:processor-completed",
1817
+ FORGE_PROCESSOR_OUTPUT_VERIFIED: "forge:processor-output-verified",
1818
+ VERIFY_FILE_CHECKED: "verify:file-checked",
1819
+ VERIFY_COMPLETED: "verify:completed",
1820
+ REPAIR_PHASE_CHANGED: "repair:phase-changed",
1821
+ LAUNCH_STARTING: "launch:starting",
1822
+ LAUNCH_STARTED: "launch:started",
1823
+ LAUNCH_STDOUT: "launch:stdout",
1824
+ LAUNCH_STDERR: "launch:stderr",
1825
+ LAUNCH_EXITED: "launch:exited",
1826
+ LAUNCH_ABORTED: "launch:aborted"
1827
+ };
1096
1828
 
1097
1829
  // src/http/download.ts
1098
- async function downloadFile(http, input) {
1099
- const fileRef = { url: input.url, target: input.target, category: input.category };
1830
+ var downloadFile = async (http, input) => {
1831
+ assertSafeDownloadUrl(input.url, input.hostAllowList);
1832
+ const fileRef = {
1833
+ url: input.url,
1834
+ target: input.target,
1835
+ ...input.category !== void 0 ? { category: input.category } : {}
1836
+ };
1100
1837
  if (input.expectedSha1 !== void 0) {
1101
1838
  const existing = await checkExistingFile(input.target, input.expectedSha1, input.expectedSize);
1102
1839
  if (existing.matches) {
1103
- input.onEvent?.({ type: "download:skipped", file: fileRef });
1840
+ input.onEvent?.({ type: EventTypes.DOWNLOAD_SKIPPED, file: fileRef });
1104
1841
  return { bytesDownloaded: 0, sha1: existing.sha1, skipped: true };
1105
1842
  }
1106
1843
  }
@@ -1109,29 +1846,32 @@ async function downloadFile(http, input) {
1109
1846
  return withRetry(
1110
1847
  async () => {
1111
1848
  input.onEvent?.({
1112
- type: "download:started",
1849
+ type: EventTypes.DOWNLOAD_STARTED,
1113
1850
  file: fileRef,
1114
1851
  expectedSize: input.expectedSize ?? 0
1115
1852
  });
1116
1853
  const startedAt = Date.now();
1117
1854
  let bytesDownloaded = 0;
1118
1855
  const hash = crypto2__default.default.createHash("sha1");
1119
- const response = await http.request(input.url, { signal: input.signal });
1856
+ const response = await http.request(input.url, {
1857
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
1858
+ });
1120
1859
  const contentLength = Number(response.headers["content-length"] ?? "0");
1121
1860
  const total = input.expectedSize ?? (Number.isFinite(contentLength) ? contentLength : 0);
1122
1861
  const sourceIterable = response.stream();
1123
1862
  const counting = (async function* () {
1124
1863
  for await (const chunk of sourceIterable) {
1125
- if (input.pauseController?.paused) {
1126
- await input.pauseController.waitWhilePaused();
1127
- }
1128
- if (input.signal?.aborted) {
1129
- throw new MinecraftKitError("LAUNCH_ABORTED", "Download aborted by signal");
1130
- }
1864
+ await checkpoint(
1865
+ {
1866
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
1867
+ ...input.pauseController !== void 0 ? { pauseController: input.pauseController } : {}
1868
+ },
1869
+ "Download aborted by signal"
1870
+ );
1131
1871
  bytesDownloaded += chunk.byteLength;
1132
1872
  hash.update(chunk);
1133
1873
  input.onEvent?.({
1134
- type: "download:progress",
1874
+ type: EventTypes.DOWNLOAD_PROGRESS,
1135
1875
  file: fileRef,
1136
1876
  bytesDownloaded,
1137
1877
  totalBytes: total
@@ -1144,7 +1884,7 @@ async function downloadFile(http, input) {
1144
1884
  } catch (cause) {
1145
1885
  await safeUnlink(tmp);
1146
1886
  throw new MinecraftKitError(
1147
- "FILESYSTEM_WRITE_ERROR",
1887
+ MinecraftKitErrorCodes.FILESYSTEM_WRITE_ERROR,
1148
1888
  `Failed to write download: ${input.target}`,
1149
1889
  { cause, context: { filePath: input.target, url: input.url } }
1150
1890
  );
@@ -1152,50 +1892,58 @@ async function downloadFile(http, input) {
1152
1892
  const computedSha1 = hash.digest("hex");
1153
1893
  if (input.expectedSize !== void 0 && bytesDownloaded !== input.expectedSize) {
1154
1894
  await safeUnlink(tmp);
1155
- throw new MinecraftKitError("INTEGRITY_SIZE_MISMATCH", `Size mismatch for ${input.url}`, {
1156
- context: {
1157
- url: input.url,
1158
- expectedSize: input.expectedSize,
1159
- actualSize: bytesDownloaded
1895
+ throw new MinecraftKitError(
1896
+ MinecraftKitErrorCodes.INTEGRITY_SIZE_MISMATCH,
1897
+ `Size mismatch for ${input.url}`,
1898
+ {
1899
+ context: {
1900
+ url: input.url,
1901
+ expectedSize: input.expectedSize,
1902
+ actualSize: bytesDownloaded
1903
+ }
1160
1904
  }
1161
- });
1905
+ );
1162
1906
  }
1163
1907
  if (input.expectedSha1 !== void 0 && computedSha1 !== input.expectedSha1) {
1164
1908
  await safeUnlink(tmp);
1165
1909
  input.onEvent?.({
1166
- type: "integrity:mismatch",
1910
+ type: EventTypes.INTEGRITY_MISMATCH,
1167
1911
  file: fileRef,
1168
1912
  algorithm: "sha1",
1169
1913
  expected: input.expectedSha1,
1170
1914
  actual: computedSha1
1171
1915
  });
1172
- throw new MinecraftKitError("INTEGRITY_HASH_MISMATCH", `SHA-1 mismatch for ${input.url}`, {
1173
- context: {
1174
- url: input.url,
1175
- expectedHash: input.expectedSha1,
1176
- actualHash: computedSha1
1916
+ throw new MinecraftKitError(
1917
+ MinecraftKitErrorCodes.INTEGRITY_HASH_MISMATCH,
1918
+ `SHA-1 mismatch for ${input.url}`,
1919
+ {
1920
+ context: {
1921
+ url: input.url,
1922
+ expectedHash: input.expectedSha1,
1923
+ actualHash: computedSha1
1924
+ }
1177
1925
  }
1178
- });
1926
+ );
1179
1927
  }
1180
1928
  try {
1181
1929
  await fs__default.default.rename(tmp, input.target);
1182
1930
  } catch (cause) {
1183
1931
  await safeUnlink(tmp);
1184
1932
  throw new MinecraftKitError(
1185
- "FILESYSTEM_WRITE_ERROR",
1933
+ MinecraftKitErrorCodes.FILESYSTEM_WRITE_ERROR,
1186
1934
  `Failed to finalize download: ${input.target}`,
1187
1935
  { cause, context: { filePath: input.target } }
1188
1936
  );
1189
1937
  }
1190
1938
  input.onEvent?.({
1191
- type: "download:completed",
1939
+ type: EventTypes.DOWNLOAD_COMPLETED,
1192
1940
  file: fileRef,
1193
1941
  durationMs: Date.now() - startedAt,
1194
1942
  bytes: bytesDownloaded
1195
1943
  });
1196
1944
  if (input.expectedSha1 !== void 0) {
1197
1945
  input.onEvent?.({
1198
- type: "integrity:verified",
1946
+ type: EventTypes.INTEGRITY_VERIFIED,
1199
1947
  file: fileRef,
1200
1948
  algorithm: "sha1",
1201
1949
  hash: computedSha1
@@ -1208,7 +1956,7 @@ async function downloadFile(http, input) {
1208
1956
  ...input.signal !== void 0 ? { signal: input.signal } : {},
1209
1957
  onAttemptFailed: (error, attempt) => {
1210
1958
  input.onEvent?.({
1211
- type: "download:failed",
1959
+ type: EventTypes.DOWNLOAD_FAILED,
1212
1960
  file: fileRef,
1213
1961
  error: error instanceof Error ? error : new Error(String(error)),
1214
1962
  willRetry: isHttpRetryable(error) && attempt < HTTP_RETRY_MAX - 1
@@ -1216,8 +1964,8 @@ async function downloadFile(http, input) {
1216
1964
  }
1217
1965
  }
1218
1966
  );
1219
- }
1220
- async function checkExistingFile(target, expectedSha1, expectedSize) {
1967
+ };
1968
+ var checkExistingFile = async (target, expectedSha1, expectedSize) => {
1221
1969
  let stat;
1222
1970
  try {
1223
1971
  stat = await fs__default.default.stat(target);
@@ -1233,21 +1981,60 @@ async function checkExistingFile(target, expectedSha1, expectedSize) {
1233
1981
  const buf = await fs__default.default.readFile(target);
1234
1982
  const sha1 = crypto2__default.default.createHash("sha1").update(buf).digest("hex");
1235
1983
  return { matches: sha1 === expectedSha1, sha1 };
1236
- }
1237
- async function safeUnlink(filePath) {
1984
+ };
1985
+ var safeUnlink = async (filePath) => {
1238
1986
  try {
1239
1987
  await fs__default.default.unlink(filePath);
1240
1988
  } catch {
1241
1989
  }
1242
- }
1990
+ };
1991
+ var assertSafeDownloadUrl = (url, allowList) => {
1992
+ let parsed;
1993
+ try {
1994
+ parsed = new URL(url);
1995
+ } catch {
1996
+ throw new MinecraftKitError(
1997
+ MinecraftKitErrorCodes.INVALID_INPUT,
1998
+ `Download URL is not parseable: ${url}`,
1999
+ {
2000
+ context: { url }
2001
+ }
2002
+ );
2003
+ }
2004
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
2005
+ throw new MinecraftKitError(
2006
+ MinecraftKitErrorCodes.INVALID_INPUT,
2007
+ `Download URL must use http(s); refusing scheme ${parsed.protocol}`,
2008
+ { context: { url, scheme: parsed.protocol } }
2009
+ );
2010
+ }
2011
+ if (allowList !== void 0 && !matchesHostAllowList(parsed.hostname, allowList)) {
2012
+ throw new MinecraftKitError(
2013
+ MinecraftKitErrorCodes.INVALID_INPUT,
2014
+ `Download URL host is not in the allow-list: ${parsed.hostname}`,
2015
+ { context: { url, host: parsed.hostname } }
2016
+ );
2017
+ }
2018
+ };
2019
+ var matchesHostAllowList = (hostname, allowList) => {
2020
+ const host = hostname.toLowerCase();
2021
+ return allowList.some((entry) => matchesHostEntry(host, entry.toLowerCase()));
2022
+ };
2023
+ var matchesHostEntry = (host, entry) => {
2024
+ if (entry.startsWith("*.")) {
2025
+ const suffix = entry.slice(1);
2026
+ return host === entry.slice(2) || host.endsWith(suffix);
2027
+ }
2028
+ return host === entry;
2029
+ };
1243
2030
 
1244
2031
  // src/install/forge-install.ts
1245
- async function planForgeInstall(input) {
2032
+ var planForgeInstall = async (input) => {
1246
2033
  const installerPath = targetPaths.forgeInstaller(input.directory, input.loader.fullVersion);
1247
2034
  await downloadFile(input.http, {
1248
2035
  url: input.loader.installerUrl,
1249
2036
  target: installerPath,
1250
- category: "forge-installer",
2037
+ category: DownloadCategories.FORGE_INSTALLER,
1251
2038
  ...input.signal !== void 0 ? { signal: input.signal } : {},
1252
2039
  ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
1253
2040
  });
@@ -1255,7 +2042,7 @@ async function planForgeInstall(input) {
1255
2042
  kind: InstallActionKinds.DOWNLOAD_FILE,
1256
2043
  url: input.loader.installerUrl,
1257
2044
  target: installerPath,
1258
- category: "forge-installer"
2045
+ category: DownloadCategories.FORGE_INSTALLER
1259
2046
  };
1260
2047
  const profile = await readJsonEntry(installerPath, "install_profile.json");
1261
2048
  const versionRelative = profile.json.startsWith("/") ? profile.json.slice(1) : profile.json;
@@ -1271,14 +2058,14 @@ async function planForgeInstall(input) {
1271
2058
  directory: input.directory,
1272
2059
  system: input.system,
1273
2060
  versionId: input.minecraft.version,
1274
- category: "forge-library"
2061
+ category: DownloadCategories.FORGE_LIBRARY
1275
2062
  });
1276
2063
  const versionLibraries = planLibraryDownloads({
1277
2064
  libraries: version.libraries,
1278
2065
  directory: input.directory,
1279
2066
  system: input.system,
1280
2067
  versionId: version.id,
1281
- category: "forge-library"
2068
+ category: DownloadCategories.FORGE_LIBRARY
1282
2069
  });
1283
2070
  const dedupedDownloads = dedupeBy(
1284
2071
  [...installerLibraries.downloads, ...versionLibraries.downloads],
@@ -1313,27 +2100,23 @@ async function planForgeInstall(input) {
1313
2100
  profile,
1314
2101
  version
1315
2102
  };
1316
- }
1317
- async function readJsonEntry(zipPath, entryName) {
2103
+ };
2104
+ var readJsonEntry = async (zipPath, entryName) => {
1318
2105
  const buffer = await readEntryBuffer(zipPath, entryName);
1319
2106
  if (!buffer) {
1320
2107
  throw new MinecraftKitError(
1321
- "FORGE_INSTALLER_INVALID",
2108
+ MinecraftKitErrorCodes.FORGE_INSTALLER_INVALID,
1322
2109
  `Forge installer is missing required entry: ${entryName}`,
1323
2110
  { context: { filePath: zipPath, entryName } }
1324
2111
  );
1325
2112
  }
1326
- try {
1327
- return JSON.parse(buffer.toString("utf8"));
1328
- } catch (cause) {
1329
- throw new MinecraftKitError(
1330
- "FORGE_INSTALLER_INVALID",
1331
- `Forge installer entry is not valid JSON: ${entryName}`,
1332
- { cause, context: { filePath: zipPath, entryName } }
1333
- );
1334
- }
1335
- }
1336
- async function extractInstallerMavenEntries(installerPath, directory) {
2113
+ return parseJsonStrict(buffer.toString("utf8"), {
2114
+ code: MinecraftKitErrorCodes.FORGE_INSTALLER_INVALID,
2115
+ message: `Forge installer entry is not valid JSON: ${entryName}`,
2116
+ context: { filePath: zipPath, entryName }
2117
+ });
2118
+ };
2119
+ var extractInstallerMavenEntries = async (installerPath, directory) => {
1337
2120
  const reader = await openZip(installerPath);
1338
2121
  try {
1339
2122
  for await (const entry of reader.entries()) {
@@ -1346,16 +2129,16 @@ async function extractInstallerMavenEntries(installerPath, directory) {
1346
2129
  } finally {
1347
2130
  reader.close();
1348
2131
  }
1349
- }
1350
- async function resolveProfileData(input) {
2132
+ };
2133
+ var resolveProfileData = async (input) => {
1351
2134
  const tokens = {};
1352
2135
  for (const [key, sided] of Object.entries(input.profile.data)) {
1353
2136
  const raw = sided.client;
1354
2137
  tokens[key] = await resolveDataValue(raw, input.installerPath, input.directory);
1355
2138
  }
1356
2139
  return { tokens };
1357
- }
1358
- async function resolveDataValue(raw, installerPath, directory) {
2140
+ };
2141
+ var resolveDataValue = async (raw, installerPath, directory) => {
1359
2142
  if (raw.startsWith("[") && raw.endsWith("]")) {
1360
2143
  const coord = raw.slice(1, -1);
1361
2144
  const relativePath = mavenRelativePathFor(coord);
@@ -1374,8 +2157,8 @@ async function resolveDataValue(raw, installerPath, directory) {
1374
2157
  return { value: destination, isPath: true };
1375
2158
  }
1376
2159
  return { value: raw, isPath: false };
1377
- }
1378
- async function buildProcessorActions(input) {
2160
+ };
2161
+ var buildProcessorActions = async (input) => {
1379
2162
  const builtIns = {
1380
2163
  SIDE: { value: "client", isPath: false },
1381
2164
  MINECRAFT_JAR: {
@@ -1408,12 +2191,12 @@ async function buildProcessorActions(input) {
1408
2191
  index++;
1409
2192
  }
1410
2193
  return actions;
1411
- }
1412
- function processorAppliesToClient(processor) {
2194
+ };
2195
+ var processorAppliesToClient = (processor) => {
1413
2196
  if (!processor.sides || processor.sides.length === 0) return true;
1414
2197
  return processor.sides.includes("client");
1415
- }
1416
- function buildProcessorAction(input) {
2198
+ };
2199
+ var buildProcessorAction = (input) => {
1417
2200
  const jarPath = path__default.default.join(
1418
2201
  targetPaths.librariesDir(input.directory),
1419
2202
  mavenRelativePathFor(input.processor.jar)
@@ -1440,26 +2223,30 @@ function buildProcessorAction(input) {
1440
2223
  args,
1441
2224
  outputs
1442
2225
  };
1443
- }
1444
- function substituteToken(raw, tokens) {
2226
+ };
2227
+ var substituteToken = (raw, tokens) => {
1445
2228
  if (raw.startsWith("[") && raw.endsWith("]")) {
1446
2229
  return path__default.default.join(...mavenRelativePathFor(raw.slice(1, -1)).split("/"));
1447
2230
  }
1448
2231
  return raw.replaceAll(/\{([A-Z0-9_]+)\}/g, (match, key) => {
1449
2232
  const token = tokens[key];
1450
2233
  if (token === void 0) {
1451
- throw new MinecraftKitError("FORGE_INSTALLER_INVALID", `Unknown processor token: ${match}`, {
1452
- context: { token: key }
1453
- });
2234
+ throw new MinecraftKitError(
2235
+ MinecraftKitErrorCodes.FORGE_INSTALLER_INVALID,
2236
+ `Unknown processor token: ${match}`,
2237
+ {
2238
+ context: { token: key }
2239
+ }
2240
+ );
1454
2241
  }
1455
2242
  return token.value;
1456
2243
  });
1457
- }
1458
- function stripLiteralPrefix(value) {
2244
+ };
2245
+ var stripLiteralPrefix = (value) => {
1459
2246
  const stripped = value.startsWith("'") ? value.slice(1) : value;
1460
2247
  return stripped.endsWith("'") ? stripped.slice(0, -1) : stripped;
1461
- }
1462
- async function planRuntimeDownloads(input) {
2248
+ };
2249
+ var planRuntimeDownloads = async (input) => {
1463
2250
  const manifest = await fetchJson(input.http, input.cache, {
1464
2251
  url: input.runtime.manifestUrl,
1465
2252
  cacheKey: `runtime-manifest:${input.runtime.component}:${input.runtime.platformKey}:${input.runtime.manifestSha1}`,
@@ -1480,14 +2267,14 @@ async function planRuntimeDownloads(input) {
1480
2267
  target,
1481
2268
  expectedSha1: entry.downloads.raw.sha1,
1482
2269
  expectedSize: entry.downloads.raw.size,
1483
- category: "runtime-file"
2270
+ category: DownloadCategories.RUNTIME_FILE
1484
2271
  });
1485
2272
  }
1486
2273
  return { actions, manifest };
1487
- }
2274
+ };
1488
2275
 
1489
2276
  // src/install/planner.ts
1490
- async function planInstall(input) {
2277
+ var planInstall = async (input) => {
1491
2278
  const { target } = input;
1492
2279
  const actions = [];
1493
2280
  actions.push({
@@ -1496,7 +2283,7 @@ async function planInstall(input) {
1496
2283
  target: targetPaths.versionJar(target.directory, target.minecraft.version),
1497
2284
  expectedSha1: target.minecraft.manifest.downloads.client.sha1,
1498
2285
  expectedSize: target.minecraft.manifest.downloads.client.size,
1499
- category: "client-jar"
2286
+ category: DownloadCategories.CLIENT_JAR
1500
2287
  });
1501
2288
  actions.push({
1502
2289
  kind: InstallActionKinds.WRITE_VERSION_JSON,
@@ -1509,7 +2296,7 @@ async function planInstall(input) {
1509
2296
  directory: target.directory,
1510
2297
  system: target.runtime.system,
1511
2298
  versionId: target.minecraft.version,
1512
- category: "library"
2299
+ category: DownloadCategories.LIBRARY
1513
2300
  });
1514
2301
  actions.push(...vanillaLibraries.downloads);
1515
2302
  actions.push(...vanillaLibraries.nativeExtractions);
@@ -1529,7 +2316,7 @@ async function planInstall(input) {
1529
2316
  target: targetPaths.loggingConfig(target.directory, logging.file.id),
1530
2317
  expectedSha1: logging.file.sha1,
1531
2318
  expectedSize: logging.file.size,
1532
- category: "logging-config"
2319
+ category: DownloadCategories.LOGGING_CONFIG
1533
2320
  });
1534
2321
  }
1535
2322
  const runtimePlan = await planRuntimeDownloads({
@@ -1579,7 +2366,7 @@ async function planInstall(input) {
1579
2366
  totalActions: actions.length,
1580
2367
  totalBytes
1581
2368
  };
1582
- }
2369
+ };
1583
2370
 
1584
2371
  // node_modules/yocto-queue/index.js
1585
2372
  var Node = class {
@@ -1682,22 +2469,126 @@ function pLimit(concurrency) {
1682
2469
  const generator = (function_, ...arguments_) => new Promise((resolve) => {
1683
2470
  enqueue(function_, resolve, arguments_);
1684
2471
  });
1685
- Object.defineProperties(generator, {
1686
- activeCount: {
1687
- get: () => activeCount
1688
- },
1689
- pendingCount: {
1690
- get: () => queue.size
1691
- },
1692
- clearQueue: {
1693
- value() {
1694
- queue.clear();
1695
- }
2472
+ Object.defineProperties(generator, {
2473
+ activeCount: {
2474
+ get: () => activeCount
2475
+ },
2476
+ pendingCount: {
2477
+ get: () => queue.size
2478
+ },
2479
+ clearQueue: {
2480
+ value() {
2481
+ queue.clear();
2482
+ }
2483
+ }
2484
+ });
2485
+ return generator;
2486
+ }
2487
+ var sha1OfFile = async (filePath) => {
2488
+ const hash = crypto2__default.default.createHash("sha1");
2489
+ const stream = fs$1.createReadStream(filePath);
2490
+ try {
2491
+ await new Promise((resolve, reject) => {
2492
+ stream.on("data", (chunk) => hash.update(chunk));
2493
+ stream.on("end", resolve);
2494
+ stream.on("error", reject);
2495
+ });
2496
+ } finally {
2497
+ if (!stream.destroyed) stream.destroy();
2498
+ }
2499
+ return hash.digest("hex");
2500
+ };
2501
+
2502
+ // src/install/processor.ts
2503
+ var runProcessor = async (input) => {
2504
+ const startedAt = Date.now();
2505
+ const mainClass = await resolveProcessorMainClass(input.action);
2506
+ emit(input, { type: EventTypes.FORGE_PROCESSOR_STARTED, index: input.action.index, mainClass });
2507
+ const exit = await spawnProcessor(input, mainClass);
2508
+ if (exit.code !== 0) {
2509
+ throw new MinecraftKitError(
2510
+ MinecraftKitErrorCodes.FORGE_PROCESSOR_FAILED,
2511
+ `Forge processor exited with code ${exit.code ?? "(signal)"}: ${mainClass}`,
2512
+ {
2513
+ context: {
2514
+ mainClass,
2515
+ stderr: exit.stderr,
2516
+ ...exit.code !== null ? { exitCode: exit.code } : {}
2517
+ }
2518
+ }
2519
+ );
2520
+ }
2521
+ input.onEvent?.({
2522
+ type: EventTypes.FORGE_PROCESSOR_COMPLETED,
2523
+ processor: { index: input.action.index, mainClass },
2524
+ exitCode: exit.code ?? 0,
2525
+ durationMs: Date.now() - startedAt
2526
+ });
2527
+ await verifyProcessorOutputs(input, mainClass);
2528
+ };
2529
+ var resolveProcessorMainClass = async (action) => {
2530
+ const processorJar = action.classpath[0];
2531
+ if (processorJar === void 0) {
2532
+ throw new MinecraftKitError(
2533
+ MinecraftKitErrorCodes.FORGE_INSTALLER_INVALID,
2534
+ "Forge processor has an empty classpath",
2535
+ { context: { processorIndex: action.index } }
2536
+ );
2537
+ }
2538
+ const mainClass = await readJarMainClass(processorJar);
2539
+ if (!mainClass) {
2540
+ throw new MinecraftKitError(
2541
+ MinecraftKitErrorCodes.FORGE_INSTALLER_INVALID,
2542
+ `Forge processor jar has no Main-Class: ${processorJar}`,
2543
+ { context: { filePath: processorJar } }
2544
+ );
2545
+ }
2546
+ return mainClass;
2547
+ };
2548
+ var spawnProcessor = async (input, mainClass) => {
2549
+ const classpathSeparator = process.platform === "win32" ? ";" : ":";
2550
+ const args = [
2551
+ "-cp",
2552
+ input.action.classpath.join(classpathSeparator),
2553
+ mainClass,
2554
+ ...input.action.args
2555
+ ];
2556
+ const stderrTail = [];
2557
+ const child = input.spawner.spawn(input.javaPath, args, { cwd: process.cwd() });
2558
+ child.stdout.on("data", () => {
2559
+ });
2560
+ child.stderr.on("data", (line) => {
2561
+ if (stderrTail.length >= MAX_PROCESSOR_STDERR_LINES) stderrTail.shift();
2562
+ stderrTail.push(line);
2563
+ });
2564
+ const exit = await child.exited;
2565
+ return { code: exit.code, stderr: stderrTail.join("\n") };
2566
+ };
2567
+ var verifyProcessorOutputs = async (input, mainClass) => {
2568
+ for (const [outputPath, expectedSha1] of Object.entries(input.action.outputs)) {
2569
+ const sha1 = await sha1OfFile(outputPath);
2570
+ if (sha1 !== expectedSha1) {
2571
+ throw new MinecraftKitError(
2572
+ MinecraftKitErrorCodes.FORGE_PROCESSOR_FAILED,
2573
+ `Processor output hash mismatch: ${outputPath}`,
2574
+ { context: { filePath: outputPath, expectedHash: expectedSha1, actualHash: sha1 } }
2575
+ );
1696
2576
  }
2577
+ input.onEvent?.({
2578
+ type: EventTypes.FORGE_PROCESSOR_OUTPUT_VERIFIED,
2579
+ processor: { index: input.action.index, mainClass },
2580
+ path: outputPath
2581
+ });
2582
+ }
2583
+ };
2584
+ var emit = (input, event) => {
2585
+ input.onEvent?.({
2586
+ type: event.type,
2587
+ processor: { index: event.index, mainClass: event.mainClass },
2588
+ total: input.total
1697
2589
  });
1698
- return generator;
1699
- }
1700
- async function materializeRuntimeExtras(input) {
2590
+ };
2591
+ var materializeRuntimeExtras = async (input) => {
1701
2592
  const root = targetPaths.runtimeRoot(
1702
2593
  input.directory,
1703
2594
  input.runtime.component,
@@ -1716,20 +2607,20 @@ async function materializeRuntimeExtras(input) {
1716
2607
  });
1717
2608
  }
1718
2609
  }
1719
- }
1720
- async function unlinkIfPresent(target) {
2610
+ };
2611
+ var unlinkIfPresent = async (target) => {
1721
2612
  try {
1722
2613
  await fs__default.default.unlink(target);
1723
2614
  } catch (cause) {
1724
2615
  if (isNotFound(cause)) return;
1725
2616
  throw new MinecraftKitError(
1726
- "FILESYSTEM_WRITE_ERROR",
2617
+ MinecraftKitErrorCodes.FILESYSTEM_WRITE_ERROR,
1727
2618
  `Failed to remove stale runtime entry: ${target}`,
1728
2619
  { cause, context: { filePath: target } }
1729
2620
  );
1730
2621
  }
1731
- }
1732
- async function createLinkOrCopy(root, relativePath, linkTarget, destination) {
2622
+ };
2623
+ var createLinkOrCopy = async (root, relativePath, linkTarget, destination) => {
1733
2624
  try {
1734
2625
  await fs__default.default.symlink(linkTarget, destination);
1735
2626
  return;
@@ -1739,7 +2630,7 @@ async function createLinkOrCopy(root, relativePath, linkTarget, destination) {
1739
2630
  await fs__default.default.copyFile(absoluteSource, destination);
1740
2631
  } catch (copyError) {
1741
2632
  throw new MinecraftKitError(
1742
- "FILESYSTEM_WRITE_ERROR",
2633
+ MinecraftKitErrorCodes.FILESYSTEM_WRITE_ERROR,
1743
2634
  `Failed to materialize runtime entry: ${destination}`,
1744
2635
  {
1745
2636
  cause: copyError,
@@ -1752,283 +2643,209 @@ async function createLinkOrCopy(root, relativePath, linkTarget, destination) {
1752
2643
  );
1753
2644
  }
1754
2645
  }
1755
- }
1756
- function isNotFound(error) {
2646
+ };
2647
+ var isNotFound = (error) => {
1757
2648
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1758
- }
1759
- function errorMessage(error) {
2649
+ };
2650
+ var errorMessage = (error) => {
1760
2651
  return error instanceof Error ? error.message : String(error);
1761
- }
2652
+ };
1762
2653
 
1763
2654
  // src/install/runner.ts
1764
2655
  var DOWNLOAD_GROUPS = [
1765
- { categories: ["runtime-file"], phase: InstallPhases.INSTALLING_RUNTIME },
1766
- { categories: ["client-jar"], phase: InstallPhases.DOWNLOADING_CLIENT_JAR },
1767
- { categories: ["library"], phase: InstallPhases.DOWNLOADING_LIBRARIES },
1768
- { categories: ["asset-index"], phase: InstallPhases.DOWNLOADING_ASSET_INDEX },
1769
- { categories: ["asset"], phase: InstallPhases.DOWNLOADING_ASSETS },
1770
- { categories: ["logging-config"], phase: InstallPhases.WRITING_FILES },
1771
- { categories: ["fabric-library"], phase: InstallPhases.INSTALLING_FABRIC },
1772
- { categories: ["forge-installer", "forge-library"], phase: InstallPhases.INSTALLING_FORGE }
2656
+ { categories: [DownloadCategories.RUNTIME_FILE], phase: InstallPhases.INSTALLING_RUNTIME },
2657
+ { categories: [DownloadCategories.CLIENT_JAR], phase: InstallPhases.DOWNLOADING_CLIENT_JAR },
2658
+ { categories: [DownloadCategories.LIBRARY], phase: InstallPhases.DOWNLOADING_LIBRARIES },
2659
+ { categories: [DownloadCategories.ASSET_INDEX], phase: InstallPhases.DOWNLOADING_ASSET_INDEX },
2660
+ { categories: [DownloadCategories.ASSET], phase: InstallPhases.DOWNLOADING_ASSETS },
2661
+ { categories: [DownloadCategories.LOGGING_CONFIG], phase: InstallPhases.WRITING_FILES },
2662
+ { categories: [DownloadCategories.FABRIC_LIBRARY], phase: InstallPhases.INSTALLING_FABRIC },
2663
+ {
2664
+ categories: [DownloadCategories.FORGE_INSTALLER, DownloadCategories.FORGE_LIBRARY],
2665
+ phase: InstallPhases.INSTALLING_FORGE
2666
+ }
1773
2667
  ];
1774
- async function runInstall(input) {
2668
+ var runInstall = async (input) => {
1775
2669
  const startedAt = Date.now();
1776
- let bytesDownloaded = 0;
1777
- let actionsCompleted = 0;
1778
- let actionsSkipped = 0;
1779
- const onEvent = input.onEvent;
2670
+ const counters = { bytesDownloaded: 0, actionsCompleted: 0, actionsSkipped: 0 };
2671
+ const ctx = createContext(input, counters);
2672
+ ctx.enterPhase(InstallPhases.PLANNING);
2673
+ const plannedActions = partitionActions(input);
2674
+ await runDownloadsStage(ctx, plannedActions.downloads);
2675
+ await runWritesStage(ctx, plannedActions.writes);
2676
+ await runNativesStage(ctx, plannedActions.natives);
2677
+ await runRuntimeStage(ctx);
2678
+ await runProcessorsStage(ctx, plannedActions.processors);
2679
+ ctx.enterPhase(InstallPhases.COMPLETED);
2680
+ return {
2681
+ targetId: input.plan.targetId,
2682
+ bytesDownloaded: counters.bytesDownloaded,
2683
+ actionsCompleted: counters.actionsCompleted,
2684
+ actionsSkipped: counters.actionsSkipped,
2685
+ durationMs: Date.now() - startedAt
2686
+ };
2687
+ };
2688
+ var createContext = (input, counters) => {
1780
2689
  let currentPhase = null;
1781
2690
  const enterPhase = (phase) => {
1782
2691
  if (phase === currentPhase) return;
1783
- onEvent?.({ type: "install:phase-changed", phase, previous: currentPhase });
2692
+ input.onEvent?.({ type: EventTypes.INSTALL_PHASE_CHANGED, phase, previous: currentPhase });
1784
2693
  currentPhase = phase;
1785
2694
  };
1786
- const checkpoint = async () => {
1787
- if (input.signal?.aborted) {
1788
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1789
- }
1790
- await input.pauseController?.waitWhilePaused();
1791
- if (input.signal?.aborted) {
1792
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1793
- }
2695
+ const checkpoint2 = () => checkpoint(
2696
+ {
2697
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
2698
+ ...input.pauseController !== void 0 ? { pauseController: input.pauseController } : {}
2699
+ },
2700
+ "Install aborted by signal"
2701
+ );
2702
+ return {
2703
+ input,
2704
+ counters,
2705
+ checkpoint: checkpoint2,
2706
+ enterPhase,
2707
+ limit: pLimit(input.concurrency ?? DOWNLOAD_CONCURRENCY)
2708
+ };
2709
+ };
2710
+ var partitionActions = (input) => {
2711
+ const filter = input.actionCategories;
2712
+ return {
2713
+ downloads: input.plan.actions.filter(isDownload).filter((a) => filter ? filter.has(a.category) : true),
2714
+ natives: input.plan.actions.filter(isNative),
2715
+ writes: input.plan.actions.filter(isWrite),
2716
+ processors: input.plan.actions.filter(isProcessor)
1794
2717
  };
1795
- const categoryFilter = input.actionCategories;
1796
- const downloads = input.plan.actions.filter(isDownload).filter((a) => categoryFilter ? categoryFilter.has(a.category) : true);
1797
- const natives = input.plan.actions.filter(isNative);
1798
- const writeActions = input.plan.actions.filter(isWrite);
1799
- const processors = input.plan.actions.filter(isProcessor);
1800
- enterPhase(InstallPhases.PLANNING);
1801
- const limit = pLimit(input.concurrency ?? DOWNLOAD_CONCURRENCY);
2718
+ };
2719
+ var runDownloadsStage = async (ctx, downloads) => {
2720
+ if (downloads.length === 0) return;
1802
2721
  for (const group of DOWNLOAD_GROUPS) {
1803
2722
  const groupActions = downloads.filter((action) => group.categories.includes(action.category));
1804
2723
  if (groupActions.length === 0) continue;
1805
- await checkpoint();
1806
- enterPhase(group.phase);
1807
- await Promise.all(
1808
- groupActions.map(
1809
- (action) => limit(async () => {
1810
- await checkpoint();
1811
- const result = await downloadFile(input.http, {
1812
- url: action.url,
1813
- target: action.target,
1814
- ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
1815
- ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
1816
- ...action.category !== void 0 ? { category: action.category } : {},
1817
- ...input.signal !== void 0 ? { signal: input.signal } : {},
1818
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {},
1819
- ...input.pauseController !== void 0 ? { pauseController: input.pauseController } : {}
1820
- });
1821
- bytesDownloaded += result.bytesDownloaded;
1822
- if (result.skipped) actionsSkipped++;
1823
- actionsCompleted++;
1824
- })
1825
- )
1826
- );
2724
+ await ctx.checkpoint();
2725
+ ctx.enterPhase(group.phase);
2726
+ await runDownloadGroup(ctx, groupActions);
1827
2727
  }
1828
2728
  const ungrouped = downloads.filter(
1829
2729
  (action) => !DOWNLOAD_GROUPS.some((g) => g.categories.includes(action.category))
1830
2730
  );
1831
2731
  if (ungrouped.length > 0) {
1832
- await checkpoint();
1833
- enterPhase(InstallPhases.DOWNLOADING_LIBRARIES);
1834
- await Promise.all(
1835
- ungrouped.map(
1836
- (action) => limit(async () => {
1837
- await checkpoint();
1838
- const result = await downloadFile(input.http, {
1839
- url: action.url,
1840
- target: action.target,
1841
- ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
1842
- ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
1843
- ...action.category !== void 0 ? { category: action.category } : {},
1844
- ...input.signal !== void 0 ? { signal: input.signal } : {},
1845
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {},
1846
- ...input.pauseController !== void 0 ? { pauseController: input.pauseController } : {}
1847
- });
1848
- bytesDownloaded += result.bytesDownloaded;
1849
- if (result.skipped) actionsSkipped++;
1850
- actionsCompleted++;
1851
- })
1852
- )
1853
- );
1854
- }
1855
- if (writeActions.length > 0) {
1856
- await checkpoint();
1857
- enterPhase(InstallPhases.WRITING_FILES);
1858
- for (const action of writeActions) {
1859
- await checkpoint();
1860
- await atomicWrite(action.path, action.content);
1861
- actionsCompleted++;
1862
- }
2732
+ await ctx.checkpoint();
2733
+ ctx.enterPhase(InstallPhases.DOWNLOADING_LIBRARIES);
2734
+ await runDownloadGroup(ctx, ungrouped);
1863
2735
  }
1864
- if (natives.length > 0) {
1865
- await checkpoint();
1866
- enterPhase(InstallPhases.EXTRACTING_NATIVES);
1867
- for (const action of natives) {
1868
- await checkpoint();
1869
- const { fileCount } = await extractAllToDir(action.source, action.destination, {
1870
- excludePrefixes: action.exclude
1871
- });
1872
- input.onEvent?.({
1873
- type: "archive:extracted",
1874
- archive: action.source,
1875
- target: action.destination,
1876
- fileCount
1877
- });
1878
- actionsCompleted++;
1879
- }
2736
+ };
2737
+ var runDownloadGroup = async (ctx, groupActions) => {
2738
+ await Promise.all(
2739
+ groupActions.map(
2740
+ (action) => ctx.limit(async () => {
2741
+ await ctx.checkpoint();
2742
+ const result = await downloadFile(ctx.input.http, {
2743
+ url: action.url,
2744
+ target: action.target,
2745
+ ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
2746
+ ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
2747
+ ...action.category !== void 0 ? { category: action.category } : {},
2748
+ ...ctx.input.signal !== void 0 ? { signal: ctx.input.signal } : {},
2749
+ ...ctx.input.onEvent !== void 0 ? { onEvent: ctx.input.onEvent } : {},
2750
+ ...ctx.input.pauseController !== void 0 ? { pauseController: ctx.input.pauseController } : {}
2751
+ });
2752
+ ctx.counters.bytesDownloaded += result.bytesDownloaded;
2753
+ if (result.skipped) ctx.counters.actionsSkipped++;
2754
+ ctx.counters.actionsCompleted++;
2755
+ })
2756
+ )
2757
+ );
2758
+ };
2759
+ var runWritesStage = async (ctx, writes) => {
2760
+ if (writes.length === 0) return;
2761
+ await ctx.checkpoint();
2762
+ ctx.enterPhase(InstallPhases.WRITING_FILES);
2763
+ for (const action of writes) {
2764
+ await ctx.checkpoint();
2765
+ await atomicWrite(action.path, action.content);
2766
+ ctx.counters.actionsCompleted++;
1880
2767
  }
1881
- if (input.plan.target.runtime !== void 0) {
1882
- await checkpoint();
1883
- enterPhase(InstallPhases.INSTALLING_RUNTIME);
1884
- const runtimePlan = await planRuntimeDownloads({
1885
- runtime: input.plan.target.runtime,
1886
- directory: input.plan.directory,
1887
- http: input.http,
1888
- cache: input.cache,
1889
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2768
+ };
2769
+ var runNativesStage = async (ctx, natives) => {
2770
+ if (natives.length === 0) return;
2771
+ await ctx.checkpoint();
2772
+ ctx.enterPhase(InstallPhases.EXTRACTING_NATIVES);
2773
+ for (const action of natives) {
2774
+ await ctx.checkpoint();
2775
+ const { fileCount } = await extractAllToDir(action.source, action.destination, {
2776
+ excludePrefixes: action.exclude
1890
2777
  });
1891
- await materializeRuntimeExtras({
1892
- runtime: input.plan.target.runtime,
1893
- directory: input.plan.directory,
1894
- manifest: runtimePlan.manifest
2778
+ ctx.input.onEvent?.({
2779
+ type: EventTypes.ARCHIVE_EXTRACTED,
2780
+ archive: action.source,
2781
+ target: action.destination,
2782
+ fileCount
1895
2783
  });
2784
+ ctx.counters.actionsCompleted++;
1896
2785
  }
1897
- if (processors.length > 0) {
1898
- await checkpoint();
1899
- enterPhase(InstallPhases.RUNNING_FORGE_PROCESSORS);
1900
- if (input.plan.target.loader.type !== Loaders.FORGE) {
1901
- throw new MinecraftKitError(
1902
- "FORGE_PROCESSOR_FAILED",
1903
- "Forge processors planned for a non-Forge target"
1904
- );
1905
- }
1906
- const javaPath = targetPaths.runtimeJavaExecutable(
1907
- input.plan.directory,
1908
- input.plan.target.runtime.component,
1909
- input.plan.target.runtime.system.os,
1910
- input.plan.target.runtime.installRoot
1911
- );
1912
- for (const action of processors) {
1913
- await checkpoint();
1914
- await runProcessor({
1915
- action,
1916
- javaPath,
1917
- spawner: input.spawner,
1918
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {},
1919
- total: processors.length
1920
- });
1921
- actionsCompleted++;
1922
- }
1923
- }
1924
- enterPhase(InstallPhases.COMPLETED);
1925
- return {
1926
- targetId: input.plan.targetId,
1927
- bytesDownloaded,
1928
- actionsCompleted,
1929
- actionsSkipped,
1930
- durationMs: Date.now() - startedAt
1931
- };
1932
- }
1933
- function isDownload(action) {
1934
- return action.kind === InstallActionKinds.DOWNLOAD_FILE;
1935
- }
1936
- function isNative(action) {
1937
- return action.kind === InstallActionKinds.EXTRACT_NATIVE;
1938
- }
1939
- function isProcessor(action) {
1940
- return action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR;
1941
- }
1942
- function isWrite(action) {
1943
- return action.kind === InstallActionKinds.WRITE_VERSION_JSON || action.kind === InstallActionKinds.WRITE_LOGGING_CONFIG;
1944
- }
1945
- async function runProcessor(input) {
1946
- const startedAt = Date.now();
1947
- const processorJar = input.action.classpath[0];
1948
- if (processorJar === void 0) {
1949
- throw new MinecraftKitError(
1950
- "FORGE_INSTALLER_INVALID",
1951
- "Forge processor has an empty classpath",
1952
- { context: { processorIndex: input.action.index } }
1953
- );
1954
- }
1955
- const mainClass = await readJarMainClass(processorJar);
1956
- if (!mainClass) {
1957
- throw new MinecraftKitError(
1958
- "FORGE_INSTALLER_INVALID",
1959
- `Forge processor jar has no Main-Class: ${processorJar}`,
1960
- { context: { filePath: processorJar } }
1961
- );
1962
- }
1963
- const classpathSeparator = process.platform === "win32" ? ";" : ":";
1964
- const args = [
1965
- "-cp",
1966
- input.action.classpath.join(classpathSeparator),
1967
- mainClass,
1968
- ...input.action.args
1969
- ];
1970
- input.onEvent?.({
1971
- type: "forge:processor-started",
1972
- processor: { index: input.action.index, mainClass },
1973
- total: input.total
1974
- });
1975
- const stderrTail = [];
1976
- const child = input.spawner.spawn(input.javaPath, args, { cwd: process.cwd() });
1977
- child.stdout.on("data", () => {
2786
+ };
2787
+ var runRuntimeStage = async (ctx) => {
2788
+ const runtime = ctx.input.plan.target.runtime;
2789
+ if (runtime === void 0) return;
2790
+ await ctx.checkpoint();
2791
+ ctx.enterPhase(InstallPhases.INSTALLING_RUNTIME);
2792
+ const runtimePlan = await planRuntimeDownloads({
2793
+ runtime,
2794
+ directory: ctx.input.plan.directory,
2795
+ http: ctx.input.http,
2796
+ cache: ctx.input.cache,
2797
+ ...ctx.input.signal !== void 0 ? { signal: ctx.input.signal } : {}
1978
2798
  });
1979
- child.stderr.on("data", (line) => {
1980
- if (stderrTail.length >= MAX_PROCESSOR_STDERR_LINES) stderrTail.shift();
1981
- stderrTail.push(line);
2799
+ await materializeRuntimeExtras({
2800
+ runtime,
2801
+ directory: ctx.input.plan.directory,
2802
+ manifest: runtimePlan.manifest
1982
2803
  });
1983
- const exit = await child.exited;
1984
- if (exit.code !== 0) {
2804
+ };
2805
+ var runProcessorsStage = async (ctx, processors) => {
2806
+ if (processors.length === 0) return;
2807
+ await ctx.checkpoint();
2808
+ ctx.enterPhase(InstallPhases.RUNNING_FORGE_PROCESSORS);
2809
+ const target = ctx.input.plan.target;
2810
+ if (target.loader?.type !== Loaders.FORGE) {
1985
2811
  throw new MinecraftKitError(
1986
- "FORGE_PROCESSOR_FAILED",
1987
- `Forge processor exited with code ${exit.code ?? "(signal)"}: ${mainClass}`,
1988
- {
1989
- context: {
1990
- exitCode: exit.code ?? void 0,
1991
- mainClass,
1992
- stderr: stderrTail.join("\n")
1993
- }
1994
- }
2812
+ MinecraftKitErrorCodes.FORGE_PROCESSOR_FAILED,
2813
+ "Forge processors planned for a non-Forge target"
1995
2814
  );
1996
2815
  }
1997
- input.onEvent?.({
1998
- type: "forge:processor-completed",
1999
- processor: { index: input.action.index, mainClass },
2000
- exitCode: exit.code ?? 0,
2001
- durationMs: Date.now() - startedAt
2002
- });
2003
- for (const [outputPath, expectedSha1] of Object.entries(input.action.outputs)) {
2004
- const sha1 = await sha1OfFileStreaming(outputPath);
2005
- if (sha1 !== expectedSha1) {
2006
- throw new MinecraftKitError(
2007
- "FORGE_PROCESSOR_FAILED",
2008
- `Processor output hash mismatch: ${outputPath}`,
2009
- { context: { filePath: outputPath, expectedHash: expectedSha1, actualHash: sha1 } }
2010
- );
2011
- }
2012
- input.onEvent?.({
2013
- type: "forge:processor-output-verified",
2014
- processor: { index: input.action.index, mainClass },
2015
- path: outputPath
2816
+ const javaPath = targetPaths.runtimeJavaExecutable(
2817
+ ctx.input.plan.directory,
2818
+ target.runtime.component,
2819
+ target.runtime.system.os,
2820
+ target.runtime.installRoot
2821
+ );
2822
+ for (const action of processors) {
2823
+ await ctx.checkpoint();
2824
+ await runProcessor({
2825
+ action,
2826
+ javaPath,
2827
+ spawner: ctx.input.spawner,
2828
+ ...ctx.input.onEvent !== void 0 ? { onEvent: ctx.input.onEvent } : {},
2829
+ total: processors.length
2016
2830
  });
2831
+ ctx.counters.actionsCompleted++;
2017
2832
  }
2018
- }
2019
- async function sha1OfFileStreaming(filePath) {
2020
- const hash = crypto2__default.default.createHash("sha1");
2021
- await new Promise((resolve, reject) => {
2022
- const stream = fs$1.createReadStream(filePath);
2023
- stream.on("data", (chunk) => hash.update(chunk));
2024
- stream.on("end", () => resolve());
2025
- stream.on("error", reject);
2026
- });
2027
- return hash.digest("hex");
2028
- }
2833
+ };
2834
+ var isDownload = (action) => {
2835
+ return action.kind === InstallActionKinds.DOWNLOAD_FILE;
2836
+ };
2837
+ var isNative = (action) => {
2838
+ return action.kind === InstallActionKinds.EXTRACT_NATIVE;
2839
+ };
2840
+ var isProcessor = (action) => {
2841
+ return action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR;
2842
+ };
2843
+ var isWrite = (action) => {
2844
+ return action.kind === InstallActionKinds.WRITE_VERSION_JSON || action.kind === InstallActionKinds.WRITE_LOGGING_CONFIG;
2845
+ };
2029
2846
 
2030
2847
  // src/install/runtime-install.ts
2031
- async function planRuntimeInstall(input) {
2848
+ var planRuntimeInstall = async (input) => {
2032
2849
  const runtimePlan = await planRuntimeDownloads({
2033
2850
  runtime: input.target.runtime,
2034
2851
  directory: input.target.directory,
@@ -2049,8 +2866,8 @@ async function planRuntimeInstall(input) {
2049
2866
  totalActions: actions.length,
2050
2867
  totalBytes
2051
2868
  };
2052
- }
2053
- async function planStandaloneRuntimeInstall(input) {
2869
+ };
2870
+ var planStandaloneRuntimeInstall = async (input) => {
2054
2871
  const runtimePlan = await planRuntimeDownloads({
2055
2872
  runtime: input.runtime,
2056
2873
  directory: input.directory,
@@ -2066,9 +2883,7 @@ async function planStandaloneRuntimeInstall(input) {
2066
2883
  const target = {
2067
2884
  id: input.id,
2068
2885
  directory: input.directory,
2069
- runtime: input.runtime,
2070
- minecraft: void 0,
2071
- loader: void 0
2886
+ runtime: input.runtime
2072
2887
  };
2073
2888
  return {
2074
2889
  targetId: input.id,
@@ -2078,7 +2893,7 @@ async function planStandaloneRuntimeInstall(input) {
2078
2893
  totalActions: actions.length,
2079
2894
  totalBytes
2080
2895
  };
2081
- }
2896
+ };
2082
2897
 
2083
2898
  // src/constants/launch.ts
2084
2899
  var BASE_JVM_ARGS = [
@@ -2099,7 +2914,7 @@ var LEGACY_JVM_ARGS = [
2099
2914
  var MACOS_JVM_ARGS = ["-Xdock:name=Minecraft"];
2100
2915
 
2101
2916
  // src/launch/arguments.ts
2102
- function flattenArguments(entries, context) {
2917
+ var flattenArguments = (entries, context) => {
2103
2918
  const result = [];
2104
2919
  for (const entry of entries) {
2105
2920
  if (typeof entry === "string") {
@@ -2114,16 +2929,16 @@ function flattenArguments(entries, context) {
2114
2929
  }
2115
2930
  }
2116
2931
  return result;
2117
- }
2118
- function splitLegacyArguments(raw) {
2932
+ };
2933
+ var splitLegacyArguments = (raw) => {
2119
2934
  return raw.trim().length === 0 ? [] : raw.trim().split(/\s+/);
2120
- }
2121
- function pickArguments(args, context) {
2935
+ };
2936
+ var pickArguments = (args, context) => {
2122
2937
  return {
2123
2938
  game: flattenArguments(args?.game ?? [], context),
2124
2939
  jvm: flattenArguments(args?.jvm ?? [], context)
2125
2940
  };
2126
- }
2941
+ };
2127
2942
 
2128
2943
  // src/launch/jvm-compat.ts
2129
2944
  var FLAG_MIN_JAVA = [
@@ -2132,7 +2947,7 @@ var FLAG_MIN_JAVA = [
2132
2947
  { prefix: "-XX:+UseCompactObjectHeaders", minJava: 24 },
2133
2948
  { prefix: "-XX:+UseZGC", minJava: 15 }
2134
2949
  ];
2135
- function filterArgsForJava(input) {
2950
+ var filterArgsForJava = (input) => {
2136
2951
  if (!Number.isFinite(input.javaMajor) || input.javaMajor <= 0) return input.args;
2137
2952
  const out = [];
2138
2953
  for (const arg of input.args) {
@@ -2150,26 +2965,30 @@ function filterArgsForJava(input) {
2150
2965
  out.push(arg);
2151
2966
  }
2152
2967
  return out;
2153
- }
2968
+ };
2154
2969
 
2155
2970
  // src/launch/placeholders.ts
2156
- function substituteArg(raw, values) {
2971
+ var substituteArg = (raw, values) => {
2157
2972
  return raw.replaceAll(/\$\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
2158
2973
  const value = values[key];
2159
2974
  if (value === void 0) {
2160
- throw new MinecraftKitError("INVALID_INPUT", `Unknown launch placeholder: ${match}`, {
2161
- context: { placeholder: key }
2162
- });
2975
+ throw new MinecraftKitError(
2976
+ MinecraftKitErrorCodes.INVALID_INPUT,
2977
+ `Unknown launch placeholder: ${match}`,
2978
+ {
2979
+ context: { placeholder: key }
2980
+ }
2981
+ );
2163
2982
  }
2164
2983
  return value;
2165
2984
  });
2166
- }
2167
- function substituteArgs(args, values) {
2985
+ };
2986
+ var substituteArgs = (args, values) => {
2168
2987
  return args.map((arg) => substituteArg(arg, values));
2169
- }
2988
+ };
2170
2989
 
2171
2990
  // src/launch/args-composition.ts
2172
- function composeArgs(input) {
2991
+ var composeArgs = (input) => {
2173
2992
  const minMb = input.options.memory?.minMb ?? DEFAULT_MIN_MB;
2174
2993
  const maxMb = input.options.memory?.maxMb ?? DEFAULT_MAX_MB;
2175
2994
  const memoryArgs = [`-Xms${minMb}M`, `-Xmx${maxMb}M`];
@@ -2219,8 +3038,8 @@ function composeArgs(input) {
2219
3038
  );
2220
3039
  }
2221
3040
  return { jvmArgs: [...jvmArgs, ...extraJvm], gameArgs };
2222
- }
2223
- function buildClasspath(input) {
3041
+ };
3042
+ var buildClasspath = (input) => {
2224
3043
  const seen = /* @__PURE__ */ new Set();
2225
3044
  const entries = [];
2226
3045
  for (const library of input.merged.libraries) {
@@ -2236,41 +3055,33 @@ function buildClasspath(input) {
2236
3055
  const versionJar = targetPaths.versionJar(input.directory, input.versionId);
2237
3056
  if (!seen.has(versionJar)) entries.push(versionJar);
2238
3057
  return entries;
2239
- }
2240
- function relativeFor(library) {
3058
+ };
3059
+ var relativeFor = (library) => {
2241
3060
  if (library.downloads?.artifact?.path) return library.downloads.artifact.path;
2242
3061
  if (library.name) {
2243
3062
  const coord = parseMavenCoordinate(library.name);
2244
3063
  return mavenRelativePath(coord);
2245
3064
  }
2246
3065
  return null;
2247
- }
2248
- function offlineUuidFor(username) {
3066
+ };
3067
+ var offlineUuidFor = (username) => {
2249
3068
  const md5 = crypto2__default.default.createHash("md5");
2250
3069
  md5.update(`OfflinePlayer:${username}`, "utf8");
2251
3070
  const bytes = md5.digest();
2252
3071
  bytes[6] = (bytes[6] ?? 0) & 15 | 48;
2253
3072
  bytes[8] = (bytes[8] ?? 0) & 63 | 128;
2254
3073
  return formatUuid(bytes);
2255
- }
2256
- function formatUuid(bytes) {
3074
+ };
3075
+ var formatUuid = (bytes) => {
2257
3076
  const hex = bytes.toString("hex");
2258
3077
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
2259
- }
2260
- function stripUuidDashes(uuid) {
3078
+ };
3079
+ var stripUuidDashes = (uuid) => {
2261
3080
  return uuid.replaceAll("-", "");
2262
- }
2263
-
2264
- // src/types/auth.ts
2265
- var AuthModes = {
2266
- /** Offline-mode play with a chosen username and synthetic UUID. */
2267
- OFFLINE: "offline",
2268
- /** Pre-authenticated session — caller provides the access token and identity. */
2269
- ONLINE: "online"
2270
3081
  };
2271
3082
 
2272
3083
  // src/launch/placeholder-values.ts
2273
- function buildPlaceholderValues(input) {
3084
+ var buildPlaceholderValues = (input) => {
2274
3085
  const cpSeparator = process.platform === "win32" ? ";" : ":";
2275
3086
  const directory = input.target.directory;
2276
3087
  const username = input.auth.username;
@@ -2303,11 +3114,20 @@ function buildPlaceholderValues(input) {
2303
3114
  resolution_width: input.options.resolution?.width.toString() ?? "",
2304
3115
  resolution_height: input.options.resolution?.height.toString() ?? ""
2305
3116
  };
2306
- }
3117
+ };
2307
3118
 
2308
3119
  // src/core/manifest-merge.ts
2309
- function mergeManifest(parent, child) {
2310
- const merged = {
3120
+ var mergeManifest = (parent, child) => {
3121
+ const args = mergeArguments(parent.arguments, child.arguments);
3122
+ const minecraftArguments = child.minecraftArguments ?? parent.minecraftArguments;
3123
+ const javaVersion = child.javaVersion ?? parent.javaVersion;
3124
+ const logging = child.logging ?? parent.logging;
3125
+ const inheritsFrom = child.inheritsFrom ?? parent.inheritsFrom;
3126
+ const releaseTime = child.releaseTime ?? parent.releaseTime;
3127
+ const time = child.time ?? parent.time;
3128
+ const minimumLauncherVersion = child.minimumLauncherVersion ?? parent.minimumLauncherVersion;
3129
+ const complianceLevel = child.complianceLevel ?? parent.complianceLevel;
3130
+ return {
2311
3131
  id: child.id || parent.id,
2312
3132
  type: child.type ?? parent.type,
2313
3133
  mainClass: child.mainClass ?? parent.mainClass,
@@ -2315,19 +3135,18 @@ function mergeManifest(parent, child) {
2315
3135
  assets: child.assets ?? parent.assets,
2316
3136
  downloads: { ...parent.downloads, ...child.downloads },
2317
3137
  libraries: mergeLibraries(parent.libraries, child.libraries),
2318
- arguments: mergeArguments(parent.arguments, child.arguments),
2319
- minecraftArguments: child.minecraftArguments ?? parent.minecraftArguments,
2320
- javaVersion: child.javaVersion ?? parent.javaVersion,
2321
- logging: child.logging ?? parent.logging,
2322
- inheritsFrom: child.inheritsFrom ?? parent.inheritsFrom,
2323
- releaseTime: child.releaseTime ?? parent.releaseTime,
2324
- time: child.time ?? parent.time,
2325
- minimumLauncherVersion: child.minimumLauncherVersion ?? parent.minimumLauncherVersion,
2326
- complianceLevel: child.complianceLevel ?? parent.complianceLevel
3138
+ ...args !== void 0 ? { arguments: args } : {},
3139
+ ...minecraftArguments !== void 0 ? { minecraftArguments } : {},
3140
+ ...javaVersion !== void 0 ? { javaVersion } : {},
3141
+ ...logging !== void 0 ? { logging } : {},
3142
+ ...inheritsFrom !== void 0 ? { inheritsFrom } : {},
3143
+ ...releaseTime !== void 0 ? { releaseTime } : {},
3144
+ ...time !== void 0 ? { time } : {},
3145
+ ...minimumLauncherVersion !== void 0 ? { minimumLauncherVersion } : {},
3146
+ ...complianceLevel !== void 0 ? { complianceLevel } : {}
2327
3147
  };
2328
- return merged;
2329
- }
2330
- function libraryDedupeKey(library) {
3148
+ };
3149
+ var libraryDedupeKey = (library) => {
2331
3150
  if (!library.name) return null;
2332
3151
  try {
2333
3152
  const coord = parseMavenCoordinate(library.name);
@@ -2336,8 +3155,8 @@ function libraryDedupeKey(library) {
2336
3155
  } catch {
2337
3156
  return null;
2338
3157
  }
2339
- }
2340
- function mergeLibraries(parent, child) {
3158
+ };
3159
+ var mergeLibraries = (parent, child) => {
2341
3160
  const byKey = /* @__PURE__ */ new Map();
2342
3161
  const unkeyed = [];
2343
3162
  for (const lib of [...parent, ...child]) {
@@ -2349,8 +3168,8 @@ function mergeLibraries(parent, child) {
2349
3168
  byKey.set(key, lib);
2350
3169
  }
2351
3170
  return [...byKey.values(), ...unkeyed];
2352
- }
2353
- function mergeArguments(parent, child) {
3171
+ };
3172
+ var mergeArguments = (parent, child) => {
2354
3173
  if (!parent && !child) return void 0;
2355
3174
  const parentGame = parent?.game ?? [];
2356
3175
  const parentJvm = parent?.jvm ?? [];
@@ -2360,10 +3179,10 @@ function mergeArguments(parent, child) {
2360
3179
  game: [...parentGame, ...childGame],
2361
3180
  jvm: [...parentJvm, ...childJvm]
2362
3181
  };
2363
- }
3182
+ };
2364
3183
 
2365
3184
  // src/launch/version-resolution.ts
2366
- async function resolveLaunchVersion(target) {
3185
+ var resolveLaunchVersion = async (target) => {
2367
3186
  if (target.loader.type === Loaders.VANILLA) {
2368
3187
  return {
2369
3188
  versionId: target.minecraft.version,
@@ -2374,8 +3193,8 @@ async function resolveLaunchVersion(target) {
2374
3193
  const versionId = await pickInstalledVersionId(target);
2375
3194
  const merged = await loadAndMerge(target.directory, versionId, target.minecraft.manifest);
2376
3195
  return { versionId, merged, chain: [versionId, target.minecraft.version] };
2377
- }
2378
- async function pickClientJarVersionId(directory, chain) {
3196
+ };
3197
+ var pickClientJarVersionId = async (directory, chain) => {
2379
3198
  for (const id of chain) {
2380
3199
  const jar = targetPaths.versionJar(directory, id);
2381
3200
  if (await fileExists(jar)) return id;
@@ -2383,13 +3202,13 @@ async function pickClientJarVersionId(directory, chain) {
2383
3202
  const fallback = chain.at(-1);
2384
3203
  if (fallback === void 0) {
2385
3204
  throw new MinecraftKitError(
2386
- "MANIFEST_NOT_FOUND",
3205
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
2387
3206
  "Cannot resolve a client jar version id from an empty inheritsFrom chain"
2388
3207
  );
2389
3208
  }
2390
3209
  return fallback;
2391
- }
2392
- async function pickInstalledVersionId(target) {
3210
+ };
3211
+ var pickInstalledVersionId = async (target) => {
2393
3212
  if (target.loader.type === Loaders.FABRIC) {
2394
3213
  const candidate = target.loader.profile.id;
2395
3214
  const versionJsonPath = targetPaths.versionJson(target.directory, candidate);
@@ -2401,46 +3220,38 @@ async function pickInstalledVersionId(target) {
2401
3220
  const versionJsonPath = targetPaths.versionJson(target.directory, id);
2402
3221
  if (!await fileExists(versionJsonPath)) continue;
2403
3222
  const text = await readText(versionJsonPath);
2404
- try {
2405
- const parsed = JSON.parse(text);
2406
- if (parsed.inheritsFrom === target.minecraft.version && (id.includes("forge") || (parsed.id ?? "").includes("forge"))) {
2407
- return id;
2408
- }
2409
- } catch {
3223
+ const parsed = parseJsonOrUndefined(text);
3224
+ if (parsed?.inheritsFrom === target.minecraft.version && (id.includes("forge") || (parsed.id ?? "").includes("forge"))) {
3225
+ return id;
2410
3226
  }
2411
3227
  }
2412
3228
  }
2413
3229
  throw new MinecraftKitError(
2414
- "MANIFEST_NOT_FOUND",
3230
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
2415
3231
  `Could not find an installed version JSON for target ${target.id}`,
2416
3232
  { context: { targetId: target.id, loaderType: target.loader.type } }
2417
3233
  );
2418
- }
2419
- async function loadAndMerge(directory, versionId, parentManifest) {
3234
+ };
3235
+ var loadAndMerge = async (directory, versionId, parentManifest) => {
2420
3236
  const versionJsonPath = targetPaths.versionJson(directory, versionId);
2421
3237
  const text = await readText(versionJsonPath);
2422
- let child;
2423
- try {
2424
- child = JSON.parse(text);
2425
- } catch (cause) {
2426
- throw new MinecraftKitError(
2427
- "MANIFEST_INVALID",
2428
- `Version JSON is not valid JSON: ${versionJsonPath}`,
2429
- { cause, context: { filePath: versionJsonPath } }
2430
- );
2431
- }
3238
+ const child = parseJsonStrict(text, {
3239
+ code: MinecraftKitErrorCodes.MANIFEST_INVALID,
3240
+ message: `Version JSON is not valid JSON: ${versionJsonPath}`,
3241
+ context: { filePath: versionJsonPath }
3242
+ });
2432
3243
  if (child.inheritsFrom !== void 0 && child.inheritsFrom !== parentManifest.id) {
2433
3244
  return mergeManifest(parentManifest, child);
2434
3245
  }
2435
3246
  return mergeManifest(parentManifest, child);
2436
- }
3247
+ };
2437
3248
 
2438
3249
  // src/launch/compose.ts
2439
- async function composeLaunch(input) {
3250
+ var composeLaunch = async (input) => {
2440
3251
  const { target, options } = input;
2441
3252
  if (!options.auth.username || options.auth.username.length === 0) {
2442
3253
  throw new MinecraftKitError(
2443
- "INVALID_INPUT",
3254
+ MinecraftKitErrorCodes.INVALID_INPUT,
2444
3255
  `Auth username must be non-empty (target ${target.id})`,
2445
3256
  { context: { targetId: target.id } }
2446
3257
  );
@@ -2487,22 +3298,22 @@ async function composeLaunch(input) {
2487
3298
  auth: options.auth,
2488
3299
  workingDirectory: target.directory
2489
3300
  };
2490
- }
2491
- function buildFeatures(options) {
3301
+ };
3302
+ var buildFeatures = (options) => {
2492
3303
  const features = { ...options.features ?? {} };
2493
3304
  if (options.resolution !== void 0) {
2494
3305
  features.has_custom_resolution = true;
2495
3306
  }
2496
3307
  return features;
2497
- }
3308
+ };
2498
3309
 
2499
3310
  // src/launch/runner.ts
2500
- function runLaunch(input) {
3311
+ var runLaunch = (input) => {
2501
3312
  const composition = input.composition;
2502
3313
  const options = input.options ?? {};
2503
3314
  const args = [...composition.jvmArgs, composition.mainClass, ...composition.gameArgs];
2504
3315
  options.onEvent?.({
2505
- type: "launch:starting",
3316
+ type: EventTypes.LAUNCH_STARTING,
2506
3317
  command: composition.javaPath,
2507
3318
  args,
2508
3319
  cwd: composition.workingDirectory
@@ -2511,19 +3322,19 @@ function runLaunch(input) {
2511
3322
  cwd: composition.workingDirectory,
2512
3323
  ...composition.env !== void 0 ? { env: composition.env } : {}
2513
3324
  });
2514
- options.onEvent?.({ type: "launch:started", pid: child.pid });
3325
+ options.onEvent?.({ type: EventTypes.LAUNCH_STARTED, pid: child.pid });
2515
3326
  child.stdout.on("data", (line) => {
2516
- options.onEvent?.({ type: "launch:stdout", line });
3327
+ options.onEvent?.({ type: EventTypes.LAUNCH_STDOUT, line });
2517
3328
  });
2518
3329
  child.stderr.on("data", (line) => {
2519
- options.onEvent?.({ type: "launch:stderr", line });
3330
+ options.onEvent?.({ type: EventTypes.LAUNCH_STDERR, line });
2520
3331
  });
2521
3332
  const grace = options.killGracePeriodMs ?? DEFAULT_KILL_GRACE_MS;
2522
3333
  let aborted = false;
2523
3334
  const doAbort = (reason) => {
2524
3335
  if (aborted) return;
2525
3336
  aborted = true;
2526
- options.onEvent?.({ type: "launch:aborted", reason });
3337
+ options.onEvent?.({ type: EventTypes.LAUNCH_ABORTED, reason });
2527
3338
  child.kill("SIGTERM");
2528
3339
  setTimeout(() => child.kill("SIGKILL"), grace).unref();
2529
3340
  };
@@ -2538,10 +3349,10 @@ function runLaunch(input) {
2538
3349
  }
2539
3350
  const exited = (async () => {
2540
3351
  const { code, signal } = await child.exited;
2541
- options.onEvent?.({ type: "launch:exited", code, signal });
3352
+ options.onEvent?.({ type: EventTypes.LAUNCH_EXITED, code, signal });
2542
3353
  if (!aborted && code !== 0 && code !== null) {
2543
3354
  throw new MinecraftKitError(
2544
- "LAUNCH_PROCESS_FAILED",
3355
+ MinecraftKitErrorCodes.LAUNCH_PROCESS_FAILED,
2545
3356
  `Minecraft process exited with code ${code}`,
2546
3357
  { context: { exitCode: code } }
2547
3358
  );
@@ -2555,13 +3366,13 @@ function runLaunch(input) {
2555
3366
  doAbort(reason ?? "user");
2556
3367
  }
2557
3368
  };
2558
- }
2559
- function reasonFrom(value) {
3369
+ };
3370
+ var reasonFrom = (value) => {
2560
3371
  if (value === void 0) return "aborted";
2561
3372
  if (typeof value === "string") return value;
2562
3373
  if (value instanceof Error) return value.message;
2563
3374
  return String(value);
2564
- }
3375
+ };
2565
3376
  var ChildProcessSpawner = class {
2566
3377
  spawn(command, args, options) {
2567
3378
  const child = child_process.spawn(command, [...args], {
@@ -2585,14 +3396,14 @@ var ChildProcessSpawner = class {
2585
3396
  };
2586
3397
  }
2587
3398
  };
2588
- function streamFromBuffer(stream) {
3399
+ var streamFromBuffer = (stream) => {
2589
3400
  if (!stream) {
2590
3401
  return { on() {
2591
3402
  } };
2592
3403
  }
2593
3404
  let buffer$1 = "";
2594
3405
  const listeners = /* @__PURE__ */ new Set();
2595
- const emit = (line) => {
3406
+ const emit2 = (line) => {
2596
3407
  for (const listener of listeners) listener(line);
2597
3408
  };
2598
3409
  stream.on("data", (chunk) => {
@@ -2602,35 +3413,36 @@ function streamFromBuffer(stream) {
2602
3413
  while (index !== -1) {
2603
3414
  const line = buffer$1.slice(0, index).replace(/\r$/, "");
2604
3415
  buffer$1 = buffer$1.slice(index + 1);
2605
- emitBounded(emit, line);
3416
+ emitBounded(emit2, line);
2606
3417
  index = buffer$1.indexOf("\n");
2607
3418
  }
2608
3419
  while (buffer$1.length > SPAWNER_MAX_LINE_BYTES) {
2609
- emit(buffer$1.slice(0, SPAWNER_MAX_LINE_BYTES));
3420
+ emit2(buffer$1.slice(0, SPAWNER_MAX_LINE_BYTES));
2610
3421
  buffer$1 = buffer$1.slice(SPAWNER_MAX_LINE_BYTES);
2611
3422
  }
2612
3423
  });
2613
3424
  stream.on("end", () => {
2614
3425
  if (buffer$1.length > 0) {
2615
- emitBounded(emit, buffer$1);
3426
+ emitBounded(emit2, buffer$1);
2616
3427
  buffer$1 = "";
2617
3428
  }
3429
+ listeners.clear();
2618
3430
  });
2619
3431
  return {
2620
3432
  on(_event, listener) {
2621
3433
  listeners.add(listener);
2622
3434
  }
2623
3435
  };
2624
- }
2625
- function emitBounded(emit, line) {
3436
+ };
3437
+ var emitBounded = (emit2, line) => {
2626
3438
  if (line.length <= SPAWNER_MAX_LINE_BYTES) {
2627
- emit(line);
3439
+ emit2(line);
2628
3440
  return;
2629
3441
  }
2630
3442
  for (let i = 0; i < line.length; i += SPAWNER_MAX_LINE_BYTES) {
2631
- emit(line.slice(i, i + SPAWNER_MAX_LINE_BYTES));
3443
+ emit2(line.slice(i, i + SPAWNER_MAX_LINE_BYTES));
2632
3444
  }
2633
- }
3445
+ };
2634
3446
 
2635
3447
  // src/types/verify.ts
2636
3448
  var VerificationKinds = {
@@ -2655,24 +3467,14 @@ var VerifyFileCategories = {
2655
3467
  RUNTIME_FILE: "runtime-file",
2656
3468
  LOGGING_CONFIG: "logging-config"
2657
3469
  };
2658
- async function sha1OfFile(filePath) {
2659
- const hash = crypto2__default.default.createHash("sha1");
2660
- await new Promise((resolve, reject) => {
2661
- const stream = fs$1.createReadStream(filePath);
2662
- stream.on("data", (chunk) => hash.update(chunk));
2663
- stream.on("end", resolve);
2664
- stream.on("error", reject);
2665
- });
2666
- return hash.digest("hex");
2667
- }
2668
3470
 
2669
3471
  // src/verify/helpers.ts
2670
- async function runVerification(input, check) {
3472
+ var runVerification = async (input, check) => {
2671
3473
  const startedAt = Date.now();
2672
3474
  const results = [];
2673
3475
  const record = (result) => {
2674
3476
  results.push(result);
2675
- input.onEvent?.({ type: "verify:file-checked", file: result });
3477
+ input.onEvent?.({ type: EventTypes.VERIFY_FILE_CHECKED, file: result });
2676
3478
  };
2677
3479
  await check(record);
2678
3480
  return {
@@ -2683,8 +3485,8 @@ async function runVerification(input, check) {
2683
3485
  checkedFiles: results.length,
2684
3486
  durationMs: Date.now() - startedAt
2685
3487
  };
2686
- }
2687
- async function verifyHashedFile(input) {
3488
+ };
3489
+ var verifyHashedFile = async (input) => {
2688
3490
  if (!await fileExists(input.path)) {
2689
3491
  return {
2690
3492
  path: input.path,
@@ -2731,8 +3533,8 @@ async function verifyHashedFile(input) {
2731
3533
  ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2732
3534
  ...input.url !== void 0 ? { url: input.url } : {}
2733
3535
  };
2734
- }
2735
- async function verifyExistence(input) {
3536
+ };
3537
+ var verifyExistence = async (input) => {
2736
3538
  if (await fileExists(input.path)) {
2737
3539
  return {
2738
3540
  path: input.path,
@@ -2747,8 +3549,8 @@ async function verifyExistence(input) {
2747
3549
  status: VerifyFileStatuses.MISSING,
2748
3550
  ...input.url !== void 0 ? { url: input.url } : {}
2749
3551
  };
2750
- }
2751
- async function findForgeVersionJsonPath(directory, minecraftVersion) {
3552
+ };
3553
+ var findForgeVersionJsonPath = async (directory, minecraftVersion) => {
2752
3554
  const versionsDir = targetPaths.versionsDir(directory);
2753
3555
  const dirs = await listChildDirectories(versionsDir);
2754
3556
  for (const id of dirs) {
@@ -2761,21 +3563,17 @@ async function findForgeVersionJsonPath(directory, minecraftVersion) {
2761
3563
  if (parsed === minecraftVersion) return jsonPath;
2762
3564
  }
2763
3565
  return null;
2764
- }
2765
- async function tryParseInheritsFrom(jsonPath) {
2766
- try {
2767
- const parsed = JSON.parse(await readText(jsonPath));
2768
- return parsed.inheritsFrom;
2769
- } catch {
2770
- return void 0;
2771
- }
2772
- }
3566
+ };
3567
+ var tryParseInheritsFrom = async (jsonPath) => {
3568
+ const parsed = parseJsonOrUndefined(await readText(jsonPath));
3569
+ return parsed?.inheritsFrom;
3570
+ };
2773
3571
 
2774
3572
  // src/verify/fabric.ts
2775
- async function verifyFabric(input) {
3573
+ var verifyFabric = async (input) => {
2776
3574
  if (input.target.loader.type !== Loaders.FABRIC) {
2777
3575
  throw new MinecraftKitError(
2778
- "INVALID_INPUT",
3576
+ MinecraftKitErrorCodes.INVALID_INPUT,
2779
3577
  `verify.fabric requires a Fabric target (got ${input.target.loader.type})`
2780
3578
  );
2781
3579
  }
@@ -2798,14 +3596,14 @@ async function verifyFabric(input) {
2798
3596
  directory: input.target.directory,
2799
3597
  system: input.target.runtime.system,
2800
3598
  versionId: input.target.minecraft.version,
2801
- category: "fabric-library"
3599
+ category: DownloadCategories.FABRIC_LIBRARY
2802
3600
  });
2803
3601
  for (const action of fabricLibraries.downloads) {
2804
3602
  record(
2805
3603
  await verifyHashedFile({
2806
3604
  path: action.target,
2807
- expectedSha1: action.expectedSha1,
2808
- expectedSize: action.expectedSize,
3605
+ ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
3606
+ ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
2809
3607
  ...action.url ? { url: action.url } : {},
2810
3608
  category: VerifyFileCategories.LOADER_LIBRARY
2811
3609
  })
@@ -2813,13 +3611,13 @@ async function verifyFabric(input) {
2813
3611
  }
2814
3612
  }
2815
3613
  );
2816
- }
3614
+ };
2817
3615
 
2818
3616
  // src/verify/forge.ts
2819
- async function verifyForge(input) {
3617
+ var verifyForge = async (input) => {
2820
3618
  if (input.target.loader.type !== Loaders.FORGE) {
2821
3619
  throw new MinecraftKitError(
2822
- "INVALID_INPUT",
3620
+ MinecraftKitErrorCodes.INVALID_INPUT,
2823
3621
  `verify.forge requires a Forge target (got ${input.target.loader.type})`
2824
3622
  );
2825
3623
  }
@@ -2842,10 +3640,8 @@ async function verifyForge(input) {
2842
3640
  })
2843
3641
  );
2844
3642
  if (!await fileExists(forgeVersionJsonPath)) return;
2845
- let parsed;
2846
- try {
2847
- parsed = JSON.parse(await readText(forgeVersionJsonPath));
2848
- } catch {
3643
+ const parsed = parseJsonOrUndefined(await readText(forgeVersionJsonPath));
3644
+ if (parsed === void 0) {
2849
3645
  record({
2850
3646
  path: forgeVersionJsonPath,
2851
3647
  category: VerifyFileCategories.LOADER_LIBRARY,
@@ -2858,14 +3654,14 @@ async function verifyForge(input) {
2858
3654
  directory: input.target.directory,
2859
3655
  system: input.target.runtime.system,
2860
3656
  versionId: input.target.minecraft.version,
2861
- category: "forge-library"
3657
+ category: DownloadCategories.FORGE_LIBRARY
2862
3658
  });
2863
3659
  for (const action of forgeLibraries.downloads) {
2864
3660
  record(
2865
3661
  await verifyHashedFile({
2866
3662
  path: action.target,
2867
- expectedSha1: action.expectedSha1,
2868
- expectedSize: action.expectedSize,
3663
+ ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
3664
+ ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
2869
3665
  ...action.url ? { url: action.url } : {},
2870
3666
  category: VerifyFileCategories.LOADER_LIBRARY
2871
3667
  })
@@ -2873,10 +3669,10 @@ async function verifyForge(input) {
2873
3669
  }
2874
3670
  }
2875
3671
  );
2876
- }
3672
+ };
2877
3673
 
2878
3674
  // src/verify/minecraft.ts
2879
- async function verifyMinecraft(input) {
3675
+ var verifyMinecraft = async (input) => {
2880
3676
  return runVerification(
2881
3677
  {
2882
3678
  targetId: input.target.id,
@@ -2917,14 +3713,14 @@ async function verifyMinecraft(input) {
2917
3713
  directory,
2918
3714
  system: runtime.system,
2919
3715
  versionId: minecraft.version,
2920
- category: "library"
3716
+ category: DownloadCategories.LIBRARY
2921
3717
  });
2922
3718
  for (const action of libraryPlan.downloads) {
2923
3719
  record(
2924
3720
  await verifyHashedFile({
2925
3721
  path: action.target,
2926
- expectedSha1: action.expectedSha1,
2927
- expectedSize: action.expectedSize,
3722
+ ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
3723
+ ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
2928
3724
  url: action.url,
2929
3725
  category: VerifyFileCategories.LIBRARY
2930
3726
  })
@@ -2971,8 +3767,8 @@ async function verifyMinecraft(input) {
2971
3767
  }
2972
3768
  }
2973
3769
  );
2974
- }
2975
- async function verifyRuntime(input) {
3770
+ };
3771
+ var verifyRuntime = async (input) => {
2976
3772
  return runVerification(
2977
3773
  {
2978
3774
  targetId: input.target.id,
@@ -3014,13 +3810,13 @@ async function verifyRuntime(input) {
3014
3810
  }
3015
3811
  }
3016
3812
  );
3017
- }
3813
+ };
3018
3814
 
3019
3815
  // src/repair/helpers.ts
3020
- function asResultArray(from) {
3816
+ var asResultArray = (from) => {
3021
3817
  return Array.isArray(from) ? from : [from];
3022
- }
3023
- function buildIssueIndex(from) {
3818
+ };
3819
+ var buildIssueIndex = (from) => {
3024
3820
  const map = /* @__PURE__ */ new Map();
3025
3821
  for (const v of asResultArray(from)) {
3026
3822
  for (const issue of v.issues) {
@@ -3041,16 +3837,16 @@ function buildIssueIndex(from) {
3041
3837
  },
3042
3838
  categoriesAt: (path13) => map.get(path13) ?? /* @__PURE__ */ new Set()
3043
3839
  };
3044
- }
3045
- function sumDownloadBytes(actions) {
3840
+ };
3841
+ var sumDownloadBytes = (actions) => {
3046
3842
  return actions.reduce((sum, action) => {
3047
3843
  if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3048
3844
  return sum + (action.expectedSize ?? 0);
3049
3845
  }
3050
3846
  return sum;
3051
3847
  }, 0);
3052
- }
3053
- function buildRepairPlan(target, actions) {
3848
+ };
3849
+ var buildRepairPlan = (target, actions) => {
3054
3850
  return {
3055
3851
  targetId: target.id,
3056
3852
  directory: target.directory,
@@ -3059,8 +3855,8 @@ function buildRepairPlan(target, actions) {
3059
3855
  totalActions: actions.length,
3060
3856
  totalBytes: sumDownloadBytes(actions)
3061
3857
  };
3062
- }
3063
- async function planAspectRepair(input, aspectFilter, postprocess) {
3858
+ };
3859
+ var planAspectRepair = async (input, aspectFilter, postprocess) => {
3064
3860
  const installPlan = await planInstall({
3065
3861
  target: input.target,
3066
3862
  http: input.http,
@@ -3076,8 +3872,8 @@ async function planAspectRepair(input, aspectFilter, postprocess) {
3076
3872
  });
3077
3873
  postprocess?.({ actions, installPlan, issues });
3078
3874
  return buildRepairPlan(input.target, actions);
3079
- }
3080
- function selectRepairActions(input) {
3875
+ };
3876
+ var selectRepairActions = (input) => {
3081
3877
  const matching = [];
3082
3878
  for (const action of input.installPlan.actions) {
3083
3879
  if (!input.aspectFilter(action)) continue;
@@ -3098,13 +3894,13 @@ function selectRepairActions(input) {
3098
3894
  }
3099
3895
  }
3100
3896
  return matching;
3101
- }
3897
+ };
3102
3898
 
3103
3899
  // src/repair/fabric.ts
3104
- async function planFabricRepair(input) {
3900
+ var planFabricRepair = async (input) => {
3105
3901
  if (input.target.loader.type !== Loaders.FABRIC) {
3106
3902
  throw new MinecraftKitError(
3107
- "INVALID_INPUT",
3903
+ MinecraftKitErrorCodes.INVALID_INPUT,
3108
3904
  `repair.fabric requires a Fabric target (got ${input.target.loader.type})`
3109
3905
  );
3110
3906
  }
@@ -3114,24 +3910,24 @@ async function planFabricRepair(input) {
3114
3910
  );
3115
3911
  return planAspectRepair(input, (action) => {
3116
3912
  if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3117
- return action.category === "fabric-library";
3913
+ return action.category === DownloadCategories.FABRIC_LIBRARY;
3118
3914
  }
3119
3915
  if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
3120
3916
  return action.path === fabricJsonPath;
3121
3917
  }
3122
3918
  return false;
3123
3919
  });
3124
- }
3920
+ };
3125
3921
 
3126
3922
  // src/repair/forge.ts
3127
3923
  var FORGE_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
3128
- "forge-library",
3129
- "forge-installer"
3924
+ DownloadCategories.FORGE_LIBRARY,
3925
+ DownloadCategories.FORGE_INSTALLER
3130
3926
  ]);
3131
- async function planForgeRepair(input) {
3927
+ var planForgeRepair = async (input) => {
3132
3928
  if (input.target.loader.type !== Loaders.FORGE) {
3133
3929
  throw new MinecraftKitError(
3134
- "INVALID_INPUT",
3930
+ MinecraftKitErrorCodes.INVALID_INPUT,
3135
3931
  `repair.forge requires a Forge target (got ${input.target.loader.type})`
3136
3932
  );
3137
3933
  }
@@ -3156,7 +3952,7 @@ async function planForgeRepair(input) {
3156
3952
  actions.filter((a) => a.kind === InstallActionKinds.DOWNLOAD_FILE).map((a) => a.target)
3157
3953
  );
3158
3954
  for (const action of installPlan.actions) {
3159
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "forge-library" && !alreadyIncluded.has(action.target)) {
3955
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === DownloadCategories.FORGE_LIBRARY && !alreadyIncluded.has(action.target)) {
3160
3956
  actions.push(action);
3161
3957
  } else if (action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR) {
3162
3958
  actions.push(action);
@@ -3164,17 +3960,17 @@ async function planForgeRepair(input) {
3164
3960
  }
3165
3961
  }
3166
3962
  );
3167
- }
3963
+ };
3168
3964
 
3169
3965
  // src/repair/minecraft.ts
3170
3966
  var MINECRAFT_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
3171
- "client-jar",
3172
- "library",
3173
- "asset-index",
3174
- "asset",
3175
- "logging-config"
3967
+ DownloadCategories.CLIENT_JAR,
3968
+ DownloadCategories.LIBRARY,
3969
+ DownloadCategories.ASSET_INDEX,
3970
+ DownloadCategories.ASSET,
3971
+ DownloadCategories.LOGGING_CONFIG
3176
3972
  ]);
3177
- async function planMinecraftRepair(input) {
3973
+ var planMinecraftRepair = async (input) => {
3178
3974
  const vanillaJsonPath = targetPaths.versionJson(
3179
3975
  input.target.directory,
3180
3976
  input.target.minecraft.version
@@ -3191,10 +3987,10 @@ async function planMinecraftRepair(input) {
3191
3987
  }
3192
3988
  return false;
3193
3989
  });
3194
- }
3990
+ };
3195
3991
 
3196
3992
  // src/repair/runner.ts
3197
- async function runRepair(input) {
3993
+ var runRepair = async (input) => {
3198
3994
  const report = await runInstall({
3199
3995
  plan: {
3200
3996
  ...input.plan,
@@ -3213,18 +4009,18 @@ async function runRepair(input) {
3213
4009
  actionsCompleted: report.actionsCompleted,
3214
4010
  durationMs: report.durationMs
3215
4011
  };
3216
- }
4012
+ };
3217
4013
 
3218
4014
  // src/repair/runtime.ts
3219
- async function planRuntimeRepair(input) {
4015
+ var planRuntimeRepair = async (input) => {
3220
4016
  return planAspectRepair(
3221
4017
  input,
3222
- (action) => action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "runtime-file"
4018
+ (action) => action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === DownloadCategories.RUNTIME_FILE
3223
4019
  );
3224
- }
4020
+ };
3225
4021
 
3226
4022
  // src/repair/all.ts
3227
- async function repairAll(input) {
4023
+ var repairAll = async (input) => {
3228
4024
  const startedAt = Date.now();
3229
4025
  const ctx = {
3230
4026
  target: input.target,
@@ -3267,7 +4063,7 @@ async function repairAll(input) {
3267
4063
  bytesDownloaded,
3268
4064
  durationMs: Date.now() - startedAt
3269
4065
  };
3270
- }
4066
+ };
3271
4067
  var PLANNERS = {
3272
4068
  minecraft: planMinecraftRepair,
3273
4069
  runtime: planRuntimeRepair,
@@ -3306,14 +4102,27 @@ var TargetsApi = class {
3306
4102
  /** Build a {@link Target} from already-resolved components. */
3307
4103
  create(input) {
3308
4104
  if (!input.id) {
3309
- throw new MinecraftKitError("INVALID_INPUT", "Target id must be non-empty");
4105
+ throw new MinecraftKitError(
4106
+ MinecraftKitErrorCodes.INVALID_INPUT,
4107
+ "Target id must be non-empty"
4108
+ );
3310
4109
  }
3311
4110
  if (!input.directory) {
3312
- throw new MinecraftKitError("INVALID_INPUT", "Target directory must be non-empty");
4111
+ throw new MinecraftKitError(
4112
+ MinecraftKitErrorCodes.INVALID_INPUT,
4113
+ "Target directory must be non-empty"
4114
+ );
4115
+ }
4116
+ if (!path__default.default.isAbsolute(input.directory)) {
4117
+ throw new MinecraftKitError(
4118
+ MinecraftKitErrorCodes.INVALID_INPUT,
4119
+ `Target directory must be an absolute path, got: ${input.directory}`,
4120
+ { context: { directory: input.directory } }
4121
+ );
3313
4122
  }
3314
4123
  if (input.loader.minecraftVersion !== input.minecraft.version) {
3315
4124
  throw new MinecraftKitError(
3316
- "INVALID_INPUT",
4125
+ MinecraftKitErrorCodes.INVALID_INPUT,
3317
4126
  `Loader Minecraft version (${input.loader.minecraftVersion}) does not match resolved Minecraft (${input.minecraft.version})`,
3318
4127
  {
3319
4128
  context: {
@@ -3390,7 +4199,7 @@ var TargetsApi = class {
3390
4199
  return results;
3391
4200
  }
3392
4201
  };
3393
- async function discoverInstallation(id, directory) {
4202
+ var discoverInstallation = async (id, directory) => {
3394
4203
  const versionsDir = path__default.default.join(directory, VERSIONS_DIR);
3395
4204
  const librariesDir = path__default.default.join(directory, LIBRARIES_DIR);
3396
4205
  const assetsDir = path__default.default.join(directory, ASSETS_DIR);
@@ -3412,8 +4221,8 @@ async function discoverInstallation(id, directory) {
3412
4221
  }
3413
4222
  const runtime = await discoverRuntime(directory);
3414
4223
  return { id, directory, minecraftVersions, loaders, ...runtime ? { runtime } : {} };
3415
- }
3416
- async function discoverRuntime(directory) {
4224
+ };
4225
+ var discoverRuntime = async (directory) => {
3417
4226
  const runtimeDir = path__default.default.join(directory, RUNTIMES_DIR);
3418
4227
  if (!await dirExists(runtimeDir)) return void 0;
3419
4228
  let components;
@@ -3424,14 +4233,14 @@ async function discoverRuntime(directory) {
3424
4233
  }
3425
4234
  for (const component of components) {
3426
4235
  const root = path__default.default.join(runtimeDir, component);
3427
- const javaPath = process.platform === "win32" ? path__default.default.join(root, "bin", "javaw.exe") : process.platform === "darwin" ? path__default.default.join(root, "jre.bundle", "Contents", "Home", "bin", "java") : path__default.default.join(root, "bin", "java");
4236
+ const javaPath = javaExecutablePath(root);
3428
4237
  if (await fileExists(javaPath)) {
3429
4238
  return { component, javaPath };
3430
4239
  }
3431
4240
  }
3432
4241
  return void 0;
3433
- }
3434
- function inferLoaderFromVersionId(versionId) {
4242
+ };
4243
+ var inferLoaderFromVersionId = (versionId) => {
3435
4244
  const fabricMatch = /^fabric-loader-([^-]+)-(.+)$/.exec(versionId);
3436
4245
  if (fabricMatch?.[1] && fabricMatch[2]) {
3437
4246
  return { type: Loaders.FABRIC, version: fabricMatch[1], minecraftVersion: fabricMatch[2] };
@@ -3441,18 +4250,25 @@ function inferLoaderFromVersionId(versionId) {
3441
4250
  return { type: Loaders.FORGE, minecraftVersion: forgeMatch[1], version: forgeMatch[2] };
3442
4251
  }
3443
4252
  return null;
3444
- }
4253
+ };
4254
+ var javaExecutablePath = (runtimeRoot) => {
4255
+ if (process.platform === "win32") return path__default.default.join(runtimeRoot, "bin", "javaw.exe");
4256
+ if (process.platform === "darwin") {
4257
+ return path__default.default.join(runtimeRoot, "jre.bundle", "Contents", "Home", "bin", "java");
4258
+ }
4259
+ return path__default.default.join(runtimeRoot, "bin", "java");
4260
+ };
3445
4261
 
3446
4262
  // src/update/runner.ts
3447
- async function planUpdate(input) {
4263
+ var planUpdate = async (input) => {
3448
4264
  return planInstall({
3449
4265
  target: input.target,
3450
4266
  http: input.http,
3451
4267
  cache: input.cache,
3452
4268
  ...input.signal !== void 0 ? { signal: input.signal } : {}
3453
4269
  });
3454
- }
3455
- async function runUpdate(input) {
4270
+ };
4271
+ var runUpdate = async (input) => {
3456
4272
  const report = await runInstall({
3457
4273
  plan: input.plan,
3458
4274
  http: input.http,
@@ -3468,7 +4284,7 @@ async function runUpdate(input) {
3468
4284
  actionsSkipped: report.actionsSkipped,
3469
4285
  durationMs: report.durationMs
3470
4286
  };
3471
- }
4287
+ };
3472
4288
 
3473
4289
  // src/versions/fabric.ts
3474
4290
  var FabricVersionsApi = class {
@@ -3504,7 +4320,7 @@ var FabricVersionsApi = class {
3504
4320
  });
3505
4321
  if (loaders.length === 0) {
3506
4322
  throw new MinecraftKitError(
3507
- "MANIFEST_NOT_FOUND",
4323
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
3508
4324
  `No Fabric loader available for Minecraft ${input.minecraftVersion}`,
3509
4325
  { context: { version: input.minecraftVersion } }
3510
4326
  );
@@ -3512,9 +4328,11 @@ var FabricVersionsApi = class {
3512
4328
  const chosen = pickFabricLoader(loaders, input);
3513
4329
  if (!chosen) {
3514
4330
  throw new MinecraftKitError(
3515
- "MANIFEST_NOT_FOUND",
4331
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
3516
4332
  `Fabric loader version not found: ${input.loaderVersion ?? "(none matched)"}`,
3517
- { context: { version: input.loaderVersion } }
4333
+ {
4334
+ context: input.loaderVersion !== void 0 ? { version: input.loaderVersion } : {}
4335
+ }
3518
4336
  );
3519
4337
  }
3520
4338
  const profile = await fetchJson(this.ctx.http, this.ctx.cache, {
@@ -3530,7 +4348,7 @@ var FabricVersionsApi = class {
3530
4348
  };
3531
4349
  }
3532
4350
  };
3533
- function pickFabricLoader(loaders, input) {
4351
+ var pickFabricLoader = (loaders, input) => {
3534
4352
  if (input.loaderVersion !== void 0) {
3535
4353
  return loaders.find((l) => l.version === input.loaderVersion);
3536
4354
  }
@@ -3540,17 +4358,17 @@ function pickFabricLoader(loaders, input) {
3540
4358
  if (stable) return stable;
3541
4359
  }
3542
4360
  return loaders[0];
3543
- }
4361
+ };
3544
4362
 
3545
4363
  // src/core/xml.ts
3546
- function parseMavenMetadataVersions(xml) {
4364
+ var parseMavenMetadataVersions = (xml) => {
3547
4365
  const versions = [];
3548
4366
  const regex = /<version>\s*([^<]+?)\s*<\/version>/g;
3549
4367
  for (const match of xml.matchAll(regex)) {
3550
4368
  if (match[1]) versions.push(match[1]);
3551
4369
  }
3552
4370
  return versions;
3553
- }
4371
+ };
3554
4372
 
3555
4373
  // src/versions/forge.ts
3556
4374
  var ForgeVersionsApi = class {
@@ -3583,7 +4401,7 @@ var ForgeVersionsApi = class {
3583
4401
  });
3584
4402
  if (builds.length === 0) {
3585
4403
  throw new MinecraftKitError(
3586
- "MANIFEST_NOT_FOUND",
4404
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
3587
4405
  `No Forge build available for Minecraft ${input.minecraftVersion}`,
3588
4406
  { context: { version: input.minecraftVersion } }
3589
4407
  );
@@ -3591,9 +4409,11 @@ var ForgeVersionsApi = class {
3591
4409
  const chosen = pickForge(builds, input);
3592
4410
  if (!chosen) {
3593
4411
  throw new MinecraftKitError(
3594
- "MANIFEST_NOT_FOUND",
4412
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
3595
4413
  `Forge build not found for ${input.minecraftVersion}: ${input.forgeVersion ?? "(none matched)"}`,
3596
- { context: { version: input.forgeVersion } }
4414
+ {
4415
+ context: input.forgeVersion !== void 0 ? { version: input.forgeVersion } : {}
4416
+ }
3597
4417
  );
3598
4418
  }
3599
4419
  return {
@@ -3605,7 +4425,7 @@ var ForgeVersionsApi = class {
3605
4425
  };
3606
4426
  }
3607
4427
  };
3608
- function buildSummary(fullVersion, promotions) {
4428
+ var buildSummary = (fullVersion, promotions) => {
3609
4429
  const dashIndex = fullVersion.indexOf("-");
3610
4430
  if (dashIndex <= 0 || dashIndex === fullVersion.length - 1) return null;
3611
4431
  const minecraftVersion = fullVersion.slice(0, dashIndex);
@@ -3620,8 +4440,8 @@ function buildSummary(fullVersion, promotions) {
3620
4440
  isRecommended: recommended === forgeVersion,
3621
4441
  isLatest: latest === forgeVersion
3622
4442
  };
3623
- }
3624
- function pickForge(builds, input) {
4443
+ };
4444
+ var pickForge = (builds, input) => {
3625
4445
  if (input.forgeVersion !== void 0) {
3626
4446
  return builds.find(
3627
4447
  (b) => b.forgeVersion === input.forgeVersion || b.fullVersion === input.forgeVersion
@@ -3635,7 +4455,30 @@ function pickForge(builds, input) {
3635
4455
  const latest = builds.find((b) => b.isLatest);
3636
4456
  if (latest) return latest;
3637
4457
  return builds[builds.length - 1];
3638
- }
4458
+ };
4459
+
4460
+ // src/core/guards.ts
4461
+ var isPlainObject = (value) => {
4462
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4463
+ };
4464
+ var isNonEmptyString = (value) => {
4465
+ return typeof value === "string" && value.length > 0;
4466
+ };
4467
+ var isArtifactDownload = (value) => {
4468
+ if (!isPlainObject(value)) return false;
4469
+ return typeof value.sha1 === "string" && typeof value.size === "number" && isNonEmptyString(value.url);
4470
+ };
4471
+ var isMinecraftVersionManifestShape = (value) => {
4472
+ if (!isPlainObject(value)) return false;
4473
+ if (!isNonEmptyString(value.id)) return false;
4474
+ if (!isNonEmptyString(value.mainClass)) return false;
4475
+ if (!isPlainObject(value.assetIndex)) return false;
4476
+ if (!isNonEmptyString(value.assetIndex.id) || typeof value.assetIndex.sha1 !== "string" || typeof value.assetIndex.size !== "number" || !isNonEmptyString(value.assetIndex.url)) {
4477
+ return false;
4478
+ }
4479
+ if (!isPlainObject(value.downloads)) return false;
4480
+ return isArtifactDownload(value.downloads.client);
4481
+ };
3639
4482
 
3640
4483
  // src/versions/minecraft.ts
3641
4484
  var MinecraftVersionsApi = class {
@@ -3656,7 +4499,7 @@ var MinecraftVersionsApi = class {
3656
4499
  const summary = root.versions.find((v) => v.id === targetId);
3657
4500
  if (!summary) {
3658
4501
  throw new MinecraftKitError(
3659
- "MANIFEST_NOT_FOUND",
4502
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
3660
4503
  `Latest version ${targetId} not found in manifest`
3661
4504
  );
3662
4505
  }
@@ -3668,7 +4511,7 @@ var MinecraftVersionsApi = class {
3668
4511
  const summary = root.versions.find((v) => v.id === input.version);
3669
4512
  if (!summary) {
3670
4513
  throw new MinecraftKitError(
3671
- "MANIFEST_NOT_FOUND",
4514
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
3672
4515
  `Minecraft version not found: ${input.version}`,
3673
4516
  { context: { version: input.version } }
3674
4517
  );
@@ -3678,18 +4521,19 @@ var MinecraftVersionsApi = class {
3678
4521
  /** Fetch and parse the per-version manifest in addition to the summary. */
3679
4522
  async resolve(input) {
3680
4523
  const summary = await this.get(input);
3681
- const manifest = await fetchJson(this.ctx.http, this.ctx.cache, {
4524
+ const raw = await fetchJson(this.ctx.http, this.ctx.cache, {
3682
4525
  url: summary.url,
3683
4526
  cacheKey: `minecraft-manifest:${summary.id}:${summary.sha1}`,
3684
4527
  ...input.signal !== void 0 ? { signal: input.signal } : {}
3685
4528
  });
3686
- if (!manifest.id || !manifest.mainClass) {
4529
+ if (!isMinecraftVersionManifestShape(raw)) {
3687
4530
  throw new MinecraftKitError(
3688
- "MANIFEST_INVALID",
3689
- `Per-version manifest is missing required fields: ${summary.id}`,
4531
+ MinecraftKitErrorCodes.MANIFEST_INVALID,
4532
+ `Per-version manifest does not match the expected shape: ${summary.id}`,
3690
4533
  { context: { version: summary.id, url: summary.url } }
3691
4534
  );
3692
4535
  }
4536
+ const manifest = raw;
3693
4537
  return {
3694
4538
  version: summary.id,
3695
4539
  channel: summary.type,
@@ -3742,7 +4586,7 @@ var RuntimeVersionsApi = class {
3742
4586
  const platform = index[platformKey];
3743
4587
  if (!platform) {
3744
4588
  throw new MinecraftKitError(
3745
- "RUNTIME_UNSUPPORTED_PLATFORM",
4589
+ MinecraftKitErrorCodes.RUNTIME_UNSUPPORTED_PLATFORM,
3746
4590
  `No runtimes published for platform: ${platformKey}`,
3747
4591
  { context: { platform: platformKey } }
3748
4592
  );
@@ -3759,7 +4603,7 @@ var RuntimeVersionsApi = class {
3759
4603
  }
3760
4604
  }
3761
4605
  throw new MinecraftKitError(
3762
- "RUNTIME_NOT_FOUND",
4606
+ MinecraftKitErrorCodes.RUNTIME_NOT_FOUND,
3763
4607
  `Runtime component ${component} not available on ${platformKey}`,
3764
4608
  { context: { platform: platformKey, version: component } }
3765
4609
  );
@@ -3767,7 +4611,7 @@ var RuntimeVersionsApi = class {
3767
4611
  const entry = candidates[0];
3768
4612
  if (!entry) {
3769
4613
  throw new MinecraftKitError(
3770
- "RUNTIME_NOT_FOUND",
4614
+ MinecraftKitErrorCodes.RUNTIME_NOT_FOUND,
3771
4615
  `Runtime component ${component} list is empty for ${platformKey}`,
3772
4616
  { context: { platform: platformKey, version: component } }
3773
4617
  );
@@ -3782,11 +4626,11 @@ var RuntimeVersionsApi = class {
3782
4626
  });
3783
4627
  }
3784
4628
  };
3785
- function pickPlatformKey(system) {
4629
+ var pickPlatformKey = (system) => {
3786
4630
  const archMap = RUNTIME_PLATFORM_KEYS[system.os];
3787
4631
  return archMap[system.arch];
3788
- }
3789
- function pickLatestAcrossComponents(entries) {
4632
+ };
4633
+ var pickLatestAcrossComponents = (entries) => {
3790
4634
  let bestComponent = null;
3791
4635
  let bestEntry = null;
3792
4636
  for (const [component, list] of entries) {
@@ -3799,8 +4643,8 @@ function pickLatestAcrossComponents(entries) {
3799
4643
  }
3800
4644
  if (!bestComponent || !bestEntry) return null;
3801
4645
  return { component: bestComponent, entry: bestEntry };
3802
- }
3803
- function toResolved(component, platformKey, entry, system) {
4646
+ };
4647
+ var toResolved = (component, platformKey, entry, system) => {
3804
4648
  const majorVersion = parseMajorVersion(entry.version.name);
3805
4649
  return {
3806
4650
  component,
@@ -3811,13 +4655,13 @@ function toResolved(component, platformKey, entry, system) {
3811
4655
  manifestUrl: entry.manifest.url,
3812
4656
  manifestSha1: entry.manifest.sha1
3813
4657
  };
3814
- }
3815
- function parseMajorVersion(versionName) {
4658
+ };
4659
+ var parseMajorVersion = (versionName) => {
3816
4660
  const match = /^(\d+)/.exec(versionName);
3817
4661
  if (!match || !match[1]) return void 0;
3818
4662
  const parsed = Number.parseInt(match[1], 10);
3819
4663
  return Number.isFinite(parsed) ? parsed : void 0;
3820
- }
4664
+ };
3821
4665
 
3822
4666
  // src/kit.ts
3823
4667
  var MinecraftKit = class {
@@ -3828,6 +4672,12 @@ var MinecraftKit = class {
3828
4672
  verify;
3829
4673
  repair;
3830
4674
  launch;
4675
+ /**
4676
+ * Microsoft / Mojang authentication. Implements the device-code flow against Microsoft
4677
+ * Entra, exchanges the resulting tokens for an XSTS + Minecraft session, and returns
4678
+ * everything needed to compose an `OnlineAuth` for `launch.compose`.
4679
+ */
4680
+ auth;
3831
4681
  /** Cache surface useful for advanced consumers (e.g. clearing between operations). */
3832
4682
  cache;
3833
4683
  constructor(options = {}) {
@@ -3843,6 +4693,7 @@ var MinecraftKit = class {
3843
4693
  const runtime = new RuntimeVersionsApi(ctx);
3844
4694
  this.versions = { minecraft, fabric, forge, runtime };
3845
4695
  this.targets = new TargetsApi({ minecraft, fabric, forge, runtime, system });
4696
+ this.auth = new MojangAuthApi(http);
3846
4697
  this.cache = cache;
3847
4698
  const carry = (opts) => ({
3848
4699
  ...opts?.signal !== void 0 ? { signal: opts.signal } : {},
@@ -3923,26 +4774,9 @@ var MinecraftKit = class {
3923
4774
  }
3924
4775
  };
3925
4776
 
3926
- // src/core/pause-controller.ts
3927
- var PauseController = class {
3928
- #paused = false;
3929
- #waiters = [];
3930
- get paused() {
3931
- return this.#paused;
3932
- }
3933
- pause() {
3934
- this.#paused = true;
3935
- }
3936
- resume() {
3937
- this.#paused = false;
3938
- const list = this.#waiters;
3939
- this.#waiters = [];
3940
- for (const resolve of list) resolve();
3941
- }
3942
- waitWhilePaused() {
3943
- if (!this.#paused) return Promise.resolve();
3944
- return new Promise((resolve) => this.#waiters.push(resolve));
3945
- }
4777
+ // src/core/assert-never.ts
4778
+ var assertNever = (value) => {
4779
+ throw new Error(`Unhandled variant: ${JSON.stringify(value)}`);
3946
4780
  };
3947
4781
 
3948
4782
  // src/install/progress-tracker.ts
@@ -3954,15 +4788,15 @@ var InstallStages = {
3954
4788
  FINALIZE: "finalize"
3955
4789
  };
3956
4790
  var STAGE_FOR_CATEGORY = {
3957
- "runtime-file": InstallStages.RUNTIME,
3958
- "client-jar": InstallStages.MINECRAFT,
3959
- library: InstallStages.MINECRAFT,
3960
- "asset-index": InstallStages.MINECRAFT,
3961
- asset: InstallStages.MINECRAFT,
3962
- "logging-config": InstallStages.MINECRAFT,
3963
- "fabric-library": InstallStages.LOADER,
3964
- "forge-library": InstallStages.LOADER,
3965
- "forge-installer": InstallStages.LOADER
4791
+ [DownloadCategories.RUNTIME_FILE]: InstallStages.RUNTIME,
4792
+ [DownloadCategories.CLIENT_JAR]: InstallStages.MINECRAFT,
4793
+ [DownloadCategories.LIBRARY]: InstallStages.MINECRAFT,
4794
+ [DownloadCategories.ASSET_INDEX]: InstallStages.MINECRAFT,
4795
+ [DownloadCategories.ASSET]: InstallStages.MINECRAFT,
4796
+ [DownloadCategories.LOGGING_CONFIG]: InstallStages.MINECRAFT,
4797
+ [DownloadCategories.FABRIC_LIBRARY]: InstallStages.LOADER,
4798
+ [DownloadCategories.FORGE_LIBRARY]: InstallStages.LOADER,
4799
+ [DownloadCategories.FORGE_INSTALLER]: InstallStages.LOADER
3966
4800
  };
3967
4801
  var STAGE_FOR_PHASE = {
3968
4802
  [InstallPhases.PLANNING]: InstallStages.PREPARE,
@@ -3986,7 +4820,7 @@ var ALL_STAGES = [
3986
4820
  InstallStages.LOADER,
3987
4821
  InstallStages.FINALIZE
3988
4822
  ];
3989
- function createInstallProgressTracker(plan, options = {}) {
4823
+ var createInstallProgressTracker = (plan, options = {}) => {
3990
4824
  const throttleMs = options.throttleMs ?? 100;
3991
4825
  const stageOfTarget = /* @__PURE__ */ new Map();
3992
4826
  const expectedSizeOf = /* @__PURE__ */ new Map();
@@ -4070,7 +4904,7 @@ function createInstallProgressTracker(plan, options = {}) {
4070
4904
  };
4071
4905
  const onEvent = (event) => {
4072
4906
  switch (event.type) {
4073
- case "install:phase-changed": {
4907
+ case EventTypes.INSTALL_PHASE_CHANGED: {
4074
4908
  const next = STAGE_FOR_PHASE[event.phase];
4075
4909
  if (next && next !== currentStage) {
4076
4910
  currentStage = next;
@@ -4079,14 +4913,14 @@ function createInstallProgressTracker(plan, options = {}) {
4079
4913
  }
4080
4914
  return;
4081
4915
  }
4082
- case "download:started": {
4916
+ case EventTypes.DOWNLOAD_STARTED: {
4083
4917
  const stage = stageOfTarget.get(event.file.target) ?? currentStage;
4084
4918
  inFlightByTarget.set(event.file.target, { stage, bytes: 0 });
4085
4919
  currentFile = event.file.target;
4086
4920
  schedulePush();
4087
4921
  return;
4088
4922
  }
4089
- case "download:progress": {
4923
+ case EventTypes.DOWNLOAD_PROGRESS: {
4090
4924
  const entry = inFlightByTarget.get(event.file.target);
4091
4925
  if (entry) {
4092
4926
  const delta = event.bytesDownloaded - entry.bytes;
@@ -4100,7 +4934,7 @@ function createInstallProgressTracker(plan, options = {}) {
4100
4934
  schedulePush();
4101
4935
  return;
4102
4936
  }
4103
- case "download:skipped": {
4937
+ case EventTypes.DOWNLOAD_SKIPPED: {
4104
4938
  const stage = stageOfTarget.get(event.file.target);
4105
4939
  if (stage) {
4106
4940
  const size = expectedSizeOf.get(event.file.target) ?? 0;
@@ -4110,7 +4944,7 @@ function createInstallProgressTracker(plan, options = {}) {
4110
4944
  }
4111
4945
  return;
4112
4946
  }
4113
- case "download:completed": {
4947
+ case EventTypes.DOWNLOAD_COMPLETED: {
4114
4948
  const entry = inFlightByTarget.get(event.file.target);
4115
4949
  if (entry) {
4116
4950
  const finalBytes = event.bytes ?? entry.bytes;
@@ -4157,36 +4991,60 @@ function createInstallProgressTracker(plan, options = {}) {
4157
4991
  for (const listener of listeners) listener(snap);
4158
4992
  }
4159
4993
  };
4160
- }
4161
- function clamp(value) {
4994
+ };
4995
+ var clamp = (value) => {
4162
4996
  if (value <= 0) return 0;
4163
4997
  if (value >= 100) return 100;
4164
4998
  return value;
4165
- }
4999
+ };
4166
5000
 
4167
- // src/types/events.ts
4168
- var EventTypes = {
4169
- INSTALL_PHASE_CHANGED: "install:phase-changed",
4170
- DOWNLOAD_STARTED: "download:started",
4171
- DOWNLOAD_PROGRESS: "download:progress",
4172
- DOWNLOAD_SKIPPED: "download:skipped",
4173
- DOWNLOAD_COMPLETED: "download:completed",
4174
- DOWNLOAD_FAILED: "download:failed",
4175
- INTEGRITY_VERIFIED: "integrity:verified",
4176
- INTEGRITY_MISMATCH: "integrity:mismatch",
4177
- ARCHIVE_EXTRACTED: "archive:extracted",
4178
- FORGE_PROCESSOR_STARTED: "forge:processor-started",
4179
- FORGE_PROCESSOR_COMPLETED: "forge:processor-completed",
4180
- FORGE_PROCESSOR_OUTPUT_VERIFIED: "forge:processor-output-verified",
4181
- VERIFY_FILE_CHECKED: "verify:file-checked",
4182
- VERIFY_COMPLETED: "verify:completed",
4183
- REPAIR_PHASE_CHANGED: "repair:phase-changed",
4184
- LAUNCH_STARTING: "launch:starting",
4185
- LAUNCH_STARTED: "launch:started",
4186
- LAUNCH_STDOUT: "launch:stdout",
4187
- LAUNCH_STDERR: "launch:stderr",
4188
- LAUNCH_EXITED: "launch:exited",
4189
- LAUNCH_ABORTED: "launch:aborted"
5001
+ // src/core/pause-controller.ts
5002
+ var PauseController = class {
5003
+ #paused = false;
5004
+ #waiters = [];
5005
+ get paused() {
5006
+ return this.#paused;
5007
+ }
5008
+ pause() {
5009
+ this.#paused = true;
5010
+ }
5011
+ resume() {
5012
+ this.#paused = false;
5013
+ const list = this.#waiters;
5014
+ this.#waiters = [];
5015
+ for (const resolve of list) resolve();
5016
+ }
5017
+ waitWhilePaused() {
5018
+ if (!this.#paused) return Promise.resolve();
5019
+ return new Promise((resolve) => this.#waiters.push(resolve));
5020
+ }
5021
+ };
5022
+
5023
+ // src/constants/placeholders.ts
5024
+ var LAUNCH_PLACEHOLDERS = {
5025
+ "${auth_player_name}": "Player display name.",
5026
+ "${version_name}": "Resolved Minecraft version id.",
5027
+ "${game_directory}": "Per-target directory.",
5028
+ "${assets_root}": "Assets root (`<directory>/assets`).",
5029
+ "${assets_index_name}": "Asset index id from the manifest.",
5030
+ "${auth_uuid}": "Player UUID (no dashes).",
5031
+ "${auth_access_token}": "Yggdrasil/MSA access token.",
5032
+ "${auth_session}": "Legacy session token (`token:<token>:<uuid>`).",
5033
+ "${clientid}": "MSA client id.",
5034
+ "${auth_xuid}": "Xbox user id.",
5035
+ "${user_type}": "`msa` | `mojang` | `legacy`.",
5036
+ "${user_properties}": "User properties JSON (often `{}`).",
5037
+ "${version_type}": "Channel string, e.g. `release`.",
5038
+ "${game_assets}": "Legacy virtual assets directory.",
5039
+ "${resolution_width}": "Window width (feature-gated).",
5040
+ "${resolution_height}": "Window height (feature-gated).",
5041
+ "${natives_directory}": "Extracted natives directory.",
5042
+ "${classpath}": "Joined classpath of libraries + version jar.",
5043
+ "${classpath_separator}": "OS-specific classpath separator (`:` / `;`).",
5044
+ "${library_directory}": "Per-target libraries directory.",
5045
+ "${launcher_name}": "Launcher brand string.",
5046
+ "${launcher_version}": "Launcher version string.",
5047
+ "${path}": "Path to the log4j config file (logging.client.argument only)."
4190
5048
  };
4191
5049
 
4192
5050
  // src/types/minecraft.ts
@@ -4221,33 +5079,6 @@ var Architectures = {
4221
5079
  ARM64: "arm64"
4222
5080
  };
4223
5081
 
4224
- // src/constants/placeholders.ts
4225
- var LAUNCH_PLACEHOLDERS = {
4226
- "${auth_player_name}": "Player display name.",
4227
- "${version_name}": "Resolved Minecraft version id.",
4228
- "${game_directory}": "Per-target directory.",
4229
- "${assets_root}": "Assets root (`<directory>/assets`).",
4230
- "${assets_index_name}": "Asset index id from the manifest.",
4231
- "${auth_uuid}": "Player UUID (no dashes).",
4232
- "${auth_access_token}": "Yggdrasil/MSA access token.",
4233
- "${auth_session}": "Legacy session token (`token:<token>:<uuid>`).",
4234
- "${clientid}": "MSA client id.",
4235
- "${auth_xuid}": "Xbox user id.",
4236
- "${user_type}": "`msa` | `mojang` | `legacy`.",
4237
- "${user_properties}": "User properties JSON (often `{}`).",
4238
- "${version_type}": "Channel string, e.g. `release`.",
4239
- "${game_assets}": "Legacy virtual assets directory.",
4240
- "${resolution_width}": "Window width (feature-gated).",
4241
- "${resolution_height}": "Window height (feature-gated).",
4242
- "${natives_directory}": "Extracted natives directory.",
4243
- "${classpath}": "Joined classpath of libraries + version jar.",
4244
- "${classpath_separator}": "OS-specific classpath separator (`:` / `;`).",
4245
- "${library_directory}": "Per-target libraries directory.",
4246
- "${launcher_name}": "Launcher brand string.",
4247
- "${launcher_version}": "Launcher version string.",
4248
- "${path}": "Path to the log4j config file (logging.client.argument only)."
4249
- };
4250
-
4251
5082
  exports.ASSETS_DIR = ASSETS_DIR;
4252
5083
  exports.ASSETS_INDEXES_DIR = ASSETS_INDEXES_DIR;
4253
5084
  exports.ASSETS_LEGACY_DIR = ASSETS_LEGACY_DIR;
@@ -4261,6 +5092,7 @@ exports.AuthModes = AuthModes;
4261
5092
  exports.BASE_JVM_ARGS = BASE_JVM_ARGS;
4262
5093
  exports.CACHE_MAX_ENTRIES = CACHE_MAX_ENTRIES;
4263
5094
  exports.CACHE_TTL_MS = CACHE_TTL_MS;
5095
+ exports.CLIENT_ID_ENV_VAR = CLIENT_ID_ENV_VAR;
4264
5096
  exports.ChildProcessSpawner = ChildProcessSpawner;
4265
5097
  exports.DEFAULT_KILL_GRACE_MS = DEFAULT_KILL_GRACE_MS;
4266
5098
  exports.DEFAULT_LAUNCHER_NAME = DEFAULT_LAUNCHER_NAME;
@@ -4269,6 +5101,7 @@ exports.DEFAULT_LIBRARY_REPOSITORY = DEFAULT_LIBRARY_REPOSITORY;
4269
5101
  exports.DEFAULT_MAX_MB = DEFAULT_MAX_MB;
4270
5102
  exports.DEFAULT_MIN_MB = DEFAULT_MIN_MB;
4271
5103
  exports.DOWNLOAD_CONCURRENCY = DOWNLOAD_CONCURRENCY;
5104
+ exports.DownloadCategories = DownloadCategories;
4272
5105
  exports.EXTRACTION_MAX_COMPRESSION_RATIO = EXTRACTION_MAX_COMPRESSION_RATIO;
4273
5106
  exports.EXTRACTION_MAX_ENTRY_COUNT = EXTRACTION_MAX_ENTRY_COUNT;
4274
5107
  exports.EXTRACTION_MAX_FILE_SIZE = EXTRACTION_MAX_FILE_SIZE;
@@ -4301,7 +5134,9 @@ exports.MAX_PROCESSOR_STDERR_LINES = MAX_PROCESSOR_STDERR_LINES;
4301
5134
  exports.MinecraftChannels = MinecraftChannels;
4302
5135
  exports.MinecraftKit = MinecraftKit;
4303
5136
  exports.MinecraftKitError = MinecraftKitError;
5137
+ exports.MinecraftKitErrorCodes = MinecraftKitErrorCodes;
4304
5138
  exports.MinecraftVersionsApi = MinecraftVersionsApi;
5139
+ exports.MojangAuthApi = MojangAuthApi;
4305
5140
  exports.NATIVES_DIR_NAME = NATIVES_DIR_NAME;
4306
5141
  exports.NODE_ARCH_TO_MOJANG_ARCH = NODE_ARCH_TO_MOJANG_ARCH;
4307
5142
  exports.NODE_PLATFORM_TO_MOJANG_OS = NODE_PLATFORM_TO_MOJANG_OS;
@@ -4322,6 +5157,7 @@ exports.VerificationKinds = VerificationKinds;
4322
5157
  exports.VerifyFileCategories = VerifyFileCategories;
4323
5158
  exports.VerifyFileStatuses = VerifyFileStatuses;
4324
5159
  exports.VersionPreference = VersionPreference;
5160
+ exports.assertNever = assertNever;
4325
5161
  exports.consoleLogger = consoleLogger;
4326
5162
  exports.createInstallProgressTracker = createInstallProgressTracker;
4327
5163
  exports.createMemoryCache = createMemoryCache;
@@ -4340,9 +5176,11 @@ exports.planStandaloneRuntimeInstall = planStandaloneRuntimeInstall;
4340
5176
  exports.repairAll = repairAll;
4341
5177
  exports.resolveLaunchVersion = resolveLaunchVersion;
4342
5178
  exports.runRepair = runRepair;
5179
+ exports.scopedLogger = scopedLogger;
4343
5180
  exports.silentLogger = silentLogger;
4344
5181
  exports.stripUuidDashes = stripUuidDashes;
4345
5182
  exports.targetPaths = targetPaths;
5183
+ exports.toOnlineAuth = toOnlineAuth;
4346
5184
  exports.verifyFabric = verifyFabric;
4347
5185
  exports.verifyForge = verifyForge;
4348
5186
  exports.verifyMinecraft = verifyMinecraft;