GO-7395 Approve local-link pairing before minting the code - #3225
Open
requilence wants to merge 1 commit into
Open
GO-7395 Approve local-link pairing before minting the code#3225requilence wants to merge 1 commit into
requilence wants to merge 1 commit into
Conversation
Local-link pairing minted the 4-digit code the moment a challenge was requested and broadcast it to every session, so an unapproved request always had a secret to guess and the per-run counters were the only DoS defence. Rework the flow to approve-then-mint: - StartNewChallenge registers a pending request and mints nothing. The new LinkApprovalRequest event names the caller (process path, browser origin) and carries no code. - AccountLocalLinkApproveChallenge is the user's decision. It is the only place a code is minted and returns it to the approving session alone, never on the event bus. Addressed by (processPath, origin) — the caller the prompt displayed; one pending prompt per caller keeps it unambiguous. - SolveChallenge gates on approved state; a solve against a pending challenge does not count against the failure budget. - Denials are remembered for the app run; pending prompts expire after 60s, approved codes 5 min after approval (SweepExpired). The requesting client's API (/v1/auth/challenges, /v1/auth/api_keys) is unchanged; only the desktop client gains the approval step. Headless deployments use AccountLocalLinkCreateApp, which mints a key with no challenge, so no auto-approve backdoor is added. Security: AccountLocalLinkApproveChallenge stays out of noAuthMethods and limitedScopeMethods so authorization admits full scope alone, and it rejects any caller carrying an Origin header. Both pinned by tests. Incorporates the origin-attribution groundwork it depends on: ClientInfo.origin from the browser's Origin header, process resolution for JSON API callers, and per-caller challenge budgets. Docs: docs/LocalLinkPairingApproval.md (design), docs/LocalLinkApprovalDesktopGuide.md (client integration).
requilence
added a commit
that referenced
this pull request
Sep 10, 2026
Merges the approve-then-mint pairing flow (branch go-7395-link-approval, PR #3225) into the API-key grant work on this branch, and adds the consent surface the grant was missing: the approval prompt collects a space grant rather than a yes/no. Records the decisions: requestedGrant on the challenge path becomes requestedPerm (an external app cannot know a space id, and the field is unusable today because ValidateAppLinkGrant rejects an empty space list); ApproveChallenge carries the user's grant and heart persists it verbatim; allSpaces is a dynamic flag covering spaces created later, never spelled as an empty list; the tech space is excluded from it; the grant schema version bumps so an older reader fails closed with a diagnosable error.
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.
Problem
Local-link pairing minted the 4-digit code the moment a challenge was requested and broadcast it to every session. So an unapproved request always had a secret to guess (~0.2% per run against 10⁴), and the per-run counters were the only DoS defence — exhausting them locked pairing for every client until restart.
Change: approve-then-mint
StartNewChallengeregisters a pending request and mints nothing. The newLinkApprovalRequestevent names the caller (process path, browser origin) and carries no code.AccountLocalLinkApproveChallengeis the user's decision — the only place a code is minted, returned to the approving session alone, never on the event bus. Addressed by(processPath, origin)— the caller the prompt displayed; one pending prompt per caller keeps it unambiguous.SolveChallengegates on approved state; a solve against a pending challenge does not burn the failure budget (else a caller could lock pairing for everyone by solving its own unapproved challenge).SweepExpired).An unapproved request has no code associated with it, so there is nothing to brute-force before a human has named and accepted the caller.
Proto
LinkChallenge/LinkChallengeHideare replaced byLinkApprovalRequest/LinkApprovalHide— clean fields, no code, noneedApprove(the event's arrival is the signal). A distinct name stops a client subscribing to the old code-carrying shape by accident. Field numbers reused (events aren't persisted).Security
AccountLocalLinkApproveChallengestays out ofnoAuthMethodsandlimitedScopeMethods, so authorization admits full scope alone (the desktop UI). It also rejects any caller carrying anOriginheader. Both pinned by tests, so a future edit fails the build rather than the threat model.Compatibility
/v1/auth/challenges,/v1/auth/api_keys) is unchanged — no SDK or extension needs updating.AccountLocalLinkCreateApp, which mints a key with no challenge, so no auto-approve backdoor is added.AccountLocalLinkApproveChallenge. heart and an approval-capable client must ship together.Docs
docs/LocalLinkPairingApproval.md— design/specdocs/LocalLinkApprovalDesktopGuide.md— client integration contractTests
New coverage: brute-force is impossible (all 10⁴ answers rejected while pending), approve mints/returns the code, deny is remembered, one-prompt-per-caller, no id sharing, TTL expiry (injected clock), auth-map regression, browser-origin rejection, per-caller vs run budgets.
go build ./...clean;core/session,core/application,core,core/api/server,util/localoriginall pass.Linear: GO-7395