bleam 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +246 -18
- package/dist/cli.d.cts +30 -2
- package/dist/cli.d.ts +30 -2
- package/dist/cli.js +247 -19
- package/dist/platform.d.cts +2 -0
- package/dist/platform.d.ts +2 -0
- package/dist/{ui-1WepaMS4.d.cts → ui-Dd7SXdbg.d.cts} +7 -7
- package/dist/ui.d.cts +1 -1
- package/dist/window.d.cts +1 -1
- package/package.json +2 -1
- package/templates/native/ios/Bleam/AppDelegate.swift +129 -20
- package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +19 -18
- package/templates/native/ios/PlatformHelper/main.swift +457 -66
- package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +21 -5
- package/templates/updates/README.md +272 -0
- package/templates/updates/src/index.ts +385 -0
- package/templates/updates/src/schema.ts +385 -0
- package/templates/updates/tsconfig.json +12 -0
- package/templates/updates/wrangler.jsonc +19 -0
- package/templates/native/ios/PlatformHelper/Info.plist +0 -29
|
@@ -13,9 +13,11 @@ private struct DownloadRequest: Decodable {
|
|
|
13
13
|
let serviceURL: URL
|
|
14
14
|
let bundleIdentifier: String
|
|
15
15
|
let teamIdentifier: String
|
|
16
|
+
let platformVersion: String
|
|
17
|
+
let platformFingerprint: String
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
private struct ApplyRequest:
|
|
20
|
+
private struct ApplyRequest: Codable {
|
|
19
21
|
let releaseId: String
|
|
20
22
|
let bundleIdentifier: String
|
|
21
23
|
let teamIdentifier: String
|
|
@@ -23,6 +25,30 @@ private struct ApplyRequest: Decodable {
|
|
|
23
25
|
let appPID: Int32
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
private struct ApplyTransaction: Codable {
|
|
29
|
+
enum Stage: String, Codable {
|
|
30
|
+
case accepted, waitingForExit, verifying, replacing, failed
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let request: ApplyRequest
|
|
34
|
+
var stage: Stage
|
|
35
|
+
var updatedAt: Date
|
|
36
|
+
var failure: String?
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private struct VerifiedReceipt: Codable {
|
|
40
|
+
let releaseId: String
|
|
41
|
+
let bundleIdentifier: String
|
|
42
|
+
let teamIdentifier: String
|
|
43
|
+
let appVersion: String
|
|
44
|
+
let buildNumber: String
|
|
45
|
+
let platformVersion: String
|
|
46
|
+
let platformFingerprint: String
|
|
47
|
+
let executable: String
|
|
48
|
+
let architecture: String
|
|
49
|
+
let appPath: String
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
private struct PlatformRelease: Decodable {
|
|
27
53
|
struct Artifact: Decodable {
|
|
28
54
|
let format: String
|
|
@@ -38,6 +64,8 @@ private struct PlatformRelease: Decodable {
|
|
|
38
64
|
let appVersion: String
|
|
39
65
|
let buildNumber: String
|
|
40
66
|
let bleamPlatformVersion: String
|
|
67
|
+
let platformFingerprint: String
|
|
68
|
+
let redeploy: Bool?
|
|
41
69
|
let target: Target
|
|
42
70
|
let artifact: Artifact
|
|
43
71
|
|
|
@@ -46,13 +74,27 @@ private struct PlatformRelease: Decodable {
|
|
|
46
74
|
}
|
|
47
75
|
}
|
|
48
76
|
|
|
49
|
-
private final class PlatformHelper: NSObject
|
|
77
|
+
private final class PlatformHelper: NSObject {
|
|
78
|
+
private let bundleIdentifier: String
|
|
79
|
+
private let teamIdentifier: String
|
|
80
|
+
private let applyQueue = DispatchQueue(label: "dev.bleam.platform-helper.apply", qos: .userInitiated)
|
|
81
|
+
|
|
82
|
+
init(bundleIdentifier: String, teamIdentifier: String) {
|
|
83
|
+
self.bundleIdentifier = bundleIdentifier
|
|
84
|
+
self.teamIdentifier = teamIdentifier
|
|
85
|
+
super.init()
|
|
86
|
+
applyQueue.async { [weak self] in
|
|
87
|
+
self?.resumePendingApplies()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
50
91
|
func health(reply: @escaping (Data?) -> Void) {
|
|
92
|
+
let environment = ProcessInfo.processInfo.environment
|
|
51
93
|
let response: [String: Any] = [
|
|
52
94
|
"healthy": true,
|
|
53
95
|
"protocolVersion": 1,
|
|
54
|
-
"helperVersion":
|
|
55
|
-
"platformVersion":
|
|
96
|
+
"helperVersion": environment["BLEAM_HELPER_VERSION"] ?? "unknown",
|
|
97
|
+
"platformVersion": environment["BLEAM_PLATFORM_VERSION"] ?? "unknown",
|
|
56
98
|
"capabilities": ["platform-updates"],
|
|
57
99
|
]
|
|
58
100
|
reply(try? JSONSerialization.data(withJSONObject: response))
|
|
@@ -61,6 +103,8 @@ private final class PlatformHelper: NSObject, PlatformHelperProtocol {
|
|
|
61
103
|
func downloadPlatformUpdate(requestData: Data, reply: @escaping (Data?) -> Void) {
|
|
62
104
|
guard
|
|
63
105
|
let request = try? JSONDecoder().decode(DownloadRequest.self, from: requestData),
|
|
106
|
+
request.bundleIdentifier == bundleIdentifier,
|
|
107
|
+
request.teamIdentifier == teamIdentifier,
|
|
64
108
|
let resolverURL = URL(
|
|
65
109
|
string: "/v1/apps/\(request.bundleIdentifier)/platform/macos-arm64",
|
|
66
110
|
relativeTo: request.serviceURL
|
|
@@ -78,6 +122,8 @@ private final class PlatformHelper: NSObject, PlatformHelperProtocol {
|
|
|
78
122
|
release.kind == "platform",
|
|
79
123
|
release.bundleIdentifier == request.bundleIdentifier,
|
|
80
124
|
release.teamIdentifier == request.teamIdentifier,
|
|
125
|
+
release.bleamPlatformVersion.compare(request.platformVersion, options: .numeric) != .orderedAscending,
|
|
126
|
+
release.platformFingerprint != request.platformFingerprint || release.redeploy == true,
|
|
81
127
|
release.artifact.format == "bleam-macos-app-zip-v1",
|
|
82
128
|
release.releaseId.range(of: "^[a-z][a-z0-9_-]{2,127}$", options: .regularExpression) != nil
|
|
83
129
|
else {
|
|
@@ -119,26 +165,21 @@ private final class PlatformHelper: NSObject, PlatformHelperProtocol {
|
|
|
119
165
|
"appVersion": release.appVersion,
|
|
120
166
|
"buildNumber": release.buildNumber,
|
|
121
167
|
"platformVersion": release.bleamPlatformVersion,
|
|
168
|
+
"platformFingerprint": release.platformFingerprint,
|
|
169
|
+
"redeploy": release.redeploy == true,
|
|
122
170
|
"artifactPath": artifactURL.path,
|
|
123
171
|
"appPath": appURL.path,
|
|
124
172
|
"sizeBytes": release.artifact.sizeBytes,
|
|
125
173
|
"sha256": release.artifact.sha256,
|
|
126
174
|
"verified": true,
|
|
127
175
|
]
|
|
128
|
-
let
|
|
129
|
-
|
|
130
|
-
"bundleIdentifier": request.bundleIdentifier,
|
|
131
|
-
"teamIdentifier": request.teamIdentifier,
|
|
132
|
-
"appPath": appURL.path,
|
|
133
|
-
]
|
|
134
|
-
let receiptData = try PropertyListSerialization.data(
|
|
135
|
-
fromPropertyList: receipt,
|
|
136
|
-
format: .binary,
|
|
137
|
-
options: 0
|
|
176
|
+
let receiptData = try PropertyListEncoder().encode(
|
|
177
|
+
self.verifiedReceipt(appURL: appURL, release: release, request: request)
|
|
138
178
|
)
|
|
139
179
|
try receiptData.write(to: cache.appendingPathComponent("verified.plist"), options: .atomic)
|
|
140
180
|
reply(try? JSONSerialization.data(withJSONObject: result))
|
|
141
181
|
} catch {
|
|
182
|
+
NSLog("Bleam platform download failed: %@", String(describing: error))
|
|
142
183
|
reply(try? JSONSerialization.data(withJSONObject: [
|
|
143
184
|
"error": String(describing: error),
|
|
144
185
|
]))
|
|
@@ -147,62 +188,248 @@ private final class PlatformHelper: NSObject, PlatformHelperProtocol {
|
|
|
147
188
|
}.resume()
|
|
148
189
|
}
|
|
149
190
|
|
|
150
|
-
func applyPlatformUpdate(
|
|
191
|
+
func applyPlatformUpdate(
|
|
192
|
+
requestData: Data,
|
|
193
|
+
clientProcessIdentifier: pid_t,
|
|
194
|
+
reply: @escaping (Bool) -> Void
|
|
195
|
+
) {
|
|
196
|
+
guard let request = try? JSONDecoder().decode(ApplyRequest.self, from: requestData) else {
|
|
197
|
+
NSLog("Bleam platform apply rejected: invalid request")
|
|
198
|
+
reply(false)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
guard
|
|
202
|
+
request.bundleIdentifier == bundleIdentifier,
|
|
203
|
+
request.teamIdentifier == teamIdentifier,
|
|
204
|
+
clientProcessIdentifier == request.appPID,
|
|
205
|
+
validReleaseId(request.releaseId)
|
|
206
|
+
else {
|
|
207
|
+
NSLog("Bleam platform apply rejected: request identity mismatch")
|
|
208
|
+
reply(false)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
151
211
|
guard
|
|
152
|
-
let request = try? JSONDecoder().decode(ApplyRequest.self, from: requestData),
|
|
153
212
|
let installedURL = supportedInstalledURL(path: request.installedAppPath),
|
|
154
|
-
|
|
213
|
+
(try? validateInstalledBundle(at: installedURL)) != nil
|
|
155
214
|
else {
|
|
215
|
+
NSLog("Bleam platform apply rejected: installed app identity mismatch")
|
|
216
|
+
reply(false)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
guard
|
|
220
|
+
let candidate = verifiedCandidate(request: request),
|
|
221
|
+
let destinationURL = replacementURL(installedURL: installedURL, candidateURL: candidate.appURL),
|
|
222
|
+
destinationURL == installedURL || !FileManager.default.fileExists(atPath: destinationURL.path)
|
|
223
|
+
else {
|
|
224
|
+
NSLog("Bleam platform apply rejected: verified candidate unavailable")
|
|
225
|
+
reply(false)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
guard acceptTransaction(request) else {
|
|
229
|
+
NSLog("Bleam platform apply rejected: transaction could not be persisted")
|
|
156
230
|
reply(false)
|
|
157
231
|
return
|
|
158
232
|
}
|
|
159
|
-
|
|
160
233
|
reply(true)
|
|
234
|
+
applyQueue.async { [weak self] in
|
|
235
|
+
self?.performApply(request)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private func resumePendingApplies() {
|
|
240
|
+
let receipts = receiptsURL(bundleIdentifier: bundleIdentifier)
|
|
241
|
+
guard let files = try? FileManager.default.contentsOfDirectory(
|
|
242
|
+
at: receipts,
|
|
243
|
+
includingPropertiesForKeys: nil
|
|
244
|
+
) else {
|
|
245
|
+
clearPendingMarkerIfIdle()
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
for file in files where file.pathExtension == "applying" {
|
|
249
|
+
guard
|
|
250
|
+
let data = try? Data(contentsOf: file),
|
|
251
|
+
let transaction = try? PropertyListDecoder().decode(ApplyTransaction.self, from: data),
|
|
252
|
+
transaction.request.bundleIdentifier == bundleIdentifier,
|
|
253
|
+
validReleaseId(transaction.request.releaseId),
|
|
254
|
+
transaction.stage != .failed
|
|
255
|
+
else { continue }
|
|
256
|
+
performApply(transaction.request)
|
|
257
|
+
}
|
|
258
|
+
clearPendingMarkerIfIdle()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private func performApply(_ request: ApplyRequest) {
|
|
262
|
+
let completionURL = receiptsURL(bundleIdentifier: request.bundleIdentifier)
|
|
263
|
+
.appendingPathComponent("\(request.releaseId).complete")
|
|
264
|
+
if FileManager.default.fileExists(atPath: completionURL.path) {
|
|
265
|
+
try? FileManager.default.removeItem(at: transactionURL(for: request))
|
|
266
|
+
clearPendingMarkerIfIdle()
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
161
270
|
let activity = ProcessInfo.processInfo.beginActivity(
|
|
162
|
-
options: [.userInitiated, .idleSystemSleepDisabled
|
|
271
|
+
options: [.userInitiated, .idleSystemSleepDisabled],
|
|
163
272
|
reason: "Applying a verified Bleam platform update"
|
|
164
273
|
)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
274
|
+
defer { ProcessInfo.processInfo.endActivity(activity) }
|
|
275
|
+
|
|
276
|
+
guard
|
|
277
|
+
request.bundleIdentifier == bundleIdentifier,
|
|
278
|
+
request.teamIdentifier == teamIdentifier,
|
|
279
|
+
let installedURL = supportedInstalledURL(path: request.installedAppPath),
|
|
280
|
+
let candidate = verifiedCandidate(request: request),
|
|
281
|
+
let destinationURL = replacementURL(installedURL: installedURL, candidateURL: candidate.appURL)
|
|
282
|
+
else {
|
|
283
|
+
recordFailure(request, error: VerificationError.invalidTransaction)
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
let candidateURL = candidate.appURL
|
|
287
|
+
let releaseRoot = candidateURL
|
|
288
|
+
.deletingLastPathComponent().deletingLastPathComponent()
|
|
289
|
+
.deletingLastPathComponent()
|
|
290
|
+
let backupURL = releaseRoot.appendingPathComponent("previous.app")
|
|
291
|
+
|
|
292
|
+
do {
|
|
293
|
+
try writeTransaction(request: request, stage: .waitingForExit)
|
|
294
|
+
for _ in 0..<50 where isApplicationProcessRunning(request) {
|
|
168
295
|
usleep(100_000)
|
|
169
296
|
}
|
|
170
|
-
if
|
|
297
|
+
if isApplicationProcessRunning(request) {
|
|
171
298
|
_ = kill(request.appPID, SIGTERM)
|
|
172
|
-
for _ in 0..<50 where
|
|
299
|
+
for _ in 0..<50 where isApplicationProcessRunning(request) {
|
|
173
300
|
usleep(100_000)
|
|
174
301
|
}
|
|
175
302
|
}
|
|
176
|
-
guard
|
|
177
|
-
|
|
303
|
+
guard !isApplicationProcessRunning(request) else {
|
|
304
|
+
throw VerificationError.appDidNotTerminate
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try writeTransaction(request: request, stage: .verifying)
|
|
308
|
+
if isValidBundle(at: destinationURL, expected: candidate.receipt) {
|
|
309
|
+
try finishApply(request: request, destinationURL: destinationURL)
|
|
178
310
|
return
|
|
179
311
|
}
|
|
180
312
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
let backupURL = releaseRoot.appendingPathComponent("previous.app")
|
|
185
|
-
do {
|
|
313
|
+
try validateBundle(at: candidateURL, expected: candidate.receipt)
|
|
314
|
+
if FileManager.default.fileExists(atPath: installedURL.path) {
|
|
315
|
+
try validateInstalledBundle(at: installedURL)
|
|
186
316
|
try? FileManager.default.removeItem(at: backupURL)
|
|
187
317
|
try FileManager.default.moveItem(at: installedURL, to: backupURL)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
318
|
+
} else if !FileManager.default.fileExists(atPath: backupURL.path) {
|
|
319
|
+
throw VerificationError.invalidTransaction
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
try writeTransaction(request: request, stage: .replacing)
|
|
323
|
+
try FileManager.default.moveItem(at: candidateURL, to: destinationURL)
|
|
324
|
+
try validateBundle(at: destinationURL, expected: candidate.receipt)
|
|
325
|
+
try finishApply(request: request, destinationURL: destinationURL)
|
|
326
|
+
} catch {
|
|
327
|
+
if FileManager.default.fileExists(atPath: backupURL.path) {
|
|
328
|
+
try? FileManager.default.removeItem(at: destinationURL)
|
|
329
|
+
if !FileManager.default.fileExists(atPath: installedURL.path) {
|
|
191
330
|
try? FileManager.default.moveItem(at: backupURL, to: installedURL)
|
|
192
|
-
|
|
331
|
+
_ = try? run("/usr/bin/open", [installedURL.path])
|
|
193
332
|
}
|
|
194
|
-
let receipts = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
|
|
195
|
-
.appendingPathComponent("Library/Application Support/Bleam/Updates", isDirectory: true)
|
|
196
|
-
.appendingPathComponent(request.bundleIdentifier, isDirectory: true)
|
|
197
|
-
try FileManager.default.createDirectory(at: receipts, withIntermediateDirectories: true)
|
|
198
|
-
try Data().write(to: receipts.appendingPathComponent("\(request.releaseId).complete"))
|
|
199
|
-
_ = try self.run("/usr/bin/open", [installedURL.path])
|
|
200
|
-
} catch {
|
|
201
|
-
NSLog("Bleam platform replacement failed: %@", error.localizedDescription)
|
|
202
333
|
}
|
|
334
|
+
recordFailure(request, error: error)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private func finishApply(request: ApplyRequest, destinationURL: URL) throws {
|
|
339
|
+
let receipts = receiptsURL(bundleIdentifier: request.bundleIdentifier)
|
|
340
|
+
try FileManager.default.createDirectory(at: receipts, withIntermediateDirectories: true)
|
|
341
|
+
try Data().write(
|
|
342
|
+
to: receipts.appendingPathComponent("\(request.releaseId).complete"),
|
|
343
|
+
options: .atomic
|
|
344
|
+
)
|
|
345
|
+
try? FileManager.default.removeItem(at: transactionURL(for: request))
|
|
346
|
+
try? FileManager.default.removeItem(at: failureURL(for: request))
|
|
347
|
+
clearPendingMarkerIfIdle()
|
|
348
|
+
_ = try? run("/usr/bin/open", [destinationURL.path])
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private func recordFailure(_ request: ApplyRequest, error: Error) {
|
|
352
|
+
NSLog("Bleam platform replacement failed: %@", String(describing: error))
|
|
353
|
+
do {
|
|
354
|
+
try writeTransaction(
|
|
355
|
+
request: request,
|
|
356
|
+
stage: .failed,
|
|
357
|
+
failure: String(describing: error),
|
|
358
|
+
destination: failureURL(for: request)
|
|
359
|
+
)
|
|
360
|
+
try? FileManager.default.removeItem(at: transactionURL(for: request))
|
|
361
|
+
clearPendingMarkerIfIdle()
|
|
362
|
+
} catch {
|
|
363
|
+
NSLog("Bleam platform failure receipt could not be written: %@", String(describing: error))
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private func acceptTransaction(_ request: ApplyRequest) -> Bool {
|
|
368
|
+
do {
|
|
369
|
+
try writeTransaction(request: request, stage: .accepted)
|
|
370
|
+
try Data().write(to: pendingMarkerURL(), options: .atomic)
|
|
371
|
+
return true
|
|
372
|
+
} catch {
|
|
373
|
+
try? FileManager.default.removeItem(at: transactionURL(for: request))
|
|
374
|
+
clearPendingMarkerIfIdle()
|
|
375
|
+
return false
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
@discardableResult
|
|
380
|
+
private func writeTransaction(
|
|
381
|
+
request: ApplyRequest,
|
|
382
|
+
stage: ApplyTransaction.Stage,
|
|
383
|
+
failure: String? = nil,
|
|
384
|
+
destination: URL? = nil
|
|
385
|
+
) throws -> URL {
|
|
386
|
+
let url = destination ?? transactionURL(for: request)
|
|
387
|
+
try FileManager.default.createDirectory(
|
|
388
|
+
at: url.deletingLastPathComponent(),
|
|
389
|
+
withIntermediateDirectories: true
|
|
390
|
+
)
|
|
391
|
+
let data = try PropertyListEncoder().encode(
|
|
392
|
+
ApplyTransaction(request: request, stage: stage, updatedAt: Date(), failure: failure)
|
|
393
|
+
)
|
|
394
|
+
try data.write(to: url, options: .atomic)
|
|
395
|
+
return url
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private func transactionURL(for request: ApplyRequest) -> URL {
|
|
399
|
+
receiptsURL(bundleIdentifier: request.bundleIdentifier)
|
|
400
|
+
.appendingPathComponent("\(request.releaseId).applying")
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private func failureURL(for request: ApplyRequest) -> URL {
|
|
404
|
+
receiptsURL(bundleIdentifier: request.bundleIdentifier)
|
|
405
|
+
.appendingPathComponent("\(request.releaseId).failed")
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private func pendingMarkerURL() -> URL {
|
|
409
|
+
receiptsURL(bundleIdentifier: bundleIdentifier).appendingPathComponent("platform-apply.pending")
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private func clearPendingMarkerIfIdle() {
|
|
413
|
+
let receipts = receiptsURL(bundleIdentifier: bundleIdentifier)
|
|
414
|
+
let hasPendingTransaction = (try? FileManager.default.contentsOfDirectory(
|
|
415
|
+
at: receipts,
|
|
416
|
+
includingPropertiesForKeys: nil
|
|
417
|
+
).contains(where: { $0.pathExtension == "applying" })) == true
|
|
418
|
+
if !hasPendingTransaction {
|
|
419
|
+
try? FileManager.default.removeItem(at: pendingMarkerURL())
|
|
203
420
|
}
|
|
204
421
|
}
|
|
205
422
|
|
|
423
|
+
private func receiptsURL(bundleIdentifier: String) -> URL {
|
|
424
|
+
URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
|
|
425
|
+
.appendingPathComponent("Library/Application Support/Bleam/Updates", isDirectory: true)
|
|
426
|
+
.appendingPathComponent(bundleIdentifier, isDirectory: true)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private func validReleaseId(_ releaseId: String) -> Bool {
|
|
430
|
+
releaseId.range(of: "^[a-z][a-z0-9_-]{2,127}$", options: .regularExpression) != nil
|
|
431
|
+
}
|
|
432
|
+
|
|
206
433
|
private func supportedInstalledURL(path: String) -> URL? {
|
|
207
434
|
let applications = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
|
|
208
435
|
.appendingPathComponent("Applications", isDirectory: true).standardizedFileURL
|
|
@@ -214,7 +441,25 @@ private final class PlatformHelper: NSObject, PlatformHelperProtocol {
|
|
|
214
441
|
return candidate
|
|
215
442
|
}
|
|
216
443
|
|
|
217
|
-
private func
|
|
444
|
+
private func replacementURL(installedURL: URL, candidateURL: URL) -> URL? {
|
|
445
|
+
let replacement = installedURL.deletingLastPathComponent()
|
|
446
|
+
.appendingPathComponent(candidateURL.lastPathComponent, isDirectory: true)
|
|
447
|
+
.standardizedFileURL
|
|
448
|
+
guard
|
|
449
|
+
replacement.pathExtension == "app",
|
|
450
|
+
replacement.deletingLastPathComponent() == installedURL.deletingLastPathComponent()
|
|
451
|
+
else { return nil }
|
|
452
|
+
return replacement
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private func isApplicationProcessRunning(_ request: ApplyRequest) -> Bool {
|
|
456
|
+
var path = [CChar](repeating: 0, count: Int(MAXPATHLEN) * 4)
|
|
457
|
+
guard proc_pidpath(request.appPID, &path, UInt32(path.count)) > 0 else { return false }
|
|
458
|
+
let executablePath = String(cString: path)
|
|
459
|
+
return executablePath.hasPrefix(request.installedAppPath + "/Contents/MacOS/")
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private func verifiedCandidate(request: ApplyRequest) -> (appURL: URL, receipt: VerifiedReceipt)? {
|
|
218
463
|
let root = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
|
219
464
|
.appendingPathComponent("Bleam/Platform", isDirectory: true)
|
|
220
465
|
.appendingPathComponent(request.bundleIdentifier, isDirectory: true)
|
|
@@ -222,13 +467,18 @@ private final class PlatformHelper: NSObject, PlatformHelperProtocol {
|
|
|
222
467
|
let receiptURL = root.appendingPathComponent("verified.plist")
|
|
223
468
|
guard
|
|
224
469
|
let data = try? Data(contentsOf: receiptURL),
|
|
225
|
-
let receipt = try?
|
|
226
|
-
receipt
|
|
227
|
-
receipt
|
|
228
|
-
receipt
|
|
229
|
-
let path = receipt["appPath"]
|
|
470
|
+
let receipt = try? PropertyListDecoder().decode(VerifiedReceipt.self, from: data),
|
|
471
|
+
receipt.releaseId == request.releaseId,
|
|
472
|
+
receipt.bundleIdentifier == request.bundleIdentifier,
|
|
473
|
+
receipt.teamIdentifier == request.teamIdentifier
|
|
230
474
|
else { return nil }
|
|
231
|
-
|
|
475
|
+
let appURL = URL(fileURLWithPath: receipt.appPath, isDirectory: true).standardizedFileURL
|
|
476
|
+
let appRoot = root.appendingPathComponent("extracted/app", isDirectory: true).standardizedFileURL
|
|
477
|
+
guard
|
|
478
|
+
appURL.pathExtension == "app",
|
|
479
|
+
appURL.deletingLastPathComponent() == appRoot
|
|
480
|
+
else { return nil }
|
|
481
|
+
return (appURL, receipt)
|
|
232
482
|
}
|
|
233
483
|
|
|
234
484
|
private func verify(
|
|
@@ -264,13 +514,76 @@ private final class PlatformHelper: NSObject, PlatformHelperProtocol {
|
|
|
264
514
|
try FileManager.default.createDirectory(at: extracted, withIntermediateDirectories: true)
|
|
265
515
|
_ = try run("/usr/bin/ditto", ["-x", "-k", artifactURL.path, extracted.path])
|
|
266
516
|
let appURL = extracted.appendingPathComponent("app/\(appName)")
|
|
517
|
+
try validateBundle(at: appURL, expected: verifiedReceipt(appURL: appURL, release: release, request: request))
|
|
518
|
+
|
|
519
|
+
let required = release.target.minimumMacOSVersion.split(separator: ".").compactMap { Int($0) }
|
|
520
|
+
let current = ProcessInfo.processInfo.operatingSystemVersion
|
|
521
|
+
let installed = [current.majorVersion, current.minorVersion, current.patchVersion]
|
|
522
|
+
guard installed.lexicographicallyPrecedes(required) == false else {
|
|
523
|
+
throw VerificationError.unsupportedOS
|
|
524
|
+
}
|
|
525
|
+
return appURL
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private func verifiedReceipt(
|
|
529
|
+
appURL: URL,
|
|
530
|
+
release: PlatformRelease,
|
|
531
|
+
request: DownloadRequest
|
|
532
|
+
) throws -> VerifiedReceipt {
|
|
267
533
|
guard
|
|
268
534
|
let info = NSDictionary(contentsOf: appURL.appendingPathComponent("Contents/Info.plist")),
|
|
269
|
-
info["CFBundleIdentifier"] as? String == request.bundleIdentifier,
|
|
270
|
-
info["CFBundleShortVersionString"] as? String == release.appVersion,
|
|
271
|
-
info["CFBundleVersion"] as? String == release.buildNumber,
|
|
272
535
|
let executable = info["CFBundleExecutable"] as? String
|
|
273
536
|
else { throw VerificationError.invalidIdentity }
|
|
537
|
+
return VerifiedReceipt(
|
|
538
|
+
releaseId: release.releaseId,
|
|
539
|
+
bundleIdentifier: request.bundleIdentifier,
|
|
540
|
+
teamIdentifier: request.teamIdentifier,
|
|
541
|
+
appVersion: release.appVersion,
|
|
542
|
+
buildNumber: release.buildNumber,
|
|
543
|
+
platformVersion: release.bleamPlatformVersion,
|
|
544
|
+
platformFingerprint: release.platformFingerprint,
|
|
545
|
+
executable: executable,
|
|
546
|
+
architecture: "arm64",
|
|
547
|
+
appPath: appURL.path
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private func validateArchiveSymlinks(in appURL: URL) throws {
|
|
552
|
+
let root = appURL.resolvingSymlinksInPath().standardizedFileURL
|
|
553
|
+
let rootValues = try appURL.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey])
|
|
554
|
+
var enumerationFailed = false
|
|
555
|
+
guard rootValues.isDirectory == true,
|
|
556
|
+
rootValues.isSymbolicLink != true,
|
|
557
|
+
let enumerator = FileManager.default.enumerator(
|
|
558
|
+
at: appURL,
|
|
559
|
+
includingPropertiesForKeys: [.isSymbolicLinkKey],
|
|
560
|
+
errorHandler: { _, _ in enumerationFailed = true; return false }
|
|
561
|
+
)
|
|
562
|
+
else { throw VerificationError.unsafeArchivePath }
|
|
563
|
+
for case let url as URL in enumerator {
|
|
564
|
+
guard (try url.resourceValues(forKeys: [.isSymbolicLinkKey])).isSymbolicLink == true else { continue }
|
|
565
|
+
let target = url.resolvingSymlinksInPath().standardizedFileURL
|
|
566
|
+
guard
|
|
567
|
+
target == root || target.path.hasPrefix(root.path + "/"),
|
|
568
|
+
FileManager.default.fileExists(atPath: target.path)
|
|
569
|
+
else {
|
|
570
|
+
throw VerificationError.unsafeArchivePath
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
guard !enumerationFailed else { throw VerificationError.unsafeArchivePath }
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private func validateBundle(at appURL: URL, expected: VerifiedReceipt) throws {
|
|
577
|
+
try validateArchiveSymlinks(in: appURL)
|
|
578
|
+
guard
|
|
579
|
+
let info = NSDictionary(contentsOf: appURL.appendingPathComponent("Contents/Info.plist")),
|
|
580
|
+
info["CFBundleIdentifier"] as? String == expected.bundleIdentifier,
|
|
581
|
+
info["CFBundleShortVersionString"] as? String == expected.appVersion,
|
|
582
|
+
info["CFBundleVersion"] as? String == expected.buildNumber,
|
|
583
|
+
info["BleamRuntimeVersion"] as? String == expected.platformVersion,
|
|
584
|
+
info["BleamPlatformFingerprint"] as? String == expected.platformFingerprint,
|
|
585
|
+
info["CFBundleExecutable"] as? String == expected.executable
|
|
586
|
+
else { throw VerificationError.invalidIdentity }
|
|
274
587
|
|
|
275
588
|
var staticCode: SecStaticCode?
|
|
276
589
|
guard
|
|
@@ -281,22 +594,43 @@ private final class PlatformHelper: NSObject, PlatformHelperProtocol {
|
|
|
281
594
|
var signingInfo: CFDictionary?
|
|
282
595
|
guard
|
|
283
596
|
SecCodeCopySigningInformation(staticCode, SecCSFlags(rawValue: kSecCSSigningInformation), &signingInfo) == errSecSuccess,
|
|
284
|
-
(signingInfo as? [String: Any])?[kSecCodeInfoTeamIdentifier as String] as? String ==
|
|
597
|
+
(signingInfo as? [String: Any])?[kSecCodeInfoTeamIdentifier as String] as? String == expected.teamIdentifier
|
|
285
598
|
else { throw VerificationError.invalidSignature }
|
|
286
599
|
|
|
287
600
|
let architectures = try run(
|
|
288
601
|
"/usr/bin/lipo",
|
|
289
|
-
["-archs", appURL.appendingPathComponent("Contents/MacOS/\(executable)").path]
|
|
602
|
+
["-archs", appURL.appendingPathComponent("Contents/MacOS/\(expected.executable)").path]
|
|
290
603
|
).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
291
|
-
guard architectures ==
|
|
604
|
+
guard architectures == expected.architecture else { throw VerificationError.invalidArchitecture }
|
|
605
|
+
}
|
|
292
606
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
607
|
+
private func validateInstalledBundle(at appURL: URL) throws {
|
|
608
|
+
guard
|
|
609
|
+
let info = NSDictionary(contentsOf: appURL.appendingPathComponent("Contents/Info.plist")),
|
|
610
|
+
info["CFBundleIdentifier"] as? String == bundleIdentifier
|
|
611
|
+
else { throw VerificationError.invalidIdentity }
|
|
612
|
+
|
|
613
|
+
var staticCode: SecStaticCode?
|
|
614
|
+
guard
|
|
615
|
+
SecStaticCodeCreateWithPath(appURL as CFURL, [], &staticCode) == errSecSuccess,
|
|
616
|
+
let staticCode,
|
|
617
|
+
SecStaticCodeCheckValidity(staticCode, SecCSFlags(rawValue: kSecCSStrictValidate), nil) == errSecSuccess
|
|
618
|
+
else { throw VerificationError.invalidSignature }
|
|
619
|
+
var signingInfo: CFDictionary?
|
|
620
|
+
guard
|
|
621
|
+
SecCodeCopySigningInformation(staticCode, SecCSFlags(rawValue: kSecCSSigningInformation), &signingInfo) == errSecSuccess,
|
|
622
|
+
(signingInfo as? [String: Any])?[kSecCodeInfoTeamIdentifier as String] as? String == teamIdentifier
|
|
623
|
+
else { throw VerificationError.invalidSignature }
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private func isValidBundle(at appURL: URL, expected: VerifiedReceipt) -> Bool {
|
|
627
|
+
guard FileManager.default.fileExists(atPath: appURL.path) else { return false }
|
|
628
|
+
do {
|
|
629
|
+
try validateBundle(at: appURL, expected: expected)
|
|
630
|
+
return true
|
|
631
|
+
} catch {
|
|
632
|
+
return false
|
|
298
633
|
}
|
|
299
|
-
return appURL
|
|
300
634
|
}
|
|
301
635
|
|
|
302
636
|
private func run(_ executable: String, _ arguments: [String]) throws -> String {
|
|
@@ -314,22 +648,79 @@ private final class PlatformHelper: NSObject, PlatformHelperProtocol {
|
|
|
314
648
|
}
|
|
315
649
|
}
|
|
316
650
|
|
|
651
|
+
private final class PlatformHelperConnection: NSObject, PlatformHelperProtocol {
|
|
652
|
+
private let helper: PlatformHelper
|
|
653
|
+
private let processIdentifier: pid_t
|
|
654
|
+
|
|
655
|
+
init(helper: PlatformHelper, processIdentifier: pid_t) {
|
|
656
|
+
self.helper = helper
|
|
657
|
+
self.processIdentifier = processIdentifier
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
func health(reply: @escaping (Data?) -> Void) {
|
|
661
|
+
helper.health(reply: reply)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
func downloadPlatformUpdate(requestData: Data, reply: @escaping (Data?) -> Void) {
|
|
665
|
+
helper.downloadPlatformUpdate(requestData: requestData, reply: reply)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
func applyPlatformUpdate(requestData: Data, reply: @escaping (Bool) -> Void) {
|
|
669
|
+
helper.applyPlatformUpdate(
|
|
670
|
+
requestData: requestData,
|
|
671
|
+
clientProcessIdentifier: processIdentifier,
|
|
672
|
+
reply: reply
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
317
677
|
private enum VerificationError: Error {
|
|
318
678
|
case invalidArtifact, unsafeArchivePath, invalidArchiveLayout, invalidIdentity, invalidSignature
|
|
319
|
-
case invalidArchitecture, unsupportedOS, commandFailed
|
|
679
|
+
case invalidArchitecture, unsupportedOS, commandFailed, invalidTransaction, appDidNotTerminate
|
|
320
680
|
}
|
|
321
681
|
|
|
322
682
|
private final class PlatformHelperDelegate: NSObject, NSXPCListenerDelegate {
|
|
683
|
+
private let helper: PlatformHelper
|
|
684
|
+
private let codeSigningRequirement: String
|
|
685
|
+
|
|
686
|
+
init(helper: PlatformHelper, bundleIdentifier: String, teamIdentifier: String) {
|
|
687
|
+
self.helper = helper
|
|
688
|
+
self.codeSigningRequirement = "anchor apple generic and identifier \"\(bundleIdentifier)\" and certificate leaf[subject.OU] = \"\(teamIdentifier)\""
|
|
689
|
+
}
|
|
690
|
+
|
|
323
691
|
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection connection: NSXPCConnection) -> Bool {
|
|
692
|
+
guard
|
|
693
|
+
connection.processIdentifier > 0,
|
|
694
|
+
connection.effectiveUserIdentifier == getuid()
|
|
695
|
+
else { return false }
|
|
696
|
+
connection.setCodeSigningRequirement(codeSigningRequirement)
|
|
324
697
|
connection.exportedInterface = NSXPCInterface(with: PlatformHelperProtocol.self)
|
|
325
|
-
connection.exportedObject =
|
|
698
|
+
connection.exportedObject = PlatformHelperConnection(
|
|
699
|
+
helper: helper,
|
|
700
|
+
processIdentifier: connection.processIdentifier
|
|
701
|
+
)
|
|
326
702
|
connection.resume()
|
|
327
703
|
return true
|
|
328
704
|
}
|
|
329
705
|
}
|
|
330
706
|
|
|
331
|
-
|
|
332
|
-
|
|
707
|
+
let environment = ProcessInfo.processInfo.environment
|
|
708
|
+
guard
|
|
709
|
+
let serviceName = environment["XPC_SERVICE_NAME"],
|
|
710
|
+
let bundleIdentifier = environment["BLEAM_BUNDLE_IDENTIFIER"],
|
|
711
|
+
bundleIdentifier.range(of: "^[A-Za-z0-9.-]+$", options: .regularExpression) != nil,
|
|
712
|
+
let teamIdentifier = environment["BLEAM_TEAM_IDENTIFIER"],
|
|
713
|
+
teamIdentifier.range(of: "^[A-Z0-9]{10}$", options: .regularExpression) != nil
|
|
714
|
+
else {
|
|
715
|
+
fatalError("Bleam platform helper requires launchd service metadata")
|
|
716
|
+
}
|
|
717
|
+
private let helper = PlatformHelper(bundleIdentifier: bundleIdentifier, teamIdentifier: teamIdentifier)
|
|
718
|
+
private let delegate = PlatformHelperDelegate(
|
|
719
|
+
helper: helper,
|
|
720
|
+
bundleIdentifier: bundleIdentifier,
|
|
721
|
+
teamIdentifier: teamIdentifier
|
|
722
|
+
)
|
|
723
|
+
private let listener = NSXPCListener(machServiceName: serviceName)
|
|
333
724
|
listener.delegate = delegate
|
|
334
725
|
listener.resume()
|
|
335
726
|
RunLoop.main.run()
|