[C6] End-to-end risk probe: signup→login→read-mailbox + failure injection
closedGoal
The Phase-0 exit gate: one scripted scenario exercising every risky seam in sequence, plus deliberate failure injection. Purpose is not product code — it is to find where the issues are, and to force every hidden assumption into a failing test or an ADR.
Context: docs/01-architecture.md §3–§6.
Tasks
- [ ] Scripted happy path (CI job
risk-probe):- Publish claim record on local PDS test account (direct repo write)
- Minimal watcher stub consumes jetstream event → DB row
- DNS probe stubbed-pass (harness DNS) → state
active stalwartsyncprovisions Domain+Account with alias- Browser-sim atproto OAuth login for that DID
- sovrn OIDC token minted → JMAP session opens → mailbox list returned
- [ ] Failure injections, each asserting graceful behaviour + recovery: Stalwart container stopped mid-step-4; PDS unreachable at step 5; tokenissuer keys unavailable at step 6; duplicate jetstream event replay
- [ ] Written findings:
docs/notes/phase0-findings.md— every friction point, surprise, doc correction; new bug issues filed per finding and linked here
Acceptance criteria
- Happy path green in CI end-to-end, repeatable from clean stack
- Each injected failure produces the designed failure-mode behaviour from docs/01 §6 (no crashes, no stuck states)
- Findings doc committed; follow-up issues created and referenced in tracker f98ccdc
Risks this should surface (expected unknowns)
- Timing/ordering seams between watcher stub and provisioning
- Real latency of Stalwart cache invalidation after Account set
- Anything about DPoP/JWKS/directory caching that only appears under restart cycles
6 Comments
Signup journey gains a pre-provisioning ownership gate
The signup story now has a two-gate flow (ADR-0005, docs/03-provisioning.md §4/§4a). Step 3 in the happy path changes materially:
verifyingclaim must pass the ownership gate — either Mechanism A (TXT challenge_sovrn-challenge.<domain>=sovrn-verification=<per-claim nonce>) or Mechanism C (apex-handle_atprotoTXT proof, only when handle == registrable domain).verifiedclaim triggersstalwartsyncDomain+Account provisioning (step 4). The old single “DNS probe stubbed-pass →active” collapses into two stub points: ownership probe, then readiness probe (MX/SPF/DKIM).Implications for this risk probe:
verified→activedeterministically.b5a31bd([C7]) and ADR-0005.Re-scoped after ADR-0006 (data placement & space topology) — 2026-08-28
The original tasks baked in an un-examined assumption: user publishes a public
com.sovrn.mail.claim→ jetstream → reconciler → space projection. ADR-0006 removes the public claim and the jetstream signup path. Updated happy path and failure injections below.Happy path (revised)
claimedverifiedat.sovrn.domain.detail+at.sovrn.mail.accountrecords (tenant + domain spaces)Registry/setactiveat.sovrn.mail.servicerecord written to the user’s public repo (endpoints only)Failure injections (revised)
authorDid == postmasterfilter)duplicate jetstream event replay(dropped: jetstream is identity-only now, not in the signup path)Acceptance criteria (revised)
authorDidenforcement proven: non-postmaster records are ignoredf98ccdcDesign basis: docs/adr/0006-data-placement-and-space-topology.md.
Lexicon design landed — at.sovrn.* (feeds this probe + f6192a1/a472ea3)
Delivered the first-cut
at.sovrn.*lexicon surface and Go codegen, per ADR-0007 (write-through postmaster PDS projection; facts on protocol, app DB = transient/ security/cache). Files underlexicons/at/sovrn/:Space types (
"type": "space", proposal-0016 wire shape): -at.sovrn.space.tenant— collections[at.sovrn.tenant.detail, at.sovrn.domain.detail]-at.sovrn.space.domain— collections[at.sovrn.mail.account]Records: -
at.sovrn.tenant.detail(tenant space,literal:self) —name,createdAt-at.sovrn.domain.detail(tenant space,tid) —domain,mode,status,mxVerifiedAt,dkimState,tierDefaults,dns{mx[],spf,dkimSelector,dkimTxt}-at.sovrn.mail.account(domain space,tid) —did,address,status,verifiedAt-at.sovrn.mail.service(public repo,literal:self) —appview,webmail, optionalmailEndpoints{jmap,imap,smtp}XRPC (app-view / reconciler, org→domain→account progression): -
at.sovrn.tenant.create—{name?}→{tenantId, name, space}(name defaults to handle) -at.sovrn.domain.create—{tenantId, domain, mode}→{domainId, state, challenge?}-at.sovrn.domain.list/at.sovrn.domain.getDnsState-at.sovrn.mail.createAccount—{domainId, address, did?}→{accountId, state}(did omitted = self) -at.sovrn.mail.listAccounts/at.sovrn.mail.removeAccountCodegen:
cmd/sovrn-lexgen(wraps indigolex) emitsapi/sovrntypes for records + XRPC + views; strips indigo’s dynamic$typeregistration (JSON-only, no cbor-gen dep) — regenerate viajust gen/go generate ./api/sovrn. Space types are hand-written inapi/sovrn/spacetypes.go(indigolexhas no"type":"space"case — worth a follow-up).Records relevant to this probe’s steps: step 4 =
at.sovrn.domain.detail+at.sovrn.mail.account(tenant/domain spaces); step 7 =at.sovrn.mail.service(public repo). These schemas are the source of truth the spaceproj publisher/ reader (12ce549) and reconciler (f15633a) validate against.Deferred to those bugs: the reconstruction test (fresh appview state from PDS-only reads, diffed vs app DB — proves completeness/portability) and the CI lexlint/idempotency check, both of which need the spaceproj/PDS harness.
Verification:
go build ./...,go vet ./...,go test ./...all green.Happy-path risk probe:
TestIntegrationHappyPathLexicon(uses the new lexicons)Added
internal/integration/happypath_test.go— a Phase-0 risk probe that walks the ADR-0007 happy path through the generatedat.sovrn.*types, drives the implemented slice (Stalwart provisioning) from those records, and records a FINDING for every missing piece. Runs against the live harness;go test ./internal/integration/green.What it proves (CORE): - All four records (
tenant.detail,domain.detail,mail.account,mail.service) and the three XRPC inputs (tenant.create,domain.create,mail.createAccount) round-trip JSON with correct$typeidentity. - The record→Stalwart mapping is sufficient to provision:tenant.detail.name→ tenant,domain.detail.domain→ domain,mail.account.address→ account, plus an app password — the executable slice of the happy path.Findings it surfaces (gap inventory): - Data gaps (new, no existing bug covers them): 1.
at.sovrn.tenant.detailcarriesname+createdAtonly — tenant quotas/ tier (maxDomains, maxAccounts, diskQuota) have no protocol home. 2.domain.detail.dns(mx/spf/dkimSelector/dkimTxt) is declared but has no writer/reader; Stalwart auto-DKIM generates its own selector/key, which is not reflected back → desired-state vs applied-state divergence. 3. enum/knownValues (mode/status/dkimState) are plain strings in the generated Go — no validation; needs the reconciler validation layer (docs/03 §3). 4.mail.account.didhas no Stalwart equivalent (by design); the three-way diff can’t verify the DID↔mailbox correlation against Stalwart. - Component gaps (already tracked): OAuth/PDS (no PDS in harness), app-view XRPC server (b04aeb9), dnsprober gates (b5a31bd), spaceproj publisher (12ce549), reconciler (f15633a), mail.service endpoints (a472ea3/ac83db8). Step 8 (OIDC→JMAP) is already covered byTestIntegrationOIDCBearerAuth.Recommend filing the three “data gaps” against
f6192a1(lexicon schemas) or as new child issues; the component gaps are already in the tracker.DKIM correctness fix landed — manual, sovrn-generated RSA-2048, no rotation
Resolved the
domain.detail.dnsgap (ADR-0007 D19). The record is now populated synchronously with Stalwart-derived values — no async generation, no polling, no rotation.Changes: -
internal/domain: domains now setdkimManagement: {"@type":"Manual"}instead ofAutomatic; dropped theDKIMAutoprojection and the “patch toward Automatic” Ensure behavior (key lifecycle is sovrn’s, not Stalwart’s). -internal/dkim(new):Generatecreates an RSA-2048 key, installs the private half into Stalwart as aDkim1RsaSha256DkimSignature (selector="sovrn",privateKeyas SecretText), reads back the decoratedpublicKey, and returns the DNS TXT (v=DKIM1; k=rsa; h=sha256; p=…) atsovrn._domainkey.<domain>. The private key is never retained by sovrn. - Happy-path probe:dkim.Generate→ populatedomain.detail.dns.dkimSelector/dkimTxtis now a CORE assertion (was a FINDING). Verified live against Stalwart: key accepted,publicKeyread back, record round-trips.Decisions (per owner): RSA-2048 only (major receivers lack ed25519 — Gmail/ MS365/Yahoo/iCloud); fixed selector
sovrnfor easy copy/paste/verify; no v1 rotation; Stalwart sole holder of the private key (regenerate on migration).Still out of scope (noted, not fixed):
dns.mx/dns.spfare sovrn ingress config, not Stalwart-generated — populated from config separately.Verification:
go build ./...,go vet ./...,gofmt -l,go test ./...all green (unit + integration, incl. live Stalwart harness).Closing: Phase-0 risk probe served its purpose
The C6 probe’s goal was to surface unknowns and force assumptions into decisions, not to ship product code. It did: the data-placement question it flagged in week one produced ADR-0006 (space topology) and ADR-0007 (postmaster write-through projection), which re-scoped this bug twice. The remaining work is now concrete, filed Phase-1 issues rather than open questions.
Delivered in this bug’s orbit: -
at.sovrn.*lexicons + Go codegen (cmd/sovrn-lexgen→api/sovrn) —f6192a1/a472ea3schemas. -TestIntegrationHappyPathLexicon— the signup story expressed through the lexicon types, driving Stalwart from record data with a FINDING inventory. - Manual DKIM fix (ADR-0007 D19):internal/dkimgenerates RSA-2048, Stalwart holds the private key, the record carriessovrn._domainkey.<domain>— thedomain.detail.dnscorrectness bug is resolved and asserted live. - Real app-view XRPC server: rootrouter.go(NewApp(cfg)),internal/store(SQLite TID↔Stalwart-ID mapping), 7 handlers ininternal/appview,cmd/sovrnd.TestIntegrationAppViewXRPCEndToEnddrives all 7 endpoints over HTTP against live Stalwart — the earlier “XRPC endpoints not exercised” gap is closed. - Config surface tracked in82af326; ADR-0002 amended to mattn/go-sqlite3.What the probe surfaced → now tracked elsewhere (not this bug): -
12ce549(spaceproj),f15633a(reconciler),b04aeb9(app view UI),ee2256b(postmaster/PDS),b5a31bd(dnsprober),ac83db8(webmail),a472ea3(mail.service endpoints).Failure injections: the revised set (comment 09d75f4) targeted components that did not yet exist when those would run. They are superseded by the Phase-1 component test suites (spaceproj forged-record
authorDidtest, reconciler idempotent re-apply, dnsprober gate), not lost. Auth (OAuth) and DNS (dnsprober) remain stubbed in the app view until those bugs land — by design.Closing as the tracker is complete; the design decisions and follow-ups above carry the remaining work.