Repository navigation
fix(vscode): pair the extension so inbox calls carry a bearer token - #1290
Merged
github-actions[bot] merged 4 commits intoOct 16, 2026
Merged
github-actions[bot] merged 4 commits into
github-actions[bot] merged 4 commits into
Conversation
Every /api/sync/inbox call from the extension went out without an Authorization header and got 401. The extension now redeems a desktop-issued pairing code, keeps the credential in SecretStorage, exchanges it for a short-lived device token and sends it on every inbox call; a rejected credential offers re-pairing. Adds the extension's first test harness (npm test) and runs it in CI, and shows the pairing step in the building-block and runtime views. Decision taken without asking: the full pairing flow (option 1) over parking the extension; the alternative is named in the pull request. Refs #718 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeQL flagged the stub fetch's url.startsWith(baseUrl) as incomplete URL substring sanitization: it would also accept a host such as sync.test.example.com. Compare the parsed origin instead. Refs #718 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…2026-10-15/718-vscode-sync-401
…2026-10-15/718-vscode-sync-401
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 #718
Summary
The VS Code extension now joins the desktop's owner the way a second device does, so its inbox calls stop getting 401. A new
vscode-free module,src/sync-client.ts, redeems a desktop-issued pairing code atPOST /api/sync/devices/pair, keeps{deviceId, credential}in VS CodeSecretStorage, trades the credential at/devices/tokenfor a short-lived bearer token (cached, renewed 2 minutes early), and sendsAuthorization: Beareron every/api/sync/inboxcall. An inbox 401 retries once with a fresh token; a token-endpoint 401 forgets the credential and offers re-pairing. Two commands: Backlog: Pair with Desktop and Backlog: Forget Pairing. The extension gets its first test harness (npm test:tscthennode --test, 13 tests), run in thevscode-extensionCI job. The building-block view, runtime view andtech/ide.mdshow the pairing step. No server change: every route already existed.Decision taken without asking (follow it, or ask for the alternative)
Question. Should the VS Code extension get the full device-pairing flow (option 1), or be parked with a not-yet-paired message and lose the 'editor extension' claim from the endpoint docs (option 2)?
Chosen. Option 1, implemented as pairing by code redemption. The extension redeems a desktop-issued pairing code through POST /api/sync/devices/pair (not /devices/register, which would mint a separate empty owner). It keeps deviceId and credential in VS Code SecretStorage, exchanges them at /devices/token for a short-lived bearer token, sends it on every inbox call, and on a 401 retries once with a fresh token, then prompts to re-pair when the credential is rejected. The logic sits in a vscode-free syncClient.ts module covered by node:test.
Alternative. Option 2: change refreshInbox and capture so they report 'Backlog for VS Code is not yet paired-device aware' and make no network call, remove 'the editor extension' from the InboxEndpoints.cs summary (and from the InboxContracts.cs and InboxCaptureEndpointTests.cs comments that repeat it), mark the extension as parked in the building-block and runtime views and tech/ide.md, and record the parked channel as a TDR under .devbook/arc42/tdr/, because it knowingly leaves an ADR-0005 channel unserved.
Why the choice. Local ADR 0005 (Scope, and the deferred-transport section) names 'an IDE extension on a second machine' as one of the two channels the sync service exists to serve. Its Identity section prescribes exactly this flow: pair once by short code, keep a long-lived credential in the OS store, exchange it for a 15-60 minute JWT. The building-block view documents the extension as a live sync client. Every piece the flow needs already ships: the anonymous /devices/pair and /devices/token routes, the desktop's Devices settings tab that issues codes, and a C# reference client (DevicePairingClient, SyncTokenProvider) to mirror. Option 2 would contradict an accepted ADR and leave a documented channel dead, which needs a debt record. Option 1 makes the docs true with a small, self-contained TypeScript change and gives the extension its first tests. SecretStorage is available within the held ^1.90.0 engine, so the reach decision pinned by VsCodeExtensionReachTests is untouched.
To follow the alternative instead. Following option 2 instead would remove syncClient.ts, the pair and unpair commands and most of the new tests, replacing them with a short not-paired guard in extension.ts and one test (or README note) asserting no inbox fetch is made. The 'editor extension' wording would come out of InboxEndpoints.cs, InboxContracts.cs and the InboxCaptureEndpointTests.cs comments. The devbook edits would flip from 'pairs and sends a bearer token' to 'parked', and a new TDR would be added. The CI
npm teststep could stay or go. No server or desktop code changes in either option, so the swap is confined to src/App/Backlog.Ide.VsCode, the doc comments and the devbook chapters.Acceptance criteria
Testing
cd "D:/iswp/718-vscode-sync-401" && dotnet build Backlog.sln && dotnet test Backlog.sln --no-build ; cd src/App/Backlog.Ide.VsCode && npm testnpm testinsrc/App/Backlog.Ide.VsCode— 13 passed, 0 failed.dotnet testofBacklog.UI.Components.UnitTests(3429) andBacklog.Desktop.UI.UnitTests(5134) — 0 failed. Both devbook checks pass (build.mjs --check;check-metadata.mjs: 0 blocking).Review
sync-client.ts,test/sync-client.test.ts) per.agents/rules/naming.md; imports, README, building-block view andtech/ide.mdfollow.pairing.code_expiredthatRedeemPairingCodeCommandreturns throughSyncResults.StatusFor.testscript passesnode --testa quoted glob ("out/test/**/*.test.js"), and Node only expands globs in--testfrom v21. .devbook/tech/shared.md#nodejs records the Node floor as>=18. On Node 18 or 20, which are inside that range,npm testcompiles and then fails with "Could not find .../out/test/**/*.test.js". CI does not catch this because it pins Node 24.x.pair()stores a new deviceId and credential. The stale request then returns 401 and runssecrets.delete(DEVICE_SECRET_KEY). That deletes the new pairing, and its single-use code is already burned.showErrorMessage. They now go through the sharedreportFailure, which shows everyunavailableresult withshowWarningMessage. A failed capture loses the title the person just typed, but it now shows at the same warning level as a tree refresh, which is a quiet change from how the module reported capture failures before.opt 401 from the token endpointblock comes after the inbox POST and its alt. In the code (SyncClient.accessToken), a 401 from the token endpoint returnscredentialRejectedbefore any inbox request is sent. A reader of the diagram would conclude that an inbox POST goes out before the credential is found to be rejected.TokenResultcopiesSyncResult<T>field by field and only renamesvaluetotoken. EverynotPaired,credentialRejectedorunavailablecase now has to be kept the same in two unions.sendalready depends on the two matching, because it returns aTokenResultwhere aSyncResult<T>is expected.vscode-extensionjob now runsnpm test, but it is still named 'Compile VS Code extension'. The sibling .NET job is named 'Build and test .NET', so a failing extension test shows up in the PR checks under a compile-only name.What could not be proved
WaitForAssertiontimeouts inSearchBoxTests.Typing_reports_the_query_once_the_debounce_has_elapsedandHomeDevbookPaneTests.Pressing_an_entry_s_devbook_reference_opens_it_in_the_devbook_pane, in .NET projects this TypeScript-and-docs change does not touch. Both pass alone, and both whole projects passed on a re-run (3429 and 5134, 0 failed). They are known timing flakes under full-suite load.extension.tsin a real VS Code host. Check against the running AppHost (ide-vscode-hostplus the cloud resource): (1) unpaired, the Backlog Inbox view warns 'not paired yet' and Capture Selection asks to pair first; (2) a code from the desktop's Settings › Devices › Let another device in, entered through Backlog: Pair with Desktop, pairs and the inbox lists the desktop owner's captures; (3) a capture lands in the desktop inbox; (4) an expired or wrong code shows a readable 'pairing failed' message; (5) after the device is revoked, refresh offers re-pairing rather than 'cloud sync unavailable'; (6) Backlog: Forget Pairing returns to the unpaired state and clears the tree.npm testglob (out/test/**/*.test.js) relies on Node 21+ expanding--testglobs. CI pins Node 24.x, but.devbook/tech/shared.mdrecords a Node floor of 18; watch thevscode-extensionjob on this pull request.Merge Danger
Door. Two-way: a revert undoes it. The only stored state is one
SecretStorageentry (backlog.sync.device) on the editor's machine; no server, schema or setting changes. No decision record is needed: local ADR 0005 already prescribes this pairing flow.Blast radius. local — the VS Code extension and the
vscode-extensionCI job (now also runsnpm test).Assumptions taken instead of asking
VS Code on <os.hostname()>, with no setting.${vscode.env.appName} on ${os.hostname()}.node --test out/test/treats the directory as a module and fails. Node expands the glob itself, so the same script works under Windows cmd and Linux sh.Opened as a draft by an attended run of
delivery-schedule:schedule-issue-sweep(2026-10-15), told by the owner to decide without asking and to name the choice and the alternative.🤖 Generated with Claude Code