evOo-local/apps/www/api/waitlist.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

88 lines
2.3 KiB
TypeScript

import {
captureWaitlistSignup,
normalizeWaitlistEmail,
parseWaitlistBody,
} from "../src/waitlist.js";
const MAX_BODY_BYTES = 2_048;
function json(status: number, body: unknown) {
return Response.json(body, {
status,
headers: { Allow: "POST", "Cache-Control": "no-store" },
});
}
async function readBody(request: Request): Promise<string | null> {
if (!request.body) return "";
const reader = request.body.getReader();
const decoder = new TextDecoder();
let bytes = 0;
let body = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
bytes += value.byteLength;
if (bytes > MAX_BODY_BYTES) {
cancelBody(reader);
return null;
}
body += decoder.decode(value, { stream: true });
}
return body + decoder.decode();
} finally {
reader.releaseLock();
}
}
function cancelBody(body: { cancel(): Promise<void> } | null): void {
try {
void body?.cancel().catch(() => undefined);
} catch {
// Best-effort cleanup must not delay the rejection response.
}
}
async function fetchWaitlist(request: Request) {
if (request.method !== "POST") {
return json(405, { error: "Method not allowed" });
}
const contentLengthHeader = request.headers.get("content-length");
if (contentLengthHeader !== null) {
const contentLength = Number(contentLengthHeader);
if (!Number.isFinite(contentLength) || contentLength < 0 || contentLength > MAX_BODY_BYTES) {
cancelBody(request.body);
return json(400, { error: "Invalid request" });
}
}
const rawBody = await readBody(request).catch(() => null);
const body = rawBody === null ? null : parseWaitlistBody(rawBody);
if (!body) {
return json(400, { error: "Enter a valid email address" });
}
// A hidden field catches basic form spam without confirming the trap to bots.
if (body.contactNote?.trim()) {
return json(200, { ok: true });
}
const email = normalizeWaitlistEmail(body.email);
if (!email) {
return json(400, { error: "Enter a valid email address" });
}
const captured = await captureWaitlistSignup(email, process.env).catch(() => false);
if (!captured) {
return json(503, { error: "Waitlist is temporarily unavailable" });
}
return json(200, { ok: true });
}
export default { fetch: fetchWaitlist };