feat: implement signet unlink with web API - #321
Conversation
|
@Anambraboi-1 is attempting to deploy a commit to the blockchainmaxis-8449's projects Team on Vercel. A member of the Team first needs to authorize it. |
✅ Deploy Preview for stellar-signet ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
blockchain-maxis
left a comment
There was a problem hiding this comment.
Same call as #320, and for the same reasons — flagging it here so you don't have to infer it from that thread.
The command itself is right in the ways that matter for #261: it proves ownership by signing a challenge rather than trusting the caller's word, it fails closed when the signature doesn't verify, and the --yes / confirm() handling matches the acceptance criterion exactly. The unlink flow is sound.
What blocks it:
1. Architecture. This adds to apps/cli (TypeScript). #251 scaffolds the CLI as a Go module at cli/, and the rest of the series — #335, #369, #370, #358, #342 and others — is already building there. npx @signet/cli unlink still works under that plan; #293 covers cross-compiling the Go binary behind an npm wrapper.
2. getSecret() puts the deploy key's secret in process memory. #253 is explicit that the CLI should never hold key material — stellar tx sign --sign-with-key <identity> signs without the secret leaving stellar, and brings hardware-wallet support with it. This applies to unlink exactly as it does to link, since both sign a challenge.
One thing specific to this PR, worth carrying over to whichever implementation lands: /api/cli/challenge mints a challenge for any pubkey the caller names, and /api/cli/unlink accepts it back with a signature. That's the right shape, but the challenge needs to be bound to the operation and origin it was issued for — otherwise a challenge minted for one purpose can be replayed against another. #269 covers exactly this domain separation, and #343 is open against it.
Leaving this open. If you want to keep going, signet unlink inside cli/ on top of #335's scaffold is unclaimed and the series needs it.
blockchain-maxis#261: a link with no unlink is a one-way door. A rotated or compromised deploy key keeps feeding a profile, and the only way to stop it is the dashboard — which is no help to whoever holds the key but not the handle. `signet unlink` proves control of the deploy key by signing a SEP-10 challenge, exactly as `link` does, and `POST /api/cli/unlink` removes the binding. **Why this needs one proof where linking needs two.** `pair/complete` requires the handle owner's browser session *and* a signature, because attaching a wallet makes a claim about someone's profile. Detaching makes no claim: it withdraws one. The party controlling the key is the party whose attestation the profile was displaying, and letting them take it back is the point — requiring the handle owner's consent too would mean a developer who left a team could not stop their key feeding a profile they no longer control. The asymmetry is safe in the direction that matters: removing a wallet can only reduce what a profile claims, never attribute work to anybody. Refusals: an unsigned or wrongly-signed challenge (`bad-challenge`), a wallet no profile holds (`not-linked`), and the primary wallet — which is the handle→wallet claim itself, so releasing it is an on-chain registry operation rather than a row delete, the same refusal the dashboard gives. Verify → spend → delete, in that order, mirroring `completePairing`: consuming the challenge any earlier would let a bystander who merely saw it (it is not secret) burn it with a junk signature and lock the real caller out. A test pins that. **Shared challenge spending.** The review on this PR flagged that a challenge minted for one operation can be replayed against another (blockchain-maxis#269, tracked in blockchain-maxis#343). Full binding needs the challenge to carry its purpose and cannot be retrofitted here — but the cheaper version of that bug was about to be introduced: with pairing spending `pair:<hash>` and unlink spending `unlink:<hash>`, one intercepted envelope could be redeemed once against each. Both now share `sep10-challenge:<hash>`, so it is one use in total. The new module says explicitly that this is single-use, not operation binding, and points at blockchain-maxis#343. Confirms before acting unless `--yes`, naming the key rather than asking about "your wallet" — the resolved identity may not be the one the developer expected. Anything but an explicit y/yes is a no; a prompt that reads a stray newline as consent is not a confirmation, and a test covers that. The branch's TypeScript CLI under apps/cli is dropped, along with its getSecret() — signing goes through `stellar tx sign` (keys.SignChallenge), so the secret never enters this process.
011f6e5
into
blockchain-maxis:main
Fixes #261. This PR implements the signet unlink command via a web API call.