Account merge: ledger, Heimdall mover client, crash-safe saga (merge design cuts A+B+saga) #86

Merged
heimdall merged 21 commits from heimdall/account-merge into main 2026-11-14 09:42:49 +00:00
Member

Ledger (suspension standing, atomic commit, unique index vs overlapping merges, sweeper), Heimdall identity-move client and fake, and the Confirm/Move/Commit/Undo saga with stable moveId. Soul: cut-merge-saga.s1 (not clean) then cut-merge-saga.s2 clean at 757ca2e. Not deployed; deploy list section 10. Open Lows: cut-merge-saga.s2.digest-pair-binding-unpinned, cut-merge-mover-client.s2.auth-clients-follow-redirects. Member notices and pages are later cuts.

🤖 Generated with Claude Code

Ledger (suspension standing, atomic commit, unique index vs overlapping merges, sweeper), Heimdall identity-move client and fake, and the Confirm/Move/Commit/Undo saga with stable moveId. Soul: cut-merge-saga.s1 (not clean) then cut-merge-saga.s2 clean at 757ca2e. Not deployed; deploy list section 10. Open Lows: cut-merge-saga.s2.digest-pair-binding-unpinned, cut-merge-mover-client.s2.auth-clients-follow-redirects. Member notices and pages are later cuts. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
AccountMergeLedger owns what a merge does to Bifrost records: PlanAsync (dry run, block codes),
CommitAsync (one save, total, idempotent), UndoLocalAsync (journal replay), FinalizeDueAsync
(AccountMergeSweeper) and StandingAsync/ResolveAsync. Votes and concept appraisals are voided by a
stamp that a model query filter hides from every tally. The actor accessor no longer ORs several
credentials under SingleOrDefault (a latent crash); a frozen or folded account resolves to no
account on every route, the OIDC authorize and chat refusal deny it, and patron intake follows the
alias. Migration AddAccountMerges is additive. No UI, no Heimdall call, nothing deployed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
heimdall changed title from WIP: Account merge ledger (stacked merge saga, cut 1 of N; final PR opened later) to Account merge: ledger, Heimdall mover client, crash-safe saga (merge design cuts A+B+saga) 2026-11-14 09:42:49 +00:00
POST /v1/apps/bifrost/identity-moves under the app secret (HeimdallAuthService.ConfigureClient,
same header redaction), 15 s timeout, request validated to the contract's closed body before any
I/O, 200 trusted only as a whole receipt answering this request. HeimdallRefusal gains ReadAsync
(code plus safe proof side and provider); CodeAsync delegates to it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
FakeHeimdall.Moves owns per-account logins, a bearer registry, revoked sessions and stored moves,
and answers the identity-moves route in decideMove's order (4fe367a): replay before proofs,
strict passkey coverage, freshness windows, first-receipt replay. Fault switches for status,
apply-then-fail and down.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
400 for every body the route schema rejects (oneOf toProof/reversesMoveId, duplicate or empty
identityIds, proof bounds), 404 identity_not_found for a source holding nothing, coverage-all-null
403 before the store, store order not_reversible/already_moved/not_found/not_proved/provider_conflict,
replay by signature without liveness (409 move_id_conflict for unsigned proofs), future-dated proofs
stale. Soul's P1-P4 are committed as FakeHeimdallMoverTests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A 3xx is Unknown and never carries the secret or proofs off origin (registration owns the handler);
the HttpClient timeout now bounds the body read and any IOException or stall is Unknown; receipts
over 64 KiB are Unknown; the request's ToString hides its proofs; move_id_conflict is Unknown because
a committed move answers it when its proofs no longer verify, which keeps the sealed proofs resumable;
identity ids need not be GUIDs, proofs are bounded to 8192 and identityIds unique.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An uncoded 4xx (gateway 404, 408, 410, unknown code) is Unknown: Refused wipes sealed proofs. Unrecognized leaves the refusal enum.
The catch lists no types: only the caller's cancellation propagates. moveId and reversesMoveId patterns use \z in client and fake,
so a trailing line feed is refused as Heimdall's ajv refuses it; 64, 65 and newline are pinned.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An nginx HTML 400, an empty 400 or any other 400 is Unknown. The fake answers its schema 400 with Heimdall's error handler body.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
WriteAsAsync takes a shared Postgres advisory lock on the account (the key the chain trigger locks exclusively) across the standing read and the writer's save; CommitAsync takes the exclusive lock before it reduces. Votes, appraisals, sealed commit and reveal, patron events (RecordSupportEventAsync routes and journals in the same save, so the manual Ledger door obeys it too) and the Heimdall sign-in pass through it; SupportTargetAsync, MayWriteAsync and JournalIntakeAsync are deleted. A folded or frozen account's sign-in writes no membership. A retry of an event stranded on the folded account journals nothing. Tests: Down refuses in every merge state, Stripe journal prior owner and undo, the lock racing a freeze and a commit on live Postgres. Docs say what the lock guarantees and what it does not.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
AccountMergeSaga is the only caller of the move client and the only writer of Confirmed, HeimdallMoved, Failed, UndoRequested and HeimdallReversed; the
ledger keeps Committed, Final and Undone. Migration AddAccountMergeSaga adds the saga columns and the two move-id keys; its Down is roll-forward only.
AccountMergeSweeper becomes the one loop: saga advance, then finalize, every 30 s.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/Bifrost!86
No description provided.