From 12ac7b4daf21f76ce15633a6b3b3f6d4b1da0c28 Mon Sep 17 00:00:00 2001 From: Valentin Brosseau Date: Thu, 5 Nov 2026 19:46:18 +0100 Subject: [PATCH 1/2] profiles: replace xtool's own profile when the bundle ID has several With one profile on the bundle ID, xtool deletes it before creating its own. With several (Xcode, or another signing tool, made one next to it), it deleted none and created "XTL profile " again, which the API refuses: 409 "Multiple profiles found with the name 'XTL profile '". The other profiles stay; the one named like xtool's is deleted first. --- .../DeveloperServicesFetchProfileOperation.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Sources/XKit/DeveloperServices/Profiles/DeveloperServicesFetchProfileOperation.swift b/Sources/XKit/DeveloperServices/Profiles/DeveloperServicesFetchProfileOperation.swift index c9ac8a3f..e00ff9ff 100644 --- a/Sources/XKit/DeveloperServices/Profiles/DeveloperServicesFetchProfileOperation.swift +++ b/Sources/XKit/DeveloperServices/Profiles/DeveloperServicesFetchProfileOperation.swift @@ -57,8 +57,17 @@ public struct DeveloperServicesFetchProfileOperation: DeveloperServicesOperation case 1: _ = try await context.developerAPIClient.profilesDeleteInstance(path: .init(id: profiles[0].id)).noContent default: - // if the user has >1 profile, it's probably okay to add another one (acceptable for non-free accounts?) - break + // Several profiles (one made by Xcode or another tool next to ours): keep theirs, but + // the one xtool made earlier must go, or creating it again fails on its name + // (409 "Multiple profiles found with the name 'XTL profile …'"). + let ownName = "XTL profile \(bundleID.id)" + let own = try await context.developerAPIClient + .profilesGetCollection(query: .init(filter_lbrack_name_rbrack_: [ownName])) + .ok.body.json.data + .filter { $0.attributes?.name == ownName } + for profile in own { + _ = try await context.developerAPIClient.profilesDeleteInstance(path: .init(id: profile.id)).noContent + } } // Match locally rather than with filter[serialNumber]: the API lists serials with a From ee96c541e44b995a4fb77a08035d73d71f64933b Mon Sep 17 00:00:00 2001 From: Valentin Brosseau Date: Thu, 5 Nov 2026 21:25:25 +0100 Subject: [PATCH 2/2] profiles: delete only this bundle ID's own profile, tolerate one already gone The name match is intersected with the bundle ID's profiles, and a profile deleted meanwhile by another install of the same bundle ID (404) is not an error. --- .../DeveloperServicesFetchProfileOperation.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Sources/XKit/DeveloperServices/Profiles/DeveloperServicesFetchProfileOperation.swift b/Sources/XKit/DeveloperServices/Profiles/DeveloperServicesFetchProfileOperation.swift index e00ff9ff..1ae0fb34 100644 --- a/Sources/XKit/DeveloperServices/Profiles/DeveloperServicesFetchProfileOperation.swift +++ b/Sources/XKit/DeveloperServices/Profiles/DeveloperServicesFetchProfileOperation.swift @@ -60,13 +60,21 @@ public struct DeveloperServicesFetchProfileOperation: DeveloperServicesOperation // Several profiles (one made by Xcode or another tool next to ours): keep theirs, but // the one xtool made earlier must go, or creating it again fails on its name // (409 "Multiple profiles found with the name 'XTL profile …'"). + // Only this bundle ID's profiles of that name are deleted; one already gone (another + // install of the same bundle ID deleting it first) is fine. let ownName = "XTL profile \(bundleID.id)" + let bundleProfileIDs = Set(profiles.map(\.id)) let own = try await context.developerAPIClient .profilesGetCollection(query: .init(filter_lbrack_name_rbrack_: [ownName])) .ok.body.json.data - .filter { $0.attributes?.name == ownName } + .filter { $0.attributes?.name == ownName && bundleProfileIDs.contains($0.id) } for profile in own { - _ = try await context.developerAPIClient.profilesDeleteInstance(path: .init(id: profile.id)).noContent + switch try await context.developerAPIClient.profilesDeleteInstance(path: .init(id: profile.id)) { + case .noContent, .notFound: + break + case let other: + _ = try other.noContent + } } }