Member identity list and step-up-gated unlink; a link never moves an identity #14
Loading…
Reference in a new issue
No description provided.
Delete branch "heimdall/identity-unlink"
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?
What changed
GET /v1/me/identities(bearer of any app; ids, provider, providerUserId, username/displayName, linkedAt, scopes; never token columns, profile or email; no-store) andPOST /v1/me/identities/:identityId/unlink(bearer plus step-up proof; 401 sign_in_required, 403 step_up_required, 404 identity_not_found for a missing or another account's id, 409 last_identity, 200 with the remaining list). Audit eventidentity_unlinked(provider, identityId only). When the account then holds no identity of that provider, revocation receipts are written for its live grants of that provider, through onerevokeLiveGrantshelper now shared with the grants revoke route.step_up_at(epoch seconds), set only when the callback's signed state hasstep_upand modesign_in; a refresh never carries it.STEP_UP_PROOF_TTL_SECONDS = 600insrc/claims.ts. One bearer verifier:resolveBearerreturns{accountId, claims};resolveLinkAccountandresolveAccessAccountare thin wrappers.upsertLinkedIdentityno longer moves an identity: the conflict arm dropsaccount_id = EXCLUDED.account_idand gainsWHERE linked_identities.account_id = EXCLUDED.account_id; another account's identity throwsIdentityHeldElsewhereError(memory store the same). links/confirm maps it to 409identity_linked_elsewhere; the callback maps it tolink_initiator_mismatch.unlinkLinkedIdentityis the only delete: one transaction locks the account row, refuses the last identity, deletesWHERE id AND account_id.STORE_TABLE_PRIVILEGES.linked_identitiesgains DELETE.Deploy: grant before start
A build of this branch needs, run as the owner role BEFORE it starts:
Without it startup refuses with
SCHEMA_PRIVILEGES. The grant is a root-list item at deploy, in the shape of root-list-heimdall-oauth-state-grant-20261113.md. Nothing is deployed or granted by this PR.Tests
npx vitest run(no Postgres): 682 passed, 202 skipped.bash scripts/test-live-pg.sh(throwaway cluster, 127.0.0.1:54329): 868 passed, 16 skipped (the skips are the two pg-mem cases that need real Postgres, plus environment-gated files). Includes concurrent unlink of the last two identities from two pools over 10 rounds, the upsert guard on real Postgres, and the startup refusal for a role without DELETE on linked_identities.>=mutant fails), a one-identity bearer naming a foreign id gets 404 not 409, and a forged signed link-mode state withstep_upyields nostep_up_at(dropping themode === "sign_in"guard fails it).account_id = EXCLUDED.account_idarm fails the live-PG test.tests/identity-unlink.test.ts(HTTP, real step-up sign-ins and refresh); additions to store, link-binding, google-link and step-up tests.pnpm buildcould not run in the sandbox (pnpm tool dir not writable); the same script,tsc -p tsconfig.json, passes.Known limits
Ledger: one line appended to
state/evidence.jsonl(the repo has no CHANGELOG), as the step-up change did.🤖 Generated with Claude Code