Cypress
Call Mailwurf from Cypress tasks, so the API key never reaches the browser.
Cypress specs run in the browser. Keep the API key on the Node side and expose inbox calls as tasks.
import { defineConfig } from "cypress";
import { createMailwurf } from "@mailwurf/e2e";
const mailwurf = createMailwurf(process.env.MAILWURF_API_KEY!); // API key (mw_live_…) from Dashboard → API keys
export default defineConfig({
e2e: {
baseUrl: process.env.APP_URL ?? "http://localhost:3000",
setupNodeEvents(on) {
on("task", {
async createInbox() {
const inbox = await mailwurf.createInbox();
return { id: inbox.id, address: inbox.address };
},
async waitForMail({ inboxId, subject }: { inboxId: string; subject: string }) {
const mail = await mailwurf.waitFor(inboxId, { subject, timeout: 30_000 });
return { subject: mail.subject, text: mail.text, links: mail.links, otp: mail.otp };
},
});
},
},
});Tasks must return plain data. Return the fields the specs need instead of the message object.
type Inbox = { id: string; address: string };
type Mail = { subject: string; text: string; links: string[]; otp: string };
it("confirms signup from the inbox", () => {
cy.task<Inbox>("createInbox").then((inbox) => {
cy.visit("/signup");
cy.get("input[name=email]").type(inbox.address);
cy.contains("button", "Sign up").click();
cy.task<Mail>("waitForMail", { inboxId: inbox.id, subject: "Confirm" }).then((mail) => {
const link = mail.links.find((url) => url.includes("/confirm"));
expect(link, "confirm link").to.be.a("string");
cy.visit(link!.replaceAll("&", "&"));
});
cy.contains("confirmed").should("be.visible");
});
});Cypress stops a task after taskTimeout (60 seconds by default). Keep the Mailwurf timeout below it.
@mailwurf/e2e is ESM-only. If Cypress cannot load it from cypress.config.ts, rename the file to cypress.config.mjs or set "type": "module" in package.json.
Mailwurf