| fixed | high | src/app/api/subscribe/route.ts. Fixed 2026-09-19 and swept across every product with a subscribe route. They carried it in two shapes, shipwall already had the correct one, and the template was fixed too so the next fork does not reintroduce it. Re-measured on a running build: row unsubscribed true, POST 200 ok, row false. | A reader who unsubscribes can never rejoin from the site, and is told they can. POST /api/subscribe upserts with ignoreDuplicates, so an existing row is untouched, and the confirmation send is gated on `!row.confirmed`. For a confirmed-then-unsubscribed address both branches fall through: unsubscribed_at stays set, no mail is attempted, the route answers 200 and the footer says 'Check your inbox and confirm.' Measured 2026-09-19 on the running build: cassian.orme@awdesk.invalid unsubscribed_at 2026-09-05 20:11:00+00 before and after, HTTP 200 {ok:true}, and no [email] line for that address in the server log. |
| fixed | medium | src/app/api/subscribe/confirm/route.ts, src/app/api/subscribe/unsubscribe/route.ts, scripts/send-digest.mjs | One uuid is both the confirm token and the unsubscribe token, it never rotates and it never expires. Anyone who ever holds a confirmation link holds a permanent one-click unsubscribe for that address, and send-digest.mjs prints the same uuid in the List-Unsubscribe header of every issue, so the value is in the mail headers of every message that address has ever been sent. |
| fixed | medium | src/app/api/subscribe/route.ts, src/components/Subscribe.tsx | The subscribe route has no bot check and no rate limit, and it re-sends the confirmation on every request while the address is unconfirmed. The honeypot exists only in the React component, which reads input[name=website] client side; the route reads {email, source} and nothing else. Measured 2026-09-19: five POSTs for one unconfirmed address produced five confirmation sends, which with a live Resend key is five real emails to an address the caller typed. |
| fixed | medium | scripts/mirror-posts.mjs | scripts/mirror-posts.mjs hardcodes SITE_URL as https://agentwire.thecompound.tech and POSTs /api/revalidate to it after any successful write, so a developer or test run of the mirror on any machine that has REVALIDATE_SECRET in its environment reaches production. This environment leaves that variable unset and firewalls the host, and the run prints 'no REVALIDATE_SECRET on this machine'. |
| fixed | low | src/app/api/digest-items/route.ts. Fixed 2026-09-19 in agentwire 035f862: the route accepts the cron credential only in x-cron-secret; query strings can no longer authenticate it. | GET /api/digest-items accepts CRON_SECRET in a query string as well as a header, so the shared cron secret lands in access logs, proxy logs and any Referer a rendered link would carry. |
| fixed | low | public.agentwire_email_sends. Fixed 2026-09-19 in agentwire d50385c: POST /api/email/webhook verifies Resend's Svix signature and replay window, then stamps the named send's first opened_at or clicked_at value. | agentwire_email_sends ships opened_at and clicked_at and no code path anywhere writes them, so every engagement figure the table implies reads as zero forever rather than as unmeasured. |