Skip to content

refactor(forms-api): declarative per-form spam policy#85

Merged
markgoho merged 3 commits into
trunkfrom
refactor/form-spam-policy
May 8, 2026
Merged

refactor(forms-api): declarative per-form spam policy#85
markgoho merged 3 commits into
trunkfrom
refactor/form-spam-policy

Conversation

@markgoho
Copy link
Copy Markdown
Owner

@markgoho markgoho commented May 7, 2026

Summary

  • Extract spam-protection pipeline into runSpamChecks driven by a declarative SpamPolicy (recaptcha + optional honeypot/gibberish/timing).
  • Extract email-then-persist with emailSent/warning fallback into sendAndPersist.
  • Contact form gets the full policy; doula-match form gets recaptcha-only.
  • Record the reactive-per-form decision in ADR-0001 so the asymmetry is intentional and not "fixed" by future reviews.

Why

Both handlers had near-duplicated plumbing, but the contact form had accumulated three extra spam checks (honeypot, gibberish, timing) that the match form lacks — because spam volume against the two forms differs. The duplication made the policy implicit (visible only in what code is/isn't there). After this refactor each handler reads as a single declarative object, and adding a check on one form is a one-line edit, not a cross-handler weave.

Test plan

  • bun test src/forms-api/routes/ — all 25 route tests pass without modification (HTTP contract preserved)
  • tsc clean
  • bun run lint — no new lint errors in the touched files

markgoho added 3 commits May 6, 2026 22:21
Extract the spam-protection pipeline (recaptcha + honeypot + gibberish +
timing) into a single declarative SpamPolicy passed to runSpamChecks, and
extract the email-then-persist sequence (with emailSent/warning fallback)
into sendAndPersist. Contact form gets the full policy; doula-match form
gets recaptcha-only, matching observed spam volume.

Records the reactive-per-form decision in ADR-0001 so future reviews do
not "fix" the asymmetry by unifying the two policies.

HTTP contract preserved; all 25 route tests pass unchanged.
- Wrap persist() in try/catch; log CRITICAL with emailSent+persistFailed
  when email succeeded but Firestore persist failed, then re-throw
- Tighten SendAndPersistResult to a discriminated union and document
  the persist callback's idempotency contract
- Use static log message prefixes in run-spam-checks (formType moved
  to structured metadata) and emit debug logs when honeypot/gibberish/
  timing policy sections are not configured
- Add route-level test exercising multi-field gibberish flagging
Auto-fix from eslint --fix to clear pre-existing
@typescript-eslint/no-unnecessary-type-assertion errors that were
failing CI.
@markgoho markgoho merged commit abacae1 into trunk May 8, 2026
2 checks passed
@markgoho markgoho deleted the refactor/form-spam-policy branch May 8, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant