From 3fec8b33fc064606b77177c214ae58f2484b6dff Mon Sep 17 00:00:00 2001 From: goncalo-frade-iohk Date: Sat, 27 Sep 2025 15:21:54 +0100 Subject: [PATCH] feat(castor)!: http did resolver This is quite extensive PR since it makes the DIDDocument codable. Signed-off-by: goncalo-frade-iohk --- Core/Sources/Helpers/OneOrMany.swift | 2 +- .../Sources/AuthenticateChallenged.swift | 8 +- .../Sources/AuthenticateChallenger.swift | 8 +- .../Builders/Sources/CastorBuilder.swift | 2 +- EdgeAgentSDK/Castor/Sources/CastorImpl.swift | 5 +- .../Operations/CreatePeerDIDOperation.swift | 6 +- .../Operations/CreatePrismDIDOperation.swift | 6 +- .../Resolvers/CompactPrismDIDResolver.swift | 27 + ...pointShortFormPrismDIDRemoteResolver.swift | 38 ++ .../Resolvers/GitHubPrismDIDResolver.swift | 59 +++ .../Resolvers/LongFormPrismDIDResolver.swift | 4 +- .../Sources/Resolvers/PeerDIDResolver.swift | 18 +- .../Tests/CompactPrismDIDResolverTests.swift | 46 ++ ...FormPrismDIDRemoteResolverSmokeTests.swift | 40 ++ .../Castor/Tests/PeerDIDCreationTests.swift | 6 +- .../Castor/Tests/ResolverTestMocks.swift | 70 +++ EdgeAgentSDK/Domain/Sources/Models/DID.swift | 13 +- .../Domain/Sources/Models/DIDDocument.swift | 461 ++++++++++++++++-- .../Domain/Sources/Models/DIDUrl.swift | 13 +- .../DIDCommAgent/DIDCommAgent+DIDs.swift | 6 +- .../DIDCommAgent+Invitations.swift | 6 +- .../Connection/DownloadDataWithResolver.swift | 2 +- .../DIDCommDIDResolverWrapper.swift | 6 +- .../Sources/MecuryImpl+SendMessage.swift | 6 +- .../Pollux/Tests/AnoncredsTests.swift | 74 +-- .../Pollux/Tests/Mocks/MockIssuer.swift | 12 +- 26 files changed, 824 insertions(+), 120 deletions(-) create mode 100644 EdgeAgentSDK/Castor/Sources/Resolvers/CompactPrismDIDResolver.swift create mode 100644 EdgeAgentSDK/Castor/Sources/Resolvers/EndpointShortFormPrismDIDRemoteResolver.swift create mode 100644 EdgeAgentSDK/Castor/Sources/Resolvers/GitHubPrismDIDResolver.swift create mode 100644 EdgeAgentSDK/Castor/Tests/CompactPrismDIDResolverTests.swift create mode 100644 EdgeAgentSDK/Castor/Tests/EndpointShortFormPrismDIDRemoteResolverSmokeTests.swift create mode 100644 EdgeAgentSDK/Castor/Tests/ResolverTestMocks.swift diff --git a/Core/Sources/Helpers/OneOrMany.swift b/Core/Sources/Helpers/OneOrMany.swift index a008b8c5..5480e546 100644 --- a/Core/Sources/Helpers/OneOrMany.swift +++ b/Core/Sources/Helpers/OneOrMany.swift @@ -86,7 +86,7 @@ /// - Parameters: /// - encoder: The encoder to write data to. /// - Throws: An error if encoding fails. -public enum OneOrMany: RawCodable { +public enum OneOrMany: RawCodable { case one(T) case many([T]) diff --git a/EdgeAgentSDK/Authenticate/Sources/AuthenticateChallenged.swift b/EdgeAgentSDK/Authenticate/Sources/AuthenticateChallenged.swift index 3f603a85..c06b01e1 100644 --- a/EdgeAgentSDK/Authenticate/Sources/AuthenticateChallenged.swift +++ b/EdgeAgentSDK/Authenticate/Sources/AuthenticateChallenged.swift @@ -25,8 +25,8 @@ public struct AuthenticateChallenged { masterPublicKey: publicKey, services: [.init( id: "deeplink", - type: ["deeplink"], - serviceEndpoint: [.init(uri: scheme.scheme + "://" + scheme.host)] + type: .one("deeplink"), + serviceEndpoint: .one(.init(uri: scheme.scheme + "://" + scheme.host)) )] ) } @@ -65,8 +65,8 @@ public struct AuthenticateChallenged { let didDocument = try await castor.resolveDID(did: did) guard let service = didDocument.services - .first(where: { $0.type.contains(where: { $0 == "deeplink" }) })? - .serviceEndpoint.first?.uri + .first(where: { $0.type.array.contains(where: { $0 == "deeplink" }) })? + .serviceEndpoint.array.first?.uri else { throw AuthenticateError.cannotFindDeepLinkServiceError } guard diff --git a/EdgeAgentSDK/Authenticate/Sources/AuthenticateChallenger.swift b/EdgeAgentSDK/Authenticate/Sources/AuthenticateChallenger.swift index 9918fe04..eeb61704 100644 --- a/EdgeAgentSDK/Authenticate/Sources/AuthenticateChallenger.swift +++ b/EdgeAgentSDK/Authenticate/Sources/AuthenticateChallenger.swift @@ -25,8 +25,8 @@ public struct AuthenticateChallenger { masterPublicKey: publicKey, services: [.init( id: "deeplink", - type: ["deeplink"], - serviceEndpoint: [.init(uri: scheme.scheme + "://" + scheme.host)] + type: .one("deeplink"), + serviceEndpoint: .one(.init(uri: scheme.scheme + "://" + scheme.host)) )] ) } @@ -54,8 +54,8 @@ public struct AuthenticateChallenger { let didDocument = try await castor.resolveDID(did: did) guard let service = didDocument.services - .first(where: { $0.type.contains(where: { $0 == "deeplink" }) })? - .serviceEndpoint.first?.uri + .first(where: { $0.type.array.contains(where: { $0 == "deeplink" }) })? + .serviceEndpoint.array.first?.uri else { throw AuthenticateError.cannotFindDeepLinkServiceError } guard diff --git a/EdgeAgentSDK/Builders/Sources/CastorBuilder.swift b/EdgeAgentSDK/Builders/Sources/CastorBuilder.swift index 6c6ecede..9eb5c91e 100644 --- a/EdgeAgentSDK/Builders/Sources/CastorBuilder.swift +++ b/EdgeAgentSDK/Builders/Sources/CastorBuilder.swift @@ -8,7 +8,7 @@ public struct CastorBuilder { self.apollo = apollo } - public func build() -> Castor { + public func build(resolvers: [DIDResolverDomain] = []) -> Castor { CastorImpl(apollo: apollo) } } diff --git a/EdgeAgentSDK/Castor/Sources/CastorImpl.swift b/EdgeAgentSDK/Castor/Sources/CastorImpl.swift index 181c8f76..80c608e5 100644 --- a/EdgeAgentSDK/Castor/Sources/CastorImpl.swift +++ b/EdgeAgentSDK/Castor/Sources/CastorImpl.swift @@ -11,7 +11,10 @@ public struct CastorImpl { self.logger = SDKLogger(category: LogComponent.castor) self.apollo = apollo self.resolvers = resolvers + [ - LongFormPrismDIDResolver(apollo: apollo, logger: logger), + CompactPrismDIDResolver( + longFormResolver: LongFormPrismDIDResolver(apollo: apollo, logger: logger), + shortFormResolver: EndpointShortFormPrismDIDRemoteResolver.githubResolver() + ), PeerDIDResolver() ] } diff --git a/EdgeAgentSDK/Castor/Sources/Operations/CreatePeerDIDOperation.swift b/EdgeAgentSDK/Castor/Sources/Operations/CreatePeerDIDOperation.swift index 6057bece..f0194a47 100644 --- a/EdgeAgentSDK/Castor/Sources/Operations/CreatePeerDIDOperation.swift +++ b/EdgeAgentSDK/Castor/Sources/Operations/CreatePeerDIDOperation.swift @@ -23,10 +23,10 @@ struct CreatePeerDIDOperation { authenticationKeys: authenticationKeys, agreementKeys: agreementKeys, services: services.flatMap { service in - service.serviceEndpoint.map { + service.serviceEndpoint.array.map { AnyCodable(dictionaryLiteral: - ("id", service.id), - ("type", service.type.first ?? ""), + ("id", service.id ?? ""), + ("type", service.type.array.first ?? ""), ("serviceEndpoint", [ "uri" : $0.uri, "accept" : $0.accept, diff --git a/EdgeAgentSDK/Castor/Sources/Operations/CreatePrismDIDOperation.swift b/EdgeAgentSDK/Castor/Sources/Operations/CreatePrismDIDOperation.swift index 5f67671f..de7e7fa9 100644 --- a/EdgeAgentSDK/Castor/Sources/Operations/CreatePrismDIDOperation.swift +++ b/EdgeAgentSDK/Castor/Sources/Operations/CreatePrismDIDOperation.swift @@ -45,9 +45,9 @@ struct CreatePrismDIDOperation { didData.publicKeys = try publicKeys.map { try $0.toProto() } didData.services = services.map { var service = Io_Iohk_Atala_Prism_Protos_Service() - service.id = $0.id - service.type = $0.type.first ?? "" - service.serviceEndpoint = $0.serviceEndpoint.map { $0.uri } + service.id = $0.id ?? "" + service.type = $0.type.array.first ?? "" + service.serviceEndpoint = $0.serviceEndpoint.array.map { $0.uri } return service } diff --git a/EdgeAgentSDK/Castor/Sources/Resolvers/CompactPrismDIDResolver.swift b/EdgeAgentSDK/Castor/Sources/Resolvers/CompactPrismDIDResolver.swift new file mode 100644 index 00000000..848d04d9 --- /dev/null +++ b/EdgeAgentSDK/Castor/Sources/Resolvers/CompactPrismDIDResolver.swift @@ -0,0 +1,27 @@ +import Domain + +public struct CompactPrismDIDResolver: DIDResolverDomain { + public let method = "prism" + let longFormResolver: DIDResolverDomain + let shortFormResolver: DIDResolverDomain + + init(longFormResolver: DIDResolverDomain, shortFormResolver: DIDResolverDomain) { + self.longFormResolver = longFormResolver + self.shortFormResolver = shortFormResolver + } + + public func resolve(did: Domain.DID) async throws -> Domain.DIDDocument { + do { + return try await shortFormResolver.resolve(did: did) + } catch { + guard did.isLongFormDID() else { throw error } + return try await longFormResolver.resolve(did: did) + } + } +} + +private extension DID { + func isLongFormDID() -> Bool { + return string.split(separator: ":").count > 3 + } +} diff --git a/EdgeAgentSDK/Castor/Sources/Resolvers/EndpointShortFormPrismDIDRemoteResolver.swift b/EdgeAgentSDK/Castor/Sources/Resolvers/EndpointShortFormPrismDIDRemoteResolver.swift new file mode 100644 index 00000000..5e3fec50 --- /dev/null +++ b/EdgeAgentSDK/Castor/Sources/Resolvers/EndpointShortFormPrismDIDRemoteResolver.swift @@ -0,0 +1,38 @@ +import Domain +import Foundation + +public protocol DIDDocumentUrlBuilder { + func didDocumentEndpoint(did: DID) throws -> String +} + +public protocol DIDDocumentSerializer { + func serialize(data: Data) throws -> DIDDocument +} + +public struct EndpointShortFormPrismDIDRemoteResolver: DIDResolverDomain { + public let method = "prism" + let urlBuilder: DIDDocumentUrlBuilder + let downloader: Downloader + let serializer: DIDDocumentSerializer + + init(urlBuilder: DIDDocumentUrlBuilder, downloader: Downloader, serializer: DIDDocumentSerializer) { + self.urlBuilder = urlBuilder + self.downloader = downloader + self.serializer = serializer + } + + public func resolve(did: DID) async throws -> DIDDocument { + let did = try did.removingPrismLongForm() + let data = try await downloader.downloadFromEndpoint(urlOrDID: urlBuilder.didDocumentEndpoint(did: did)) + return try serializer.serialize(data: data) + } +} + +private extension DID { + func removingPrismLongForm() throws -> DID { + let separated = string + .split(separator: ":") + let shortForm = separated.prefix(3).joined(separator: ":") + return try DID(string: shortForm) + } +} diff --git a/EdgeAgentSDK/Castor/Sources/Resolvers/GitHubPrismDIDResolver.swift b/EdgeAgentSDK/Castor/Sources/Resolvers/GitHubPrismDIDResolver.swift new file mode 100644 index 00000000..e524d492 --- /dev/null +++ b/EdgeAgentSDK/Castor/Sources/Resolvers/GitHubPrismDIDResolver.swift @@ -0,0 +1,59 @@ +import Domain +import Foundation + +public extension EndpointShortFormPrismDIDRemoteResolver { + static func githubResolver() -> Self { + .init( + urlBuilder: GitHubResolverURLBuilder(), + downloader: DownloadDataWithResolver(), + serializer: GitHubResolverDIDDocumentSerializer() + ) + } +} + +public struct GitHubResolverURLBuilder: DIDDocumentUrlBuilder { + let baseURL = "https://raw.githubusercontent.com/FabioPinheiro/prism-vdr/refs/heads/main/mainnet/diddoc/" + + public func didDocumentEndpoint(did: DID) throws -> String { + return baseURL.appending(did.string) + } +} + +public struct GitHubResolverDIDDocumentSerializer: DIDDocumentSerializer { + let coder: JSONDecoder + + init(coder: JSONDecoder = .normalized) { + self.coder = coder + } + + public func serialize(data: Data) throws -> DIDDocument { + return try coder.decode(DIDDocument.self, from: data) + } +} + +fileprivate struct DownloadDataWithResolver: Downloader { + + func downloadFromEndpoint(urlOrDID: String) async throws -> Data { + let url: URL + + if let validUrl = URL(string: urlOrDID.replacingOccurrences(of: "host.docker.internal", with: "localhost")) { + url = validUrl + } else { + throw CommonError.invalidURLError(url: urlOrDID) + } + + let (data, urlResponse) = try await URLSession.shared.data(from: url) + + guard + let code = (urlResponse as? HTTPURLResponse)?.statusCode, + 200...299 ~= code + else { + throw CommonError.httpError( + code: (urlResponse as? HTTPURLResponse)?.statusCode ?? 500, + message: String(data: data, encoding: .utf8) ?? "" + ) + } + + return data + } +} diff --git a/EdgeAgentSDK/Castor/Sources/Resolvers/LongFormPrismDIDResolver.swift b/EdgeAgentSDK/Castor/Sources/Resolvers/LongFormPrismDIDResolver.swift index c97f28ec..bf57817c 100644 --- a/EdgeAgentSDK/Castor/Sources/Resolvers/LongFormPrismDIDResolver.swift +++ b/EdgeAgentSDK/Castor/Sources/Resolvers/LongFormPrismDIDResolver.swift @@ -108,8 +108,8 @@ struct LongFormPrismDIDResolver: DIDResolverDomain { let services = operation.createDid.didData.services.map { DIDDocument.Service( id: $0.id, - type: [$0.type], - serviceEndpoint: $0.serviceEndpoint.map { .init(uri: $0) } + type: .one($0.type), + serviceEndpoint: .many($0.serviceEndpoint.map { .init(uri: $0) }) ) } let groupByPurpose = Dictionary( diff --git a/EdgeAgentSDK/Castor/Sources/Resolvers/PeerDIDResolver.swift b/EdgeAgentSDK/Castor/Sources/Resolvers/PeerDIDResolver.swift index 6e52c984..b3e42b68 100644 --- a/EdgeAgentSDK/Castor/Sources/Resolvers/PeerDIDResolver.swift +++ b/EdgeAgentSDK/Castor/Sources/Resolvers/PeerDIDResolver.swift @@ -39,10 +39,10 @@ extension DIDCore.DIDDocument { let keyAgreementIds = from.keyAgreement.map(\.id.string) let services = from.services.flatMap { service in - service.serviceEndpoint.map { + service.serviceEndpoint.array.map { DIDCore.DIDDocument.Service( - id: service.id, - type: service.type.first ?? "", + id: service.id ?? "", + type: service.type.array.first ?? "", serviceEndpoint: AnyCodable( dictionaryLiteral: ("uri", $0.uri), @@ -108,26 +108,26 @@ extension DIDCore.DIDDocument { } return Domain.DIDDocument.Service( id: service.id, - type: [service.type], - serviceEndpoint: [ + type: .one(service.type), + serviceEndpoint: .one( .init( uri: uri, accept: endpoint["accept"] as? [String] ?? [], routingKeys: endpoint["routing_keys"] as? [String] ?? [] ) - ] + ) ) case let endpoint as String: return Domain.DIDDocument.Service( id: service.id, - type: [service.type], - serviceEndpoint: [ + type: .one(service.type), + serviceEndpoint: .one( .init( uri: endpoint, accept: ($0.value as? [String: Any])?["accept"] as? [String] ?? [], routingKeys: ($0.value as? [String: Any])?["routing_keys"] as? [String] ?? [] ) - ] + ) ) default: throw CastorError.notPossibleToResolveDID(did: service.id, reason: "Invalid service") diff --git a/EdgeAgentSDK/Castor/Tests/CompactPrismDIDResolverTests.swift b/EdgeAgentSDK/Castor/Tests/CompactPrismDIDResolverTests.swift new file mode 100644 index 00000000..9861833d --- /dev/null +++ b/EdgeAgentSDK/Castor/Tests/CompactPrismDIDResolverTests.swift @@ -0,0 +1,46 @@ +import XCTest +import Domain +@testable import Castor + +final class CompactPrismDIDResolverTests: XCTestCase { + func testUsesShortFormResolverWhenItSucceeds() async throws { + let did = DID(method: "prism", methodId: "abc") + let expected = DIDDocument(id: did, coreProperties: []) + + let short = DIDResolverMock(result: .success(expected)) + let long = DIDResolverMock(result: .failure(MockError.forced)) + let sut = CompactPrismDIDResolver(longFormResolver: long, shortFormResolver: short) + let doc = try await sut.resolve(did: did) + + XCTAssertEqual(doc.id, did) + XCTAssertEqual(short.calls, [did]) + XCTAssertTrue(long.calls.isEmpty) + } + + func testFallsBackToLongFormWhenShortFailsAndDIDIsLongForm() async throws { + let longForm = DID(method: "prism", methodId: "abc:def") + let expected = DIDDocument(id: longForm, coreProperties: []) + + let short = DIDResolverMock(result: .failure(MockError.forced)) + let long = DIDResolverMock(result: .success(expected)) + let sut = CompactPrismDIDResolver(longFormResolver: long, shortFormResolver: short) + let doc = try await sut.resolve(did: longForm) + + XCTAssertEqual(doc.id, longForm) + XCTAssertEqual(short.calls, [longForm]) + XCTAssertEqual(long.calls, [longForm]) + } + + func testRethrowsWhenShortFailsAndDIDIsShortForm() async { + let shortForm = DID(method: "prism", methodId: "abc") + let short = DIDResolverMock(result: .failure(MockError.forced)) + let long = DIDResolverMock(result: .success(DIDDocument(id: shortForm, coreProperties: []))) + let sut = CompactPrismDIDResolver(longFormResolver: long, shortFormResolver: short) + do { + _ = try await sut.resolve(did: shortForm) + XCTFail("Expected error") + } catch { + XCTAssertTrue(long.calls.isEmpty) + } + } +} diff --git a/EdgeAgentSDK/Castor/Tests/EndpointShortFormPrismDIDRemoteResolverSmokeTests.swift b/EdgeAgentSDK/Castor/Tests/EndpointShortFormPrismDIDRemoteResolverSmokeTests.swift new file mode 100644 index 00000000..494043bf --- /dev/null +++ b/EdgeAgentSDK/Castor/Tests/EndpointShortFormPrismDIDRemoteResolverSmokeTests.swift @@ -0,0 +1,40 @@ +import Domain +@testable import Castor +import XCTest + +final class EndpointShortFormPrismDIDRemoteResolverSmokeTests: XCTestCase { + func testMethodIsPrism() { + let sut = EndpointShortFormPrismDIDRemoteResolver( + urlBuilder: DIDDocumentUrlBuilderMock(), + downloader: DownloaderMock(result: .failure(MockError.forced)), + serializer: DIDDocumentSerializerMock(result: .failure(MockError.forced)) + ) + XCTAssertEqual(sut.method, "prism") + } + + func testTrimsLongFormBeforeBuildingURL() async { + let longForm = DID(method: "prism", methodId: "abc:def") + let expectedShort = DID(method: "prism", methodId: "abc") + let urlBuilder = DIDDocumentUrlBuilderMock() + let downloader = DownloaderMock(result: .success(Data("{}".utf8))) + let serializer = DIDDocumentSerializerMock(result: .failure(MockError.forced)) + + let sut = EndpointShortFormPrismDIDRemoteResolver( + urlBuilder: urlBuilder, + downloader: downloader, + serializer: serializer + ) + + do { + _ = try await sut.resolve(did: longForm) + XCTFail("Expected error") + } catch { + XCTAssertEqual(urlBuilder.dids.first, expectedShort) + } + } + + func testIntegrationWithGithubResolver() async throws { + let githubResolver = EndpointShortFormPrismDIDRemoteResolver.githubResolver() + let document = try await githubResolver.resolve(did: DID(string: "did:prism:076b993f6070d39ee0f0964970ef3d07af3e821cb51106952100fa803b03cc51")) + } +} diff --git a/EdgeAgentSDK/Castor/Tests/PeerDIDCreationTests.swift b/EdgeAgentSDK/Castor/Tests/PeerDIDCreationTests.swift index 87bfc243..301427c6 100644 --- a/EdgeAgentSDK/Castor/Tests/PeerDIDCreationTests.swift +++ b/EdgeAgentSDK/Castor/Tests/PeerDIDCreationTests.swift @@ -25,11 +25,11 @@ final class PeerDIDCreationTests: XCTestCase { let service = DIDDocument.Service( id: "didcomm", - type: ["DIDCommMessaging"], - serviceEndpoint: [.init( + type: .one("DIDCommMessaging"), + serviceEndpoint: .one(.init( uri: "https://example.com/endpoint", routingKeys: ["did:example:somemediator#somekey"] - )] + )) ) let did = try castor.createPeerDID( keyAgreementPublicKey: keyAgreementPrivateKey.publicKey(), diff --git a/EdgeAgentSDK/Castor/Tests/ResolverTestMocks.swift b/EdgeAgentSDK/Castor/Tests/ResolverTestMocks.swift new file mode 100644 index 00000000..615a0e1f --- /dev/null +++ b/EdgeAgentSDK/Castor/Tests/ResolverTestMocks.swift @@ -0,0 +1,70 @@ +import Foundation +import Domain +@testable import Castor + +// Simple error for forcing failures in mocks +enum MockError: Error { case forced } + +// MARK: - Downloader Mock +final class DownloaderMock: Downloader { + private(set) var requests: [String] = [] + var result: Result + + init(result: Result) { + self.result = result + } + + func downloadFromEndpoint(urlOrDID: String) async throws -> Data { + requests.append(urlOrDID) + return try result.get() + } +} + +// MARK: - URL Builder Mock +final class DIDDocumentUrlBuilderMock: DIDDocumentUrlBuilder { + private(set) var dids: [DID] = [] + var endpoint: String + var error: Error? + + init(endpoint: String = "https://example.com/did.json", error: Error? = nil) { + self.endpoint = endpoint + self.error = error + } + + func didDocumentEndpoint(did: DID) throws -> String { + dids.append(did) + if let error { throw error } + return endpoint + } +} + +// MARK: - Serializer Mock +final class DIDDocumentSerializerMock: DIDDocumentSerializer { + private(set) var datas: [Data] = [] + var result: Result + + init(result: Result) { + self.result = result + } + + func serialize(data: Data) throws -> DIDDocument { + datas.append(data) + return try result.get() + } +} + +// MARK: - Generic DID Resolver Mock +final class DIDResolverMock: DIDResolverDomain { + var method: DIDMethod = "prism" + private(set) var calls: [DID] = [] + var result: Result + + init(result: Result) { + self.result = result + } + + func resolve(did: DID) async throws -> DIDDocument { + calls.append(did) + return try result.get() + } +} diff --git a/EdgeAgentSDK/Domain/Sources/Models/DID.swift b/EdgeAgentSDK/Domain/Sources/Models/DID.swift index 46e2dfda..19bb06a3 100644 --- a/EdgeAgentSDK/Domain/Sources/Models/DID.swift +++ b/EdgeAgentSDK/Domain/Sources/Models/DID.swift @@ -8,7 +8,7 @@ public typealias DIDMethodId = String /// A DID is a unique and persistent identifier for a subject or object, such as a person, organization, or device. It is created and managed using a specific DID method, and consists of a schema, method, and method ID. The schema indicates the type of DID (e.g. "did"), the method indicates the specific protocol or process used to resolve and manage the DID (e.g. "prism"), and the method ID is a unique identifier within the DID method. /// As specified in the [W3C DID standards](https://www.w3.org/TR/did-core/#dfn-did-schemes). -public struct DID: Equatable { +public struct DID: Hashable, Equatable, Codable { /// The schema of the DID (e.g. "did") public let schema: String @@ -33,6 +33,17 @@ public struct DID: Equatable { self.methodId = methodId } + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + try self.init(string: string) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(string) + } + /// String representation of this DID as specified in [w3 standards](https://www.w3.org/TR/did-core/#dfn-did-schemes) /// This is a combination of the schema, method, and method ID, separated by colons (e.g. "did:prism:0xabc123"). public var string: String { "\(schema):\(method):\(methodId)" } diff --git a/EdgeAgentSDK/Domain/Sources/Models/DIDDocument.swift b/EdgeAgentSDK/Domain/Sources/Models/DIDDocument.swift index a6fd24c2..4982741c 100644 --- a/EdgeAgentSDK/Domain/Sources/Models/DIDDocument.swift +++ b/EdgeAgentSDK/Domain/Sources/Models/DIDDocument.swift @@ -1,18 +1,50 @@ +import Core import Foundation /// Represents a Core Property in a DID Document. /// This allows for extensability of the properties. /// /// As specified in [w3 standards](https://www.w3.org/TR/did-core/#data-model) -public protocol DIDDocumentCoreProperty {} +public protocol DIDDocumentCoreProperty: RawCodable {} /// Represents a DIDDocument with ``DID`` and ``[DIDDocumentCoreProperty]`` /// As specified in [w3 standards](https://www.w3.org/TR/did-core/#data-model) /// A DID Document consists of a DID, public keys, authentication protocols, service endpoints, and other metadata. It is used to verify the authenticity and identity of the DID, and to discover and interact with the associated subjects or objects. -public struct DIDDocument { +public struct DIDDocument: RawCodable{ + + enum CodingKeys: CodingKey { + case id + case alsoKnownAs + case controller + case service + case verificationMethod + case authentication + case assertionMethod + case keyAgreement + case capabilityInvocation + case capabilityDelegation + } + + struct URLOrVerificationMethodCoder: RawCodable { + let raw: AnyCodable? + + init(raw: AnyCodable? = nil) { + self.raw = raw + } + + init(from decoder: any Decoder) throws { + self.raw = try AnyCodable(from: decoder) + } + + func encode(to encoder: any Encoder) throws { + guard let raw else { return } + try raw.encode(to: encoder) + } + } + /// Represents a Verification Method, which is a public key or other evidence used to authenticate the identity of a Decentralized Identifier (DID) or other subject or object. /// /// A Verification Method consists of a type (indicating the type of key or evidence), a public key or other data, and optional metadata such as a controller (the DID that controls the verification method) and purpose (the intended use of the verification method). It is typically included in a DID Document or other authentication credential. - public struct VerificationMethod { + public struct VerificationMethod: RawCodable { /// The ID of the verification method, represented as a DID URL. public let id: DIDUrl @@ -27,19 +59,22 @@ public struct DIDDocument { /// The public key of the verification method, represented as a multibase encoded string. public let publicKeyMultibase: String? + public let raw: AnyCodable? public init( id: DIDUrl, controller: DID, type: String, publicKeyJwk: [String: String]? = nil, - publicKeyMultibase: String? = nil + publicKeyMultibase: String? = nil, + raw: AnyCodable? = nil ) { self.id = id self.controller = controller self.type = type self.publicKeyJwk = publicKeyJwk self.publicKeyMultibase = publicKeyMultibase + self.raw = raw } // // public var publicKey: PublicKey? { @@ -52,9 +87,23 @@ public struct DIDDocument { /// Represents a Service, which is a capability or endpoint offered by a Decentralized Identifier (DID) or other subject or object. /// /// A Service consists of an ID, type, and service endpoint, as well as optional metadata such as a priority and a description. It is typically included in a DID Document and can be used to discover and interact with the associated DID or subject or object. - public struct Service: DIDDocumentCoreProperty { + public struct Service: RawCodable { + + enum CodingKeys: CodingKey { + case id + case type + case serviceEndpoint + } + /// Represents a service endpoint, which is a URI and other information that indicates how to access the service. - public struct ServiceEndpoint { + public struct ServiceEndpoint: RawCodable { + + public enum CodingKeys: CodingKey { + case uri + case accept + case routingKeys + } + /// The URI of the service endpoint. public let uri: String @@ -63,35 +112,94 @@ public struct DIDDocument { /// The routing keys that can be used to route messages to the service endpoint. public let routingKeys: [String] + public let raw: AnyCodable? public init( uri: String, accept: [String] = [], - routingKeys: [String] = [] + routingKeys: [String] = [], + raw: AnyCodable? = nil ) { self.uri = uri self.accept = accept self.routingKeys = routingKeys + self.raw = raw + } + + public init(from decoder: any Decoder) throws { + if + let singleValueContainer = try? decoder.singleValueContainer(), + let uri = try? singleValueContainer.decode(String.self) + { + self.uri = uri + self.accept = [] + self.routingKeys = [] + + } else { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.uri = try container.decode(String.self, forKey: .uri) + self.accept = try container.decode([String].self, forKey: .accept) + self.routingKeys = try container.decode([String].self, forKey: .routingKeys) + } + self.raw = try AnyCodable(from: decoder) + } + + public func encode(to encoder: any Encoder) throws { + guard let raw else { + guard accept.isEmpty || routingKeys.isEmpty else { + var container = encoder.singleValueContainer() + try container.encode(uri) + return + } + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(uri, forKey: .uri) + try container.encode(accept, forKey: .accept) + try container.encode(routingKeys, forKey: .routingKeys) + return + } + try raw.encode(to: encoder) } } /// The ID of the service, represented as a string. - public let id: String + public let id: String? /// The types of the service, indicated as an array of strings. - public let type: [String] + public let type: OneOrMany /// The service endpoint of the service. - public let serviceEndpoint: [ServiceEndpoint] + public let serviceEndpoint: OneOrMany + public let raw: AnyCodable? public init( id: String, - type: [String], - serviceEndpoint: [ServiceEndpoint] + type: OneOrMany, + serviceEndpoint: OneOrMany, + raw: AnyCodable? = nil ) { self.id = id self.type = type self.serviceEndpoint = serviceEndpoint + self.raw = raw + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decodeIfPresent(String.self, forKey: .id) + self.type = try container.decode(OneOrMany.self, forKey: .type) + self.serviceEndpoint = try container.decode(OneOrMany.self, forKey: .serviceEndpoint) + self.raw = try AnyCodable(from: decoder) + } + + public func encode(to encoder: any Encoder) throws { + guard let raw else { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(self.id, forKey: .id) + try container.encode(self.type, forKey: .type) + try container.encode(self.serviceEndpoint, forKey: .serviceEndpoint) + return + } + try raw.encode(to: encoder) } } @@ -100,10 +208,27 @@ public struct DIDDocument { /// The "also known as" property is typically included in a DID Document and can be used to associate the DID or subject or object with other names or identifiers. public struct AlsoKnownAs: DIDDocumentCoreProperty { /// The values of the "also known as" property, represented as an array of strings. - public let values: [String] + public let values: Set + public let raw: AnyCodable? - public init(values: [String]) { + public init(values: Set, raw: AnyCodable? = nil) { self.values = values + self.raw = raw + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + self.values = Set(try container.decode(OneOrMany.self).array) + self.raw = try AnyCodable(from: decoder) + } + + public func encode(to encoder: any Encoder) throws { + guard let raw else { + var container = encoder.singleValueContainer() + try container.encode(self.values) + return + } + try raw.encode(to: encoder) } } @@ -112,10 +237,27 @@ public struct DIDDocument { /// The "controller" property is typically included in a DID Document and can be used to indicate who has the authority to update or deactivate the DID or subject or object. public struct Controller: DIDDocumentCoreProperty { /// The values of the "controller" property, represented as an array of DIDs. - public let values: [DID] + public let values: Set + public let raw: AnyCodable? - public init(values: [DID]) { + public init(values: Set, raw: AnyCodable? = nil) { self.values = values + self.raw = raw + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + self.values = Set(try container.decode(OneOrMany.self).array) + self.raw = try AnyCodable(from: decoder) + } + + public func encode(to encoder: any Encoder) throws { + guard let raw else { + var container = encoder.singleValueContainer() + try container.encode(self.values) + return + } + try raw.encode(to: encoder) } } @@ -125,9 +267,26 @@ public struct DIDDocument { public struct VerificationMethods: DIDDocumentCoreProperty { /// The values of the "verification methods" property, represented as an array of VerificationMethod structs. public let values: [VerificationMethod] + public let raw: AnyCodable? - public init(values: [VerificationMethod]) { + public init(values: [VerificationMethod], raw: AnyCodable? = nil) { self.values = values + self.raw = raw + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + self.values = try container.decode([VerificationMethod].self) + self.raw = try AnyCodable(from: decoder) + } + + public func encode(to encoder: any Encoder) throws { + guard let raw else { + var container = encoder.singleValueContainer() + try container.encode(self.values) + return + } + try raw.encode(to: encoder) } } @@ -137,9 +296,26 @@ public struct DIDDocument { public struct Services: DIDDocumentCoreProperty { /// The values of the "services" property, represented as an array of Service structs. public let values: [Service] + public let raw: AnyCodable? - public init(values: [Service]) { + public init(values: [Service], raw: AnyCodable? = nil) { self.values = values + self.raw = raw + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + self.values = try container.decode([Service].self) + self.raw = try AnyCodable(from: decoder) + } + + public func encode(to encoder: any Encoder) throws { + guard let raw else { + var container = encoder.singleValueContainer() + try container.encode(self.values) + return + } + try raw.encode(to: encoder) } } @@ -152,10 +328,30 @@ public struct DIDDocument { /// The Verification Methods of the authentication property. public let verificationMethods: [VerificationMethod] + public let raw: AnyCodable? - public init(urls: [String], verificationMethods: [VerificationMethod]) { + public init(urls: [String], verificationMethods: [VerificationMethod], raw: AnyCodable? = nil) { self.urls = urls self.verificationMethods = verificationMethods + self.raw = raw + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let decoded = try container.decode([URLOrVerificationMethodCoder].self) + self.urls = decoded.compactMap { try? $0.decodedAs() } + self.verificationMethods = decoded.compactMap { try? $0.decodedAs() } + self.raw = try AnyCodable(from: decoder) + } + + public func encode(to encoder: any Encoder) throws { + guard let raw else { + var container = encoder.singleValueContainer() + let mapped = urls.map { URLOrVerificationMethodCoder(raw: AnyCodable($0)) } + verificationMethods.map { URLOrVerificationMethodCoder(raw: AnyCodable($0))} + try container.encode(mapped) + return + } + try raw.encode(to: encoder) } } @@ -168,10 +364,30 @@ public struct DIDDocument { /// The Verification Methods of the assertion method property. public let verificationMethods: [VerificationMethod] + public let raw: AnyCodable? - public init(urls: [String], verificationMethods: [VerificationMethod]) { + public init(urls: [String], verificationMethods: [VerificationMethod], raw: AnyCodable? = nil) { self.urls = urls self.verificationMethods = verificationMethods + self.raw = raw + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let decoded = try container.decode([URLOrVerificationMethodCoder].self) + self.urls = decoded.compactMap { try? $0.decodedAs() } + self.verificationMethods = decoded.compactMap { try? $0.decodedAs() } + self.raw = try AnyCodable(from: decoder) + } + + public func encode(to encoder: any Encoder) throws { + guard let raw else { + var container = encoder.singleValueContainer() + let mapped = urls.map { URLOrVerificationMethodCoder(raw: AnyCodable($0)) } + verificationMethods.map { URLOrVerificationMethodCoder(raw: AnyCodable($0))} + try container.encode(mapped) + return + } + try raw.encode(to: encoder) } } @@ -184,11 +400,31 @@ public struct DIDDocument { /// The Verification Methods of the key agreement property. public let verificationMethods: [VerificationMethod] + public let raw: AnyCodable? /// Initializes the KeyAgreement struct with an array of URIs and an array of VerificationMethods. - public init(urls: [String], verificationMethods: [VerificationMethod]) { + public init(urls: [String], verificationMethods: [VerificationMethod], raw: AnyCodable? = nil) { self.urls = urls self.verificationMethods = verificationMethods + self.raw = raw + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let decoded = try container.decode([URLOrVerificationMethodCoder].self) + self.urls = decoded.compactMap { try? $0.decodedAs() } + self.verificationMethods = decoded.compactMap { try? $0.decodedAs() } + self.raw = try AnyCodable(from: decoder) + } + + public func encode(to encoder: any Encoder) throws { + guard let raw else { + var container = encoder.singleValueContainer() + let mapped = urls.map { URLOrVerificationMethodCoder(raw: AnyCodable($0)) } + verificationMethods.map { URLOrVerificationMethodCoder(raw: AnyCodable($0))} + try container.encode(mapped) + return + } + try raw.encode(to: encoder) } } @@ -201,10 +437,30 @@ public struct DIDDocument { /// The Verification Methods of the capability invocation property. public let verificationMethods: [VerificationMethod] + public let raw: AnyCodable? - public init(urls: [String], verificationMethods: [VerificationMethod]) { + public init(urls: [String], verificationMethods: [VerificationMethod], raw: AnyCodable? = nil) { self.urls = urls self.verificationMethods = verificationMethods + self.raw = raw + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let decoded = try container.decode([URLOrVerificationMethodCoder].self) + self.urls = decoded.compactMap { try? $0.decodedAs() } + self.verificationMethods = decoded.compactMap { try? $0.decodedAs() } + self.raw = try AnyCodable(from: decoder) + } + + public func encode(to encoder: any Encoder) throws { + guard let raw else { + var container = encoder.singleValueContainer() + let mapped = urls.map { URLOrVerificationMethodCoder(raw: AnyCodable($0)) } + verificationMethods.map { URLOrVerificationMethodCoder(raw: AnyCodable($0))} + try container.encode(mapped) + return + } + try raw.encode(to: encoder) } } @@ -217,25 +473,126 @@ public struct DIDDocument { /// The Verification Methods of the capability delegation property. public let verificationMethods: [VerificationMethod] + public let raw: AnyCodable? - public init(urls: [String], verificationMethods: [VerificationMethod]) { + public init(urls: [String], verificationMethods: [VerificationMethod], raw: AnyCodable? = nil) { self.urls = urls self.verificationMethods = verificationMethods + self.raw = raw + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let decoded = try container.decode([URLOrVerificationMethodCoder].self) + self.urls = decoded.compactMap { try? $0.decodedAs() } + self.verificationMethods = decoded.compactMap { try? $0.decodedAs() } + self.raw = try AnyCodable(from: decoder) + } + + public func encode(to encoder: any Encoder) throws { + guard let raw else { + var container = encoder.singleValueContainer() + let mapped = urls.map { URLOrVerificationMethodCoder(raw: AnyCodable($0)) } + verificationMethods.map { URLOrVerificationMethodCoder(raw: AnyCodable($0))} + try container.encode(mapped) + return + } + try raw.encode(to: encoder) } } public let id: DID public let coreProperties: [DIDDocumentCoreProperty] + public let raw: AnyCodable? - public init(id: DID, coreProperties: [DIDDocumentCoreProperty]) { + public init( + id: DID, + coreProperties: [DIDDocumentCoreProperty], + raw: AnyCodable? = nil + ) { self.id = id self.coreProperties = coreProperties + self.raw = raw + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(DID.self, forKey: .id) + let controller = try container.decodeIfPresent(Controller.self, forKey: .controller) + let alsoKnownAs = try container.decodeIfPresent(AlsoKnownAs.self, forKey: .alsoKnownAs) + let verificationMethods = try container.decodeIfPresent(VerificationMethods.self, forKey: .verificationMethod) + let authenticationMethods = try container.decodeIfPresent(Authentication.self, forKey: .authentication) + let assertionMethods = try container.decodeIfPresent(AssertionMethod.self, forKey: .assertionMethod) + let keyAgreementMethods = try container.decodeIfPresent(KeyAgreement.self, forKey: .keyAgreement) + let capabilityInvocationMethods = try container.decodeIfPresent(CapabilityInvocation.self, forKey: .capabilityInvocation) + let capabilityDelegationMethods = try container.decodeIfPresent(CapabilityDelegation.self, forKey: .capabilityDelegation) + let services = try container.decodeIfPresent(Services.self, forKey: .service) + + let coreProperties: [DIDDocumentCoreProperty?] = [ + controller, + alsoKnownAs, + verificationMethods, + authenticationMethods, + assertionMethods, + keyAgreementMethods, + capabilityInvocationMethods, + capabilityDelegationMethods, + services + ] + self.coreProperties = coreProperties.compactMap { $0 } + self.raw = try AnyCodable(from: decoder) + } + + public func encode(to encoder: any Encoder) throws { + guard let raw else { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + let alsoKnownAs = coreProperties.first { $0 is AlsoKnownAs } as? AlsoKnownAs + let controller = coreProperties.first { $0 is Controller } as? Controller + let verificationMethods = coreProperties.first { $0 is VerificationMethods } as? VerificationMethods + let authenticationMethods = coreProperties.first { $0 is Authentication } as? Authentication + let assertionMethods = coreProperties.first { $0 is AssertionMethod } as? AssertionMethod + let keyAgreementMethods = coreProperties.first { $0 is KeyAgreement } as? KeyAgreement + let capabilityInvocationMethods = coreProperties.first { $0 is CapabilityInvocation } as? CapabilityInvocation + let capabilityDelegationMethods = coreProperties.first { $0 is CapabilityDelegation } as? CapabilityDelegation + let services = coreProperties.first { $0 is Services } as? Services + try container.encodeIfPresent(controller, forKey: .controller) + try container.encodeIfPresent(alsoKnownAs, forKey: .alsoKnownAs) + try container.encodeIfPresent(verificationMethods, forKey: .verificationMethod) + try container.encodeIfPresent(authenticationMethods, forKey: .authentication) + try container.encodeIfPresent(assertionMethods, forKey: .assertionMethod) + try container.encodeIfPresent(keyAgreementMethods, forKey: .keyAgreement) + try container.encodeIfPresent(capabilityInvocationMethods, forKey: .capabilityInvocation) + try container.encodeIfPresent(capabilityDelegationMethods, forKey: .capabilityDelegation) + try container.encodeIfPresent(services, forKey: .service) + return + } + try raw.encode(to: encoder) + } + + public var controller: Set { + guard + let property = coreProperties + .first(where: { $0 is Controller }) + .map({ $0 as? Controller }), + let controllerProperty = property + else { return [] } + return Set(controllerProperty.values) + } + + public var alsoKnownAs: Set { + guard + let property = coreProperties + .first(where: { $0 is AlsoKnownAs }) + .map({ $0 as? Controller }), + let controllerProperty = property + else { return [] } + return Set(controllerProperty.values) } public var authenticate: [VerificationMethod] { guard let property = coreProperties - .first(where: { $0 as? Authentication != nil }) + .first(where: { $0 is Authentication }) .map({ $0 as? Authentication }), let authenticateProperty = property else { return [] } @@ -248,10 +605,26 @@ public struct DIDDocument { return authenticateProperty.verificationMethods } + public var assertion: [VerificationMethod] { + guard + let property = coreProperties + .first(where: { $0 is AssertionMethod }) + .map({ $0 as? AssertionMethod }), + let assertionProperty = property + else { return [] } + + guard assertionProperty.urls.isEmpty else { + return assertionProperty.urls.compactMap { uri in + verificationMethods.first { $0.id.string == uri } + } + assertionProperty.verificationMethods + } + return assertionProperty.verificationMethods + } + public var keyAgreement: [VerificationMethod] { guard let property = coreProperties - .first(where: { $0 as? KeyAgreement != nil }) + .first(where: { $0 is KeyAgreement }) .map({ $0 as? KeyAgreement }), let keyAgreementProperty = property else { return [] } @@ -264,10 +637,42 @@ public struct DIDDocument { return keyAgreementProperty.verificationMethods } + public var capabilityInvocation: [VerificationMethod] { + guard + let property = coreProperties + .first(where: { $0 is CapabilityInvocation }) + .map({ $0 as? CapabilityInvocation }), + let capabilityInvocationProperty = property + else { return [] } + + guard capabilityInvocationProperty.urls.isEmpty else { + return capabilityInvocationProperty.urls.compactMap { uri in + verificationMethods.first { $0.id.string == uri } + } + capabilityInvocationProperty.verificationMethods + } + return capabilityInvocationProperty.verificationMethods + } + + public var capabilityDelegation: [VerificationMethod] { + guard + let property = coreProperties + .first(where: { $0 is CapabilityDelegation }) + .map({ $0 as? CapabilityDelegation }), + let capabilityDelegationProperty = property + else { return [] } + + guard capabilityDelegationProperty.urls.isEmpty else { + return capabilityDelegationProperty.urls.compactMap { uri in + verificationMethods.first { $0.id.string == uri } + } + capabilityDelegationProperty.verificationMethods + } + return capabilityDelegationProperty.verificationMethods + } + public var verificationMethods: [VerificationMethod] { guard let property = coreProperties - .first(where: { $0 as? VerificationMethods != nil }) + .first(where: { $0 is VerificationMethods }) .map({ $0 as? VerificationMethods }), let verificationMethodsProperty = property else { return [] } @@ -278,7 +683,7 @@ public struct DIDDocument { public var services: [Service] { guard let property = coreProperties - .first(where: { $0 as? Services != nil }) + .first(where: { $0 is Services }) .map({ $0 as? Services }), let servicesProperty = property else { return [] } diff --git a/EdgeAgentSDK/Domain/Sources/Models/DIDUrl.swift b/EdgeAgentSDK/Domain/Sources/Models/DIDUrl.swift index 13a7bc05..ea44a2fa 100644 --- a/EdgeAgentSDK/Domain/Sources/Models/DIDUrl.swift +++ b/EdgeAgentSDK/Domain/Sources/Models/DIDUrl.swift @@ -2,7 +2,7 @@ import Foundation /// Represents a DIDUrl with "did", "path", "parameters", "fragment" /// As specified in [w3 standards](`https://www.w3.org/TR/did-core/#dfn-did-urls`) -public struct DIDUrl { +public struct DIDUrl: Hashable, Equatable, Codable { /// The associated Decentralized Identifier (DID). public let did: DID @@ -85,6 +85,17 @@ public struct DIDUrl { } } + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + try self.init(string: string) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(string) + } + /// A string representation of this `DIDUrl`. public var string: String { did.string + pathString + queryString + fragmentString diff --git a/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+DIDs.swift b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+DIDs.swift index 80b80184..168ef7db 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+DIDs.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+DIDs.swift @@ -68,10 +68,10 @@ public extension DIDCommAgent { if updateMediator, let routingDID = mediatorRoutingDID?.string { withServices = services + [.init( id: "#didcomm-1", - type: ["DIDCommMessaging"], - serviceEndpoint: [.init( + type: .one("DIDCommMessaging"), + serviceEndpoint: .one(.init( uri: routingDID - )])] + )))] } else { withServices = services } diff --git a/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Invitations.swift b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Invitations.swift index 9c7807f5..60e641aa 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Invitations.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Invitations.swift @@ -120,10 +120,10 @@ public extension DIDCommAgent { let ownDID = try await createNewPeerDID( services: [.init( id: "#didcomm-1", - type: ["DIDCommMessaging"], - serviceEndpoint: [.init( + type: .one("DIDCommMessaging"), + serviceEndpoint: .one(.init( uri: "https://localhost:8080/didcomm" - )] + )) )], updateMediator: false ) diff --git a/EdgeAgentSDK/EdgeAgent/Sources/Protocols/Connection/DownloadDataWithResolver.swift b/EdgeAgentSDK/EdgeAgent/Sources/Protocols/Connection/DownloadDataWithResolver.swift index 9326db29..d476450d 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/Protocols/Connection/DownloadDataWithResolver.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/Protocols/Connection/DownloadDataWithResolver.swift @@ -14,7 +14,7 @@ public struct DownloadDataWithResolver: Downloader { if let did = try? castor.parseDID(str: urlOrDID) { let document = try await castor.resolveDID(did: did) guard - let urlStr = document.services.first?.serviceEndpoint.first?.uri, + let urlStr = document.services.first?.serviceEndpoint.array.first?.uri, let validUrl = URL(string: urlStr) else { throw CommonError.invalidURLError(url: "Could not find any URL on DID") diff --git a/EdgeAgentSDK/Mercury/Sources/DIDCommWrappers/DIDCommDIDResolverWrapper.swift b/EdgeAgentSDK/Mercury/Sources/DIDCommWrappers/DIDCommDIDResolverWrapper.swift index e20b4f07..b835b03c 100644 --- a/EdgeAgentSDK/Mercury/Sources/DIDCommWrappers/DIDCommDIDResolverWrapper.swift +++ b/EdgeAgentSDK/Mercury/Sources/DIDCommWrappers/DIDCommDIDResolverWrapper.swift @@ -67,10 +67,10 @@ extension DIDCore.DIDDocument { } let services = from.services.flatMap { service in - service.serviceEndpoint.map { + service.serviceEndpoint.array.map { return Service( - id: service.id, - type: service.type.first ?? "", + id: service.id ?? "", + type: service.type.array.first ?? "", serviceEndpoint: AnyCodable( dictionaryLiteral: ("uri", $0.uri), diff --git a/EdgeAgentSDK/Mercury/Sources/MecuryImpl+SendMessage.swift b/EdgeAgentSDK/Mercury/Sources/MecuryImpl+SendMessage.swift index 7cbfa86d..f73b1af7 100644 --- a/EdgeAgentSDK/Mercury/Sources/MecuryImpl+SendMessage.swift +++ b/EdgeAgentSDK/Mercury/Sources/MecuryImpl+SendMessage.swift @@ -113,9 +113,10 @@ extension MercuryImpl { private func getDIDCommURL(document: DIDDocument) -> URL? { document.services - .first { $0.type.contains("DIDCommMessaging") } + .first { $0.type.array.contains("DIDCommMessaging") } .map { $0.serviceEndpoint + .array .map { URL(string: $0.uri) } .compactMap { $0 } }?.first @@ -123,9 +124,10 @@ extension MercuryImpl { private func getDIDCommDID(document: DIDDocument) -> DID? { document.services - .first { $0.type.contains("DIDCommMessaging") } + .first { $0.type.array.contains("DIDCommMessaging") } .map { $0.serviceEndpoint + .array .map { try? castor.parseDID(str: $0.uri) } .compactMap { $0 } }?.first diff --git a/EdgeAgentSDK/Pollux/Tests/AnoncredsTests.swift b/EdgeAgentSDK/Pollux/Tests/AnoncredsTests.swift index a398a4b8..ae3d57b1 100644 --- a/EdgeAgentSDK/Pollux/Tests/AnoncredsTests.swift +++ b/EdgeAgentSDK/Pollux/Tests/AnoncredsTests.swift @@ -59,41 +59,41 @@ final class AnoncredsTests: XCTestCase { } -// func testProvingCredential() async throws { -// let offer = try issuer.createOffer() -// let linkSecretValue = try linkSecret.getValue() -// -// let credDef = issuer.credDef -// let defDownloader = MockDownloader(returnData: try credDef.getJson().data(using: .utf8)!) -// let schemaDownloader = MockDownloader(returnData: issuer.getSchemaJson().data(using: .utf8)!) -// let prover = MockProver(linkSecret: linkSecret, credDef: credDef) -// let request = try prover.createRequest(offer: offer) -// let credentialMetadata = try StorableCredentialRequestMetadata( -// metadataJson: request.1.getJson().tryData(using: .utf8), -// storingId: "1" -// ) -// try await pluto.storeCredential(credential: credentialMetadata).first().await() -// let issuedMessage = try issuer.issueCredential(offer: offer, request: request.0) -// let credential = try await PolluxImpl(castor: castor, pluto: pluto).parseCredential( -// issuedCredential: issuedMessage, -// options: [ -// .linkSecret(id: "test", secret: linkSecretValue), -// .credentialDefinitionDownloader(downloader: defDownloader), -// .schemaDownloader(downloader: schemaDownloader) -// ] -// ) -// XCTAssertTrue(credential.isProofable) -// -// let presentationRequest = try issuer.createPresentationRequest() -// -// let presentation = try await credential.proof!.presentation( -// request: presentationRequest.message, -// options: [ -// .linkSecret(id: "", secret: issuer.linkSecret.getValue()), -// ] -// ) -// -// let value = try issuer.verifyPresentation(presentation: presentation, request: presentationRequest.requestStr) -// XCTAssertTrue(value) -// } + func testProvingCredential() async throws { + let offer = try issuer.createOffer() + let linkSecretValue = try linkSecret.getValue() + + let credDef = issuer.credDef + let defDownloader = MockDownloader(returnData: try credDef.getJson().data(using: .utf8)!) + let schemaDownloader = MockDownloader(returnData: issuer.getSchemaJson().data(using: .utf8)!) + let prover = MockProver(linkSecret: linkSecret, credDef: credDef) + let request = try prover.createRequest(offer: offer) + let credentialMetadata = try StorableCredentialRequestMetadata( + metadataJson: request.1.getJson().tryData(using: .utf8), + storingId: "1" + ) + try await pluto.storeCredential(credential: credentialMetadata).first().await() + let issuedMessage = try issuer.issueCredential(offer: offer, request: request.0) + let credential = try await PolluxImpl(castor: castor, pluto: pluto).parseCredential( + issuedCredential: issuedMessage, + options: [ + .linkSecret(id: "test", secret: linkSecretValue), + .credentialDefinitionDownloader(downloader: defDownloader), + .schemaDownloader(downloader: schemaDownloader) + ] + ) + XCTAssertTrue(credential.isProofable) + + let presentationRequest = try issuer.createPresentationRequest() + let presentation = try await credential.proof!.presentation( + type: "", + requestPayload: presentationRequest.request, + options: [ + .linkSecret(id: "", secret: issuer.linkSecret.getValue()), + ] + ) + + let value = try issuer.verifyPresentation(presentation: presentation, request: presentationRequest.requestStr) + XCTAssertTrue(value) + } } diff --git a/EdgeAgentSDK/Pollux/Tests/Mocks/MockIssuer.swift b/EdgeAgentSDK/Pollux/Tests/Mocks/MockIssuer.swift index 86c83a93..bb2dc6eb 100644 --- a/EdgeAgentSDK/Pollux/Tests/Mocks/MockIssuer.swift +++ b/EdgeAgentSDK/Pollux/Tests/Mocks/MockIssuer.swift @@ -114,19 +114,11 @@ struct MockIssuer { ) } - func createPresentationRequest() throws -> (message: Message, requestStr: String) { + func createPresentationRequest() throws -> (request: Data, requestStr: String) { let presentation = """ {"nonce":"1103253414365527824079144","name":"proof_req_1","version":"0.1","requested_attributes":{"sex":{"name":"sex", "restrictions":{"attr::sex::value":"M","cred_def_id":"mock:uri3"}}},"requested_predicates":{"age":{"name":"age", "p_type":">=", "p_value":18}}} """ - return (Message( - piuri: "", - body: Data(), - attachments: [ - .init( - data: AttachmentBase64(base64: try presentation.tryData(using: .utf8).base64EncodedString()) - ) - ] - ), presentation) + return (try presentation.tryData(using: .utf8), presentation) } func getSchemaJson() -> String {