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.
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/index.js +35 -0
- package/package.json +71 -7
- package/src/constants.js +30 -0
- package/src/errors.js +8 -0
- package/src/h3/routes.js +218 -0
- package/src/player/account/index.js +76 -0
- package/src/player/identity/index.js +35 -0
- package/src/player/profile/index.js +144 -0
- package/src/player/resolve.js +31 -0
- package/src/player/skin.js +93 -0
- package/src/player/textures.js +39 -0
- package/src/server/bedrock/status.js +147 -0
- package/src/server/java/status.js +259 -0
- package/src/server/shared.js +13 -0
- package/src/server/status.js +41 -0
- package/src/utils/cache/index.js +65 -0
- package/src/utils/formatting.js +281 -0
- package/src/utils/http/index.js +23 -0
- package/src/utils/network.js +15 -0
- package/src/utils/validation.js +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 26bz https://26bz.com/
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# minecraft-toolkit
|
|
2
|
+
|
|
3
|
+
<!-- automd:badges name="minecraft-toolkit" github="26bz/minecraft-toolkit" license -->
|
|
4
|
+
|
|
5
|
+
[](https://npmjs.com/package/minecraft-toolkit)
|
|
6
|
+
[](https://npm.chart.dev/minecraft-toolkit)
|
|
7
|
+
[](https://github.com/26bz/minecraft-toolkit/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
<!-- /automd -->
|
|
10
|
+
|
|
11
|
+
Lightweight Mojang player utilities (profile, skin, UUID) for Node, Vite, and edge projects.
|
|
12
|
+
|
|
13
|
+
> This toolkit wraps Mojang APIs. Rate limits and availability still apply. Write endpoints (name change, skin upload) are not yet included.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
<!-- automd:pm-install name="minecraft-toolkit" -->
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
# ✨ Auto-detect
|
|
21
|
+
npx nypm install minecraft-toolkit
|
|
22
|
+
|
|
23
|
+
# npm
|
|
24
|
+
npm install minecraft-toolkit
|
|
25
|
+
|
|
26
|
+
# yarn
|
|
27
|
+
yarn add minecraft-toolkit
|
|
28
|
+
|
|
29
|
+
# pnpm
|
|
30
|
+
pnpm install minecraft-toolkit
|
|
31
|
+
|
|
32
|
+
# bun
|
|
33
|
+
bun install minecraft-toolkit
|
|
34
|
+
|
|
35
|
+
# deno
|
|
36
|
+
deno install minecraft-toolkit
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
<!-- /automd -->
|
|
40
|
+
|
|
41
|
+
## Core Helpers
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import {
|
|
45
|
+
fetchPlayerProfile,
|
|
46
|
+
fetchPlayerSkin,
|
|
47
|
+
fetchPlayerUUID,
|
|
48
|
+
fetchPlayerSummary,
|
|
49
|
+
fetchNameHistory,
|
|
50
|
+
fetchPlayers,
|
|
51
|
+
resolvePlayer,
|
|
52
|
+
fetchSkinMetadata,
|
|
53
|
+
} from "minecraft-toolkit";
|
|
54
|
+
|
|
55
|
+
const profile = await fetchPlayerProfile("26bz");
|
|
56
|
+
const summary = await fetchPlayerSummary("26bz");
|
|
57
|
+
const skin = await fetchPlayerSkin("26bz");
|
|
58
|
+
const uuid = await fetchPlayerUUID("26bz");
|
|
59
|
+
const history = await fetchNameHistory("069a79f444e94726a5befca90e38aaf5");
|
|
60
|
+
const batch = await fetchPlayers(["Notch", "26bz"], { delayMs: 50 });
|
|
61
|
+
const resolved = await resolvePlayer("069a79f444e94726a5befca90e38aaf5");
|
|
62
|
+
const skinMeta = await fetchSkinMetadata("26bz");
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Helpers are HTTP-agnostic and run anywhere `fetch` exists (Node 18+, Bun, Workers). All errors surface as `MinecraftToolkitError`.
|
|
66
|
+
|
|
67
|
+
## Texture & Identity Utilities
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import {
|
|
71
|
+
isValidUsername,
|
|
72
|
+
isUUID,
|
|
73
|
+
normalizeUUID,
|
|
74
|
+
uuidWithDashes,
|
|
75
|
+
uuidWithoutDashes,
|
|
76
|
+
getSkinURL,
|
|
77
|
+
getCapeURL,
|
|
78
|
+
getSkinModel,
|
|
79
|
+
extractTextureHash,
|
|
80
|
+
} from "minecraft-toolkit";
|
|
81
|
+
|
|
82
|
+
isValidUsername("26bz"); // true
|
|
83
|
+
uuidWithDashes("069a79f444e94726a5befca90e38aaf5");
|
|
84
|
+
const skinUrl = getSkinURL(await fetchPlayerProfile("26bz"));
|
|
85
|
+
const hash = extractTextureHash(skinUrl);
|
|
86
|
+
const model = getSkinModel(skinUrl); // "slim" | "classic"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Skin Metadata & Color Sampling
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { fetchSkinMetadata, computeSkinDominantColor } from "minecraft-toolkit";
|
|
93
|
+
|
|
94
|
+
const meta = await fetchSkinMetadata("26bz", {
|
|
95
|
+
dominantColor: true,
|
|
96
|
+
sampleRegion: { x: 8, y: 8, width: 8, height: 8 },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
console.log(meta.dominantColor); // e.g. "#f2d2a9"
|
|
100
|
+
|
|
101
|
+
const accent = await computeSkinDominantColor(meta.skin.url, {
|
|
102
|
+
x: 40,
|
|
103
|
+
y: 8,
|
|
104
|
+
width: 8,
|
|
105
|
+
height: 8,
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Account Helpers
|
|
110
|
+
|
|
111
|
+
A valid Microsoft/Xbox Live access token is required for `minecraftservices.com` endpoints. Missing or expired tokens throw `MinecraftToolkitError` with `statusCode: 401`.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import {
|
|
115
|
+
fetchNameChangeInfo,
|
|
116
|
+
checkNameAvailability,
|
|
117
|
+
validateGiftCode,
|
|
118
|
+
fetchBlockedServers,
|
|
119
|
+
} from "minecraft-toolkit";
|
|
120
|
+
|
|
121
|
+
const accessToken = process.env.MC_ACCESS_TOKEN;
|
|
122
|
+
|
|
123
|
+
const windowInfo = await fetchNameChangeInfo(accessToken);
|
|
124
|
+
const availability = await checkNameAvailability("fresh_name", accessToken);
|
|
125
|
+
const isGiftValid = await validateGiftCode("ABCD-1234", accessToken);
|
|
126
|
+
const blockedServer = await fetchBlockedServers(); // no token required
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`validateGiftCode` returns `true`/`false` for 200/404 responses without throwing.
|
|
130
|
+
|
|
131
|
+
## Server Status Helpers
|
|
132
|
+
|
|
133
|
+
Probe Java and Bedrock servers without bringing your own RakNet/TCP logic.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import {
|
|
137
|
+
fetchServerStatus,
|
|
138
|
+
fetchJavaServerStatus,
|
|
139
|
+
fetchBedrockServerStatus,
|
|
140
|
+
} from "minecraft-toolkit";
|
|
141
|
+
|
|
142
|
+
const javaStatus = await fetchJavaServerStatus({ host: "mc.hypixel.net", port: 25565 });
|
|
143
|
+
const bedrockStatus = await fetchBedrockServerStatus({ host: "play.example.net", port: 19132 });
|
|
144
|
+
|
|
145
|
+
// fetchServerStatus picks the right probe based on the `edition` field
|
|
146
|
+
const autoStatus = await fetchServerStatus({ host: "my.realm.net", edition: "bedrock" });
|
|
147
|
+
|
|
148
|
+
console.log(javaStatus.players.online, bedrockStatus.motd.text);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Both helpers normalize MOTD text, favicon/Base64 icons, latency, and version info. Errors surface as
|
|
152
|
+
`MinecraftToolkitError` with contextual status codes.
|
|
153
|
+
|
|
154
|
+
## Minecraft Formatting Renderer
|
|
155
|
+
|
|
156
|
+
Convert legacy `§` or `&` codes into safe HTML fragments or CSS class spans.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { toHTML, generateCSS, stripCodes, hasCodes, convertPrefix } from "minecraft-toolkit";
|
|
160
|
+
|
|
161
|
+
const motd = "§aWelcome §lHeroes§r!";
|
|
162
|
+
|
|
163
|
+
const inline = toHTML(motd); // <span style="color: #55ff55">Welcome ...</span>
|
|
164
|
+
|
|
165
|
+
const classes = toHTML(motd, { mode: "class", classPrefix: "mc" });
|
|
166
|
+
const css = generateCSS(); // drop into a <style> tag
|
|
167
|
+
|
|
168
|
+
stripCodes(motd); // "Welcome Heroes!"
|
|
169
|
+
hasCodes(motd); // true
|
|
170
|
+
convertPrefix("&aHi", "toSection"); // "§aHi"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`getMaps()` exposes the color and format metadata if you want to build custom renderers.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
Published under the [MIT](https://github.com/26bz/minecraft-toolkit/blob/main/LICENSE) license.
|
|
178
|
+
Made by [26bz](https://github.com/26bz)
|
|
179
|
+
<br><br>
|
|
180
|
+
<a href="https://github.com/26bz/minecraft-toolkit/graphs/contributors">
|
|
181
|
+
<img src="https://contrib.rocks/image?repo=26bz/minecraft-toolkit" />
|
|
182
|
+
</a>
|
package/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export {
|
|
2
|
+
fetchPlayerProfile,
|
|
3
|
+
fetchPlayerSkin,
|
|
4
|
+
fetchPlayerUUID,
|
|
5
|
+
fetchUsernameByUUID,
|
|
6
|
+
fetchNameHistory,
|
|
7
|
+
fetchPlayers,
|
|
8
|
+
fetchPlayerSummary,
|
|
9
|
+
playerExists,
|
|
10
|
+
hasSkinChanged,
|
|
11
|
+
} from "./src/player/profile/index.js";
|
|
12
|
+
export { fetchSkinMetadata, computeSkinDominantColor } from "./src/player/skin.js";
|
|
13
|
+
export { isValidUsername } from "./src/player/identity/index.js";
|
|
14
|
+
export { getSkinURL, getCapeURL, getSkinModel, extractTextureHash } from "./src/player/textures.js";
|
|
15
|
+
export { resolvePlayer } from "./src/player/resolve.js";
|
|
16
|
+
export {
|
|
17
|
+
fetchNameChangeInfo,
|
|
18
|
+
checkNameAvailability,
|
|
19
|
+
validateGiftCode,
|
|
20
|
+
fetchBlockedServers,
|
|
21
|
+
} from "./src/player/account/index.js";
|
|
22
|
+
export { createPlayerApp, createPlayerHandlers } from "./src/h3/routes.js";
|
|
23
|
+
export {
|
|
24
|
+
fetchServerStatus,
|
|
25
|
+
fetchJavaServerStatus,
|
|
26
|
+
fetchBedrockServerStatus,
|
|
27
|
+
} from "./src/server/status.js";
|
|
28
|
+
export {
|
|
29
|
+
toHTML,
|
|
30
|
+
stripCodes,
|
|
31
|
+
generateCSS,
|
|
32
|
+
hasCodes,
|
|
33
|
+
convertPrefix,
|
|
34
|
+
getMaps,
|
|
35
|
+
} from "./src/utils/formatting.js";
|
package/package.json
CHANGED
|
@@ -1,12 +1,76 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minecraft-toolkit",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Developer toolkit for working with Mojang Minecraft player data, skins, and utilities.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"minecraft",
|
|
7
|
+
"minecraft-api",
|
|
8
|
+
"minecraft-player",
|
|
9
|
+
"minecraft-skins",
|
|
10
|
+
"minecraft-tools",
|
|
11
|
+
"minecraft-utils",
|
|
12
|
+
"minecraft-uuid",
|
|
13
|
+
"mojang",
|
|
14
|
+
"mojang-api"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/26bz/minecraft-toolkit",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/26bz/minecraft-toolkit/issues"
|
|
19
|
+
},
|
|
5
20
|
"license": "MIT",
|
|
6
|
-
"author":
|
|
7
|
-
|
|
8
|
-
|
|
21
|
+
"author": {
|
|
22
|
+
"name": "26bz",
|
|
23
|
+
"email": "26bz@proton.me",
|
|
24
|
+
"url": "https://26bz.com"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/26bz/minecraft-toolkit"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src",
|
|
32
|
+
"index.js",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"type": "module",
|
|
37
|
+
"sideEffects": false,
|
|
38
|
+
"types": "./index.d.ts",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": "./index.js",
|
|
41
|
+
"./player": "./src/player/profile/index.js",
|
|
42
|
+
"./skin": "./src/player/skin.js",
|
|
43
|
+
"./identity": "./src/player/identity/index.js",
|
|
44
|
+
"./textures": "./src/player/textures.js"
|
|
45
|
+
},
|
|
9
46
|
"scripts": {
|
|
10
|
-
"test": "
|
|
11
|
-
|
|
47
|
+
"test": "vitest",
|
|
48
|
+
"test:watch": "vitest watch",
|
|
49
|
+
"test:coverage": "vitest run --coverage",
|
|
50
|
+
"lint": "oxlint",
|
|
51
|
+
"lint:fix": "oxlint --fix",
|
|
52
|
+
"fmt": "oxfmt",
|
|
53
|
+
"fmt:check": "oxfmt --check",
|
|
54
|
+
"prepare": "husky install",
|
|
55
|
+
"docs:sync": "automd"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"h3": "2.0.1-rc.14",
|
|
59
|
+
"pngjs": "^7.0.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"automd": "^0.3.2",
|
|
63
|
+
"husky": "^9.1.7",
|
|
64
|
+
"lint-staged": "^16.3.2",
|
|
65
|
+
"oxfmt": "^0.36.0",
|
|
66
|
+
"oxlint": "^1.51.0",
|
|
67
|
+
"vitest": "^4.0.18"
|
|
68
|
+
},
|
|
69
|
+
"lint-staged": {
|
|
70
|
+
"*.{js,jsx,ts,tsx,mjs,cjs}": "pnpm run lint"
|
|
71
|
+
},
|
|
72
|
+
"engines": {
|
|
73
|
+
"node": ">=18"
|
|
74
|
+
},
|
|
75
|
+
"packageManager": "pnpm@10.30.2"
|
|
12
76
|
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const pkg = require("../package.json");
|
|
5
|
+
|
|
6
|
+
export const PACKAGE_METADATA = {
|
|
7
|
+
name: pkg.name,
|
|
8
|
+
version: pkg.version,
|
|
9
|
+
description: pkg.description,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_JAVA_PORT = 25565;
|
|
13
|
+
export const DEFAULT_BEDROCK_PORT = 19132;
|
|
14
|
+
export const DEFAULT_CACHE_TTL_SECONDS = 30;
|
|
15
|
+
export const DEFAULT_TIMEOUT_MS = 5000;
|
|
16
|
+
export const DEFAULT_PROTOCOL_VERSION = 760; // Minecraft 1.20.4
|
|
17
|
+
|
|
18
|
+
export const MOJANG_PROFILE_BASE = "https://api.mojang.com/users/profiles/minecraft";
|
|
19
|
+
export const SESSION_PROFILE_BASE = "https://sessionserver.mojang.com/session/minecraft/profile";
|
|
20
|
+
export const NAME_HISTORY_BASE = "https://api.mojang.com/user/profiles";
|
|
21
|
+
|
|
22
|
+
export const USER_AGENT = `${pkg.name}/${pkg.version}`;
|
|
23
|
+
export const DEFAULT_HEADERS = {
|
|
24
|
+
accept: "application/json",
|
|
25
|
+
"user-agent": USER_AGENT,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const RAKNET_MAGIC = Buffer.from([
|
|
29
|
+
0x00, 0xff, 0xff, 0x00, 0xfe, 0xfe, 0xfe, 0xfe, 0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, 0x56, 0x78,
|
|
30
|
+
]);
|
package/src/errors.js
ADDED
package/src/h3/routes.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { H3, defineHandler, definePlugin, readBody, getQuery } from "h3";
|
|
2
|
+
import { MinecraftToolkitError } from "../errors.js";
|
|
3
|
+
import {
|
|
4
|
+
fetchPlayerProfile,
|
|
5
|
+
fetchPlayerSkin,
|
|
6
|
+
fetchPlayerSummary,
|
|
7
|
+
fetchPlayerUUID,
|
|
8
|
+
fetchPlayers,
|
|
9
|
+
fetchNameHistory,
|
|
10
|
+
playerExists,
|
|
11
|
+
} from "../player/profile/index.js";
|
|
12
|
+
import { resolvePlayer } from "../player/resolve.js";
|
|
13
|
+
import {
|
|
14
|
+
fetchNameChangeInfo,
|
|
15
|
+
checkNameAvailability,
|
|
16
|
+
validateGiftCode,
|
|
17
|
+
fetchBlockedServers,
|
|
18
|
+
} from "../player/account/index.js";
|
|
19
|
+
import { fetchServerStatus } from "../server/status.js";
|
|
20
|
+
import { normalizeAddress, validatePort } from "../utils/validation.js";
|
|
21
|
+
|
|
22
|
+
function requireParam(event, key, message) {
|
|
23
|
+
const value = event.context.params?.[key];
|
|
24
|
+
if (!value) {
|
|
25
|
+
throw new MinecraftToolkitError(message, { statusCode: 400 });
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createPlayerHandlers() {
|
|
31
|
+
const profileHandler = defineHandler(async (event) => {
|
|
32
|
+
const username = requireParam(event, "username", "Username parameter is required");
|
|
33
|
+
return fetchPlayerProfile(username);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const skinHandler = defineHandler(async (event) => {
|
|
37
|
+
const username = requireParam(event, "username", "Username parameter is required");
|
|
38
|
+
return fetchPlayerSkin(username);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const summaryHandler = defineHandler(async (event) => {
|
|
42
|
+
const username = requireParam(event, "username", "Username parameter is required");
|
|
43
|
+
return fetchPlayerSummary(username);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const uuidHandler = defineHandler(async (event) => {
|
|
47
|
+
const username = requireParam(event, "username", "Username parameter is required");
|
|
48
|
+
return fetchPlayerUUID(username);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const resolverHandler = defineHandler(async (event) => {
|
|
52
|
+
const input = requireParam(event, "input", "Username or UUID parameter is required");
|
|
53
|
+
return resolvePlayer(input);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const nameHistoryHandler = defineHandler(async (event) => {
|
|
57
|
+
const uuid = requireParam(event, "uuid", "UUID parameter is required");
|
|
58
|
+
return fetchNameHistory(uuid);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const existsHandler = defineHandler(async (event) => {
|
|
62
|
+
const username = requireParam(event, "username", "Username parameter is required");
|
|
63
|
+
const exists = await playerExists(username);
|
|
64
|
+
return { username, exists };
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const batchHandler = defineHandler(async (event) => {
|
|
68
|
+
const body = (await readBody(event)) ?? {};
|
|
69
|
+
const usernames = Array.isArray(body.usernames) ? body.usernames : null;
|
|
70
|
+
if (!usernames || usernames.length === 0) {
|
|
71
|
+
throw new MinecraftToolkitError("Body must include a non-empty usernames array", {
|
|
72
|
+
statusCode: 400,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const delayMs = typeof body.delayMs === "number" ? body.delayMs : undefined;
|
|
76
|
+
return fetchPlayers(usernames, { delayMs });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const nameChangeInfoHandler = defineHandler(async (event) => {
|
|
80
|
+
const token = requireAccessToken(event);
|
|
81
|
+
return fetchNameChangeInfo(token);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const nameAvailabilityHandler = defineHandler(async (event) => {
|
|
85
|
+
const name = requireParam(event, "name", "Name parameter is required");
|
|
86
|
+
const token = requireAccessToken(event);
|
|
87
|
+
return checkNameAvailability(name, token);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const giftCodeValidationHandler = defineHandler(async (event) => {
|
|
91
|
+
const token = requireAccessToken(event);
|
|
92
|
+
const body = (await readBody(event)) ?? {};
|
|
93
|
+
const code = typeof body.code === "string" ? body.code.trim() : "";
|
|
94
|
+
if (!code) {
|
|
95
|
+
throw new MinecraftToolkitError("Gift code is required", { statusCode: 400 });
|
|
96
|
+
}
|
|
97
|
+
const valid = await validateGiftCode(code, token);
|
|
98
|
+
return { code, valid };
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const blockedServersHandler = defineHandler(async () => fetchBlockedServers());
|
|
102
|
+
|
|
103
|
+
const serverStatusHandler = defineHandler(async (event) => {
|
|
104
|
+
const address = normalizeAddress(
|
|
105
|
+
requireParam(event, "address", "Server address parameter is required"),
|
|
106
|
+
);
|
|
107
|
+
const query = getQuery(event);
|
|
108
|
+
const edition = typeof query.edition === "string" ? query.edition : undefined;
|
|
109
|
+
const port = typeof query.port === "string" ? validatePort(query.port) : undefined;
|
|
110
|
+
const timeoutMs =
|
|
111
|
+
typeof query.timeoutMs === "string" ? Number.parseInt(query.timeoutMs, 10) : undefined;
|
|
112
|
+
const protocolVersion =
|
|
113
|
+
typeof query.protocolVersion === "string"
|
|
114
|
+
? Number.parseInt(query.protocolVersion, 10)
|
|
115
|
+
: undefined;
|
|
116
|
+
|
|
117
|
+
return fetchServerStatus(address, {
|
|
118
|
+
edition,
|
|
119
|
+
port,
|
|
120
|
+
timeoutMs,
|
|
121
|
+
protocolVersion,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
profileHandler,
|
|
127
|
+
skinHandler,
|
|
128
|
+
summaryHandler,
|
|
129
|
+
uuidHandler,
|
|
130
|
+
resolverHandler,
|
|
131
|
+
nameHistoryHandler,
|
|
132
|
+
existsHandler,
|
|
133
|
+
batchHandler,
|
|
134
|
+
nameChangeInfoHandler,
|
|
135
|
+
nameAvailabilityHandler,
|
|
136
|
+
giftCodeValidationHandler,
|
|
137
|
+
blockedServersHandler,
|
|
138
|
+
serverStatusHandler,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createPlayerApp(options = {}) {
|
|
143
|
+
const app = new H3(options?.app);
|
|
144
|
+
const handlers = createPlayerHandlers();
|
|
145
|
+
|
|
146
|
+
app.get("/player/:username", handlers.profileHandler, {
|
|
147
|
+
meta: { category: "player", resource: "profile" },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
app.get("/player/:username/skin", handlers.skinHandler, {
|
|
151
|
+
meta: { category: "player", resource: "skin" },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
app.get("/player/:username/summary", handlers.summaryHandler, {
|
|
155
|
+
meta: { category: "player", resource: "summary" },
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
app.get("/player/:username/uuid", handlers.uuidHandler, {
|
|
159
|
+
meta: { category: "player", resource: "uuid" },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
app.get("/player/:input/resolve", handlers.resolverHandler, {
|
|
163
|
+
meta: { category: "player", resource: "resolve" },
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
app.get("/player/:uuid/names", handlers.nameHistoryHandler, {
|
|
167
|
+
meta: { category: "player", resource: "name-history" },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
app.get("/player/:username/exists", handlers.existsHandler, {
|
|
171
|
+
meta: { category: "player", resource: "exists" },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
app.post("/players/batch", handlers.batchHandler, {
|
|
175
|
+
meta: { category: "player", resource: "batch" },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
app.get("/account/namechange", handlers.nameChangeInfoHandler, {
|
|
179
|
+
meta: { category: "account", resource: "namechange" },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
app.get("/account/name/:name/availability", handlers.nameAvailabilityHandler, {
|
|
183
|
+
meta: { category: "account", resource: "name-availability" },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
app.post("/account/gift-code/validate", handlers.giftCodeValidationHandler, {
|
|
187
|
+
meta: { category: "account", resource: "gift-code" },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
app.get("/account/blocked-servers", handlers.blockedServersHandler, {
|
|
191
|
+
meta: { category: "account", resource: "blocked-servers" },
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
app.get("/server/:address/status", handlers.serverStatusHandler, {
|
|
195
|
+
meta: { category: "server", resource: "status" },
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return { app, handlers };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const playerPlugin = definePlugin((app) => {
|
|
202
|
+
const { handlers } = createPlayerApp({ app });
|
|
203
|
+
return handlers;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
function requireAccessToken(event) {
|
|
207
|
+
const header = event.req?.headers?.get?.("authorization") ?? "";
|
|
208
|
+
if (!header.toLowerCase().startsWith("bearer ")) {
|
|
209
|
+
throw new MinecraftToolkitError("Authorization header with Bearer token is required", {
|
|
210
|
+
statusCode: 401,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const token = header.slice(7).trim();
|
|
214
|
+
if (!token) {
|
|
215
|
+
throw new MinecraftToolkitError("Access token is required", { statusCode: 401 });
|
|
216
|
+
}
|
|
217
|
+
return token;
|
|
218
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { DEFAULT_HEADERS } from "../../constants.js";
|
|
2
|
+
import { MinecraftToolkitError } from "../../errors.js";
|
|
3
|
+
import { fetchJson } from "../../utils/http/index.js";
|
|
4
|
+
|
|
5
|
+
const API_BASE = "https://api.minecraftservices.com";
|
|
6
|
+
|
|
7
|
+
function assertAccessToken(accessToken) {
|
|
8
|
+
if (!accessToken || typeof accessToken !== "string") {
|
|
9
|
+
throw new MinecraftToolkitError("A valid access token is required", { statusCode: 401 });
|
|
10
|
+
}
|
|
11
|
+
return accessToken;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function authHeaders(accessToken) {
|
|
15
|
+
return {
|
|
16
|
+
Authorization: `Bearer ${assertAccessToken(accessToken)}`,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function fetchNameChangeInfo(accessToken) {
|
|
21
|
+
return fetchJson(`${API_BASE}/minecraft/profile/namechange`, {
|
|
22
|
+
headers: authHeaders(accessToken),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function checkNameAvailability(name, accessToken) {
|
|
27
|
+
if (!name || typeof name !== "string") {
|
|
28
|
+
throw new MinecraftToolkitError("Name is required", { statusCode: 400 });
|
|
29
|
+
}
|
|
30
|
+
return fetchJson(`${API_BASE}/minecraft/profile/name/${encodeURIComponent(name)}/available`, {
|
|
31
|
+
headers: authHeaders(accessToken),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function validateGiftCode(code, accessToken) {
|
|
36
|
+
if (!code || typeof code !== "string") {
|
|
37
|
+
throw new MinecraftToolkitError("Gift code is required", { statusCode: 400 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const response = await fetch(`${API_BASE}/productvoucher/giftcode/${encodeURIComponent(code)}`, {
|
|
41
|
+
headers: {
|
|
42
|
+
...DEFAULT_HEADERS,
|
|
43
|
+
...authHeaders(accessToken),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (response.status === 200 || response.status === 204) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (response.status === 404) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw new MinecraftToolkitError(`Failed to validate gift code ${code}`, {
|
|
56
|
+
statusCode: response.status,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function fetchBlockedServers() {
|
|
61
|
+
const response = await fetch(`https://sessionserver.mojang.com/blockedservers`, {
|
|
62
|
+
headers: DEFAULT_HEADERS,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new MinecraftToolkitError("Unable to fetch blocked servers", {
|
|
67
|
+
statusCode: response.status,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const text = await response.text();
|
|
72
|
+
return text
|
|
73
|
+
.split("\n")
|
|
74
|
+
.map((entry) => entry.trim())
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { MinecraftToolkitError } from "../../errors.js";
|
|
2
|
+
|
|
3
|
+
const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,16}$/;
|
|
4
|
+
const UUID_HEX = /^[0-9a-fA-F]+$/;
|
|
5
|
+
|
|
6
|
+
export function isValidUsername(username) {
|
|
7
|
+
return typeof username === "string" && USERNAME_REGEX.test(username.trim());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isUUID(value) {
|
|
11
|
+
if (typeof value !== "string") {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const normalized = value.replace(/-/g, "");
|
|
15
|
+
return normalized.length === 32 && UUID_HEX.test(normalized);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeUUID(uuid) {
|
|
19
|
+
if (!isUUID(uuid)) {
|
|
20
|
+
throw new MinecraftToolkitError("Invalid UUID", { statusCode: 400 });
|
|
21
|
+
}
|
|
22
|
+
return uuidWithoutDashes(uuid).toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function uuidWithDashes(uuid) {
|
|
26
|
+
const compact = uuidWithoutDashes(uuid);
|
|
27
|
+
return compact.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, "$1-$2-$3-$4-$5");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function uuidWithoutDashes(uuid) {
|
|
31
|
+
if (typeof uuid !== "string") {
|
|
32
|
+
return uuid;
|
|
33
|
+
}
|
|
34
|
+
return uuid.replace(/-/g, "");
|
|
35
|
+
}
|