import { existsSync, readFileSync } from "node:fs"; const baseUrlArg = process.argv[2]; const fixturePath = process.argv[3] ?? "fixtures/mime/simple-text.eml"; if (!baseUrlArg) { console.error("Usage: pnpm smoke:email:local [fixture]"); process.exit(1); } const baseUrl: string = baseUrlArg; // The /api/debug/phase0/* routes fail closed (404) unless the caller presents the same // PHASE0_DEBUG_TOKEN the Worker was started with. Prefer an explicit env var (CI sets one); // otherwise fall back to reading the local `.dev.vars` file (auto-generated by `pnpm dev` // via scripts/ensure-dev-vars.ts, see README quickstart) so this script keeps working with // zero extra manual steps. function readDebugToken(): string | undefined { if (process.env.PHASE0_DEBUG_TOKEN) { return process.env.PHASE0_DEBUG_TOKEN; } if (!existsSync(".dev.vars")) { return undefined; } const content = readFileSync(".dev.vars", "utf8"); for (const line of content.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const separator = trimmed.indexOf("="); if (separator === -1) continue; const key = trimmed.slice(0, separator).trim(); if (key === "PHASE0_DEBUG_TOKEN") { return trimmed.slice(separator + 1).trim(); } } return undefined; } const debugToken = readDebugToken(); const debugHeaders: HeadersInit | undefined = debugToken ? { "x-phase0-debug-token": debugToken } : undefined; const raw = readFileSync(fixturePath); const to = "test@example.com"; const from = "sender@example.com"; const emailUrl = new URL("/cdn-cgi/handler/email", baseUrl); emailUrl.searchParams.set("from", from); emailUrl.searchParams.set("to", to); async function postFixture(label: string) { const response = await fetch(emailUrl, { method: "POST", body: raw, }); const text = await response.text(); console.log(`${label}:`, text); if (!response.ok) { throw new Error(`${label} failed with ${response.status}`); } } async function fetchDebug(baseUrl: string) { const mailboxResponse = await fetch(new URL("/api/debug/phase0/test-mailbox-id", baseUrl), { headers: debugHeaders, }); const { mailboxId } = (await mailboxResponse.json()) as { mailboxId: string }; const response = await fetch(new URL(`/api/debug/phase0/mailboxes/${mailboxId}`, baseUrl), { headers: debugHeaders, }); if (!response.ok) { throw new Error(`debug failed with ${response.status}: ${await response.text()}`); } return (await response.json()) as { messageCount: number; messages: Array<{ raw_r2_key: string; raw_sha256: string; idempotency_key: string; subject: string | null; }>; }; } async function waitForMessageCount(expected: number) { const deadline = Date.now() + 10_000; let lastDebug: Awaited> | null = null; while (Date.now() < deadline) { lastDebug = await fetchDebug(baseUrl); if (lastDebug.messageCount === expected) { return lastDebug; } await new Promise((resolve) => setTimeout(resolve, 300)); } throw new Error(`messageCount did not become ${expected}; last=${JSON.stringify(lastDebug)}`); } await postFixture("first-delivery"); const firstDebug = await waitForMessageCount(1); const message = firstDebug.messages[0]; if (!message) { throw new Error("missing message after first delivery"); } const r2HeadUrl = new URL("/api/debug/phase0/r2/head", baseUrl); r2HeadUrl.searchParams.set("key", message.raw_r2_key); const r2Head = (await (await fetch(r2HeadUrl, { headers: debugHeaders })).json()) as { exists?: boolean; }; console.log("r2-head:", JSON.stringify(r2Head)); if (!r2Head.exists) { throw new Error(`R2 object missing: ${message.raw_r2_key}`); } await postFixture("duplicate-delivery"); const duplicateDebug = await waitForMessageCount(1); console.log("debug:", JSON.stringify(duplicateDebug)); console.log( "queue-payload-sample:", JSON.stringify({ eventType: "email.received.v1", mailboxId: message.idempotency_key.split(":")[2] ?? "unknown", rawR2Key: message.raw_r2_key, rawSha256: message.raw_sha256, idempotencyKey: message.idempotency_key, }), ); console.log("PASS: local email smoke completed with one DO message after duplicate delivery");