Update index.ts

This commit is contained in:
Ari 2026-06-28 00:13:52 +01:00 committed by GitHub
parent c61cb7edc7
commit d9f9c28aef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 35 additions and 16 deletions

View File

@ -31,23 +31,37 @@ const LIMITS = {
website: 200, website: 200,
} as const; } as const;
// Supports any number of allowed origins, e.g.
// ALLOWED_ORIGINS = "https://example.com,https://www.example.com,https://staging.example.com"
function corsHeaders(request: Request, env: Env): Record<string, string> { function corsHeaders(request: Request, env: Env): Record<string, string> {
const requestOrigin = request.headers.get("Origin") ?? ""; const requestOrigin = request.headers.get("Origin") ?? "";
const allowedOrigins = const allowedOrigins =
env.ALLOWED_ORIGINS?.split(",").map(o => o.trim()) ?? ["*"]; env.ALLOWED_ORIGINS?.split(",")
.map(o => o.trim())
.filter(Boolean) ?? ["*"];
const wildcard = allowedOrigins.includes("*");
const origin = const origin =
allowedOrigins.includes(requestOrigin) || allowedOrigins.includes("*") wildcard || allowedOrigins.includes(requestOrigin)
? requestOrigin ? requestOrigin || (wildcard ? "*" : allowedOrigins[0])
: allowedOrigins[0]; : allowedOrigins[0];
return { const headers: Record<string, string> = {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type", "Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Max-Age": "86400", "Access-Control-Max-Age": "86400",
"Access-Control-Allow-Origin": origin,
}; };
// When echoing a specific origin (not "*"), tell caches the response
// varies by Origin so one origin's CORS headers don't get cached/served
// to another origin.
if (origin !== "*") {
headers["Vary"] = "Origin";
}
return headers;
} }
function json( function json(
@ -126,7 +140,7 @@ async function readEntries(env: Env): Promise<GuestbookEntry[]> {
} }
} }
async function handleGet(url: URL, env: Env): Promise<Response> { async function handleGet(url: URL, request: Request, env: Env): Promise<Response> {
const entries = await readEntries(env); const entries = await readEntries(env);
const limit = Math.min( const limit = Math.min(
Math.max(parseInt(url.searchParams.get("limit") || "") || 50, 1), Math.max(parseInt(url.searchParams.get("limit") || "") || 50, 1),
@ -141,6 +155,7 @@ async function handleGet(url: URL, env: Env): Promise<Response> {
offset, offset,
}, },
200, 200,
request,
env env
); );
} }
@ -152,12 +167,12 @@ async function handlePost(request: Request, env: Env): Promise<Response> {
try { try {
body = (await request.json()) as PostBody; body = (await request.json()) as PostBody;
} catch { } catch {
return json({ error: "Invalid JSON body." }, 400, env); return json({ error: "Invalid JSON body." }, 400, request, env);
} }
// Honeypot: real users never fill this hidden field. // Honeypot: real users never fill this hidden field.
if (clean(body.url2 ?? "", 100)) { if (clean(body.url2 ?? "", 100)) {
return json({ ok: true, skipped: true }, 200, env); // pretend success return json({ ok: true, skipped: true }, 200, request, env); // pretend success
} }
// Turnstile (only enforced if a secret is configured) // Turnstile (only enforced if a secret is configured)
@ -167,15 +182,15 @@ async function handlePost(request: Request, env: Env): Promise<Response> {
env env
); );
if (!turnstileOk) { if (!turnstileOk) {
return json({ error: "Captcha verification failed. Please try again." }, 403, env); return json({ error: "Captcha verification failed. Please try again." }, 403, request, env);
} }
const name = clean(body.name, LIMITS.name); const name = clean(body.name, LIMITS.name);
const message = clean(body.message, LIMITS.message); const message = clean(body.message, LIMITS.message);
const website = cleanWebsite(body.website); const website = cleanWebsite(body.website);
if (!name) return json({ error: "Please enter a name." }, 400, env); if (!name) return json({ error: "Please enter a name." }, 400, request, env);
if (!message) return json({ error: "Please enter a message." }, 400, env); if (!message) return json({ error: "Please enter a message." }, 400, request, env);
// Rate limit per IP // Rate limit per IP
if (ip) { if (ip) {
@ -185,6 +200,7 @@ async function handlePost(request: Request, env: Env): Promise<Response> {
return json( return json(
{ error: `Slow down a moment — you can post again in ~${RATE_LIMIT_SECONDS}s.` }, { error: `Slow down a moment — you can post again in ~${RATE_LIMIT_SECONDS}s.` },
429, 429,
request,
env env
); );
} }
@ -204,7 +220,7 @@ async function handlePost(request: Request, env: Env): Promise<Response> {
if (entries.length > MAX_ENTRIES) entries.length = MAX_ENTRIES; if (entries.length > MAX_ENTRIES) entries.length = MAX_ENTRIES;
await env.GUESTBOOK.put(ENTRIES_KEY, JSON.stringify(entries)); await env.GUESTBOOK.put(ENTRIES_KEY, JSON.stringify(entries));
return json({ ok: true, entry }, 201, env); return json({ ok: true, entry }, 201, request, env);
} }
export default { export default {
@ -213,20 +229,23 @@ export default {
const url = new URL(request.url); const url = new URL(request.url);
if (request.method === "OPTIONS") { if (request.method === "OPTIONS") {
return new Response(null, { status: 204, headers: corsHeaders(env) }); return new Response(null, {
status: 204,
headers: corsHeaders(request, env),
});
} }
if (request.method === "GET") { if (request.method === "GET") {
return await handleGet(url, env); return await handleGet(url, request, env);
} }
if (request.method === "POST") { if (request.method === "POST") {
return await handlePost(request, env); return await handlePost(request, env);
} }
return json({ error: "Method not allowed." }, 405, env); return json({ error: "Method not allowed." }, 405, request, env);
} catch (err) { } catch (err) {
// Always attach CORS headers, even on unexpected errors, so the browser // Always attach CORS headers, even on unexpected errors, so the browser
// surfaces a real message instead of a masked CORS/network error. // surfaces a real message instead of a masked CORS/network error.
console.error("[guestbook] unhandled error", err); console.error("[guestbook] unhandled error", err);
return json({ error: "Internal error." }, 500, env); return json({ error: "Internal error." }, 500, request, env);
} }
}, },
} satisfies ExportedHandler<Env>; } satisfies ExportedHandler<Env>;