Turn anti-spam back on for the hire form - #177
Conversation
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5K2AxX2QZ98JW1Exe5wup
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
ThreatCrush Security Scan12 finding(s) HIGH/CRITICAL: 1 | MEDIUM: 6 | LOW: 5
Snippets are redacted; ThreatCrush never prints matched credential material. |
Why
Contact-form spam POSTs straight at
/api/contactwithout loading the page. A hidden honeypot field therefore ends up absent from the body rather than filled, and "is it empty?" answers yes, so the check passes. A honeypot only catches a bot that renders your page.Part of a fleet-wide rollout after a submission got through a working honeypot on another property.
This route had anti-spam switched off
honeypot: false, with nothing in its place./api/contactaccepted whatever anyone sent it.The honeypot is back on and, unlike several sibling repos in this rollout, the field is actually rendered so it can fire. On its own that still misses the common case, which is what the token is for.
What
Bumps
@profullstack/stackto 0.2.0 and passes its newguard. The page mints a signed proof-of-render token and the route requires it back, so a request that never loaded the form has nothing to present. The token carries its issue time, which gives a fill-time floor for free.[spam? N]Only the first four block. Scoring can tag a message but never drop one — every signal it reads has an innocent explanation.
The guard runs before field validation on purpose, so a bot that gets a response has not learned which fields the route wants. Delivered mail gains a provenance block: IP, user-agent, fill time, and which signals fired.
Wiring
The page was a client component, so there was nowhere on the server to mint a token, and the secret must never reach the browser.
HireFormgains three props and renders the honeypot;/hiremints the token and passes them down./hirebecomesforce-dynamic— a cached page would hand every visitor the same dead token.Config
No new env var needed. The secret falls back to
RESEND_API_KEY, which sending already cannot work without. If neither that norFORM_GUARD_SECRETis set the guard disables itself and says so in the logs rather than silently.FORM_GUARD_ENFORCE=0drops back to scoring-only if a real sender ever reports being turned away.🤖 Generated with Claude Code
https://claude.ai/code/session_01H5K2AxX2QZ98JW1Exe5wup