Skip to content

Commit 9bdf465

Browse files
Refactor worker.js to implement route handling and rate limiting; update wrangler.toml to include CSS rules
1 parent 05938bc commit 9bdf465

14 files changed

Lines changed: 12678 additions & 13 deletions

config/constants.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const CACHE_TTL_SECONDS = 12 * 60 * 60;
2+
3+
export const STATUS = {
4+
OK: "OK",
5+
NOT_FOUND: "NOT_FOUND",
6+
BAD_REQUEST: "BAD_REQUEST",
7+
TOO_MANY_REQUESTS: "TOO_MANY_REQUESTS",
8+
INTERNAL_ERROR: "INTERNAL_ERROR"
9+
};
10+
11+
export const UPSTREAM = {
12+
LOOKUP_BASE: "https://api.minecraftservices.com/minecraft/profile/lookup",
13+
SESSION_PROFILE_BASE: "https://sessionserver.mojang.com/session/minecraft/profile"
14+
};

middleware/rateLimit.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { tooManyRequests } from "../utils/responses";
2+
3+
export async function enforceRateLimit(request, env) {
4+
if (!env?.RATE_LIMITER || typeof env.RATE_LIMITER.limit !== "function") {
5+
return null;
6+
}
7+
8+
const clientIp = request.headers.get("cf-connecting-ip") || "unknown";
9+
const result = await env.RATE_LIMITER.limit({ key: clientIp });
10+
11+
return result?.success ? null : tooManyRequests();
12+
}

pages/defaultPage.css

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
:root {
2+
--bg: #0f172a;
3+
--panel: #111827;
4+
--text: #e5e7eb;
5+
--muted: #9ca3af;
6+
--accent: #22c55e;
7+
--border: #1f2937;
8+
}
9+
10+
* {
11+
box-sizing: border-box;
12+
}
13+
14+
body {
15+
margin: 0;
16+
font-family: "Consolas", "Courier New", monospace;
17+
background: radial-gradient(circle at top, #1e293b 0%, var(--bg) 45%);
18+
color: var(--text);
19+
line-height: 1.45;
20+
}
21+
22+
.wrap {
23+
max-width: 1080px;
24+
margin: 0 auto;
25+
padding: 24px;
26+
}
27+
28+
h1 {
29+
margin: 0 0 8px;
30+
}
31+
32+
h2 {
33+
margin: 0 0 10px;
34+
font-size: 1.1rem;
35+
}
36+
37+
p {
38+
color: var(--muted);
39+
}
40+
41+
.grid {
42+
display: grid;
43+
gap: 16px;
44+
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
45+
}
46+
47+
.card {
48+
background: color-mix(in srgb, var(--panel) 92%, black 8%);
49+
border: 1px solid var(--border);
50+
border-radius: 10px;
51+
padding: 16px;
52+
}
53+
54+
.card-top-space {
55+
margin-top: 16px;
56+
}
57+
58+
code.inline {
59+
color: var(--accent);
60+
}
61+
62+
pre {
63+
margin: 8px 0 0;
64+
padding: 12px;
65+
border-radius: 8px;
66+
background: #020617;
67+
border: 1px solid #1e293b;
68+
overflow: auto;
69+
}
70+
71+
table {
72+
width: 100%;
73+
border-collapse: collapse;
74+
margin-top: 6px;
75+
}
76+
77+
th,
78+
td {
79+
border: 1px solid var(--border);
80+
padding: 8px;
81+
text-align: left;
82+
}
83+
84+
th {
85+
color: var(--accent);
86+
}

pages/defaultPage.html

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>MineTools API</title>
7+
<style>/*__INLINE_CSS__*/</style>
8+
</head>
9+
<body>
10+
<main class="wrap">
11+
<h1>MineTools API</h1>
12+
<p>Lookup Minecraft UUID and profile texture data with caching.</p>
13+
14+
<section class="grid">
15+
<article class="card">
16+
<h2>Route: /uuid/&#123;username|uuid&#125;</h2>
17+
<div>Usage:</div>
18+
<pre><code>__UUID_USAGE__</code></pre>
19+
<div>Example response (jeb_):</div>
20+
<pre><code>__UUID_EXAMPLE__</code></pre>
21+
</article>
22+
23+
<article class="card">
24+
<h2>Route: /profile/&#123;uuid&#125;</h2>
25+
<div>Usage:</div>
26+
<pre><code>__PROFILE_USAGE__</code></pre>
27+
<div>Example response (jeb_):</div>
28+
<pre><code>__PROFILE_EXAMPLE__</code></pre>
29+
</article>
30+
</section>
31+
32+
<section class="card card-top-space">
33+
<h2>Error behavior</h2>
34+
<table>
35+
<thead>
36+
<tr>
37+
<th>Status</th>
38+
<th>Error</th>
39+
<th>When it happens</th>
40+
</tr>
41+
</thead>
42+
<tbody>
43+
<tr>
44+
<td>400</td>
45+
<td>BAD_REQUEST</td>
46+
<td>Missing identifier on <code class="inline">/uuid</code> or invalid UUID format on <code class="inline">/profile/&#123;uuid&#125;</code>.</td>
47+
</tr>
48+
<tr>
49+
<td>404</td>
50+
<td>NOT_FOUND</td>
51+
<td>Unknown route, or username/UUID does not exist upstream.</td>
52+
</tr>
53+
<tr>
54+
<td>429</td>
55+
<td>TOO_MANY_REQUESTS</td>
56+
<td>Rate limiter blocks a valid API route request.</td>
57+
</tr>
58+
<tr>
59+
<td>500</td>
60+
<td>INTERNAL_ERROR</td>
61+
<td>Unhandled worker error or upstream failure.</td>
62+
</tr>
63+
</tbody>
64+
</table>
65+
</section>
66+
</main>
67+
</body>
68+
</html>

pages/defaultPage.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import cssTemplate from "./defaultPage.css";
2+
import htmlTemplate from "./defaultPage.html";
3+
4+
function asText(templateModule) {
5+
if (typeof templateModule === "string") {
6+
return templateModule;
7+
}
8+
9+
if (templateModule && typeof templateModule.default === "string") {
10+
return templateModule.default;
11+
}
12+
13+
return String(templateModule ?? "");
14+
}
15+
16+
const UUID_EXAMPLE = {
17+
cache: {
18+
HIT: true,
19+
cache_time: 43200,
20+
cache_time_left: 28910,
21+
cached_at: 1774166400,
22+
cached_until: 1774209600
23+
},
24+
id: "853c80ef3c3749fdaa49938b674adae6",
25+
name: "jeb_",
26+
status: "OK"
27+
};
28+
29+
const PROFILE_EXAMPLE = {
30+
decoded: {
31+
profileId: "853c80ef3c3749fdaa49938b674adae6",
32+
profileName: "jeb_",
33+
signatureRequired: true,
34+
textures: {
35+
SKIN: {
36+
url: "http://textures.minecraft.net/texture/a846b82963924cb13211122489263941d1403689f90151120d5234be4a73fb"
37+
}
38+
},
39+
timestamp: 1521401553373
40+
},
41+
raw: {
42+
id: "853c80ef3c3749fdaa49938b674adae6",
43+
name: "jeb_",
44+
properties: [
45+
{
46+
name: "textures",
47+
value: "<base64>",
48+
signature: "<signature>"
49+
}
50+
],
51+
cache: {
52+
HIT: true,
53+
cache_time: 43200,
54+
cache_time_left: 29712,
55+
cached_at: 1774166400,
56+
cached_until: 1774209600
57+
},
58+
status: "OK"
59+
}
60+
};
61+
62+
function escapeHtml(value) {
63+
return value
64+
.replace(/&/g, "&amp;")
65+
.replace(/</g, "&lt;")
66+
.replace(/>/g, "&gt;")
67+
.replace(/\"/g, "&quot;")
68+
.replace(/'/g, "&#39;");
69+
}
70+
71+
function renderJson(value) {
72+
return escapeHtml(JSON.stringify(value, null, 2));
73+
}
74+
75+
export function serveDefaultPage(request) {
76+
const url = new URL(request.url);
77+
const base = `${url.protocol}//${url.host}`;
78+
const htmlSource = asText(htmlTemplate);
79+
const cssSource = asText(cssTemplate);
80+
81+
const html = htmlSource
82+
.replace("/*__INLINE_CSS__*/", cssSource)
83+
.replace("__UUID_USAGE__", escapeHtml(`${base}/uuid/jeb_\n${base}/uuid/853c80ef3c3749fdaa49938b674adae6`))
84+
.replace("__PROFILE_USAGE__", escapeHtml(`${base}/profile/853c80ef3c3749fdaa49938b674adae6`))
85+
.replace("__UUID_EXAMPLE__", renderJson(UUID_EXAMPLE))
86+
.replace("__PROFILE_EXAMPLE__", renderJson(PROFILE_EXAMPLE));
87+
88+
return new Response(html, {
89+
headers: {
90+
"content-type": "text/html; charset=utf-8"
91+
}
92+
});
93+
}

routes/profileRoute.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { CACHE_TTL_SECONDS, STATUS } from "../config/constants";
2+
import { fetchSessionProfile } from "../services/minecraftApi";
3+
import { getCachedEntry, setCachedEntry } from "../utils/cache";
4+
import { badRequest, internalError, jsonResponse, notFound } from "../utils/responses";
5+
import {
6+
buildCacheMeta,
7+
decodeBase64Json,
8+
isUuid,
9+
normalizeUuid,
10+
nowSeconds
11+
} from "../utils/utils";
12+
13+
function profileKey(uuidNoDashes) {
14+
return `profile:${uuidNoDashes}`;
15+
}
16+
17+
function decodeTextures(rawProfile) {
18+
const texturesProperty = rawProfile.properties?.find((property) => property.name === "textures");
19+
return texturesProperty?.value ? decodeBase64Json(texturesProperty.value) : null;
20+
}
21+
22+
function buildResponse(rawProfile, cache) {
23+
return {
24+
decoded: decodeTextures(rawProfile),
25+
raw: {
26+
...rawProfile,
27+
cache,
28+
status: STATUS.OK
29+
}
30+
};
31+
}
32+
33+
function isValidCachedProfile(cached) {
34+
return Boolean(cached?.payload) && typeof cached.cachedAt === "number";
35+
}
36+
37+
export async function handleProfileRoute(identifier, env) {
38+
if (!identifier || !isUuid(identifier.trim())) {
39+
return badRequest("Invalid uuid format");
40+
}
41+
42+
const normalizedUuid = normalizeUuid(identifier.trim());
43+
const cacheKey = profileKey(normalizedUuid);
44+
45+
const cached = await getCachedEntry(env, cacheKey);
46+
if (isValidCachedProfile(cached)) {
47+
const cache = buildCacheMeta(true, CACHE_TTL_SECONDS, cached.cachedAt);
48+
return jsonResponse(buildResponse(cached.payload, cache));
49+
}
50+
51+
try {
52+
const upstream = await fetchSessionProfile(normalizedUuid);
53+
if (upstream.notFound) {
54+
return notFound("Player profile not found");
55+
}
56+
57+
const rawProfile = upstream.data;
58+
await setCachedEntry(env, cacheKey, rawProfile, CACHE_TTL_SECONDS);
59+
60+
const cache = buildCacheMeta(false, CACHE_TTL_SECONDS, nowSeconds());
61+
return jsonResponse(buildResponse(rawProfile, cache));
62+
} catch (error) {
63+
console.error("Profile route error", error);
64+
return internalError("Failed to fetch profile");
65+
}
66+
}

0 commit comments

Comments
 (0)