Commit 0aa8cdf3bb

0aa8cdf3bb55f31039df1ce473dc23eca22b6449

parent: 2279dfa7c4

Unsigned

cmc <hello@cleberg.net> · 2026-07-24 22:51 UTC

fix: reach CloudKit instead of gating on unreliable entitlement introspection (#29)

iCloud sync always failed with "This build does not have the CloudKit
entitlement required for iCloud sync." on normal Debug/device builds.

`cloudKitContainer()` gated construction on `isEntitlementConfigurationAvailable()`,
which inspected `Info.plist` keys and an `archived-expanded-entitlements.xcent`
file. Entitlements live in the code signature, not `Info.plist`, and the `.xcent`
file only exists in archived/distribution builds — so the check returned false for
a correctly-signed development build, the container was never created, and every
path reported `.missingEntitlement`.

Stop introspecting entitlements. Always construct `CKContainer.default()` and let
CloudKit's own `accountStatus()` drive availability. When the entitlement is
genuinely absent, CloudKit surfaces `.missingEntitlement` / `.badContainer` /
`.permissionFailure`, which `accountStatus()` now maps back to the "no entitlement"
message — preserving that signal without the false negative.

Layout: unified · split

DomainDig/CloudSyncService.swift +13 −40
@@ -336,9 +336,7 @@ final class CloudSyncService {
336 } 336 }
337 337
338 func acceptShare(metadata: CKShare.Metadata) async throws { 338 func acceptShare(metadata: CKShare.Metadata) async throws {
339 guard let container = cloudKitContainer() else { 339 let container = cloudKitContainer()
340 throw CloudSyncRuntimeError.missingEntitlement
341 }
342 340
343 if metadata.participantStatus == .pending { 341 if metadata.participantStatus == .pending {
344 _ = try await container.accept(metadata) 342 _ = try await container.accept(metadata)
@@ -1019,19 +1017,19 @@ final class CloudSyncService {
1019 } 1017 }
1020 1018
1021 private func accountStatus() async -> AvailabilityState { 1019 private func accountStatus() async -> AvailabilityState {
1022 guard let container = cloudKitContainer() else { 1020 let container = cloudKitContainer()
1023 return .missingEntitlement
1024 }
1025 1021
1026 return await withCheckedContinuation { continuation in 1022 return await withCheckedContinuation { continuation in
1027 container.accountStatus { status, error in 1023 container.accountStatus { status, error in
1028 if let error { 1024 if let error {
1029 let ckError = error as? CKError 1025 switch (error as? CKError)?.code {
1030 if ckError?.code == .notAuthenticated { 1026 case .notAuthenticated:
1031 continuation.resume(returning: AvailabilityState.noAccount) 1027 continuation.resume(returning: AvailabilityState.noAccount)
1032 return 1028 case .missingEntitlement, .badContainer, .permissionFailure:
1029 continuation.resume(returning: AvailabilityState.missingEntitlement)
1030 default:
1031 continuation.resume(returning: AvailabilityState.unknown)
1033 } 1032 }
1034 continuation.resume(returning: AvailabilityState.unknown)
1035 return 1033 return
1036 } 1034 }
1037 1035
@@ -1385,18 +1383,14 @@ final class CloudSyncService {
1385 .lowercased() ?? "" 1383 .lowercased() ?? ""
1386 } 1384 }
1387 1385
1388 private func cloudKitContainer() -> CKContainer? { 1386 private func cloudKitContainer() -> CKContainer {
1389 if let container { 1387 if let container {
1390 return container 1388 return container
1391 } 1389 }
1392 1390
1393 if isEntitlementConfigurationAvailable() { 1391 let container = CKContainer.default()
1394 let container = CKContainer.default() 1392 self.container = container
1395 self.container = container 1393 return container
1396 return container
1397 }
1398
1399 return nil
1400 } 1394 }
1401 1395
1402 private func cloudKitDatabase(in scope: SyncDatabaseScope) -> CKDatabase? { 1396 private func cloudKitDatabase(in scope: SyncDatabaseScope) -> CKDatabase? {
@@ -1411,9 +1405,7 @@ final class CloudSyncService {
1411 } 1405 }
1412 } 1406 }
1413 1407
1414 guard let container = cloudKitContainer() else { 1408 let container = cloudKitContainer()
1415 return nil
1416 }
1417 1409
1418 switch scope { 1410 switch scope {
1419 case .privateDatabase: 1411 case .privateDatabase:
@@ -1427,25 +1419,6 @@ final class CloudSyncService {
1427 } 1419 }
1428 } 1420 }
1429 1421
1430 private func isEntitlementConfigurationAvailable() -> Bool {
1431 if Bundle.main.object(forInfoDictionaryKey: "com.apple.developer.icloud-container-identifiers") != nil {
1432 return true
1433 }
1434
1435 if Bundle.main.object(forInfoDictionaryKey: "com.apple.developer.ubiquity-container-identifiers") != nil {
1436 return true
1437 }
1438
1439 if let entitlementsURL = Bundle.main.url(forResource: "archived-expanded-entitlements", withExtension: "xcent"),
1440 let data = try? Data(contentsOf: entitlementsURL),
1441 let contents = String(data: data, encoding: .utf8) {
1442 return contents.contains("com.apple.developer.icloud-services")
1443 && (contents.contains("CloudKit") || contents.contains("CloudKit-Anonymous"))
1444 }
1445
1446 return false
1447 }
1448
1449 private var missingEntitlementMessage: String { 1422 private var missingEntitlementMessage: String {
1450 "This build does not have the CloudKit entitlement required for iCloud sync." 1423 "This build does not have the CloudKit entitlement required for iCloud sync."
1451 } 1424 }