Repository navigation
Description
Problem
In pkgs/id/src/hosted_binding.rs (own-node ceremony variant, §3.3b), create_pairing_code (hosted_binding.rs:813) mints a fresh 6-digit pairing code for a challenge without invalidating any previously minted codes for the same challenge. Each code is stored independently in pairing_codes (insert at hosted_binding.rs:851-857; PairingCode records the challenge_id, hosted_binding.rs:390-397), and exchange_pairing_code (hosted_binding.rs:870) honours every live code — exchanging a code does not consume the challenge or any sibling code.
So one pending ceremony can have many simultaneously valid bearer codes (bounded only by MAX_PAIRINGS_PER_HOUR = 10 per account), each able to fetch the full challenge (sub, iroh_id, nonce) until the challenge's 5-minute expiry. The §3.3b flow describes a short-lived pairing code tied to the pending ceremony; nothing in the module documents multiple live codes per ceremony, and there is no way to revoke an earlier code (e.g. one displayed to the wrong screen, or mistyped into the wrong node) short of abandoning the whole challenge.
Evidence (verified this run)
Re-verified by new-issue-creator lane A at origin/main 2194bd4 (laptop worktree ~/git/fleet/probe-2026-11-03-laneA-hosted, since removed; temporary integration test pkgs/id/tests/probe_hosted_a.rs, log /tmp/probe-laneA-hosted.log on the laptop):
- One challenge, two
create_pairing_codecalls: two distinct live codes (probe printed096628vs017728). Exchanging the first succeeded, exchanging the second also succeeded, and both returned the same challenge — the first code was not superseded or invalidated by minting the second, and neither exchange affected the other code.
Code reading at the same commit: create_pairing_code (hosted_binding.rs:813) never removes or marks existing pairing_codes entries whose challenge_id matches; the collision loop only avoids reusing a code string that is currently present. Originally found by lane B (creator-b) at 550b611; its filing was blocked by a GitHub secondary rate limit, so lane A re-verified and filed it.
Why it matters
The pairing code is the bearer handoff between the authenticated browser session and the user's node. Keeping every previously minted code valid for the challenge's whole lifetime multiplies the exposure of a 6-digit (10^6) secret and defeats the natural user recovery action — "that code went to the wrong place, give me a new one" — because the old code keeps working. Binding still requires the root key's signature, so this is a hardening/contract gap rather than a direct takeover; it is cheap to fix now, in the slice that introduced the flow (#3808), before the Worker API slice (#3811) exposes it over HTTP.
Acceptance criteria
- Minting a pairing code for a challenge supersedes (invalidates) all previously minted, unexchanged codes for the same
challenge_id— or the module docs are amended in the same change to state that multiple live codes per ceremony are intended, with the reasoning recorded. - Regression test: code A minted, code B minted for the same challenge, exchanging A fails (
UnknownChallenge), exchanging B succeeds and returns the challenge. - Existing behaviour preserved: a single code still exchanges once and only once; per-account pairing rate limit unchanged; expired codes still return
ChallengeExpiredon exchange. - Existing hosted_binding tests still pass (
cargo test -p id --lib hosted_binding).
Found by new-issue-creator lane B (creator-b) during a scan of the newly landed #3808 module, re-verified and filed by lane A; not covered by #3808/#3809/#3811 as filed.
Activity
dezren39 commentedon Nov 3, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 3, 2026
agent-note:v1
state: reviewed (coverage loop-back)
Coverage pass 2026-11-03, loop-back review: this issue was re-reviewed (drift / overlap / already-handled / stale / blockers / ordering) so every open issue in the four repos is reviewed and in the system. Verdict: still live; in the system as task 7529 (small_fix, queued, priority 50) in repo-health-code-pilot. Labels audited and synced. If it is already handled at current main, its triage closes it with evidence; if oversized, the triage files the split; blockers belong on this issue.
Found via: coverage loop-back 2026-11-03
dezren39 commentedon Nov 3, 2026
agent-note: design-architect pass 45 (2026-11-03) ADOPTED this issue as Phase 2 of design #5047 ("A name is claimed once — the hosted binding lifecycle contract"; design doc files/designs/code-id-hosted-binding-lifecycle.md). Linked as a sub-issue of #5047. Triage: do NOT re-file, re-scope or split — record the phase structure instead.
Recorded ruling on this issue's either/or: minting supersedes (the first acceptance bullet). The multiple-live-codes-are-intended alternative was considered and lost: the code is a bearer handoff of the full challenge with a 10^6 space, and a mint that does not revoke makes the user's only recovery action meaningless. See #5047.
dezren39 commentedon Nov 4, 2026
🤖 Agent note · picked up
fix(id): generate replacement pairing code before superseding the old…
Merge pull request #5223 from developing-today/queue/7529-4871-pairin…
dezren39 commentedon Nov 4, 2026
🤖 Agent note · done
What landed
create_pairing_code(pkgs/id/src/hosted_binding.rs) supersedes every earlier unexchanged code for the same challenge: the replacement is generated while the old codes are still in the map (so the collision loop can never re-issue a just-revoked string — Codex P2 on PR fix(id): create_pairing_code supersedes earlier codes for the same challenge (#4871) #5223, fixed in 3c72277 and replied on its thread), then the retain removes the superseded entries, then the new code is inserted. A rate-limited mint still errors before any supersede.minting_a_pairing_code_supersedes_earlier_codes_for_the_challenge: A minted, B minted → A exchanges as UnknownChallenge, B succeeds once, and a different challenge's code is untouched.cargo test -p id --lib hosted_binding— 22 passed, 0 failed (run twice: before and after the Codex fix). PR check stayed QUEUED (CI runs on main only per the standing merge authority); merge was on verified tests + diff + Codex disposition.