diff --git a/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connectionless/ConnectionlessExchangeTest.kt b/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connectionless/ConnectionlessExchangeTest.kt new file mode 100644 index 0000000..f533bde --- /dev/null +++ b/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connectionless/ConnectionlessExchangeTest.kt @@ -0,0 +1,162 @@ +package org.hyperledger.ariesframework.connectionless + +import androidx.test.filters.LargeTest +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.test.runTest +import org.hyperledger.ariesframework.TestHelper +import org.hyperledger.ariesframework.agent.Agent +import org.hyperledger.ariesframework.agent.SubjectOutboundTransport +import org.hyperledger.ariesframework.connection.models.ConnectionState +import org.hyperledger.ariesframework.credentials.models.AutoAcceptCredential +import org.hyperledger.ariesframework.credentials.models.CreateOfferOptions +import org.hyperledger.ariesframework.credentials.models.CredentialPreview +import org.hyperledger.ariesframework.credentials.models.CredentialState +import org.hyperledger.ariesframework.oob.models.CreateOutOfBandInvitationConfig +import org.hyperledger.ariesframework.proofs.ProofService +import org.hyperledger.ariesframework.proofs.models.AttributeFilter +import org.hyperledger.ariesframework.proofs.models.AutoAcceptProof +import org.hyperledger.ariesframework.proofs.models.PredicateType +import org.hyperledger.ariesframework.proofs.models.ProofAttributeInfo +import org.hyperledger.ariesframework.proofs.models.ProofPredicateInfo +import org.hyperledger.ariesframework.proofs.models.ProofRequest +import org.hyperledger.ariesframework.proofs.models.ProofState +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test +import kotlin.time.Duration.Companion.seconds + +class ConnectionlessExchangeTest { + lateinit var issuerAgent: Agent + lateinit var holderAgent: Agent + lateinit var verifierAgent: Agent + + lateinit var credDefId: String + + val credentialPreview = CredentialPreview.fromDictionary(mapOf("name" to "John", "age" to "99")) + val context = InstrumentationRegistry.getInstrumentation().targetContext + + @Before + fun setUp() = runTest(timeout = 30.seconds) { + val issuerConfig = TestHelper.getBaseConfig("issuer", true) + issuerConfig.autoAcceptCredential = AutoAcceptCredential.Always + issuerAgent = Agent(context, issuerConfig) + + val holderConfig = TestHelper.getBaseConfig("holder", true) + holderConfig.autoAcceptCredential = AutoAcceptCredential.Always + holderConfig.autoAcceptProof = AutoAcceptProof.Always + holderAgent = Agent(context, holderConfig) + + val verifierConfig = TestHelper.getBaseConfig("verifier", true) + verifierConfig.autoAcceptProof = AutoAcceptProof.Always + verifierAgent = Agent(context, verifierConfig) + + issuerAgent.initialize() + holderAgent.initialize() + verifierAgent.initialize() + + credDefId = TestHelper.prepareForIssuance(issuerAgent, listOf("name", "age")) + } + + @After + fun tearDown() = runTest { + issuerAgent.reset() + holderAgent.reset() + verifierAgent.reset() + } + + @Test @LargeTest + fun testConnectionlessExchange() = runTest { + issuerAgent.setOutboundTransport(SubjectOutboundTransport(holderAgent)) + holderAgent.setOutboundTransport(SubjectOutboundTransport(issuerAgent)) + + val offerOptions = CreateOfferOptions( + connection = null, + credentialDefinitionId = credDefId, + attributes = credentialPreview.attributes, + comment = "credential-offer for test", + ) + val (message, record) = issuerAgent.credentialService.createOffer(offerOptions) + validateState(issuerAgent, record.threadId, CredentialState.OfferSent) + + val oobConfig = CreateOutOfBandInvitationConfig( + label = "issuer-to-holder-invitation", + alias = "issuer-to-holder-invitation", + handshake = false, + messages = listOf(message), + multiUseInvitation = false, + autoAcceptConnection = true, + ) + val oobInvitation = issuerAgent.oob.createInvitation(oobConfig) + + val (oob, connection) = holderAgent.oob.receiveInvitation(oobInvitation.outOfBandInvitation) + assertNotNull(connection) + assertEquals(connection?.state, ConnectionState.Complete) + assertNotNull(oob) + + validateState(holderAgent, record.threadId, CredentialState.Done) + validateState(issuerAgent, record.threadId, CredentialState.Done) + + // credential exchange done. + + holderAgent.setOutboundTransport(SubjectOutboundTransport(verifierAgent)) + verifierAgent.setOutboundTransport(SubjectOutboundTransport(holderAgent)) + + val proofRequest = getProofRequest() + val (proofRequestMessage, proofExchangeRecord) = verifierAgent.proofService.createRequest( + proofRequest, + ) + validateState(verifierAgent, proofExchangeRecord.threadId, ProofState.RequestSent) + + val oobConfigForProofExchange = CreateOutOfBandInvitationConfig( + label = "verifier-to-holder-invitation", + alias = "verifier-to-holder-invitation", + handshake = false, + messages = listOf(proofRequestMessage), + multiUseInvitation = false, + autoAcceptConnection = true, + ) + val oobInvitationForProofExchange = + verifierAgent.oob.createInvitation(oobConfigForProofExchange) + + val (oobForProofExchange, connectionForProofExchange) = holderAgent.oob.receiveInvitation( + oobInvitationForProofExchange.outOfBandInvitation, + ) + assertNotNull(connectionForProofExchange) + assertEquals(connectionForProofExchange?.state, ConnectionState.Complete) + assertNotNull(oobForProofExchange) + + validateState(holderAgent, proofExchangeRecord.threadId, ProofState.Done) + validateState(verifierAgent, proofExchangeRecord.threadId, ProofState.Done) + } + + private suspend fun validateState(agent: Agent, threadId: String, state: CredentialState) { + val record = agent.credentialExchangeRepository.getByThreadAndConnectionId(threadId, null) + assertEquals(record.state, state) + } + + private suspend fun validateState(agent: Agent, threadId: String, state: ProofState) { + val record = agent.proofRepository.getByThreadAndConnectionId(threadId, null) + assertEquals(record.state, state) + } + + private suspend fun getProofRequest(): ProofRequest { + val attributes = mapOf( + "name" to ProofAttributeInfo( + name = "name", + restrictions = listOf(AttributeFilter(credentialDefinitionId = credDefId)), + ), + ) + val predicates = mapOf( + "age" to ProofPredicateInfo( + name = "age", + predicateType = PredicateType.GreaterThanOrEqualTo, + predicateValue = 50, + restrictions = listOf(AttributeFilter(credentialDefinitionId = credDefId)), + ), + ) + val nonce = ProofService.generateProofRequestNonce() + return ProofRequest(nonce = nonce, requestedAttributes = attributes, requestedPredicates = predicates) + } +} diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/AgentConfig.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/AgentConfig.kt index fc3069f..f3fde19 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/AgentConfig.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/AgentConfig.kt @@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable import org.hyperledger.ariesframework.credentials.models.AutoAcceptCredential import org.hyperledger.ariesframework.oob.models.HandshakeProtocol import org.hyperledger.ariesframework.proofs.models.AutoAcceptProof +import org.hyperledger.ariesframework.routing.Routing @Serializable enum class MediatorPickupStrategy { @@ -63,5 +64,5 @@ data class AgentConfig( var preferredHandshakeProtocol: HandshakeProtocol = HandshakeProtocol.Connections, ) { val endpoints: List - get() = agentEndpoints ?: listOf("didcomm:transport/queue") + get() = agentEndpoints ?: listOf(Routing.DID_COMM_TRANSPORT_QUEUE) } diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/MessageReceiver.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/MessageReceiver.kt index 2716f6e..189a188 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/MessageReceiver.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/MessageReceiver.kt @@ -3,7 +3,10 @@ package org.hyperledger.ariesframework.agent import org.hyperledger.ariesframework.DecryptedMessageContext import org.hyperledger.ariesframework.EncryptedMessage import org.hyperledger.ariesframework.InboundMessageContext +import org.hyperledger.ariesframework.connection.models.didauth.DidCommService +import org.hyperledger.ariesframework.connection.models.didauth.DidDoc import org.hyperledger.ariesframework.connection.repository.ConnectionRecord +import org.hyperledger.ariesframework.routing.Routing import org.slf4j.LoggerFactory class MessageReceiver(val agent: Agent) { @@ -12,8 +15,8 @@ class MessageReceiver(val agent: Agent) { suspend fun receiveMessage(encryptedMessage: EncryptedMessage) { try { val decryptedMessage = agent.wallet.unpack(encryptedMessage) - val connection = findConnectionByMessageKeys(decryptedMessage) val message = MessageSerializer.decodeFromString(decryptedMessage.plaintextMessage) + val connection = findConnection(decryptedMessage, message) val messageContext = InboundMessageContext( message, decryptedMessage.plaintextMessage, @@ -43,6 +46,44 @@ class MessageReceiver(val agent: Agent) { } } + private suspend fun findConnection(decryptedMessage: DecryptedMessageContext, message: AgentMessage): ConnectionRecord? { + var connection = findConnectionByMessageKeys(decryptedMessage) + if (connection == null) { + connection = findConnectionByMessageThreadId(message) + if (connection != null) { + updateConnectionTheirDidDoc(connection, decryptedMessage.senderKey) + } + } + return connection + } + + private suspend fun findConnectionByMessageThreadId(message: AgentMessage): ConnectionRecord? { + val pthId = message.thread?.parentThreadId ?: "" + val oobRecord = agent.outOfBandService.findByInvitationId(pthId) + val invitationKey = oobRecord?.outOfBandInvitation?.invitationKey() ?: "" + return agent.connectionService.findByInvitationKey(invitationKey) + } + + private suspend fun updateConnectionTheirDidDoc(connection: ConnectionRecord, senderKey: String?) { + if (senderKey == null) { + return + } + val service = DidCommService( + id = "${connection.id}#1", + serviceEndpoint = Routing.DID_COMM_TRANSPORT_QUEUE, + recipientKeys = listOf(senderKey), + ) + + val theirDidDoc = DidDoc( + id = senderKey, + publicKey = emptyList(), + service = listOf(service), + authentication = emptyList(), + ) + connection.theirDidDoc = theirDidDoc + agent.connectionRepository.update(connection) + } + private suspend fun findConnectionByMessageKeys(decryptedMessage: DecryptedMessageContext): ConnectionRecord? { return agent.connectionService.findByKeys( decryptedMessage.senderKey ?: "", diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/ConnectionCommand.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/ConnectionCommand.kt index 9d7059c..3c2e59b 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/ConnectionCommand.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/ConnectionCommand.kt @@ -17,6 +17,8 @@ import org.hyperledger.ariesframework.connection.messages.DidExchangeCompleteMes import org.hyperledger.ariesframework.connection.messages.DidExchangeRequestMessage import org.hyperledger.ariesframework.connection.messages.DidExchangeResponseMessage import org.hyperledger.ariesframework.connection.messages.TrustPingMessage +import org.hyperledger.ariesframework.connection.models.ConnectionState +import org.hyperledger.ariesframework.connection.models.didauth.DidDoc import org.hyperledger.ariesframework.connection.repository.ConnectionRecord import org.hyperledger.ariesframework.oob.messages.OutOfBandInvitation import org.hyperledger.ariesframework.oob.models.HandshakeProtocol @@ -150,7 +152,7 @@ class ConnectionCommand(val agent: Agent, private val dispatcher: Dispatcher) { */ suspend fun acceptOutOfBandInvitation( outOfBandRecord: OutOfBandRecord, - handshakeProtocol: HandshakeProtocol, + handshakeProtocol: HandshakeProtocol? = null, config: ReceiveOutOfBandInvitationConfig? = null, ): ConnectionRecord { val connection = receiveInvitation( @@ -159,6 +161,14 @@ class ConnectionCommand(val agent: Agent, private val dispatcher: Dispatcher) { false, config?.alias, ) + + if (handshakeProtocol == null) { + val didDocServices = outOfBandRecord.outOfBandInvitation.services.mapNotNull { it.asDidCommService() } + connection.theirDidDoc = connection.theirDidDoc ?: DidDoc(didDocServices) + agent.connectionService.updateState(connection, ConnectionState.Complete) + return connection + } + val message = if (handshakeProtocol == HandshakeProtocol.Connections) { agent.connectionService.createRequest( connection.id, diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDoc.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDoc.kt index 5fcb3fc..1dd24e6 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDoc.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDoc.kt @@ -73,4 +73,13 @@ class DidDoc( } } } + + constructor(services: List) : this(id = "") { + val service = services.firstOrNull() + ?: throw Exception("Creating a DidDoc from DidCommServices failed. services is empty.") + val key = service.recipientKeys.firstOrNull() + ?: throw Exception("Creating a DidDoc from DidCommServices failed. recipientKeys is empty.") + this.id = key + this.service = services + } } diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/credentials/CredentialService.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/credentials/CredentialService.kt index 151e365..366dfda 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/credentials/CredentialService.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/credentials/CredentialService.kt @@ -238,7 +238,7 @@ class CredentialService(val agent: Agent) { var credentialRecord = credentialExchangeRepository.getByThreadAndConnectionId( requestMessage.threadId, - messageContext.connection?.id, + null, ) // The credential offer may have been a connectionless-offer. diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/oob/OutOfBandCommand.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/oob/OutOfBandCommand.kt index 1184508..aad1459 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/oob/OutOfBandCommand.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/oob/OutOfBandCommand.kt @@ -6,6 +6,7 @@ import org.hyperledger.ariesframework.agent.AgentEvents import org.hyperledger.ariesframework.agent.Dispatcher import org.hyperledger.ariesframework.agent.MessageSerializer import org.hyperledger.ariesframework.connection.messages.ConnectionInvitationMessage +import org.hyperledger.ariesframework.connection.models.ConnectionRole import org.hyperledger.ariesframework.connection.models.ConnectionState import org.hyperledger.ariesframework.connection.repository.ConnectionRecord import org.hyperledger.ariesframework.oob.handlers.HandshakeReuseAcceptedHandler @@ -94,8 +95,26 @@ class OutOfBandCommand(val agent: Agent, private val dispatcher: Dispatcher) { imageUrl = imageUrl, ) - messages.forEach { message -> - outOfBandInvitation.addRequest(message) + if (messages.isNotEmpty()) { + messages.forEach { message -> + outOfBandInvitation.addRequest(message) + } + if (!handshake) { + val connectionRecord = agent.connectionService.createConnection( + role = ConnectionRole.Inviter, + state = ConnectionState.Complete, + outOfBandInvitation = outOfBandInvitation, + alias = null, + routing = routing, + theirLabel = null, + autoAcceptConnection = true, + multiUseInvitation = false, + tags = null, + imageUrl = null, + threadId = null, + ) + agent.connectionRepository.save(connectionRecord) + } } val outOfBandRecord = OutOfBandRecord( @@ -178,7 +197,7 @@ class OutOfBandCommand(val agent: Agent, private val dispatcher: Dispatcher) { val imageUrl = config?.imageUrl ?: agent.agentConfig.connectionImageUrl val messages = invitation.getRequestsJson() - require(invitation.handshakeProtocols?.size ?: 0 > 0 || messages.size > 0) { + require((invitation.handshakeProtocols?.size ?: 0) > 0 || messages.isNotEmpty()) { "One of handshake_protocols and requests~attach MUST be included in the message." } require(invitation.fingerprints().isNotEmpty()) { @@ -240,60 +259,44 @@ class OutOfBandCommand(val agent: Agent, private val dispatcher: Dispatcher) { val messages = outOfBandRecord.outOfBandInvitation.getRequestsJson() val handshakeProtocols = outOfBandRecord.outOfBandInvitation.handshakeProtocols ?: emptyList() - if (handshakeProtocols.isNotEmpty()) { - var connectionRecord: ConnectionRecord? = null - if (existingConnection != null && config?.reuseConnection == true) { - if (messages.isNotEmpty()) { - logger.debug("Skip handshake and reuse existing connection ${existingConnection.id}") + var connectionRecord: ConnectionRecord? = null + if (existingConnection != null && config?.reuseConnection == true) { + if (messages.isNotEmpty()) { + logger.debug("Skip handshake and reuse existing connection ${existingConnection.id}") + connectionRecord = existingConnection + } else { + logger.debug("Start handshake to reuse connection.") + val isHandshakeReuseSuccessful = + handleHandshakeReuse(outOfBandRecord = outOfBandRecord, connectionRecord = existingConnection) + if (isHandshakeReuseSuccessful) { connectionRecord = existingConnection } else { - logger.debug("Start handshake to reuse connection.") - val isHandshakeReuseSuccessful = - handleHandshakeReuse(outOfBandRecord = outOfBandRecord, connectionRecord = existingConnection) - if (isHandshakeReuseSuccessful) { - connectionRecord = existingConnection - } else { - logger.warn("Handshake reuse failed. Not using existing connection ${existingConnection.id}") - } + logger.warn("Handshake reuse failed. Not using existing connection ${existingConnection.id}") } } + } - val handshakeProtocol = selectHandshakeProtocol(handshakeProtocols) - if (connectionRecord == null) { - logger.debug("Creating new connection.") - if (!handshakeProtocols.contains(HandshakeProtocol.Connections)) { - throw Exception("Unsupported handshake protocol. Supported protocols: $handshakeProtocols") - } - - connectionRecord = agent.connections.acceptOutOfBandInvitation(outOfBandRecord, handshakeProtocol, config) - } - - if (agent.connectionService.fetchState(connectionRecord) != ConnectionState.Complete) { - val result = agent.eventBus.waitFor { it.record.state == ConnectionState.Complete } - if (!result) { - throw Exception("Connection timed out.") - } - } - connectionRecord = agent.connectionRepository.getById(connectionRecord.id) - if (!outOfBandRecord.reusable) { - agent.outOfBandService.updateState(outOfBandRecord, OutOfBandState.Done) - } + val handshakeProtocol = selectHandshakeProtocol(handshakeProtocols) + if (connectionRecord == null) { + logger.debug("Creating new connection.") + connectionRecord = agent.connections.acceptOutOfBandInvitation(outOfBandRecord, handshakeProtocol, config) + } - if (messages.isNotEmpty()) { - processMessages(messages, connectionRecord) - } - return Pair(outOfBandRecord, connectionRecord) - } else if (messages.isNotEmpty()) { - logger.debug("Out of band message contains only request messages.") - if (existingConnection != null) { - processMessages(messages, existingConnection) - } else { - // TODO: send message to the service endpoint - throw Exception("Cannot process request messages. No connection found.") + if (handshakeProtocol != null && agent.connectionService.fetchState(connectionRecord) != ConnectionState.Complete) { + val result = agent.eventBus.waitFor { it.record.state == ConnectionState.Complete } + if (!result) { + throw Exception("Connection timed out.") } } + connectionRecord = agent.connectionRepository.getById(connectionRecord.id) + if (!outOfBandRecord.reusable) { + agent.outOfBandService.updateState(outOfBandRecord, OutOfBandState.Done) + } - return Pair(outOfBandRecord, null) + if (messages.isNotEmpty()) { + processMessages(messages, connectionRecord) + } + return Pair(outOfBandRecord, connectionRecord) } private suspend fun processMessages(messages: List, connectionRecord: ConnectionRecord) { @@ -335,7 +338,11 @@ class OutOfBandCommand(val agent: Agent, private val dispatcher: Dispatcher) { return connections.firstOrNull { it.isReady() } } - private suspend fun selectHandshakeProtocol(handshakeProtocols: List): HandshakeProtocol { + private suspend fun selectHandshakeProtocol(handshakeProtocols: List): HandshakeProtocol? { + if (handshakeProtocols.isEmpty()) { + return null + } + val supportedProtocols = getSupportedHandshakeProtocols() if (handshakeProtocols.contains(agent.agentConfig.preferredHandshakeProtocol) && supportedProtocols.contains(agent.agentConfig.preferredHandshakeProtocol) diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/proofs/ProofService.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/proofs/ProofService.kt index 59b20b1..b9e004f 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/proofs/ProofService.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/proofs/ProofService.kt @@ -69,18 +69,18 @@ class ProofService(val agent: Agent) { */ suspend fun createRequest( proofRequest: ProofRequest, - connectionRecord: ConnectionRecord, + connectionRecord: ConnectionRecord? = null, comment: String? = null, autoAcceptProof: AutoAcceptProof? = null, ): Pair { - connectionRecord.assertReady() + connectionRecord?.assertReady() val proofRequestJson = Json.encodeToString(proofRequest) val attachment = Attachment.fromData(proofRequestJson.toByteArray(), RequestPresentationMessage.INDY_PROOF_REQUEST_ATTACHMENT_ID) val message = RequestPresentationMessage(comment, listOf(attachment)) val proofRecord = ProofExchangeRecord( - connectionId = connectionRecord.id, + connectionId = connectionRecord?.id ?: "connectionless-proof-request", threadId = message.threadId, state = ProofState.RequestSent, autoAcceptProof = autoAcceptProof, @@ -162,9 +162,8 @@ class ProofService(val agent: Agent) { */ suspend fun processPresentation(messageContext: InboundMessageContext): ProofExchangeRecord { val presentationMessage = MessageSerializer.decodeFromString(messageContext.plaintextMessage) as PresentationMessage - val connection = messageContext.assertReadyConnection() - val proofRecord = agent.proofRepository.getByThreadAndConnectionId(presentationMessage.threadId, connection.id) + val proofRecord = agent.proofRepository.getByThreadAndConnectionId(presentationMessage.threadId, null) proofRecord.assertState(ProofState.RequestSent) val indyProofJson = presentationMessage.indyProof() diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/routing/Routing.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/routing/Routing.kt index 896fe49..6650462 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/routing/Routing.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/routing/Routing.kt @@ -6,4 +6,8 @@ data class Routing( val did: String, val routingKeys: List, val mediatorId: String?, -) +) { + companion object { + val DID_COMM_TRANSPORT_QUEUE = "didcomm:transport/queue" + } +}