fix: address remote pairing review findings

This commit is contained in:
Jinjing
2026-07-30 00:10:46 -07:00
parent 29e8459fc8
commit e27fa68c1f
8 changed files with 119 additions and 12 deletions
@@ -0,0 +1,38 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { encodePairingOffer, PAIRING_OFFER_VERSION } from '../../../../shared/pairing'
import { parseHostAccessLink } from '../../../../shared/remote-pairing-address'
import { RemoteServerFields } from './AddRemoteHostFields'
function loopbackAccessLink(): string {
return encodePairingOffer({
v: PAIRING_OFFER_VERSION,
endpoint: 'ws://127.0.0.1:6768',
deviceToken: 'token',
publicKeyB64: 'key',
scope: 'runtime'
})
}
describe('RemoteServerFields', () => {
it('associates blocked loopback guidance with the access-link input', () => {
const pairingCode = loopbackAccessLink()
const markup = renderToStaticMarkup(
<RemoteServerFields
name="Remote workstation"
pairingCode={pairingCode}
parsedLink={parseHostAccessLink(pairingCode)}
disabled={false}
onNameChange={vi.fn()}
onPairingCodeChange={vi.fn()}
allowLoopback={false}
onAllowLoopbackChange={vi.fn()}
onSubmit={vi.fn()}
/>
)
expect(markup).toContain('aria-invalid="true"')
expect(markup).toContain('aria-describedby="add-server-loopback-blocked"')
expect(markup).toContain('id="add-server-loopback-blocked"')
})
})
@@ -148,6 +148,11 @@ export function RemoteServerFields({
const inputError = pairingCode.trim() !== '' && !parsedLink.ok
const loopbackBlocked =
parsedLink.ok && parsedLink.value.endpointKind === 'loopback' && !allowLoopback
const pairingCodeDescriptionId = inputError
? 'add-server-pairing-code-error'
: loopbackBlocked
? 'add-server-loopback-blocked'
: 'add-server-pairing-code-help'
return (
<form
className="space-y-3"
@@ -178,8 +183,8 @@ export function RemoteServerFields({
</Label>
<Input
id="add-server-pairing-code"
aria-invalid={inputError}
aria-describedby="add-server-pairing-code-help"
aria-invalid={inputError || loopbackBlocked}
aria-describedby={pairingCodeDescriptionId}
value={pairingCode}
disabled={disabled}
onChange={(event) => onPairingCodeChange(event.target.value)}
@@ -196,7 +201,7 @@ export function RemoteServerFields({
)}
</p>
{inputError ? (
<p role="alert" className="text-xs text-destructive">
<p id="add-server-pairing-code-error" role="alert" className="text-xs text-destructive">
{parsedLink.ok ? null : translateHostAccessLinkError(parsedLink.kind)}
</p>
) : null}
@@ -239,7 +244,7 @@ export function RemoteServerFields({
</div>
) : null}
{loopbackBlocked ? (
<p role="alert" className="text-xs text-destructive">
<p id="add-server-loopback-blocked" role="alert" className="text-xs text-destructive">
{translate(
'auto.components.sidebar.AddRemoteHostDialog.loopbackBlocked',
'Enable the SSH tunnel override or create a new link using the other host’s Tailscale or LAN address.'
@@ -631,6 +631,35 @@ describe('web runtime environment identity', () => {
{ id: 'web-server-a' }
])
})
it('classifies coded browser authorization failures without relying on copy', async () => {
vi.doMock('./web-runtime-client', () => ({
WebRuntimeClient: class {
call(): Promise<RuntimeRpcResponse<unknown>> {
return Promise.reject(
Object.assign(new Error('Access grant rejected.'), { code: 'unauthorized' })
)
}
close(): void {}
}
}))
const globals = installBrowserGlobals('Linux')
writeStoredRuntimeEnvironment(globals.storage, 'web-server-a')
const { installWebPreloadApi } = await import('./web-preload-api')
installWebPreloadApi()
await expect(
globals.window.api.runtimeEnvironments.verifyAndAddFromPairingCode({
name: 'Expired server',
pairingCode: encodePairingCode()
})
).resolves.toMatchObject({
ok: false,
kind: 'access-link-invalid',
message: 'Access grant rejected.'
})
})
})
describe('web browser-local port capability', () => {
+5 -1
View File
@@ -115,6 +115,7 @@ import {
import { parseWebPairingInput } from './web-pairing'
import { copyClipboardTextViaExecCommand } from './web-clipboard-copy-fallback'
import { WebRuntimeClient } from './web-runtime-client'
import { isWebRuntimeUnauthorizedError } from './web-runtime-client-error'
import { RuntimeRpcCallQueuePool } from '../../../shared/runtime-rpc-call-queue'
import {
assertClipboardTextWriteWithinLimitWithYield,
@@ -1408,7 +1409,10 @@ function createRuntimeEnvironmentsApi(): NonNullable<Partial<PreloadApi>['runtim
)
}
}
if (error instanceof Error && error.message.startsWith('Unauthorized.')) {
if (
isWebRuntimeUnauthorizedError(error) ||
(error instanceof Error && error.message.startsWith('Unauthorized.'))
) {
return {
ok: false,
kind: 'access-link-invalid',
@@ -0,0 +1,25 @@
const UNAUTHORIZED_MESSAGE = 'Unauthorized. Pair this web client again.'
export class WebRuntimeClientError extends Error {
constructor(
message: string,
readonly code: 'unauthorized'
) {
super(message)
this.name = 'WebRuntimeClientError'
}
}
export function createWebRuntimeUnauthorizedError(): WebRuntimeClientError {
return new WebRuntimeClientError(UNAUTHORIZED_MESSAGE, 'unauthorized')
}
export function isWebRuntimeUnauthorizedError(
error: unknown
): error is Error & { code: 'unauthorized' } {
return (
error instanceof Error &&
'code' in error &&
(error as { code?: unknown }).code === 'unauthorized'
)
}
+7 -6
View File
@@ -15,6 +15,7 @@ import {
publicKeyToBase64
} from './web-e2ee'
import { SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY } from '../../../shared/protocol-version'
import { createWebRuntimeUnauthorizedError } from './web-runtime-client-error'
type WebRuntimeConnectionState =
| 'disconnected'
@@ -478,7 +479,7 @@ export class WebRuntimeClient {
} else if (control.type === 'e2ee_error' || control.error?.code === 'unauthorized') {
this.intentionallyClosed = true
this.setState('auth-failed')
this.rejectAllPending('Unauthorized. Pair this web client again.')
this.rejectAllPending(createWebRuntimeUnauthorizedError())
this.notifySubscriptionsError('unauthorized', 'Unauthorized. Pair this web client again.')
this.ws?.close()
}
@@ -530,7 +531,7 @@ export class WebRuntimeClient {
if (isRuntimeFailureResponse(response) && response.error.code === 'unauthorized') {
this.intentionallyClosed = true
this.setState('auth-failed')
this.rejectAllPending('Unauthorized. Pair this web client again.')
this.rejectAllPending(createWebRuntimeUnauthorizedError())
this.notifySubscriptionsError('unauthorized', 'Unauthorized. Pair this web client again.')
this.ws?.close()
return
@@ -584,7 +585,7 @@ export class WebRuntimeClient {
return Promise.resolve()
}
if (this.state === 'auth-failed') {
return Promise.reject(new Error('Unauthorized. Pair this web client again.'))
return Promise.reject(createWebRuntimeUnauthorizedError())
}
if (this.intentionallyClosed) {
return Promise.reject(new Error('Remote Orca runtime connection closed.'))
@@ -658,7 +659,7 @@ export class WebRuntimeClient {
waiter.resolve()
}
} else if (next === 'auth-failed') {
this.rejectAllWaiters(new Error('Unauthorized. Pair this web client again.'))
this.rejectAllWaiters(createWebRuntimeUnauthorizedError())
}
}
@@ -667,8 +668,8 @@ export class WebRuntimeClient {
return `web-rpc-${this.requestCounter}-${Date.now()}`
}
private rejectAllPending(reason: string): void {
const error = new Error(reason)
private rejectAllPending(reason: string | Error): void {
const error = typeof reason === 'string' ? new Error(reason) : reason
for (const [id, pending] of this.pending) {
this.pending.delete(id)
window.clearTimeout(pending.timeout)
@@ -69,6 +69,10 @@ describe('remote pairing address', () => {
ok: false,
kind: 'non-connectable-destination'
})
expect(parseHostAccessLink(accessLink('wss://orca.example.com:0'))).toMatchObject({
ok: false,
kind: 'non-connectable-destination'
})
})
it('blocks absolute localhost names used in access links', () => {
+2 -1
View File
@@ -127,7 +127,8 @@ export function parseHostAccessLink(input: string): ParseHostAccessLinkResult {
if (
normalizedHostname === '0.0.0.0' ||
normalizedHostname === '::' ||
getEmbeddedIPv4Address(normalizedHostname) === '0.0.0.0'
getEmbeddedIPv4Address(normalizedHostname) === '0.0.0.0' ||
endpoint.port === '0'
) {
return {
ok: false,