@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
831 lines (830 loc) • 33.7 kB
JavaScript
"use strict";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UNSAFE_PORTS = void 0;
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const uuid_1 = require("uuid");
const NodeStorage_1 = __importDefault(require("./NodeStorage"));
const ILocalUtilities_1 = require("../core/ILocalUtilities");
const Log_1 = __importDefault(require("../core/Log"));
const ImageCodecNode_1 = __importDefault(require("./ImageCodecNode"));
/**
* Ports that should never be used for MCTools HTTP servers.
*
* This list includes:
* 1. Browser-blocked ports - Chrome/Firefox/Safari block these for security (ERR_UNSAFE_PORT)
* See: https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/net/base/port_util.cc
*
* 2. Security-sensitive service ports - commonly targeted for attacks or conflicts:
* - Database ports (Redis, etc.) - targeted for data theft
* - IRC ports - used by botnets and C&C servers
* - Other commonly exploited services
*/
exports.UNSAFE_PORTS = new Set([
// === Browser-blocked ports (Chrome/Firefox/Safari) ===
1, // tcpmux
7, // echo
9, // discard
11, // systat
13, // daytime
15, // netstat
17, // qotd
19, // chargen
20, // ftp data
21, // ftp access
22, // ssh
23, // telnet
25, // smtp
37, // time
42, // name
43, // nicname
53, // domain
69, // tftp
77, // priv-rjs
79, // finger
87, // ttylink
95, // supdup
101, // hostriame
102, // iso-tsap
103, // gppitnp
104, // acr-nema
109, // pop2
110, // pop3
111, // sunrpc
113, // auth
115, // sftp
117, // uucp-path
119, // nntp
123, // NTP
135, // loc-srv / epmap
137, // netbios
139, // netbios
143, // imap2
161, // snmp
179, // BGP
389, // ldap
427, // SLP (Also used by determine.exe)
465, // smtp+ssl
512, // print / exec
513, // login
514, // shell
515, // printer
526, // tempo
530, // courier
531, // chat
532, // netnews
540, // uucp
548, // AFP (Apple Filing Protocol)
554, // rtsp
556, // remotefs
563, // nntp+ssl
587, // smtp (submission)
601, // syslog-conn
636, // ldap+ssl
989, // ftps-data
990, // ftps
993, // ldap+ssl
995, // pop3+ssl
1719, // h323gatestat
1720, // h323hostcall
1723, // pptp
2049, // nfs
3659, // apple-sasl / PasswordServer
4045, // lockd
5060, // sip
5061, // sips
6000, // X11
6566, // sane-port
6665, // Alternate IRC (browser-blocked + botnet target)
6666, // Alternate IRC (browser-blocked + botnet target)
6667, // Standard IRC (browser-blocked + botnet target)
6668, // Alternate IRC (browser-blocked + botnet target)
6669, // Alternate IRC (browser-blocked + botnet target)
6697, // IRC + TLS (browser-blocked + botnet target)
10080, // Amanda
// === Security-sensitive service ports (not browser-blocked but should be avoided) ===
6379, // Redis
]);
class LocalUtilities {
#productNameSeed = "mctools";
#basePathAdjust = undefined;
static #bedrockSchemasRoot = undefined;
/**
* Resolves the root directory of the @minecraft/bedrock-schemas package.
* Forms and schemas are served directly from this package at runtime
* rather than being copied into the build output.
*/
static get bedrockSchemasRoot() {
if (LocalUtilities.#bedrockSchemasRoot !== undefined) {
return LocalUtilities.#bedrockSchemasRoot;
}
try {
// catalog.json is in the package exports map, so require.resolve works
const catalogPath = require.resolve("@minecraft/bedrock-schemas/catalog.json");
LocalUtilities.#bedrockSchemasRoot = path.dirname(catalogPath);
}
catch {
LocalUtilities.#bedrockSchemasRoot = "";
}
return LocalUtilities.#bedrockSchemasRoot || undefined;
}
/**
* Get the data directory override from MCTOOLS_DATA_DIR environment variable.
* This is primarily used for Docker containers where data should be stored
* in a volume-mounted directory like /data.
*/
static get dataDirectoryOverride() {
return process.env.MCTOOLS_DATA_DIR;
}
/**
* Check if MCTOOLS_I_ACCEPT_EULA_AT_MINECRAFTDOTNETSLASHEULA environment variable is set to "true".
* Used for non-interactive EULA acceptance in Docker containers.
* The long name is intentional to ensure users explicitly acknowledge the EULA.
*/
static get eulaAcceptedViaEnvironment() {
const value = process.env.MCTOOLS_I_ACCEPT_EULA_AT_MINECRAFTDOTNETSLASHEULA;
return value?.toLowerCase() === "true";
}
get basePathAdjust() {
return this.#basePathAdjust;
}
set basePathAdjust(pathAdjust) {
this.#basePathAdjust = pathAdjust;
}
get platform() {
switch (os.platform()) {
case "win32":
return ILocalUtilities_1.Platform.windows;
case "darwin":
return ILocalUtilities_1.Platform.macOS;
case "linux":
return ILocalUtilities_1.Platform.linux;
default:
return ILocalUtilities_1.Platform.unsupported;
}
}
get productNameSeed() {
return this.#productNameSeed;
}
setProductNameSeed(newSeed) {
this.#productNameSeed = newSeed;
}
get userDataPath() {
return os.homedir();
}
get localAppDataPath() {
if (this.platform === ILocalUtilities_1.Platform.windows) {
return (this.userDataPath +
NodeStorage_1.default.platformFolderDelimiter +
"AppData" +
NodeStorage_1.default.platformFolderDelimiter +
"Local" +
NodeStorage_1.default.platformFolderDelimiter);
}
else {
return this.userDataPath;
}
}
get roamingAppDataPath() {
if (this.platform === ILocalUtilities_1.Platform.windows) {
return (this.userDataPath +
NodeStorage_1.default.platformFolderDelimiter +
"AppData" +
NodeStorage_1.default.platformFolderDelimiter +
"Roaming" +
NodeStorage_1.default.platformFolderDelimiter);
}
else {
return this.userDataPath;
}
}
get localReleaseServerLogPath() {
if (this.platform === ILocalUtilities_1.Platform.windows) {
return (this.userDataPath +
NodeStorage_1.default.platformFolderDelimiter +
"AppData" +
NodeStorage_1.default.platformFolderDelimiter +
"Roaming" +
NodeStorage_1.default.platformFolderDelimiter +
"Minecraft Bedrock" +
NodeStorage_1.default.platformFolderDelimiter +
"logs" +
NodeStorage_1.default.platformFolderDelimiter);
}
else {
return "." + NodeStorage_1.default.platformFolderDelimiter;
}
}
get localPreviewServerLogPath() {
if (this.platform === ILocalUtilities_1.Platform.windows) {
return (this.userDataPath +
NodeStorage_1.default.platformFolderDelimiter +
"AppData" +
NodeStorage_1.default.platformFolderDelimiter +
"Roaming" +
NodeStorage_1.default.platformFolderDelimiter +
"Minecraft Bedrock Preview" +
NodeStorage_1.default.platformFolderDelimiter +
"logs" +
NodeStorage_1.default.platformFolderDelimiter);
}
else {
return "." + NodeStorage_1.default.platformFolderDelimiter;
}
}
get minecraftPath() {
return (this.userDataPath +
NodeStorage_1.default.platformFolderDelimiter +
"AppData" +
NodeStorage_1.default.platformFolderDelimiter +
"Roaming" +
NodeStorage_1.default.platformFolderDelimiter +
"Microsoft Bedrock" +
NodeStorage_1.default.platformFolderDelimiter +
"Users" +
NodeStorage_1.default.platformFolderDelimiter +
"Shared" +
NodeStorage_1.default.platformFolderDelimiter +
"games" +
NodeStorage_1.default.platformFolderDelimiter +
"com.mojang" +
NodeStorage_1.default.platformFolderDelimiter);
}
get minecraftPreviewPath() {
return (this.userDataPath +
NodeStorage_1.default.platformFolderDelimiter +
"AppData" +
NodeStorage_1.default.platformFolderDelimiter +
"Roaming" +
NodeStorage_1.default.platformFolderDelimiter +
"Microsoft Bedrock Preview" +
NodeStorage_1.default.platformFolderDelimiter +
"Users" +
NodeStorage_1.default.platformFolderDelimiter +
"Shared" +
NodeStorage_1.default.platformFolderDelimiter +
"games" +
NodeStorage_1.default.platformFolderDelimiter +
"com.mojang" +
NodeStorage_1.default.platformFolderDelimiter);
}
get minecraftUwpPath() {
return (this.localAppDataPath +
"Packages" +
NodeStorage_1.default.platformFolderDelimiter +
"Microsoft.MinecraftUWP_8wekyb3d8bbwe" +
NodeStorage_1.default.platformFolderDelimiter +
"LocalState" +
NodeStorage_1.default.platformFolderDelimiter +
"games" +
NodeStorage_1.default.platformFolderDelimiter +
"com.mojang" +
NodeStorage_1.default.platformFolderDelimiter);
}
get minecraftPreviewUwpPath() {
return (this.localAppDataPath +
"Packages" +
NodeStorage_1.default.platformFolderDelimiter +
"Microsoft.MinecraftWindowsBeta_8wekyb3d8bbwe" +
NodeStorage_1.default.platformFolderDelimiter +
"LocalState" +
NodeStorage_1.default.platformFolderDelimiter +
"games" +
NodeStorage_1.default.platformFolderDelimiter +
"com.mojang" +
NodeStorage_1.default.platformFolderDelimiter);
}
get testWorkingPath() {
// If MCTOOLS_DATA_DIR is set (e.g., in Docker), use it
const dataDir = LocalUtilities.dataDirectoryOverride;
if (dataDir) {
return NodeStorage_1.default.ensureEndsWithDelimiter(dataDir) + "test" + NodeStorage_1.default.platformFolderDelimiter;
}
let path = this.localAppDataPath;
if (this.platform === ILocalUtilities_1.Platform.windows) {
path =
NodeStorage_1.default.ensureEndsWithDelimiter(path) +
this.#productNameSeed +
"_test" +
NodeStorage_1.default.platformFolderDelimiter;
}
else {
// Linux/macOS: Use ~/.mctools/test/ structure
path =
NodeStorage_1.default.ensureEndsWithDelimiter(path) +
"." +
this.#productNameSeed +
NodeStorage_1.default.platformFolderDelimiter +
"test" +
NodeStorage_1.default.platformFolderDelimiter;
}
return path;
}
get cliWorkingPath() {
// If MCTOOLS_DATA_DIR is set (e.g., in Docker), use it
const dataDir = LocalUtilities.dataDirectoryOverride;
if (dataDir) {
return NodeStorage_1.default.ensureEndsWithDelimiter(dataDir) + "cli" + NodeStorage_1.default.platformFolderDelimiter;
}
let path = this.localAppDataPath;
if (this.platform === ILocalUtilities_1.Platform.windows) {
path =
NodeStorage_1.default.ensureEndsWithDelimiter(path) +
this.#productNameSeed +
"_cli" +
NodeStorage_1.default.platformFolderDelimiter;
}
else {
// Linux/macOS: Use ~/.mctools/cli/ structure
path =
NodeStorage_1.default.ensureEndsWithDelimiter(path) +
"." +
this.#productNameSeed +
NodeStorage_1.default.platformFolderDelimiter +
"cli" +
NodeStorage_1.default.platformFolderDelimiter;
}
return path;
}
get serverWorkingPath() {
// If MCTOOLS_DATA_DIR is set (e.g., in Docker), use it
const dataDir = LocalUtilities.dataDirectoryOverride;
if (dataDir) {
return NodeStorage_1.default.ensureEndsWithDelimiter(dataDir) + "server" + NodeStorage_1.default.platformFolderDelimiter;
}
let path = this.localAppDataPath;
if (this.platform === ILocalUtilities_1.Platform.windows) {
path =
NodeStorage_1.default.ensureEndsWithDelimiter(path) +
this.#productNameSeed +
"_server" +
NodeStorage_1.default.platformFolderDelimiter;
}
else {
// Linux/macOS: Use ~/.mctools/server/ structure
path =
NodeStorage_1.default.ensureEndsWithDelimiter(path) +
"." +
this.#productNameSeed +
NodeStorage_1.default.platformFolderDelimiter +
"server" +
NodeStorage_1.default.platformFolderDelimiter;
}
return path;
}
get worldsWorkingPath() {
// If MCTOOLS_DATA_DIR is set (e.g., in Docker), use it
const dataDir = LocalUtilities.dataDirectoryOverride;
if (dataDir) {
return NodeStorage_1.default.ensureEndsWithDelimiter(dataDir) + "worlds" + NodeStorage_1.default.platformFolderDelimiter;
}
let path = this.localAppDataPath;
if (this.platform === ILocalUtilities_1.Platform.windows) {
path =
NodeStorage_1.default.ensureEndsWithDelimiter(path) +
this.#productNameSeed +
"_worlds" +
NodeStorage_1.default.platformFolderDelimiter;
}
else {
// Linux/macOS: Use ~/.mctools/worlds/ structure
path =
NodeStorage_1.default.ensureEndsWithDelimiter(path) +
"." +
this.#productNameSeed +
NodeStorage_1.default.platformFolderDelimiter +
"worlds" +
NodeStorage_1.default.platformFolderDelimiter;
}
return path;
}
get serversPath() {
let path = this.serverWorkingPath;
path = NodeStorage_1.default.ensureEndsWithDelimiter(path) + "servers" + NodeStorage_1.default.platformFolderDelimiter;
return path;
}
get sourceServersPath() {
let path = this.serverWorkingPath;
path = NodeStorage_1.default.ensureEndsWithDelimiter(path) + "serverSources" + NodeStorage_1.default.platformFolderDelimiter;
return path;
}
get packCachePath() {
let path = this.serverWorkingPath;
path = NodeStorage_1.default.ensureEndsWithDelimiter(path) + "packCache" + NodeStorage_1.default.platformFolderDelimiter;
return path;
}
get envPrefsPath() {
let path = this.serverWorkingPath;
path = NodeStorage_1.default.ensureEndsWithDelimiter(path) + "envprefs" + NodeStorage_1.default.platformFolderDelimiter;
return path;
}
generateCryptoRandomNumber(toVal) {
// Use rejection sampling to avoid modulo bias when generating random numbers
// from a cryptographically secure source
const maxUint32 = 0xffffffff;
const limit = maxUint32 - (maxUint32 % toVal);
let randomValue;
do {
randomValue = new Uint32Array(crypto.randomBytes(4).buffer)[0];
} while (randomValue >= limit);
return randomValue % toVal;
}
generateUuid() {
return (0, uuid_1.v4)();
}
validateFolderPath(path) {
// banned character combos
if (path.indexOf("..") >= 0 || path.indexOf("\\\\") >= 0 || path.indexOf("//") >= 0) {
throw new Error("Unsupported path combinations: " + path);
}
if (path.lastIndexOf(":") >= 3) {
throw new Error("Unsupported drive location: " + path);
}
const count = this.countChar(path, "\\") + this.countChar(path, "/");
if (count < 3) {
throw new Error("Unsupported base path: " + path);
}
}
countChar(source, find) {
let count = 0;
let index = source.indexOf(find);
while (index >= 0) {
count++;
index = source.indexOf(find, index + find.length);
}
return count;
}
ensureStartsWithSlash(pathSegment) {
if (!pathSegment.startsWith("/")) {
pathSegment = "/" + pathSegment;
}
return pathSegment;
}
ensureEndsWithSlash(pathSegment) {
if (!pathSegment.endsWith("/")) {
pathSegment += "/";
}
return pathSegment;
}
ensureStartsWithBackSlash(pathSegment) {
if (!pathSegment.startsWith("\\")) {
pathSegment = "\\" + pathSegment;
}
return pathSegment;
}
ensureEndsWithBackSlash(pathSegment) {
if (!pathSegment.endsWith("\\")) {
pathSegment += "\\";
}
return pathSegment;
}
getFullPath(relativePath) {
// Redirect forms and schemas to @minecraft/bedrock-schemas package
const pkgRoot = LocalUtilities.bedrockSchemasRoot;
if (pkgRoot) {
if (relativePath.startsWith("data/forms/") || relativePath.startsWith("data\\forms\\")) {
const subPath = relativePath.substring("data/forms/".length);
return path.join(pkgRoot, "forms", subPath);
}
const schemasPrefix = relativePath.startsWith("/schemas/")
? "/schemas/"
: relativePath.startsWith("schemas/")
? "schemas/"
: relativePath.startsWith("\\schemas\\")
? "\\schemas\\"
: undefined;
if (schemasPrefix) {
const subPath = relativePath.substring(schemasPrefix.length);
return path.join(pkgRoot, "schemas", subPath);
}
}
let fullPath;
if (this.#basePathAdjust) {
// When basePathAdjust is set (via --base-path CLI option),
// resolve it relative to process.cwd() (where the command was run)
// This allows users to specify paths relative to their working directory
fullPath = path.resolve(process.cwd(), this.#basePathAdjust);
fullPath = NodeStorage_1.default.ensureEndsWithDelimiter(fullPath);
}
else {
// Fall back to __dirname-based resolution for backwards compatibility
fullPath = __dirname;
const lastSlash = Math.max(fullPath.lastIndexOf("\\", fullPath.length - 2), fullPath.lastIndexOf("/", fullPath.length - 2));
if (lastSlash >= 0) {
fullPath = fullPath.substring(0, lastSlash + 1);
}
}
if (this.platform === ILocalUtilities_1.Platform.windows) {
fullPath += relativePath.replace(/\//g, "\\");
}
else {
fullPath += relativePath.replace(/\\/g, NodeStorage_1.default.platformFolderDelimiter);
}
return fullPath;
}
createStorage(path) {
const fullPath = this.getFullPath(path);
return new NodeStorage_1.default(fullPath, "");
}
async readJsonFile(path) {
const fs = require("fs");
const fullPath = this.getFullPath(path);
try {
if (!fs.existsSync(fullPath)) {
return null;
}
const rawData = fs.readFileSync(fullPath);
if (!rawData) {
return null;
}
const jsonData = JSON.parse(rawData);
return jsonData;
}
catch (e) {
// Silently return null for parse errors - the caller can handle missing data
// This is common for malformed JSON files in vanilla/sample content
return null;
}
}
async processConversion(conversionSettings) {
// Placeholder for future conversion logic
return true;
}
/**
* Check if a port is available for use.
* @param port The port number to check
* @param host The host to check (default: localhost)
* @returns Promise that resolves to true if the port is available, false otherwise
*/
static isPortAvailable(port, host = "localhost") {
const net = require("net");
return new Promise((resolve) => {
const server = net.createServer();
server.once("error", (err) => {
if (err.code === "EADDRINUSE") {
resolve(false);
}
else {
// Other errors - treat as unavailable
resolve(false);
}
});
server.once("listening", () => {
server.close();
resolve(true);
});
server.listen(port, host);
});
}
/**
* Find an available port within a given range.
* @param startPort The starting port of the range (inclusive)
* @param endPort The ending port of the range (inclusive)
* @param host The host to check (default: localhost)
* @returns Promise that resolves to an available port, or undefined if none found
*/
static async findAvailablePort(startPort, endPort, host = "localhost") {
// Create an array of ports in the range, excluding unsafe ports
const ports = [];
for (let port = startPort; port <= endPort; port++) {
if (!exports.UNSAFE_PORTS.has(port)) {
ports.push(port);
}
}
// Fisher-Yates shuffle for random port selection
for (let i = ports.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[ports[i], ports[j]] = [ports[j], ports[i]];
}
// Try each port until we find an available one
for (const port of ports) {
const available = await LocalUtilities.isPortAvailable(port, host);
if (available) {
return port;
}
}
return undefined;
}
/**
* Get a random port within a range, excluding browser-unsafe ports.
* This is a synchronous function that doesn't check if the port is available.
* Use findAvailablePort() if you need to verify the port is not in use.
*
* @param startPort The starting port of the range (inclusive)
* @param endPort The ending port of the range (inclusive)
* @returns A random port in the range that is not in the UNSAFE_PORTS list
*/
static getRandomSafePort(startPort, endPort) {
// Build array of safe ports in range
const safePorts = [];
for (let port = startPort; port <= endPort; port++) {
if (!exports.UNSAFE_PORTS.has(port)) {
safePorts.push(port);
}
}
if (safePorts.length === 0) {
// Fallback: if no safe ports in range, just return startPort
// This shouldn't happen with reasonable port ranges
Log_1.default.debugAlert(`No safe ports available in range ${startPort}-${endPort}, using ${startPort}`);
return startPort;
}
return safePorts[Math.floor(Math.random() * safePorts.length)];
}
/**
* Verifies the Authenticode digital signature of a Windows executable.
* Uses PowerShell's Get-AuthenticodeSignature cmdlet, which is built into
* every modern Windows and requires no native Node.js addons.
*
* @param exePath Absolute path to the executable to verify
* @returns Promise with verification result including validity, signer, and Microsoft check
*/
static async verifyAuthenticodeSignature(exePath) {
// Only works on Windows
if (os.platform() !== "win32") {
return {
isValid: false,
status: "UnsupportedPlatform",
error: "Authenticode signature verification is only supported on Windows",
};
}
// Validate the path exists
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
if (!fs.existsSync(exePath)) {
return {
isValid: false,
status: "FileNotFound",
error: `File not found: ${exePath}`,
};
}
try {
const { execFile } = await Promise.resolve().then(() => __importStar(require("child_process")));
const { existsSync: execExistsSync } = await Promise.resolve().then(() => __importStar(require("fs")));
// Use PowerShell's Get-AuthenticodeSignature which wraps the Windows
// WinVerifyTrust API. This avoids the need for native Node.js addons
// (win-verify-signature) which require platform-specific compiled binaries
// that are fragile across Node.js versions and packaging scenarios.
//
// We use -EncodedCommand with a base64-encoded UTF-16LE script to avoid
// all command-line escaping issues between Node.js and PowerShell.
//
// We prefer pwsh.exe (PowerShell 7+) because it has more reliable module
// auto-loading. PowerShell 5.1 (powershell.exe) can fail on some machines
// due to type data conflicts in the Security module. We try both.
// SECURITY: Instead of interpolating the file path into the PowerShell script
// (which risks command injection via newlines, backticks, or other control
// characters in the path), we pass the path via an environment variable.
// The script reads $env:MCT_SIG_VERIFY_PATH, so the path never enters
// the script text and cannot break out of string boundaries.
// Defense-in-depth: reject paths with characters that have no business
// in a Windows file path. This guards against exotic injection even if
// the env-var approach were somehow bypassed.
if (/[\x00-\x1f\x7f`${}]/.test(exePath)) {
return {
isValid: false,
status: "InvalidPath",
error: "File path contains invalid characters",
};
}
const psScript = [
`$sig = Get-AuthenticodeSignature -LiteralPath $env:MCT_SIG_VERIFY_PATH`,
`$r = @{ Status = $sig.Status.ToString(); SignerCN = ''; SignerSubject = '' }`,
`if ($sig.SignerCertificate) {`,
` $r.SignerSubject = $sig.SignerCertificate.Subject`,
` if ($sig.SignerCertificate.Subject -match 'CN=([^,]+)') { $r.SignerCN = $Matches[1].Trim('"') }`,
`}`,
`$r | ConvertTo-Json -Compress`,
].join("\n");
// Encode as UTF-16LE base64 for PowerShell's -EncodedCommand
const encoded = Buffer.from(psScript, "utf16le").toString("base64");
// Build ordered list of PowerShell executables to try.
// pwsh.exe (PowerShell 7+) is preferred for reliability.
const candidates = [];
const pwsh7Path = "C:\\Program Files\\PowerShell\\7\\pwsh.exe";
if (execExistsSync(pwsh7Path)) {
candidates.push(pwsh7Path);
}
candidates.push("pwsh.exe", "powershell.exe");
const runPowerShell = (psExe) => new Promise((resolve, reject) => {
execFile(psExe, ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], {
timeout: 15000,
// Pass the file path via environment variable instead of
// interpolating it into the script. This prevents injection.
env: { ...process.env, MCT_SIG_VERIFY_PATH: exePath },
}, (error, stdout, stderr) => {
if (error) {
reject(new Error(`${psExe} failed: ${error.message}`));
return;
}
const trimmed = stdout.trim();
if (!trimmed) {
reject(new Error(`${psExe} returned empty output${stderr ? ": " + stderr.substring(0, 200) : ""}`));
return;
}
resolve(trimmed);
});
});
// Try each candidate until one succeeds
let psResult;
let lastError;
for (const psExe of candidates) {
try {
psResult = await runPowerShell(psExe);
break;
}
catch (err) {
lastError = err instanceof Error ? err : new Error(String(err));
}
}
if (!psResult) {
return {
isValid: false,
status: "VerificationError",
error: `No working PowerShell found. Install PowerShell 7+ (https://aka.ms/powershell) or use --unsafe-skip-signature-validation. Last error: ${lastError?.message}`,
};
}
const parsed = JSON.parse(psResult);
const isValid = parsed.Status === "Valid";
const signerSubject = parsed.SignerSubject || undefined;
const signerCN = parsed.SignerCN || "";
// Microsoft Corporation is the expected signer for Minecraft Dedicated Server
// We also accept Mojang (owned by Microsoft) for legacy compatibility
const isMicrosoftSigned = isValid &&
(signerCN === "Microsoft Corporation" ||
signerCN === "Mojang" ||
(signerSubject !== undefined &&
(signerSubject.includes("Microsoft Corporation") || signerSubject.includes("Mojang"))));
return {
isValid,
status: isValid ? "Valid" : parsed.Status,
signer: signerSubject,
isMicrosoftSigned,
error: isValid ? undefined : `Signature status: ${parsed.Status}`,
};
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
return {
isValid: false,
status: "VerificationError",
error: `Failed to verify signature: ${errorMessage}`,
};
}
}
// ============================================================================
// IMAGE CODEC METHODS (Node.js specific)
// ============================================================================
/**
* Decode PNG image data using pngjs.
* This is a Node.js-specific implementation that uses native modules.
*/
decodePng(data) {
return ImageCodecNode_1.default.decodePng(data);
}
/**
* Encode RGBA pixel data to PNG format using pngjs.
* This is a Node.js-specific implementation that uses native modules.
*/
encodeToPng(pixels, width, height) {
return ImageCodecNode_1.default.encodeToPng(pixels, width, height);
}
}
exports.default = LocalUtilities;