The mover's inline no-redirect handler becomes the shared extension; the
app-secret auth client and the member-bearer client use it too.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
AddHeimdallClients gathers the three Heimdall client registrations so the test
exercises the registrations Program uses. The auth and member clients still
follow redirects here; the mover already does not.
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>
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>
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>
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>
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>
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>
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>
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>
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>
Delta Soul clean at 73f97ed (no blocking findings; one Low: challenge check and spend not atomic).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With no current list the proof goes only through a provider the member names, never the
session's sign-in provider: a sign-in through an unlinked provider makes Heimdall mint an
orphan account holding that login. Link buttons need a current list. The unlink completion
must say a step-up was requested. POST /auth/heimdall/complete answers no-store. The fake
Heimdall now models orphan minting, derives step_up_at from the start's stepUp flag, and
enforces the app secret, sign_in-only, unsupported-provider and 600 s rules.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A member sees their Heimdall logins, links Discord, Patreon or Google, and unlinks one
with a fresh Google step-up sign-in. Bifrost keeps no Heimdall token: each action is a
fresh Heimdall sign-in started by Bifrost whose bearer lives inside the completion
request. The list is kept five minutes in this process as non-secret fields only.
The Status page's Google-only link button and handler are cut; the Linked accounts page
owns link starts. HeimdallStartRequest gains stepUp. Heimdall's member routes use a
client without the app secret.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Docs only. Mouth line numbers (shifted by about 100 lines of mouth code), bridge.md, packages.lock.json refs refreshed; Heimdall PR #6 is merged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Docs only. Refreshes stale file and line references, drops the claim that OIDC must move to Status == Active (main already admits any non-Suspended account with a live training-notice receipt), names the new IsActiveMember callers, and updates the provider-list and Heimdall PR #6 notes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A missing secret silently switched Discord and Patreon member sign-in off.
Outside Development and Testing the validator now fails naming
Heimdall:AppSecretFile and Heimdall:AppSecret, never a value.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The await step passed replaceSlot to StartAsync but named the slot from its own cookieName, so replaceSlot decided nothing. The step now returns the slot the started attempt chose, one owner.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The account page's POST starts an ordinary sign-in whose attempt (a purpose, the expected
account id, a pending link id; never a token) is only the proof for a link. Each Heimdall
call that needs a bearer follows its own fresh sign-in; the bearer lives in that request.
Google stays out of AllowedProviders and member access. Authorization is redacted from
HttpClient logging. Outcomes redirect to a fixed closed set of wording.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>