diff --git a/lib/bedrock.js b/lib/bedrock.js index f231c68..3ba0bf0 100644 --- a/lib/bedrock.js +++ b/lib/bedrock.js @@ -18,44 +18,50 @@ const UNCONNECTED_PONG = 0x1c; /** * Representation of raw, semicolon-delimited MOTD string. * This struct directly mirrors the fields and order from the server response. - * @see {@link https://minecraft.wiki/w/RakNet#Unconnected_Pong} + * See [`Unconnected Pong Documentation`](https://minecraft.wiki/w/RakNet#Unconnected_Pong) for more details. * @typedef {object} BedrockMotd - * @property {string} edition - The edition of the server (MCPE or MCEE) - * @property {string} name - The primary name of the server (first line of MOTD) - * @property {number} protocol - The protocol version - * @property {string} version - The game version (e.g., "1.21.2") - * @property {number} playerCount - The current number of players online - * @property {number} playerMax - The maximum number of players allowed - * @property {bigint} serverGuid - The server's GUID - * @property {string} subName - The secondary name of the server (second line of MOTD) - * @property {string} gamemode - The default gamemode (e.g., "Survival") - * @property {boolean | undefined} nintendoLimited - Whether the server is Nintendo limited - * @property {string | undefined} port - The server's IPv4 port, if provided - * @property {string | undefined} ipv6Port - The server's IPv6 port, if provided - * @property {string | undefined} editorMode - Whether the server is in editor mode, if provided. See: https://learn.microsoft.com/en-us/minecraft/creator/documents/bedrockeditor/editoroverview?view=minecraft-bedrock-stable + * @property {string} edition - The edition of the server (MCPE or MCEE). + * @property {string} name - The primary name of the server (first line of MOTD). + * @property {number} protocol - The protocol version. + * @property {string} version - The game version (e.g., "1.21.2"). + * @property {number} playerCount - The current number of players online. + * @property {number} playerMax - The maximum number of players allowed. + * @property {bigint} serverGuid - The server's GUID. + * @property {string} subName - The secondary name of the server (second line of MOTD). + * @property {string} gamemode - The default gamemode (e.g., "Survival"). + * @property {boolean} [nintendoLimited] - Whether the server is Nintendo limited. + * @property {number} [port] - The server's IPv4 port, if provided. + * @property {number} [ipv6Port] - The server's IPv6 port, if provided. + * @property {boolean} [editorMode] - Whether the server is in editor mode, if provided. See [Minecraft Editor Mode Documentation](https://learn.microsoft.com/en-us/minecraft/creator/documents/bedrockeditor/editoroverview?view=minecraft-bedrock-stable) for more details. */ /** * Represents the structured and user-friendly response from a server ping. * This is the public-facing object that users of the library will receive. * @typedef {object} BedrockPingResponse - * @property {string} edition - * @property {string} name - * @property {string} levelName - * @property {string} gamemode - * @property {{ protocol: number, minecraft: string }} version - * @property {{ online: number, max: number }} players - * @property {{ v4: number | undefined, v6: number | undefined }} port - * @property {bigint} guid - * @property {boolean | undefined} isNintendoLimited - * @property {string | undefined} isEditorModeEnabled + * @property {string} edition - The edition of the server (MCPE or MCEE). + * @property {string} name - The primary name of the server (first line of MOTD). + * @property {string} levelName - The name of the world or level being hosted. + * @property {string} gamemode - The default gamemode of the server. + * @property {{ protocol: number, minecraft: string }} version - Game and protocol versions. + * @property {{ online: number, max: number }} players - Current and maximum player counts. + * @property {{ v4?: number, v6?: number }} port - Announced IPv4 and IPv6 ports. + * @property {bigint} guid - The server's unique 64-bit GUID. + * @property {boolean} [isNintendoLimited] - True if the server restricts Nintendo Switch players. + * @property {boolean} [isEditorModeEnabled] - True if the server is in editor mode. See [Minecraft Editor Mode Documentation](https://learn.microsoft.com/en-us/minecraft/creator/documents/bedrockeditor/editoroverview?view=minecraft-bedrock-stable) for more details. + */ + +/** + * @typedef {object} BedrockPingOptions + * @property {number} [port=19132] - The server port to ping. + * @property {number} [timeout=5000] - The timeout in milliseconds for the request. */ /** * Creates an Unconnected Ping packet. - * @param {number} timestamp - The current time delta since the script started + * See [Unconnected Ping Documentation](https://minecraft.wiki/w/RakNet#Unconnected_Ping) for more details. + * @param {number} timestamp - The current time delta since the script started. * @returns {Buffer} - * @see {@link https://minecraft.wiki/w/RakNet#Unconnected_Ping} */ const createUnconnectedPingFrame = (timestamp) => { const buffer = Buffer.alloc(33); @@ -68,8 +74,9 @@ const createUnconnectedPingFrame = (timestamp) => { /** * Parses the semicolon-delimited MOTD string into a structured object. - * @param {string} motdString - The raw MOTD string from the server - * @throws {Error} If the MOTD string is missing required fields + * @param {string} motdString - The raw MOTD string from the server. + * @returns {BedrockMotd} The parsed internal MOTD object. + * @throws {Error} If the MOTD string is missing required fields. */ const parseMotd = (motdString) => { const parts = motdString.split(";"); @@ -122,8 +129,8 @@ const parseMotd = (motdString) => { /** * Transforms the raw MOTD object into a user-friendly, nested structure. - * @param {BedrockMotd} motd - The parsed MOTD object - * @returns {BedrockPingResponse} + * @param {BedrockMotd} motd - The parsed MOTD object. + * @returns {BedrockPingResponse} The final, user-facing response object. */ const transformMotd = (motd) => { return { @@ -151,9 +158,9 @@ const transformMotd = (motd) => { /** * Extracts the MOTD string from an Unconnected Pong packet and parses it. - * @param {Buffer} pongPacket - The raw pong packet from the server - * @returns {BedrockPingResponse} - * @throws {Error} If the packet is malformed + * @param {Buffer} pongPacket - The raw pong packet from the server. + * @returns {BedrockPingResponse} The final response object. + * @throws {Error} If the packet is malformed. */ const parseUnconnectedPong = (pongPacket) => { if (!Buffer.isBuffer(pongPacket) || pongPacket.length < 35) { @@ -189,11 +196,9 @@ const parseUnconnectedPong = (pongPacket) => { /** * Asynchronously pings a Minecraft Bedrock server. - * @param {string} host - The IP address or hostname of the server - * @param {object} [options] - Optional configuration - * @param {number} [options.port=19132] - The server port - * @param {number} [options.timeout=5000] - The request timeout in milliseconds - * @returns {Promise} A promise that resolves with the server's parsed MOTD + * @param {string} host - The IP address or hostname of the server. + * @param {BedrockPingOptions} [options={}] - Optional configuration. + * @returns {Promise} A promise that resolves with the server's parsed MOTD. */ export const pingBedrock = (host, options = {}) => { if (!host) { diff --git a/lib/java.js b/lib/java.js index 2bcd793..1150ff5 100644 --- a/lib/java.js +++ b/lib/java.js @@ -14,15 +14,22 @@ const debug = createDebug("mineping:java"); /** * Represents the structured and user-friendly response from a server ping. - * The fields and their optionality are based on the official protocol documentation. + * The fields and their optionality are based on the protocol documentation. + * See [Status Response Documentation](https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Status_Response) for more details. * @typedef {object} JavaPingResponse - * @property {{ name: string, protocol: number }} version - Contains the server's version name and protocol number - * @property {{ max: number, online: number, sample?: Array<{ name: string, id: string }> } | undefined} players - Player count and a sample of online players. - * @property {object | string | undefined} description - Optional. The server's Message of the Day (MOTD) - * @property {string | undefined} favicon - Optional. A Base64-encoded 64x64 PNG image data URI - * @property {boolean | undefined} enforcesSecureChat - Optional. True if the server requires clients to have a Mojang-signed public key - * @property {boolean | undefined} preventsChatReports - Optional. True if a mod is installed to disable chat reporting - * @see {@link https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Status_Response} + * @property {{ name: string, protocol: number }} version - Contains the server's version name and protocol number. + * @property {{ max: number, online: number, sample?: Array<{ name: string, id: string }> }} [players] - Player count and a sample of online players. + * @property {object | string} [description] - The server's Message of the Day (MOTD). + * @property {string} [favicon] - A Base64-encoded 64x64 PNG image data URI. + * @property {boolean} [enforcesSecureChat] - True if the server requires clients to have a Mojang-signed public key. + * @property {boolean} [preventsChatReports] - True if a mod is installed to disable chat reporting. + */ + +/** + * @typedef {object} JavaPingOptions + * @property {number} [port=25565] - The fallback port if an SRV record is not found. + * @property {number} [timeout=5000] - The connection timeout in milliseconds. + * @property {number} [protocolVersion=-1] - The protocol version to use in the handshake. `-1` is for auto-detection. */ /** @@ -106,24 +113,24 @@ function processResponse(buffer) { return { response, remainder }; } catch (err) { // If the buffer is too short for a VarInt, it's a recoverable state. - if (err.code === varint.ERR_VARINT_BUFFER_UNDERFLOW) { - debug("buffer underflow while parsing VarInt, waiting for more data"); - return null; // Wait for more data. + if (err instanceof varint.VarIntError) { + if (err.code === varint.ERR_VARINT_BUFFER_UNDERFLOW) { + debug("buffer underflow while parsing VarInt, waiting for more data"); + return null; // Wait for more data. + } + // For malformed VarInts or JSON, throw the error to reject the promise. + throw err; } - // For malformed VarInts or JSON, throw the error to reject the promise. throw err; } } /** - * Pings a Minecraft Java Edition server. + * Asynchronously Pings a Minecraft Java Edition server. * This function performs an SRV lookup and then attempts to connect and retrieve the server status. - * @param {string} host The server address to ping - * @param {object} [options={}] Optional configuration - * @param {number} [options.port=25565] The fallback port if an SRV record is not found - * @param {number} [options.timeout=5000] The connection timeout in milliseconds - * @param {number} [options.protocolVersion=-1] The protocol version to use in the handshake. `-1` is for auto-detection - * @returns {Promise} A promise that resolves with the server's status + * @param {string} host - The server address to ping. + * @param {JavaPingOptions} [options={}] - Optional configuration. + * @returns {Promise} A promise that resolves with the server's status. */ export async function pingJava(host, options = {}) { if (typeof host !== "string" || host.trim() === "") { @@ -150,10 +157,17 @@ export async function pingJava(host, options = {}) { } } catch (err) { // Common errors like ENODATA or ENOTFOUND are expected when a server - // does not have an SRV record, so we ignore them and proceed. - if (!["ENODATA", "ENOTFOUND"].includes(err.code)) { - debug("SRV lookup for %s failed (%s), using fallback", host, err.code); - // For other errors we should re-throw. + // does not have an SRV record, so we ignore them and proceed + if ( + err instanceof Error && + "code" in err && + (err.code === "ENODATA" || err.code === "ENOTFOUND") + ) { + // Non fatal DNS error, log it and continue + debug("SRV lookup for %s failed (%s), using fallback.", host, err.code); + } else { + // Re-throw anything else to fail the operation + debug("SRV lookup for %s failed unexpectedly, re-throwing.", host, err); throw err; } } diff --git a/lib/varint.js b/lib/varint.js index 84245d4..2139723 100644 --- a/lib/varint.js +++ b/lib/varint.js @@ -6,16 +6,16 @@ export const ERR_VARINT_BUFFER_UNDERFLOW = "VARINT_BUFFER_UNDERFLOW"; export const ERR_VARINT_MALFORMED = "VARINT_MALFORMED"; export const ERR_VARINT_ENCODE_TOO_LARGE = "VARINT_ENCODE_TOO_LARGE"; -/** - * Creates a custom error object with a code property. - * @param {string} message The error message - * @param {string} code The error code - * @returns {Error} - */ -function createVarIntError(message, code) { - const err = new Error(message); - err.code = code; - return err; +export class VarIntError extends Error { + /** + * @param {string} message The error message. + * @param {string} code The error code. + */ + constructor(message, code) { + super(message); + this.name = "VarIntError"; + this.code = code; + } } /** @@ -23,7 +23,7 @@ function createVarIntError(message, code) { * VarInts are never longer than 5 bytes for the Minecraft protocol. * @param {number} value The integer to encode * @returns {Buffer} The encoded VarInt as a buffer - * @throws {Error} if the value is too large to be encoded + * @throws {VarIntError} if the value is too large to be encoded */ export function encodeVarInt(value) { const buf = Buffer.alloc(5); @@ -42,7 +42,7 @@ export function encodeVarInt(value) { buf.writeUInt8(byte | 0x80, written++); if (written >= 5 && val > 0) { - throw createVarIntError( + throw new VarIntError( "Value too large for a 5-byte VarInt", ERR_VARINT_ENCODE_TOO_LARGE ); @@ -89,11 +89,11 @@ export function concatPackets(chunks) { * @param {Buffer} buffer The buffer to read from * @param {number} [offset=0] The starting offset in the buffer * @returns {{ value: number, bytesRead: number }} - * @throws {Error} if the buffer is too short or the VarInt is malformed + * @throws {VarIntError} if the buffer is too short or the VarInt is malformed */ export function decodeVarInt(buffer, offset = 0) { if (offset >= buffer.length) { - throw createVarIntError( + throw new VarIntError( "Buffer underflow: Cannot decode VarInt at or beyond buffer length.", ERR_VARINT_BUFFER_UNDERFLOW ); @@ -113,7 +113,7 @@ export function decodeVarInt(buffer, offset = 0) { // Max 4 more bytes (total 5 bytes for a VarInt) for (let i = 0; i < 4; i++) { if (currentOffset >= buffer.length) { - throw createVarIntError( + throw new VarIntError( "Buffer underflow: Incomplete VarInt, expected more bytes.", ERR_VARINT_BUFFER_UNDERFLOW ); @@ -131,7 +131,7 @@ export function decodeVarInt(buffer, offset = 0) { } } - throw createVarIntError( + throw new VarIntError( "VarInt is too big or malformed: 5 bytes read with continuation bit still set.", ERR_VARINT_MALFORMED ); diff --git a/package-lock.json b/package-lock.json index 60532c0..374142a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,20 @@ { "name": "@minescope/mineping", - "version": "1.7.0-beta.0", + "version": "1.7.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@minescope/mineping", - "version": "1.7.0-beta.0", + "version": "1.7.0-beta.1", "license": "MIT", "dependencies": { "debug": "^4.4.1" }, "devDependencies": { + "@types/debug": "^4.1.12", + "@types/node": "^24.0.3", + "typescript": "^5.8.3", "vitest": "^3.2.3" }, "engines": { @@ -740,6 +743,16 @@ "@types/deep-eql": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -754,6 +767,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, "node_modules/@vitest/expect": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.3.tgz", @@ -1308,6 +1338,27 @@ "node": ">=14.0.0" } }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", diff --git a/package.json b/package.json index ffe27e9..8e70489 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "types": "types/index.d.ts", "scripts": { "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "typecheck": "tsc --noEmit" }, "repository": { "type": "git", @@ -27,6 +28,9 @@ "debug": "^4.4.1" }, "devDependencies": { + "@types/debug": "^4.1.12", + "@types/node": "^24.0.3", + "typescript": "^5.8.3", "vitest": "^3.2.3" } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..79a5509 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true, + "moduleResolution": "node", + "target": "ES2022", + "lib": ["ES2022"], + "esModuleInterop": true + }, + "include": ["lib", "index.js"], + "exclude": ["node_modules"] +}