Critical fixes:
- sanitizeLoadedHitStats/VisitorStats: restore full state shape so a
snapshot restore no longer crashes hit-counting middleware (missing
byPathReal, byPathBot, byDayReal, byDayBot, botReasons, ipHashIndex)
- /questions/share/🆔 read state.questions only, not draft questions
- inbound-email: validate date with Number.isFinite before toISOString
- study-reminders: wrap each send in try/catch so one failure doesn't
block remaining users; persist sent-markers after each success
Security:
- getClientIp: use req.ip (trust-proxy-resolved) instead of raw
x-forwarded-for header to prevent IP spoofing
- env-snapshot.env: delete immediately after backup tar stream ends
so secrets don't linger on disk between exports
Correctness / UX:
- contact form: email failures no longer 500 the user after the
submission is already saved; log and fall through instead
- study-account profile: cap data URI avatar at 6 MB
- admin enrollment PATCH: validate slug against study catalog
- signup: return 503 at MAX_STUDY_USERS instead of silently dropping
oldest accounts
Memory leaks:
- contactHits, downloadHits Maps: prune stale entries at 5000 entries
- resendEmailSubmissionIndex: trim to 2000 entries (oldest first)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- server/data.js: preserve source/htmlBody/inboundTo/messageId across
server restarts (sanitizeLoadedContactSubmissions was silently
dropping them on reload from disk)
- cloudflare/email-worker.js: rewrite MIME parsing to split on the
actual boundary marker instead of any literal "--", unfold
multi-line headers, and correctly recombine multi-byte UTF-8 in
quoted-printable decoding
- server/routes/inbound-email.js: validate Message-ID against RFC 5322
grammar before storing/using it, and compare the webhook secret with
timingSafeEqual to match the rest of the codebase's auth checks
- server/routes/contact.js: re-validate messageId at the point it's
injected into outgoing In-Reply-To/References headers; move the
allowed reply-from addresses into a shared config constant
- src/AdminPage.tsx: 30s inbox poll now syncs field updates (e.g.
archived) on already-loaded submissions instead of only appending
new ones; consolidate the duplicated from-address list
- .claude/launch.json: add a vite dev server preview config used to
verify these changes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Worker now extracts both text/plain and text/html MIME parts and sends
htmlBody in the webhook payload. Server stores it on the submission.
Admin inbox renders htmlBody in a sandboxed iframe when present, falling
back to plain text with whitespace preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cloudflare Email Worker forwards hello@ and nate@ to Gmail and POSTs
parsed MIME email to /api/inbound-email as admin inbox entries
- New server route authenticates via shared secret and deduplicates by
Message-ID before storing inbound emails as contact submissions
- Admin inbox shows "email" badge for inbound messages; reply composer
opens blank with auto-subject "Re: [original]" and signature preview
- Documents setup steps in cloudflare/email-worker.js and .env.example
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>