minecraft-toolkit 0.1.0 → 0.1.2

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.
@@ -0,0 +1,13 @@
1
+ import { DEFAULT_TIMEOUT_MS } from "../constants.js";
2
+ import { MinecraftToolkitError } from "../errors.js";
3
+
4
+ export function resolveTimeout(timeout) {
5
+ if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0) {
6
+ return timeout;
7
+ }
8
+ return DEFAULT_TIMEOUT_MS;
9
+ }
10
+
11
+ export function makeError(message, cause) {
12
+ return new MinecraftToolkitError(message, { cause });
13
+ }
@@ -0,0 +1,41 @@
1
+ import { fetchJavaServerStatus } from "./java/status.js";
2
+ import { fetchBedrockServerStatus } from "./bedrock/status.js";
3
+ import { MinecraftToolkitError } from "../errors.js";
4
+
5
+ export { fetchJavaServerStatus } from "./java/status.js";
6
+ export { fetchBedrockServerStatus } from "./bedrock/status.js";
7
+
8
+ export async function fetchServerStatus(address, options = {}) {
9
+ const { edition, type, ...rest } = options;
10
+ const target =
11
+ (typeof (edition ?? type) === "string" ? (edition ?? type).trim().toLowerCase() : null) ||
12
+ "java";
13
+
14
+ if (target === "java") {
15
+ return fetchJavaServerStatus(address, rest);
16
+ }
17
+
18
+ if (target === "bedrock") {
19
+ return fetchBedrockServerStatus(address, rest);
20
+ }
21
+
22
+ if (target === "auto") {
23
+ let javaError;
24
+ try {
25
+ return await fetchJavaServerStatus(address, rest);
26
+ } catch (error) {
27
+ javaError = error;
28
+ }
29
+
30
+ return fetchBedrockServerStatus(address, rest).catch((bedrockError) => {
31
+ throw new MinecraftToolkitError("Unable to query server status", {
32
+ statusCode: bedrockError.statusCode ?? javaError?.statusCode ?? 500,
33
+ cause: bedrockError,
34
+ });
35
+ });
36
+ }
37
+
38
+ throw new MinecraftToolkitError('Edition must be "java", "bedrock", or "auto"', {
39
+ statusCode: 400,
40
+ });
41
+ }
@@ -0,0 +1,65 @@
1
+ const DEFAULT_CACHE_TTL_MS = 30 * 1000;
2
+
3
+ export class ResponseCache {
4
+ constructor(ttlMs = DEFAULT_CACHE_TTL_MS) {
5
+ this.ttlMs = ttlMs;
6
+ this.store = new Map();
7
+ }
8
+
9
+ get(key) {
10
+ const entry = this.store.get(key);
11
+ if (!entry) {
12
+ return undefined;
13
+ }
14
+
15
+ if (entry.expiresAt <= Date.now()) {
16
+ this.store.delete(key);
17
+ return undefined;
18
+ }
19
+
20
+ return entry.value;
21
+ }
22
+
23
+ set(key, value) {
24
+ this.store.set(key, {
25
+ value,
26
+ expiresAt: Date.now() + this.ttlMs,
27
+ });
28
+ }
29
+
30
+ delete(key) {
31
+ this.store.delete(key);
32
+ }
33
+
34
+ clear() {
35
+ this.store.clear();
36
+ }
37
+ }
38
+
39
+ export function createCache(options = {}) {
40
+ if (options.cache === false) {
41
+ return null;
42
+ }
43
+
44
+ const ttlSeconds = options.cache?.ttlSeconds ?? options.cacheTtl ?? options.ttlSeconds ?? null;
45
+ if (ttlSeconds === null || ttlSeconds === undefined) {
46
+ return new ResponseCache();
47
+ }
48
+
49
+ return new ResponseCache(Math.max(ttlSeconds, 0) * 1000);
50
+ }
51
+
52
+ export async function withCache(cache, key, resolver) {
53
+ if (!cache) {
54
+ return resolver();
55
+ }
56
+
57
+ const cached = cache.get(key);
58
+ if (cached) {
59
+ return cached;
60
+ }
61
+
62
+ const value = await resolver();
63
+ cache.set(key, value);
64
+ return value;
65
+ }
@@ -0,0 +1,281 @@
1
+ import { MinecraftToolkitError } from "../errors.js";
2
+
3
+ const DEFAULT_CLASS_PREFIX = "mc";
4
+ const DEFAULT_ANIMATION_NAME = "mc-obfuscated-flicker";
5
+ const DEFAULT_OBFUSCATED_SPEED_MS = 110;
6
+
7
+ const COLOR_CODES = freezeNested({
8
+ 0: { name: "black", classSuffix: "black", hex: "#000000" },
9
+ 1: { name: "dark_blue", classSuffix: "dark-blue", hex: "#0000aa" },
10
+ 2: { name: "dark_green", classSuffix: "dark-green", hex: "#00aa00" },
11
+ 3: { name: "dark_aqua", classSuffix: "dark-aqua", hex: "#00aaaa" },
12
+ 4: { name: "dark_red", classSuffix: "dark-red", hex: "#aa0000" },
13
+ 5: { name: "dark_purple", classSuffix: "dark-purple", hex: "#aa00aa" },
14
+ 6: { name: "gold", classSuffix: "gold", hex: "#ffaa00" },
15
+ 7: { name: "gray", classSuffix: "gray", hex: "#aaaaaa" },
16
+ 8: { name: "dark_gray", classSuffix: "dark-gray", hex: "#555555" },
17
+ 9: { name: "blue", classSuffix: "blue", hex: "#5555ff" },
18
+ a: { name: "green", classSuffix: "green", hex: "#55ff55" },
19
+ b: { name: "aqua", classSuffix: "aqua", hex: "#55ffff" },
20
+ c: { name: "red", classSuffix: "red", hex: "#ff5555" },
21
+ d: { name: "light_purple", classSuffix: "light-purple", hex: "#ff55ff" },
22
+ e: { name: "yellow", classSuffix: "yellow", hex: "#ffff55" },
23
+ f: { name: "white", classSuffix: "white", hex: "#ffffff" },
24
+ g: { name: "minecoin_gold", classSuffix: "minecoin-gold", hex: "#e1c158" },
25
+ h: { name: "material_quartz", classSuffix: "material-quartz", hex: "#ece6d8" },
26
+ i: { name: "material_iron", classSuffix: "material-iron", hex: "#cacaca" },
27
+ j: { name: "material_netherite", classSuffix: "material-netherite", hex: "#4b4946" },
28
+ n: { name: "material_redstone", classSuffix: "material-redstone", hex: "#b02e26" },
29
+ p: { name: "material_prismarine", classSuffix: "material-prismarine", hex: "#1ba19b" },
30
+ q: { name: "material_obsidian", classSuffix: "material-obsidian", hex: "#0b0b0b" },
31
+ s: { name: "material_crimson", classSuffix: "material-crimson", hex: "#a02c44" },
32
+ t: { name: "material_gold", classSuffix: "material-gold", hex: "#d8af48" },
33
+ u: { name: "material_emerald", classSuffix: "material-emerald", hex: "#30c67c" },
34
+ v: { name: "material_diamond", classSuffix: "material-diamond", hex: "#5be5e5" },
35
+ });
36
+
37
+ const FORMAT_CODES = freezeNested({
38
+ k: { name: "obfuscated", classSuffix: "obfuscated" },
39
+ l: { name: "bold", classSuffix: "bold" },
40
+ m: { name: "strikethrough", classSuffix: "strikethrough" },
41
+ n: { name: "underline", classSuffix: "underline" },
42
+ o: { name: "italic", classSuffix: "italic" },
43
+ });
44
+
45
+ const VALID_CODE_CHARS = new Set([...Object.keys(COLOR_CODES), ...Object.keys(FORMAT_CODES), "r"]);
46
+ const COLOR_KEYS = new Set(Object.keys(COLOR_CODES));
47
+
48
+ export function toHTML(input, options) {
49
+ const value = coerceInput(input);
50
+ if (!value) {
51
+ return "";
52
+ }
53
+
54
+ const resolved = resolveRenderOptions(options);
55
+ const segments = tokenize(value);
56
+
57
+ return segments.map((segment) => renderSegment(segment, resolved)).join("");
58
+ }
59
+
60
+ export function stripCodes(input) {
61
+ const value = coerceInput(input);
62
+ return value.replaceAll(/(?:§|&)[0-9a-fghijklmnpqrstuvr]/gi, "");
63
+ }
64
+
65
+ export function hasCodes(input) {
66
+ const value = coerceInput(input);
67
+ for (let i = 0; i < value.length - 1; i += 1) {
68
+ const candidate = value[i];
69
+ if (
70
+ (candidate === "§" || candidate === "&") &&
71
+ VALID_CODE_CHARS.has(value[i + 1]?.toLowerCase())
72
+ ) {
73
+ return true;
74
+ }
75
+ }
76
+ return false;
77
+ }
78
+
79
+ export function generateCSS(options) {
80
+ const resolved = resolveRenderOptions(options);
81
+ const lines = [];
82
+
83
+ lines.push(
84
+ `.${resolved.classPrefix}-segment { color: inherit; font-weight: inherit; font-style: inherit; }`,
85
+ );
86
+
87
+ Object.values(COLOR_CODES).forEach((entry) => {
88
+ lines.push(`.${resolved.classPrefix}-color-${entry.classSuffix} { color: ${entry.hex}; }`);
89
+ });
90
+
91
+ lines.push(`.${resolved.classPrefix}-format-bold { font-weight: 700; }`);
92
+ lines.push(`.${resolved.classPrefix}-format-italic { font-style: italic; }`);
93
+ lines.push(`.${resolved.classPrefix}-format-underline { text-decoration: underline; }`);
94
+ lines.push(`.${resolved.classPrefix}-format-strikethrough { text-decoration: line-through; }`);
95
+ lines.push(
96
+ `.${resolved.classPrefix}-format-underline.${resolved.classPrefix}-format-strikethrough { text-decoration: underline line-through; }`,
97
+ );
98
+ lines.push(
99
+ `.${resolved.classPrefix}-format-obfuscated { animation: ${resolved.animationName} ${resolved.obfuscatedSpeedMs}ms steps(10, end) infinite; display: inline-block; }`,
100
+ );
101
+
102
+ lines.push(
103
+ `@keyframes ${resolved.animationName} { 0%, 100% { opacity: 0.8; } 50% { opacity: 0.2; } }`,
104
+ );
105
+
106
+ return lines.join("\n");
107
+ }
108
+
109
+ export function convertPrefix(input, direction = "toSection") {
110
+ const value = coerceInput(input);
111
+
112
+ const normalized = direction?.toLowerCase();
113
+ if (normalized !== "tosection" && normalized !== "toampersand") {
114
+ throw new MinecraftToolkitError("direction must be either 'toSection' or 'toAmpersand'");
115
+ }
116
+
117
+ if (normalized === "toampersand") {
118
+ return value.replaceAll("§", "&");
119
+ }
120
+ return value.replaceAll("&", "§");
121
+ }
122
+
123
+ export function getMaps() {
124
+ return {
125
+ colors: COLOR_CODES,
126
+ formats: FORMAT_CODES,
127
+ };
128
+ }
129
+
130
+ function tokenize(input) {
131
+ const segments = [];
132
+ let color = null;
133
+ const formats = new Set();
134
+ let buffer = "";
135
+
136
+ for (let i = 0; i < input.length; i += 1) {
137
+ const char = input[i];
138
+ const next = input[i + 1]?.toLowerCase();
139
+
140
+ if ((char === "§" || char === "&") && next && VALID_CODE_CHARS.has(next)) {
141
+ if (buffer) {
142
+ segments.push({ text: buffer, color, formats: Array.from(formats) });
143
+ buffer = "";
144
+ }
145
+
146
+ if (COLOR_KEYS.has(next)) {
147
+ color = next;
148
+ formats.clear();
149
+ } else if (next === "r") {
150
+ color = null;
151
+ formats.clear();
152
+ } else {
153
+ formats.add(next);
154
+ }
155
+ i += 1;
156
+ continue;
157
+ }
158
+
159
+ buffer += char;
160
+ }
161
+
162
+ if (buffer) {
163
+ segments.push({ text: buffer, color, formats: Array.from(formats) });
164
+ }
165
+
166
+ return segments;
167
+ }
168
+
169
+ function renderSegment(segment, options) {
170
+ const safeText = options.escapeHtml ? escapeHtml(segment.text) : segment.text;
171
+ const needsStyling = segment.color || segment.formats.length;
172
+ if (!needsStyling) {
173
+ return safeText;
174
+ }
175
+
176
+ if (options.mode === "class") {
177
+ const classNames = buildClassNames(segment, options);
178
+ return classNames.length
179
+ ? `<span class="${classNames.join(" ")}">${safeText}</span>`
180
+ : safeText;
181
+ }
182
+
183
+ const inlineStyle = buildInlineStyle(segment, options);
184
+ return inlineStyle ? `<span style="${inlineStyle}">${safeText}</span>` : safeText;
185
+ }
186
+
187
+ function buildClassNames(segment, options) {
188
+ const classes = [`${options.classPrefix}-segment`];
189
+
190
+ if (segment.color) {
191
+ const colorMeta = COLOR_CODES[segment.color];
192
+ classes.push(`${options.classPrefix}-color-${colorMeta.classSuffix}`);
193
+ }
194
+
195
+ segment.formats.forEach((code) => {
196
+ const meta = FORMAT_CODES[code];
197
+ if (meta) {
198
+ classes.push(`${options.classPrefix}-format-${meta.classSuffix}`);
199
+ }
200
+ });
201
+
202
+ return classes;
203
+ }
204
+
205
+ function buildInlineStyle(segment, options) {
206
+ const declarations = [];
207
+ const textDecorations = new Set();
208
+
209
+ if (segment.color) {
210
+ declarations.push(`color: ${COLOR_CODES[segment.color].hex}`);
211
+ }
212
+
213
+ segment.formats.forEach((code) => {
214
+ switch (code) {
215
+ case "l":
216
+ declarations.push("font-weight: 700");
217
+ break;
218
+ case "o":
219
+ declarations.push("font-style: italic");
220
+ break;
221
+ case "m":
222
+ textDecorations.add("line-through");
223
+ break;
224
+ case "n":
225
+ textDecorations.add("underline");
226
+ break;
227
+ case "k":
228
+ declarations.push(
229
+ `animation: ${options.animationName} ${options.obfuscatedSpeedMs}ms steps(10, end) infinite`,
230
+ );
231
+ declarations.push("display: inline-block");
232
+ break;
233
+ default:
234
+ break;
235
+ }
236
+ });
237
+
238
+ if (textDecorations.size) {
239
+ declarations.push(`text-decoration: ${Array.from(textDecorations).join(" ")}`);
240
+ }
241
+
242
+ return declarations.join("; ");
243
+ }
244
+
245
+ function escapeHtml(value) {
246
+ return value
247
+ .replaceAll(/&/g, "&amp;")
248
+ .replaceAll(/</g, "&lt;")
249
+ .replaceAll(/>/g, "&gt;")
250
+ .replaceAll(/"/g, "&quot;")
251
+ .replaceAll(/'/g, "&#39;");
252
+ }
253
+
254
+ function coerceInput(input) {
255
+ if (input == null) {
256
+ return "";
257
+ }
258
+ return typeof input === "string" ? input : String(input);
259
+ }
260
+
261
+ function resolveRenderOptions(options = {}) {
262
+ const mode = options.mode === "class" ? "class" : "inline";
263
+ const classPrefix = options.classPrefix ?? DEFAULT_CLASS_PREFIX;
264
+ const animationName = options.animationName ?? DEFAULT_ANIMATION_NAME;
265
+ const obfuscatedSpeedMs = Number.isFinite(options.obfuscatedSpeedMs)
266
+ ? Number(options.obfuscatedSpeedMs)
267
+ : DEFAULT_OBFUSCATED_SPEED_MS;
268
+
269
+ return {
270
+ mode,
271
+ classPrefix,
272
+ animationName,
273
+ obfuscatedSpeedMs,
274
+ escapeHtml: options.escapeHtml !== false,
275
+ };
276
+ }
277
+
278
+ function freezeNested(map) {
279
+ Object.values(map).forEach((entry) => Object.freeze(entry));
280
+ return Object.freeze(map);
281
+ }
@@ -0,0 +1,23 @@
1
+ import { DEFAULT_HEADERS } from "../../constants.js";
2
+ import { MinecraftToolkitError } from "../../errors.js";
3
+
4
+ export async function fetchJson(url, { notFoundMessage, headers } = {}) {
5
+ const response = await fetch(url, {
6
+ headers: {
7
+ ...DEFAULT_HEADERS,
8
+ ...headers,
9
+ },
10
+ });
11
+
12
+ if (response.status === 404 && notFoundMessage) {
13
+ throw new MinecraftToolkitError(notFoundMessage, { statusCode: 404 });
14
+ }
15
+
16
+ if (!response.ok) {
17
+ throw new MinecraftToolkitError(`Failed to fetch ${url}`, {
18
+ statusCode: response.status,
19
+ });
20
+ }
21
+
22
+ return response.json();
23
+ }
@@ -0,0 +1,15 @@
1
+ import { validatePort } from "./validation.js";
2
+
3
+ export function resolveAddress(address, overridePort, fallbackPort) {
4
+ if (overridePort) {
5
+ return { host: address, port: validatePort(overridePort) };
6
+ }
7
+
8
+ const parts = address.split(":");
9
+ if (parts.length > 1 && parts[parts.length - 1] !== "") {
10
+ const extractedPort = parts.pop();
11
+ return { host: parts.join(":"), port: validatePort(extractedPort) };
12
+ }
13
+
14
+ return { host: address, port: fallbackPort };
15
+ }
@@ -0,0 +1,28 @@
1
+ import { MinecraftToolkitError } from "../errors.js";
2
+
3
+ export function normalizeAddress(address) {
4
+ if (!address || typeof address !== "string") {
5
+ throw new MinecraftToolkitError("Server address is required", { statusCode: 400 });
6
+ }
7
+
8
+ return address.trim();
9
+ }
10
+
11
+ export function normalizeUsername(username) {
12
+ if (!username || typeof username !== "string") {
13
+ throw new MinecraftToolkitError("Username is required", { statusCode: 400 });
14
+ }
15
+
16
+ return username.trim();
17
+ }
18
+
19
+ export function validatePort(port) {
20
+ const parsed = Number(port);
21
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
22
+ throw new MinecraftToolkitError("Port must be an integer between 1 and 65535", {
23
+ statusCode: 400,
24
+ });
25
+ }
26
+
27
+ return parsed;
28
+ }