Repository navigation
feat: record a buddies inbox's successor after a key change - #76
Merged
Merged
Conversation
leviFrosty
force-pushed
the
inbox-successor
branch
from
October 31, 2026 00:15
c451aba to
102392a
Compare
leviFrosty
force-pushed
the
inbox-successor
branch
from
October 31, 2026 00:21
102392a to
f8d63a0
Compare
This was referenced Oct 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a relay-attested successor for a Buddies inbox after an account key change (witness-work ADR 0039 §5), so a buddy can check a
pair.reconnectthat anyone holding the old key could otherwise forge.Wire contract
account_inboxes.key_epochis the epoch of the key that verified the link's proof. It's read together with that key, so a key change landing in between leaves the link at the old epoch. A laterinbox/registerwhose proof the current key signed moves an existing link up to the current epoch (never down). Migration0008_inbox-successor.sqlbackfills each existing link with the epoch its account had when it was linked: one past the last key retired before the link'screated_at, else 0, and a link made within a second after a change counts as the old key's. That gives 0 for every account that never changed its key, and it never gives an old-key link the current epoch.inbox/delete{ inboxId, successor? }. Requests withoutsuccessorbehave as before. With it:forbidden.forbiddenotherwise), and the account must not be suspended (403suspended; deleting without a successor still works).unauthorizedotherwise). A key change ends every other session, but their access tokens still verify for minutes.successormust be another registered inbox (not deleted or wiped), linked to the same account at its current key epoch, with no successor of its own (no chains or cycles). Otherwise it's 409conflictand nothing is deleted.inbox/registerfor it is 409conflict, bearer or legacy, proof or not, while the record is kept. Repeats still work for the recording account.ok, including after the inbox is gone). A different successor is 409. A record that no longer qualifies (made before a key change) is replaced.buddies.deletedis logged only for the deletion itself.POST /buddies/v1/inbox/successor{ inboxId }→{ ok: true, successor: string | null }.unauthorized.disabled).nullunless the recorded successor is still linked to the same account at the account's currentkey_epoch, and the account isn't suspended. A successor a K1 holder recorded before the change is never vouched for after it.nullfor an inbox nobody replaced and for records over 90 days old.Why a thief can't pass
After the change, K1 opens no session (
key_retired), and its sessions have ended, so a leftover token gets 401. K1's link proofs no longer verify. An inbox K1 linked earlier stays at epoch 0, so naming it is 409, and a record K1 made before the change reads asnulluntil the User's own successor replaces it.What it doesn't cover
The guarantee holds only after the key change. Buddies who accept a K1 holder's reconnect before it aren't recovered. The old inbox must be linked to the account before the change (the app links it first and skips invites if it can't): another account holding the old key could link an unlinked old inbox and name its own successor.
Tests
src/buddies/successor.test.ts(31):not_linked) or throwing drops the record (readnull), and a retry records it; a suspended account gets 403 and its successor readsnulluntil reinstated;nullcases, auth, kill switch, 90-day prune;nullafter a rekey, then the User's own successor reads back.src/e2e/inboxSuccessor.e2e.test.ts(5) runs over HTTP againstscripts/verify/dev.mjs(local worker on :8796, migration applied by wrangler to local D1).pnpm test1973 passed,pnpm typecheckclean,pnpm fuzz:buddies --runs 300 --seed 5007ok after the black-box fixes; the black-box scripts (H, E) rerun against this branch show L1 and L2 refused.Docs: OpenAPI,
AGENTS.md,docs/buddies-protocol.md(new "Inbox successor" section), and the verify skill's tier list. No architecture diagram change (new endpoint in an existing route group, new table in the existing D1).