Postmaster identity creation on the new instance
closedDeliverable
Ensure the per-domain postmaster identity (postmaster.at.<domain>) is created
against the PDS instance provisioned by Task 1 and is a durable per-domain
authority: invite via the cell admin token → createAccount with the shared
per-cell password → createSession on the ErrExists retry path → author the
public at.sovrn.domain.route record last.
This is mostly implemented in the saga (internal/appview/provision.go) and
internal/postmaster; the work here is hardening and verification, not a new
record type.
Scope
- Runs against the instance created in Task 1 (not a pre-existing one).
- Idempotent: retries reuse the account via
ErrExists/session. - The route record is the on-protocol “which cell hosts this domain” claim and a future extension point; the pri-20 relay does not consume it.
Non-goals
- No KMS keys, no
#atproto_spaceverification key /#atproto_space_hostservice entry, no space repo, noat.sovrn.domain.detail/at.sovrn.mail.accountwrites (deferred to Spaces Stage B).
Acceptance
- After a successful saga the postmaster account exists on the domain’s PDS and owns the route record.
- Retries after a crash reuse the account and converge without duplicate DIDs.
2 Comments
Implementation Plan: Postmaster identity on a newly provisioned PDS
Scope decision (from design review)
28c7d1f.9f75370).Current state (already implemented — do not rebuild)
internal/appview/provision.go:348-469—EnsureProvisioned→ invite (CreateInvite) →CreateAccount→ErrExists/CreateSessionretry →PutRouteRecord→ DB row last, with reverse compensations gated bypostmasterCreated/dkimCreated/domainChanged.internal/postmaster/postmaster.go,route.go(validate, retire,VerifyAuthor),route_read.go(GetRouteRecord).router.go:481-556sagaWiringbuildsPostmasterDepsfrom the cell admin token + shared per-cellPostmaster.PasswordFile, gated oncfg.PDS.Supervision. PDS handle domains come fromHandleDomainsForDomain=.<origin>,.at.<domain>(internal/pdslifecycle/env.go:194), which already matchespostmaster.at.<domain>.provision_saga_test.go(order, reuse, per-step faults, compensation, provenance, race backstop),postmaster_test.go(body shapes,ErrExists, delete-absent).Gaps
postmaster.at.<domain>and the routedomainfrom the raw (lowercased) name (provision.go:372-373), while the PDS serves the IDNA-normalized suffix andValidateRouteis ASCII-only (route.go:44). An IDN hosted domain would create the Stalwart domain + PDS instance, then fail atCreateAccount/PutRouteRecord— after side effects, and as a retryable 500.internal/pdslifecycle/handles.goalready hasValidateAccountLabel/ValidateVanityHandle; the saga never uses them.domain_create.go:52-55);IsUserInputError(provision.go:79) does not know them → infinite retry.createAccountbody (handle/email/shared password/inviteCode) nor thatEnsureProvisionedreceives the normalized domain.postmaster_test.goonly pins endpoints against ZDS source comments;scripts/smoke-zds.shexercisescreateAccountbut not the postmaster sequence. (Deferred to28c7d1f.)Task 1 — Reject IDN domains before mutation
internal/appview/provision.go: afterdomainNameis lowercased/trimmed (provision.go:194) and after the duplicate short-circuit (provision.go:209-220), reject any name containing non-ASCII bytes with a new sentinelErrInvalidDomain. Placement guarantees zero Stalwart/DKIM/PDS/postmaster calls, and preserves idempotent duplicate behavior for existing rows.ErrInvalidDomaintoIsUserInputError(provision.go:79) so the UI path classifies it too.internal/appview/domain_create.go:47-56: mapErrInvalidDomain→400 InvalidRequest(alongsideErrTenantNotFound).Task 2 — Classify postmaster terminal errors
ErrInvalidDomain(Task 1).ErrInvalidRoute/author errors already surface only where the reconciler consumes them.internal/appview/ui/handler.goalready routes unknown create errors viaIsUserInputError(handler.go:319-332); add/extend a UI test asserting a bad domain renders a 4xx form error, not a 500.Task 3 — Strengthen the saga unit tests
In
internal/appview/provision_saga_test.go, using the existingsagaZDS/sagaFixture:createAccountbody for the happy path: handlepostmaster.at.b.example, email[email protected], sharedPassword, and theinviteCodeequal to the minted invite (sagaZDScurrently only records call order — add agotCreatecapture).fakePDS.EnsureProvisionedreceives the normalized domain (record the arg).PutRouteRecordoverwrite, no second account."münchen.de"→ErrInvalidDomain, and assert nostalwart:/pds:/zds:calls.domain_createhandler test mappingErrInvalidDomain→ 400.Task 4 — Docs/comments
ProvisionDomaindoc comment and theinternal/postmaster/route.go:44cross-reference.Verification
go test ./internal/postmaster/... ./internal/appview/... ./internal/pdslifecycle/...just test(SOVRN_INTEGRATION=0 go test ./...)Non-goals (unchanged)
No KMS/
#atproto_space, no space repo, noat.sovrn.domain.detail/at.sovrn.mail.accountwrites; the pri-20 relay does not consume the route record.Acceptance mapping
provision.go:434) and pinned byTestProvisionDomainSagaHappyPath.provision.go:392-407) and pinned byTestProvisionDomainSagaReusesExistingPostmaster; Task 3 strengthens the body/domain assertions.Implementation (Tasks 1-4) landed in the working copy, uncommitted:
Verification: gofmt clean; go vet clean; go test ./internal/appview/… ./internal/postmaster/… ./internal/pdslifecycle/… and SOVRN_INTEGRATION=0 go test ./… all green. No live ZDS smoke (deferred to 28c7d1f) and no read-after-write (reconciler 9f75370), per plan.