Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 02f35c1

Browse files
committedJan 14, 2025
chore: extract CoderSDK to framework
1 parent 46c2c09 commit 02f35c1

File tree

21 files changed

+774
-325
lines changed

21 files changed

+774
-325
lines changed
 

Diff for: ‎Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj

+342-59
Large diffs are not rendered by default.

Diff for: ‎Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

+1-10
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
{
2-
"originHash" : "1cd4f7368eeddbaa35ef829e13093bc7081a4e6d3da9492d22db0757464ad473",
2+
"originHash" : "ec40e522ec1a2416e8e8f5cbe97424ab3e4a614e6ef453c10ea28e84e88b6771",
33
"pins" : [
4-
{
5-
"identity" : "alamofire",
6-
"kind" : "remoteSourceControl",
7-
"location" : "https://github.com/Alamofire/Alamofire",
8-
"state" : {
9-
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
10-
"version" : "5.10.2"
11-
}
12-
},
134
{
145
"identity" : "fluid-menu-bar-extra",
156
"kind" : "remoteSourceControl",

Diff for: ‎Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/Coder Desktop.xcscheme

+11
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@
7979
ReferencedContainer = "container:Coder Desktop.xcodeproj">
8080
</BuildableReference>
8181
</TestableReference>
82+
<TestableReference
83+
skipped = "NO"
84+
parallelizable = "YES">
85+
<BuildableReference
86+
BuildableIdentifier = "primary"
87+
BlueprintIdentifier = "AA3B40972D2FC8560099996A"
88+
BuildableName = "CoderSDKTests.xctest"
89+
BlueprintName = "CoderSDKTests"
90+
ReferencedContainer = "container:Coder Desktop.xcodeproj">
91+
</BuildableReference>
92+
</TestableReference>
8293
</Testables>
8394
</TestAction>
8495
<LaunchAction

Diff for: ‎Coder Desktop/Coder Desktop.xctestplan

+13-6
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,17 @@
1919
{
2020
"target" : {
2121
"containerPath" : "container:Coder Desktop.xcodeproj",
22-
"identifier" : "9616790E2CFF100E00B2B6DF",
23-
"name" : "Coder DesktopTests"
22+
"identifier" : "AA3B40972D2FC8560099996A",
23+
"name" : "CoderSDKTests"
24+
}
25+
},
26+
{
27+
"enabled" : false,
28+
"parallelizable" : true,
29+
"target" : {
30+
"containerPath" : "container:Coder Desktop.xcodeproj",
31+
"identifier" : "961679182CFF100E00B2B6DF",
32+
"name" : "Coder DesktopUITests"
2433
}
2534
},
2635
{
@@ -31,12 +40,10 @@
3140
}
3241
},
3342
{
34-
"enabled" : false,
35-
"parallelizable" : true,
3643
"target" : {
3744
"containerPath" : "container:Coder Desktop.xcodeproj",
38-
"identifier" : "961679182CFF100E00B2B6DF",
39-
"name" : "Coder DesktopUITests"
45+
"identifier" : "9616790E2CFF100E00B2B6DF",
46+
"name" : "Coder DesktopTests"
4047
}
4148
}
4249
],

Diff for: ‎Coder Desktop/Coder Desktop/Coder_DesktopApp.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ struct DesktopApp: App {
1111
EmptyView()
1212
}
1313
Window("Sign In", id: Windows.login.rawValue) {
14-
LoginForm<PreviewClient, PreviewSession>()
14+
LoginForm<PreviewSession>()
1515
}.environmentObject(appDelegate.session)
1616
.windowResizability(.contentSize)
1717
}

Diff for: ‎Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift

-29
This file was deleted.

Diff for: ‎Coder Desktop/Coder Desktop/SDK/Client.swift

-140
This file was deleted.

Diff for: ‎Coder Desktop/Coder Desktop/SDK/User.swift

-37
This file was deleted.

Diff for: ‎Coder Desktop/Coder Desktop/Views/LoginForm.swift

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import CoderSDK
12
import SwiftUI
23

3-
struct LoginForm<C: Client, S: Session>: View {
4+
struct LoginForm<S: Session>: View {
45
@EnvironmentObject var session: S
56
@Environment(\.dismiss) private var dismiss
67

@@ -69,7 +70,7 @@ struct LoginForm<C: Client, S: Session>: View {
6970
}
7071
loading = true
7172
defer { loading = false }
72-
let client = C(url: url, token: sessionToken)
73+
let client = Client(url: url, token: sessionToken)
7374
do {
7475
_ = try await client.user("me")
7576
} catch {
@@ -188,6 +189,6 @@ enum LoginField: Hashable {
188189
}
189190

190191
#Preview {
191-
LoginForm<PreviewClient, PreviewSession>()
192+
LoginForm<PreviewSession>()
192193
.environmentObject(PreviewSession())
193194
}

Diff for: ‎Coder Desktop/Coder DesktopTests/LoginFormTests.swift

+34-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
@testable import Coder_Desktop
2+
@testable import CoderSDK
3+
import Mocker
24
import SwiftUI
35
import Testing
46
import ViewInspector
@@ -7,12 +9,12 @@ import ViewInspector
79
@Suite(.timeLimit(.minutes(1)))
810
struct LoginTests {
911
let session: MockSession
10-
let sut: LoginForm<MockClient, MockSession>
12+
let sut: LoginForm<MockSession>
1113
let view: any View
1214

1315
init() {
1416
session = MockSession()
15-
sut = LoginForm<MockClient, MockSession>()
17+
sut = LoginForm<MockSession>()
1618
view = sut.environmentObject(session)
1719
}
1820

@@ -68,14 +70,16 @@ struct LoginTests {
6870

6971
@Test
7072
func testFailedAuthentication() async throws {
71-
let login = LoginForm<MockErrorClient, MockSession>()
73+
let login = LoginForm<MockSession>()
74+
let url = URL(string: "https://testFailedAuthentication.com")!
75+
Mock(url: url.appendingPathComponent("/api/v2/users/me"), statusCode: 401, data: [.get: Data()]).register()
7276

7377
try await ViewHosting.host(login.environmentObject(session)) {
7478
try await login.inspection.inspect { view in
75-
try view.find(ViewType.TextField.self).setInput("https://coder.example.com")
79+
try view.find(ViewType.TextField.self).setInput(url.absoluteString)
7680
try view.find(button: "Next").tap()
7781
#expect(throws: Never.self) { try view.find(text: "Session Token") }
78-
try view.find(ViewType.SecureField.self).setInput("valid-token")
82+
try view.find(ViewType.SecureField.self).setInput("invalid-token")
7983
try await view.actualView().submit()
8084
#expect(throws: Never.self) { try view.find(ViewType.Alert.self) }
8185
}
@@ -84,9 +88,33 @@ struct LoginTests {
8488

8589
@Test
8690
func testSuccessfulLogin() async throws {
91+
let url = URL(string: "https://testSuccessfulLogin.com")!
92+
93+
let user = User(
94+
id: UUID(),
95+
username: "admin",
96+
avatar_url: "",
97+
name: "admin",
98+
email: "admin@coder.com",
99+
created_at: Date.now,
100+
updated_at: Date.now,
101+
last_seen_at: Date.now,
102+
status: "active",
103+
login_type: "none",
104+
theme_preference: "dark",
105+
organization_ids: [],
106+
roles: []
107+
)
108+
109+
try Mock(
110+
url: url.appendingPathComponent("/api/v2/users/me"),
111+
statusCode: 200,
112+
data: [.get: Client.encoder.encode(user)]
113+
).register()
114+
87115
try await ViewHosting.host(view) {
88116
try await sut.inspection.inspect { view in
89-
try view.find(ViewType.TextField.self).setInput("https://coder.example.com")
117+
try view.find(ViewType.TextField.self).setInput(url.absoluteString)
90118
try view.find(button: "Next").tap()
91119
try view.find(ViewType.SecureField.self).setInput("valid-token")
92120
try await view.actualView().submit()

Diff for: ‎Coder Desktop/Coder DesktopTests/Util.swift

+1-30
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class MockVPNService: VPNService, ObservableObject {
2424

2525
class MockSession: Session {
2626
@Published
27-
var hasSession: Bool = true
27+
var hasSession: Bool = false
2828
@Published
2929
var sessionToken: String? = "fake-token"
3030
@Published
@@ -43,33 +43,4 @@ class MockSession: Session {
4343
}
4444
}
4545

46-
struct MockClient: Client {
47-
init(url _: URL, token _: String? = nil) {}
48-
49-
func user(_: String) async throws(ClientError) -> Coder_Desktop.User {
50-
User(
51-
id: UUID(),
52-
username: "admin",
53-
avatar_url: "",
54-
name: "admin",
55-
email: "admin@coder.com",
56-
created_at: Date.now,
57-
updated_at: Date.now,
58-
last_seen_at: Date.now,
59-
status: "active",
60-
login_type: "none",
61-
theme_preference: "dark",
62-
organization_ids: [],
63-
roles: []
64-
)
65-
}
66-
}
67-
68-
struct MockErrorClient: Client {
69-
init(url _: URL, token _: String?) {}
70-
func user(_: String) async throws(ClientError) -> Coder_Desktop.User {
71-
throw .reqError(.explicitlyCancelled)
72-
}
73-
}
74-
7546
extension Inspection: @unchecked Sendable, @retroactive InspectionEmissary {}

Diff for: ‎Coder Desktop/Coder DesktopTests/VPNMenuTests.swift

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ struct VPNMenuTests {
104104

105105
@Test
106106
func testOffWhenFailed() async throws {
107+
session.hasSession = true
107108
try await ViewHosting.host(view) {
108109
try await sut.inspection.inspect { view in
109110
let toggle = try view.find(ViewType.Toggle.self)

Diff for: ‎Coder Desktop/CoderSDK/Client.swift

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import Foundation
2+
3+
public struct Client {
4+
public let url: URL
5+
public var token: String?
6+
7+
public init(url: URL, token: String? = nil) {
8+
self.url = url
9+
self.token = token
10+
}
11+
12+
static let decoder: JSONDecoder = {
13+
var dec = JSONDecoder()
14+
dec.dateDecodingStrategy = .iso8601withOptionalFractionalSeconds
15+
return dec
16+
}()
17+
18+
static let encoder: JSONEncoder = {
19+
var enc = JSONEncoder()
20+
enc.dateEncodingStrategy = .iso8601withFractionalSeconds
21+
return enc
22+
}()
23+
24+
func request<T: Encodable & Sendable>(
25+
_ path: String,
26+
method: HTTPMethod,
27+
body: T? = nil
28+
) async throws(ClientError) -> HTTPResponse {
29+
let url = self.url.appendingPathComponent(path)
30+
var req = URLRequest(url: url)
31+
if let token { req.addValue(token, forHTTPHeaderField: Headers.sessionToken) }
32+
req.httpMethod = method.rawValue
33+
do {
34+
if let body { req.httpBody = try Client.encoder.encode(body) }
35+
} catch {
36+
throw .encodeFailure
37+
}
38+
let data: Data
39+
let resp: URLResponse
40+
do {
41+
(data, resp) = try await URLSession.shared.data(for: req)
42+
} catch {
43+
throw .network(error)
44+
}
45+
guard let httpResponse = resp as? HTTPURLResponse else {
46+
throw .unexpectedResponse(data)
47+
}
48+
return HTTPResponse(resp: httpResponse, data: data, req: req)
49+
}
50+
51+
func request(
52+
_ path: String,
53+
method: HTTPMethod
54+
) async throws(ClientError) -> HTTPResponse {
55+
let url = self.url.appendingPathComponent(path)
56+
var req = URLRequest(url: url)
57+
if let token { req.addValue(token, forHTTPHeaderField: Headers.sessionToken) }
58+
req.httpMethod = method.rawValue
59+
let data: Data
60+
let resp: URLResponse
61+
do {
62+
(data, resp) = try await URLSession.shared.data(for: req)
63+
} catch {
64+
throw .network(error)
65+
}
66+
guard let httpResponse = resp as? HTTPURLResponse else {
67+
throw .unexpectedResponse(data)
68+
}
69+
return HTTPResponse(resp: httpResponse, data: data, req: req)
70+
}
71+
72+
func responseAsError(_ resp: HTTPResponse) -> ClientError {
73+
do {
74+
let body = try Client.decoder.decode(Response.self, from: resp.data)
75+
let out = APIError(
76+
response: body,
77+
statusCode: resp.resp.statusCode,
78+
method: resp.req.httpMethod!,
79+
url: resp.req.url!
80+
)
81+
return .api(out)
82+
} catch {
83+
return .unexpectedResponse(resp.data.prefix(1024))
84+
}
85+
}
86+
}
87+
88+
public struct APIError: Decodable {
89+
let response: Response
90+
let statusCode: Int
91+
let method: String
92+
let url: URL
93+
94+
var description: String {
95+
var components = ["\(method) \(url.absoluteString)\nUnexpected status code \(statusCode):\n\(response.message)"]
96+
if let detail = response.detail {
97+
components.append("\tError: \(detail)")
98+
}
99+
if let validations = response.validations, !validations.isEmpty {
100+
let validationMessages = validations.map { "\t\($0.field): \($0.detail)" }
101+
components.append(contentsOf: validationMessages)
102+
}
103+
return components.joined(separator: "\n")
104+
}
105+
}
106+
107+
public struct Response: Decodable {
108+
let message: String
109+
let detail: String?
110+
let validations: [FieldValidation]?
111+
}
112+
113+
public struct FieldValidation: Decodable {
114+
let field: String
115+
let detail: String
116+
}
117+
118+
public enum ClientError: Error {
119+
case api(APIError)
120+
case network(any Error)
121+
case unexpectedResponse(Data)
122+
case encodeFailure
123+
124+
public var description: String {
125+
switch self {
126+
case let .api(error):
127+
return error.description
128+
case let .network(error):
129+
return error.localizedDescription
130+
case let .unexpectedResponse(data):
131+
return "Unexpected or non HTTP response: \(data)"
132+
case .encodeFailure:
133+
return "Failed to encode body"
134+
}
135+
}
136+
}

Diff for: ‎Coder Desktop/CoderSDK/CoderSDK.h

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#import <Foundation/Foundation.h>
2+
3+
//! Project version number for CoderSDK.
4+
FOUNDATION_EXPORT double CoderSDKVersionNumber;
5+
6+
//! Project version string for CoderSDK.
7+
FOUNDATION_EXPORT const unsigned char CoderSDKVersionString[];
8+
9+
// In this header, you should import all the public headers of your framework using statements like #import <CoderSDK/PublicHeader.h>
10+
11+

Diff for: ‎Coder Desktop/Coder Desktop/SDK/Date.swift renamed to ‎Coder Desktop/CoderSDK/Date.swift

+6
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,9 @@ extension JSONEncoder.DateEncodingStrategy {
2828
try container.encode($0.formatted(.iso8601withFractionalSeconds))
2929
}
3030
}
31+
32+
public extension Date {
33+
static func == (lhs: Date, rhs: Date) -> Bool {
34+
abs(lhs.timeIntervalSince1970 - rhs.timeIntervalSince1970) < 0.001
35+
}
36+
}

Diff for: ‎Coder Desktop/CoderSDK/Deployment.swift

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
public extension Client {
2+
func buildInfo() async throws(ClientError) -> BuildInfoResponse {
3+
let res = try await request("/api/v2/buildinfo", method: .get)
4+
guard res.resp.statusCode == 200 else {
5+
throw responseAsError(res)
6+
}
7+
do {
8+
return try Client.decoder.decode(BuildInfoResponse.self, from: res.data)
9+
} catch {
10+
throw .unexpectedResponse(res.data.prefix(1024))
11+
}
12+
}
13+
}
14+
15+
public struct BuildInfoResponse: Encodable, Decodable, Equatable, Sendable {
16+
public let external_url: String
17+
public let version: String
18+
public let dashboard_url: String
19+
public let telemetry: Bool
20+
public let workspace_proxy: Bool
21+
public let agent_api_version: String
22+
public let provisioner_api_version: String
23+
public let upgrade_message: String
24+
public let deployment_id: String
25+
26+
// `version` in the form `[0-9]+.[0-9]+.[0-9]+`
27+
public var semver: String? {
28+
return try? NSRegularExpression(pattern: #"v(\d+\.\d+\.\d+)"#)
29+
.firstMatch(in: version, range: NSRange(version.startIndex ..< version.endIndex, in: version))
30+
.flatMap { Range($0.range(at: 1), in: version).map { String(version[$0]) } }
31+
}
32+
}

Diff for: ‎Coder Desktop/CoderSDK/HTTP.swift

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
public struct HTTPResponse {
2+
let resp: HTTPURLResponse
3+
let data: Data
4+
let req: URLRequest
5+
}
6+
7+
enum HTTPMethod: String, Equatable, Hashable, Sendable {
8+
case get = "GET"
9+
case post = "POST"
10+
case delete = "DELETE"
11+
case put = "PUT"
12+
case head = "HEAD"
13+
}
14+
15+
enum Headers {
16+
static let sessionToken = "Coder-Session-Token"
17+
}

Diff for: ‎Coder Desktop/CoderSDK/User.swift

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import Foundation
2+
3+
public extension Client {
4+
func user(_ ident: String) async throws(ClientError) -> User {
5+
let res = try await request("/api/v2/users/\(ident)", method: .get)
6+
guard res.resp.statusCode == 200 else {
7+
throw responseAsError(res)
8+
}
9+
do {
10+
return try Client.decoder.decode(User.self, from: res.data)
11+
} catch {
12+
throw .unexpectedResponse(res.data.prefix(1024))
13+
}
14+
}
15+
}
16+
17+
public struct User: Encodable, Decodable, Equatable, Sendable {
18+
public let id: UUID
19+
public let username: String
20+
public let avatar_url: String
21+
public let name: String
22+
public let email: String
23+
public let created_at: Date
24+
public let updated_at: Date
25+
public let last_seen_at: Date
26+
public let status: String
27+
public let login_type: String
28+
public let theme_preference: String
29+
public let organization_ids: [UUID]
30+
public let roles: [Role]
31+
32+
public init(
33+
id: UUID,
34+
username: String,
35+
avatar_url: String,
36+
name: String,
37+
email: String,
38+
created_at: Date,
39+
updated_at: Date,
40+
last_seen_at: Date,
41+
status: String,
42+
login_type: String,
43+
theme_preference: String,
44+
organization_ids: [UUID],
45+
roles: [Role]
46+
) {
47+
self.id = id
48+
self.username = username
49+
self.avatar_url = avatar_url
50+
self.name = name
51+
self.email = email
52+
self.created_at = created_at
53+
self.updated_at = updated_at
54+
self.last_seen_at = last_seen_at
55+
self.status = status
56+
self.login_type = login_type
57+
self.theme_preference = theme_preference
58+
self.organization_ids = organization_ids
59+
self.roles = roles
60+
}
61+
}
62+
63+
public struct Role: Encodable, Decodable, Equatable, Sendable {
64+
public let name: String
65+
public let display_name: String
66+
public let organization_id: UUID?
67+
68+
public init(name: String, display_name: String, organization_id: UUID?) {
69+
self.name = name
70+
self.display_name = display_name
71+
self.organization_id = organization_id
72+
}
73+
}

Diff for: ‎Coder Desktop/CoderSDKTests/CoderSDKTests.swift

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
@testable import CoderSDK
2+
import Mocker
3+
import Testing
4+
5+
@Suite(.timeLimit(.minutes(1)))
6+
struct CoderSDKTests {
7+
@Test
8+
func user() async throws {
9+
let now = Date.now
10+
let user = User(
11+
id: UUID(),
12+
username: "johndoe",
13+
avatar_url: "https://example.com/img.png",
14+
name: "John Doe",
15+
email: "john.doe@example.com",
16+
created_at: now,
17+
updated_at: now,
18+
last_seen_at: now,
19+
status: "active",
20+
login_type: "email",
21+
theme_preference: "dark",
22+
organization_ids: [UUID()],
23+
roles: [
24+
Role(name: "user", display_name: "User", organization_id: UUID()),
25+
]
26+
)
27+
28+
let url = URL(string: "https://example.com")!
29+
let token = "fake-token"
30+
let client = Client(url: url, token: token)
31+
var mock = try Mock(
32+
url: url.appending(path: "api/v2/users/johndoe"),
33+
contentType: .json,
34+
statusCode: 200,
35+
data: [.get: Client.encoder.encode(user)]
36+
)
37+
var tokenSent = false
38+
mock.onRequestHandler = OnRequestHandler { req in
39+
tokenSent = req.value(forHTTPHeaderField: Headers.sessionToken) == token
40+
}
41+
mock.register()
42+
43+
let retUser = try await client.user(user.username)
44+
#expect(user == retUser)
45+
#expect(tokenSent)
46+
}
47+
48+
@Test
49+
func buildInfo() async throws {
50+
let buildInfo = BuildInfoResponse(
51+
external_url: "https://example.com",
52+
version: "v2.18.2-devel+630fd7c0a",
53+
dashboard_url: "https://example.com/dashboard",
54+
telemetry: true,
55+
workspace_proxy: false,
56+
agent_api_version: "1.0",
57+
provisioner_api_version: "1.2",
58+
upgrade_message: "foo",
59+
deployment_id: UUID().uuidString
60+
)
61+
62+
let url = URL(string: "https://example.com")!
63+
let client = Client(url: url)
64+
try Mock(
65+
url: url.appending(path: "api/v2/buildinfo"),
66+
contentType: .json,
67+
statusCode: 200,
68+
data: [.get: Client.encoder.encode(buildInfo)]
69+
).register()
70+
71+
let retBuildInfo = try await client.buildInfo()
72+
#expect(buildInfo == retBuildInfo)
73+
#expect(retBuildInfo.semver == "2.18.2")
74+
}
75+
}

Diff for: ‎Coder Desktop/VPN/Manager.swift

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import CoderSDK
12
import NetworkExtension
23
import os
34
import VPNLib
@@ -30,8 +31,18 @@ actor Manager {
3031
} catch {
3132
throw .download(error)
3233
}
34+
let client = Client(url: cfg.serverUrl)
35+
let buildInfo: BuildInfoResponse
3336
do {
34-
try SignatureValidator.validate(path: dest)
37+
buildInfo = try await client.buildInfo()
38+
} catch {
39+
throw .serverInfo(error.description)
40+
}
41+
guard let semver = buildInfo.semver else {
42+
throw .serverInfo("invalid version: \(buildInfo.version)")
43+
}
44+
do {
45+
try SignatureValidator.validate(path: dest, expectedVersion: semver)
3546
} catch {
3647
throw .validation(error)
3748
}
@@ -181,6 +192,7 @@ enum ManagerError: Error {
181192
case validation(ValidationError)
182193
case incorrectResponse(Vpn_TunnelMessage)
183194
case failedRPC(any Error)
195+
case serverInfo(String)
184196
case errorResponse(msg: String)
185197
case noTunnelFileDescriptor
186198
}

Diff for: ‎Coder Desktop/VPNLib/Download.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ public class SignatureValidator {
3737
private static let expectedName = "CoderVPN"
3838
private static let expectedIdentifier = "com.coder.Coder-Desktop.VPN.dylib"
3939
private static let expectedTeamIdentifier = "4399GN35BJ"
40-
private static let minDylibVersion = "2.18.1"
4140

4241
private static let infoIdentifierKey = "CFBundleIdentifier"
4342
private static let infoNameKey = "CFBundleName"
4443
private static let infoShortVersionKey = "CFBundleShortVersionString"
4544

4645
private static let signInfoFlags: SecCSFlags = .init(rawValue: kSecCSSigningInformation)
4746

48-
public static func validate(path: URL) throws(ValidationError) {
47+
// `expectedVersion` must be of the form `[0-9]+.[0-9]+.[0-9]+`
48+
public static func validate(path: URL, expectedVersion: String) throws(ValidationError) {
4949
guard FileManager.default.fileExists(atPath: path.path) else {
5050
throw .fileNotFound
5151
}
@@ -94,7 +94,7 @@ public class SignatureValidator {
9494
}
9595

9696
guard let dylibVersion = infoPlist[infoShortVersionKey] as? String,
97-
minDylibVersion.compare(dylibVersion, options: .numeric) != .orderedDescending
97+
expectedVersion.compare(dylibVersion, options: .numeric) != .orderedDescending
9898
else {
9999
throw .invalidVersion(version: infoPlist[infoShortVersionKey] as? String)
100100
}

0 commit comments

Comments
 (0)
Please sign in to comment.