Optional contact on a name, behind a forwarding guard address - #442
Merged
Conversation
…address The registry had no contact field and was publishing contact details anyway: GET /api/moshpit/tlds returned `owner_email` in cleartext for every ending, unauthenticated and pageable — thousands of real addresses, other people's included. The worst of both: personal addresses exposed with no consent, and still no dependable way to reach whoever holds a name. Both halves land together. A holder can now opt in on /pit/contact. They say where they read mail; the registry publishes `<token>@moshcode.sh`, which forwards there via Forward Email. The real address is never in a page, an API response, or the log — including on the holder's own management route, which does not need it. Three states per name or ending: `guard` (the forwarding address, default), `public` (their address as typed), `none` (nothing published, token kept — a published address lives in other people's address books, so taking it down for a week must not mint a different one on the way back). Absence of a row is the default and means no contact, which is what every existing name has, with no backfill. Tokens are ten characters of digits and consonants. Excluding vowels means a token can never spell a word, so a minted address can never collide with a mailbox someone holds at the same domain — support@, abuse@ and notify@ are unreachable from the alphabet rather than from a reserved list. A contact dies with the name: releaseName drops the row and destroys the alias alongside the pins, records and twin, or the next holder inherits a forwarding address pointing at the last one. Aliases are created with recipient verification on, so publishing a guard address needs consent from the address itself and not just from whoever filled in the form — otherwise the registry is a spam relay wearing our domain. Disabled aliases reject with 550 rather than silently discarding. The endings list now follows the policy /api/moshpit/log already wrote down: ownership is public, the account behind it is not. Endings keep the same owner digest the log publishes, so two held by one person are still visibly one person. No client read `owner_email` — not moshpit-registry, the DNS bridge, or any page here. Inert until DNS is done: moshcode.sh has no MX today. Without a key a contact is recorded, stays `pending`, and publishes nothing. docs/ has the three steps. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017QSp1aAoQQB7jFV7hwt4jJ
| * returns an address or nothing. | ||
| */ | ||
| export async function getContactPrivate(tld, label = "") { | ||
| return get(`SELECT ${CONTACT_COLS} FROM moshpit_contacts WHERE tld = ? AND label = ?`, [tld, label]); |
|
|
||
| /** Every contact a holder has, for the /pit page to draw. */ | ||
| export async function listContactsForUser(userId) { | ||
| return all(`SELECT ${CONTACT_COLS} FROM moshpit_contacts WHERE user_id = ? ORDER BY tld, label`, [userId]); |
| */ | ||
| export async function unsyncedContacts(limit = 200) { | ||
| return all( | ||
| `SELECT ${CONTACT_COLS} FROM moshpit_contacts WHERE alias_status IN ('pending','failed') ORDER BY updated_at LIMIT ?`, |
| test("a guard contact publishes the guard address, never the real one", () => { | ||
| const row = { | ||
| email: "real@example.com", visibility: "guard", | ||
| guard_token: "k7m2xqbn3f", alias_status: "live", |
| // The failure this prevents is publishing an address that bounces, which is | ||
| // worse than publishing none: the holder looks reachable and is not. | ||
| for (const alias_status of ["pending", "failed", "revoked"]) { | ||
| const row = { email: "real@example.com", visibility: "guard", guard_token: "k7m2xqbn3f", alias_status }; |
| }); | ||
|
|
||
| test("`public` is the only way the real address is ever shown, and `none` shows nothing", () => { | ||
| const base = { email: "Real@Example.com", guard_token: "k7m2xqbn3f", alias_status: "live" }; |
ThreatCrush Security Scan33 finding(s) in the 9 file(s) this pull request changes. MEDIUM: 30 | LOW: 3
38 pre-existing finding(s) elsewhere in the repository — **HIGH/CRITICAL**: 5 | **MEDIUM**: 27 | **LOW**: 6Not introduced by this pull request. The full set is in the Security tab.
…and 18 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
This was referenced Aug 29, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A Moshpit name can now say how to reach whoever holds it, without saying who that is. Opt-in, off by default.
The holder gives an address they read on
/pit/contact; the registry publishesk7m2xqbn3f@moshcode.sh, which forwards there via Forward Email. Their real address is never in a page, an API response, or the allocation log — including on their own management route, which does not need it to work.Why now
The registry had no contact field and was publishing contact details anyway.
GET /api/moshpit/tldsreturnedowner_emailin cleartext for every ending, unauthenticated and pageable — thousands of real addresses, other people's included, to anyone who could count to 200 in an?offset=. The worst of both: personal addresses exposed with no consent, and still no dependable way to reach a name's holder.Both halves are in this PR, because shipping either alone gets the trade wrong.
The redaction
The endings list now follows the policy
/api/moshpit/logalready wrote down — ownership is public, the account behind it is not. Endings keep the sameownerdigest the log publishes, so two held by one person are still visibly one person and "who holds how much of the namespace" stays answerable. What is gone is the address.Nothing read that field: not
moshpit-registry, not the DNS bridge, notmoshpit-name/moshpit-resolve, not a page in this app. The search branch of the same endpoint and/api/moshpit/marketalready redacted; this brings the last branch into line.Three states
guard<token>@moshcode.sh, forwarding. The default.publicnonenoneis not the same as no contact. A published address lives in other people's address books and on pages we do not control, so taking one down for a week must not mint a different one on the way back.Details worth a look
support@,abuse@,notify@are unreachable from the alphabet rather than from a reserved list somebody has to maintain.releaseNamedrops the row and destroys the alias alongside the pins, records and twin. Otherwise the next holder inherits a forwarding address pointing at the last one, and an offer for the name goes to a stranger./pit/contactstates rather than letting the holder find out from a bounce.Not live yet — needs DNS
moshcode.shhas no MX and no SPF today, so no alias can be minted. WithoutFORWARDEMAIL_API_KEYthe feature is inert rather than broken: a contact is recorded,alias_statusstayspending,/pit/contactsays plainly that no mail host is configured, and nothing is published on any name.Three steps in
docs/contact-and-guard-addresses.md. The first is a hand-off —moshcode.shis on Porkbun and there are no credentials on the dev box:mx1/mx2.forwardemail.net, SPF, and their site-verification TXT.FORWARDEMAIL_API_KEYon the Railway service, from the vault.Tests
28 new, covering the rules, the storage lifecycle, the routes and the pages. Full suite 670/670 green.
🤖 Generated with Claude Code
https://claude.ai/code/session_017QSp1aAoQQB7jFV7hwt4jJ