Read-only ActivityPub → atproto bridge for the threadiverse using Coves lexicons

fix(optout): a withdrawal that survives its own replay, and stays withdrawn (17d review) master

The destructive tier worked on the happy path and failed in three ways that only appear once something goes wrong. All three came out of review. THE PURGE WAS NOT REPLAY-SAFE, and the cancel-before-purge ordering adopted after the deadlock is what broke it. The purge commits on its own transaction and the gate commits later, so a failure between them replays the record. On replay the sweeping cancel ran FIRST and cancelled the Delete{Person} fan-out and the vote Undos the previous purge had already committed; EnqueueTx then refused to revive them by design, and the enumeration returned nothing because those votes were already marked undone. Net result: actor tombstoned, prefs recorded, ZERO activities ever delivered, logged as "destructive opt-out applied". The same missing predicate over-reached in the soft tier, where opting out cancelled a user's own pending self-delete and left the post standing on Lemmy forever. The cancel is now two statements named for two different decisions. CancelForActor stays sweeping for the kill switch and the operator's manual cancel, which mean "stop the queue". CancelOutwardForActorTx is consent: it cancels what PUBLISHES and lets store.RetractionKinds go out. worker.isRetraction reads that same list, so the queue and the worker cannot drift about what a stopped user is still owed. That predicate also dissolves the ordering constraint rather than relocating it — verified the hard way, since biting it does not fail the replay test, it DEADLOCKS it, which is the deadlock the ordering existed to avoid. A VOTE HELD FOR SETTLEMENT WAS INVISIBLE TO THE ERASURE. A hold is a delivery the peer ACCEPTED — only our bookkeeping is outstanding — but the enumeration read delivered_state alone, so the purge owed nothing while the peer held the Like. ListStandingForActor now reads delivered UNION held-for-settlement. AND UNDONE WAS NOT ACTUALLY TERMINAL. The enumeration fix alone is undone by the very settlement it races: the held delivery resumes after the withdrawal and writes 'delivered' over the retraction, so 17b's reseed keeps subtracting a vote from a served score on behalf of somebody who no longer exists. The guard now lives in SetDeliveredState, and its two zero-row cases are kept apart deliberately — an already-retracted row is a DECIDED no-op and reports success, because an error there holds the delivery forever retrying a write that can never apply, while a genuinely missing row still reports NotFound. The 410 was observable before the withdrawal was delivered: a peer that re-dereferenced the actor to verify the signature on the Delete got Gone for the message announcing that deletion. Both actor routes and the outbox now answer with an AS2 Tombstone carrying formerType, deleted, and the public key — verification material, none of the profile. Keeping the full document served until every delivery is terminal was considered and rejected: it publishes an erased user's document for as long as any peer is down, which is a worse failure for an erasure tier than the one it fixes. The residual is in FOLLOWUPS. Also: a tombstoned actor can no longer be re-enabled (forced in the statement, so no caller gets a spurious NotFound) and a purged preference cannot be deleted; federation_prefs.purged_at separates requested from committed and is stamped inside the purge's own transaction; the confirmation resolver requires https, refuses userinfo/query/fragment, matches the #atproto_pds fragment exactly, and pins every redirect hop to the named authority, with every violation an error rather than a verdict; one unresolvable community no longer holds an entire erasure hostage; and the purge resolves every inbox BEFORE opening its transaction, which is why the ingest package took 332s and failed while each test passed alone. Tests: whole-package -count=2 across store/outbound/consume/ingest, full suite 21/21, e2e 284s. Every guard tooth-checked, including one bite of mine that was itself invalid (an untyped parameter made Postgres fail type inference and reddened all five tests for a reason that had nothing to do with the guard). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>