Moderation flow

MODERATION_REQUIRED defaults to true.

Moderation on

submit
  → honeypot / length / link count / rate limit
  → Cloudflare Turnstile (if configured)
  → seal into a signed, encrypted envelope
  → email to moderator
moderator opens the link
  → static page previews only
  → POST approve
  → re-verify signature, 7-day window, origin allowlist
  → GitHub Contents API write
  → visible in about 60 seconds

Pending data is never stored. It exists only inside the URL fragment of the review email.

Approve must be POST

Mail security gateways prefetch links. If approval were a GET, a scanner would approve on your behalf.

Why the fragment

  • Fragments are never sent to the server, so they never reach access logs
  • No practical length limit
  • The review page clears the hash with history.replaceState right after reading it

Credential rules

  • HMAC-SHA256 over the content
  • 7-day expiry
  • Idempotent: same id and content will not be written twice; same id with different content returns 409 and does not overwrite

Moderation off

With MODERATION_REQUIRED=false, a submission that passes Turnstile is written to Git directly. No email is sent, mail variables are optional, and the widget reports「留言已发布,约 1 分钟后可见」.

Risk: when Turnstile is not configured, or is bypassed, anonymous submissions reach the target repository. Same-repo mode makes that worse.

Captcha toggle (optional)

Setting both TURNSTILE_SITE_KEY and TURNSTILE_SECRET enables the captcha; leaving both empty disables it. It is independent of the moderation toggle:

ModerationTurnstilePath after submit
onsetverify → moderation email → approval writes
onunsetmoderation email right away (weaker anti-spam)
offsetverify → direct write
offunsetdirect write (honeypot and rate limit only)

Configuring only one of the two keys is a configuration error and health reports degraded.

Regardless of moderation, the form requires:

I agree that my name and message will be public after review and kept in the public Git repository history.

Deleting a comment removes the current version only; history keeps it.