Spam submissions were still getting through with placeholder data. Two
more content filters on the public form endpoints:
- Reject emails from reserved documentation domains (example.com/.org/
/.net/.edu and subdomains) and reserved TLDs (.test/.example/.invalid/
localhost). testing@example.com and friends are never real leads.
- Reject a homesites count below 10. Real associations are larger; the
junk uses 0/1/2.
Both are validated server-side in security.js (validateEmail gains a
domain blocklist, new validateHomesites) and mirrored client-side in
app.js for immediate feedback. The homesites input min attribute goes
from 1 to 10. Blocked submissions return 400 with a `field` hint and
store nothing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ROI calculator accepted the email field with no validation at all and
stored whatever arrived, which is where the spam submissions were putting
shell-command payloads. Nothing was executable — there is no child_process
or eval in the codebase and all writes are parameterized — but the junk was
being persisted, and the field is the obvious place to stop it.
Adds security.validateEmail(), deliberately stricter than RFC 5322: the
local part is limited to the characters real addresses use, which excludes
every shell metacharacter (; | & ` $ ( ) < > \ " ' space) and CSV-injection
lead-ins. Also rejects control characters (including the CR/LF used for
mail-header injection), caps lengths at 254/64/253, rejects non-strings,
and normalizes to trimmed lowercase before storage.
Applied to /api/calculate (optional field — empty is fine, present must be
valid) and to /api/leads, replacing its much weaker regex. The client
mirrors the check for immediate feedback; the server remains authoritative.
Also hardens the /api/leads required-field checks, which called .trim() on
unvalidated input and returned a 500 rather than a 400 when a bot posted a
non-string.
Trade-off: RFC-legal but vanishingly rare addresses (foo!bar$baz@x.com, a
leading + in the local part) are rejected. Those characters are the
injection surface.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The calculator endpoint was open to anyone, and it has been collecting
spam submissions. Add a layered guard that runs before anything is
written to the DB or sent to the AI provider:
- Cloudflare Turnstile CAPTCHA, verified server-side (active when
TURNSTILE_* keys are configured; fails closed if unverifiable)
- Honeypot field that only bots fill in
- Signed, single-use form token enforcing a 4s minimum fill time
- Per-IP rate limiting (5 per 10 min, 25 per 24h)
Layers 2-4 need no configuration and work on their own, so submissions
are throttled immediately; adding Turnstile keys upgrades it to a full
challenge. Blocked submissions now stop the flow client-side instead of
silently showing a result.
Also sets `trust proxy` for correct client IPs behind nginx, caps the
JSON body at 32kb, and fixes a latent ReferenceError in the
"AI not configured" branch that called saveCalcSubmission() before the
variables it closes over were declared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AI text is still generated and saved to calc_submissions for internal
follow-up, but no longer displayed to the user — keeps the detailed
analysis behind the product sign-up wall.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Button loading spinner + disable on submit to prevent duplicate AI calls
- Email field and opt-in consent checkbox on calculator form (checked by default)
- Privacy assurance blurb below opt-in
- Refinement blurb on results screen explaining live cash-flow optimization
- calc_submissions SQLite table stores form inputs, computed results, AI text, email, opt-in
- GET /api/calc-submissions endpoint (x-admin-key protected) to retrieve submissions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- server.js: add /api/calculate endpoint using openai SDK with configurable
AI_API_URL, AI_API_KEY, AI_MODEL, AI_DEBUG env vars (works with any
OpenAI-compatible provider: NVIDIA NIM, Together AI, Groq, Ollama, etc.)
- app.js: make calculator submit handler async; call /api/calculate with
graceful fallback to client-side generated text if AI is unavailable
- package.json: add openai and dotenv dependencies
- AI_SETUP.md: rewrite to document new unified env var config with provider examples
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New "✦ Calculate Your Interest Income Potential" button between hero CTAs
with gradient border styling distinct from primary/ghost buttons
- Modal overlay collects: homesites, property type, annual dues + frequency,
reserve fund balance, and 2025 actual interest income
- Conservative calculation model: 4.0% HYSA for operating cash, 4.25% CD
ladder for 65% of investable reserves; operating multiplier varies by
payment frequency (monthly 10%, quarterly 20%, annual 35%)
- Results screen shows animated dollar counter, operating vs reserve
breakdown, AI-style narrative recommendation, and direct CTA to signup
- Modal closes on backdrop click or Escape key; CTA closes modal and
scrolls to early access signup form
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>