Identity moves: sanctioned mover (auth_at claims, identity_moves, POST /v1/apps/{app}/identity-moves) #16
Loading…
Reference in a new issue
No description provided.
Delete branch "heimdall/identity-moves"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Executes cut_spec
heimdall-body:cut_spec:cut-identity-moves.r1with rulingmove-proof-strict-passkey.What moves
POST /v1/apps/{app}/identity-moves: the only sanctioned way a login changes account. Bifrost-only (profile grantidentityMoves: { accountProofMethods: ["bifrost-passkey"] }). Accounts come only from the proofs (two bearers), never from the body. IdempotentmoveId; reversible once (reversesMoveId).bifrost-passkeycovers a whole account;bifrost-email-codedoes not.moveIdentities(Postgres and in-memory) is now the only writer oflinked_identities.account_id. One transaction: locks both accounts in id order, answers a replayedmoveIdfrom its row, re-checks proof sessions, strips moved credentials, revokes the source's sessions, expires its pending links and unredeemed completions, appends anidentity_movesrow. Revocation receipts for the source's grants and theidentity_movedaudit are written after commit; a replay heals missing receipts.auth_atandauth_identity_id, set only by asign_incallback.proofTier/proofCoversinclaims.ts; the unlink route's inline step-up window test is deleted forproofTier(...) === "strong"(same 600 s rule).decideMove(newsrc/store/identity-move.ts) holds the move rules; both stores gather facts and apply its answer.Owner action BEFORE the new build starts (DDL + grant)
Startup refuses with SCHEMA_MISSING / SCHEMA_PRIVILEGES otherwise. Apply the
identity_movestable, itsidentity_moves_reverses_unique_idxandidentity_moves_from_idxfromsrc/store/schema.ts(append-only block afterconsent_receipts_account_idx), then:No other grant changes (linked_identities, sessions, pending_links, auth_completions already hold UPDATE).
Idempotency ruling
Idempotency is by moveId (steward ruling): a replay with the same moveId and different identityIds returns the first receipt and moves nothing; Bifrost retries with the same body. Pinned by a test that documents it. A replay under another reversesMoveId, source, destination or app is 409 move_id_conflict.
Not done / limits
proofCoverstakesnowS(the spec signature omitted it though its rule needs the tier), and the route is registered after the unlink route, whererevokeLiveGrantsis in scope, not beside step-up.Tests
bash scripts/test-live-pg.sh(real scratch Postgres 16): 925 passed, 17 skipped, 34 files (at2f1b733). New:tests/identity-moves.test.ts(23, HTTP on InMemoryStore), store contract on memory and live PG, opposite concurrent moves over two pools x10 rounds, identical concurrent moveIds, startup refusing a role without INSERT or SELECT onidentity_moves.tsc --noEmitclean.Structural delta: +1531 / -9 over 15 files; one new source file, one new table, no dependencies, no targets.
🤖 Generated with Claude Code
Soul s1 follow-up at
a591c71: tests now kill M01 (lock order moved into code, sorted), M09, M11, M14, M17, M18, M19/M19b, M21, M23, M28, M29, M31 (each re-run by hand); resolveAccount deleted (no caller; wrong after chains). Suite 923 passed, 17 skipped on scratch PG 16; tsc clean. Open: a replay with changed identityIds under a used moveId still answers the first receipt.🤖 Generated with Claude Code