From 58ddf9ab48787e86b31bff502867698305f3220f Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 7 Sep 2026 00:26:36 +0000 Subject: [PATCH] feat(contact): turn anti-spam back on for the hire form The route had anti-spam switched off outright (`honeypot: false`) with nothing in its place, so /api/contact accepted anything anyone sent it. The honeypot is back on and, unlike several sibling repos, the field is actually rendered so it can fire. On its own that still misses the common case: contact-form spam POSTs straight at /api/contact without loading the page, which leaves a hidden field absent from the body rather than filled, and the check passes. So this also bumps @profullstack/stack to 0.2.0 and passes its new `guard`. The hire page mints a signed token at render time and the route requires it back, so a request that never loaded the form has nothing to present. The token carries its issue time, giving a fill-time floor, and the guard adds a per-IP rate limit. The guard runs before field validation on purpose, so a bot that gets a response has not learned which fields the route wants. /hire becomes force-dynamic, since a cached page would hand every visitor the same dead token. Nothing else changes render mode. Content scoring only tags: a suspicious message still arrives and is still persisted, with [spam? N] in the subject and a provenance block naming the sender's IP, user-agent, fill time and the signals that fired. It can never drop one. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01H5K2AxX2QZ98JW1Exe5wup --- apps/web/package.json | 5 ++-- apps/web/src/app/api/contact/route.ts | 8 +++++- apps/web/src/app/hire/page.tsx | 16 +++++++++-- apps/web/src/components/HireForm.tsx | 32 ++++++++++++++++++++- apps/web/src/lib/contact-guard.ts | 41 +++++++++++++++++++++++++++ pnpm-lock.yaml | 20 +++++++++---- 6 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 apps/web/src/lib/contact-guard.ts diff --git a/apps/web/package.json b/apps/web/package.json index e1a7950..93ff1c4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,7 +12,7 @@ "@iarna/toml": "^2.2.5", "@profullstack/autoblog": "github:profullstack/autoblog#v0.4.0", "@profullstack/pluginstore": "^0.1.1", - "@profullstack/stack": "^0.1.3", + "@profullstack/stack": "^0.2.0", "@profullstack/x402-gateway": "^0.1.0", "@supabase/supabase-js": "^2.101.1", "@threatcrush/scan": "workspace:*", @@ -22,7 +22,8 @@ "qrcode.react": "^4.2.0", "react": "19.2.4", "react-dom": "19.2.4", - "resend": "^6.12.2" + "resend": "^6.12.2", + "server-only": "^0.0.1" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/apps/web/src/app/api/contact/route.ts b/apps/web/src/app/api/contact/route.ts index 0b993f6..6f7a306 100644 --- a/apps/web/src/app/api/contact/route.ts +++ b/apps/web/src/app/api/contact/route.ts @@ -1,4 +1,5 @@ import { createContactRoute } from "@profullstack/stack/email"; +import { contactGuard } from "@/lib/contact-guard"; import { createClient, type SupabaseClient } from "@supabase/supabase-js"; let supabase: SupabaseClient | undefined; @@ -40,7 +41,12 @@ function messageWithExtras(s: { export const POST = createContactRoute({ from: "ThreatCrush ", to: "hello@threatcrush.com", - honeypot: false, + // Was `false`, with nothing in its place. The field is rendered now, so + // it can actually fire. + honeypot: "website", + // Requires a token minted when the form rendered. Runs before field + // validation, so a bot never learns which fields the route wants. + guard: contactGuard ?? undefined, fieldLabels: FIELD_LABELS, subject: (s) => `[ThreatCrush] New ${s.fields.topic ?? "general"} inquiry from ${s.name}`, diff --git a/apps/web/src/app/hire/page.tsx b/apps/web/src/app/hire/page.tsx index 101bf9f..1736361 100644 --- a/apps/web/src/app/hire/page.tsx +++ b/apps/web/src/app/hire/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { SITE_URL } from "@/lib/blog"; import { HireForm } from "@/components/HireForm"; +import { contactGuard } from "@/lib/contact-guard"; export const metadata: Metadata = { title: "Hire Us — human-led security assessments", @@ -88,7 +89,14 @@ const steps = [ }, ]; -export default function HirePage() { +// The hire form carries a token minted at render time, so this page must +// not be cached. A stale page would hand every visitor the same dead token. +export const dynamic = "force-dynamic"; + +export default async function HirePage() { + const token = contactGuard ? await contactGuard.issue() : null; + const guardFields = token ? contactGuard!.fields(token) : null; + return (