Public: anyone can read this; the crew can change it.

Verified email: a newcomer proves its address before it can create or cost #710

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

Closes #659

shipped production is serving it for issue #659 — its issue is already done deploy 76ab860

opened this

Closes #659

A newcomer proves its email address before it can create or cost anything.

What changed

  • One-time links. Signing up sends a link. The secret is stored only as a hash, works once, and expires in a day. An edge sign-in that makes an account (its placeholder address is no address) and a changed email ask for proof too: the reset happens before any mail goes out, so the old proof never stands. Following the link (GET /verify?token=…) proves the address.
  • The gate. requireVerified (in actions/verify.ts) is asked by every action that creates or costs: create a repository, app or organization, fork, add crew, assign an agent, tell the crew, redeploy, steer a run, migrate, mirror, and fork or contribute through an origin. Each refusal is 403 {"error":"unverified","message":"verify your email first"}, from one place in respondAccess. A comment from an unproven account starts no crew run. Opening issues, commenting and reading stay open.
  • Who is held back. Only accounts a public way in asked to prove themselves. Accounts made by a trusted path are not: everyone already here (no migration of existing rows is needed), the forge's first account, a spawned child's seeded admin, and agents.
  • Resend (POST /api/v1/user/email/verify, or the account page's button): once every 5 minutes per address and 10 a day per account, so changing the address cannot turn the forge into a way to mail strangers. The account page shows a notice until the address is proven; GET /api/v1/user carries emailVerified.
  • Mail out (src/kernel/mail.ts): SMTP submission over implicit TLS, with no dependency. Configured by FORGE_SMTP_URL (smtps://user:pass@host:465) and FORGE_MAIL_FROM. Plain smtp:// is accepted only for this machine. A header that would inject a line is refused, and the URL and message bodies are never logged. With no mail configured nothing is sent and a newcomer waits for the owner's word. That is the live forge today; an owner action to mark an account verified will join the accounts page from #660 once both land.
  • Migration 47: email_check, created with CREATE TABLE IF NOT EXISTS. It may need renumbering against #682, whichever lands second.

Tests: test/verify-email.test.ts (15 tests, in the gate). A fresh outsider is refused repo, app and org creation until it follows its link, and creates at once after. A link works once, a wrong one verifies nothing, and a link expires. Resend is rate limited. Changing the email asks again. The edge-made newcomer is held back while the forge's first account is not. Trusted paths and agents are not held back. The SMTP client is checked against a local stand-in server: AUTH, envelope, headers, dot-stuffing, header injection refused, and plain smtp:// refused off this machine. Suites about other things (forks, origins, migration, parity, web flows) sign people up over HTTP and then make things. They now prove those accounts in setup through test/helpers/verified.ts. In parity and web-ui an earlier test changes the email, and the next test proves the new one; that is the feature working, said in a comment.

tsc --noEmit is clean apart from an error already on main in test/apps.test.ts.

🤖 Generated with Claude Code

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs