Let clients without a managed backend manage pairing#4496
Let clients without a managed backend manage pairing#4496colonelpanic8 wants to merge 4 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ApprovabilityVerdict: Needs human review This PR introduces new authentication/authorization capability for managing pairing on non-primary environments. Changes to auth-sensitive code paths and scope handling, combined with an unresolved bug report about potential URL parsing crashes, warrant human review. You can customize Macroscope's approvability policy. Learn more. |
Access management was bound to the primary environment, so it was reachable only from a client that owns a local backend. A hosted-app client has no primary at all — `platformConnectionSourceLayer` emits no registrations for `isHostedStaticApp()` — so the Connections page hid the whole "Authorized clients" section. A user who had paired the hosted app to their own server could not mint a pairing link, revoke a client, or see a pairing QR code for that server. Route the access-management endpoints through the environment supervisor's prepared connection instead of the primary, and administer the active environment when there is no primary. A desktop that manages its own backend keeps administering that backend, so managed mode is unchanged. Pairing also silently lost administrative scope: onboarding always requested the standard client scopes, and the server rejects a wider request rather than clamping it, so a client bootstrapped from a server's own startup token was downgraded to a standard session and could not manage the access it had just been handed. Request no scopes and take exactly what the credential carries. Only a directly addressable HTTP origin yields a shareable pairing URL; relay and SSH environments are reached through a broker or a local tunnel whose address means nothing on another device, so those fall back to showing the bare pairing code.
`t3 serve` already prints a QR code next to its startup pairing URL, but a link minted later with `t3 pair` printed only text, so pairing a phone meant transcribing a URL by hand. Encode the pair URL when there is one. A bare token is not actionable on the device that scans it, and JSON output stays machine-readable.
When the Connections page administers an environment it is not served by — a relay or SSH environment, or a bearer environment whose profile is missing — that environment has no addressable base URL, and the pairing row is meant to fall back to the bare pairing code. It instead fell through to the current page's `/pair` URL whenever this page was itself on a non-loopback host, so the QR code and the copy button handed out a link to this client app rather than to the server the link belongs to. The page's own origin only reaches the administered server when that server is the one that served the page, which is the managed-backend case. Make that a stated input of the fallback rather than an accident of the page's hostname, and move the choice into a tested helper.
b38c6b5 to
f145096
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b824828. Configure here.
| // outer URL can be public even when the target still resolves to the scanning | ||
| // device itself. | ||
| const targetUrl = pairingUrl.searchParams.get("host"); | ||
| return targetUrl !== null && isLoopbackHost(new URL(targetUrl).hostname); |
There was a problem hiding this comment.
Malformed host crashes pairing row
Medium Severity
isLoopbackPairingUrl calls new URL(targetUrl) on the host query param without guarding parse failures. Pairing URLs built from resolveCurrentOriginPairingUrl inherit the page’s search string, so a missing, empty, or non-absolute host value can throw during render and break the Authorized clients list.
Reviewed by Cursor Bugbot for commit b824828. Configure here.


What Changed
Access management (pairing links, authorized clients, and the pairing QR code) is now scoped to whichever server the Connections page is administering, instead of always the primary environment.
resolveAccessEnvironment()administers the primary when one exists, and the active environment otherwise. A desktop that manages its own backend is unaffected.packages/client-runtime/src/state/authHttp.tsroutes/api/auth/session,/api/auth/pairing-token,/api/auth/pairing-links/revoke,/api/auth/clients/revoke, and/api/auth/clients/revoke-othersthrough the environment supervisor's prepared connection.environmentPairingBaseUrl()produces a shareable pairing URL only for a directly addressable HTTP origin. Relay and SSH environments fall back to showing the bare pairing code.t3 pairprints the same scannable QR code thatt3 servealready prints at startup.Why
platformConnectionSourceLayerreturns no registrations whenisHostedStaticApp(), so a hosted-app client has no primary environment at all.canManageLocalBackendreads scopes from the primary session, which isnullin that case, so the entire "Authorized clients" section was hidden. A user who had paired the hosted app to their own server could not create a pairing link, revoke a client, or display a pairing QR code for that server.Worse, the page rendered a contradictory "Administrative access — pairing links and client-session management require the access:write scope for this backend" row even when the client had paired using the server's own administrative startup token.
That second part had its own cause: onboarding always requested the standard client scopes, and the server rejects a wider request rather than clamping it, so a client bootstrapped from a server's startup token was silently downgraded to a standard session and could not manage the access it had just been handed. Requesting no scopes and accepting what the credential carries fixes it.
The access-management endpoints were already per-environment; only the client bound them to the primary.
UI Changes
Hosted web client (no primary environment, served from a non-loopback origin on
:53595) paired to astandalone
t3 serveon:53141, atSettings → Connections. Same viewport, theme, and backend forevery shot; only the
apps/webbuild was swapped between commits.Before (
38cfc25, main) — no "Authorized clients" section at all, so there is no way to create apairing link or revoke a client, plus a misleading scope message despite having paired with the
server's administrative startup token:
After (
b38c6b5) — the section renders against the saved environment, withCreate link, revokecontrols, and the pairing link's QR affordance:
After (
b38c6b5) — the pairing link and its QR resolve to the administered backend's own origin(
:53141), not to the client app's origin (:53595). The token is redacted and the QR blurred; thelink was left to expire and the disposable backend was destroyed after capture:
Control for the
resolveShareablePairingUrlreview fix (unchanged rendering)The review fix only changes the case where the administered environment has no directly
addressable HTTP origin — a relay environment, an SSH environment, or a bearer entry with no profile.
Those states are not reachable from a locally runnable web client (relay needs managed-relay
infrastructure to connect; SSH needs
window.desktopBridge; a connected bearer environment always hasa base URL, since the resolver reads it from the same profile
environmentPairingBaseUrl()reads), sothat branch is covered by the unit tests in
ConnectionsSettings.logic.test.tsrather than by ascreenshot. For the case that is reachable here — an environment with a usable HTTP base URL — the
rendering is unchanged, as it should be:
Before the fix (
dcd149d):After the fix (
b38c6b5):Only the expiry countdown differs.
Checklist
Validation
Integrated verification: hosted-mode web client (
VITE_HOSTED_APP_CHANNELset, noVITE_HTTP_URL/VITE_WS_URL) paired to an isolatedt3 serve, then created a pairing link, opened its QR popover, and confirmed the link resolves to the environment's own base URL. Also confirmed a client holding only standard scopes still sees no access-management section.Note for reviewers
The section's visibility is gated on
access:writewhile its data stream requiresaccess:read, so a credential granted one but not the other renders an error state. That mismatch already exists onmainfor the primary path, so it is left unchanged here.Note
Medium Risk
Touches auth pairing scope exchange and per-environment access mutations; incorrect URL or environment routing could misdirect pairing or revoke the wrong server’s sessions.
Overview
Connections now administers pairing links and authorized clients on whichever server the page targets—primary when present, otherwise the active saved environment—so hosted or client-only setups can manage access without a managed backend.
New client-runtime
authHttppaths wire session state and pairing/revocation mutations through each environment’s prepared connection and credentials. ConnectionsSettings uses those atoms for non-primary environments, gates UI on that environment’saccess:writescope, and shows a dedicated Authorized clients section when there is no primary.Shareable pairing URLs are chosen via
resolveShareablePairingUrl/environmentPairingBaseUrl: prefer advertised or base HTTP origins, skip loopback and hosted links that wrap loopback, and avoid pointing scanners at the client app when administering a remote server (fallback to bare token).Pairing onboarding no longer sends a
scopeon token exchange so the server grants exactly what the credential carries (fixes admin startup tokens being downgraded to standard scopes).CLI human-readable
t3 pairoutput adds a terminal QR when a pair URL exists; JSON output stays machine-readable without QR.Reviewed by Cursor Bugbot for commit b824828. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Allow clients without a managed backend to manage pairing links and client sessions
createPairingCredential,revokePairingLink,revokeClientSession,revokeOtherClientSessions) and asessionStatequery in authHttp.ts, enabling direct auth management against a connected non-primary environment.formatIssuedPairingCredentialoutput when apairUrlis present; JSON output is unchanged.scopeparameter — scopes are now granted strictly per the pairing credential. Behavioral Change: previously, scopes frompresentation.scopeswere forwarded during token exchange.Macroscope summarized b824828.