Publish at.sovrn.mail.service lexicon (goat, after webmail eval ac83db8)

open
#764b8e2 opened by agent Sep 16

Goal

Publish the locked at.sovrn.mail.service record lexicon so any PDS can resolve and validate it on putRecord (explicit validate:true writes).

Blocked until after webmail client evaluation (ac83db8): if the webmail fork needs extra endpoint fields (e.g. mailEndpoints.jmap/imap/smtp inventory), finalize those schema changes first. Lexicon evolution is append-only (new optional fields only; breaking change = new NSID), so do not publish until the record shape is final.

Context

  • Canonical file: lexicons/at/sovrn/mail/service.json (lexicon:1, id: at.sovrn.mail.service, defs.main.type: record, key: literal:self, required appview, webmail URIs, optional mailEndpoints{jmap,imap,smtp}).
  • NSID authority: at.sovrn.mail.service → name=service, authority=at.sovrn.mail → domain mail.sovrn.at. Resolver looks up _lexicon.mail.sovrn.at TXT only (no hierarchy walk). _lexicon.sovrn.at alone does NOT cover it.
  • Authority account: did:plc:54ba6vxrdbzi6nexczpk5xzb (alsoKnownAs: at://sovrn.at, PDS https://selfhosted.social). Verified via plc.directory.
  • DNS today (checked 2026-09-16): both _lexicon.sovrn.at and _lexicon.mail.sovrn.at NXDOMAIN — nothing published yet.
  • Production write path: internal/authbroker/servicerecord.go:111-122 (serviceRecordBody) via internal/authbroker/authbroker.go:188 (com.atproto.repo.putRecord, currently no validate flag → optimistic validation).

Steps (goat + app password)

1. Freeze and lint canonical lexicon

cat lexicons/at/sovrn/mail/service.json
jj diff -- lexicons/at/sovrn/mail/service.json   # must be clean; v1 locked, do not edit
goat lex lint ./lexicons/at/sovrn/mail/service.json

Confirm no external $refs (only string/format:uri + nested object) — single-record publish suffices. Do NOT publish account.json, defs.json, etc. in this issue.

2. Publish DNS authority TXT

In the sovrn.at zone:

_lexicon.mail.sovrn.at.  300  IN  TXT  "did=did:plc:54ba6vxrdbzi6nexczpk5xzb"

Optional (future at.sovrn.* group lexicons):

_lexicon.sovrn.at.  300  IN  TXT  "did=did:plc:54ba6vxrdbzi6nexczpk5xzb"

Verify:

dig +short TXT _lexicon.mail.sovrn.at
curl -s 'https://dns.google/resolve?name=_lexicon.mail.sovrn.at&type=TXT' | jq .

Must return "did=did:plc:54ba6vxrdbzi6nexczpk5xzb" before proceeding (allow 5-10 min, TTL 300).

3. Publish schema record with goat (app password, never main password)

export PDS_URL='https://selfhosted.social'
goat account login --pds "$PDS_URL"   # handle sovrn.at + app-password
goat account whoami                    # must be did:plc:54ba6vxrdbzi6nexczpk5xzb
goat lex publish ./lexicons/at/sovrn/mail/service.json
# expected: 🟢 at.sovrn.mail.service
goat lex pull at.sovrn.mail.service
diff ./lexicons/at/sovrn/mail/service.json ./lexicons/at/sovrn/mail/service.json && echo IDENTICAL

This writes com.atproto.lexicon.schema with rkey: at.sovrn.mail.service and record: {$type: com.atproto.lexicon.schema, lexicon:1, id, defs, description}. Do not add revision or extra fields.

4. Verify resolution (DNS → DID → PDS → record)

curl -s 'https://selfhosted.social/xrpc/com.atproto.repo.getRecord?repo=did:plc:54ba6vxrdbzi6nexczpk5xzb&collection=com.atproto.lexicon.schema&rkey=at.sovrn.mail.service' | jq '{uri,cid,value:.value.id}'
curl -s 'https://selfhosted.social/xrpc/com.atproto.lexicon.resolveLexicon?nsid=at.sovrn.mail.service' | jq .

Expect uri: at://did:plc:54ba6vxrdbzi6nexczpk5xzb/com.atproto.lexicon.schema/at.sovrn.mail.service. AuthorityNotPublished = DNS TXT wrong/missing. Also check https://pds.ls/at://did:plc:54ba6vxrdbzi6nexczpk5xzb/com.atproto.lexicon.schema/at.sovrn.mail.service renders.

5. Prove PDS validation on write (throwaway test account only)

# valid write
curl -s -X POST "$PDS/xrpc/com.atproto.repo.putRecord" \
  -H "Authorization: Bearer $TEST_JWT" -H 'Content-Type: application/json' \
  -d '{"repo":"<TEST-DID>","collection":"at.sovrn.mail.service","rkey":"self","validate":true,"record":{"$type":"at.sovrn.mail.service","appview":"https://mx1.eu.sovrn.at","webmail":"https://mx1.eu.sovrn.at/webmail"}}' | jq .
# expect validationStatus: valid

# invalid write (missing appview) must fail
curl -s -X POST "$PDS/xrpc/com.atproto.repo.putRecord" \
  -H "Authorization: Bearer $TEST_JWT" -H 'Content-Type: application/json' \
  -d '{"repo":"<TEST-DID>","collection":"at.sovrn.mail.service","rkey":"self","validate":true,"record":{"$type":"at.sovrn.mail.service","webmail":"https://mx1.eu.sovrn.at/webmail"}}' | jq .
# expect 400 LexiconViolation / missing appview

Clean up throwaway test records afterwards; leave real user records untouched.

6. Follow-ups (separate)

  • Consider adding "validate": true to serviceRecordBody (internal/authbroker/servicerecord.go:111) so first-login writes fail loudly (existing ErrPDSUnavailable retry page) instead of optimistic writes. File as separate bug; out of scope here.
  • Update docs/09-open-questions.md Q3 row once published (date + CID + AT-URI).

References