feat(permissions): add local network connection test

This commit is contained in:
Brennan Benson
2026-08-06 13:22:40 -07:00
parent 7b703aac40
commit 403818fed2
12 changed files with 672 additions and 30 deletions
+7
View File
@@ -2,6 +2,7 @@ import { execFile } from 'node:child_process'
import dgram from 'node:dgram'
import { ipcMain, shell, systemPreferences } from 'electron'
import { getMacosFullDiskAccessStatus } from '../macos-full-disk-access-status'
import { testLocalNetworkConnection } from './local-network-connection-test'
import type {
DeveloperPermissionId,
DeveloperPermissionRequestResult,
@@ -237,4 +238,10 @@ export function registerDeveloperPermissionHandlers(): void {
await openPrivacyPane(args.id)
}
)
ipcMain.handle('developerPermissions:testLocalNetworkConnection', async (_event, args: unknown) =>
testLocalNetworkConnection(
args && typeof args === 'object' ? (args as { host?: unknown; port?: unknown }) : {}
)
)
}
@@ -0,0 +1,89 @@
import { createServer } from 'node:net'
import { describe, expect, it, vi } from 'vitest'
import { testLocalNetworkConnection } from './local-network-connection-test'
describe('testLocalNetworkConnection', () => {
it('runs a bounded Electron child probe and reports a verified connection', async () => {
const runChild = vi.fn((_file, _args, _options, callback) => callback(null, '', ''))
await expect(
testLocalNetworkConnection(
{ host: '192.168.1.20', port: 3000 },
{ platform: 'darwin', now: () => 1234, runChild }
)
).resolves.toEqual({
ok: true,
host: '192.168.1.20',
port: 3000,
testedAt: 1234
})
expect(runChild).toHaveBeenCalledWith(
process.execPath,
expect.arrayContaining(['192.168.1.20', '3000']),
expect.objectContaining({
timeout: 5000,
env: expect.objectContaining({ ELECTRON_RUN_AS_NODE: '1' })
}),
expect.any(Function)
)
})
it('rejects a hostname that resolves to loopback in the real child probe', async () => {
const server = createServer()
await new Promise<void>((resolve, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', resolve)
})
const address = server.address()
if (!address || typeof address === 'string') {
throw new Error('Expected a TCP listener address')
}
try {
await expect(
testLocalNetworkConnection(
{ host: '2130706433', port: address.port },
{ platform: 'darwin' }
)
).resolves.toMatchObject({ ok: false, failure: 'invalid-target' })
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()))
}
})
it.each([
['INVALID_TARGET', 'invalid-target'],
['ETIMEDOUT', 'timeout'],
['ECONNREFUSED', 'refused'],
['EHOSTUNREACH', 'unreachable'],
['ENETUNREACH', 'unreachable'],
['ENOTFOUND', 'unresolved']
] as const)('maps %s without claiming a permission denial', async (stderr, failure) => {
const runChild = vi.fn((_file, _args, _options, callback) =>
callback(Object.assign(new Error(stderr), { code: 1 }), '', stderr)
)
await expect(
testLocalNetworkConnection(
{ host: 'devbox.local', port: 8080 },
{ platform: 'darwin', runChild }
)
).resolves.toMatchObject({ ok: false, failure })
})
it.each([
{ host: '127.0.0.1', port: 3000 },
{ host: 'localhost', port: 3000 },
{ host: 'service.localhost', port: 3000 },
{ host: '8.8.8.8', port: 53 },
{ host: 'https://devbox.local', port: 443 },
{ host: 'devbox.local', port: 0 }
])('rejects a target that does not test a LAN connection: $host:$port', async (target) => {
const runChild = vi.fn()
await expect(
testLocalNetworkConnection(target, { platform: 'darwin', runChild })
).resolves.toMatchObject({ ok: false, failure: 'invalid-target' })
expect(runChild).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,167 @@
import { execFile, type ExecFileException, type ExecFileOptions } from 'node:child_process'
import { isIP } from 'node:net'
import { classifyRemotePairingHostname } from '../../shared/remote-pairing-address'
import type {
LocalNetworkConnectionTestFailure,
LocalNetworkConnectionTestResult
} from '../../shared/developer-permissions-types'
const CONNECT_TIMEOUT_MS = 4_000
const CHILD_TIMEOUT_MS = CONNECT_TIMEOUT_MS + 1_000
const CONNECT_SCRIPT = `
const net = require('node:net')
const host = process.argv[1]
const port = Number(process.argv[2])
const socket = net.createConnection({ host, port })
let settled = false
function finish(code) {
if (settled) return
settled = true
if (code) {
process.stderr.write(code)
process.exitCode = 1
}
socket.destroy()
}
function isLanAddress(address) {
const normalized = (address || '').toLowerCase().split('%', 1)[0]
const mapped = normalized.match(/^::ffff:(\\d{1,3}(?:\\.\\d{1,3}){3})$/)?.[1]
const ipv4 = mapped || (net.isIP(normalized) === 4 ? normalized : null)
if (ipv4) {
const octets = ipv4.split('.').map(Number)
return octets[0] === 10 ||
(octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) ||
(octets[0] === 192 && octets[1] === 168) ||
(octets[0] === 169 && octets[1] === 254)
}
if (net.isIP(normalized) === 6) {
const first = Number.parseInt(normalized.split(':')[0], 16)
return (first & 0xfe00) === 0xfc00 || (first & 0xffc0) === 0xfe80
}
return false
}
socket.setTimeout(${CONNECT_TIMEOUT_MS})
socket.once('connect', () => finish(isLanAddress(socket.remoteAddress) ? undefined : 'INVALID_TARGET'))
socket.once('timeout', () => finish('ETIMEDOUT'))
socket.once('error', (error) => finish(error.code || 'FAILED'))
`
type ConnectionChildRunner = (
file: string,
args: string[],
options: ExecFileOptions,
callback: (error: ExecFileException | null, stdout: string, stderr: string) => void
) => unknown
type LocalNetworkConnectionTestOptions = {
platform?: NodeJS.Platform
now?: () => number
runChild?: ConnectionChildRunner
}
function normalizeTargetHost(rawHost: unknown): string | null {
if (typeof rawHost !== 'string') {
return null
}
const host = rawHost.trim().replace(/^\[|\]$/g, '')
if (
!host ||
host.startsWith('-') ||
host.length > 253 ||
/[\s/\\]/.test(host) ||
host.includes('://') ||
!/^[a-z0-9.:%_-]+$/i.test(host)
) {
return null
}
const addressWithoutZone = host.split('%', 1)[0] ?? host
const kind = classifyRemotePairingHostname(addressWithoutZone)
if (isIP(addressWithoutZone) === 0) {
return kind === 'loopback' ? null : host
}
const isLinkLocalIpv4 = addressWithoutZone.startsWith('169.254.')
return kind === 'lan' || isLinkLocalIpv4 ? host : null
}
function normalizeTargetPort(rawPort: unknown): number | null {
return typeof rawPort === 'number' &&
Number.isInteger(rawPort) &&
rawPort >= 1 &&
rawPort <= 65_535
? rawPort
: null
}
function failureFromChild(
error: ExecFileException,
stderr: string
): LocalNetworkConnectionTestFailure {
if (error.killed || error.signal) {
return 'timeout'
}
const code = stderr.trim().split(/\s/, 1)[0]
switch (code) {
case 'INVALID_TARGET':
return 'invalid-target'
case 'ETIMEDOUT':
return 'timeout'
case 'ECONNREFUSED':
return 'refused'
case 'EHOSTUNREACH':
case 'ENETUNREACH':
return 'unreachable'
case 'ENOTFOUND':
case 'EAI_AGAIN':
return 'unresolved'
default:
return 'failed'
}
}
export function testLocalNetworkConnection(
args: { host?: unknown; port?: unknown },
options: LocalNetworkConnectionTestOptions = {}
): Promise<LocalNetworkConnectionTestResult> {
const testedAt = (options.now ?? Date.now)()
const host = normalizeTargetHost(args.host)
const port = normalizeTargetPort(args.port)
if (!host || !port) {
return Promise.resolve({
ok: false,
host: typeof args.host === 'string' ? args.host.trim() : '',
port: typeof args.port === 'number' ? args.port : 0,
testedAt,
failure: 'invalid-target'
})
}
if ((options.platform ?? process.platform) !== 'darwin') {
return Promise.resolve({ ok: false, host, port, testedAt, failure: 'unsupported' })
}
const runChild = options.runChild ?? (execFile as ConnectionChildRunner)
return new Promise((resolve) => {
runChild(
process.execPath,
['-e', CONNECT_SCRIPT, host, String(port)],
{
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
timeout: CHILD_TIMEOUT_MS,
windowsHide: true,
maxBuffer: 1024
},
(error, _stdout, stderr) => {
if (!error) {
resolve({ ok: true, host, port, testedAt })
return
}
resolve({
ok: false,
host,
port,
testedAt,
failure: failureFromChild(error, stderr)
})
}
)
})
}
+6 -1
View File
@@ -430,7 +430,8 @@ import type {
import type {
DeveloperPermissionId,
DeveloperPermissionRequestResult,
DeveloperPermissionState
DeveloperPermissionState,
LocalNetworkConnectionTestResult
} from '../shared/developer-permissions-types'
import type {
ComputerUsePermissionId,
@@ -2552,6 +2553,10 @@ export type PreloadApi = {
getStatus: () => Promise<DeveloperPermissionState[]>
request: (args: { id: DeveloperPermissionId }) => Promise<DeveloperPermissionRequestResult>
openSettings: (args: { id: DeveloperPermissionId }) => Promise<void>
testLocalNetworkConnection: (args: {
host: string
port: number
}) => Promise<LocalNetworkConnectionTestResult>
}
computerUsePermissions: {
getStatus: () => Promise<ComputerUsePermissionStatusResult>
+3 -1
View File
@@ -2403,7 +2403,9 @@ const api = {
request: (args: { id: string }): Promise<unknown> =>
ipcRenderer.invoke('developerPermissions:request', args),
openSettings: (args: { id: string }): Promise<void> =>
ipcRenderer.invoke('developerPermissions:openSettings', args)
ipcRenderer.invoke('developerPermissions:openSettings', args),
testLocalNetworkConnection: (args: { host: string; port: number }): Promise<unknown> =>
ipcRenderer.invoke('developerPermissions:testLocalNetworkConnection', args)
},
computerUsePermissions: {
@@ -20,6 +20,7 @@ import type {
import { Button } from '../ui/button'
import { translate } from '@/i18n/i18n'
import { DeveloperPermissionActions } from './DeveloperPermissionActions'
import { LocalNetworkConnectionTest } from './LocalNetworkConnectionTest'
import {
developerPermissionStatusClass,
developerPermissionStatusLabel
@@ -345,35 +346,37 @@ export function DeveloperPermissionsPane({
const settingId = `developer-permissions-${permission.id}`
return (
<div
key={permission.id}
data-settings-section={settingId}
data-highlighted={highlightedSettingId === settingId ? 'true' : undefined}
className="flex items-center justify-between gap-4 px-4 py-3 transition-[background-color,box-shadow] duration-500 data-[highlighted=true]:bg-accent data-[highlighted=true]:ring-2 data-[highlighted=true]:ring-inset data-[highlighted=true]:ring-ring/50 motion-reduce:transition-none"
>
<div className="flex min-w-0 items-start gap-3">
<div className="mt-0.5 text-muted-foreground">{permission.icon}</div>
<div className="min-w-0 space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium">{permission.label}</span>
<span
className={`rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider ${developerPermissionStatusClass(
status
)}`}
>
{developerPermissionStatusLabel(permission.id, status)}
</span>
<div key={permission.id}>
<div
data-settings-section={settingId}
data-highlighted={highlightedSettingId === settingId ? 'true' : undefined}
className="flex items-center justify-between gap-4 px-4 py-3 transition-[background-color,box-shadow] duration-500 data-[highlighted=true]:bg-accent data-[highlighted=true]:ring-2 data-[highlighted=true]:ring-inset data-[highlighted=true]:ring-ring/50 motion-reduce:transition-none"
>
<div className="flex min-w-0 items-start gap-3">
<div className="mt-0.5 text-muted-foreground">{permission.icon}</div>
<div className="min-w-0 space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium">{permission.label}</span>
<span
className={`rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider ${developerPermissionStatusClass(
status
)}`}
>
{developerPermissionStatusLabel(permission.id, status)}
</span>
</div>
<p className="text-xs text-muted-foreground">{permission.description}</p>
</div>
<p className="text-xs text-muted-foreground">{permission.description}</p>
</div>
<DeveloperPermissionActions
id={permission.id}
actionLabel={permission.actionLabel}
pending={pending}
status={status}
onRequest={(id) => void request(id)}
/>
</div>
<DeveloperPermissionActions
id={permission.id}
actionLabel={permission.actionLabel}
pending={pending}
status={status}
onRequest={(id) => void request(id)}
/>
{permission.id === 'local-network' && <LocalNetworkConnectionTest />}
</div>
)
})}
@@ -0,0 +1,96 @@
// @vitest-environment happy-dom
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { LocalNetworkConnectionTest } from './LocalNetworkConnectionTest'
let container: HTMLDivElement
let root: Root
const testConnectionMock = vi.fn()
beforeEach(() => {
localStorage.clear()
testConnectionMock.mockReset()
Object.assign(window, {
api: {
developerPermissions: {
testLocalNetworkConnection: testConnectionMock
}
}
})
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
})
afterEach(async () => {
await act(async () => root.unmount())
container.remove()
localStorage.clear()
Reflect.deleteProperty(window, 'api')
})
function inputValue(input: HTMLInputElement, value: string): void {
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
setter?.call(input, value)
input.dispatchEvent(new Event('input', { bubbles: true }))
}
it('saves and displays the last successful target-specific test', async () => {
testConnectionMock.mockResolvedValue({
ok: true,
host: '192.168.1.20',
port: 3000,
testedAt: new Date('2026-08-06T07:00:00Z').getTime()
})
await act(async () => root.render(<LocalNetworkConnectionTest />))
const host = container.querySelector<HTMLInputElement>('#local-network-test-host')!
const port = container.querySelector<HTMLInputElement>('#local-network-test-port')!
await act(async () => {
inputValue(host, '192.168.1.20')
inputValue(port, '3000')
})
await act(async () => container.querySelector<HTMLButtonElement>('button')?.click())
expect(testConnectionMock).toHaveBeenCalledWith({ host: '192.168.1.20', port: 3000 })
expect(container.textContent).toContain('Last verified')
expect(container.textContent).toContain('192.168.1.20:3000')
await act(async () => root.render(null))
await act(async () => root.render(<LocalNetworkConnectionTest />))
expect(container.textContent).toContain('192.168.1.20:3000')
})
it('keeps the last success visible when a later test fails', async () => {
testConnectionMock
.mockResolvedValueOnce({
ok: true,
host: 'devbox.local',
port: 8080,
testedAt: 1000
})
.mockResolvedValueOnce({
ok: false,
host: 'devbox.local',
port: 8080,
testedAt: 2000,
failure: 'refused'
})
await act(async () => root.render(<LocalNetworkConnectionTest />))
const host = container.querySelector<HTMLInputElement>('#local-network-test-host')!
const port = container.querySelector<HTMLInputElement>('#local-network-test-port')!
await act(async () => {
inputValue(host, 'devbox.local')
inputValue(port, '8080')
})
await act(async () => container.querySelector<HTMLButtonElement>('button')?.click())
await act(async () => container.querySelector<HTMLButtonElement>('button')?.click())
expect(container.textContent).toContain('Last verified')
expect(container.textContent).toContain('devbox.local:8080')
expect(container.textContent).toContain('The host responded, but the port refused')
expect(container.textContent).not.toContain('Permission denied')
})
@@ -0,0 +1,188 @@
import { useState, type FormEvent } from 'react'
import { CheckCircle2 } from 'lucide-react'
import type {
LocalNetworkConnectionTestFailure,
LocalNetworkConnectionTestResult
} from '../../../../shared/developer-permissions-types'
import { translate } from '@/i18n/i18n'
import { Button } from '../ui/button'
import { Input } from '../ui/input'
import { Label } from '../ui/label'
import {
loadLocalNetworkConnectionSuccess,
saveLocalNetworkConnectionSuccess,
type LocalNetworkConnectionSuccess
} from './local-network-connection-history'
function failureMessage(failure: LocalNetworkConnectionTestFailure | undefined): string {
switch (failure) {
case 'invalid-target':
return translate(
'auto.components.settings.DeveloperPermissionsPane.connectionTestInvalidTarget',
'Enter a hostname or private LAN IP and a port from 1 to 65535.'
)
case 'timeout':
return translate(
'auto.components.settings.DeveloperPermissionsPane.connectionTestTimeout',
'Connection timed out. Check the target, service, and macOS Local Network settings.'
)
case 'refused':
return translate(
'auto.components.settings.DeveloperPermissionsPane.connectionTestRefused',
'The host responded, but the port refused the connection.'
)
case 'unreachable':
return translate(
'auto.components.settings.DeveloperPermissionsPane.connectionTestUnreachable',
'The target could not be reached.'
)
case 'unresolved':
return translate(
'auto.components.settings.DeveloperPermissionsPane.connectionTestUnresolved',
'The hostname could not be resolved.'
)
case 'unsupported':
return translate(
'auto.components.settings.DeveloperPermissionsPane.connectionTestUnsupported',
'Connection testing is available in the macOS desktop app.'
)
case 'failed':
case undefined:
return translate(
'auto.components.settings.DeveloperPermissionsPane.connectionTestFailed',
'The connection test could not be completed.'
)
}
}
function formatTarget(success: LocalNetworkConnectionSuccess): string {
return `${success.host}:${success.port}`
}
export function LocalNetworkConnectionTest(): React.JSX.Element {
const [lastSuccess, setLastSuccess] = useState(loadLocalNetworkConnectionSuccess)
const [host, setHost] = useState(lastSuccess?.host ?? '')
const [port, setPort] = useState(lastSuccess ? String(lastSuccess.port) : '')
const [running, setRunning] = useState(false)
const [failure, setFailure] = useState<LocalNetworkConnectionTestFailure | null>(null)
const submit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
event.preventDefault()
setRunning(true)
setFailure(null)
try {
const result: LocalNetworkConnectionTestResult =
await window.api.developerPermissions.testLocalNetworkConnection({
host,
port: Number(port)
})
if (result.ok) {
const success = {
host: result.host,
port: result.port,
testedAt: result.testedAt
}
saveLocalNetworkConnectionSuccess(success)
setLastSuccess(success)
} else {
setFailure(result.failure ?? 'failed')
}
} catch {
setFailure('failed')
} finally {
setRunning(false)
}
}
return (
<div className="border-t border-border/60 bg-muted/10 px-4 py-3 pl-11">
<div className="flex flex-wrap items-end justify-between gap-4">
<div className="min-w-[240px] flex-1 space-y-1">
<div className="text-sm font-medium">
{translate(
'auto.components.settings.DeveloperPermissionsPane.connectionTestTitle',
'Test connection'
)}
</div>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.DeveloperPermissionsPane.connectionTestDescription',
'Enter a service on another device on your local network. Orca tests the same network path used by terminal tools.'
)}
</p>
{lastSuccess ? (
<p className="flex items-center gap-1.5 text-xs text-emerald-700 dark:text-emerald-300">
<CheckCircle2 className="size-3.5" />
{translate(
'auto.components.settings.DeveloperPermissionsPane.connectionTestLastVerified',
'Last verified'
)}{' '}
{new Date(lastSuccess.testedAt).toLocaleString()} · {formatTarget(lastSuccess)}
</p>
) : (
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.DeveloperPermissionsPane.connectionTestNotYetVerified',
'No successful test saved.'
)}
</p>
)}
{failure && (
<p className="text-xs text-destructive" role="status">
{failureMessage(failure)}
</p>
)}
</div>
<form className="flex items-end gap-2" onSubmit={(event) => void submit(event)}>
<div className="space-y-1">
<Label htmlFor="local-network-test-host" className="text-[11px]">
{translate(
'auto.components.settings.DeveloperPermissionsPane.connectionTestHost',
'Host'
)}
</Label>
<Input
id="local-network-test-host"
value={host}
onChange={(event) => setHost(event.target.value)}
placeholder="192.168.1.20"
autoComplete="off"
className="w-44"
disabled={running}
/>
</div>
<div className="space-y-1">
<Label htmlFor="local-network-test-port" className="text-[11px]">
{translate(
'auto.components.settings.DeveloperPermissionsPane.connectionTestPort',
'Port'
)}
</Label>
<Input
id="local-network-test-port"
type="number"
min={1}
max={65_535}
value={port}
onChange={(event) => setPort(event.target.value)}
placeholder="3000"
className="w-24"
disabled={running}
/>
</div>
<Button type="submit" variant="outline" size="sm" disabled={running || !host || !port}>
{running
? translate(
'auto.components.settings.DeveloperPermissionsPane.connectionTestRunning',
'Testing...'
)
: translate(
'auto.components.settings.DeveloperPermissionsPane.connectionTestAction',
'Test Connection'
)}
</Button>
</form>
</div>
</div>
)
}
@@ -0,0 +1,45 @@
import type { LocalNetworkConnectionTestResult } from '../../../../shared/developer-permissions-types'
const STORAGE_KEY = 'orca.developer-permissions.local-network-last-success.v1'
export type LocalNetworkConnectionSuccess = Pick<
LocalNetworkConnectionTestResult,
'host' | 'port' | 'testedAt'
>
function isSavedSuccess(value: unknown): value is LocalNetworkConnectionSuccess {
if (!value || typeof value !== 'object') {
return false
}
const candidate = value as Partial<LocalNetworkConnectionSuccess>
return (
typeof candidate.host === 'string' &&
candidate.host.length > 0 &&
Number.isInteger(candidate.port) &&
candidate.port! >= 1 &&
candidate.port! <= 65_535 &&
typeof candidate.testedAt === 'number' &&
Number.isFinite(candidate.testedAt)
)
}
export function loadLocalNetworkConnectionSuccess(): LocalNetworkConnectionSuccess | null {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) {
return null
}
const parsed: unknown = JSON.parse(raw)
return isSavedSuccess(parsed) ? parsed : null
} catch {
return null
}
}
export function saveLocalNetworkConnectionSuccess(success: LocalNetworkConnectionSuccess): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(success))
} catch {
// The live result remains useful when browser storage is unavailable.
}
}
+16 -1
View File
@@ -5905,7 +5905,22 @@
"localNetworkOpenSettings": "Open System Settings",
"openSettingsFailed": "Could not open System Settings",
"localNetworkOpenSystemSettings": "Open System Settings",
"statusManagedByMacOS": "Managed by macOS"
"statusManagedByMacOS": "Managed by macOS",
"connectionTestInvalidTarget": "Enter a hostname or private LAN IP and a port from 1 to 65535.",
"connectionTestTimeout": "Connection timed out. Check the target, service, and macOS Local Network settings.",
"connectionTestRefused": "The host responded, but the port refused the connection.",
"connectionTestUnreachable": "The target could not be reached.",
"connectionTestUnresolved": "The hostname could not be resolved.",
"connectionTestUnsupported": "Connection testing is available in the macOS desktop app.",
"connectionTestFailed": "The connection test could not be completed.",
"connectionTestTitle": "Test connection",
"connectionTestDescription": "Enter a service on another device on your local network. Orca tests the same network path used by terminal tools.",
"connectionTestLastVerified": "Last verified",
"connectionTestNotYetVerified": "No successful test saved.",
"connectionTestHost": "Host",
"connectionTestPort": "Port",
"connectionTestRunning": "Testing...",
"connectionTestAction": "Test Connection"
},
"ExperimentalPane": {
"9762364929": "Uses APFS clone-copy on macOS when possible, otherwise symlinks configured folders or files into created worktrees.",
+9 -1
View File
@@ -2927,7 +2927,15 @@ function createDeveloperPermissionsApi(): NonNullable<Partial<PreloadApi>['devel
getStatus: () => Promise.resolve([]),
request: ({ id }) =>
Promise.resolve({ id, status: 'unsupported', openedSystemSettings: false } as const),
openSettings: () => Promise.resolve()
openSettings: () => Promise.resolve(),
testLocalNetworkConnection: ({ host, port }) =>
Promise.resolve({
ok: false,
host,
port,
testedAt: Date.now(),
failure: 'unsupported'
} as const)
}
}
+17
View File
@@ -28,3 +28,20 @@ export type DeveloperPermissionRequestResult = {
status: DeveloperPermissionStatus
openedSystemSettings: boolean
}
export type LocalNetworkConnectionTestFailure =
| 'invalid-target'
| 'timeout'
| 'refused'
| 'unreachable'
| 'unresolved'
| 'failed'
| 'unsupported'
export type LocalNetworkConnectionTestResult = {
ok: boolean
host: string
port: number
testedAt: number
failure?: LocalNetworkConnectionTestFailure
}