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 (