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
35 changes: 24 additions & 11 deletions .github/workflows/build-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@ jobs:
id: tags
env:
DEFAULT_PKV: ${{ steps.pkv.outputs.version }}
INPUT_TAGS: ${{ inputs.tags }}
REF_NAME: ${{ github.ref_name }}
run: |
if [ -n "${{ inputs.tags }}" ]; then
IFS=',' read -ra RAW_TAGS <<< "${{ inputs.tags }}"
if [ -n "$INPUT_TAGS" ]; then
IFS=',' read -ra RAW_TAGS <<< "$INPUT_TAGS"
else
RAW_TAGS=("${{ github.ref_name }}")
RAW_TAGS=("$REF_NAME")
fi

JSON='{"include":['
Expand Down Expand Up @@ -128,8 +130,10 @@ jobs:

- name: Resolve plugin info
id: plugin
env:
MATRIX_TAG: ${{ matrix.tag }}
run: |
TAG="${{ matrix.tag }}"
TAG="$MATRIX_TAG"
PLUGIN_NAME=$(echo "$TAG" | sed -E 's/^plugin-([a-z0-9-]+)-v([0-9].*)$/\1/')
VERSION=$(echo "$TAG" | sed -E 's/^plugin-([a-z0-9-]+)-v([0-9].*)$/\2/')

Expand Down Expand Up @@ -270,9 +274,11 @@ jobs:
./scripts/build-plugin.sh "${{ steps.plugin.outputs.target }}" x86_64 "${{ steps.plugin.outputs.version }}"

- name: Verify built PluginKit version matches the release label
env:
MATRIX_PKV: ${{ matrix.pluginKitVersion }}
run: |
BUNDLE_NAME="${{ steps.plugin.outputs.bundleName }}"
EXPECTED="${{ matrix.pluginKitVersion }}"
EXPECTED="$MATRIX_PKV"
for ARCH in arm64 x86_64; do
WORK=$(mktemp -d)
unzip -oq "build/Plugins/${BUNDLE_NAME}-${ARCH}.zip" -d "$WORK"
Expand Down Expand Up @@ -311,14 +317,16 @@ jobs:
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
MATRIX_TAG: ${{ matrix.tag }}
MATRIX_PKV: ${{ matrix.pluginKitVersion }}
run: |
TAG="${{ matrix.tag }}"
TAG="$MATRIX_TAG"
DISPLAY_NAME="${{ steps.plugin.outputs.displayName }}"
VERSION="${{ steps.plugin.outputs.version }}"
BUNDLE_NAME="${{ steps.plugin.outputs.bundleName }}"
ARM64_SHA="${{ steps.sha.outputs.arm64 }}"
X86_SHA="${{ steps.sha.outputs.x86_64 }}"
PKV="${{ matrix.pluginKitVersion }}"
PKV="$MATRIX_PKV"

RELEASE_BODY="## $DISPLAY_NAME v$VERSION

Expand Down Expand Up @@ -346,10 +354,13 @@ jobs:
build/Plugins/${BUNDLE_NAME}-x86_64.zip

- name: Verify published assets match the PluginKit label
env:
MATRIX_TAG: ${{ matrix.tag }}
MATRIX_PKV: ${{ matrix.pluginKitVersion }}
run: |
BUNDLE_NAME="${{ steps.plugin.outputs.bundleName }}"
TAG="${{ matrix.tag }}"
PKV="${{ matrix.pluginKitVersion }}"
TAG="$MATRIX_TAG"
PKV="$MATRIX_PKV"
REPO="${{ github.repository }}"
for ARCH in arm64 x86_64; do
URL="https://github.com/${REPO}/releases/download/${TAG}/${BUNDLE_NAME}-${ARCH}.zip"
Expand Down Expand Up @@ -386,8 +397,10 @@ jobs:
env:
REGISTRY_DEPLOY_KEY: ${{ secrets.REGISTRY_DEPLOY_KEY }}
GH_TOKEN: ${{ github.token }}
MATRIX_TAG: ${{ matrix.tag }}
MATRIX_PKV: ${{ matrix.pluginKitVersion }}
run: |
TAG="${{ matrix.tag }}"
TAG="$MATRIX_TAG"
BUNDLE_NAME="${{ steps.plugin.outputs.bundleName }}"
BUNDLE_ID="${{ steps.plugin.outputs.bundleId }}"
DISPLAY_NAME="${{ steps.plugin.outputs.displayName }}"
Expand All @@ -400,7 +413,7 @@ jobs:
CATEGORY="${{ steps.plugin.outputs.category }}"
ARM64_SHA="${{ steps.sha.outputs.arm64 }}"
X86_SHA="${{ steps.sha.outputs.x86_64 }}"
PKV="${{ matrix.pluginKitVersion }}"
PKV="$MATRIX_PKV"
REPO="${{ github.repository }}"
ARM64_URL="https://github.com/${REPO}/releases/download/${TAG}/${BUNDLE_NAME}-arm64.zip"
X86_64_URL="https://github.com/${REPO}/releases/download/${TAG}/${BUNDLE_NAME}-x86_64.zip"
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- iCloud Sync between the iPhone and Mac apps: the iOS app now uses the Production CloudKit environment, so a development build no longer syncs into a separate database the Mac never reads.
- Exports no longer fail mid-table on servers that enforce a statement time limit; the export session disables the limit and restores it afterwards, the same way mysqldump does. (#1633)

### Security

- Imported connections from a deep link or shared file can no longer carry a pre-connect script that runs a shell command on connect.
- External database links now ask for confirmation before connecting, and a password in the link is never saved to the Keychain.
- MCP tools now enforce each connection's external access level, per-connection AI policy, and token connection scope on every request.
- The MCP server now requires a paired token by default, even over loopback.
- An installed plugin's code signature is re-checked right before it loads, so the binary cannot be swapped after the first check.
- MongoDB filter values in the Contains, Not Contains, Starts With, Ends With, and Regex operators can no longer inject query operators.
- iOS validates TLS certificates for MySQL, PostgreSQL, and Redis connections set to a verify SSL mode.
- Database values copied on iOS stay on the device and clear from the clipboard after a minute.
- The iOS home screen widget no longer stores database host and port on disk.

## [0.50.0] - 2026-06-09

### Added
Expand Down
14 changes: 9 additions & 5 deletions Plugins/MongoDBDriverPlugin/MongoDBQueryBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,13 @@ struct MongoDBQueryBuilder {
case "<=":
return "\"\(field)\": {\"$lte\": \(jsonValue(value))}"
case "CONTAINS":
return "\"\(field)\": {\"$regex\": \"\(escapeRegexChars(value))\", \"$options\": \"i\"}"
return "\"\(field)\": \(Self.regexBody(pattern: escapeRegexChars(value)))"
case "NOT CONTAINS":
return "\"\(field)\": {\"$not\": {\"$regex\": \"\(escapeRegexChars(value))\", \"$options\": \"i\"}}"
return "\"\(field)\": {\"$not\": \(Self.regexBody(pattern: escapeRegexChars(value)))}"
case "STARTS WITH":
return "\"\(field)\": {\"$regex\": \"^\(escapeRegexChars(value))\", \"$options\": \"i\"}"
return "\"\(field)\": \(Self.regexBody(pattern: "^\(escapeRegexChars(value))"))"
case "ENDS WITH":
return "\"\(field)\": {\"$regex\": \"\(escapeRegexChars(value))$\", \"$options\": \"i\"}"
return "\"\(field)\": \(Self.regexBody(pattern: "\(escapeRegexChars(value))$"))"
case "IS NULL":
return "\"\(field)\": null"
case "IS NOT NULL":
Expand All @@ -134,7 +134,7 @@ struct MongoDBQueryBuilder {
case "IS NOT EMPTY":
return "\"\(field)\": {\"$ne\": \"\"}"
case "REGEX":
return "\"\(field)\": {\"$regex\": \"\(value)\", \"$options\": \"i\"}"
return "\"\(field)\": \(Self.regexBody(pattern: value))"
case "IN":
let items = value.split(separator: ",")
.map { jsonValue(String($0).trimmingCharacters(in: .whitespaces)) }
Expand Down Expand Up @@ -178,6 +178,10 @@ struct MongoDBQueryBuilder {
return "\"\(Self.escapeJsonString(value))\""
}

private static func regexBody(pattern: String) -> String {
"{\"$regex\": \"\(escapeJsonString(pattern))\", \"$options\": \"i\"}"
}

static func escapeJsonString(_ value: String) -> String {
var result = ""
result.reserveCapacity((value as NSString).length)
Expand Down
12 changes: 10 additions & 2 deletions TablePro/Core/Database/DatabaseManager+EnsureConnected.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ import Foundation
import os

extension DatabaseManager {
func ensureConnected(_ connection: DatabaseConnection) async throws {
func ensureConnected(
_ connection: DatabaseConnection,
passwordOverride: String? = nil,
sshPasswordOverride: String? = nil
) async throws {
if activeSessions[connection.id]?.driver != nil { return }
try await ensureConnectedDedup.execute(key: connection.id) {
try await self.connectToSession(connection)
try await self.connectToSession(
connection,
passwordOverride: passwordOverride,
sshPasswordOverride: sshPasswordOverride
)
}
}

Expand Down
15 changes: 11 additions & 4 deletions TablePro/Core/Database/DatabaseManager+Sessions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import TableProPluginKit
// MARK: - Session Management

extension DatabaseManager {
func connectToSession(_ requestedConnection: DatabaseConnection) async throws {
func connectToSession(
_ requestedConnection: DatabaseConnection,
passwordOverride incomingPasswordOverride: String? = nil,
sshPasswordOverride: String? = nil
) async throws {
let connection = resolvedConnectionDefinition(for: requestedConnection)

if let existing = activeSessions[connection.id], existing.driver != nil {
Expand All @@ -40,7 +44,10 @@ extension DatabaseManager {

let effectiveConnection: DatabaseConnection
do {
effectiveConnection = try await buildEffectiveConnection(for: resolvedConnection)
effectiveConnection = try await buildEffectiveConnection(
for: resolvedConnection,
sshPasswordOverride: sshPasswordOverride
)
} catch {
finalizeConnectionFailure(for: connection.id, cancelled: Task.isCancelled)
throw error
Expand All @@ -57,8 +64,8 @@ extension DatabaseManager {
}
}

var passwordOverride: String?
if connection.promptForPassword, !pluginManager.hidesPassword(for: connection) {
var passwordOverride: String? = incomingPasswordOverride
if passwordOverride == nil, connection.promptForPassword, !pluginManager.hidesPassword(for: connection) {
if let cached = activeSessions[connection.id]?.cachedPassword {
passwordOverride = cached
} else {
Expand Down
5 changes: 5 additions & 0 deletions TablePro/Core/MCP/Auth/MCPBearerTokenAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@ public struct MCPValidatedToken: Sendable, Equatable {
public let tokenId: UUID
public let label: String?
public let scopes: Set<MCPScope>
public let connectionAccess: ConnectionAccess
public let issuedAt: Date
public let expiresAt: Date?

public init(
tokenId: UUID,
label: String?,
scopes: Set<MCPScope>,
connectionAccess: ConnectionAccess = .all,
issuedAt: Date,
expiresAt: Date?
) {
self.tokenId = tokenId
self.label = label
self.scopes = scopes
self.connectionAccess = connectionAccess
self.issuedAt = issuedAt
self.expiresAt = expiresAt
}
Expand Down Expand Up @@ -51,6 +54,7 @@ internal extension MCPTokenStore {
tokenId: authToken.id,
label: authToken.name,
scopes: Self.mcpScopes(for: authToken.permissions),
connectionAccess: authToken.connectionAccess,
issuedAt: authToken.createdAt,
expiresAt: authToken.expiresAt
)
Expand Down Expand Up @@ -160,6 +164,7 @@ public actor MCPBearerTokenAuthenticator: MCPAuthenticator {
tokenFingerprint: fingerprint,
tokenId: validated.tokenId,
scopes: validated.scopes,
connectionAccess: validated.connectionAccess,
metadata: MCPPrincipalMetadata(
label: validated.label,
issuedAt: validated.issuedAt,
Expand Down
1 change: 1 addition & 0 deletions TablePro/Core/MCP/Auth/MCPCompositeAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public actor MCPCompositeAuthenticator: MCPAuthenticator {
tokenFingerprint: "anonymous-loopback",
tokenId: nil,
scopes: [.toolsRead, .toolsWrite, .resourcesRead, .admin],
connectionAccess: .all,
metadata: MCPPrincipalMetadata(
label: "Anonymous (loopback)",
issuedAt: .distantPast,
Expand Down
4 changes: 4 additions & 0 deletions TablePro/Core/MCP/Auth/MCPPrincipal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,28 @@ public struct MCPPrincipal: Sendable, Equatable, Hashable {
public let tokenFingerprint: String
public let tokenId: UUID?
public let scopes: Set<MCPScope>
public let connectionAccess: ConnectionAccess
public let metadata: MCPPrincipalMetadata

public init(
tokenFingerprint: String,
tokenId: UUID? = nil,
scopes: Set<MCPScope>,
connectionAccess: ConnectionAccess = .all,
metadata: MCPPrincipalMetadata
) {
self.tokenFingerprint = tokenFingerprint
self.tokenId = tokenId
self.scopes = scopes
self.connectionAccess = connectionAccess
self.metadata = metadata
}

public static func == (lhs: MCPPrincipal, rhs: MCPPrincipal) -> Bool {
lhs.tokenFingerprint == rhs.tokenFingerprint
&& lhs.tokenId == rhs.tokenId
&& lhs.scopes == rhs.scopes
&& lhs.connectionAccess == rhs.connectionAccess
&& lhs.metadata == rhs.metadata
}

Expand Down
Loading
Loading