@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.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import process2 from 'process';
2
3
  import os from 'os';
3
4
  import { LRUCache } from 'lru-cache';
4
5
  import path from 'path';
@@ -12,6 +13,317 @@ import { AsyncResource } from 'async_hooks';
12
13
  import { Buffer as Buffer$1 } from 'buffer';
13
14
  import { spawn } from 'child_process';
14
15
 
16
+ // src/types/errors.ts
17
+ var MinecraftKitErrorCodes = {
18
+ NETWORK_TIMEOUT: "NETWORK_TIMEOUT",
19
+ NETWORK_HTTP_ERROR: "NETWORK_HTTP_ERROR",
20
+ NETWORK_ABORTED: "NETWORK_ABORTED",
21
+ INTEGRITY_HASH_MISMATCH: "INTEGRITY_HASH_MISMATCH",
22
+ INTEGRITY_SIZE_MISMATCH: "INTEGRITY_SIZE_MISMATCH",
23
+ MANIFEST_INVALID: "MANIFEST_INVALID",
24
+ MANIFEST_NOT_FOUND: "MANIFEST_NOT_FOUND",
25
+ METADATA_PARSE_ERROR: "METADATA_PARSE_ERROR",
26
+ FILESYSTEM_PATH_TRAVERSAL: "FILESYSTEM_PATH_TRAVERSAL",
27
+ FILESYSTEM_WRITE_ERROR: "FILESYSTEM_WRITE_ERROR",
28
+ FILESYSTEM_READ_ERROR: "FILESYSTEM_READ_ERROR",
29
+ ARCHIVE_INVALID: "ARCHIVE_INVALID",
30
+ ARCHIVE_TOO_LARGE: "ARCHIVE_TOO_LARGE",
31
+ ARCHIVE_ENTRY_REJECTED: "ARCHIVE_ENTRY_REJECTED",
32
+ RUNTIME_NOT_FOUND: "RUNTIME_NOT_FOUND",
33
+ RUNTIME_UNSUPPORTED_PLATFORM: "RUNTIME_UNSUPPORTED_PLATFORM",
34
+ FORGE_PROCESSOR_FAILED: "FORGE_PROCESSOR_FAILED",
35
+ FORGE_INSTALLER_INVALID: "FORGE_INSTALLER_INVALID",
36
+ LAUNCH_JAVA_NOT_FOUND: "LAUNCH_JAVA_NOT_FOUND",
37
+ LAUNCH_PROCESS_FAILED: "LAUNCH_PROCESS_FAILED",
38
+ LAUNCH_ABORTED: "LAUNCH_ABORTED",
39
+ VERIFICATION_FAILED: "VERIFICATION_FAILED",
40
+ INVALID_INPUT: "INVALID_INPUT",
41
+ NOT_IMPLEMENTED: "NOT_IMPLEMENTED",
42
+ UNSUPPORTED_VERSION: "UNSUPPORTED_VERSION",
43
+ LZMA_DECODE_ERROR: "LZMA_DECODE_ERROR",
44
+ AUTH_DEVICE_CODE_EXPIRED: "AUTH_DEVICE_CODE_EXPIRED",
45
+ AUTH_DEVICE_CODE_DECLINED: "AUTH_DEVICE_CODE_DECLINED",
46
+ AUTH_DEVICE_CODE_FAILED: "AUTH_DEVICE_CODE_FAILED",
47
+ AUTH_REFRESH_FAILED: "AUTH_REFRESH_FAILED",
48
+ AUTH_XBOX_FAILED: "AUTH_XBOX_FAILED",
49
+ AUTH_XSTS_FAILED: "AUTH_XSTS_FAILED",
50
+ AUTH_MINECRAFT_FAILED: "AUTH_MINECRAFT_FAILED",
51
+ AUTH_NO_GAME_OWNERSHIP: "AUTH_NO_GAME_OWNERSHIP",
52
+ AUTH_MISSING_CLIENT_ID: "AUTH_MISSING_CLIENT_ID",
53
+ AUTH_CANCELLED: "AUTH_CANCELLED"
54
+ };
55
+
56
+ // src/core/errors.ts
57
+ var MinecraftKitError = class extends Error {
58
+ name = "MinecraftKitError";
59
+ /** Stable discriminator. */
60
+ code;
61
+ /** Structured context; safe to serialize. */
62
+ context;
63
+ constructor(code, message, options = {}) {
64
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
65
+ Object.setPrototypeOf(this, new.target.prototype);
66
+ this.code = code;
67
+ this.context = Object.freeze({ ...options.context ?? {} });
68
+ }
69
+ /** JSON-friendly representation. */
70
+ toJSON() {
71
+ return {
72
+ name: this.name,
73
+ code: this.code,
74
+ message: this.message,
75
+ context: this.context,
76
+ cause: this.cause instanceof Error ? { name: this.cause.name, message: this.cause.message } : this.cause
77
+ };
78
+ }
79
+ };
80
+ var isMinecraftKitError = (e) => {
81
+ return e instanceof MinecraftKitError;
82
+ };
83
+ var isErrorCode = (e, code) => {
84
+ return isMinecraftKitError(e) && e.code === code;
85
+ };
86
+
87
+ // src/types/auth.ts
88
+ var AuthModes = {
89
+ /** Offline-mode play with a chosen username and synthetic UUID. */
90
+ OFFLINE: "offline",
91
+ /** Pre-authenticated session — caller provides the access token and identity. */
92
+ ONLINE: "online"
93
+ };
94
+
95
+ // src/auth/microsoft.ts
96
+ var TENANT = "consumers";
97
+ var DEVICE_CODE_URL = `https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/devicecode`;
98
+ var TOKEN_URL = `https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/token`;
99
+ var SCOPE = "XboxLive.signin offline_access";
100
+ var startDeviceCode = async (input) => {
101
+ const body = new URLSearchParams({ client_id: input.clientId, scope: SCOPE });
102
+ const response = await input.http.request(DEVICE_CODE_URL, {
103
+ method: "POST",
104
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
105
+ body: body.toString(),
106
+ // We need to read the body of 400/401 responses to surface Microsoft's actual
107
+ // `error_description` rather than reporting "HTTP 400" — the most common cause is
108
+ // an app registration that doesn't allow personal MSA accounts or hasn't enabled
109
+ // public client flows.
110
+ acceptNonOk: true,
111
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
112
+ });
113
+ if (response.status < 200 || response.status >= 300) {
114
+ const err = await response.json().catch(() => ({}));
115
+ throw new MinecraftKitError(
116
+ MinecraftKitErrorCodes.AUTH_DEVICE_CODE_FAILED,
117
+ explainDeviceCodeError(err, input.clientId),
118
+ {
119
+ context: {
120
+ httpStatus: response.status,
121
+ microsoftError: err.error,
122
+ clientId: input.clientId
123
+ }
124
+ }
125
+ );
126
+ }
127
+ const data = await response.json();
128
+ const now = Date.now();
129
+ const state = {
130
+ deviceCode: data.device_code,
131
+ userCode: data.user_code,
132
+ verificationUri: data.verification_uri,
133
+ message: data.message,
134
+ expiresIn: data.expires_in,
135
+ interval: data.interval,
136
+ clientId: input.clientId,
137
+ expiresAt: now + data.expires_in * 1e3
138
+ };
139
+ const prompt = {
140
+ userCode: data.user_code,
141
+ verificationUri: data.verification_uri,
142
+ message: data.message,
143
+ expiresIn: data.expires_in,
144
+ interval: data.interval
145
+ };
146
+ return { prompt, state };
147
+ };
148
+ var pollDeviceCode = async (input) => {
149
+ let intervalSec = input.state.interval;
150
+ while (true) {
151
+ if (input.signal?.aborted) {
152
+ throw new MinecraftKitError(
153
+ MinecraftKitErrorCodes.AUTH_CANCELLED,
154
+ "Device-code polling aborted.",
155
+ {
156
+ context: { reason: input.signal.reason }
157
+ }
158
+ );
159
+ }
160
+ if (Date.now() >= input.state.expiresAt) {
161
+ throw new MinecraftKitError(
162
+ MinecraftKitErrorCodes.AUTH_DEVICE_CODE_EXPIRED,
163
+ "Device code expired before the user signed in."
164
+ );
165
+ }
166
+ const delayMs = intervalSec * 1e3;
167
+ input.onTick?.({ nextDelayMs: delayMs, expiresAt: input.state.expiresAt });
168
+ await wait(delayMs, input.signal);
169
+ const body = new URLSearchParams({
170
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
171
+ client_id: input.state.clientId,
172
+ device_code: input.state.deviceCode
173
+ });
174
+ const response = await input.http.request(TOKEN_URL, {
175
+ method: "POST",
176
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
177
+ body: body.toString(),
178
+ acceptNonOk: true
179
+ });
180
+ if (response.status >= 200 && response.status < 300) {
181
+ const ok = await response.json();
182
+ if (!ok.refresh_token) {
183
+ throw new MinecraftKitError(
184
+ MinecraftKitErrorCodes.AUTH_DEVICE_CODE_FAILED,
185
+ "Microsoft did not return a refresh token. Make sure `offline_access` is in the requested scopes."
186
+ );
187
+ }
188
+ return {
189
+ accessToken: ok.access_token,
190
+ refreshToken: ok.refresh_token,
191
+ expiresIn: ok.expires_in
192
+ };
193
+ }
194
+ const err = await response.json().catch(() => ({}));
195
+ switch (err.error) {
196
+ case "authorization_pending":
197
+ continue;
198
+ case "slow_down":
199
+ intervalSec += 5;
200
+ continue;
201
+ case "authorization_declined":
202
+ throw new MinecraftKitError(
203
+ MinecraftKitErrorCodes.AUTH_DEVICE_CODE_DECLINED,
204
+ "The user declined the sign-in request."
205
+ );
206
+ case "expired_token":
207
+ throw new MinecraftKitError(
208
+ MinecraftKitErrorCodes.AUTH_DEVICE_CODE_EXPIRED,
209
+ "Device code expired before the user signed in."
210
+ );
211
+ default:
212
+ throw new MinecraftKitError(
213
+ MinecraftKitErrorCodes.AUTH_DEVICE_CODE_FAILED,
214
+ `Microsoft device-code exchange failed: ${err.error ?? "unknown_error"}${err.error_description ? ` \u2014 ${err.error_description}` : ""}`,
215
+ { context: { httpStatus: response.status, microsoftError: err.error } }
216
+ );
217
+ }
218
+ }
219
+ };
220
+ var refreshMicrosoftToken = async (input) => {
221
+ const body = new URLSearchParams({
222
+ grant_type: "refresh_token",
223
+ client_id: input.clientId,
224
+ refresh_token: input.refreshToken,
225
+ scope: SCOPE
226
+ });
227
+ const response = await input.http.request(TOKEN_URL, {
228
+ method: "POST",
229
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
230
+ body: body.toString(),
231
+ acceptNonOk: true,
232
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
233
+ });
234
+ if (response.status < 200 || response.status >= 300) {
235
+ const err = await response.json().catch(() => ({}));
236
+ throw new MinecraftKitError(
237
+ MinecraftKitErrorCodes.AUTH_REFRESH_FAILED,
238
+ `Microsoft refused to refresh the token: ${err.error ?? "unknown_error"}${err.error_description ? ` \u2014 ${err.error_description}` : ""}`,
239
+ { context: { httpStatus: response.status, microsoftError: err.error } }
240
+ );
241
+ }
242
+ const ok = await response.json();
243
+ return {
244
+ accessToken: ok.access_token,
245
+ refreshToken: ok.refresh_token ?? input.refreshToken,
246
+ expiresIn: ok.expires_in
247
+ };
248
+ };
249
+ var explainDeviceCodeError = (err, clientId) => {
250
+ const desc = err.error_description ?? "";
251
+ const ms = desc ? ` \u2014 ${desc}` : "";
252
+ if (/AADSTS700016/i.test(desc) || /not found in the directory/i.test(desc)) {
253
+ 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}`;
254
+ }
255
+ if (/AADSTS7000218/i.test(desc) || /must either be a confidential client/i.test(desc)) {
256
+ 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}`;
257
+ }
258
+ if (/AADSTS50059/i.test(desc) || /tenant identifier/i.test(desc)) {
259
+ 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}`;
260
+ }
261
+ switch (err.error) {
262
+ case "unauthorized_client":
263
+ 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}`;
264
+ case "invalid_client":
265
+ 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}`;
266
+ case "invalid_request":
267
+ return `Microsoft rejected the device-code request as malformed.${ms}`;
268
+ case "invalid_scope":
269
+ return `Microsoft refused the requested scope (XboxLive.signin offline_access). Make sure the app is configured for Microsoft account sign-in.${ms}`;
270
+ default:
271
+ return `Microsoft device-code request failed: ${err.error ?? "unknown_error"}${ms}`;
272
+ }
273
+ };
274
+ var wait = (ms, signal) => {
275
+ return new Promise((resolve, reject) => {
276
+ if (signal?.aborted) {
277
+ reject(
278
+ new MinecraftKitError(
279
+ MinecraftKitErrorCodes.AUTH_CANCELLED,
280
+ "Device-code polling aborted.",
281
+ {
282
+ context: { reason: signal.reason }
283
+ }
284
+ )
285
+ );
286
+ return;
287
+ }
288
+ const timer = setTimeout(() => {
289
+ signal?.removeEventListener("abort", onAbort);
290
+ resolve();
291
+ }, ms);
292
+ const onAbort = () => {
293
+ clearTimeout(timer);
294
+ reject(
295
+ new MinecraftKitError(
296
+ MinecraftKitErrorCodes.AUTH_CANCELLED,
297
+ "Device-code polling aborted.",
298
+ {
299
+ context: { reason: signal?.reason }
300
+ }
301
+ )
302
+ );
303
+ };
304
+ signal?.addEventListener("abort", onAbort, { once: true });
305
+ });
306
+ };
307
+
308
+ // src/core/json.ts
309
+ var parseJsonStrict = (text, options) => {
310
+ try {
311
+ return JSON.parse(text);
312
+ } catch (cause) {
313
+ throw new MinecraftKitError(options.code, options.message, {
314
+ cause,
315
+ ...options.context !== void 0 ? { context: options.context } : {}
316
+ });
317
+ }
318
+ };
319
+ var parseJsonOrUndefined = (text) => {
320
+ try {
321
+ return JSON.parse(text);
322
+ } catch {
323
+ return void 0;
324
+ }
325
+ };
326
+
15
327
  // src/types/logger.ts
16
328
  var LogLevels = {
17
329
  DEBUG: "debug",
@@ -35,12 +347,361 @@ var consoleLogger = {
35
347
  }
36
348
  }
37
349
  };
38
- function pickConsole(level) {
350
+ var scopedLogger = (base, scope, baseFields) => {
351
+ if (base === silentLogger) return silentLogger;
352
+ return {
353
+ log(level, message, fields) {
354
+ const merged = baseFields !== void 0 || fields !== void 0 ? { ...baseFields, ...fields } : void 0;
355
+ if (merged !== void 0) {
356
+ base.log(level, `[${scope}] ${message}`, merged);
357
+ } else {
358
+ base.log(level, `[${scope}] ${message}`);
359
+ }
360
+ }
361
+ };
362
+ };
363
+ var pickConsole = (level) => {
39
364
  if (level === LogLevels.ERROR) return console.error.bind(console);
40
365
  if (level === LogLevels.WARN) return console.warn.bind(console);
41
366
  if (level === LogLevels.INFO) return console.info.bind(console);
42
367
  return console.debug.bind(console);
43
- }
368
+ };
369
+
370
+ // src/auth/debug.ts
371
+ var DEBUG_ENV_VAR = "MINECRAFT_KIT_AUTH_DEBUG";
372
+ var authDebug = (message) => {
373
+ if (process2.env[DEBUG_ENV_VAR]) {
374
+ process2.stderr.write(`[auth] ${message}
375
+ `);
376
+ }
377
+ };
378
+
379
+ // src/auth/minecraft.ts
380
+ var MC_LOGIN_URL = "https://api.minecraftservices.com/authentication/login_with_xbox";
381
+ var MC_PROFILE_URL = "https://api.minecraftservices.com/minecraft/profile";
382
+ var loginWithXbox = async (input) => {
383
+ const body = JSON.stringify({
384
+ identityToken: `XBL3.0 x=${input.userHash};${input.xstsToken}`
385
+ });
386
+ authDebug(
387
+ `login_with_xbox POST \u2014 userHashLen=${input.userHash.length}, xstsTokenLen=${input.xstsToken.length}`
388
+ );
389
+ const response = await input.http.request(MC_LOGIN_URL, {
390
+ method: "POST",
391
+ headers: {
392
+ "content-type": "application/json",
393
+ accept: "application/json",
394
+ // Mojang sometimes rejects unknown user-agents on the auth endpoints. Override the
395
+ // library default with a UA that matches what real Minecraft launchers send, so we
396
+ // don't trip an "anomalous client" filter.
397
+ "user-agent": "Minecraft Launcher/2.0 (minecraft-kit)"
398
+ },
399
+ body,
400
+ acceptNonOk: true,
401
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
402
+ });
403
+ if (response.status < 200 || response.status >= 300) {
404
+ const rawBody = await response.text().catch(() => "");
405
+ const detail = rawBody.slice(0, 400);
406
+ authDebug(`login_with_xbox failed status=${response.status} body=${detail}`);
407
+ if (response.status === 403) {
408
+ if (/invalid app registration/i.test(detail)) {
409
+ throw new MinecraftKitError(
410
+ MinecraftKitErrorCodes.AUTH_MINECRAFT_FAILED,
411
+ `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}`,
412
+ { context: { httpStatus: 403, body: detail, reason: "invalid_app_registration" } }
413
+ );
414
+ }
415
+ throw new MinecraftKitError(
416
+ MinecraftKitErrorCodes.AUTH_NO_GAME_OWNERSHIP,
417
+ `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>"}`,
418
+ { context: { httpStatus: 403, body: detail } }
419
+ );
420
+ }
421
+ throw new MinecraftKitError(
422
+ MinecraftKitErrorCodes.AUTH_MINECRAFT_FAILED,
423
+ `Minecraft sign-in failed with HTTP ${response.status}. Response: ${detail || "<empty>"}`,
424
+ { context: { httpStatus: response.status, body: detail } }
425
+ );
426
+ }
427
+ const parsed = await response.json();
428
+ if (!parsed.access_token) {
429
+ throw new MinecraftKitError(
430
+ MinecraftKitErrorCodes.AUTH_MINECRAFT_FAILED,
431
+ "Minecraft sign-in returned no access token."
432
+ );
433
+ }
434
+ return { accessToken: parsed.access_token, expiresIn: parsed.expires_in };
435
+ };
436
+ var fetchMinecraftProfile = async (input) => {
437
+ const response = await input.http.request(MC_PROFILE_URL, {
438
+ headers: {
439
+ authorization: `Bearer ${input.accessToken}`,
440
+ accept: "application/json",
441
+ "user-agent": "Minecraft Launcher/2.0 (minecraft-kit)"
442
+ },
443
+ acceptNonOk: true,
444
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
445
+ });
446
+ if (response.status === 404) {
447
+ throw new MinecraftKitError(
448
+ MinecraftKitErrorCodes.AUTH_NO_GAME_OWNERSHIP,
449
+ "This Microsoft account does not own Minecraft: Java Edition.",
450
+ { context: { httpStatus: 404 } }
451
+ );
452
+ }
453
+ if (response.status < 200 || response.status >= 300) {
454
+ throw new MinecraftKitError(
455
+ MinecraftKitErrorCodes.AUTH_MINECRAFT_FAILED,
456
+ `Failed to load Minecraft profile (HTTP ${response.status}).`,
457
+ { context: { httpStatus: response.status } }
458
+ );
459
+ }
460
+ const parsed = await response.json();
461
+ if (parsed.errorMessage || !parsed.id || !parsed.name) {
462
+ throw new MinecraftKitError(
463
+ MinecraftKitErrorCodes.AUTH_MINECRAFT_FAILED,
464
+ parsed.errorMessage ?? "Minecraft profile response was malformed."
465
+ );
466
+ }
467
+ return { uuid: dashUuid(parsed.id), username: parsed.name };
468
+ };
469
+ var extractXuid = (accessToken) => {
470
+ const parts = accessToken.split(".");
471
+ const payload = parts[1];
472
+ if (typeof payload !== "string") return "";
473
+ const json = Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString(
474
+ "utf8"
475
+ );
476
+ const parsed = parseJsonOrUndefined(json);
477
+ return typeof parsed?.xuid === "string" ? parsed.xuid : "";
478
+ };
479
+ var dashUuid = (raw) => {
480
+ if (raw.includes("-")) return raw;
481
+ if (raw.length !== 32) return raw;
482
+ return `${raw.slice(0, 8)}-${raw.slice(8, 12)}-${raw.slice(12, 16)}-${raw.slice(
483
+ 16,
484
+ 20
485
+ )}-${raw.slice(20)}`;
486
+ };
487
+
488
+ // src/auth/xbox.ts
489
+ var XBL_URL = "https://user.auth.xboxlive.com/user/authenticate";
490
+ var XSTS_URL = "https://xsts.auth.xboxlive.com/xsts/authorize";
491
+ var authenticateXbl = async (input) => {
492
+ const body = JSON.stringify({
493
+ Properties: {
494
+ AuthMethod: "RPS",
495
+ SiteName: "user.auth.xboxlive.com",
496
+ RpsTicket: `d=${input.accessToken}`
497
+ },
498
+ RelyingParty: "http://auth.xboxlive.com",
499
+ TokenType: "JWT"
500
+ });
501
+ let response;
502
+ try {
503
+ response = await input.http.request(XBL_URL, {
504
+ method: "POST",
505
+ headers: { "content-type": "application/json", accept: "application/json" },
506
+ body,
507
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
508
+ });
509
+ } catch (cause) {
510
+ throw new MinecraftKitError(
511
+ MinecraftKitErrorCodes.AUTH_XBOX_FAILED,
512
+ "Xbox Live authentication failed.",
513
+ {
514
+ cause
515
+ }
516
+ );
517
+ }
518
+ const parsed = await response.json();
519
+ const userHash = parsed.DisplayClaims?.xui?.[0]?.uhs;
520
+ if (!parsed.Token || !userHash) {
521
+ throw new MinecraftKitError(
522
+ MinecraftKitErrorCodes.AUTH_XBOX_FAILED,
523
+ "Xbox Live authentication returned an incomplete response."
524
+ );
525
+ }
526
+ input.logger?.log("debug", `XBL ok \u2014 tokenLen=${parsed.Token.length}, userHash=${userHash}`);
527
+ return { token: parsed.Token, userHash };
528
+ };
529
+ var authenticateXsts = async (input) => {
530
+ const body = JSON.stringify({
531
+ Properties: { SandboxId: "RETAIL", UserTokens: [input.xblToken] },
532
+ RelyingParty: "rp://api.minecraftservices.com/",
533
+ TokenType: "JWT"
534
+ });
535
+ const response = await input.http.request(XSTS_URL, {
536
+ method: "POST",
537
+ headers: { "content-type": "application/json", accept: "application/json" },
538
+ body,
539
+ acceptNonOk: true,
540
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
541
+ });
542
+ if (response.status === 401) {
543
+ const err = await response.json().catch(() => ({}));
544
+ throw new MinecraftKitError(MinecraftKitErrorCodes.AUTH_XSTS_FAILED, explainXErr(err.XErr), {
545
+ context: { xerr: err.XErr ?? null, message: err.Message ?? null }
546
+ });
547
+ }
548
+ if (response.status < 200 || response.status >= 300) {
549
+ throw new MinecraftKitError(
550
+ MinecraftKitErrorCodes.AUTH_XSTS_FAILED,
551
+ `XSTS authorization failed with HTTP ${response.status}.`,
552
+ { context: { httpStatus: response.status } }
553
+ );
554
+ }
555
+ const parsed = await response.json();
556
+ const userHash = parsed.DisplayClaims?.xui?.[0]?.uhs;
557
+ if (!parsed.Token || !userHash) {
558
+ throw new MinecraftKitError(
559
+ MinecraftKitErrorCodes.AUTH_XSTS_FAILED,
560
+ "XSTS authorization returned an incomplete response."
561
+ );
562
+ }
563
+ input.logger?.log("debug", `XSTS ok \u2014 tokenLen=${parsed.Token.length}, userHash=${userHash}`);
564
+ return { token: parsed.Token, userHash };
565
+ };
566
+ var explainXErr = (xerr) => {
567
+ switch (xerr) {
568
+ case 2148916227:
569
+ return "This account has been banned from Xbox.";
570
+ case 2148916233:
571
+ return "This Microsoft account has no Xbox profile yet \u2014 sign in once at https://minecraft.net to create one.";
572
+ case 2148916235:
573
+ return "Xbox Live is not available in this account's country/region.";
574
+ case 2148916236:
575
+ case 2148916237:
576
+ return "This account needs an adult to verify it. Sign in on Xbox to complete the prompt.";
577
+ case 2148916238:
578
+ return "This is a child account. Add it to a Microsoft family group with an adult to continue.";
579
+ default:
580
+ return xerr ? `XSTS authorization failed (XErr ${xerr}).` : "XSTS authorization failed.";
581
+ }
582
+ };
583
+
584
+ // src/auth/index.ts
585
+ var CLIENT_ID_ENV_VAR = "MINECRAFT_KIT_MSA_CLIENT_ID";
586
+ var MojangAuthApi = class {
587
+ constructor(http) {
588
+ this.http = http;
589
+ }
590
+ http;
591
+ /**
592
+ * Run the full device-code flow end-to-end. The caller supplies an `onPrompt` callback to
593
+ * render the URL + code to the user; this method polls until they finish.
594
+ */
595
+ async login(options) {
596
+ const clientId = resolveClientId(options.clientId);
597
+ const { prompt, state } = await startDeviceCode({
598
+ http: this.http,
599
+ clientId,
600
+ ...options.signal !== void 0 ? { signal: options.signal } : {}
601
+ });
602
+ await options.onPrompt(prompt);
603
+ const msToken = await pollDeviceCode({
604
+ http: this.http,
605
+ state,
606
+ ...options.signal !== void 0 ? { signal: options.signal } : {},
607
+ ...options.onPoll !== void 0 ? { onTick: options.onPoll } : {}
608
+ });
609
+ return this.completeMicrosoftToken(msToken, clientId, options.signal);
610
+ }
611
+ /** Refresh a previously obtained session. The Microsoft refresh token may be rotated. */
612
+ async refresh(refreshToken, options = {}) {
613
+ const clientId = resolveClientId(options.clientId);
614
+ const msToken = await refreshMicrosoftToken({
615
+ http: this.http,
616
+ refreshToken,
617
+ clientId,
618
+ ...options.signal !== void 0 ? { signal: options.signal } : {}
619
+ });
620
+ return this.completeMicrosoftToken(msToken, clientId, options.signal);
621
+ }
622
+ /**
623
+ * Lower-level device-code surface — exposed so callers can decouple "show prompt" from
624
+ * "block on poll" (e.g. a GUI that lets the user dismiss the modal). Most callers should
625
+ * use {@link login} instead.
626
+ */
627
+ deviceCode = {
628
+ start: (options = {}) => {
629
+ const clientId = resolveClientId(options.clientId);
630
+ return startDeviceCode({
631
+ http: this.http,
632
+ clientId,
633
+ ...options.signal !== void 0 ? { signal: options.signal } : {}
634
+ });
635
+ },
636
+ poll: async (state, options = {}) => {
637
+ const msToken = await pollDeviceCode({
638
+ http: this.http,
639
+ state,
640
+ ...options.signal !== void 0 ? { signal: options.signal } : {},
641
+ ...options.onTick !== void 0 ? { onTick: options.onTick } : {}
642
+ });
643
+ return this.completeMicrosoftToken(msToken, state.clientId, options.signal);
644
+ }
645
+ };
646
+ /** Steps 2 → 5: given a Microsoft access token, finish the flow. */
647
+ async completeMicrosoftToken(msToken, clientId, signal) {
648
+ const signalOpt = signal !== void 0 ? { signal } : {};
649
+ const xbl = await authenticateXbl({
650
+ http: this.http,
651
+ accessToken: msToken.accessToken,
652
+ ...signalOpt
653
+ });
654
+ const xsts = await authenticateXsts({
655
+ http: this.http,
656
+ xblToken: xbl.token,
657
+ ...signalOpt
658
+ });
659
+ const mc = await loginWithXbox({
660
+ http: this.http,
661
+ xstsToken: xsts.token,
662
+ userHash: xsts.userHash,
663
+ ...signalOpt
664
+ });
665
+ const profile = await fetchMinecraftProfile({
666
+ http: this.http,
667
+ accessToken: mc.accessToken,
668
+ ...signalOpt
669
+ });
670
+ return {
671
+ minecraft: {
672
+ username: profile.username,
673
+ uuid: profile.uuid,
674
+ accessToken: mc.accessToken,
675
+ expiresAt: Date.now() + mc.expiresIn * 1e3,
676
+ xuid: extractXuid(mc.accessToken)
677
+ },
678
+ microsoft: {
679
+ refreshToken: msToken.refreshToken,
680
+ clientId
681
+ }
682
+ };
683
+ }
684
+ };
685
+ var toOnlineAuth = (session) => {
686
+ return {
687
+ mode: AuthModes.ONLINE,
688
+ username: session.minecraft.username,
689
+ uuid: session.minecraft.uuid,
690
+ accessToken: session.minecraft.accessToken,
691
+ userType: "msa",
692
+ clientId: session.microsoft.clientId,
693
+ xuid: session.minecraft.xuid
694
+ };
695
+ };
696
+ var resolveClientId = (explicit) => {
697
+ if (typeof explicit === "string" && explicit.trim().length > 0) return explicit.trim();
698
+ const fromEnv = process.env[CLIENT_ID_ENV_VAR];
699
+ if (typeof fromEnv === "string" && fromEnv.trim().length > 0) return fromEnv.trim();
700
+ throw new MinecraftKitError(
701
+ MinecraftKitErrorCodes.AUTH_MISSING_CLIENT_ID,
702
+ `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.`
703
+ );
704
+ };
44
705
 
45
706
  // src/constants/platform.ts
46
707
  var NODE_PLATFORM_TO_MOJANG_OS = {
@@ -59,39 +720,8 @@ var RUNTIME_PLATFORM_KEYS = {
59
720
  linux: { x64: "linux", x86: "linux-i386", arm64: "linux" }
60
721
  };
61
722
 
62
- // src/core/errors.ts
63
- var MinecraftKitError = class extends Error {
64
- name = "MinecraftKitError";
65
- /** Stable discriminator. */
66
- code;
67
- /** Structured context; safe to serialize. */
68
- context;
69
- constructor(code, message, options = {}) {
70
- super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
71
- Object.setPrototypeOf(this, new.target.prototype);
72
- this.code = code;
73
- this.context = Object.freeze({ ...options.context ?? {} });
74
- }
75
- /** JSON-friendly representation. */
76
- toJSON() {
77
- return {
78
- name: this.name,
79
- code: this.code,
80
- message: this.message,
81
- context: this.context,
82
- cause: this.cause instanceof Error ? { name: this.cause.name, message: this.cause.message } : this.cause
83
- };
84
- }
85
- };
86
- function isMinecraftKitError(e) {
87
- return e instanceof MinecraftKitError;
88
- }
89
- function isErrorCode(e, code) {
90
- return isMinecraftKitError(e) && e.code === code;
91
- }
92
-
93
723
  // src/core/system.ts
94
- function detectSystem(input = {}) {
724
+ var detectSystem = (input = {}) => {
95
725
  const platform = input.platform ?? process.platform;
96
726
  const arch = input.arch ?? process.arch;
97
727
  const osVersion = input.osVersion ?? os.release();
@@ -99,13 +729,13 @@ function detectSystem(input = {}) {
99
729
  const mojangArch = NODE_ARCH_TO_MOJANG_ARCH[arch];
100
730
  if (mojangOs === void 0 || mojangArch === void 0) {
101
731
  throw new MinecraftKitError(
102
- "RUNTIME_UNSUPPORTED_PLATFORM",
732
+ MinecraftKitErrorCodes.RUNTIME_UNSUPPORTED_PLATFORM,
103
733
  `Unsupported platform/arch combination: ${platform}/${arch}`,
104
734
  { context: { platform, arch: String(arch) } }
105
735
  );
106
736
  }
107
737
  return { os: mojangOs, arch: mojangArch, osVersion };
108
- }
738
+ };
109
739
 
110
740
  // src/constants/defaults.ts
111
741
  var HTTP_TIMEOUT_MS = 3e4;
@@ -126,7 +756,7 @@ var MAX_PROCESSOR_STDERR_LINES = 20;
126
756
  var SPAWNER_MAX_LINE_BYTES = 64 * 1024;
127
757
 
128
758
  // src/http/cache.ts
129
- function createMemoryCache(options = {}) {
759
+ var createMemoryCache = (options = {}) => {
130
760
  const cache = new LRUCache({
131
761
  max: options.maxEntries ?? CACHE_MAX_ENTRIES,
132
762
  ttl: options.ttlMs ?? CACHE_TTL_MS
@@ -151,7 +781,7 @@ function createMemoryCache(options = {}) {
151
781
  cache.clear();
152
782
  }
153
783
  };
154
- }
784
+ };
155
785
 
156
786
  // src/http/client.ts
157
787
  var TIMEOUT_REASON = /* @__PURE__ */ Symbol("http-timeout");
@@ -169,39 +799,60 @@ var FetchHttpClient = class {
169
799
  }
170
800
  const timer = setTimeout(() => controller.abort(TIMEOUT_REASON), timeoutMs);
171
801
  let response;
802
+ const method = options.method ?? "GET";
172
803
  try {
173
- response = await fetch(url, {
174
- method: "GET",
804
+ const init = {
805
+ method,
175
806
  headers: { "user-agent": USER_AGENT, ...options.headers ?? {} },
176
807
  signal: controller.signal,
177
808
  redirect: "follow"
178
- });
809
+ };
810
+ if (method !== "GET" && options.body !== void 0) {
811
+ init.body = options.body;
812
+ }
813
+ response = await fetch(url, init);
179
814
  } catch (cause) {
180
815
  clearTimeout(timer);
181
816
  options.signal?.removeEventListener("abort", onParentAbort);
182
817
  if (controller.signal.reason === TIMEOUT_REASON) {
183
- throw new MinecraftKitError("NETWORK_TIMEOUT", `Request timed out: ${url}`, {
184
- cause,
185
- context: { url, timeoutMs }
186
- });
818
+ throw new MinecraftKitError(
819
+ MinecraftKitErrorCodes.NETWORK_TIMEOUT,
820
+ `Request timed out: ${url}`,
821
+ {
822
+ cause,
823
+ context: { url, timeoutMs }
824
+ }
825
+ );
187
826
  }
188
827
  if (options.signal?.aborted) {
189
- throw new MinecraftKitError("NETWORK_ABORTED", `Request aborted: ${url}`, {
828
+ throw new MinecraftKitError(
829
+ MinecraftKitErrorCodes.NETWORK_ABORTED,
830
+ `Request aborted: ${url}`,
831
+ {
832
+ cause,
833
+ context: { url }
834
+ }
835
+ );
836
+ }
837
+ throw new MinecraftKitError(
838
+ MinecraftKitErrorCodes.NETWORK_HTTP_ERROR,
839
+ `Network request failed: ${url}`,
840
+ {
190
841
  cause,
191
842
  context: { url }
192
- });
193
- }
194
- throw new MinecraftKitError("NETWORK_HTTP_ERROR", `Network request failed: ${url}`, {
195
- cause,
196
- context: { url }
197
- });
843
+ }
844
+ );
198
845
  }
199
846
  clearTimeout(timer);
200
847
  options.signal?.removeEventListener("abort", onParentAbort);
201
- if (!response.ok) {
202
- throw new MinecraftKitError("NETWORK_HTTP_ERROR", `HTTP ${response.status} for ${url}`, {
203
- context: { url, httpStatus: response.status }
204
- });
848
+ if (!response.ok && options.acceptNonOk !== true) {
849
+ throw new MinecraftKitError(
850
+ MinecraftKitErrorCodes.NETWORK_HTTP_ERROR,
851
+ `HTTP ${response.status} for ${url}`,
852
+ {
853
+ context: { url, httpStatus: response.status }
854
+ }
855
+ );
205
856
  }
206
857
  return new FetchHttpResponse(response, url);
207
858
  }
@@ -325,6 +976,17 @@ var InstallActionKinds = {
325
976
  WRITE_VERSION_JSON: "write-version-json",
326
977
  WRITE_LOGGING_CONFIG: "write-logging-config"
327
978
  };
979
+ var DownloadCategories = {
980
+ CLIENT_JAR: "client-jar",
981
+ LIBRARY: "library",
982
+ ASSET_INDEX: "asset-index",
983
+ ASSET: "asset",
984
+ LOGGING_CONFIG: "logging-config",
985
+ FABRIC_LIBRARY: "fabric-library",
986
+ FORGE_LIBRARY: "forge-library",
987
+ RUNTIME_FILE: "runtime-file",
988
+ FORGE_INSTALLER: "forge-installer"
989
+ };
328
990
 
329
991
  // src/types/loader.ts
330
992
  var Loaders = {
@@ -378,7 +1040,7 @@ var ApiEndpoints = {
378
1040
  };
379
1041
 
380
1042
  // src/http/metadata.ts
381
- async function fetchJson(http, cache, input) {
1043
+ var fetchJson = async (http, cache, input) => {
382
1044
  const key = input.cacheKey ?? `json:${input.url}`;
383
1045
  const cached = cache.get(key);
384
1046
  if (cached !== void 0) {
@@ -390,8 +1052,8 @@ async function fetchJson(http, cache, input) {
390
1052
  const value = await response.json();
391
1053
  cache.set(key, value, input.ttlMs ?? CACHE_TTL_MS);
392
1054
  return value;
393
- }
394
- async function fetchText(http, cache, input) {
1055
+ };
1056
+ var fetchText = async (http, cache, input) => {
395
1057
  const key = input.cacheKey ?? `text:${input.url}`;
396
1058
  const cached = cache.get(key);
397
1059
  if (cached !== void 0) {
@@ -403,10 +1065,10 @@ async function fetchText(http, cache, input) {
403
1065
  const text = await response.text();
404
1066
  cache.set(key, text, input.ttlMs ?? CACHE_TTL_MS);
405
1067
  return text;
406
- }
1068
+ };
407
1069
 
408
1070
  // src/install/assets.ts
409
- async function planAssetDownloads(input) {
1071
+ var planAssetDownloads = async (input) => {
410
1072
  const indexUrl = input.assetIndex.url;
411
1073
  const indexPath = targetPaths.assetIndex(input.directory, input.assetIndex.id);
412
1074
  const indexDocument = await fetchJson(input.http, input.cache, {
@@ -421,7 +1083,7 @@ async function planAssetDownloads(input) {
421
1083
  target: indexPath,
422
1084
  expectedSha1: input.assetIndex.sha1,
423
1085
  expectedSize: input.assetIndex.size,
424
- category: "asset-index"
1086
+ category: DownloadCategories.ASSET_INDEX
425
1087
  }
426
1088
  ];
427
1089
  const seen = /* @__PURE__ */ new Set();
@@ -434,11 +1096,11 @@ async function planAssetDownloads(input) {
434
1096
  target: targetPaths.assetObject(input.directory, entry.hash),
435
1097
  expectedSha1: entry.hash,
436
1098
  expectedSize: entry.size,
437
- category: "asset"
1099
+ category: DownloadCategories.ASSET
438
1100
  });
439
1101
  }
440
1102
  return { actions, indexDocument };
441
- }
1103
+ };
442
1104
 
443
1105
  // src/constants/maven.ts
444
1106
  var DEFAULT_LIBRARY_REPOSITORY = "https://libraries.minecraft.net/";
@@ -446,21 +1108,25 @@ var FABRIC_MAVEN_BASE = "https://maven.fabricmc.net/";
446
1108
  var FORGE_MAVEN_BASE = "https://maven.minecraftforge.net/";
447
1109
 
448
1110
  // src/core/maven.ts
449
- function parseMavenCoordinate(input) {
1111
+ var parseMavenCoordinate = (input) => {
450
1112
  const trimmed = input.startsWith("[") && input.endsWith("]") ? input.slice(1, -1) : input;
451
1113
  const atIndex = trimmed.indexOf("@");
452
1114
  const extension = atIndex === -1 ? "jar" : trimmed.slice(atIndex + 1);
453
1115
  const body = atIndex === -1 ? trimmed : trimmed.slice(0, atIndex);
454
1116
  const parts = body.split(":");
455
1117
  if (parts.length < 3 || parts.length > 4) {
456
- throw new MinecraftKitError("INVALID_INPUT", `Invalid Maven coordinate: ${input}`, {
457
- context: { input }
458
- });
1118
+ throw new MinecraftKitError(
1119
+ MinecraftKitErrorCodes.INVALID_INPUT,
1120
+ `Invalid Maven coordinate: ${input}`,
1121
+ {
1122
+ context: { input }
1123
+ }
1124
+ );
459
1125
  }
460
1126
  const [group, artifact, version, classifier] = parts;
461
1127
  if (!group || !artifact || !version) {
462
1128
  throw new MinecraftKitError(
463
- "INVALID_INPUT",
1129
+ MinecraftKitErrorCodes.INVALID_INPUT,
464
1130
  `Invalid Maven coordinate (missing component): ${input}`,
465
1131
  { context: { input } }
466
1132
  );
@@ -469,19 +1135,19 @@ function parseMavenCoordinate(input) {
469
1135
  return { group, artifact, version, extension };
470
1136
  }
471
1137
  return { group, artifact, version, classifier, extension };
472
- }
473
- function mavenRelativePath(coord) {
1138
+ };
1139
+ var mavenRelativePath = (coord) => {
474
1140
  const groupPath = coord.group.replaceAll(".", "/");
475
1141
  const classifierSegment = coord.classifier === void 0 ? "" : `-${coord.classifier}`;
476
1142
  const filename = `${coord.artifact}-${coord.version}${classifierSegment}.${coord.extension}`;
477
1143
  return `${groupPath}/${coord.artifact}/${coord.version}/${filename}`;
478
- }
479
- function mavenRelativePathFor(input) {
1144
+ };
1145
+ var mavenRelativePathFor = (input) => {
480
1146
  return mavenRelativePath(parseMavenCoordinate(input));
481
- }
1147
+ };
482
1148
 
483
1149
  // src/core/rules.ts
484
- function evaluateRules(rules, context) {
1150
+ var evaluateRules = (rules, context) => {
485
1151
  if (!rules || rules.length === 0) {
486
1152
  return true;
487
1153
  }
@@ -492,8 +1158,8 @@ function evaluateRules(rules, context) {
492
1158
  }
493
1159
  }
494
1160
  return allowed;
495
- }
496
- function matchesRule(rule, context) {
1161
+ };
1162
+ var matchesRule = (rule, context) => {
497
1163
  if (rule.os !== void 0) {
498
1164
  if (rule.os.name !== void 0 && rule.os.name !== context.system.os) {
499
1165
  return false;
@@ -521,21 +1187,21 @@ function matchesRule(rule, context) {
521
1187
  }
522
1188
  }
523
1189
  return true;
524
- }
525
- function normalizeArch(arch) {
1190
+ };
1191
+ var normalizeArch = (arch) => {
526
1192
  return arch === "ia32" ? "x86" : arch;
527
- }
528
- function resolveArchPlaceholder(template, archDigit2) {
1193
+ };
1194
+ var resolveArchPlaceholder = (template, archDigit2) => {
529
1195
  return template.replaceAll("${arch}", archDigit2);
530
- }
531
- function archDigit(arch) {
1196
+ };
1197
+ var archDigit = (arch) => {
532
1198
  if (arch === "x86") return "32";
533
1199
  if (arch === "x64") return "64";
534
1200
  return "64";
535
- }
1201
+ };
536
1202
 
537
1203
  // src/install/libraries.ts
538
- function planLibraryDownloads(input) {
1204
+ var planLibraryDownloads = (input) => {
539
1205
  const downloads = [];
540
1206
  const nativeExtractions = [];
541
1207
  const classpathFiles = [];
@@ -589,8 +1255,8 @@ function planLibraryDownloads(input) {
589
1255
  }
590
1256
  }
591
1257
  return { downloads, nativeExtractions, classpathFiles };
592
- }
593
- function pickPrimaryArtifact(library) {
1258
+ };
1259
+ var pickPrimaryArtifact = (library) => {
594
1260
  if (library.downloads?.artifact) {
595
1261
  return artifactFromDownload(library.downloads.artifact);
596
1262
  }
@@ -601,8 +1267,8 @@ function pickPrimaryArtifact(library) {
601
1267
  return mavenArtifactFromCoord(library.name, DEFAULT_LIBRARY_REPOSITORY);
602
1268
  }
603
1269
  return null;
604
- }
605
- function pickNative(library, system) {
1270
+ };
1271
+ var pickNative = (library, system) => {
606
1272
  if (!library.natives) return null;
607
1273
  const classifierTemplate = library.natives[system.os];
608
1274
  if (!classifierTemplate) return null;
@@ -617,22 +1283,26 @@ function pickNative(library, system) {
617
1283
  return mavenArtifactFromCoord(withClassifier, library.url ?? DEFAULT_LIBRARY_REPOSITORY);
618
1284
  }
619
1285
  return null;
620
- }
621
- function artifactFromDownload(artifact) {
1286
+ };
1287
+ var artifactFromDownload = (artifact) => {
622
1288
  return {
623
1289
  relativePath: artifact.path,
624
1290
  url: artifact.url,
625
1291
  sha1: artifact.sha1,
626
1292
  size: artifact.size
627
1293
  };
628
- }
629
- function mavenArtifactFromCoord(coord, baseUrl) {
1294
+ };
1295
+ var mavenArtifactFromCoord = (coord, baseUrl) => {
630
1296
  const relativePath = mavenRelativePathFor(coord);
631
1297
  const normalizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
632
1298
  if (!relativePath) {
633
- throw new MinecraftKitError("MANIFEST_INVALID", `Invalid library coordinate: ${coord}`, {
634
- context: { input: coord }
635
- });
1299
+ throw new MinecraftKitError(
1300
+ MinecraftKitErrorCodes.MANIFEST_INVALID,
1301
+ `Invalid library coordinate: ${coord}`,
1302
+ {
1303
+ context: { input: coord }
1304
+ }
1305
+ );
636
1306
  }
637
1307
  return {
638
1308
  relativePath,
@@ -640,10 +1310,10 @@ function mavenArtifactFromCoord(coord, baseUrl) {
640
1310
  sha1: void 0,
641
1311
  size: void 0
642
1312
  };
643
- }
1313
+ };
644
1314
 
645
1315
  // src/install/fabric-install.ts
646
- function planFabricInstall(input) {
1316
+ var planFabricInstall = (input) => {
647
1317
  const versionId = input.loader.profile.id;
648
1318
  const versionJsonPath = targetPaths.versionJson(input.directory, versionId);
649
1319
  const versionJson = {
@@ -657,7 +1327,7 @@ function planFabricInstall(input) {
657
1327
  directory: input.directory,
658
1328
  system: input.system,
659
1329
  versionId: input.minecraft.version,
660
- category: "fabric-library"
1330
+ category: DownloadCategories.FABRIC_LIBRARY
661
1331
  });
662
1332
  return {
663
1333
  versionJson,
@@ -665,7 +1335,7 @@ function planFabricInstall(input) {
665
1335
  classpathFiles: plan.classpathFiles,
666
1336
  versionId
667
1337
  };
668
- }
1338
+ };
669
1339
 
670
1340
  // src/constants/limits.ts
671
1341
  var EXTRACTION_MAX_FILE_SIZE = 256 * 1024 * 1024;
@@ -673,42 +1343,42 @@ var EXTRACTION_MAX_TOTAL_SIZE = 2 * 1024 * 1024 * 1024;
673
1343
  var EXTRACTION_MAX_COMPRESSION_RATIO = 200;
674
1344
  var EXTRACTION_MAX_ENTRY_COUNT = 1e5;
675
1345
  var FORGE_INSTALLER_MAX_SIZE = 256 * 1024 * 1024;
676
- async function ensureDir(directory) {
1346
+ var ensureDir = async (directory) => {
677
1347
  try {
678
1348
  await fs.mkdir(directory, { recursive: true });
679
1349
  } catch (cause) {
680
1350
  throw new MinecraftKitError(
681
- "FILESYSTEM_WRITE_ERROR",
1351
+ MinecraftKitErrorCodes.FILESYSTEM_WRITE_ERROR,
682
1352
  `Failed to create directory: ${directory}`,
683
1353
  { cause, context: { filePath: directory } }
684
1354
  );
685
1355
  }
686
- }
687
- async function fileExists(filePath) {
1356
+ };
1357
+ var fileExists = async (filePath) => {
688
1358
  try {
689
1359
  const stat = await fs.stat(filePath);
690
1360
  return stat.isFile();
691
1361
  } catch {
692
1362
  return false;
693
1363
  }
694
- }
695
- async function dirExists(filePath) {
1364
+ };
1365
+ var dirExists = async (filePath) => {
696
1366
  try {
697
1367
  const stat = await fs.stat(filePath);
698
1368
  return stat.isDirectory();
699
1369
  } catch {
700
1370
  return false;
701
1371
  }
702
- }
703
- async function fileSize(filePath) {
1372
+ };
1373
+ var fileSize = async (filePath) => {
704
1374
  try {
705
1375
  const stat = await fs.stat(filePath);
706
1376
  return stat.size;
707
1377
  } catch {
708
1378
  return -1;
709
1379
  }
710
- }
711
- async function atomicWrite(target, data) {
1380
+ };
1381
+ var atomicWrite = async (target, data) => {
712
1382
  await ensureDir(path.dirname(target));
713
1383
  const tmp = `${target}.${crypto2.randomBytes(4).toString("hex")}.tmp`;
714
1384
  try {
@@ -723,31 +1393,39 @@ async function atomicWrite(target, data) {
723
1393
  await fs.unlink(tmp);
724
1394
  } catch {
725
1395
  }
726
- throw new MinecraftKitError("FILESYSTEM_WRITE_ERROR", `Failed to write file: ${target}`, {
727
- cause,
728
- context: { filePath: target }
729
- });
1396
+ throw new MinecraftKitError(
1397
+ MinecraftKitErrorCodes.FILESYSTEM_WRITE_ERROR,
1398
+ `Failed to write file: ${target}`,
1399
+ {
1400
+ cause,
1401
+ context: { filePath: target }
1402
+ }
1403
+ );
730
1404
  }
731
- }
732
- async function readText(filePath) {
1405
+ };
1406
+ var readText = async (filePath) => {
733
1407
  try {
734
1408
  return await fs.readFile(filePath, "utf8");
735
1409
  } catch (cause) {
736
- throw new MinecraftKitError("FILESYSTEM_READ_ERROR", `Failed to read file: ${filePath}`, {
737
- cause,
738
- context: { filePath }
739
- });
1410
+ throw new MinecraftKitError(
1411
+ MinecraftKitErrorCodes.FILESYSTEM_READ_ERROR,
1412
+ `Failed to read file: ${filePath}`,
1413
+ {
1414
+ cause,
1415
+ context: { filePath }
1416
+ }
1417
+ );
740
1418
  }
741
- }
742
- async function listChildDirectories(directory) {
1419
+ };
1420
+ var listChildDirectories = async (directory) => {
743
1421
  try {
744
1422
  const entries = await fs.readdir(directory, { withFileTypes: true });
745
1423
  return entries.filter((e) => e.isDirectory()).map((e) => e.name);
746
1424
  } catch {
747
1425
  return [];
748
1426
  }
749
- }
750
- async function chmodExecutable(filePath) {
1427
+ };
1428
+ var chmodExecutable = async (filePath) => {
751
1429
  if (process.platform === "win32") {
752
1430
  return;
753
1431
  }
@@ -755,35 +1433,43 @@ async function chmodExecutable(filePath) {
755
1433
  await fs.chmod(filePath, 493);
756
1434
  } catch {
757
1435
  }
758
- }
759
- function assertWithinRoot(root, child) {
1436
+ };
1437
+ var assertWithinRoot = (root, child) => {
760
1438
  const normalizedRoot = path.resolve(root);
761
1439
  const normalizedChild = path.resolve(root, child);
762
1440
  const sep = path.sep;
763
1441
  if (normalizedChild !== normalizedRoot && !normalizedChild.startsWith(normalizedRoot + sep)) {
764
- throw new MinecraftKitError("FILESYSTEM_PATH_TRAVERSAL", `Path escapes root: ${child}`, {
765
- context: { filePath: child, rootDirectory: root }
766
- });
1442
+ throw new MinecraftKitError(
1443
+ MinecraftKitErrorCodes.FILESYSTEM_PATH_TRAVERSAL,
1444
+ `Path escapes root: ${child}`,
1445
+ {
1446
+ context: { filePath: child, rootDirectory: root }
1447
+ }
1448
+ );
767
1449
  }
768
- }
1450
+ };
769
1451
 
770
1452
  // src/core/archive.ts
771
- function openZip(filePath) {
1453
+ var openZip = (filePath) => {
772
1454
  return new Promise((resolve, reject) => {
773
1455
  yauzl.open(filePath, { lazyEntries: true, autoClose: false }, (err, zipFile) => {
774
1456
  if (err || !zipFile) {
775
1457
  reject(
776
- new MinecraftKitError("ARCHIVE_INVALID", `Failed to open archive: ${filePath}`, {
777
- cause: err,
778
- context: { filePath }
779
- })
1458
+ new MinecraftKitError(
1459
+ MinecraftKitErrorCodes.ARCHIVE_INVALID,
1460
+ `Failed to open archive: ${filePath}`,
1461
+ {
1462
+ cause: err,
1463
+ context: { filePath }
1464
+ }
1465
+ )
780
1466
  );
781
1467
  return;
782
1468
  }
783
1469
  resolve(new ZipReader(zipFile, filePath));
784
1470
  });
785
1471
  });
786
- }
1472
+ };
787
1473
  var ZipReader = class {
788
1474
  constructor(file, filePath) {
789
1475
  this.file = file;
@@ -801,7 +1487,7 @@ var ZipReader = class {
801
1487
  count++;
802
1488
  if (count > EXTRACTION_MAX_ENTRY_COUNT) {
803
1489
  throw new MinecraftKitError(
804
- "ARCHIVE_TOO_LARGE",
1490
+ MinecraftKitErrorCodes.ARCHIVE_TOO_LARGE,
805
1491
  `Archive contains too many entries: ${this.filePath}`,
806
1492
  { context: { filePath: this.filePath } }
807
1493
  );
@@ -833,10 +1519,14 @@ var ZipReader = class {
833
1519
  const onError = (err) => {
834
1520
  cleanup();
835
1521
  reject(
836
- new MinecraftKitError("ARCHIVE_INVALID", "Failed to read archive entry", {
837
- cause: err,
838
- context: { filePath: this.filePath }
839
- })
1522
+ new MinecraftKitError(
1523
+ MinecraftKitErrorCodes.ARCHIVE_INVALID,
1524
+ "Failed to read archive entry",
1525
+ {
1526
+ cause: err,
1527
+ context: { filePath: this.filePath }
1528
+ }
1529
+ )
840
1530
  );
841
1531
  };
842
1532
  const cleanup = () => {
@@ -861,14 +1551,14 @@ var ZipReader = class {
861
1551
  readBuffer: async () => {
862
1552
  if (entry.uncompressedSize > EXTRACTION_MAX_FILE_SIZE) {
863
1553
  throw new MinecraftKitError(
864
- "ARCHIVE_TOO_LARGE",
1554
+ MinecraftKitErrorCodes.ARCHIVE_TOO_LARGE,
865
1555
  `Archive entry exceeds size cap: ${name}`,
866
1556
  { context: { filePath: this.filePath, entryName: name, size: entry.uncompressedSize } }
867
1557
  );
868
1558
  }
869
1559
  if (entry.compressedSize > 0 && entry.uncompressedSize / entry.compressedSize > EXTRACTION_MAX_COMPRESSION_RATIO) {
870
1560
  throw new MinecraftKitError(
871
- "ARCHIVE_TOO_LARGE",
1561
+ MinecraftKitErrorCodes.ARCHIVE_TOO_LARGE,
872
1562
  `Archive entry exceeds compression-ratio cap: ${name}`,
873
1563
  { context: { filePath: this.filePath, entryName: name } }
874
1564
  );
@@ -884,13 +1574,13 @@ var ZipReader = class {
884
1574
  };
885
1575
  }
886
1576
  };
887
- function openStream(file, entry, archivePath) {
1577
+ var openStream = (file, entry, archivePath) => {
888
1578
  return new Promise((resolve, reject) => {
889
1579
  file.openReadStream(entry, (err, stream) => {
890
1580
  if (err || !stream) {
891
1581
  reject(
892
1582
  new MinecraftKitError(
893
- "ARCHIVE_INVALID",
1583
+ MinecraftKitErrorCodes.ARCHIVE_INVALID,
894
1584
  `Failed to open archive entry: ${entry.fileName}`,
895
1585
  { cause: err, context: { filePath: archivePath, entryName: entry.fileName } }
896
1586
  )
@@ -900,8 +1590,8 @@ function openStream(file, entry, archivePath) {
900
1590
  resolve(stream);
901
1591
  });
902
1592
  });
903
- }
904
- async function extractAllToDir(zipPath, targetDir, options = {}) {
1593
+ };
1594
+ var extractAllToDir = async (zipPath, targetDir, options = {}) => {
905
1595
  const exclude = options.excludePrefixes ?? ["META-INF/"];
906
1596
  let fileCount = 0;
907
1597
  let totalSize = 0;
@@ -917,14 +1607,14 @@ async function extractAllToDir(zipPath, targetDir, options = {}) {
917
1607
  totalSize += entry.uncompressedSize;
918
1608
  if (totalSize > EXTRACTION_MAX_TOTAL_SIZE) {
919
1609
  throw new MinecraftKitError(
920
- "ARCHIVE_TOO_LARGE",
1610
+ MinecraftKitErrorCodes.ARCHIVE_TOO_LARGE,
921
1611
  `Archive total size cap exceeded: ${zipPath}`,
922
1612
  { context: { filePath: zipPath } }
923
1613
  );
924
1614
  }
925
1615
  if (entry.uncompressedSize > EXTRACTION_MAX_FILE_SIZE) {
926
1616
  throw new MinecraftKitError(
927
- "ARCHIVE_TOO_LARGE",
1617
+ MinecraftKitErrorCodes.ARCHIVE_TOO_LARGE,
928
1618
  `Archive entry exceeds size cap: ${entry.name}`,
929
1619
  { context: { filePath: zipPath, entryName: entry.name } }
930
1620
  );
@@ -941,9 +1631,9 @@ async function extractAllToDir(zipPath, targetDir, options = {}) {
941
1631
  reader.close();
942
1632
  }
943
1633
  return { fileCount };
944
- }
1634
+ };
945
1635
  var RESERVED_NAME = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\..*)?$/i;
946
- function assertSafeEntryName(name) {
1636
+ var assertSafeEntryName = (name) => {
947
1637
  if (!name) {
948
1638
  throw rejectEntry(name, "empty entry name");
949
1639
  }
@@ -965,15 +1655,15 @@ function assertSafeEntryName(name) {
965
1655
  throw rejectEntry(name, "trailing dot or whitespace");
966
1656
  }
967
1657
  }
968
- }
969
- function rejectEntry(name, reason) {
1658
+ };
1659
+ var rejectEntry = (name, reason) => {
970
1660
  return new MinecraftKitError(
971
- "ARCHIVE_ENTRY_REJECTED",
1661
+ MinecraftKitErrorCodes.ARCHIVE_ENTRY_REJECTED,
972
1662
  `Archive entry rejected (${reason}): ${name}`,
973
1663
  { context: { entryName: name, reason } }
974
1664
  );
975
- }
976
- async function readEntryBuffer(zipPath, entryName) {
1665
+ };
1666
+ var readEntryBuffer = async (zipPath, entryName) => {
977
1667
  const reader = await openZip(zipPath);
978
1668
  try {
979
1669
  const entry = await reader.findEntry(entryName);
@@ -982,19 +1672,23 @@ async function readEntryBuffer(zipPath, entryName) {
982
1672
  } finally {
983
1673
  reader.close();
984
1674
  }
985
- }
986
- async function extractSingleEntry(zipPath, entryName, destination) {
1675
+ };
1676
+ var extractSingleEntry = async (zipPath, entryName, destination) => {
987
1677
  const buffer = await readEntryBuffer(zipPath, entryName);
988
1678
  if (!buffer) {
989
- throw new MinecraftKitError("ARCHIVE_INVALID", `Archive entry not found: ${entryName}`, {
990
- context: { filePath: zipPath, entryName }
991
- });
1679
+ throw new MinecraftKitError(
1680
+ MinecraftKitErrorCodes.ARCHIVE_INVALID,
1681
+ `Archive entry not found: ${entryName}`,
1682
+ {
1683
+ context: { filePath: zipPath, entryName }
1684
+ }
1685
+ );
992
1686
  }
993
1687
  await atomicWrite(destination, buffer);
994
- }
1688
+ };
995
1689
  var MANIFEST_LINE_CONTINUATION = /\r?\n[ \t]/g;
996
1690
  var MANIFEST_MAIN_CLASS = /^Main-Class:\s*(.+)$/i;
997
- async function readJarMainClass(zipPath) {
1691
+ var readJarMainClass = async (zipPath) => {
998
1692
  const buf = await readEntryBuffer(zipPath, "META-INF/MANIFEST.MF");
999
1693
  if (!buf) return void 0;
1000
1694
  const text = buf.toString("utf8").replaceAll(MANIFEST_LINE_CONTINUATION, "");
@@ -1003,10 +1697,10 @@ async function readJarMainClass(zipPath) {
1003
1697
  if (match?.[1]) return match[1].trim();
1004
1698
  }
1005
1699
  return void 0;
1006
- }
1700
+ };
1007
1701
 
1008
1702
  // src/core/collections.ts
1009
- function dedupeBy(values, key) {
1703
+ var dedupeBy = (values, key) => {
1010
1704
  const seen = /* @__PURE__ */ new Set();
1011
1705
  const result = [];
1012
1706
  for (const value of values) {
@@ -1016,13 +1710,25 @@ function dedupeBy(values, key) {
1016
1710
  result.push(value);
1017
1711
  }
1018
1712
  return result;
1019
- }
1020
- function dedupe(values) {
1713
+ };
1714
+ var dedupe = (values) => {
1021
1715
  return dedupeBy(values, (v) => v);
1022
- }
1716
+ };
1717
+
1718
+ // src/core/abort.ts
1719
+ var assertNotAborted = (signal, message) => {
1720
+ if (signal?.aborted) {
1721
+ throw new MinecraftKitError(MinecraftKitErrorCodes.LAUNCH_ABORTED, message);
1722
+ }
1723
+ };
1724
+ var checkpoint = async (sources, message = "Operation aborted by signal") => {
1725
+ assertNotAborted(sources.signal, message);
1726
+ await sources.pauseController?.waitWhilePaused();
1727
+ assertNotAborted(sources.signal, message);
1728
+ };
1023
1729
 
1024
1730
  // src/core/retry.ts
1025
- function abortableSleep(ms, signal) {
1731
+ var abortableSleep = (ms, signal) => {
1026
1732
  return new Promise((resolve, reject) => {
1027
1733
  if (signal?.aborted) {
1028
1734
  reject(toAbortError(signal));
@@ -1038,13 +1744,13 @@ function abortableSleep(ms, signal) {
1038
1744
  };
1039
1745
  signal?.addEventListener("abort", onAbort, { once: true });
1040
1746
  });
1041
- }
1042
- function toAbortError(signal) {
1043
- return new MinecraftKitError("NETWORK_ABORTED", "Operation aborted", {
1747
+ };
1748
+ var toAbortError = (signal) => {
1749
+ return new MinecraftKitError(MinecraftKitErrorCodes.NETWORK_ABORTED, "Operation aborted", {
1044
1750
  context: { reason: signal?.reason }
1045
1751
  });
1046
- }
1047
- async function withRetry(op, isRetryable, options = {}) {
1752
+ };
1753
+ var withRetry = async (op, isRetryable, options = {}) => {
1048
1754
  const max = options.maxAttempts ?? HTTP_RETRY_MAX;
1049
1755
  const base = options.baseMs ?? HTTP_RETRY_BACKOFF_BASE_MS;
1050
1756
  const cap = options.capMs ?? HTTP_RETRY_BACKOFF_CAP_MS;
@@ -1069,8 +1775,8 @@ async function withRetry(op, isRetryable, options = {}) {
1069
1775
  }
1070
1776
  }
1071
1777
  throw lastError ?? new Error("withRetry exhausted attempts");
1072
- }
1073
- function isHttpRetryable(error) {
1778
+ };
1779
+ var isHttpRetryable = (error) => {
1074
1780
  if (!isMinecraftKitError(error)) {
1075
1781
  return false;
1076
1782
  }
@@ -1083,15 +1789,45 @@ function isHttpRetryable(error) {
1083
1789
  return status === 0;
1084
1790
  }
1085
1791
  return false;
1086
- }
1792
+ };
1793
+
1794
+ // src/types/events.ts
1795
+ var EventTypes = {
1796
+ INSTALL_PHASE_CHANGED: "install:phase-changed",
1797
+ DOWNLOAD_STARTED: "download:started",
1798
+ DOWNLOAD_PROGRESS: "download:progress",
1799
+ DOWNLOAD_SKIPPED: "download:skipped",
1800
+ DOWNLOAD_COMPLETED: "download:completed",
1801
+ DOWNLOAD_FAILED: "download:failed",
1802
+ INTEGRITY_VERIFIED: "integrity:verified",
1803
+ INTEGRITY_MISMATCH: "integrity:mismatch",
1804
+ ARCHIVE_EXTRACTED: "archive:extracted",
1805
+ FORGE_PROCESSOR_STARTED: "forge:processor-started",
1806
+ FORGE_PROCESSOR_COMPLETED: "forge:processor-completed",
1807
+ FORGE_PROCESSOR_OUTPUT_VERIFIED: "forge:processor-output-verified",
1808
+ VERIFY_FILE_CHECKED: "verify:file-checked",
1809
+ VERIFY_COMPLETED: "verify:completed",
1810
+ REPAIR_PHASE_CHANGED: "repair:phase-changed",
1811
+ LAUNCH_STARTING: "launch:starting",
1812
+ LAUNCH_STARTED: "launch:started",
1813
+ LAUNCH_STDOUT: "launch:stdout",
1814
+ LAUNCH_STDERR: "launch:stderr",
1815
+ LAUNCH_EXITED: "launch:exited",
1816
+ LAUNCH_ABORTED: "launch:aborted"
1817
+ };
1087
1818
 
1088
1819
  // src/http/download.ts
1089
- async function downloadFile(http, input) {
1090
- const fileRef = { url: input.url, target: input.target, category: input.category };
1820
+ var downloadFile = async (http, input) => {
1821
+ assertSafeDownloadUrl(input.url, input.hostAllowList);
1822
+ const fileRef = {
1823
+ url: input.url,
1824
+ target: input.target,
1825
+ ...input.category !== void 0 ? { category: input.category } : {}
1826
+ };
1091
1827
  if (input.expectedSha1 !== void 0) {
1092
1828
  const existing = await checkExistingFile(input.target, input.expectedSha1, input.expectedSize);
1093
1829
  if (existing.matches) {
1094
- input.onEvent?.({ type: "download:skipped", file: fileRef });
1830
+ input.onEvent?.({ type: EventTypes.DOWNLOAD_SKIPPED, file: fileRef });
1095
1831
  return { bytesDownloaded: 0, sha1: existing.sha1, skipped: true };
1096
1832
  }
1097
1833
  }
@@ -1100,29 +1836,32 @@ async function downloadFile(http, input) {
1100
1836
  return withRetry(
1101
1837
  async () => {
1102
1838
  input.onEvent?.({
1103
- type: "download:started",
1839
+ type: EventTypes.DOWNLOAD_STARTED,
1104
1840
  file: fileRef,
1105
1841
  expectedSize: input.expectedSize ?? 0
1106
1842
  });
1107
1843
  const startedAt = Date.now();
1108
1844
  let bytesDownloaded = 0;
1109
1845
  const hash = crypto2.createHash("sha1");
1110
- const response = await http.request(input.url, { signal: input.signal });
1846
+ const response = await http.request(input.url, {
1847
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
1848
+ });
1111
1849
  const contentLength = Number(response.headers["content-length"] ?? "0");
1112
1850
  const total = input.expectedSize ?? (Number.isFinite(contentLength) ? contentLength : 0);
1113
1851
  const sourceIterable = response.stream();
1114
1852
  const counting = (async function* () {
1115
1853
  for await (const chunk of sourceIterable) {
1116
- if (input.pauseController?.paused) {
1117
- await input.pauseController.waitWhilePaused();
1118
- }
1119
- if (input.signal?.aborted) {
1120
- throw new MinecraftKitError("LAUNCH_ABORTED", "Download aborted by signal");
1121
- }
1854
+ await checkpoint(
1855
+ {
1856
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
1857
+ ...input.pauseController !== void 0 ? { pauseController: input.pauseController } : {}
1858
+ },
1859
+ "Download aborted by signal"
1860
+ );
1122
1861
  bytesDownloaded += chunk.byteLength;
1123
1862
  hash.update(chunk);
1124
1863
  input.onEvent?.({
1125
- type: "download:progress",
1864
+ type: EventTypes.DOWNLOAD_PROGRESS,
1126
1865
  file: fileRef,
1127
1866
  bytesDownloaded,
1128
1867
  totalBytes: total
@@ -1135,7 +1874,7 @@ async function downloadFile(http, input) {
1135
1874
  } catch (cause) {
1136
1875
  await safeUnlink(tmp);
1137
1876
  throw new MinecraftKitError(
1138
- "FILESYSTEM_WRITE_ERROR",
1877
+ MinecraftKitErrorCodes.FILESYSTEM_WRITE_ERROR,
1139
1878
  `Failed to write download: ${input.target}`,
1140
1879
  { cause, context: { filePath: input.target, url: input.url } }
1141
1880
  );
@@ -1143,50 +1882,58 @@ async function downloadFile(http, input) {
1143
1882
  const computedSha1 = hash.digest("hex");
1144
1883
  if (input.expectedSize !== void 0 && bytesDownloaded !== input.expectedSize) {
1145
1884
  await safeUnlink(tmp);
1146
- throw new MinecraftKitError("INTEGRITY_SIZE_MISMATCH", `Size mismatch for ${input.url}`, {
1147
- context: {
1148
- url: input.url,
1149
- expectedSize: input.expectedSize,
1150
- actualSize: bytesDownloaded
1885
+ throw new MinecraftKitError(
1886
+ MinecraftKitErrorCodes.INTEGRITY_SIZE_MISMATCH,
1887
+ `Size mismatch for ${input.url}`,
1888
+ {
1889
+ context: {
1890
+ url: input.url,
1891
+ expectedSize: input.expectedSize,
1892
+ actualSize: bytesDownloaded
1893
+ }
1151
1894
  }
1152
- });
1895
+ );
1153
1896
  }
1154
1897
  if (input.expectedSha1 !== void 0 && computedSha1 !== input.expectedSha1) {
1155
1898
  await safeUnlink(tmp);
1156
1899
  input.onEvent?.({
1157
- type: "integrity:mismatch",
1900
+ type: EventTypes.INTEGRITY_MISMATCH,
1158
1901
  file: fileRef,
1159
1902
  algorithm: "sha1",
1160
1903
  expected: input.expectedSha1,
1161
1904
  actual: computedSha1
1162
1905
  });
1163
- throw new MinecraftKitError("INTEGRITY_HASH_MISMATCH", `SHA-1 mismatch for ${input.url}`, {
1164
- context: {
1165
- url: input.url,
1166
- expectedHash: input.expectedSha1,
1167
- actualHash: computedSha1
1906
+ throw new MinecraftKitError(
1907
+ MinecraftKitErrorCodes.INTEGRITY_HASH_MISMATCH,
1908
+ `SHA-1 mismatch for ${input.url}`,
1909
+ {
1910
+ context: {
1911
+ url: input.url,
1912
+ expectedHash: input.expectedSha1,
1913
+ actualHash: computedSha1
1914
+ }
1168
1915
  }
1169
- });
1916
+ );
1170
1917
  }
1171
1918
  try {
1172
1919
  await fs.rename(tmp, input.target);
1173
1920
  } catch (cause) {
1174
1921
  await safeUnlink(tmp);
1175
1922
  throw new MinecraftKitError(
1176
- "FILESYSTEM_WRITE_ERROR",
1923
+ MinecraftKitErrorCodes.FILESYSTEM_WRITE_ERROR,
1177
1924
  `Failed to finalize download: ${input.target}`,
1178
1925
  { cause, context: { filePath: input.target } }
1179
1926
  );
1180
1927
  }
1181
1928
  input.onEvent?.({
1182
- type: "download:completed",
1929
+ type: EventTypes.DOWNLOAD_COMPLETED,
1183
1930
  file: fileRef,
1184
1931
  durationMs: Date.now() - startedAt,
1185
1932
  bytes: bytesDownloaded
1186
1933
  });
1187
1934
  if (input.expectedSha1 !== void 0) {
1188
1935
  input.onEvent?.({
1189
- type: "integrity:verified",
1936
+ type: EventTypes.INTEGRITY_VERIFIED,
1190
1937
  file: fileRef,
1191
1938
  algorithm: "sha1",
1192
1939
  hash: computedSha1
@@ -1199,7 +1946,7 @@ async function downloadFile(http, input) {
1199
1946
  ...input.signal !== void 0 ? { signal: input.signal } : {},
1200
1947
  onAttemptFailed: (error, attempt) => {
1201
1948
  input.onEvent?.({
1202
- type: "download:failed",
1949
+ type: EventTypes.DOWNLOAD_FAILED,
1203
1950
  file: fileRef,
1204
1951
  error: error instanceof Error ? error : new Error(String(error)),
1205
1952
  willRetry: isHttpRetryable(error) && attempt < HTTP_RETRY_MAX - 1
@@ -1207,8 +1954,8 @@ async function downloadFile(http, input) {
1207
1954
  }
1208
1955
  }
1209
1956
  );
1210
- }
1211
- async function checkExistingFile(target, expectedSha1, expectedSize) {
1957
+ };
1958
+ var checkExistingFile = async (target, expectedSha1, expectedSize) => {
1212
1959
  let stat;
1213
1960
  try {
1214
1961
  stat = await fs.stat(target);
@@ -1224,21 +1971,60 @@ async function checkExistingFile(target, expectedSha1, expectedSize) {
1224
1971
  const buf = await fs.readFile(target);
1225
1972
  const sha1 = crypto2.createHash("sha1").update(buf).digest("hex");
1226
1973
  return { matches: sha1 === expectedSha1, sha1 };
1227
- }
1228
- async function safeUnlink(filePath) {
1974
+ };
1975
+ var safeUnlink = async (filePath) => {
1229
1976
  try {
1230
1977
  await fs.unlink(filePath);
1231
1978
  } catch {
1232
1979
  }
1233
- }
1980
+ };
1981
+ var assertSafeDownloadUrl = (url, allowList) => {
1982
+ let parsed;
1983
+ try {
1984
+ parsed = new URL(url);
1985
+ } catch {
1986
+ throw new MinecraftKitError(
1987
+ MinecraftKitErrorCodes.INVALID_INPUT,
1988
+ `Download URL is not parseable: ${url}`,
1989
+ {
1990
+ context: { url }
1991
+ }
1992
+ );
1993
+ }
1994
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
1995
+ throw new MinecraftKitError(
1996
+ MinecraftKitErrorCodes.INVALID_INPUT,
1997
+ `Download URL must use http(s); refusing scheme ${parsed.protocol}`,
1998
+ { context: { url, scheme: parsed.protocol } }
1999
+ );
2000
+ }
2001
+ if (allowList !== void 0 && !matchesHostAllowList(parsed.hostname, allowList)) {
2002
+ throw new MinecraftKitError(
2003
+ MinecraftKitErrorCodes.INVALID_INPUT,
2004
+ `Download URL host is not in the allow-list: ${parsed.hostname}`,
2005
+ { context: { url, host: parsed.hostname } }
2006
+ );
2007
+ }
2008
+ };
2009
+ var matchesHostAllowList = (hostname, allowList) => {
2010
+ const host = hostname.toLowerCase();
2011
+ return allowList.some((entry) => matchesHostEntry(host, entry.toLowerCase()));
2012
+ };
2013
+ var matchesHostEntry = (host, entry) => {
2014
+ if (entry.startsWith("*.")) {
2015
+ const suffix = entry.slice(1);
2016
+ return host === entry.slice(2) || host.endsWith(suffix);
2017
+ }
2018
+ return host === entry;
2019
+ };
1234
2020
 
1235
2021
  // src/install/forge-install.ts
1236
- async function planForgeInstall(input) {
2022
+ var planForgeInstall = async (input) => {
1237
2023
  const installerPath = targetPaths.forgeInstaller(input.directory, input.loader.fullVersion);
1238
2024
  await downloadFile(input.http, {
1239
2025
  url: input.loader.installerUrl,
1240
2026
  target: installerPath,
1241
- category: "forge-installer",
2027
+ category: DownloadCategories.FORGE_INSTALLER,
1242
2028
  ...input.signal !== void 0 ? { signal: input.signal } : {},
1243
2029
  ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
1244
2030
  });
@@ -1246,7 +2032,7 @@ async function planForgeInstall(input) {
1246
2032
  kind: InstallActionKinds.DOWNLOAD_FILE,
1247
2033
  url: input.loader.installerUrl,
1248
2034
  target: installerPath,
1249
- category: "forge-installer"
2035
+ category: DownloadCategories.FORGE_INSTALLER
1250
2036
  };
1251
2037
  const profile = await readJsonEntry(installerPath, "install_profile.json");
1252
2038
  const versionRelative = profile.json.startsWith("/") ? profile.json.slice(1) : profile.json;
@@ -1262,14 +2048,14 @@ async function planForgeInstall(input) {
1262
2048
  directory: input.directory,
1263
2049
  system: input.system,
1264
2050
  versionId: input.minecraft.version,
1265
- category: "forge-library"
2051
+ category: DownloadCategories.FORGE_LIBRARY
1266
2052
  });
1267
2053
  const versionLibraries = planLibraryDownloads({
1268
2054
  libraries: version.libraries,
1269
2055
  directory: input.directory,
1270
2056
  system: input.system,
1271
2057
  versionId: version.id,
1272
- category: "forge-library"
2058
+ category: DownloadCategories.FORGE_LIBRARY
1273
2059
  });
1274
2060
  const dedupedDownloads = dedupeBy(
1275
2061
  [...installerLibraries.downloads, ...versionLibraries.downloads],
@@ -1304,27 +2090,23 @@ async function planForgeInstall(input) {
1304
2090
  profile,
1305
2091
  version
1306
2092
  };
1307
- }
1308
- async function readJsonEntry(zipPath, entryName) {
2093
+ };
2094
+ var readJsonEntry = async (zipPath, entryName) => {
1309
2095
  const buffer = await readEntryBuffer(zipPath, entryName);
1310
2096
  if (!buffer) {
1311
2097
  throw new MinecraftKitError(
1312
- "FORGE_INSTALLER_INVALID",
2098
+ MinecraftKitErrorCodes.FORGE_INSTALLER_INVALID,
1313
2099
  `Forge installer is missing required entry: ${entryName}`,
1314
2100
  { context: { filePath: zipPath, entryName } }
1315
2101
  );
1316
2102
  }
1317
- try {
1318
- return JSON.parse(buffer.toString("utf8"));
1319
- } catch (cause) {
1320
- throw new MinecraftKitError(
1321
- "FORGE_INSTALLER_INVALID",
1322
- `Forge installer entry is not valid JSON: ${entryName}`,
1323
- { cause, context: { filePath: zipPath, entryName } }
1324
- );
1325
- }
1326
- }
1327
- async function extractInstallerMavenEntries(installerPath, directory) {
2103
+ return parseJsonStrict(buffer.toString("utf8"), {
2104
+ code: MinecraftKitErrorCodes.FORGE_INSTALLER_INVALID,
2105
+ message: `Forge installer entry is not valid JSON: ${entryName}`,
2106
+ context: { filePath: zipPath, entryName }
2107
+ });
2108
+ };
2109
+ var extractInstallerMavenEntries = async (installerPath, directory) => {
1328
2110
  const reader = await openZip(installerPath);
1329
2111
  try {
1330
2112
  for await (const entry of reader.entries()) {
@@ -1337,16 +2119,16 @@ async function extractInstallerMavenEntries(installerPath, directory) {
1337
2119
  } finally {
1338
2120
  reader.close();
1339
2121
  }
1340
- }
1341
- async function resolveProfileData(input) {
2122
+ };
2123
+ var resolveProfileData = async (input) => {
1342
2124
  const tokens = {};
1343
2125
  for (const [key, sided] of Object.entries(input.profile.data)) {
1344
2126
  const raw = sided.client;
1345
2127
  tokens[key] = await resolveDataValue(raw, input.installerPath, input.directory);
1346
2128
  }
1347
2129
  return { tokens };
1348
- }
1349
- async function resolveDataValue(raw, installerPath, directory) {
2130
+ };
2131
+ var resolveDataValue = async (raw, installerPath, directory) => {
1350
2132
  if (raw.startsWith("[") && raw.endsWith("]")) {
1351
2133
  const coord = raw.slice(1, -1);
1352
2134
  const relativePath = mavenRelativePathFor(coord);
@@ -1365,8 +2147,8 @@ async function resolveDataValue(raw, installerPath, directory) {
1365
2147
  return { value: destination, isPath: true };
1366
2148
  }
1367
2149
  return { value: raw, isPath: false };
1368
- }
1369
- async function buildProcessorActions(input) {
2150
+ };
2151
+ var buildProcessorActions = async (input) => {
1370
2152
  const builtIns = {
1371
2153
  SIDE: { value: "client", isPath: false },
1372
2154
  MINECRAFT_JAR: {
@@ -1399,12 +2181,12 @@ async function buildProcessorActions(input) {
1399
2181
  index++;
1400
2182
  }
1401
2183
  return actions;
1402
- }
1403
- function processorAppliesToClient(processor) {
2184
+ };
2185
+ var processorAppliesToClient = (processor) => {
1404
2186
  if (!processor.sides || processor.sides.length === 0) return true;
1405
2187
  return processor.sides.includes("client");
1406
- }
1407
- function buildProcessorAction(input) {
2188
+ };
2189
+ var buildProcessorAction = (input) => {
1408
2190
  const jarPath = path.join(
1409
2191
  targetPaths.librariesDir(input.directory),
1410
2192
  mavenRelativePathFor(input.processor.jar)
@@ -1431,26 +2213,30 @@ function buildProcessorAction(input) {
1431
2213
  args,
1432
2214
  outputs
1433
2215
  };
1434
- }
1435
- function substituteToken(raw, tokens) {
2216
+ };
2217
+ var substituteToken = (raw, tokens) => {
1436
2218
  if (raw.startsWith("[") && raw.endsWith("]")) {
1437
2219
  return path.join(...mavenRelativePathFor(raw.slice(1, -1)).split("/"));
1438
2220
  }
1439
2221
  return raw.replaceAll(/\{([A-Z0-9_]+)\}/g, (match, key) => {
1440
2222
  const token = tokens[key];
1441
2223
  if (token === void 0) {
1442
- throw new MinecraftKitError("FORGE_INSTALLER_INVALID", `Unknown processor token: ${match}`, {
1443
- context: { token: key }
1444
- });
2224
+ throw new MinecraftKitError(
2225
+ MinecraftKitErrorCodes.FORGE_INSTALLER_INVALID,
2226
+ `Unknown processor token: ${match}`,
2227
+ {
2228
+ context: { token: key }
2229
+ }
2230
+ );
1445
2231
  }
1446
2232
  return token.value;
1447
2233
  });
1448
- }
1449
- function stripLiteralPrefix(value) {
2234
+ };
2235
+ var stripLiteralPrefix = (value) => {
1450
2236
  const stripped = value.startsWith("'") ? value.slice(1) : value;
1451
2237
  return stripped.endsWith("'") ? stripped.slice(0, -1) : stripped;
1452
- }
1453
- async function planRuntimeDownloads(input) {
2238
+ };
2239
+ var planRuntimeDownloads = async (input) => {
1454
2240
  const manifest = await fetchJson(input.http, input.cache, {
1455
2241
  url: input.runtime.manifestUrl,
1456
2242
  cacheKey: `runtime-manifest:${input.runtime.component}:${input.runtime.platformKey}:${input.runtime.manifestSha1}`,
@@ -1471,14 +2257,14 @@ async function planRuntimeDownloads(input) {
1471
2257
  target,
1472
2258
  expectedSha1: entry.downloads.raw.sha1,
1473
2259
  expectedSize: entry.downloads.raw.size,
1474
- category: "runtime-file"
2260
+ category: DownloadCategories.RUNTIME_FILE
1475
2261
  });
1476
2262
  }
1477
2263
  return { actions, manifest };
1478
- }
2264
+ };
1479
2265
 
1480
2266
  // src/install/planner.ts
1481
- async function planInstall(input) {
2267
+ var planInstall = async (input) => {
1482
2268
  const { target } = input;
1483
2269
  const actions = [];
1484
2270
  actions.push({
@@ -1487,7 +2273,7 @@ async function planInstall(input) {
1487
2273
  target: targetPaths.versionJar(target.directory, target.minecraft.version),
1488
2274
  expectedSha1: target.minecraft.manifest.downloads.client.sha1,
1489
2275
  expectedSize: target.minecraft.manifest.downloads.client.size,
1490
- category: "client-jar"
2276
+ category: DownloadCategories.CLIENT_JAR
1491
2277
  });
1492
2278
  actions.push({
1493
2279
  kind: InstallActionKinds.WRITE_VERSION_JSON,
@@ -1500,7 +2286,7 @@ async function planInstall(input) {
1500
2286
  directory: target.directory,
1501
2287
  system: target.runtime.system,
1502
2288
  versionId: target.minecraft.version,
1503
- category: "library"
2289
+ category: DownloadCategories.LIBRARY
1504
2290
  });
1505
2291
  actions.push(...vanillaLibraries.downloads);
1506
2292
  actions.push(...vanillaLibraries.nativeExtractions);
@@ -1520,7 +2306,7 @@ async function planInstall(input) {
1520
2306
  target: targetPaths.loggingConfig(target.directory, logging.file.id),
1521
2307
  expectedSha1: logging.file.sha1,
1522
2308
  expectedSize: logging.file.size,
1523
- category: "logging-config"
2309
+ category: DownloadCategories.LOGGING_CONFIG
1524
2310
  });
1525
2311
  }
1526
2312
  const runtimePlan = await planRuntimeDownloads({
@@ -1570,7 +2356,7 @@ async function planInstall(input) {
1570
2356
  totalActions: actions.length,
1571
2357
  totalBytes
1572
2358
  };
1573
- }
2359
+ };
1574
2360
 
1575
2361
  // node_modules/yocto-queue/index.js
1576
2362
  var Node = class {
@@ -1673,22 +2459,126 @@ function pLimit(concurrency) {
1673
2459
  const generator = (function_, ...arguments_) => new Promise((resolve) => {
1674
2460
  enqueue(function_, resolve, arguments_);
1675
2461
  });
1676
- Object.defineProperties(generator, {
1677
- activeCount: {
1678
- get: () => activeCount
1679
- },
1680
- pendingCount: {
1681
- get: () => queue.size
1682
- },
1683
- clearQueue: {
1684
- value() {
1685
- queue.clear();
1686
- }
2462
+ Object.defineProperties(generator, {
2463
+ activeCount: {
2464
+ get: () => activeCount
2465
+ },
2466
+ pendingCount: {
2467
+ get: () => queue.size
2468
+ },
2469
+ clearQueue: {
2470
+ value() {
2471
+ queue.clear();
2472
+ }
2473
+ }
2474
+ });
2475
+ return generator;
2476
+ }
2477
+ var sha1OfFile = async (filePath) => {
2478
+ const hash = crypto2.createHash("sha1");
2479
+ const stream = createReadStream(filePath);
2480
+ try {
2481
+ await new Promise((resolve, reject) => {
2482
+ stream.on("data", (chunk) => hash.update(chunk));
2483
+ stream.on("end", resolve);
2484
+ stream.on("error", reject);
2485
+ });
2486
+ } finally {
2487
+ if (!stream.destroyed) stream.destroy();
2488
+ }
2489
+ return hash.digest("hex");
2490
+ };
2491
+
2492
+ // src/install/processor.ts
2493
+ var runProcessor = async (input) => {
2494
+ const startedAt = Date.now();
2495
+ const mainClass = await resolveProcessorMainClass(input.action);
2496
+ emit(input, { type: EventTypes.FORGE_PROCESSOR_STARTED, index: input.action.index, mainClass });
2497
+ const exit = await spawnProcessor(input, mainClass);
2498
+ if (exit.code !== 0) {
2499
+ throw new MinecraftKitError(
2500
+ MinecraftKitErrorCodes.FORGE_PROCESSOR_FAILED,
2501
+ `Forge processor exited with code ${exit.code ?? "(signal)"}: ${mainClass}`,
2502
+ {
2503
+ context: {
2504
+ mainClass,
2505
+ stderr: exit.stderr,
2506
+ ...exit.code !== null ? { exitCode: exit.code } : {}
2507
+ }
2508
+ }
2509
+ );
2510
+ }
2511
+ input.onEvent?.({
2512
+ type: EventTypes.FORGE_PROCESSOR_COMPLETED,
2513
+ processor: { index: input.action.index, mainClass },
2514
+ exitCode: exit.code ?? 0,
2515
+ durationMs: Date.now() - startedAt
2516
+ });
2517
+ await verifyProcessorOutputs(input, mainClass);
2518
+ };
2519
+ var resolveProcessorMainClass = async (action) => {
2520
+ const processorJar = action.classpath[0];
2521
+ if (processorJar === void 0) {
2522
+ throw new MinecraftKitError(
2523
+ MinecraftKitErrorCodes.FORGE_INSTALLER_INVALID,
2524
+ "Forge processor has an empty classpath",
2525
+ { context: { processorIndex: action.index } }
2526
+ );
2527
+ }
2528
+ const mainClass = await readJarMainClass(processorJar);
2529
+ if (!mainClass) {
2530
+ throw new MinecraftKitError(
2531
+ MinecraftKitErrorCodes.FORGE_INSTALLER_INVALID,
2532
+ `Forge processor jar has no Main-Class: ${processorJar}`,
2533
+ { context: { filePath: processorJar } }
2534
+ );
2535
+ }
2536
+ return mainClass;
2537
+ };
2538
+ var spawnProcessor = async (input, mainClass) => {
2539
+ const classpathSeparator = process.platform === "win32" ? ";" : ":";
2540
+ const args = [
2541
+ "-cp",
2542
+ input.action.classpath.join(classpathSeparator),
2543
+ mainClass,
2544
+ ...input.action.args
2545
+ ];
2546
+ const stderrTail = [];
2547
+ const child = input.spawner.spawn(input.javaPath, args, { cwd: process.cwd() });
2548
+ child.stdout.on("data", () => {
2549
+ });
2550
+ child.stderr.on("data", (line) => {
2551
+ if (stderrTail.length >= MAX_PROCESSOR_STDERR_LINES) stderrTail.shift();
2552
+ stderrTail.push(line);
2553
+ });
2554
+ const exit = await child.exited;
2555
+ return { code: exit.code, stderr: stderrTail.join("\n") };
2556
+ };
2557
+ var verifyProcessorOutputs = async (input, mainClass) => {
2558
+ for (const [outputPath, expectedSha1] of Object.entries(input.action.outputs)) {
2559
+ const sha1 = await sha1OfFile(outputPath);
2560
+ if (sha1 !== expectedSha1) {
2561
+ throw new MinecraftKitError(
2562
+ MinecraftKitErrorCodes.FORGE_PROCESSOR_FAILED,
2563
+ `Processor output hash mismatch: ${outputPath}`,
2564
+ { context: { filePath: outputPath, expectedHash: expectedSha1, actualHash: sha1 } }
2565
+ );
1687
2566
  }
2567
+ input.onEvent?.({
2568
+ type: EventTypes.FORGE_PROCESSOR_OUTPUT_VERIFIED,
2569
+ processor: { index: input.action.index, mainClass },
2570
+ path: outputPath
2571
+ });
2572
+ }
2573
+ };
2574
+ var emit = (input, event) => {
2575
+ input.onEvent?.({
2576
+ type: event.type,
2577
+ processor: { index: event.index, mainClass: event.mainClass },
2578
+ total: input.total
1688
2579
  });
1689
- return generator;
1690
- }
1691
- async function materializeRuntimeExtras(input) {
2580
+ };
2581
+ var materializeRuntimeExtras = async (input) => {
1692
2582
  const root = targetPaths.runtimeRoot(
1693
2583
  input.directory,
1694
2584
  input.runtime.component,
@@ -1707,20 +2597,20 @@ async function materializeRuntimeExtras(input) {
1707
2597
  });
1708
2598
  }
1709
2599
  }
1710
- }
1711
- async function unlinkIfPresent(target) {
2600
+ };
2601
+ var unlinkIfPresent = async (target) => {
1712
2602
  try {
1713
2603
  await fs.unlink(target);
1714
2604
  } catch (cause) {
1715
2605
  if (isNotFound(cause)) return;
1716
2606
  throw new MinecraftKitError(
1717
- "FILESYSTEM_WRITE_ERROR",
2607
+ MinecraftKitErrorCodes.FILESYSTEM_WRITE_ERROR,
1718
2608
  `Failed to remove stale runtime entry: ${target}`,
1719
2609
  { cause, context: { filePath: target } }
1720
2610
  );
1721
2611
  }
1722
- }
1723
- async function createLinkOrCopy(root, relativePath, linkTarget, destination) {
2612
+ };
2613
+ var createLinkOrCopy = async (root, relativePath, linkTarget, destination) => {
1724
2614
  try {
1725
2615
  await fs.symlink(linkTarget, destination);
1726
2616
  return;
@@ -1730,7 +2620,7 @@ async function createLinkOrCopy(root, relativePath, linkTarget, destination) {
1730
2620
  await fs.copyFile(absoluteSource, destination);
1731
2621
  } catch (copyError) {
1732
2622
  throw new MinecraftKitError(
1733
- "FILESYSTEM_WRITE_ERROR",
2623
+ MinecraftKitErrorCodes.FILESYSTEM_WRITE_ERROR,
1734
2624
  `Failed to materialize runtime entry: ${destination}`,
1735
2625
  {
1736
2626
  cause: copyError,
@@ -1743,283 +2633,209 @@ async function createLinkOrCopy(root, relativePath, linkTarget, destination) {
1743
2633
  );
1744
2634
  }
1745
2635
  }
1746
- }
1747
- function isNotFound(error) {
2636
+ };
2637
+ var isNotFound = (error) => {
1748
2638
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1749
- }
1750
- function errorMessage(error) {
2639
+ };
2640
+ var errorMessage = (error) => {
1751
2641
  return error instanceof Error ? error.message : String(error);
1752
- }
2642
+ };
1753
2643
 
1754
2644
  // src/install/runner.ts
1755
2645
  var DOWNLOAD_GROUPS = [
1756
- { categories: ["runtime-file"], phase: InstallPhases.INSTALLING_RUNTIME },
1757
- { categories: ["client-jar"], phase: InstallPhases.DOWNLOADING_CLIENT_JAR },
1758
- { categories: ["library"], phase: InstallPhases.DOWNLOADING_LIBRARIES },
1759
- { categories: ["asset-index"], phase: InstallPhases.DOWNLOADING_ASSET_INDEX },
1760
- { categories: ["asset"], phase: InstallPhases.DOWNLOADING_ASSETS },
1761
- { categories: ["logging-config"], phase: InstallPhases.WRITING_FILES },
1762
- { categories: ["fabric-library"], phase: InstallPhases.INSTALLING_FABRIC },
1763
- { categories: ["forge-installer", "forge-library"], phase: InstallPhases.INSTALLING_FORGE }
2646
+ { categories: [DownloadCategories.RUNTIME_FILE], phase: InstallPhases.INSTALLING_RUNTIME },
2647
+ { categories: [DownloadCategories.CLIENT_JAR], phase: InstallPhases.DOWNLOADING_CLIENT_JAR },
2648
+ { categories: [DownloadCategories.LIBRARY], phase: InstallPhases.DOWNLOADING_LIBRARIES },
2649
+ { categories: [DownloadCategories.ASSET_INDEX], phase: InstallPhases.DOWNLOADING_ASSET_INDEX },
2650
+ { categories: [DownloadCategories.ASSET], phase: InstallPhases.DOWNLOADING_ASSETS },
2651
+ { categories: [DownloadCategories.LOGGING_CONFIG], phase: InstallPhases.WRITING_FILES },
2652
+ { categories: [DownloadCategories.FABRIC_LIBRARY], phase: InstallPhases.INSTALLING_FABRIC },
2653
+ {
2654
+ categories: [DownloadCategories.FORGE_INSTALLER, DownloadCategories.FORGE_LIBRARY],
2655
+ phase: InstallPhases.INSTALLING_FORGE
2656
+ }
1764
2657
  ];
1765
- async function runInstall(input) {
2658
+ var runInstall = async (input) => {
1766
2659
  const startedAt = Date.now();
1767
- let bytesDownloaded = 0;
1768
- let actionsCompleted = 0;
1769
- let actionsSkipped = 0;
1770
- const onEvent = input.onEvent;
2660
+ const counters = { bytesDownloaded: 0, actionsCompleted: 0, actionsSkipped: 0 };
2661
+ const ctx = createContext(input, counters);
2662
+ ctx.enterPhase(InstallPhases.PLANNING);
2663
+ const plannedActions = partitionActions(input);
2664
+ await runDownloadsStage(ctx, plannedActions.downloads);
2665
+ await runWritesStage(ctx, plannedActions.writes);
2666
+ await runNativesStage(ctx, plannedActions.natives);
2667
+ await runRuntimeStage(ctx);
2668
+ await runProcessorsStage(ctx, plannedActions.processors);
2669
+ ctx.enterPhase(InstallPhases.COMPLETED);
2670
+ return {
2671
+ targetId: input.plan.targetId,
2672
+ bytesDownloaded: counters.bytesDownloaded,
2673
+ actionsCompleted: counters.actionsCompleted,
2674
+ actionsSkipped: counters.actionsSkipped,
2675
+ durationMs: Date.now() - startedAt
2676
+ };
2677
+ };
2678
+ var createContext = (input, counters) => {
1771
2679
  let currentPhase = null;
1772
2680
  const enterPhase = (phase) => {
1773
2681
  if (phase === currentPhase) return;
1774
- onEvent?.({ type: "install:phase-changed", phase, previous: currentPhase });
2682
+ input.onEvent?.({ type: EventTypes.INSTALL_PHASE_CHANGED, phase, previous: currentPhase });
1775
2683
  currentPhase = phase;
1776
2684
  };
1777
- const checkpoint = async () => {
1778
- if (input.signal?.aborted) {
1779
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1780
- }
1781
- await input.pauseController?.waitWhilePaused();
1782
- if (input.signal?.aborted) {
1783
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1784
- }
2685
+ const checkpoint2 = () => checkpoint(
2686
+ {
2687
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
2688
+ ...input.pauseController !== void 0 ? { pauseController: input.pauseController } : {}
2689
+ },
2690
+ "Install aborted by signal"
2691
+ );
2692
+ return {
2693
+ input,
2694
+ counters,
2695
+ checkpoint: checkpoint2,
2696
+ enterPhase,
2697
+ limit: pLimit(input.concurrency ?? DOWNLOAD_CONCURRENCY)
2698
+ };
2699
+ };
2700
+ var partitionActions = (input) => {
2701
+ const filter = input.actionCategories;
2702
+ return {
2703
+ downloads: input.plan.actions.filter(isDownload).filter((a) => filter ? filter.has(a.category) : true),
2704
+ natives: input.plan.actions.filter(isNative),
2705
+ writes: input.plan.actions.filter(isWrite),
2706
+ processors: input.plan.actions.filter(isProcessor)
1785
2707
  };
1786
- const categoryFilter = input.actionCategories;
1787
- const downloads = input.plan.actions.filter(isDownload).filter((a) => categoryFilter ? categoryFilter.has(a.category) : true);
1788
- const natives = input.plan.actions.filter(isNative);
1789
- const writeActions = input.plan.actions.filter(isWrite);
1790
- const processors = input.plan.actions.filter(isProcessor);
1791
- enterPhase(InstallPhases.PLANNING);
1792
- const limit = pLimit(input.concurrency ?? DOWNLOAD_CONCURRENCY);
2708
+ };
2709
+ var runDownloadsStage = async (ctx, downloads) => {
2710
+ if (downloads.length === 0) return;
1793
2711
  for (const group of DOWNLOAD_GROUPS) {
1794
2712
  const groupActions = downloads.filter((action) => group.categories.includes(action.category));
1795
2713
  if (groupActions.length === 0) continue;
1796
- await checkpoint();
1797
- enterPhase(group.phase);
1798
- await Promise.all(
1799
- groupActions.map(
1800
- (action) => limit(async () => {
1801
- await checkpoint();
1802
- const result = await downloadFile(input.http, {
1803
- url: action.url,
1804
- target: action.target,
1805
- ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
1806
- ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
1807
- ...action.category !== void 0 ? { category: action.category } : {},
1808
- ...input.signal !== void 0 ? { signal: input.signal } : {},
1809
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {},
1810
- ...input.pauseController !== void 0 ? { pauseController: input.pauseController } : {}
1811
- });
1812
- bytesDownloaded += result.bytesDownloaded;
1813
- if (result.skipped) actionsSkipped++;
1814
- actionsCompleted++;
1815
- })
1816
- )
1817
- );
2714
+ await ctx.checkpoint();
2715
+ ctx.enterPhase(group.phase);
2716
+ await runDownloadGroup(ctx, groupActions);
1818
2717
  }
1819
2718
  const ungrouped = downloads.filter(
1820
2719
  (action) => !DOWNLOAD_GROUPS.some((g) => g.categories.includes(action.category))
1821
2720
  );
1822
2721
  if (ungrouped.length > 0) {
1823
- await checkpoint();
1824
- enterPhase(InstallPhases.DOWNLOADING_LIBRARIES);
1825
- await Promise.all(
1826
- ungrouped.map(
1827
- (action) => limit(async () => {
1828
- await checkpoint();
1829
- const result = await downloadFile(input.http, {
1830
- url: action.url,
1831
- target: action.target,
1832
- ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
1833
- ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
1834
- ...action.category !== void 0 ? { category: action.category } : {},
1835
- ...input.signal !== void 0 ? { signal: input.signal } : {},
1836
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {},
1837
- ...input.pauseController !== void 0 ? { pauseController: input.pauseController } : {}
1838
- });
1839
- bytesDownloaded += result.bytesDownloaded;
1840
- if (result.skipped) actionsSkipped++;
1841
- actionsCompleted++;
1842
- })
1843
- )
1844
- );
1845
- }
1846
- if (writeActions.length > 0) {
1847
- await checkpoint();
1848
- enterPhase(InstallPhases.WRITING_FILES);
1849
- for (const action of writeActions) {
1850
- await checkpoint();
1851
- await atomicWrite(action.path, action.content);
1852
- actionsCompleted++;
1853
- }
2722
+ await ctx.checkpoint();
2723
+ ctx.enterPhase(InstallPhases.DOWNLOADING_LIBRARIES);
2724
+ await runDownloadGroup(ctx, ungrouped);
1854
2725
  }
1855
- if (natives.length > 0) {
1856
- await checkpoint();
1857
- enterPhase(InstallPhases.EXTRACTING_NATIVES);
1858
- for (const action of natives) {
1859
- await checkpoint();
1860
- const { fileCount } = await extractAllToDir(action.source, action.destination, {
1861
- excludePrefixes: action.exclude
1862
- });
1863
- input.onEvent?.({
1864
- type: "archive:extracted",
1865
- archive: action.source,
1866
- target: action.destination,
1867
- fileCount
1868
- });
1869
- actionsCompleted++;
1870
- }
2726
+ };
2727
+ var runDownloadGroup = async (ctx, groupActions) => {
2728
+ await Promise.all(
2729
+ groupActions.map(
2730
+ (action) => ctx.limit(async () => {
2731
+ await ctx.checkpoint();
2732
+ const result = await downloadFile(ctx.input.http, {
2733
+ url: action.url,
2734
+ target: action.target,
2735
+ ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
2736
+ ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
2737
+ ...action.category !== void 0 ? { category: action.category } : {},
2738
+ ...ctx.input.signal !== void 0 ? { signal: ctx.input.signal } : {},
2739
+ ...ctx.input.onEvent !== void 0 ? { onEvent: ctx.input.onEvent } : {},
2740
+ ...ctx.input.pauseController !== void 0 ? { pauseController: ctx.input.pauseController } : {}
2741
+ });
2742
+ ctx.counters.bytesDownloaded += result.bytesDownloaded;
2743
+ if (result.skipped) ctx.counters.actionsSkipped++;
2744
+ ctx.counters.actionsCompleted++;
2745
+ })
2746
+ )
2747
+ );
2748
+ };
2749
+ var runWritesStage = async (ctx, writes) => {
2750
+ if (writes.length === 0) return;
2751
+ await ctx.checkpoint();
2752
+ ctx.enterPhase(InstallPhases.WRITING_FILES);
2753
+ for (const action of writes) {
2754
+ await ctx.checkpoint();
2755
+ await atomicWrite(action.path, action.content);
2756
+ ctx.counters.actionsCompleted++;
1871
2757
  }
1872
- if (input.plan.target.runtime !== void 0) {
1873
- await checkpoint();
1874
- enterPhase(InstallPhases.INSTALLING_RUNTIME);
1875
- const runtimePlan = await planRuntimeDownloads({
1876
- runtime: input.plan.target.runtime,
1877
- directory: input.plan.directory,
1878
- http: input.http,
1879
- cache: input.cache,
1880
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2758
+ };
2759
+ var runNativesStage = async (ctx, natives) => {
2760
+ if (natives.length === 0) return;
2761
+ await ctx.checkpoint();
2762
+ ctx.enterPhase(InstallPhases.EXTRACTING_NATIVES);
2763
+ for (const action of natives) {
2764
+ await ctx.checkpoint();
2765
+ const { fileCount } = await extractAllToDir(action.source, action.destination, {
2766
+ excludePrefixes: action.exclude
1881
2767
  });
1882
- await materializeRuntimeExtras({
1883
- runtime: input.plan.target.runtime,
1884
- directory: input.plan.directory,
1885
- manifest: runtimePlan.manifest
2768
+ ctx.input.onEvent?.({
2769
+ type: EventTypes.ARCHIVE_EXTRACTED,
2770
+ archive: action.source,
2771
+ target: action.destination,
2772
+ fileCount
1886
2773
  });
2774
+ ctx.counters.actionsCompleted++;
1887
2775
  }
1888
- if (processors.length > 0) {
1889
- await checkpoint();
1890
- enterPhase(InstallPhases.RUNNING_FORGE_PROCESSORS);
1891
- if (input.plan.target.loader.type !== Loaders.FORGE) {
1892
- throw new MinecraftKitError(
1893
- "FORGE_PROCESSOR_FAILED",
1894
- "Forge processors planned for a non-Forge target"
1895
- );
1896
- }
1897
- const javaPath = targetPaths.runtimeJavaExecutable(
1898
- input.plan.directory,
1899
- input.plan.target.runtime.component,
1900
- input.plan.target.runtime.system.os,
1901
- input.plan.target.runtime.installRoot
1902
- );
1903
- for (const action of processors) {
1904
- await checkpoint();
1905
- await runProcessor({
1906
- action,
1907
- javaPath,
1908
- spawner: input.spawner,
1909
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {},
1910
- total: processors.length
1911
- });
1912
- actionsCompleted++;
1913
- }
1914
- }
1915
- enterPhase(InstallPhases.COMPLETED);
1916
- return {
1917
- targetId: input.plan.targetId,
1918
- bytesDownloaded,
1919
- actionsCompleted,
1920
- actionsSkipped,
1921
- durationMs: Date.now() - startedAt
1922
- };
1923
- }
1924
- function isDownload(action) {
1925
- return action.kind === InstallActionKinds.DOWNLOAD_FILE;
1926
- }
1927
- function isNative(action) {
1928
- return action.kind === InstallActionKinds.EXTRACT_NATIVE;
1929
- }
1930
- function isProcessor(action) {
1931
- return action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR;
1932
- }
1933
- function isWrite(action) {
1934
- return action.kind === InstallActionKinds.WRITE_VERSION_JSON || action.kind === InstallActionKinds.WRITE_LOGGING_CONFIG;
1935
- }
1936
- async function runProcessor(input) {
1937
- const startedAt = Date.now();
1938
- const processorJar = input.action.classpath[0];
1939
- if (processorJar === void 0) {
1940
- throw new MinecraftKitError(
1941
- "FORGE_INSTALLER_INVALID",
1942
- "Forge processor has an empty classpath",
1943
- { context: { processorIndex: input.action.index } }
1944
- );
1945
- }
1946
- const mainClass = await readJarMainClass(processorJar);
1947
- if (!mainClass) {
1948
- throw new MinecraftKitError(
1949
- "FORGE_INSTALLER_INVALID",
1950
- `Forge processor jar has no Main-Class: ${processorJar}`,
1951
- { context: { filePath: processorJar } }
1952
- );
1953
- }
1954
- const classpathSeparator = process.platform === "win32" ? ";" : ":";
1955
- const args = [
1956
- "-cp",
1957
- input.action.classpath.join(classpathSeparator),
1958
- mainClass,
1959
- ...input.action.args
1960
- ];
1961
- input.onEvent?.({
1962
- type: "forge:processor-started",
1963
- processor: { index: input.action.index, mainClass },
1964
- total: input.total
1965
- });
1966
- const stderrTail = [];
1967
- const child = input.spawner.spawn(input.javaPath, args, { cwd: process.cwd() });
1968
- child.stdout.on("data", () => {
2776
+ };
2777
+ var runRuntimeStage = async (ctx) => {
2778
+ const runtime = ctx.input.plan.target.runtime;
2779
+ if (runtime === void 0) return;
2780
+ await ctx.checkpoint();
2781
+ ctx.enterPhase(InstallPhases.INSTALLING_RUNTIME);
2782
+ const runtimePlan = await planRuntimeDownloads({
2783
+ runtime,
2784
+ directory: ctx.input.plan.directory,
2785
+ http: ctx.input.http,
2786
+ cache: ctx.input.cache,
2787
+ ...ctx.input.signal !== void 0 ? { signal: ctx.input.signal } : {}
1969
2788
  });
1970
- child.stderr.on("data", (line) => {
1971
- if (stderrTail.length >= MAX_PROCESSOR_STDERR_LINES) stderrTail.shift();
1972
- stderrTail.push(line);
2789
+ await materializeRuntimeExtras({
2790
+ runtime,
2791
+ directory: ctx.input.plan.directory,
2792
+ manifest: runtimePlan.manifest
1973
2793
  });
1974
- const exit = await child.exited;
1975
- if (exit.code !== 0) {
2794
+ };
2795
+ var runProcessorsStage = async (ctx, processors) => {
2796
+ if (processors.length === 0) return;
2797
+ await ctx.checkpoint();
2798
+ ctx.enterPhase(InstallPhases.RUNNING_FORGE_PROCESSORS);
2799
+ const target = ctx.input.plan.target;
2800
+ if (target.loader?.type !== Loaders.FORGE) {
1976
2801
  throw new MinecraftKitError(
1977
- "FORGE_PROCESSOR_FAILED",
1978
- `Forge processor exited with code ${exit.code ?? "(signal)"}: ${mainClass}`,
1979
- {
1980
- context: {
1981
- exitCode: exit.code ?? void 0,
1982
- mainClass,
1983
- stderr: stderrTail.join("\n")
1984
- }
1985
- }
2802
+ MinecraftKitErrorCodes.FORGE_PROCESSOR_FAILED,
2803
+ "Forge processors planned for a non-Forge target"
1986
2804
  );
1987
2805
  }
1988
- input.onEvent?.({
1989
- type: "forge:processor-completed",
1990
- processor: { index: input.action.index, mainClass },
1991
- exitCode: exit.code ?? 0,
1992
- durationMs: Date.now() - startedAt
1993
- });
1994
- for (const [outputPath, expectedSha1] of Object.entries(input.action.outputs)) {
1995
- const sha1 = await sha1OfFileStreaming(outputPath);
1996
- if (sha1 !== expectedSha1) {
1997
- throw new MinecraftKitError(
1998
- "FORGE_PROCESSOR_FAILED",
1999
- `Processor output hash mismatch: ${outputPath}`,
2000
- { context: { filePath: outputPath, expectedHash: expectedSha1, actualHash: sha1 } }
2001
- );
2002
- }
2003
- input.onEvent?.({
2004
- type: "forge:processor-output-verified",
2005
- processor: { index: input.action.index, mainClass },
2006
- path: outputPath
2806
+ const javaPath = targetPaths.runtimeJavaExecutable(
2807
+ ctx.input.plan.directory,
2808
+ target.runtime.component,
2809
+ target.runtime.system.os,
2810
+ target.runtime.installRoot
2811
+ );
2812
+ for (const action of processors) {
2813
+ await ctx.checkpoint();
2814
+ await runProcessor({
2815
+ action,
2816
+ javaPath,
2817
+ spawner: ctx.input.spawner,
2818
+ ...ctx.input.onEvent !== void 0 ? { onEvent: ctx.input.onEvent } : {},
2819
+ total: processors.length
2007
2820
  });
2821
+ ctx.counters.actionsCompleted++;
2008
2822
  }
2009
- }
2010
- async function sha1OfFileStreaming(filePath) {
2011
- const hash = crypto2.createHash("sha1");
2012
- await new Promise((resolve, reject) => {
2013
- const stream = createReadStream(filePath);
2014
- stream.on("data", (chunk) => hash.update(chunk));
2015
- stream.on("end", () => resolve());
2016
- stream.on("error", reject);
2017
- });
2018
- return hash.digest("hex");
2019
- }
2823
+ };
2824
+ var isDownload = (action) => {
2825
+ return action.kind === InstallActionKinds.DOWNLOAD_FILE;
2826
+ };
2827
+ var isNative = (action) => {
2828
+ return action.kind === InstallActionKinds.EXTRACT_NATIVE;
2829
+ };
2830
+ var isProcessor = (action) => {
2831
+ return action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR;
2832
+ };
2833
+ var isWrite = (action) => {
2834
+ return action.kind === InstallActionKinds.WRITE_VERSION_JSON || action.kind === InstallActionKinds.WRITE_LOGGING_CONFIG;
2835
+ };
2020
2836
 
2021
2837
  // src/install/runtime-install.ts
2022
- async function planRuntimeInstall(input) {
2838
+ var planRuntimeInstall = async (input) => {
2023
2839
  const runtimePlan = await planRuntimeDownloads({
2024
2840
  runtime: input.target.runtime,
2025
2841
  directory: input.target.directory,
@@ -2040,8 +2856,8 @@ async function planRuntimeInstall(input) {
2040
2856
  totalActions: actions.length,
2041
2857
  totalBytes
2042
2858
  };
2043
- }
2044
- async function planStandaloneRuntimeInstall(input) {
2859
+ };
2860
+ var planStandaloneRuntimeInstall = async (input) => {
2045
2861
  const runtimePlan = await planRuntimeDownloads({
2046
2862
  runtime: input.runtime,
2047
2863
  directory: input.directory,
@@ -2057,9 +2873,7 @@ async function planStandaloneRuntimeInstall(input) {
2057
2873
  const target = {
2058
2874
  id: input.id,
2059
2875
  directory: input.directory,
2060
- runtime: input.runtime,
2061
- minecraft: void 0,
2062
- loader: void 0
2876
+ runtime: input.runtime
2063
2877
  };
2064
2878
  return {
2065
2879
  targetId: input.id,
@@ -2069,7 +2883,7 @@ async function planStandaloneRuntimeInstall(input) {
2069
2883
  totalActions: actions.length,
2070
2884
  totalBytes
2071
2885
  };
2072
- }
2886
+ };
2073
2887
 
2074
2888
  // src/constants/launch.ts
2075
2889
  var BASE_JVM_ARGS = [
@@ -2090,7 +2904,7 @@ var LEGACY_JVM_ARGS = [
2090
2904
  var MACOS_JVM_ARGS = ["-Xdock:name=Minecraft"];
2091
2905
 
2092
2906
  // src/launch/arguments.ts
2093
- function flattenArguments(entries, context) {
2907
+ var flattenArguments = (entries, context) => {
2094
2908
  const result = [];
2095
2909
  for (const entry of entries) {
2096
2910
  if (typeof entry === "string") {
@@ -2105,16 +2919,16 @@ function flattenArguments(entries, context) {
2105
2919
  }
2106
2920
  }
2107
2921
  return result;
2108
- }
2109
- function splitLegacyArguments(raw) {
2922
+ };
2923
+ var splitLegacyArguments = (raw) => {
2110
2924
  return raw.trim().length === 0 ? [] : raw.trim().split(/\s+/);
2111
- }
2112
- function pickArguments(args, context) {
2925
+ };
2926
+ var pickArguments = (args, context) => {
2113
2927
  return {
2114
2928
  game: flattenArguments(args?.game ?? [], context),
2115
2929
  jvm: flattenArguments(args?.jvm ?? [], context)
2116
2930
  };
2117
- }
2931
+ };
2118
2932
 
2119
2933
  // src/launch/jvm-compat.ts
2120
2934
  var FLAG_MIN_JAVA = [
@@ -2123,7 +2937,7 @@ var FLAG_MIN_JAVA = [
2123
2937
  { prefix: "-XX:+UseCompactObjectHeaders", minJava: 24 },
2124
2938
  { prefix: "-XX:+UseZGC", minJava: 15 }
2125
2939
  ];
2126
- function filterArgsForJava(input) {
2940
+ var filterArgsForJava = (input) => {
2127
2941
  if (!Number.isFinite(input.javaMajor) || input.javaMajor <= 0) return input.args;
2128
2942
  const out = [];
2129
2943
  for (const arg of input.args) {
@@ -2141,26 +2955,30 @@ function filterArgsForJava(input) {
2141
2955
  out.push(arg);
2142
2956
  }
2143
2957
  return out;
2144
- }
2958
+ };
2145
2959
 
2146
2960
  // src/launch/placeholders.ts
2147
- function substituteArg(raw, values) {
2961
+ var substituteArg = (raw, values) => {
2148
2962
  return raw.replaceAll(/\$\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
2149
2963
  const value = values[key];
2150
2964
  if (value === void 0) {
2151
- throw new MinecraftKitError("INVALID_INPUT", `Unknown launch placeholder: ${match}`, {
2152
- context: { placeholder: key }
2153
- });
2965
+ throw new MinecraftKitError(
2966
+ MinecraftKitErrorCodes.INVALID_INPUT,
2967
+ `Unknown launch placeholder: ${match}`,
2968
+ {
2969
+ context: { placeholder: key }
2970
+ }
2971
+ );
2154
2972
  }
2155
2973
  return value;
2156
2974
  });
2157
- }
2158
- function substituteArgs(args, values) {
2975
+ };
2976
+ var substituteArgs = (args, values) => {
2159
2977
  return args.map((arg) => substituteArg(arg, values));
2160
- }
2978
+ };
2161
2979
 
2162
2980
  // src/launch/args-composition.ts
2163
- function composeArgs(input) {
2981
+ var composeArgs = (input) => {
2164
2982
  const minMb = input.options.memory?.minMb ?? DEFAULT_MIN_MB;
2165
2983
  const maxMb = input.options.memory?.maxMb ?? DEFAULT_MAX_MB;
2166
2984
  const memoryArgs = [`-Xms${minMb}M`, `-Xmx${maxMb}M`];
@@ -2210,8 +3028,8 @@ function composeArgs(input) {
2210
3028
  );
2211
3029
  }
2212
3030
  return { jvmArgs: [...jvmArgs, ...extraJvm], gameArgs };
2213
- }
2214
- function buildClasspath(input) {
3031
+ };
3032
+ var buildClasspath = (input) => {
2215
3033
  const seen = /* @__PURE__ */ new Set();
2216
3034
  const entries = [];
2217
3035
  for (const library of input.merged.libraries) {
@@ -2227,41 +3045,33 @@ function buildClasspath(input) {
2227
3045
  const versionJar = targetPaths.versionJar(input.directory, input.versionId);
2228
3046
  if (!seen.has(versionJar)) entries.push(versionJar);
2229
3047
  return entries;
2230
- }
2231
- function relativeFor(library) {
3048
+ };
3049
+ var relativeFor = (library) => {
2232
3050
  if (library.downloads?.artifact?.path) return library.downloads.artifact.path;
2233
3051
  if (library.name) {
2234
3052
  const coord = parseMavenCoordinate(library.name);
2235
3053
  return mavenRelativePath(coord);
2236
3054
  }
2237
3055
  return null;
2238
- }
2239
- function offlineUuidFor(username) {
3056
+ };
3057
+ var offlineUuidFor = (username) => {
2240
3058
  const md5 = crypto2.createHash("md5");
2241
3059
  md5.update(`OfflinePlayer:${username}`, "utf8");
2242
3060
  const bytes = md5.digest();
2243
3061
  bytes[6] = (bytes[6] ?? 0) & 15 | 48;
2244
3062
  bytes[8] = (bytes[8] ?? 0) & 63 | 128;
2245
3063
  return formatUuid(bytes);
2246
- }
2247
- function formatUuid(bytes) {
3064
+ };
3065
+ var formatUuid = (bytes) => {
2248
3066
  const hex = bytes.toString("hex");
2249
3067
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
2250
- }
2251
- function stripUuidDashes(uuid) {
3068
+ };
3069
+ var stripUuidDashes = (uuid) => {
2252
3070
  return uuid.replaceAll("-", "");
2253
- }
2254
-
2255
- // src/types/auth.ts
2256
- var AuthModes = {
2257
- /** Offline-mode play with a chosen username and synthetic UUID. */
2258
- OFFLINE: "offline",
2259
- /** Pre-authenticated session — caller provides the access token and identity. */
2260
- ONLINE: "online"
2261
3071
  };
2262
3072
 
2263
3073
  // src/launch/placeholder-values.ts
2264
- function buildPlaceholderValues(input) {
3074
+ var buildPlaceholderValues = (input) => {
2265
3075
  const cpSeparator = process.platform === "win32" ? ";" : ":";
2266
3076
  const directory = input.target.directory;
2267
3077
  const username = input.auth.username;
@@ -2294,11 +3104,20 @@ function buildPlaceholderValues(input) {
2294
3104
  resolution_width: input.options.resolution?.width.toString() ?? "",
2295
3105
  resolution_height: input.options.resolution?.height.toString() ?? ""
2296
3106
  };
2297
- }
3107
+ };
2298
3108
 
2299
3109
  // src/core/manifest-merge.ts
2300
- function mergeManifest(parent, child) {
2301
- const merged = {
3110
+ var mergeManifest = (parent, child) => {
3111
+ const args = mergeArguments(parent.arguments, child.arguments);
3112
+ const minecraftArguments = child.minecraftArguments ?? parent.minecraftArguments;
3113
+ const javaVersion = child.javaVersion ?? parent.javaVersion;
3114
+ const logging = child.logging ?? parent.logging;
3115
+ const inheritsFrom = child.inheritsFrom ?? parent.inheritsFrom;
3116
+ const releaseTime = child.releaseTime ?? parent.releaseTime;
3117
+ const time = child.time ?? parent.time;
3118
+ const minimumLauncherVersion = child.minimumLauncherVersion ?? parent.minimumLauncherVersion;
3119
+ const complianceLevel = child.complianceLevel ?? parent.complianceLevel;
3120
+ return {
2302
3121
  id: child.id || parent.id,
2303
3122
  type: child.type ?? parent.type,
2304
3123
  mainClass: child.mainClass ?? parent.mainClass,
@@ -2306,19 +3125,18 @@ function mergeManifest(parent, child) {
2306
3125
  assets: child.assets ?? parent.assets,
2307
3126
  downloads: { ...parent.downloads, ...child.downloads },
2308
3127
  libraries: mergeLibraries(parent.libraries, child.libraries),
2309
- arguments: mergeArguments(parent.arguments, child.arguments),
2310
- minecraftArguments: child.minecraftArguments ?? parent.minecraftArguments,
2311
- javaVersion: child.javaVersion ?? parent.javaVersion,
2312
- logging: child.logging ?? parent.logging,
2313
- inheritsFrom: child.inheritsFrom ?? parent.inheritsFrom,
2314
- releaseTime: child.releaseTime ?? parent.releaseTime,
2315
- time: child.time ?? parent.time,
2316
- minimumLauncherVersion: child.minimumLauncherVersion ?? parent.minimumLauncherVersion,
2317
- complianceLevel: child.complianceLevel ?? parent.complianceLevel
3128
+ ...args !== void 0 ? { arguments: args } : {},
3129
+ ...minecraftArguments !== void 0 ? { minecraftArguments } : {},
3130
+ ...javaVersion !== void 0 ? { javaVersion } : {},
3131
+ ...logging !== void 0 ? { logging } : {},
3132
+ ...inheritsFrom !== void 0 ? { inheritsFrom } : {},
3133
+ ...releaseTime !== void 0 ? { releaseTime } : {},
3134
+ ...time !== void 0 ? { time } : {},
3135
+ ...minimumLauncherVersion !== void 0 ? { minimumLauncherVersion } : {},
3136
+ ...complianceLevel !== void 0 ? { complianceLevel } : {}
2318
3137
  };
2319
- return merged;
2320
- }
2321
- function libraryDedupeKey(library) {
3138
+ };
3139
+ var libraryDedupeKey = (library) => {
2322
3140
  if (!library.name) return null;
2323
3141
  try {
2324
3142
  const coord = parseMavenCoordinate(library.name);
@@ -2327,8 +3145,8 @@ function libraryDedupeKey(library) {
2327
3145
  } catch {
2328
3146
  return null;
2329
3147
  }
2330
- }
2331
- function mergeLibraries(parent, child) {
3148
+ };
3149
+ var mergeLibraries = (parent, child) => {
2332
3150
  const byKey = /* @__PURE__ */ new Map();
2333
3151
  const unkeyed = [];
2334
3152
  for (const lib of [...parent, ...child]) {
@@ -2340,8 +3158,8 @@ function mergeLibraries(parent, child) {
2340
3158
  byKey.set(key, lib);
2341
3159
  }
2342
3160
  return [...byKey.values(), ...unkeyed];
2343
- }
2344
- function mergeArguments(parent, child) {
3161
+ };
3162
+ var mergeArguments = (parent, child) => {
2345
3163
  if (!parent && !child) return void 0;
2346
3164
  const parentGame = parent?.game ?? [];
2347
3165
  const parentJvm = parent?.jvm ?? [];
@@ -2351,10 +3169,10 @@ function mergeArguments(parent, child) {
2351
3169
  game: [...parentGame, ...childGame],
2352
3170
  jvm: [...parentJvm, ...childJvm]
2353
3171
  };
2354
- }
3172
+ };
2355
3173
 
2356
3174
  // src/launch/version-resolution.ts
2357
- async function resolveLaunchVersion(target) {
3175
+ var resolveLaunchVersion = async (target) => {
2358
3176
  if (target.loader.type === Loaders.VANILLA) {
2359
3177
  return {
2360
3178
  versionId: target.minecraft.version,
@@ -2365,8 +3183,8 @@ async function resolveLaunchVersion(target) {
2365
3183
  const versionId = await pickInstalledVersionId(target);
2366
3184
  const merged = await loadAndMerge(target.directory, versionId, target.minecraft.manifest);
2367
3185
  return { versionId, merged, chain: [versionId, target.minecraft.version] };
2368
- }
2369
- async function pickClientJarVersionId(directory, chain) {
3186
+ };
3187
+ var pickClientJarVersionId = async (directory, chain) => {
2370
3188
  for (const id of chain) {
2371
3189
  const jar = targetPaths.versionJar(directory, id);
2372
3190
  if (await fileExists(jar)) return id;
@@ -2374,13 +3192,13 @@ async function pickClientJarVersionId(directory, chain) {
2374
3192
  const fallback = chain.at(-1);
2375
3193
  if (fallback === void 0) {
2376
3194
  throw new MinecraftKitError(
2377
- "MANIFEST_NOT_FOUND",
3195
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
2378
3196
  "Cannot resolve a client jar version id from an empty inheritsFrom chain"
2379
3197
  );
2380
3198
  }
2381
3199
  return fallback;
2382
- }
2383
- async function pickInstalledVersionId(target) {
3200
+ };
3201
+ var pickInstalledVersionId = async (target) => {
2384
3202
  if (target.loader.type === Loaders.FABRIC) {
2385
3203
  const candidate = target.loader.profile.id;
2386
3204
  const versionJsonPath = targetPaths.versionJson(target.directory, candidate);
@@ -2392,46 +3210,38 @@ async function pickInstalledVersionId(target) {
2392
3210
  const versionJsonPath = targetPaths.versionJson(target.directory, id);
2393
3211
  if (!await fileExists(versionJsonPath)) continue;
2394
3212
  const text = await readText(versionJsonPath);
2395
- try {
2396
- const parsed = JSON.parse(text);
2397
- if (parsed.inheritsFrom === target.minecraft.version && (id.includes("forge") || (parsed.id ?? "").includes("forge"))) {
2398
- return id;
2399
- }
2400
- } catch {
3213
+ const parsed = parseJsonOrUndefined(text);
3214
+ if (parsed?.inheritsFrom === target.minecraft.version && (id.includes("forge") || (parsed.id ?? "").includes("forge"))) {
3215
+ return id;
2401
3216
  }
2402
3217
  }
2403
3218
  }
2404
3219
  throw new MinecraftKitError(
2405
- "MANIFEST_NOT_FOUND",
3220
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
2406
3221
  `Could not find an installed version JSON for target ${target.id}`,
2407
3222
  { context: { targetId: target.id, loaderType: target.loader.type } }
2408
3223
  );
2409
- }
2410
- async function loadAndMerge(directory, versionId, parentManifest) {
3224
+ };
3225
+ var loadAndMerge = async (directory, versionId, parentManifest) => {
2411
3226
  const versionJsonPath = targetPaths.versionJson(directory, versionId);
2412
3227
  const text = await readText(versionJsonPath);
2413
- let child;
2414
- try {
2415
- child = JSON.parse(text);
2416
- } catch (cause) {
2417
- throw new MinecraftKitError(
2418
- "MANIFEST_INVALID",
2419
- `Version JSON is not valid JSON: ${versionJsonPath}`,
2420
- { cause, context: { filePath: versionJsonPath } }
2421
- );
2422
- }
3228
+ const child = parseJsonStrict(text, {
3229
+ code: MinecraftKitErrorCodes.MANIFEST_INVALID,
3230
+ message: `Version JSON is not valid JSON: ${versionJsonPath}`,
3231
+ context: { filePath: versionJsonPath }
3232
+ });
2423
3233
  if (child.inheritsFrom !== void 0 && child.inheritsFrom !== parentManifest.id) {
2424
3234
  return mergeManifest(parentManifest, child);
2425
3235
  }
2426
3236
  return mergeManifest(parentManifest, child);
2427
- }
3237
+ };
2428
3238
 
2429
3239
  // src/launch/compose.ts
2430
- async function composeLaunch(input) {
3240
+ var composeLaunch = async (input) => {
2431
3241
  const { target, options } = input;
2432
3242
  if (!options.auth.username || options.auth.username.length === 0) {
2433
3243
  throw new MinecraftKitError(
2434
- "INVALID_INPUT",
3244
+ MinecraftKitErrorCodes.INVALID_INPUT,
2435
3245
  `Auth username must be non-empty (target ${target.id})`,
2436
3246
  { context: { targetId: target.id } }
2437
3247
  );
@@ -2478,22 +3288,22 @@ async function composeLaunch(input) {
2478
3288
  auth: options.auth,
2479
3289
  workingDirectory: target.directory
2480
3290
  };
2481
- }
2482
- function buildFeatures(options) {
3291
+ };
3292
+ var buildFeatures = (options) => {
2483
3293
  const features = { ...options.features ?? {} };
2484
3294
  if (options.resolution !== void 0) {
2485
3295
  features.has_custom_resolution = true;
2486
3296
  }
2487
3297
  return features;
2488
- }
3298
+ };
2489
3299
 
2490
3300
  // src/launch/runner.ts
2491
- function runLaunch(input) {
3301
+ var runLaunch = (input) => {
2492
3302
  const composition = input.composition;
2493
3303
  const options = input.options ?? {};
2494
3304
  const args = [...composition.jvmArgs, composition.mainClass, ...composition.gameArgs];
2495
3305
  options.onEvent?.({
2496
- type: "launch:starting",
3306
+ type: EventTypes.LAUNCH_STARTING,
2497
3307
  command: composition.javaPath,
2498
3308
  args,
2499
3309
  cwd: composition.workingDirectory
@@ -2502,19 +3312,19 @@ function runLaunch(input) {
2502
3312
  cwd: composition.workingDirectory,
2503
3313
  ...composition.env !== void 0 ? { env: composition.env } : {}
2504
3314
  });
2505
- options.onEvent?.({ type: "launch:started", pid: child.pid });
3315
+ options.onEvent?.({ type: EventTypes.LAUNCH_STARTED, pid: child.pid });
2506
3316
  child.stdout.on("data", (line) => {
2507
- options.onEvent?.({ type: "launch:stdout", line });
3317
+ options.onEvent?.({ type: EventTypes.LAUNCH_STDOUT, line });
2508
3318
  });
2509
3319
  child.stderr.on("data", (line) => {
2510
- options.onEvent?.({ type: "launch:stderr", line });
3320
+ options.onEvent?.({ type: EventTypes.LAUNCH_STDERR, line });
2511
3321
  });
2512
3322
  const grace = options.killGracePeriodMs ?? DEFAULT_KILL_GRACE_MS;
2513
3323
  let aborted = false;
2514
3324
  const doAbort = (reason) => {
2515
3325
  if (aborted) return;
2516
3326
  aborted = true;
2517
- options.onEvent?.({ type: "launch:aborted", reason });
3327
+ options.onEvent?.({ type: EventTypes.LAUNCH_ABORTED, reason });
2518
3328
  child.kill("SIGTERM");
2519
3329
  setTimeout(() => child.kill("SIGKILL"), grace).unref();
2520
3330
  };
@@ -2529,10 +3339,10 @@ function runLaunch(input) {
2529
3339
  }
2530
3340
  const exited = (async () => {
2531
3341
  const { code, signal } = await child.exited;
2532
- options.onEvent?.({ type: "launch:exited", code, signal });
3342
+ options.onEvent?.({ type: EventTypes.LAUNCH_EXITED, code, signal });
2533
3343
  if (!aborted && code !== 0 && code !== null) {
2534
3344
  throw new MinecraftKitError(
2535
- "LAUNCH_PROCESS_FAILED",
3345
+ MinecraftKitErrorCodes.LAUNCH_PROCESS_FAILED,
2536
3346
  `Minecraft process exited with code ${code}`,
2537
3347
  { context: { exitCode: code } }
2538
3348
  );
@@ -2546,13 +3356,13 @@ function runLaunch(input) {
2546
3356
  doAbort(reason ?? "user");
2547
3357
  }
2548
3358
  };
2549
- }
2550
- function reasonFrom(value) {
3359
+ };
3360
+ var reasonFrom = (value) => {
2551
3361
  if (value === void 0) return "aborted";
2552
3362
  if (typeof value === "string") return value;
2553
3363
  if (value instanceof Error) return value.message;
2554
3364
  return String(value);
2555
- }
3365
+ };
2556
3366
  var ChildProcessSpawner = class {
2557
3367
  spawn(command, args, options) {
2558
3368
  const child = spawn(command, [...args], {
@@ -2576,14 +3386,14 @@ var ChildProcessSpawner = class {
2576
3386
  };
2577
3387
  }
2578
3388
  };
2579
- function streamFromBuffer(stream) {
3389
+ var streamFromBuffer = (stream) => {
2580
3390
  if (!stream) {
2581
3391
  return { on() {
2582
3392
  } };
2583
3393
  }
2584
3394
  let buffer = "";
2585
3395
  const listeners = /* @__PURE__ */ new Set();
2586
- const emit = (line) => {
3396
+ const emit2 = (line) => {
2587
3397
  for (const listener of listeners) listener(line);
2588
3398
  };
2589
3399
  stream.on("data", (chunk) => {
@@ -2593,35 +3403,36 @@ function streamFromBuffer(stream) {
2593
3403
  while (index !== -1) {
2594
3404
  const line = buffer.slice(0, index).replace(/\r$/, "");
2595
3405
  buffer = buffer.slice(index + 1);
2596
- emitBounded(emit, line);
3406
+ emitBounded(emit2, line);
2597
3407
  index = buffer.indexOf("\n");
2598
3408
  }
2599
3409
  while (buffer.length > SPAWNER_MAX_LINE_BYTES) {
2600
- emit(buffer.slice(0, SPAWNER_MAX_LINE_BYTES));
3410
+ emit2(buffer.slice(0, SPAWNER_MAX_LINE_BYTES));
2601
3411
  buffer = buffer.slice(SPAWNER_MAX_LINE_BYTES);
2602
3412
  }
2603
3413
  });
2604
3414
  stream.on("end", () => {
2605
3415
  if (buffer.length > 0) {
2606
- emitBounded(emit, buffer);
3416
+ emitBounded(emit2, buffer);
2607
3417
  buffer = "";
2608
3418
  }
3419
+ listeners.clear();
2609
3420
  });
2610
3421
  return {
2611
3422
  on(_event, listener) {
2612
3423
  listeners.add(listener);
2613
3424
  }
2614
3425
  };
2615
- }
2616
- function emitBounded(emit, line) {
3426
+ };
3427
+ var emitBounded = (emit2, line) => {
2617
3428
  if (line.length <= SPAWNER_MAX_LINE_BYTES) {
2618
- emit(line);
3429
+ emit2(line);
2619
3430
  return;
2620
3431
  }
2621
3432
  for (let i = 0; i < line.length; i += SPAWNER_MAX_LINE_BYTES) {
2622
- emit(line.slice(i, i + SPAWNER_MAX_LINE_BYTES));
3433
+ emit2(line.slice(i, i + SPAWNER_MAX_LINE_BYTES));
2623
3434
  }
2624
- }
3435
+ };
2625
3436
 
2626
3437
  // src/types/verify.ts
2627
3438
  var VerificationKinds = {
@@ -2646,24 +3457,14 @@ var VerifyFileCategories = {
2646
3457
  RUNTIME_FILE: "runtime-file",
2647
3458
  LOGGING_CONFIG: "logging-config"
2648
3459
  };
2649
- async function sha1OfFile(filePath) {
2650
- const hash = crypto2.createHash("sha1");
2651
- await new Promise((resolve, reject) => {
2652
- const stream = createReadStream(filePath);
2653
- stream.on("data", (chunk) => hash.update(chunk));
2654
- stream.on("end", resolve);
2655
- stream.on("error", reject);
2656
- });
2657
- return hash.digest("hex");
2658
- }
2659
3460
 
2660
3461
  // src/verify/helpers.ts
2661
- async function runVerification(input, check) {
3462
+ var runVerification = async (input, check) => {
2662
3463
  const startedAt = Date.now();
2663
3464
  const results = [];
2664
3465
  const record = (result) => {
2665
3466
  results.push(result);
2666
- input.onEvent?.({ type: "verify:file-checked", file: result });
3467
+ input.onEvent?.({ type: EventTypes.VERIFY_FILE_CHECKED, file: result });
2667
3468
  };
2668
3469
  await check(record);
2669
3470
  return {
@@ -2674,8 +3475,8 @@ async function runVerification(input, check) {
2674
3475
  checkedFiles: results.length,
2675
3476
  durationMs: Date.now() - startedAt
2676
3477
  };
2677
- }
2678
- async function verifyHashedFile(input) {
3478
+ };
3479
+ var verifyHashedFile = async (input) => {
2679
3480
  if (!await fileExists(input.path)) {
2680
3481
  return {
2681
3482
  path: input.path,
@@ -2722,8 +3523,8 @@ async function verifyHashedFile(input) {
2722
3523
  ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2723
3524
  ...input.url !== void 0 ? { url: input.url } : {}
2724
3525
  };
2725
- }
2726
- async function verifyExistence(input) {
3526
+ };
3527
+ var verifyExistence = async (input) => {
2727
3528
  if (await fileExists(input.path)) {
2728
3529
  return {
2729
3530
  path: input.path,
@@ -2738,8 +3539,8 @@ async function verifyExistence(input) {
2738
3539
  status: VerifyFileStatuses.MISSING,
2739
3540
  ...input.url !== void 0 ? { url: input.url } : {}
2740
3541
  };
2741
- }
2742
- async function findForgeVersionJsonPath(directory, minecraftVersion) {
3542
+ };
3543
+ var findForgeVersionJsonPath = async (directory, minecraftVersion) => {
2743
3544
  const versionsDir = targetPaths.versionsDir(directory);
2744
3545
  const dirs = await listChildDirectories(versionsDir);
2745
3546
  for (const id of dirs) {
@@ -2752,21 +3553,17 @@ async function findForgeVersionJsonPath(directory, minecraftVersion) {
2752
3553
  if (parsed === minecraftVersion) return jsonPath;
2753
3554
  }
2754
3555
  return null;
2755
- }
2756
- async function tryParseInheritsFrom(jsonPath) {
2757
- try {
2758
- const parsed = JSON.parse(await readText(jsonPath));
2759
- return parsed.inheritsFrom;
2760
- } catch {
2761
- return void 0;
2762
- }
2763
- }
3556
+ };
3557
+ var tryParseInheritsFrom = async (jsonPath) => {
3558
+ const parsed = parseJsonOrUndefined(await readText(jsonPath));
3559
+ return parsed?.inheritsFrom;
3560
+ };
2764
3561
 
2765
3562
  // src/verify/fabric.ts
2766
- async function verifyFabric(input) {
3563
+ var verifyFabric = async (input) => {
2767
3564
  if (input.target.loader.type !== Loaders.FABRIC) {
2768
3565
  throw new MinecraftKitError(
2769
- "INVALID_INPUT",
3566
+ MinecraftKitErrorCodes.INVALID_INPUT,
2770
3567
  `verify.fabric requires a Fabric target (got ${input.target.loader.type})`
2771
3568
  );
2772
3569
  }
@@ -2789,14 +3586,14 @@ async function verifyFabric(input) {
2789
3586
  directory: input.target.directory,
2790
3587
  system: input.target.runtime.system,
2791
3588
  versionId: input.target.minecraft.version,
2792
- category: "fabric-library"
3589
+ category: DownloadCategories.FABRIC_LIBRARY
2793
3590
  });
2794
3591
  for (const action of fabricLibraries.downloads) {
2795
3592
  record(
2796
3593
  await verifyHashedFile({
2797
3594
  path: action.target,
2798
- expectedSha1: action.expectedSha1,
2799
- expectedSize: action.expectedSize,
3595
+ ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
3596
+ ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
2800
3597
  ...action.url ? { url: action.url } : {},
2801
3598
  category: VerifyFileCategories.LOADER_LIBRARY
2802
3599
  })
@@ -2804,13 +3601,13 @@ async function verifyFabric(input) {
2804
3601
  }
2805
3602
  }
2806
3603
  );
2807
- }
3604
+ };
2808
3605
 
2809
3606
  // src/verify/forge.ts
2810
- async function verifyForge(input) {
3607
+ var verifyForge = async (input) => {
2811
3608
  if (input.target.loader.type !== Loaders.FORGE) {
2812
3609
  throw new MinecraftKitError(
2813
- "INVALID_INPUT",
3610
+ MinecraftKitErrorCodes.INVALID_INPUT,
2814
3611
  `verify.forge requires a Forge target (got ${input.target.loader.type})`
2815
3612
  );
2816
3613
  }
@@ -2833,10 +3630,8 @@ async function verifyForge(input) {
2833
3630
  })
2834
3631
  );
2835
3632
  if (!await fileExists(forgeVersionJsonPath)) return;
2836
- let parsed;
2837
- try {
2838
- parsed = JSON.parse(await readText(forgeVersionJsonPath));
2839
- } catch {
3633
+ const parsed = parseJsonOrUndefined(await readText(forgeVersionJsonPath));
3634
+ if (parsed === void 0) {
2840
3635
  record({
2841
3636
  path: forgeVersionJsonPath,
2842
3637
  category: VerifyFileCategories.LOADER_LIBRARY,
@@ -2849,14 +3644,14 @@ async function verifyForge(input) {
2849
3644
  directory: input.target.directory,
2850
3645
  system: input.target.runtime.system,
2851
3646
  versionId: input.target.minecraft.version,
2852
- category: "forge-library"
3647
+ category: DownloadCategories.FORGE_LIBRARY
2853
3648
  });
2854
3649
  for (const action of forgeLibraries.downloads) {
2855
3650
  record(
2856
3651
  await verifyHashedFile({
2857
3652
  path: action.target,
2858
- expectedSha1: action.expectedSha1,
2859
- expectedSize: action.expectedSize,
3653
+ ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
3654
+ ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
2860
3655
  ...action.url ? { url: action.url } : {},
2861
3656
  category: VerifyFileCategories.LOADER_LIBRARY
2862
3657
  })
@@ -2864,10 +3659,10 @@ async function verifyForge(input) {
2864
3659
  }
2865
3660
  }
2866
3661
  );
2867
- }
3662
+ };
2868
3663
 
2869
3664
  // src/verify/minecraft.ts
2870
- async function verifyMinecraft(input) {
3665
+ var verifyMinecraft = async (input) => {
2871
3666
  return runVerification(
2872
3667
  {
2873
3668
  targetId: input.target.id,
@@ -2908,14 +3703,14 @@ async function verifyMinecraft(input) {
2908
3703
  directory,
2909
3704
  system: runtime.system,
2910
3705
  versionId: minecraft.version,
2911
- category: "library"
3706
+ category: DownloadCategories.LIBRARY
2912
3707
  });
2913
3708
  for (const action of libraryPlan.downloads) {
2914
3709
  record(
2915
3710
  await verifyHashedFile({
2916
3711
  path: action.target,
2917
- expectedSha1: action.expectedSha1,
2918
- expectedSize: action.expectedSize,
3712
+ ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
3713
+ ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
2919
3714
  url: action.url,
2920
3715
  category: VerifyFileCategories.LIBRARY
2921
3716
  })
@@ -2962,8 +3757,8 @@ async function verifyMinecraft(input) {
2962
3757
  }
2963
3758
  }
2964
3759
  );
2965
- }
2966
- async function verifyRuntime(input) {
3760
+ };
3761
+ var verifyRuntime = async (input) => {
2967
3762
  return runVerification(
2968
3763
  {
2969
3764
  targetId: input.target.id,
@@ -3005,13 +3800,13 @@ async function verifyRuntime(input) {
3005
3800
  }
3006
3801
  }
3007
3802
  );
3008
- }
3803
+ };
3009
3804
 
3010
3805
  // src/repair/helpers.ts
3011
- function asResultArray(from) {
3806
+ var asResultArray = (from) => {
3012
3807
  return Array.isArray(from) ? from : [from];
3013
- }
3014
- function buildIssueIndex(from) {
3808
+ };
3809
+ var buildIssueIndex = (from) => {
3015
3810
  const map = /* @__PURE__ */ new Map();
3016
3811
  for (const v of asResultArray(from)) {
3017
3812
  for (const issue of v.issues) {
@@ -3032,16 +3827,16 @@ function buildIssueIndex(from) {
3032
3827
  },
3033
3828
  categoriesAt: (path13) => map.get(path13) ?? /* @__PURE__ */ new Set()
3034
3829
  };
3035
- }
3036
- function sumDownloadBytes(actions) {
3830
+ };
3831
+ var sumDownloadBytes = (actions) => {
3037
3832
  return actions.reduce((sum, action) => {
3038
3833
  if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3039
3834
  return sum + (action.expectedSize ?? 0);
3040
3835
  }
3041
3836
  return sum;
3042
3837
  }, 0);
3043
- }
3044
- function buildRepairPlan(target, actions) {
3838
+ };
3839
+ var buildRepairPlan = (target, actions) => {
3045
3840
  return {
3046
3841
  targetId: target.id,
3047
3842
  directory: target.directory,
@@ -3050,8 +3845,8 @@ function buildRepairPlan(target, actions) {
3050
3845
  totalActions: actions.length,
3051
3846
  totalBytes: sumDownloadBytes(actions)
3052
3847
  };
3053
- }
3054
- async function planAspectRepair(input, aspectFilter, postprocess) {
3848
+ };
3849
+ var planAspectRepair = async (input, aspectFilter, postprocess) => {
3055
3850
  const installPlan = await planInstall({
3056
3851
  target: input.target,
3057
3852
  http: input.http,
@@ -3067,8 +3862,8 @@ async function planAspectRepair(input, aspectFilter, postprocess) {
3067
3862
  });
3068
3863
  postprocess?.({ actions, installPlan, issues });
3069
3864
  return buildRepairPlan(input.target, actions);
3070
- }
3071
- function selectRepairActions(input) {
3865
+ };
3866
+ var selectRepairActions = (input) => {
3072
3867
  const matching = [];
3073
3868
  for (const action of input.installPlan.actions) {
3074
3869
  if (!input.aspectFilter(action)) continue;
@@ -3089,13 +3884,13 @@ function selectRepairActions(input) {
3089
3884
  }
3090
3885
  }
3091
3886
  return matching;
3092
- }
3887
+ };
3093
3888
 
3094
3889
  // src/repair/fabric.ts
3095
- async function planFabricRepair(input) {
3890
+ var planFabricRepair = async (input) => {
3096
3891
  if (input.target.loader.type !== Loaders.FABRIC) {
3097
3892
  throw new MinecraftKitError(
3098
- "INVALID_INPUT",
3893
+ MinecraftKitErrorCodes.INVALID_INPUT,
3099
3894
  `repair.fabric requires a Fabric target (got ${input.target.loader.type})`
3100
3895
  );
3101
3896
  }
@@ -3105,24 +3900,24 @@ async function planFabricRepair(input) {
3105
3900
  );
3106
3901
  return planAspectRepair(input, (action) => {
3107
3902
  if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3108
- return action.category === "fabric-library";
3903
+ return action.category === DownloadCategories.FABRIC_LIBRARY;
3109
3904
  }
3110
3905
  if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
3111
3906
  return action.path === fabricJsonPath;
3112
3907
  }
3113
3908
  return false;
3114
3909
  });
3115
- }
3910
+ };
3116
3911
 
3117
3912
  // src/repair/forge.ts
3118
3913
  var FORGE_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
3119
- "forge-library",
3120
- "forge-installer"
3914
+ DownloadCategories.FORGE_LIBRARY,
3915
+ DownloadCategories.FORGE_INSTALLER
3121
3916
  ]);
3122
- async function planForgeRepair(input) {
3917
+ var planForgeRepair = async (input) => {
3123
3918
  if (input.target.loader.type !== Loaders.FORGE) {
3124
3919
  throw new MinecraftKitError(
3125
- "INVALID_INPUT",
3920
+ MinecraftKitErrorCodes.INVALID_INPUT,
3126
3921
  `repair.forge requires a Forge target (got ${input.target.loader.type})`
3127
3922
  );
3128
3923
  }
@@ -3147,7 +3942,7 @@ async function planForgeRepair(input) {
3147
3942
  actions.filter((a) => a.kind === InstallActionKinds.DOWNLOAD_FILE).map((a) => a.target)
3148
3943
  );
3149
3944
  for (const action of installPlan.actions) {
3150
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "forge-library" && !alreadyIncluded.has(action.target)) {
3945
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === DownloadCategories.FORGE_LIBRARY && !alreadyIncluded.has(action.target)) {
3151
3946
  actions.push(action);
3152
3947
  } else if (action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR) {
3153
3948
  actions.push(action);
@@ -3155,17 +3950,17 @@ async function planForgeRepair(input) {
3155
3950
  }
3156
3951
  }
3157
3952
  );
3158
- }
3953
+ };
3159
3954
 
3160
3955
  // src/repair/minecraft.ts
3161
3956
  var MINECRAFT_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
3162
- "client-jar",
3163
- "library",
3164
- "asset-index",
3165
- "asset",
3166
- "logging-config"
3957
+ DownloadCategories.CLIENT_JAR,
3958
+ DownloadCategories.LIBRARY,
3959
+ DownloadCategories.ASSET_INDEX,
3960
+ DownloadCategories.ASSET,
3961
+ DownloadCategories.LOGGING_CONFIG
3167
3962
  ]);
3168
- async function planMinecraftRepair(input) {
3963
+ var planMinecraftRepair = async (input) => {
3169
3964
  const vanillaJsonPath = targetPaths.versionJson(
3170
3965
  input.target.directory,
3171
3966
  input.target.minecraft.version
@@ -3182,10 +3977,10 @@ async function planMinecraftRepair(input) {
3182
3977
  }
3183
3978
  return false;
3184
3979
  });
3185
- }
3980
+ };
3186
3981
 
3187
3982
  // src/repair/runner.ts
3188
- async function runRepair(input) {
3983
+ var runRepair = async (input) => {
3189
3984
  const report = await runInstall({
3190
3985
  plan: {
3191
3986
  ...input.plan,
@@ -3204,18 +3999,18 @@ async function runRepair(input) {
3204
3999
  actionsCompleted: report.actionsCompleted,
3205
4000
  durationMs: report.durationMs
3206
4001
  };
3207
- }
4002
+ };
3208
4003
 
3209
4004
  // src/repair/runtime.ts
3210
- async function planRuntimeRepair(input) {
4005
+ var planRuntimeRepair = async (input) => {
3211
4006
  return planAspectRepair(
3212
4007
  input,
3213
- (action) => action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "runtime-file"
4008
+ (action) => action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === DownloadCategories.RUNTIME_FILE
3214
4009
  );
3215
- }
4010
+ };
3216
4011
 
3217
4012
  // src/repair/all.ts
3218
- async function repairAll(input) {
4013
+ var repairAll = async (input) => {
3219
4014
  const startedAt = Date.now();
3220
4015
  const ctx = {
3221
4016
  target: input.target,
@@ -3258,7 +4053,7 @@ async function repairAll(input) {
3258
4053
  bytesDownloaded,
3259
4054
  durationMs: Date.now() - startedAt
3260
4055
  };
3261
- }
4056
+ };
3262
4057
  var PLANNERS = {
3263
4058
  minecraft: planMinecraftRepair,
3264
4059
  runtime: planRuntimeRepair,
@@ -3297,14 +4092,27 @@ var TargetsApi = class {
3297
4092
  /** Build a {@link Target} from already-resolved components. */
3298
4093
  create(input) {
3299
4094
  if (!input.id) {
3300
- throw new MinecraftKitError("INVALID_INPUT", "Target id must be non-empty");
4095
+ throw new MinecraftKitError(
4096
+ MinecraftKitErrorCodes.INVALID_INPUT,
4097
+ "Target id must be non-empty"
4098
+ );
3301
4099
  }
3302
4100
  if (!input.directory) {
3303
- throw new MinecraftKitError("INVALID_INPUT", "Target directory must be non-empty");
4101
+ throw new MinecraftKitError(
4102
+ MinecraftKitErrorCodes.INVALID_INPUT,
4103
+ "Target directory must be non-empty"
4104
+ );
4105
+ }
4106
+ if (!path.isAbsolute(input.directory)) {
4107
+ throw new MinecraftKitError(
4108
+ MinecraftKitErrorCodes.INVALID_INPUT,
4109
+ `Target directory must be an absolute path, got: ${input.directory}`,
4110
+ { context: { directory: input.directory } }
4111
+ );
3304
4112
  }
3305
4113
  if (input.loader.minecraftVersion !== input.minecraft.version) {
3306
4114
  throw new MinecraftKitError(
3307
- "INVALID_INPUT",
4115
+ MinecraftKitErrorCodes.INVALID_INPUT,
3308
4116
  `Loader Minecraft version (${input.loader.minecraftVersion}) does not match resolved Minecraft (${input.minecraft.version})`,
3309
4117
  {
3310
4118
  context: {
@@ -3381,7 +4189,7 @@ var TargetsApi = class {
3381
4189
  return results;
3382
4190
  }
3383
4191
  };
3384
- async function discoverInstallation(id, directory) {
4192
+ var discoverInstallation = async (id, directory) => {
3385
4193
  const versionsDir = path.join(directory, VERSIONS_DIR);
3386
4194
  const librariesDir = path.join(directory, LIBRARIES_DIR);
3387
4195
  const assetsDir = path.join(directory, ASSETS_DIR);
@@ -3403,8 +4211,8 @@ async function discoverInstallation(id, directory) {
3403
4211
  }
3404
4212
  const runtime = await discoverRuntime(directory);
3405
4213
  return { id, directory, minecraftVersions, loaders, ...runtime ? { runtime } : {} };
3406
- }
3407
- async function discoverRuntime(directory) {
4214
+ };
4215
+ var discoverRuntime = async (directory) => {
3408
4216
  const runtimeDir = path.join(directory, RUNTIMES_DIR);
3409
4217
  if (!await dirExists(runtimeDir)) return void 0;
3410
4218
  let components;
@@ -3415,14 +4223,14 @@ async function discoverRuntime(directory) {
3415
4223
  }
3416
4224
  for (const component of components) {
3417
4225
  const root = path.join(runtimeDir, component);
3418
- const javaPath = process.platform === "win32" ? path.join(root, "bin", "javaw.exe") : process.platform === "darwin" ? path.join(root, "jre.bundle", "Contents", "Home", "bin", "java") : path.join(root, "bin", "java");
4226
+ const javaPath = javaExecutablePath(root);
3419
4227
  if (await fileExists(javaPath)) {
3420
4228
  return { component, javaPath };
3421
4229
  }
3422
4230
  }
3423
4231
  return void 0;
3424
- }
3425
- function inferLoaderFromVersionId(versionId) {
4232
+ };
4233
+ var inferLoaderFromVersionId = (versionId) => {
3426
4234
  const fabricMatch = /^fabric-loader-([^-]+)-(.+)$/.exec(versionId);
3427
4235
  if (fabricMatch?.[1] && fabricMatch[2]) {
3428
4236
  return { type: Loaders.FABRIC, version: fabricMatch[1], minecraftVersion: fabricMatch[2] };
@@ -3432,18 +4240,25 @@ function inferLoaderFromVersionId(versionId) {
3432
4240
  return { type: Loaders.FORGE, minecraftVersion: forgeMatch[1], version: forgeMatch[2] };
3433
4241
  }
3434
4242
  return null;
3435
- }
4243
+ };
4244
+ var javaExecutablePath = (runtimeRoot) => {
4245
+ if (process.platform === "win32") return path.join(runtimeRoot, "bin", "javaw.exe");
4246
+ if (process.platform === "darwin") {
4247
+ return path.join(runtimeRoot, "jre.bundle", "Contents", "Home", "bin", "java");
4248
+ }
4249
+ return path.join(runtimeRoot, "bin", "java");
4250
+ };
3436
4251
 
3437
4252
  // src/update/runner.ts
3438
- async function planUpdate(input) {
4253
+ var planUpdate = async (input) => {
3439
4254
  return planInstall({
3440
4255
  target: input.target,
3441
4256
  http: input.http,
3442
4257
  cache: input.cache,
3443
4258
  ...input.signal !== void 0 ? { signal: input.signal } : {}
3444
4259
  });
3445
- }
3446
- async function runUpdate(input) {
4260
+ };
4261
+ var runUpdate = async (input) => {
3447
4262
  const report = await runInstall({
3448
4263
  plan: input.plan,
3449
4264
  http: input.http,
@@ -3459,7 +4274,7 @@ async function runUpdate(input) {
3459
4274
  actionsSkipped: report.actionsSkipped,
3460
4275
  durationMs: report.durationMs
3461
4276
  };
3462
- }
4277
+ };
3463
4278
 
3464
4279
  // src/versions/fabric.ts
3465
4280
  var FabricVersionsApi = class {
@@ -3495,7 +4310,7 @@ var FabricVersionsApi = class {
3495
4310
  });
3496
4311
  if (loaders.length === 0) {
3497
4312
  throw new MinecraftKitError(
3498
- "MANIFEST_NOT_FOUND",
4313
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
3499
4314
  `No Fabric loader available for Minecraft ${input.minecraftVersion}`,
3500
4315
  { context: { version: input.minecraftVersion } }
3501
4316
  );
@@ -3503,9 +4318,11 @@ var FabricVersionsApi = class {
3503
4318
  const chosen = pickFabricLoader(loaders, input);
3504
4319
  if (!chosen) {
3505
4320
  throw new MinecraftKitError(
3506
- "MANIFEST_NOT_FOUND",
4321
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
3507
4322
  `Fabric loader version not found: ${input.loaderVersion ?? "(none matched)"}`,
3508
- { context: { version: input.loaderVersion } }
4323
+ {
4324
+ context: input.loaderVersion !== void 0 ? { version: input.loaderVersion } : {}
4325
+ }
3509
4326
  );
3510
4327
  }
3511
4328
  const profile = await fetchJson(this.ctx.http, this.ctx.cache, {
@@ -3521,7 +4338,7 @@ var FabricVersionsApi = class {
3521
4338
  };
3522
4339
  }
3523
4340
  };
3524
- function pickFabricLoader(loaders, input) {
4341
+ var pickFabricLoader = (loaders, input) => {
3525
4342
  if (input.loaderVersion !== void 0) {
3526
4343
  return loaders.find((l) => l.version === input.loaderVersion);
3527
4344
  }
@@ -3531,17 +4348,17 @@ function pickFabricLoader(loaders, input) {
3531
4348
  if (stable) return stable;
3532
4349
  }
3533
4350
  return loaders[0];
3534
- }
4351
+ };
3535
4352
 
3536
4353
  // src/core/xml.ts
3537
- function parseMavenMetadataVersions(xml) {
4354
+ var parseMavenMetadataVersions = (xml) => {
3538
4355
  const versions = [];
3539
4356
  const regex = /<version>\s*([^<]+?)\s*<\/version>/g;
3540
4357
  for (const match of xml.matchAll(regex)) {
3541
4358
  if (match[1]) versions.push(match[1]);
3542
4359
  }
3543
4360
  return versions;
3544
- }
4361
+ };
3545
4362
 
3546
4363
  // src/versions/forge.ts
3547
4364
  var ForgeVersionsApi = class {
@@ -3574,7 +4391,7 @@ var ForgeVersionsApi = class {
3574
4391
  });
3575
4392
  if (builds.length === 0) {
3576
4393
  throw new MinecraftKitError(
3577
- "MANIFEST_NOT_FOUND",
4394
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
3578
4395
  `No Forge build available for Minecraft ${input.minecraftVersion}`,
3579
4396
  { context: { version: input.minecraftVersion } }
3580
4397
  );
@@ -3582,9 +4399,11 @@ var ForgeVersionsApi = class {
3582
4399
  const chosen = pickForge(builds, input);
3583
4400
  if (!chosen) {
3584
4401
  throw new MinecraftKitError(
3585
- "MANIFEST_NOT_FOUND",
4402
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
3586
4403
  `Forge build not found for ${input.minecraftVersion}: ${input.forgeVersion ?? "(none matched)"}`,
3587
- { context: { version: input.forgeVersion } }
4404
+ {
4405
+ context: input.forgeVersion !== void 0 ? { version: input.forgeVersion } : {}
4406
+ }
3588
4407
  );
3589
4408
  }
3590
4409
  return {
@@ -3596,7 +4415,7 @@ var ForgeVersionsApi = class {
3596
4415
  };
3597
4416
  }
3598
4417
  };
3599
- function buildSummary(fullVersion, promotions) {
4418
+ var buildSummary = (fullVersion, promotions) => {
3600
4419
  const dashIndex = fullVersion.indexOf("-");
3601
4420
  if (dashIndex <= 0 || dashIndex === fullVersion.length - 1) return null;
3602
4421
  const minecraftVersion = fullVersion.slice(0, dashIndex);
@@ -3611,8 +4430,8 @@ function buildSummary(fullVersion, promotions) {
3611
4430
  isRecommended: recommended === forgeVersion,
3612
4431
  isLatest: latest === forgeVersion
3613
4432
  };
3614
- }
3615
- function pickForge(builds, input) {
4433
+ };
4434
+ var pickForge = (builds, input) => {
3616
4435
  if (input.forgeVersion !== void 0) {
3617
4436
  return builds.find(
3618
4437
  (b) => b.forgeVersion === input.forgeVersion || b.fullVersion === input.forgeVersion
@@ -3626,7 +4445,30 @@ function pickForge(builds, input) {
3626
4445
  const latest = builds.find((b) => b.isLatest);
3627
4446
  if (latest) return latest;
3628
4447
  return builds[builds.length - 1];
3629
- }
4448
+ };
4449
+
4450
+ // src/core/guards.ts
4451
+ var isPlainObject = (value) => {
4452
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4453
+ };
4454
+ var isNonEmptyString = (value) => {
4455
+ return typeof value === "string" && value.length > 0;
4456
+ };
4457
+ var isArtifactDownload = (value) => {
4458
+ if (!isPlainObject(value)) return false;
4459
+ return typeof value.sha1 === "string" && typeof value.size === "number" && isNonEmptyString(value.url);
4460
+ };
4461
+ var isMinecraftVersionManifestShape = (value) => {
4462
+ if (!isPlainObject(value)) return false;
4463
+ if (!isNonEmptyString(value.id)) return false;
4464
+ if (!isNonEmptyString(value.mainClass)) return false;
4465
+ if (!isPlainObject(value.assetIndex)) return false;
4466
+ if (!isNonEmptyString(value.assetIndex.id) || typeof value.assetIndex.sha1 !== "string" || typeof value.assetIndex.size !== "number" || !isNonEmptyString(value.assetIndex.url)) {
4467
+ return false;
4468
+ }
4469
+ if (!isPlainObject(value.downloads)) return false;
4470
+ return isArtifactDownload(value.downloads.client);
4471
+ };
3630
4472
 
3631
4473
  // src/versions/minecraft.ts
3632
4474
  var MinecraftVersionsApi = class {
@@ -3647,7 +4489,7 @@ var MinecraftVersionsApi = class {
3647
4489
  const summary = root.versions.find((v) => v.id === targetId);
3648
4490
  if (!summary) {
3649
4491
  throw new MinecraftKitError(
3650
- "MANIFEST_NOT_FOUND",
4492
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
3651
4493
  `Latest version ${targetId} not found in manifest`
3652
4494
  );
3653
4495
  }
@@ -3659,7 +4501,7 @@ var MinecraftVersionsApi = class {
3659
4501
  const summary = root.versions.find((v) => v.id === input.version);
3660
4502
  if (!summary) {
3661
4503
  throw new MinecraftKitError(
3662
- "MANIFEST_NOT_FOUND",
4504
+ MinecraftKitErrorCodes.MANIFEST_NOT_FOUND,
3663
4505
  `Minecraft version not found: ${input.version}`,
3664
4506
  { context: { version: input.version } }
3665
4507
  );
@@ -3669,18 +4511,19 @@ var MinecraftVersionsApi = class {
3669
4511
  /** Fetch and parse the per-version manifest in addition to the summary. */
3670
4512
  async resolve(input) {
3671
4513
  const summary = await this.get(input);
3672
- const manifest = await fetchJson(this.ctx.http, this.ctx.cache, {
4514
+ const raw = await fetchJson(this.ctx.http, this.ctx.cache, {
3673
4515
  url: summary.url,
3674
4516
  cacheKey: `minecraft-manifest:${summary.id}:${summary.sha1}`,
3675
4517
  ...input.signal !== void 0 ? { signal: input.signal } : {}
3676
4518
  });
3677
- if (!manifest.id || !manifest.mainClass) {
4519
+ if (!isMinecraftVersionManifestShape(raw)) {
3678
4520
  throw new MinecraftKitError(
3679
- "MANIFEST_INVALID",
3680
- `Per-version manifest is missing required fields: ${summary.id}`,
4521
+ MinecraftKitErrorCodes.MANIFEST_INVALID,
4522
+ `Per-version manifest does not match the expected shape: ${summary.id}`,
3681
4523
  { context: { version: summary.id, url: summary.url } }
3682
4524
  );
3683
4525
  }
4526
+ const manifest = raw;
3684
4527
  return {
3685
4528
  version: summary.id,
3686
4529
  channel: summary.type,
@@ -3733,7 +4576,7 @@ var RuntimeVersionsApi = class {
3733
4576
  const platform = index[platformKey];
3734
4577
  if (!platform) {
3735
4578
  throw new MinecraftKitError(
3736
- "RUNTIME_UNSUPPORTED_PLATFORM",
4579
+ MinecraftKitErrorCodes.RUNTIME_UNSUPPORTED_PLATFORM,
3737
4580
  `No runtimes published for platform: ${platformKey}`,
3738
4581
  { context: { platform: platformKey } }
3739
4582
  );
@@ -3750,7 +4593,7 @@ var RuntimeVersionsApi = class {
3750
4593
  }
3751
4594
  }
3752
4595
  throw new MinecraftKitError(
3753
- "RUNTIME_NOT_FOUND",
4596
+ MinecraftKitErrorCodes.RUNTIME_NOT_FOUND,
3754
4597
  `Runtime component ${component} not available on ${platformKey}`,
3755
4598
  { context: { platform: platformKey, version: component } }
3756
4599
  );
@@ -3758,7 +4601,7 @@ var RuntimeVersionsApi = class {
3758
4601
  const entry = candidates[0];
3759
4602
  if (!entry) {
3760
4603
  throw new MinecraftKitError(
3761
- "RUNTIME_NOT_FOUND",
4604
+ MinecraftKitErrorCodes.RUNTIME_NOT_FOUND,
3762
4605
  `Runtime component ${component} list is empty for ${platformKey}`,
3763
4606
  { context: { platform: platformKey, version: component } }
3764
4607
  );
@@ -3773,11 +4616,11 @@ var RuntimeVersionsApi = class {
3773
4616
  });
3774
4617
  }
3775
4618
  };
3776
- function pickPlatformKey(system) {
4619
+ var pickPlatformKey = (system) => {
3777
4620
  const archMap = RUNTIME_PLATFORM_KEYS[system.os];
3778
4621
  return archMap[system.arch];
3779
- }
3780
- function pickLatestAcrossComponents(entries) {
4622
+ };
4623
+ var pickLatestAcrossComponents = (entries) => {
3781
4624
  let bestComponent = null;
3782
4625
  let bestEntry = null;
3783
4626
  for (const [component, list] of entries) {
@@ -3790,8 +4633,8 @@ function pickLatestAcrossComponents(entries) {
3790
4633
  }
3791
4634
  if (!bestComponent || !bestEntry) return null;
3792
4635
  return { component: bestComponent, entry: bestEntry };
3793
- }
3794
- function toResolved(component, platformKey, entry, system) {
4636
+ };
4637
+ var toResolved = (component, platformKey, entry, system) => {
3795
4638
  const majorVersion = parseMajorVersion(entry.version.name);
3796
4639
  return {
3797
4640
  component,
@@ -3802,13 +4645,13 @@ function toResolved(component, platformKey, entry, system) {
3802
4645
  manifestUrl: entry.manifest.url,
3803
4646
  manifestSha1: entry.manifest.sha1
3804
4647
  };
3805
- }
3806
- function parseMajorVersion(versionName) {
4648
+ };
4649
+ var parseMajorVersion = (versionName) => {
3807
4650
  const match = /^(\d+)/.exec(versionName);
3808
4651
  if (!match || !match[1]) return void 0;
3809
4652
  const parsed = Number.parseInt(match[1], 10);
3810
4653
  return Number.isFinite(parsed) ? parsed : void 0;
3811
- }
4654
+ };
3812
4655
 
3813
4656
  // src/kit.ts
3814
4657
  var MinecraftKit = class {
@@ -3819,6 +4662,12 @@ var MinecraftKit = class {
3819
4662
  verify;
3820
4663
  repair;
3821
4664
  launch;
4665
+ /**
4666
+ * Microsoft / Mojang authentication. Implements the device-code flow against Microsoft
4667
+ * Entra, exchanges the resulting tokens for an XSTS + Minecraft session, and returns
4668
+ * everything needed to compose an `OnlineAuth` for `launch.compose`.
4669
+ */
4670
+ auth;
3822
4671
  /** Cache surface useful for advanced consumers (e.g. clearing between operations). */
3823
4672
  cache;
3824
4673
  constructor(options = {}) {
@@ -3834,6 +4683,7 @@ var MinecraftKit = class {
3834
4683
  const runtime = new RuntimeVersionsApi(ctx);
3835
4684
  this.versions = { minecraft, fabric, forge, runtime };
3836
4685
  this.targets = new TargetsApi({ minecraft, fabric, forge, runtime, system });
4686
+ this.auth = new MojangAuthApi(http);
3837
4687
  this.cache = cache;
3838
4688
  const carry = (opts) => ({
3839
4689
  ...opts?.signal !== void 0 ? { signal: opts.signal } : {},
@@ -3914,26 +4764,9 @@ var MinecraftKit = class {
3914
4764
  }
3915
4765
  };
3916
4766
 
3917
- // src/core/pause-controller.ts
3918
- var PauseController = class {
3919
- #paused = false;
3920
- #waiters = [];
3921
- get paused() {
3922
- return this.#paused;
3923
- }
3924
- pause() {
3925
- this.#paused = true;
3926
- }
3927
- resume() {
3928
- this.#paused = false;
3929
- const list = this.#waiters;
3930
- this.#waiters = [];
3931
- for (const resolve of list) resolve();
3932
- }
3933
- waitWhilePaused() {
3934
- if (!this.#paused) return Promise.resolve();
3935
- return new Promise((resolve) => this.#waiters.push(resolve));
3936
- }
4767
+ // src/core/assert-never.ts
4768
+ var assertNever = (value) => {
4769
+ throw new Error(`Unhandled variant: ${JSON.stringify(value)}`);
3937
4770
  };
3938
4771
 
3939
4772
  // src/install/progress-tracker.ts
@@ -3945,15 +4778,15 @@ var InstallStages = {
3945
4778
  FINALIZE: "finalize"
3946
4779
  };
3947
4780
  var STAGE_FOR_CATEGORY = {
3948
- "runtime-file": InstallStages.RUNTIME,
3949
- "client-jar": InstallStages.MINECRAFT,
3950
- library: InstallStages.MINECRAFT,
3951
- "asset-index": InstallStages.MINECRAFT,
3952
- asset: InstallStages.MINECRAFT,
3953
- "logging-config": InstallStages.MINECRAFT,
3954
- "fabric-library": InstallStages.LOADER,
3955
- "forge-library": InstallStages.LOADER,
3956
- "forge-installer": InstallStages.LOADER
4781
+ [DownloadCategories.RUNTIME_FILE]: InstallStages.RUNTIME,
4782
+ [DownloadCategories.CLIENT_JAR]: InstallStages.MINECRAFT,
4783
+ [DownloadCategories.LIBRARY]: InstallStages.MINECRAFT,
4784
+ [DownloadCategories.ASSET_INDEX]: InstallStages.MINECRAFT,
4785
+ [DownloadCategories.ASSET]: InstallStages.MINECRAFT,
4786
+ [DownloadCategories.LOGGING_CONFIG]: InstallStages.MINECRAFT,
4787
+ [DownloadCategories.FABRIC_LIBRARY]: InstallStages.LOADER,
4788
+ [DownloadCategories.FORGE_LIBRARY]: InstallStages.LOADER,
4789
+ [DownloadCategories.FORGE_INSTALLER]: InstallStages.LOADER
3957
4790
  };
3958
4791
  var STAGE_FOR_PHASE = {
3959
4792
  [InstallPhases.PLANNING]: InstallStages.PREPARE,
@@ -3977,7 +4810,7 @@ var ALL_STAGES = [
3977
4810
  InstallStages.LOADER,
3978
4811
  InstallStages.FINALIZE
3979
4812
  ];
3980
- function createInstallProgressTracker(plan, options = {}) {
4813
+ var createInstallProgressTracker = (plan, options = {}) => {
3981
4814
  const throttleMs = options.throttleMs ?? 100;
3982
4815
  const stageOfTarget = /* @__PURE__ */ new Map();
3983
4816
  const expectedSizeOf = /* @__PURE__ */ new Map();
@@ -4061,7 +4894,7 @@ function createInstallProgressTracker(plan, options = {}) {
4061
4894
  };
4062
4895
  const onEvent = (event) => {
4063
4896
  switch (event.type) {
4064
- case "install:phase-changed": {
4897
+ case EventTypes.INSTALL_PHASE_CHANGED: {
4065
4898
  const next = STAGE_FOR_PHASE[event.phase];
4066
4899
  if (next && next !== currentStage) {
4067
4900
  currentStage = next;
@@ -4070,14 +4903,14 @@ function createInstallProgressTracker(plan, options = {}) {
4070
4903
  }
4071
4904
  return;
4072
4905
  }
4073
- case "download:started": {
4906
+ case EventTypes.DOWNLOAD_STARTED: {
4074
4907
  const stage = stageOfTarget.get(event.file.target) ?? currentStage;
4075
4908
  inFlightByTarget.set(event.file.target, { stage, bytes: 0 });
4076
4909
  currentFile = event.file.target;
4077
4910
  schedulePush();
4078
4911
  return;
4079
4912
  }
4080
- case "download:progress": {
4913
+ case EventTypes.DOWNLOAD_PROGRESS: {
4081
4914
  const entry = inFlightByTarget.get(event.file.target);
4082
4915
  if (entry) {
4083
4916
  const delta = event.bytesDownloaded - entry.bytes;
@@ -4091,7 +4924,7 @@ function createInstallProgressTracker(plan, options = {}) {
4091
4924
  schedulePush();
4092
4925
  return;
4093
4926
  }
4094
- case "download:skipped": {
4927
+ case EventTypes.DOWNLOAD_SKIPPED: {
4095
4928
  const stage = stageOfTarget.get(event.file.target);
4096
4929
  if (stage) {
4097
4930
  const size = expectedSizeOf.get(event.file.target) ?? 0;
@@ -4101,7 +4934,7 @@ function createInstallProgressTracker(plan, options = {}) {
4101
4934
  }
4102
4935
  return;
4103
4936
  }
4104
- case "download:completed": {
4937
+ case EventTypes.DOWNLOAD_COMPLETED: {
4105
4938
  const entry = inFlightByTarget.get(event.file.target);
4106
4939
  if (entry) {
4107
4940
  const finalBytes = event.bytes ?? entry.bytes;
@@ -4148,36 +4981,60 @@ function createInstallProgressTracker(plan, options = {}) {
4148
4981
  for (const listener of listeners) listener(snap);
4149
4982
  }
4150
4983
  };
4151
- }
4152
- function clamp(value) {
4984
+ };
4985
+ var clamp = (value) => {
4153
4986
  if (value <= 0) return 0;
4154
4987
  if (value >= 100) return 100;
4155
4988
  return value;
4156
- }
4989
+ };
4157
4990
 
4158
- // src/types/events.ts
4159
- var EventTypes = {
4160
- INSTALL_PHASE_CHANGED: "install:phase-changed",
4161
- DOWNLOAD_STARTED: "download:started",
4162
- DOWNLOAD_PROGRESS: "download:progress",
4163
- DOWNLOAD_SKIPPED: "download:skipped",
4164
- DOWNLOAD_COMPLETED: "download:completed",
4165
- DOWNLOAD_FAILED: "download:failed",
4166
- INTEGRITY_VERIFIED: "integrity:verified",
4167
- INTEGRITY_MISMATCH: "integrity:mismatch",
4168
- ARCHIVE_EXTRACTED: "archive:extracted",
4169
- FORGE_PROCESSOR_STARTED: "forge:processor-started",
4170
- FORGE_PROCESSOR_COMPLETED: "forge:processor-completed",
4171
- FORGE_PROCESSOR_OUTPUT_VERIFIED: "forge:processor-output-verified",
4172
- VERIFY_FILE_CHECKED: "verify:file-checked",
4173
- VERIFY_COMPLETED: "verify:completed",
4174
- REPAIR_PHASE_CHANGED: "repair:phase-changed",
4175
- LAUNCH_STARTING: "launch:starting",
4176
- LAUNCH_STARTED: "launch:started",
4177
- LAUNCH_STDOUT: "launch:stdout",
4178
- LAUNCH_STDERR: "launch:stderr",
4179
- LAUNCH_EXITED: "launch:exited",
4180
- LAUNCH_ABORTED: "launch:aborted"
4991
+ // src/core/pause-controller.ts
4992
+ var PauseController = class {
4993
+ #paused = false;
4994
+ #waiters = [];
4995
+ get paused() {
4996
+ return this.#paused;
4997
+ }
4998
+ pause() {
4999
+ this.#paused = true;
5000
+ }
5001
+ resume() {
5002
+ this.#paused = false;
5003
+ const list = this.#waiters;
5004
+ this.#waiters = [];
5005
+ for (const resolve of list) resolve();
5006
+ }
5007
+ waitWhilePaused() {
5008
+ if (!this.#paused) return Promise.resolve();
5009
+ return new Promise((resolve) => this.#waiters.push(resolve));
5010
+ }
5011
+ };
5012
+
5013
+ // src/constants/placeholders.ts
5014
+ var LAUNCH_PLACEHOLDERS = {
5015
+ "${auth_player_name}": "Player display name.",
5016
+ "${version_name}": "Resolved Minecraft version id.",
5017
+ "${game_directory}": "Per-target directory.",
5018
+ "${assets_root}": "Assets root (`<directory>/assets`).",
5019
+ "${assets_index_name}": "Asset index id from the manifest.",
5020
+ "${auth_uuid}": "Player UUID (no dashes).",
5021
+ "${auth_access_token}": "Yggdrasil/MSA access token.",
5022
+ "${auth_session}": "Legacy session token (`token:<token>:<uuid>`).",
5023
+ "${clientid}": "MSA client id.",
5024
+ "${auth_xuid}": "Xbox user id.",
5025
+ "${user_type}": "`msa` | `mojang` | `legacy`.",
5026
+ "${user_properties}": "User properties JSON (often `{}`).",
5027
+ "${version_type}": "Channel string, e.g. `release`.",
5028
+ "${game_assets}": "Legacy virtual assets directory.",
5029
+ "${resolution_width}": "Window width (feature-gated).",
5030
+ "${resolution_height}": "Window height (feature-gated).",
5031
+ "${natives_directory}": "Extracted natives directory.",
5032
+ "${classpath}": "Joined classpath of libraries + version jar.",
5033
+ "${classpath_separator}": "OS-specific classpath separator (`:` / `;`).",
5034
+ "${library_directory}": "Per-target libraries directory.",
5035
+ "${launcher_name}": "Launcher brand string.",
5036
+ "${launcher_version}": "Launcher version string.",
5037
+ "${path}": "Path to the log4j config file (logging.client.argument only)."
4181
5038
  };
4182
5039
 
4183
5040
  // src/types/minecraft.ts
@@ -4212,33 +5069,6 @@ var Architectures = {
4212
5069
  ARM64: "arm64"
4213
5070
  };
4214
5071
 
4215
- // src/constants/placeholders.ts
4216
- var LAUNCH_PLACEHOLDERS = {
4217
- "${auth_player_name}": "Player display name.",
4218
- "${version_name}": "Resolved Minecraft version id.",
4219
- "${game_directory}": "Per-target directory.",
4220
- "${assets_root}": "Assets root (`<directory>/assets`).",
4221
- "${assets_index_name}": "Asset index id from the manifest.",
4222
- "${auth_uuid}": "Player UUID (no dashes).",
4223
- "${auth_access_token}": "Yggdrasil/MSA access token.",
4224
- "${auth_session}": "Legacy session token (`token:<token>:<uuid>`).",
4225
- "${clientid}": "MSA client id.",
4226
- "${auth_xuid}": "Xbox user id.",
4227
- "${user_type}": "`msa` | `mojang` | `legacy`.",
4228
- "${user_properties}": "User properties JSON (often `{}`).",
4229
- "${version_type}": "Channel string, e.g. `release`.",
4230
- "${game_assets}": "Legacy virtual assets directory.",
4231
- "${resolution_width}": "Window width (feature-gated).",
4232
- "${resolution_height}": "Window height (feature-gated).",
4233
- "${natives_directory}": "Extracted natives directory.",
4234
- "${classpath}": "Joined classpath of libraries + version jar.",
4235
- "${classpath_separator}": "OS-specific classpath separator (`:` / `;`).",
4236
- "${library_directory}": "Per-target libraries directory.",
4237
- "${launcher_name}": "Launcher brand string.",
4238
- "${launcher_version}": "Launcher version string.",
4239
- "${path}": "Path to the log4j config file (logging.client.argument only)."
4240
- };
4241
-
4242
- export { ASSETS_DIR, ASSETS_INDEXES_DIR, ASSETS_LEGACY_DIR, ASSETS_LOG_CONFIGS_DIR, ASSETS_OBJECTS_DIR, ASSETS_RESOURCES_DIR, ASSETS_VIRTUAL_DIR, ApiEndpoints, Architectures, AuthModes, BASE_JVM_ARGS, CACHE_MAX_ENTRIES, CACHE_TTL_MS, ChildProcessSpawner, DEFAULT_KILL_GRACE_MS, DEFAULT_LAUNCHER_NAME, DEFAULT_LAUNCHER_VERSION, DEFAULT_LIBRARY_REPOSITORY, DEFAULT_MAX_MB, DEFAULT_MIN_MB, DOWNLOAD_CONCURRENCY, EXTRACTION_MAX_COMPRESSION_RATIO, EXTRACTION_MAX_ENTRY_COUNT, EXTRACTION_MAX_FILE_SIZE, EXTRACTION_MAX_TOTAL_SIZE, EventTypes, FABRIC_MAVEN_BASE, FALLBACK_COMPONENT, FORGE_INSTALLERS_DIR, FORGE_INSTALLER_MAX_SIZE, FORGE_MAVEN_BASE, FabricVersionsApi, FetchHttpClient, ForgeVersionsApi, HTTP_RETRY_BACKOFF_BASE_MS, HTTP_RETRY_BACKOFF_CAP_MS, HTTP_RETRY_MAX, HTTP_TIMEOUT_MS, InstallActionKinds, InstallPhases, InstallStages, JAVA_EXECUTABLE, LAUNCH_PLACEHOLDERS, LEGACY_JVM_ARGS, LIBRARIES_DIR, Loaders, LogLevels, MACOS_JVM_ARGS, MAC_RUNTIME_PREFIX, MAX_PROCESSOR_STDERR_LINES, MinecraftChannels, MinecraftKit, MinecraftKitError, MinecraftVersionsApi, NATIVES_DIR_NAME, NODE_ARCH_TO_MOJANG_ARCH, NODE_PLATFORM_TO_MOJANG_OS, OperatingSystems, PROGRESS_EVENT_INTERVAL_MS, PauseController, RUNTIMES_DIR, RUNTIME_PLATFORM_KEYS, RepairPhases, RuntimeComponents, RuntimePreference, RuntimeVersionsApi, SPAWNER_MAX_LINE_BYTES, TargetsApi, USER_AGENT, VERSIONS_DIR, VerificationKinds, VerifyFileCategories, VerifyFileStatuses, VersionPreference, consoleLogger, createInstallProgressTracker, createMemoryCache, detectSystem, isErrorCode, isMinecraftKitError, offlineUuidFor, parseMajorVersion, pickClientJarVersionId, planFabricRepair, planForgeRepair, planMinecraftRepair, planRuntimeInstall, planRuntimeRepair, planStandaloneRuntimeInstall, repairAll, resolveLaunchVersion, runRepair, silentLogger, stripUuidDashes, targetPaths, verifyFabric, verifyForge, verifyMinecraft, verifyRuntime };
5072
+ export { ASSETS_DIR, ASSETS_INDEXES_DIR, ASSETS_LEGACY_DIR, ASSETS_LOG_CONFIGS_DIR, ASSETS_OBJECTS_DIR, ASSETS_RESOURCES_DIR, ASSETS_VIRTUAL_DIR, ApiEndpoints, Architectures, AuthModes, BASE_JVM_ARGS, CACHE_MAX_ENTRIES, CACHE_TTL_MS, CLIENT_ID_ENV_VAR, ChildProcessSpawner, DEFAULT_KILL_GRACE_MS, DEFAULT_LAUNCHER_NAME, DEFAULT_LAUNCHER_VERSION, DEFAULT_LIBRARY_REPOSITORY, DEFAULT_MAX_MB, DEFAULT_MIN_MB, DOWNLOAD_CONCURRENCY, DownloadCategories, EXTRACTION_MAX_COMPRESSION_RATIO, EXTRACTION_MAX_ENTRY_COUNT, EXTRACTION_MAX_FILE_SIZE, EXTRACTION_MAX_TOTAL_SIZE, EventTypes, FABRIC_MAVEN_BASE, FALLBACK_COMPONENT, FORGE_INSTALLERS_DIR, FORGE_INSTALLER_MAX_SIZE, FORGE_MAVEN_BASE, FabricVersionsApi, FetchHttpClient, ForgeVersionsApi, HTTP_RETRY_BACKOFF_BASE_MS, HTTP_RETRY_BACKOFF_CAP_MS, HTTP_RETRY_MAX, HTTP_TIMEOUT_MS, InstallActionKinds, InstallPhases, InstallStages, JAVA_EXECUTABLE, LAUNCH_PLACEHOLDERS, LEGACY_JVM_ARGS, LIBRARIES_DIR, Loaders, LogLevels, MACOS_JVM_ARGS, MAC_RUNTIME_PREFIX, MAX_PROCESSOR_STDERR_LINES, MinecraftChannels, MinecraftKit, MinecraftKitError, MinecraftKitErrorCodes, MinecraftVersionsApi, MojangAuthApi, NATIVES_DIR_NAME, NODE_ARCH_TO_MOJANG_ARCH, NODE_PLATFORM_TO_MOJANG_OS, OperatingSystems, PROGRESS_EVENT_INTERVAL_MS, PauseController, RUNTIMES_DIR, RUNTIME_PLATFORM_KEYS, RepairPhases, RuntimeComponents, RuntimePreference, RuntimeVersionsApi, SPAWNER_MAX_LINE_BYTES, TargetsApi, USER_AGENT, VERSIONS_DIR, VerificationKinds, VerifyFileCategories, VerifyFileStatuses, VersionPreference, assertNever, consoleLogger, createInstallProgressTracker, createMemoryCache, detectSystem, isErrorCode, isMinecraftKitError, offlineUuidFor, parseMajorVersion, pickClientJarVersionId, planFabricRepair, planForgeRepair, planMinecraftRepair, planRuntimeInstall, planRuntimeRepair, planStandaloneRuntimeInstall, repairAll, resolveLaunchVersion, runRepair, scopedLogger, silentLogger, stripUuidDashes, targetPaths, toOnlineAuth, verifyFabric, verifyForge, verifyMinecraft, verifyRuntime };
4243
5073
  //# sourceMappingURL=index.mjs.map
4244
5074
  //# sourceMappingURL=index.mjs.map