Summary
POST /api/trade-inquiry validates that email is non-empty and within the length limit, but never checks its format. A malformed address is persisted to tradeLeads and passed verbatim to Resend as replyTo, where it fails — producing a guaranteed notification failure (and a Sentry alert) for a lead the owner cannot reply to anyway.
Evidence
app/api/trade-inquiry/route.ts:68 — required-field check is !email only; the length check (tradeLeadFieldTooLong) is the only other constraint.
lib/trade-leads.ts:65 — replyTo: input.email is sent unvalidated. Resend rejects an invalid replyTo, the send throws, and processTradeInquiry logs trade_inquiry.notification_failed (which forwards to Sentry in production via logError).
- The lead is still persisted and the customer sees success — so the failure mode is: dead record in Firestore plus monitoring noise, with no way to reach the submitter unless they also provided a phone number.
- Client-side
type="email" validation does not help: the endpoint is public and bots/malformed requests bypass the form entirely.
Why existing tests did not catch it
Route/validation tests cover required fields, field-length limits, honeypot, and rate limiting — not field formats. There was no email-format rule to assert.
Recommended scope
Add a format check in the route before persistence — the same EMAIL_REGEX pattern already used by app/api/admin/users/route.ts and components/admin-access.tsx — returning 400 with a clear message. Optionally consider constraining venueType to the known option set as well (currently free-text up to 64 chars server-side), though that is lower value.
Acceptance criteria
Overlap
None — #57/#59 covered persistence and retention policy, not input validation depth.
Summary
POST /api/trade-inquiryvalidates thatemailis non-empty and within the length limit, but never checks its format. A malformed address is persisted totradeLeadsand passed verbatim to Resend asreplyTo, where it fails — producing a guaranteed notification failure (and a Sentry alert) for a lead the owner cannot reply to anyway.Evidence
app/api/trade-inquiry/route.ts:68— required-field check is!emailonly; the length check (tradeLeadFieldTooLong) is the only other constraint.lib/trade-leads.ts:65—replyTo: input.emailis sent unvalidated. Resend rejects an invalidreplyTo, the send throws, andprocessTradeInquirylogstrade_inquiry.notification_failed(which forwards to Sentry in production vialogError).type="email"validation does not help: the endpoint is public and bots/malformed requests bypass the form entirely.Why existing tests did not catch it
Route/validation tests cover required fields, field-length limits, honeypot, and rate limiting — not field formats. There was no email-format rule to assert.
Recommended scope
Add a format check in the route before persistence — the same
EMAIL_REGEXpattern already used byapp/api/admin/users/route.tsandcomponents/admin-access.tsx— returning400with a clear message. Optionally consider constrainingvenueTypeto the known option set as well (currently free-text up to 64 chars server-side), though that is lower value.Acceptance criteria
emailreturns400and persists nothing.+tags and subdomains).Overlap
None — #57/#59 covered persistence and retention policy, not input validation depth.