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 {
336336 }
337337
338338 func acceptShare(metadata: CKShare.Metadata) async throws {
339 guard let container = cloudKitContainer() else {
340 throw CloudSyncRuntimeError.missingEntitlement
341 }
339 let container = cloudKitContainer()
342340
343341 if metadata.participantStatus == .pending {
344342 _ = try await container.accept(metadata)
@@ -1019,19 +1017,19 @@ final class CloudSyncService {
10191017 }
10201018
10211019 private func accountStatus() async -> AvailabilityState {
1022 guard let container = cloudKitContainer() else {
1023 return .missingEntitlement
1024 }
1020 let container = cloudKitContainer()
10251021
10261022 return await withCheckedContinuation { continuation in
10271023 container.accountStatus { status, error in
10281024 if let error {
1029 let ckError = error as? CKError
1030 if ckError?.code == .notAuthenticated {
1025 switch (error as? CKError)?.code {
1026 case .notAuthenticated:
10311027 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)
10331032 }
1034 continuation.resume(returning: AvailabilityState.unknown)
10351033 return
10361034 }
10371035
@@ -1385,18 +1383,14 @@ final class CloudSyncService {
13851383 .lowercased() ?? ""
13861384 }
13871385
1388 private func cloudKitContainer() -> CKContainer? {
1386 private func cloudKitContainer() -> CKContainer {
13891387 if let container {
13901388 return container
13911389 }
13921390
1393 if isEntitlementConfigurationAvailable() {
1394 let container = CKContainer.default()
1395 self.container = container
1396 return container
1397 }
1398
1399 return nil
1391 let container = CKContainer.default()
1392 self.container = container
1393 return container
14001394 }
14011395
14021396 private func cloudKitDatabase(in scope: SyncDatabaseScope) -> CKDatabase? {
@@ -1411,9 +1405,7 @@ final class CloudSyncService {
14111405 }
14121406 }
14131407
1414 guard let container = cloudKitContainer() else {
1415 return nil
1416 }
1408 let container = cloudKitContainer()
14171409
14181410 switch scope {
14191411 case .privateDatabase:
@@ -1427,25 +1419,6 @@ final class CloudSyncService {
14271419 }
14281420 }
14291421
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
14491422 private var missingEntitlementMessage: String {
14501423 "This build does not have the CloudKit entitlement required for iCloud sync."
14511424 }