Skip to content

Commit 7e37b0e

Browse files
authoredJan 22, 2025··
chore: extract CoderSDK to framework (#19)
Closes #2.
1 parent 51bc926 commit 7e37b0e

File tree

21 files changed

+787
-325
lines changed

21 files changed

+787
-325
lines changed
 

‎Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj

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

‎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",

‎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

‎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
],

‎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
}

‎Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift

-29
This file was deleted.

‎Coder Desktop/Coder Desktop/SDK/Client.swift

-140
This file was deleted.

‎Coder Desktop/Coder Desktop/SDK/User.swift

-37
This file was deleted.

‎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
}

‎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()

‎Coder Desktop/Coder DesktopTests/Util.swift

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

2828
class MockSession: Session {
2929
@Published
30-
var hasSession: Bool = true
30+
var hasSession: Bool = false
3131
@Published
3232
var sessionToken: String? = "fake-token"
3333
@Published
@@ -50,33 +50,4 @@ class MockSession: Session {
5050
}
5151
}
5252

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

‎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)

‎Coder Desktop/CoderSDK/Client.swift

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

‎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+

‎Coder Desktop/Coder Desktop/SDK/Date.swift ‎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+
}
+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+
}

‎Coder Desktop/CoderSDK/HTTP.swift

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
public struct HTTPResponse {
2+
let resp: HTTPURLResponse
3+
let data: Data
4+
let req: URLRequest
5+
}
6+
7+
public struct HTTPHeader: Sendable {
8+
public let header: String
9+
public let value: String
10+
public init(header: String, value: String) {
11+
self.header = header
12+
self.value = value
13+
}
14+
}
15+
16+
enum HTTPMethod: String, Equatable, Hashable, Sendable {
17+
case get = "GET"
18+
case post = "POST"
19+
case delete = "DELETE"
20+
case put = "PUT"
21+
case head = "HEAD"
22+
}
23+
24+
enum Headers {
25+
static let sessionToken = "Coder-Session-Token"
26+
}

‎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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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, headers: [.init(header: "X-Test-Header", value: "foo")])
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 correctHeaders = false
38+
mock.onRequestHandler = OnRequestHandler { req in
39+
correctHeaders = req.value(forHTTPHeaderField: Headers.sessionToken) == token &&
40+
req.value(forHTTPHeaderField: "X-Test-Header") == "foo"
41+
}
42+
mock.register()
43+
44+
let retUser = try await client.user(user.username)
45+
#expect(user == retUser)
46+
#expect(correctHeaders)
47+
}
48+
49+
@Test
50+
func buildInfo() async throws {
51+
let buildInfo = BuildInfoResponse(
52+
external_url: "https://example.com",
53+
version: "v2.18.2-devel+630fd7c0a",
54+
dashboard_url: "https://example.com/dashboard",
55+
telemetry: true,
56+
workspace_proxy: false,
57+
agent_api_version: "1.0",
58+
provisioner_api_version: "1.2",
59+
upgrade_message: "foo",
60+
deployment_id: UUID().uuidString
61+
)
62+
63+
let url = URL(string: "https://example.com")!
64+
let client = Client(url: url)
65+
try Mock(
66+
url: url.appending(path: "api/v2/buildinfo"),
67+
contentType: .json,
68+
statusCode: 200,
69+
data: [.get: Client.encoder.encode(buildInfo)]
70+
).register()
71+
72+
let retBuildInfo = try await client.buildInfo()
73+
#expect(buildInfo == retBuildInfo)
74+
#expect(retBuildInfo.semver == "2.18.2")
75+
}
76+
}

‎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
}

‎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.