Skip to content

Commit 75c9459

Browse files
Update worker.
1 parent d96897d commit 75c9459

1 file changed

Lines changed: 56 additions & 158 deletions

File tree

Lines changed: 56 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,179 +1,77 @@
1-
// worker.js
2-
const DEFAULT_ALLOWED_ORIGINS = [
3-
"https://tools.mathspp.com",
4-
"http://localhost:5173",
5-
"http://localhost:3000",
6-
];
7-
8-
const S_MAX_AGE = 3600; // 1h fresh cache
9-
const STALE_WINDOW = 24 * 3600; // 24h serve-stale if upstream errors
10-
11-
function buildAllowedOrigins(env) {
12-
const allowList = (env?.ALLOWED_ORIGINS || DEFAULT_ALLOWED_ORIGINS.join(","))
13-
.split(",").map(s => s.trim()).filter(Boolean);
14-
return new Set(allowList);
15-
}
16-
function corsHeaders(origin, allowed) {
17-
const allow = allowed.has(origin) ? origin : "";
18-
return {
19-
"Access-Control-Allow-Origin": allow,
20-
"Access-Control-Allow-Methods": "GET, OPTIONS",
21-
"Access-Control-Allow-Headers": "Content-Type",
22-
"Vary": "Origin",
23-
};
24-
}
25-
function respondJSON(origin, allowed, data, status = 200, extra = {}) {
26-
return new Response(JSON.stringify(data), {
27-
status,
28-
headers: {
29-
"Content-Type": "application/json; charset=utf-8",
30-
...corsHeaders(origin, allowed),
31-
...extra,
32-
},
33-
});
34-
}
35-
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
36-
37-
async function fetchWithBackoff(url, init, attempts = 3) {
38-
for (let i = 0; i < attempts; i++) {
39-
const res = await fetch(url, init);
40-
if (res.ok) return res;
41-
42-
if (res.status === 429 || res.status === 503) {
43-
const ra = res.headers.get("Retry-After");
44-
if (ra) {
45-
const secs = Number(ra);
46-
if (!Number.isNaN(secs) && secs >= 0 && secs <= 120) {
47-
await sleep(secs * 1000);
48-
continue;
49-
}
50-
}
51-
const delay = Math.min(1600, 400 * Math.pow(2, i)) + Math.floor(Math.random() * 150);
52-
await sleep(delay);
53-
continue;
54-
}
55-
return res; // don't retry other statuses
56-
}
57-
return fetch(url, init); // last shot
58-
}
59-
601
export default {
612
async fetch(request, env, ctx) {
62-
const origin = request.headers.get("Origin") || "";
63-
const allowed = buildAllowedOrigins(env);
643
const url = new URL(request.url);
654

66-
if (request.method === "OPTIONS") {
67-
return new Response(null, { headers: corsHeaders(origin, allowed) });
68-
}
69-
if (request.method !== "GET") {
70-
return respondJSON(origin, allowed, { error: "Method Not Allowed" }, 405);
5+
// Only handle /api/gumroad-products
6+
if (url.pathname !== "/api/gumroad-products") {
7+
return new Response("Not found", { status: 404 });
718
}
729

73-
const goodPath = url.pathname === "/api/gumroad-products" || url.pathname === "/api/gumroad-products/";
74-
if (!goodPath) {
75-
return respondJSON(origin, allowed, { error: "Not Found" }, 404);
10+
// Handle CORS preflight
11+
if (request.method === "OPTIONS") {
12+
return handleOptions(request);
7613
}
7714

78-
const u = (url.searchParams.get("u") || "").trim();
79-
if (!u || !/^[a-z0-9-]+$/i.test(u)) {
80-
return respondJSON(origin, allowed, { error: "Invalid or missing Gumroad username." }, 400);
15+
const username = url.searchParams.get("u");
16+
if (!username) {
17+
return new Response("Missing 'u' query parameter", { status: 400 });
8118
}
8219

83-
const cache = caches.default;
84-
const dataKey = new Request(`https://gumroad-products.internal/cache?u=${encodeURIComponent(u)}`);
85-
const metaKey = new Request(`https://gumroad-products.internal/meta?u=${encodeURIComponent(u)}`);
86-
87-
let cachedBody = null, cachedTs = 0;
88-
const c = await cache.match(dataKey);
89-
if (c) cachedBody = await c.text();
90-
const cm = await cache.match(metaKey);
91-
if (cm) try { cachedTs = (await cm.json()).ts || 0; } catch { }
92-
93-
// Serve fresh cache (<= 1h)
94-
const age = Math.floor(Date.now() / 1000) - cachedTs;
95-
if (cachedBody && age <= S_MAX_AGE) {
96-
return new Response(cachedBody, {
97-
headers: {
98-
"Content-Type": "text/html; charset=utf-8",
99-
"Cache-Control": `public, max-age=${S_MAX_AGE}`,
100-
"X-Cache": "HIT",
101-
"X-Upstream-Status": "none",
102-
...corsHeaders(origin, allowed),
103-
},
104-
});
20+
// (Optional) Very basic sanity-check for username
21+
if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
22+
return new Response("Invalid username", { status: 400 });
10523
}
10624

107-
// Try subdomain first, then path-based profile
108-
const profileUrlA = `https://${u}.gumroad.com/`;
109-
const profileUrlB = `https://gumroad.com/${encodeURIComponent(u)}`;
110-
111-
async function getProfileHTML(urlStr) {
112-
const res = await fetchWithBackoff(urlStr, {
113-
redirect: "follow",
114-
headers: {
115-
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
116-
"Accept-Language": "en-US,en;q=0.9",
117-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
118-
"Referer": "https://gumroad.com/",
119-
"Cache-Control": "no-cache",
120-
"Pragma": "no-cache",
121-
},
122-
});
123-
return res;
124-
}
25+
const gumroadUrl = `https://${username}.gumroad.com`;
12526

126-
// 1) Try subdomain and then path profile if it fails.
127-
let upstream = await getProfileHTML(profileUrlA);
128-
if (!upstream.ok && (upstream.status === 429 || upstream.status === 403)) {
129-
upstream = await getProfileHTML(profileUrlB);
130-
}
27+
// Fetch the Gumroad page
28+
const upstreamResp = await fetch(gumroadUrl);
13129

132-
// If still not ok, serve stale or bubble error
133-
if (!upstream.ok) {
134-
// Serve stale (<= 25h old total) instead of failing
135-
if (cachedBody && age <= (S_MAX_AGE + STALE_WINDOW)) {
136-
return new Response(cachedBody, {
137-
headers: {
138-
"Content-Type": "text/html; charset=utf-8",
139-
"Cache-Control": `public, max-age=0, stale-while-revalidate=${STALE_WINDOW}`,
140-
"X-Cache": "STALE",
141-
"X-Upstream-Status": String(upstream.status),
142-
...corsHeaders(origin, allowed),
143-
},
144-
});
145-
}
146-
// No cache to fall back to
147-
return respondJSON(origin, allowed, { error: "Upstream error", status: upstream.status }, upstream.status, {
148-
"X-Cache": "MISS",
149-
"X-Upstream-Status": String(upstream.status),
150-
});
151-
}
30+
// Get raw HTML
31+
const html = await upstreamResp.text();
15232

153-
const html = await upstream.text();
33+
// Build response with CORS headers
34+
const responseHeaders = new Headers(upstreamResp.headers);
35+
responseHeaders.set("Content-Type", "text/html; charset=utf-8");
15436

155-
// Update cache
156-
const now = Math.floor(Date.now() / 1000);
157-
const dataResp = new Response(html, {
158-
headers: {
159-
"Content-Type": "text/html; charset=utf-8",
160-
"Cache-Control": `public, max-age=${S_MAX_AGE}`,
161-
},
162-
});
163-
const metaResp = new Response(JSON.stringify({ ts: now }), {
164-
headers: { "Content-Type": "application/json" },
165-
});
166-
ctx.waitUntil(cache.put(dataKey, dataResp.clone()));
167-
ctx.waitUntil(cache.put(metaKey, metaResp.clone()));
37+
// Apply CORS policy
38+
applyCors(request, responseHeaders);
16839

16940
return new Response(html, {
170-
headers: {
171-
"Content-Type": "text/html; charset=utf-8",
172-
"Cache-Control": `public, max-age=${S_MAX_AGE}`,
173-
"X-Cache": cachedBody ? "MISS-REVAL" : "MISS",
174-
"X-Upstream-Status": "200",
175-
...corsHeaders(origin, allowed),
176-
},
41+
status: upstreamResp.status,
42+
statusText: upstreamResp.statusText,
43+
headers: responseHeaders,
17744
});
17845
},
17946
};
47+
48+
function applyCors(request, headers) {
49+
const origin = request.headers.get("Origin");
50+
const allowedOrigins = new Set([
51+
"https://mathspp.com",
52+
"https://tools.mathspp.com",
53+
]);
54+
55+
if (origin && allowedOrigins.has(origin)) {
56+
headers.set("Access-Control-Allow-Origin", origin);
57+
headers.set("Vary", "Origin");
58+
headers.set("Access-Control-Allow-Credentials", "true");
59+
}
60+
}
61+
62+
function handleOptions(request) {
63+
const headers = new Headers();
64+
applyCors(request, headers);
65+
66+
// If no allowed origin, just send generic response
67+
headers.set("Access-Control-Allow-Methods", "GET, OPTIONS");
68+
headers.set(
69+
"Access-Control-Allow-Headers",
70+
request.headers.get("Access-Control-Request-Headers") || ""
71+
);
72+
73+
return new Response(null, {
74+
status: 204,
75+
headers,
76+
});
77+
}

0 commit comments

Comments
 (0)