minecraft-toolkit 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  <!-- /automd -->
10
10
 
11
- Lightweight Mojang player utilities (profile, skin, UUID) for Node, Vite, and edge projects.
11
+ Lightweight Minecraft API + infrastructure toolkit: player profiles & textures, Java/Bedrock server status probes, and Votifier (v1/v2) clients that run in Node, Vite, and edge runtimes.
12
12
 
13
13
  > This toolkit wraps Mojang APIs. Rate limits and availability still apply. Write endpoints (name change, skin upload) are not yet included.
14
14
 
@@ -151,6 +151,48 @@ console.log(javaStatus.players.online, bedrockStatus.motd.text);
151
151
  Both helpers normalize MOTD text, favicon/Base64 icons, latency, and version info. Errors surface as
152
152
  `MinecraftToolkitError` with contextual status codes.
153
153
 
154
+ ### Server Icon Helper
155
+
156
+ ```ts
157
+ import { fetchServerIcon } from "minecraft-toolkit";
158
+
159
+ const icon = await fetchServerIcon("play.example.net");
160
+ console.log(icon.base64); // "iVBOR..."
161
+ console.log(icon.byteLength); // raw PNG size in bytes
162
+ ```
163
+
164
+ The helper reuses the Java status ping to extract the favicon, returning:
165
+
166
+ - `dataUri`: ready-to-render `data:image/png;base64,...`
167
+ - `base64`: raw Base64 payload
168
+ - `buffer` + `byteLength` for further processing (e.g., resizing, hashing)
169
+
170
+ If the server doesn’t expose an icon, it throws `MinecraftToolkitError` (404).
171
+
172
+ ## Votifier Client (Java)
173
+
174
+ Send vote notifications to classic Votifier v1 (RSA public key) and NuVotifier v2 (token/HMAC) servers without re-implementing either protocol.
175
+
176
+ ```ts
177
+ import { sendVotifierVote } from "minecraft-toolkit";
178
+
179
+ const result = await sendVotifierVote({
180
+ host: "votifier.myserver.net",
181
+ port: 8192, // defaults to 8192 if omitted
182
+ publicKey: process.env.VOTIFIER_PUBLIC_KEY!, // v1 servers
183
+ serviceName: "MyTopList",
184
+ username: "26bz",
185
+ address: "198.51.100.42",
186
+ token: listingSiteConfig.token, // v2 servers (optional)
187
+ protocol: "auto", // let the handshake decide between v1/v2
188
+ });
189
+
190
+ console.log(result.acknowledged, result.version, result.protocol);
191
+ ```
192
+
193
+ - Provide either a legacy RSA public key (for protocol v1) **or** a NuVotifier token (protocol v2). Server listing sites typically store each server's token and pass it here; `protocol: "auto"` will select the right flow based on the handshake.
194
+ - `timestamp` accepts a `Date` or millisecond value (default: `Date.now()`). All failures bubble as `MinecraftToolkitError`.
195
+
154
196
  ## Minecraft Formatting Renderer
155
197
 
156
198
  Convert legacy `§` or `&` codes into safe HTML fragments or CSS class spans.
package/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { H3 } from "h3";
2
+ import type { Buffer } from "node:buffer";
2
3
 
3
4
  export interface SkinTexture {
4
5
  url: string;
@@ -182,7 +183,19 @@ export interface BedrockServerStatus {
182
183
 
183
184
  export type ServerStatus = JavaServerStatus | BedrockServerStatus;
184
185
 
185
- export function fetchServerStatus(address: string, options?: ServerStatusOptions): Promise<ServerStatus>;
186
+ export interface ServerIconResult {
187
+ host: string;
188
+ port: number;
189
+ dataUri: string;
190
+ base64: string;
191
+ buffer: Buffer;
192
+ byteLength: number;
193
+ }
194
+
195
+ export function fetchServerStatus(
196
+ address: string,
197
+ options?: ServerStatusOptions,
198
+ ): Promise<ServerStatus>;
186
199
  export function fetchJavaServerStatus(
187
200
  address: string,
188
201
  options?: JavaServerStatusOptions,
@@ -191,6 +204,31 @@ export function fetchBedrockServerStatus(
191
204
  address: string,
192
205
  options?: BedrockServerStatusOptions,
193
206
  ): Promise<BedrockServerStatus>;
207
+ export function fetchServerIcon(
208
+ address: string,
209
+ options?: JavaServerStatusOptions,
210
+ ): Promise<ServerIconResult>;
211
+
212
+ export interface VotifierVoteOptions {
213
+ host: string;
214
+ port?: number;
215
+ publicKey: string;
216
+ serviceName: string;
217
+ username: string;
218
+ address: string;
219
+ timestamp?: number | Date;
220
+ timeoutMs?: number;
221
+ token?: string;
222
+ protocol?: "auto" | "v1" | "v2";
223
+ }
224
+
225
+ export interface VotifierVoteResult {
226
+ acknowledged: boolean;
227
+ version: string | null;
228
+ protocol: "v1" | "v2";
229
+ }
230
+
231
+ export function sendVotifierVote(options: VotifierVoteOptions): Promise<VotifierVoteResult>;
194
232
 
195
233
  export interface PlayerHandlers {
196
234
  profileHandler: any;
package/index.js CHANGED
@@ -33,3 +33,5 @@ export {
33
33
  convertPrefix,
34
34
  getMaps,
35
35
  } from "./src/utils/formatting.js";
36
+ export { sendVotifierVote } from "./src/server/votifier/index.js";
37
+ export { fetchServerIcon } from "./src/server/icon.js";
package/package.json CHANGED
@@ -1,17 +1,21 @@
1
1
  {
2
2
  "name": "minecraft-toolkit",
3
- "version": "0.1.3",
4
- "description": "Developer toolkit for working with Mojang Minecraft player data, skins, and utilities.",
3
+ "version": "0.1.4",
4
+ "description": "Minecraft toolkit for Mojang player data, server status probes, and Votifier v1/v2 clients.",
5
5
  "keywords": [
6
6
  "minecraft",
7
7
  "minecraft-api",
8
8
  "minecraft-player",
9
+ "minecraft-server",
9
10
  "minecraft-skins",
11
+ "minecraft-status",
10
12
  "minecraft-tools",
11
13
  "minecraft-utils",
12
14
  "minecraft-uuid",
15
+ "minecraft-votifier",
13
16
  "mojang",
14
- "mojang-api"
17
+ "mojang-api",
18
+ "votifier"
15
19
  ],
16
20
  "homepage": "https://github.com/26bz/minecraft-toolkit",
17
21
  "bugs": {
package/src/constants.js CHANGED
@@ -11,6 +11,7 @@ export const PACKAGE_METADATA = {
11
11
 
12
12
  export const DEFAULT_JAVA_PORT = 25565;
13
13
  export const DEFAULT_BEDROCK_PORT = 19132;
14
+ export const DEFAULT_VOTIFIER_PORT = 8192;
14
15
  export const DEFAULT_CACHE_TTL_SECONDS = 30;
15
16
  export const DEFAULT_TIMEOUT_MS = 5000;
16
17
  export const DEFAULT_PROTOCOL_VERSION = 760; // Minecraft 1.20.4
package/src/h3/routes.js CHANGED
@@ -1,4 +1,12 @@
1
- import { H3, defineHandler, definePlugin, readBody, getQuery } from "h3";
1
+ import {
2
+ H3,
3
+ defineHandler,
4
+ definePlugin,
5
+ readBody,
6
+ getQuery,
7
+ getRouterParam,
8
+ getRequestHeader,
9
+ } from "h3";
2
10
  import { MinecraftToolkitError } from "../errors.js";
3
11
  import {
4
12
  fetchPlayerProfile,
@@ -17,10 +25,11 @@ import {
17
25
  fetchBlockedServers,
18
26
  } from "../player/account/index.js";
19
27
  import { fetchServerStatus } from "../server/status.js";
28
+ import { fetchServerIcon } from "../server/icon.js";
20
29
  import { normalizeAddress, validatePort } from "../utils/validation.js";
21
30
 
22
- function requireParam(event, key, message) {
23
- const value = event.context.params?.[key];
31
+ function requireRouterParam(event, key, message) {
32
+ const value = getRouterParam(event, key);
24
33
  if (!value) {
25
34
  throw new MinecraftToolkitError(message, { statusCode: 400 });
26
35
  }
@@ -29,37 +38,37 @@ function requireParam(event, key, message) {
29
38
 
30
39
  export function createPlayerHandlers() {
31
40
  const profileHandler = defineHandler(async (event) => {
32
- const username = requireParam(event, "username", "Username parameter is required");
41
+ const username = requireRouterParam(event, "username", "Username parameter is required");
33
42
  return fetchPlayerProfile(username);
34
43
  });
35
44
 
36
45
  const skinHandler = defineHandler(async (event) => {
37
- const username = requireParam(event, "username", "Username parameter is required");
46
+ const username = requireRouterParam(event, "username", "Username parameter is required");
38
47
  return fetchPlayerSkin(username);
39
48
  });
40
49
 
41
50
  const summaryHandler = defineHandler(async (event) => {
42
- const username = requireParam(event, "username", "Username parameter is required");
51
+ const username = requireRouterParam(event, "username", "Username parameter is required");
43
52
  return fetchPlayerSummary(username);
44
53
  });
45
54
 
46
55
  const uuidHandler = defineHandler(async (event) => {
47
- const username = requireParam(event, "username", "Username parameter is required");
56
+ const username = requireRouterParam(event, "username", "Username parameter is required");
48
57
  return fetchPlayerUUID(username);
49
58
  });
50
59
 
51
60
  const resolverHandler = defineHandler(async (event) => {
52
- const input = requireParam(event, "input", "Username or UUID parameter is required");
61
+ const input = requireRouterParam(event, "input", "Username or UUID parameter is required");
53
62
  return resolvePlayer(input);
54
63
  });
55
64
 
56
65
  const nameHistoryHandler = defineHandler(async (event) => {
57
- const uuid = requireParam(event, "uuid", "UUID parameter is required");
66
+ const uuid = requireRouterParam(event, "uuid", "UUID parameter is required");
58
67
  return fetchNameHistory(uuid);
59
68
  });
60
69
 
61
70
  const existsHandler = defineHandler(async (event) => {
62
- const username = requireParam(event, "username", "Username parameter is required");
71
+ const username = requireRouterParam(event, "username", "Username parameter is required");
63
72
  const exists = await playerExists(username);
64
73
  return { username, exists };
65
74
  });
@@ -82,7 +91,7 @@ export function createPlayerHandlers() {
82
91
  });
83
92
 
84
93
  const nameAvailabilityHandler = defineHandler(async (event) => {
85
- const name = requireParam(event, "name", "Name parameter is required");
94
+ const name = requireRouterParam(event, "name", "Name parameter is required");
86
95
  const token = requireAccessToken(event);
87
96
  return checkNameAvailability(name, token);
88
97
  });
@@ -100,9 +109,31 @@ export function createPlayerHandlers() {
100
109
 
101
110
  const blockedServersHandler = defineHandler(async () => fetchBlockedServers());
102
111
 
112
+ const serverIconHandler = defineHandler(async (event) => {
113
+ const address = normalizeAddress(
114
+ requireRouterParam(event, "address", "Server address parameter is required"),
115
+ );
116
+ const query = getQuery(event);
117
+ const port = typeof query.port === "string" ? validatePort(query.port) : undefined;
118
+ const timeoutMs =
119
+ typeof query.timeoutMs === "string" ? Number.parseInt(query.timeoutMs, 10) : undefined;
120
+ const protocolVersion =
121
+ typeof query.protocolVersion === "string"
122
+ ? Number.parseInt(query.protocolVersion, 10)
123
+ : undefined;
124
+
125
+ const icon = await fetchServerIcon(address, {
126
+ port,
127
+ timeoutMs,
128
+ protocolVersion,
129
+ });
130
+
131
+ return icon.buffer;
132
+ });
133
+
103
134
  const serverStatusHandler = defineHandler(async (event) => {
104
135
  const address = normalizeAddress(
105
- requireParam(event, "address", "Server address parameter is required"),
136
+ requireRouterParam(event, "address", "Server address parameter is required"),
106
137
  );
107
138
  const query = getQuery(event);
108
139
  const edition = typeof query.edition === "string" ? query.edition : undefined;
@@ -136,6 +167,7 @@ export function createPlayerHandlers() {
136
167
  giftCodeValidationHandler,
137
168
  blockedServersHandler,
138
169
  serverStatusHandler,
170
+ serverIconHandler,
139
171
  };
140
172
  }
141
173
 
@@ -195,6 +227,10 @@ export function createPlayerApp(options = {}) {
195
227
  meta: { category: "server", resource: "status" },
196
228
  });
197
229
 
230
+ app.get("/server/:address/icon", handlers.serverIconHandler, {
231
+ meta: { category: "server", resource: "icon" },
232
+ });
233
+
198
234
  return { app, handlers };
199
235
  }
200
236
 
@@ -204,7 +240,7 @@ export const playerPlugin = definePlugin((app) => {
204
240
  });
205
241
 
206
242
  function requireAccessToken(event) {
207
- const header = event.req?.headers?.get?.("authorization") ?? "";
243
+ const header = getRequestHeader(event, "authorization") ?? "";
208
244
  if (!header.toLowerCase().startsWith("bearer ")) {
209
245
  throw new MinecraftToolkitError("Authorization header with Bearer token is required", {
210
246
  statusCode: 401,
@@ -0,0 +1,28 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { fetchJavaServerStatus } from "./status.js";
3
+ import { MinecraftToolkitError } from "../errors.js";
4
+
5
+ export async function fetchServerIcon(address, options = {}) {
6
+ const status = await fetchJavaServerStatus(address, options);
7
+ const favicon = status?.favicon ?? status?.raw?.favicon;
8
+ if (!favicon || typeof favicon !== "string") {
9
+ throw new MinecraftToolkitError("Server did not expose a favicon", { statusCode: 404 });
10
+ }
11
+
12
+ const base64 = extractBase64(favicon);
13
+ const buffer = Buffer.from(base64, "base64");
14
+
15
+ return {
16
+ host: status.host,
17
+ port: status.port,
18
+ dataUri: `data:image/png;base64,${base64}`,
19
+ base64,
20
+ buffer,
21
+ byteLength: buffer.byteLength,
22
+ };
23
+ }
24
+
25
+ function extractBase64(favicon) {
26
+ const prefix = "data:image/png;base64,";
27
+ return favicon.startsWith(prefix) ? favicon.slice(prefix.length) : favicon;
28
+ }
@@ -0,0 +1,328 @@
1
+ import { Socket } from "node:net";
2
+ import {
3
+ createPublicKey,
4
+ publicEncrypt,
5
+ constants as cryptoConstants,
6
+ createHmac,
7
+ randomBytes,
8
+ } from "node:crypto";
9
+ import { DEFAULT_TIMEOUT_MS, DEFAULT_VOTIFIER_PORT } from "../../constants.js";
10
+ import { MinecraftToolkitError } from "../../errors.js";
11
+ import { normalizeAddress, normalizeUsername } from "../../utils/validation.js";
12
+ import { resolveTimeout, makeError } from "../shared.js";
13
+
14
+ const HANDSHAKE_PREFIX = "VOTIFIER";
15
+
16
+ const PROTOCOL_V1 = "v1";
17
+ const PROTOCOL_V2 = "v2";
18
+
19
+ export async function sendVotifierVote(options = {}) {
20
+ const {
21
+ host,
22
+ port = DEFAULT_VOTIFIER_PORT,
23
+ publicKey,
24
+ serviceName,
25
+ username,
26
+ address,
27
+ timestamp = Date.now(),
28
+ timeoutMs = DEFAULT_TIMEOUT_MS,
29
+ token,
30
+ protocol = "auto",
31
+ } = options;
32
+
33
+ if (!host) {
34
+ throw new MinecraftToolkitError("Votifier host is required", { statusCode: 400 });
35
+ }
36
+
37
+ if (!serviceName || typeof serviceName !== "string") {
38
+ throw new MinecraftToolkitError("Service name is required", { statusCode: 400 });
39
+ }
40
+ if (!username || typeof username !== "string") {
41
+ throw new MinecraftToolkitError("Username is required", { statusCode: 400 });
42
+ }
43
+ if (!address || typeof address !== "string") {
44
+ throw new MinecraftToolkitError("Voter IP address is required", { statusCode: 400 });
45
+ }
46
+
47
+ const normalizedHost = normalizeAddress(host);
48
+ const normalizedUsername = normalizeUsername(username);
49
+ const sanitizedService = serviceName.trim();
50
+ const sanitizedAddress = address.trim();
51
+ if (!sanitizedService) {
52
+ throw new MinecraftToolkitError("Service name cannot be empty", { statusCode: 400 });
53
+ }
54
+ if (!sanitizedAddress) {
55
+ throw new MinecraftToolkitError("Voter IP address cannot be empty", { statusCode: 400 });
56
+ }
57
+
58
+ const resolvedPort = Number.isInteger(port) ? port : DEFAULT_VOTIFIER_PORT;
59
+ const resolvedTimeout = resolveTimeout(timeoutMs);
60
+ const normalizedProtocol = normalizeProtocol(protocol);
61
+ const hasPublicKey = typeof publicKey === "string" && publicKey.trim().length > 0;
62
+ const hasToken = typeof token === "string" && token.trim().length > 0;
63
+
64
+ if (normalizedProtocol === PROTOCOL_V1 && !hasPublicKey) {
65
+ throw new MinecraftToolkitError("Votifier public key is required for protocol v1", {
66
+ statusCode: 400,
67
+ });
68
+ }
69
+ if (normalizedProtocol === PROTOCOL_V2 && !hasToken) {
70
+ throw new MinecraftToolkitError("Votifier token is required for protocol v2", {
71
+ statusCode: 400,
72
+ });
73
+ }
74
+ if (normalizedProtocol === "auto" && !hasPublicKey && !hasToken) {
75
+ throw new MinecraftToolkitError("Either a public key or token must be provided", {
76
+ statusCode: 400,
77
+ });
78
+ }
79
+
80
+ const unixSeconds =
81
+ typeof timestamp === "number"
82
+ ? Math.floor(timestamp / 1000)
83
+ : Math.floor(timestamp.getTime() / 1000);
84
+ const voteString = buildVotePayload({
85
+ serviceName: sanitizedService,
86
+ username: normalizedUsername,
87
+ address: sanitizedAddress,
88
+ timestamp: unixSeconds,
89
+ });
90
+ const voteBufferV1 = hasPublicKey
91
+ ? encryptVotePayload(Buffer.from(voteString, "utf8"), publicKey)
92
+ : null;
93
+
94
+ return new Promise((resolve, reject) => {
95
+ const socket = new Socket();
96
+ socket.setNoDelay?.(true);
97
+
98
+ let settled = false;
99
+ let handshakeBuffer = Buffer.alloc(0);
100
+ let handshakeReceived = false;
101
+ let reportedVersion = null;
102
+ let voteDispatched = false;
103
+ let selectedProtocol = normalizedProtocol === "auto" ? null : normalizedProtocol;
104
+
105
+ function cleanup() {
106
+ socket.removeAllListeners();
107
+ socket.destroy();
108
+ }
109
+
110
+ function resolveOnce(value) {
111
+ if (settled) {
112
+ return;
113
+ }
114
+ settled = true;
115
+ cleanup();
116
+ resolve(value);
117
+ }
118
+
119
+ function rejectOnce(error) {
120
+ if (settled) {
121
+ return;
122
+ }
123
+ settled = true;
124
+ cleanup();
125
+ reject(
126
+ error instanceof MinecraftToolkitError
127
+ ? error
128
+ : makeError(`Unable to send Votifier vote to ${normalizedHost}:${resolvedPort}`, error),
129
+ );
130
+ }
131
+
132
+ socket.setTimeout(resolvedTimeout, () => {
133
+ rejectOnce(
134
+ new MinecraftToolkitError(
135
+ `Timed out while sending vote to ${normalizedHost}:${resolvedPort}`,
136
+ {
137
+ statusCode: 504,
138
+ },
139
+ ),
140
+ );
141
+ });
142
+
143
+ socket.once("error", (error) => {
144
+ rejectOnce(makeError(`Unable to connect to ${normalizedHost}:${resolvedPort}`, error));
145
+ });
146
+
147
+ socket.on("data", (chunk) => {
148
+ handshakeBuffer = Buffer.concat([handshakeBuffer, chunk]);
149
+ const handshakeText = handshakeBuffer.toString("utf8").trim();
150
+ if (!handshakeText.startsWith(HANDSHAKE_PREFIX)) {
151
+ return;
152
+ }
153
+ handshakeReceived = true;
154
+
155
+ const { version, challenge } = parseHandshake(handshakeText);
156
+ reportedVersion = version;
157
+
158
+ if (!selectedProtocol || selectedProtocol === "auto") {
159
+ if (version?.startsWith("2") && hasToken) {
160
+ selectedProtocol = PROTOCOL_V2;
161
+ } else if (hasPublicKey) {
162
+ selectedProtocol = PROTOCOL_V1;
163
+ } else if (hasToken) {
164
+ selectedProtocol = PROTOCOL_V2;
165
+ }
166
+ }
167
+
168
+ if (selectedProtocol === PROTOCOL_V1 && !hasPublicKey) {
169
+ rejectOnce(
170
+ new MinecraftToolkitError("Server expects Votifier v1 but no public key was provided", {
171
+ statusCode: 400,
172
+ }),
173
+ );
174
+ return;
175
+ }
176
+ if (selectedProtocol === PROTOCOL_V2 && !hasToken) {
177
+ rejectOnce(
178
+ new MinecraftToolkitError("Server expects Votifier v2 but no token was provided", {
179
+ statusCode: 400,
180
+ }),
181
+ );
182
+ return;
183
+ }
184
+
185
+ sendVote(challenge);
186
+ });
187
+
188
+ socket.on("close", () => {
189
+ if (settled) {
190
+ return;
191
+ }
192
+ if (!handshakeReceived || !voteDispatched) {
193
+ rejectOnce(
194
+ new MinecraftToolkitError("Connection closed before Votifier vote was sent", {
195
+ statusCode: 502,
196
+ }),
197
+ );
198
+ return;
199
+ }
200
+ resolveOnce({
201
+ acknowledged: true,
202
+ version: reportedVersion,
203
+ protocol: selectedProtocol ?? PROTOCOL_V1,
204
+ });
205
+ });
206
+
207
+ socket.connect(resolvedPort, normalizedHost);
208
+
209
+ function sendVote(challengeSegment) {
210
+ socket.removeAllListeners("data");
211
+ try {
212
+ voteDispatched = true;
213
+ if (selectedProtocol === PROTOCOL_V2) {
214
+ const payload = buildV2Payload({
215
+ serviceName: sanitizedService,
216
+ username: normalizedUsername,
217
+ address: sanitizedAddress,
218
+ timestamp: unixSeconds,
219
+ token,
220
+ challengeSegment,
221
+ });
222
+ socket.write(payload, (writeError) => {
223
+ if (writeError) {
224
+ rejectOnce(makeError("Failed to deliver Votifier v2 payload", writeError));
225
+ return;
226
+ }
227
+ socket.once("data", (response) => {
228
+ try {
229
+ const parsed = JSON.parse(response.toString("utf8"));
230
+ if (parsed?.status === "error") {
231
+ rejectOnce(
232
+ new MinecraftToolkitError(parsed.errorMessage || "Votifier v2 error", {
233
+ cause: parsed,
234
+ statusCode: 502,
235
+ }),
236
+ );
237
+ } else {
238
+ socket.end();
239
+ }
240
+ } catch (parseError) {
241
+ rejectOnce(makeError("Invalid Votifier v2 response", parseError));
242
+ }
243
+ });
244
+ });
245
+ return;
246
+ }
247
+
248
+ socket.write(voteBufferV1, (writeError) => {
249
+ if (writeError) {
250
+ rejectOnce(makeError("Failed to deliver Votifier payload", writeError));
251
+ return;
252
+ }
253
+ socket.end();
254
+ });
255
+ } catch (sendError) {
256
+ rejectOnce(makeError("Failed to send Votifier payload", sendError));
257
+ }
258
+ }
259
+ });
260
+ }
261
+
262
+ function buildVotePayload({ serviceName, username, address, timestamp }) {
263
+ return `VOTE\n${serviceName}\n${username}\n${address}\n${timestamp}\n`;
264
+ }
265
+
266
+ function buildV2Payload({ serviceName, username, address, timestamp, token, challengeSegment }) {
267
+ const normalizedChallenge = challengeSegment?.trim() || randomBytes(16).toString("hex");
268
+ const vote = {
269
+ serviceName,
270
+ username,
271
+ address,
272
+ timestamp,
273
+ challenge: normalizedChallenge,
274
+ };
275
+ const payload = JSON.stringify(vote);
276
+ const signature = createHmac("sha256", token).update(payload).digest("base64");
277
+ const message = JSON.stringify({ payload, signature });
278
+ const buffer = Buffer.alloc(4 + Buffer.byteLength(message));
279
+ buffer.writeUInt16BE(0x733a, 0);
280
+ buffer.writeUInt16BE(Buffer.byteLength(message), 2);
281
+ buffer.write(message, 4, "utf8");
282
+ return buffer;
283
+ }
284
+
285
+ function encryptVotePayload(payload, publicKey) {
286
+ let key;
287
+ try {
288
+ key = createPublicKey(
289
+ publicKey.includes("BEGIN")
290
+ ? publicKey
291
+ : `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`,
292
+ );
293
+ } catch (error) {
294
+ throw new MinecraftToolkitError("Invalid Votifier public key", {
295
+ statusCode: 400,
296
+ cause: error,
297
+ });
298
+ }
299
+
300
+ try {
301
+ return publicEncrypt(
302
+ {
303
+ key,
304
+ padding: cryptoConstants.RSA_PKCS1_PADDING,
305
+ },
306
+ payload,
307
+ );
308
+ } catch (error) {
309
+ throw new MinecraftToolkitError("Failed to encrypt Votifier payload", {
310
+ statusCode: 400,
311
+ cause: error,
312
+ });
313
+ }
314
+ }
315
+
316
+ function parseHandshake(text) {
317
+ const parts = text.split(" ");
318
+ return {
319
+ version: parts[1] ?? null,
320
+ challenge: parts[2] ?? null,
321
+ };
322
+ }
323
+
324
+ function normalizeProtocol(protocol) {
325
+ return protocol === PROTOCOL_V1 || protocol === PROTOCOL_V2 || protocol === "auto"
326
+ ? protocol
327
+ : "auto";
328
+ }