Repository navigation
fix(server): never move a linked companion to another account - #750
Merged
Merged
Conversation
LinkCompanion upserted with ON CONFLICT(pubkey) DO UPDATE SET user_id, so linking a companion that another account had already linked moved it to the new account, and the original owner lost it without notice. The insert now does nothing on conflict, and LinkCompanion returns ErrCompanionTaken when the pubkey belongs to someone else. link-companion answers 409 companion_taken. register checks before it creates the account, so a refused registration leaves no account behind. Re-linking your own companion stays idempotent. Instead of a connection or username error, the app now says the companion is already linked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
efiten
force-pushed
the
fix/companion-link-no-reassign
branch
from
October 23, 2026 09:35
c9f9dff to
fe4c35b
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
efiten
added a commit
that referenced
this pull request
Oct 23, 2026
… (#754) Closes #753 AGENTS.md said a PR with a release note touches only `changelog.d/` and that CI on master rebuilds both `changelog.json` copies. `web/parity.test.js` runs on every PR and fails until both copies match `changelog.d/`, so in practice a PR regenerates and commits them (#745, #738, #750). - AGENTS.md: a PR with an entry runs `node scripts/build-changelog.mjs` and commits both copies. Two such PRs, or a PR stacked on a branch with an entry, conflict on the copies. Resolve that by running the generator on the merged tree. - `.github/workflows/changelog.yml`: the header comment now matches this and calls the master job a safety net. Docs and a comment only. The test, the generator and the workflow steps are unchanged. No release note, since nothing changes for users. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Merged
efiten
added a commit
that referenced
this pull request
Oct 23, 2026
Closes #751 Stacked on #750 (base `fix/companion-link-no-reassign`). Merge #750 first, then retarget this PR to `master`. Register and link-companion now need proof that the caller holds the companion, not only its public pubkey. - `POST /api/auth/companion-challenge {companion_pubkey}` returns a challenge. It is stateless: expiry, a 16-byte nonce and an HMAC-SHA256 over pubkey, expiry and nonce. The key is random per process. The challenge is bound to the pubkey and valid for 5 minutes, and a restart voids open challenges. - Register and link-companion take `challenge` and `signature`, an Ed25519 signature over `core-hunter-link:` + challenge. Without a valid proof they answer `403 companion_proof_invalid`. In register the proof check runs before the companion-taken check, so a caller without the key cannot learn whether a pubkey is linked. - The app gets the challenge and has the connected radio sign it through `createSigner`, the same CMD_SIGN path the broker tokens use. Firmware `CMD_SIGN_FINISH` signs with `self_id` and asks for no confirmation on the radio (`examples/companion_radio/MyMesh.cpp:1752`). With no radio connected, the app shows a message and sends no request. Tests: - `internal/auth/companionproof_test.go`: valid proof accepted; refused for another key, a challenge for another pubkey, an expired challenge, another server's key, a tampered challenge, a signature without the prefix, non-hex input and a short pubkey. Mutation-checked: dropping the HMAC, expiry or prefix check each turns a case red. - `internal/httpapi/auth_test.go`: `TestCompanionLinkRequiresProof` (no proof, or a forged one, gives 403 on both endpoints, and register leaves no account behind) and `TestCompanionChallenge`. Existing register/link tests now send a real proof. Disabling either handler check turns the test red. - `app/src/__tests__/auth.test.js`: `companionProof` signs the prefixed challenge, and returns null without a challenge or without a signature. The body builders carry the proof. `go vet ./...` and `go test ./...` pass in `server/`. `npx vitest run` passes in `app/` (1748), and `vite build` succeeds. Not done: - Not tested on a real radio. Only dummy Ed25519 keys were used, and whether the firmware signature verifies under Go's `ed25519.Verify` was not run end to end. The firmware uses the standard ref10 `ed25519_sign` (`lib/ed25519/sign.c`), and the broker tokens already depend on the same signatures. - An older cached app version cannot register or link until it updates, because the server now requires the proof. - Companions that are already linked are not re-checked. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
efiten
pushed a commit
that referenced
this pull request
Oct 24, 2026
🤖 I have created a release *beep* *boop* --- <details><summary>app: 1.32.0</summary> ## [1.32.0](app-v1.31.0...app-v1.32.0) (2026-10-23) ### Features * **app:** publish the radio settings with every reception ([#745](#745)) ([8683703](8683703)) * **auth:** prove possession of a companion before linking it ([#752](#752)) ([5d039cd](5d039cd)) ### Bug Fixes * **server:** never move a linked companion to another account ([#750](#750)) ([a225b96](a225b96)) </details> <details><summary>server: 1.11.0</summary> ## [1.11.0](server-v1.10.1...server-v1.11.0) (2026-10-23) ### Features * **auth:** prove possession of a companion before linking it ([#752](#752)) ([5d039cd](5d039cd)) ### Bug Fixes * **server:** never move a linked companion to another account ([#750](#750)) ([a225b96](a225b96)) ### Documentation * **server:** say the positions member gate is not a confidentiality boundary ([#749](#749)) ([72c9224](72c9224)) </details> <details><summary>web: 1.28.0</summary> ## [1.28.0](web-v1.27.0...web-v1.28.0) (2026-10-23) ### Features * **app:** publish the radio settings with every reception ([#745](#745)) ([8683703](8683703)) * **auth:** prove possession of a companion before linking it ([#752](#752)) ([5d039cd](5d039cd)) ### Bug Fixes * **server:** never move a linked companion to another account ([#750](#750)) ([a225b96](a225b96)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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.
Closes #748
Linking a companion that is already linked to another account no longer moves it to the new account.
store.LinkCompanioninserts withON CONFLICT(pubkey) DO NOTHING. It returnsErrCompanionTakenwhen the pubkey belongs to another user. Re-linking your own companion is still a no-op.link-companionanswers409 companion_taken.registerchecks before it creates the account and answers409 companion_taken. No account is left behind.changelog.d/.Tests:
TestLinkCompanionDoesNotReassign(store),TestLinkCompanionTakenByAnotherUserandTestRegisterWithTakenCompanion(httpapi). Each was mutation-checked: putting back the old upsert, or removing the register check, turns the matching test red.go vet ./...andgo test ./...pass inserver/. Inapp/,npx vitest runpasses (1743) andvite buildsucceeds. The app messages were not smoke-tested on a device.