feat(desktop): add SSH to the Cloud-aware connection model

Add SSH as a separate saved connection shape while preserving Cloud URL/OAuth semantics, inactive SSH drafts, strict host/port normalization, and profile-specific precedence.
This commit is contained in:
yoniebans 2026-07-15 16:13:12 +02:00
parent ae2175a584
commit a6113b4229
2 changed files with 114 additions and 0 deletions

View file

@ -25,9 +25,14 @@ import {
cookiesHaveSession,
modeIsRemoteLike,
normalizeRemoteBaseUrl,
normalizeSshConfig,
localProfileEntry,
normAuthMode,
pathWithGlobalRemoteProfile,
profileHasRemoteConnection,
profileRemoteOverride,
profileSshOverride,
savedProfileSsh,
resolveAuthMode,
resolveTestWsUrl,
RT_COOKIE_VARIANTS,
@ -123,6 +128,51 @@ test('profileRemoteOverride tolerates a missing/!object profiles map', () => {
assert.equal(profileRemoteOverride(null, 'coder'), null)
})
test('SSH remains separate from URL-shaped remote modes', () => {
assert.equal(modeIsRemoteLike('ssh'), false)
const config = { profiles: { coder: { mode: 'ssh', host: 'alice@box:2222', keyPath: '/key' } } }
assert.equal(profileRemoteOverride(config, 'coder'), null)
assert.deepEqual(profileSshOverride(config, 'coder'), {
mode: 'ssh', host: 'box', user: 'alice', port: 2222, keyPath: '/key'
})
})
test('normalizeSshConfig handles IPv6 and strict port bounds', () => {
assert.deepEqual(normalizeSshConfig({ mode: 'ssh', host: '::1', port: 22 }), {
mode: 'ssh', host: '::1'
})
assert.deepEqual(normalizeSshConfig({ mode: 'ssh', host: '[::1]:2222' }), {
mode: 'ssh', host: '::1', port: 2222
})
assert.deepEqual(normalizeSshConfig({ mode: 'ssh', host: 'box', port: '2222junk' }), {
mode: 'ssh', host: 'box'
})
assert.deepEqual(normalizeSshConfig({ mode: 'ssh', host: 'box', port: 65536 }), {
mode: 'ssh', host: 'box'
})
})
test('localProfileEntry preserves inactive SSH drafts but drops Cloud state', () => {
const ssh = { mode: 'ssh', host: 'box', user: 'alice', remoteHermesPath: '/hermes' }
assert.deepEqual(localProfileEntry(ssh), { mode: 'local', savedSsh: ssh })
assert.deepEqual(localProfileEntry({ mode: 'local', savedSsh: ssh }), {
mode: 'local', savedSsh: ssh
})
assert.equal(localProfileEntry({ mode: 'cloud', url: 'https://agent' }), null)
})
test('saved SSH drafts are inactive and explicit overrides take precedence', () => {
const saved = { mode: 'ssh', host: 'saved' }
const config: any = { profiles: { coder: { mode: 'local', savedSsh: saved } } }
assert.deepEqual(savedProfileSsh(config, 'coder'), saved)
assert.equal(profileSshOverride(config, 'coder'), null)
assert.equal(profileHasRemoteConnection(config, 'coder'), false)
config.profiles.coder = { mode: 'ssh', host: 'active' }
assert.deepEqual(profileSshOverride(config, 'coder'), { mode: 'ssh', host: 'active' })
assert.equal(profileHasRemoteConnection(config, 'coder'), true)
})
// --- pathWithGlobalRemoteProfile ---
test('pathWithGlobalRemoteProfile appends profile in global remote mode', () => {

View file

@ -170,6 +170,65 @@ function modeIsRemoteLike(mode) {
return mode === 'remote' || mode === 'cloud'
}
function normalizeSshConfig(entry) {
if (!entry || typeof entry !== 'object' || entry.mode !== 'ssh') return null
let host = String(entry.host || '').trim()
if (!host) return null
let parsedUser
let parsedPort
const at = host.indexOf('@')
if (at > 0) {
parsedUser = host.slice(0, at)
host = host.slice(at + 1)
}
const bracketed = /^\[([^\]]+)](?::(\d+))?$/.exec(host)
if (bracketed) {
host = bracketed[1]
if (bracketed[2]) parsedPort = Number(bracketed[2])
} else if ((host.match(/:/g) || []).length === 1) {
const [name, rawPort] = host.split(':')
if (/^\d+$/.test(rawPort)) {
host = name
parsedPort = Number(rawPort)
}
}
if (!host) return null
const out: any = { mode: 'ssh', host }
const user = String(entry.user || '').trim() || parsedUser || ''
if (user) out.user = user
const rawExplicitPort = String(entry.port ?? '').trim()
const explicitPort = /^\d+$/.test(rawExplicitPort) ? Number(rawExplicitPort) : null
const port = explicitPort ?? parsedPort
if (Number.isInteger(port) && port > 0 && port <= 65535 && port !== 22) out.port = port
const keyPath = String(entry.keyPath || '').trim()
if (keyPath) out.keyPath = keyPath
const remoteHermesPath = String(entry.remoteHermesPath || '').trim()
if (remoteHermesPath) out.remoteHermesPath = remoteHermesPath
return out
}
function profileSshOverride(config, profile) {
const key = connectionScopeKey(profile)
const entry = key ? config?.profiles?.[key] : null
return normalizeSshConfig(entry)
}
function savedProfileSsh(config, profile) {
const key = connectionScopeKey(profile)
const entry = key ? config?.profiles?.[key] : null
if (!entry || entry.mode !== 'local') return null
return normalizeSshConfig(entry.savedSsh)
}
function profileHasRemoteConnection(config, profile) {
return Boolean(profileRemoteOverride(config, profile) || profileSshOverride(config, profile))
}
function localProfileEntry(existing) {
const ssh = normalizeSshConfig(existing) || normalizeSshConfig(existing?.savedSsh)
return ssh ? { mode: 'local', savedSsh: ssh } : null
}
/**
* Select a profile's explicit remote override from a connection config, or null
* when it has none (so the caller falls back to env global remote local).
@ -339,10 +398,15 @@ export {
cookiesHaveSession,
modeIsRemoteLike,
normalizeRemoteBaseUrl,
normalizeSshConfig,
localProfileEntry,
normAuthMode,
pathWithGlobalRemoteProfile,
PRIVY_SESSION_COOKIE_VARIANTS,
profileHasRemoteConnection,
profileRemoteOverride,
profileSshOverride,
savedProfileSsh,
resolveAuthMode,
resolveTestWsUrl,
RT_COOKIE_VARIANTS,