Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- SSH agent connections failing when socket path contains `~` (e.g., 1Password agent)
- Keychain authorization prompt no longer appears on every table open

## [0.18.1] - 2026-03-14
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Core/SSH/Auth/AgentAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal struct AgentAuthenticator: SSHAuthenticator {
let originalSocketPath = ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"]
let needsSocketOverride = socketPath != nil

if let overridePath = socketPath, needsSocketOverride {
if let overridePath = socketPath.map(SSHPathUtilities.expandTilde), needsSocketOverride {
Self.agentSocketLock.lock()
Self.logger.debug("Using custom SSH agent socket: \(overridePath, privacy: .private)")
setenv("SSH_AUTH_SOCK", overridePath, 1)
Expand Down
15 changes: 1 addition & 14 deletions TablePro/Core/SSH/Auth/PublicKeyAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal struct PublicKeyAuthenticator: SSHAuthenticator {
let passphrase: String?

func authenticate(session: OpaquePointer, username: String) throws {
let expandedPath = expandPath(privateKeyPath)
let expandedPath = SSHPathUtilities.expandTilde(privateKeyPath)

guard FileManager.default.fileExists(atPath: expandedPath) else {
throw SSHTunnelError.tunnelCreationFailed(
Expand Down Expand Up @@ -53,17 +53,4 @@ internal struct PublicKeyAuthenticator: SSHAuthenticator {
throw SSHTunnelError.authenticationFailed
}
}

private func expandPath(_ path: String) -> String {
if path.hasPrefix("~/") {
return FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(String(path.dropFirst(2)))
.path(percentEncoded: false)
}
if path == "~" {
return FileManager.default.homeDirectoryForCurrentUser
.path(percentEncoded: false)
}
return path
}
}
18 changes: 4 additions & 14 deletions TablePro/Core/SSH/SSHConfigParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ final class SSHConfigParser {
hostname: currentHostname,
port: currentPort,
user: currentUser,
identityFile: expandPath(currentIdentityFile),
identityAgent: expandPath(currentIdentityAgent),
identityFile: currentIdentityFile.map(SSHPathUtilities.expandTilde),
identityAgent: currentIdentityAgent.map(SSHPathUtilities.expandTilde),
proxyJump: currentProxyJump
))
}
Expand Down Expand Up @@ -133,8 +133,8 @@ final class SSHConfigParser {
hostname: currentHostname,
port: currentPort,
user: currentUser,
identityFile: expandPath(currentIdentityFile),
identityAgent: expandPath(currentIdentityAgent),
identityFile: currentIdentityFile.map(SSHPathUtilities.expandTilde),
identityAgent: currentIdentityAgent.map(SSHPathUtilities.expandTilde),
proxyJump: currentProxyJump
))
}
Expand Down Expand Up @@ -192,14 +192,4 @@ final class SSHConfigParser {
return jumpHosts
}

/// Expand ~ to home directory in path
private static func expandPath(_ path: String?) -> String? {
guard let path = path else { return nil }

if path.hasPrefix("~") {
return FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(String(path.dropFirst(2))).path(percentEncoded: false)
}
return path
}
}
23 changes: 23 additions & 0 deletions TablePro/Core/SSH/SSHPathUtilities.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// SSHPathUtilities.swift
// TablePro
//

import Foundation

enum SSHPathUtilities {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add explicit access control to the enum declaration.

The coding guidelines require explicit access control on types. Since this utility is used across multiple modules within the SSH subsystem, consider adding internal (or public if needed externally).

Proposed fix
-enum SSHPathUtilities {
+internal enum SSHPathUtilities {

As per coding guidelines: "Always specify access control explicitly (private, internal, public) on extensions and types."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
enum SSHPathUtilities {
internal enum SSHPathUtilities {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Core/SSH/SSHPathUtilities.swift` at line 8, The enum declaration for
SSHPathUtilities lacks explicit access control; update the declaration of enum
SSHPathUtilities to include the appropriate access level (e.g., internal or
public depending on cross-module usage) so the type follows the project's coding
guidelines requiring explicit access control.

/// Expand ~ to the current user's home directory in a path.
/// Unlike shell commands, `setenv()` and file APIs do not expand `~` automatically.
static func expandTilde(_ path: String) -> String {
if path.hasPrefix("~/") {
return FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(String(path.dropFirst(2)))
.path(percentEncoded: false)
}
if path == "~" {
return FileManager.default.homeDirectoryForCurrentUser
.path(percentEncoded: false)
}
return path
}
}
28 changes: 28 additions & 0 deletions TableProTests/Core/SSH/SSHConfigurationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,34 @@ struct SSHConfigurationTests {
#expect(config.isValid == false)
}

// MARK: - SSHPathUtilities

@Test("Tilde expansion resolves ~/path to home directory")
func testTildeExpansionWithSubpath() {
let home = FileManager.default.homeDirectoryForCurrentUser.path(percentEncoded: false)
let result = SSHPathUtilities.expandTilde("~/Library/agent.sock")
#expect(result == "\(home)/Library/agent.sock")
}

@Test("Tilde expansion resolves bare ~ to home directory")
func testTildeExpansionBare() {
let home = FileManager.default.homeDirectoryForCurrentUser.path(percentEncoded: false)
let result = SSHPathUtilities.expandTilde("~")
#expect(result == home)
}

@Test("Tilde expansion leaves absolute paths unchanged")
func testTildeExpansionAbsolutePath() {
let result = SSHPathUtilities.expandTilde("/absolute/path")
#expect(result == "/absolute/path")
}

@Test("Tilde expansion leaves empty string unchanged")
func testTildeExpansionEmptyString() {
let result = SSHPathUtilities.expandTilde("")
#expect(result == "")
}

@Test("Backward-compatible decoding without jumpHosts key")
func testBackwardCompatibleDecoding() throws {
let jsonString = """
Expand Down
Loading