Ejemplo n.º 1
0
    async def test_service_start_introduction_target_conn_rec_not_active(self):
        service = await demo_service.DemoIntroductionService.service_handler()(
            self.context)

        conn_rec_init = ConnectionRecord(
            connection_id=None,
            state=ConnectionRecord.STATE_ACTIVE,
        )
        await conn_rec_init.save(self.context)
        assert conn_rec_init._id

        conn_rec_target = ConnectionRecord(
            connection_id=None,
            state=ConnectionRecord.STATE_INACTIVE,
        )
        await conn_rec_target.save(self.context)
        assert conn_rec_target._id

        with self.assertRaises(base_service.IntroductionError):
            await service.start_introduction(
                init_connection_id=conn_rec_init._id,
                target_connection_id=conn_rec_target._id,
                message="Hello",
                outbound_handler=None,
            )
Ejemplo n.º 2
0
    async def test_service_start_and_return_introduction(self):
        service = await demo_service.DemoIntroductionService.service_handler()(
            self.context
        )
        start_responder = MockResponder()

        conn_rec_init = ConnectionRecord(
            connection_id=None,
            state=ConnectionRecord.STATE_ACTIVE,
        )
        await conn_rec_init.save(self.context)
        assert conn_rec_init._id

        conn_rec_target = ConnectionRecord(
            connection_id=None,
            state=ConnectionRecord.STATE_ACTIVE,
        )
        await conn_rec_target.save(self.context)
        assert conn_rec_target._id

        await service.start_introduction(
            init_connection_id=conn_rec_init._id,
            target_connection_id=conn_rec_target._id,
            message="Hello Start",
            outbound_handler=start_responder.send,
        )
        messages = start_responder.messages
        assert len(messages) == 1
        (result, target) = messages[0]
        assert isinstance(result, demo_service.InvitationRequest)
        assert result.message == "Hello Start"
        assert target["connection_id"] == conn_rec_target._id

        invite = demo_service.Invitation(
            invitation=ConnectionInvitation(
                label=TEST_LABEL,
                did=TEST_DID,
                recipient_keys=[TEST_VERKEY],
                endpoint=TEST_ENDPOINT,
                routing_keys=[TEST_ROUTE_VERKEY],
                image_url=TEST_IMAGE_URL,
            ),
            message="Hello Invite",
            _id=result._id,
        )
        return_responder = MockResponder()

        await service.return_invitation(
            target_connection_id=conn_rec_target._id,
            invitation=invite,
            outbound_handler=return_responder.send,
        )
        messages = return_responder.messages
        assert len(messages) == 1
        (result, target) = messages[0]
        assert isinstance(result, demo_service.ForwardInvitation)
        assert result.message == "Hello Invite"
        assert target["connection_id"] == conn_rec_init._id
Ejemplo n.º 3
0
    async def create_request(
        self,
        connection: ConnectionRecord,
        my_label: str = None,
        my_endpoint: str = None,
    ) -> ConnectionRequest:
        """
        Create a new connection request for a previously-received invitation.

        Args:
            connection: The `ConnectionRecord` representing the invitation to accept
            my_label: My label
            my_endpoint: My endpoint

        Returns:
            A new `ConnectionRequest` message to send to the other agent

        """
        wallet: BaseWallet = await self.context.inject(BaseWallet)
        if connection.my_did:
            my_info = await wallet.get_local_did(connection.my_did)
        else:
            # Create new DID for connection
            my_info = await wallet.create_local_did()
            connection.my_did = my_info.did

        # Create connection request message
        if not my_endpoint:
            my_endpoints = []
            default_endpoint = self.context.settings.get("default_endpoint")
            if default_endpoint:
                my_endpoints.append(default_endpoint)
            my_endpoints.extend(
                self.context.settings.get("additional_endpoints", []))
        else:
            my_endpoints = [my_endpoint]
        did_doc = await self.create_did_document(
            my_info, connection.inbound_connection_id, my_endpoints)
        if not my_label:
            my_label = self.context.settings.get("default_label")
        request = ConnectionRequest(
            label=my_label,
            connection=ConnectionDetail(did=connection.my_did,
                                        did_doc=did_doc),
        )

        # Update connection state
        connection.request_id = request._id
        connection.state = ConnectionRecord.STATE_REQUEST

        await connection.save(self.context,
                              reason="Created connection request")

        return request
Ejemplo n.º 4
0
    async def test_service_return_invitation_not_found(self):
        invite = demo_service.Invitation(
            invitation=ConnectionInvitation(
                label=TEST_LABEL,
                did=TEST_DID,
                recipient_keys=[TEST_VERKEY],
                endpoint=TEST_ENDPOINT,
                routing_keys=[TEST_ROUTE_VERKEY],
                image_url=TEST_IMAGE_URL,
            ),
            message="Hello World",
        )

        service = await demo_service.DemoIntroductionService.service_handler()(
            self.context)

        conn_rec_target = ConnectionRecord(
            connection_id=None,
            state=ConnectionRecord.STATE_ACTIVE,
        )
        await conn_rec_target.save(self.context)
        assert conn_rec_target._id

        await service.return_invitation(
            target_connection_id=conn_rec_target._id,
            invitation=invite,
            outbound_handler=None,
        )
Ejemplo n.º 5
0
    async def test_request_credential(self):
        mock = async_mock.MagicMock()
        record = ConnectionRecord(state=ConnectionRecord.STATE_ACTIVE)
        context = RequestContext(base_context=InjectionContext(
            enforce_typing=False))
        storage = BasicStorage()
        context.injector.bind_instance(BaseStorage, storage)
        connection_id = await record.save(context)
        mock.json = async_mock.CoroutineMock(
            return_value={
                "connection_id": connection_id,
                "credential_type": "TYPE_EXAMPLE",
                "credential_values": {
                    "test1": "1"
                },
            })

        mock.app = {
            "request_context": context,
            "outbound_message_router": async_mock.CoroutineMock(),
        }

        with async_mock.patch.object(
                test_module, "CredentialExchangeRecord",
                autospec=True) as mock_conn_rec, async_mock.patch.object(
                    test_module.web, "json_response") as mock_response:

            await test_module.request_credential(mock)
            mock_response.assert_called_once()
    def setUp(self):
        self.storage = BasicStorage()
        self.cache = BasicCache()
        self.wallet = BasicWallet()
        self.responder = MockResponder()
        self.responder.send = async_mock.CoroutineMock()

        self.context = InjectionContext(enforce_typing=False)
        self.context.injector.bind_instance(BaseStorage, self.storage)
        self.context.injector.bind_instance(BaseWallet, self.wallet)
        self.context.injector.bind_instance(BaseResponder, self.responder)
        self.context.injector.bind_instance(BaseCache, self.cache)
        self.context.injector.bind_instance(BaseLedger, async_mock.MagicMock())
        self.context.update_settings({
            "default_endpoint":
            "http://aries.ca/endpoint",
            "default_label":
            "This guy",
            "additional_endpoints": ["http://aries.ca/another-endpoint"],
            "debug.auto_accept_invites":
            True,
            "debug.auto_accept_requests":
            True,
        })

        self.manager = OutOfBandManager(self.context)
        self.test_conn_rec = ConnectionRecord(
            my_did=self.test_did,
            their_did=self.test_target_did,
            their_role=None,
            state=ConnectionRecord.STATE_ACTIVE,
        )
def context():
    """Fixture for context used in tests."""
    # pylint: disable=W0621
    context = RequestContext()
    context.message = ProvideData(goal_code="test_goal", data=[TEST_DATA])
    context.connection_record = ConnectionRecord(connection_id=TEST_CONN_ID)
    context.connection_ready = True
    yield context
Ejemplo n.º 8
0
 async def setUp(self):
     self.context = RequestContext(
         base_context=InjectionContext(enforce_typing=False)
     )
     self.context.connection_ready = True
     self.context.connection_record = ConnectionRecord(connection_id="conn-id")
     self.context.message_receipt = MessageReceipt(sender_verkey=TEST_VERKEY)
     self.context.injector.bind_instance(BaseStorage, BasicStorage())
Ejemplo n.º 9
0
    async def handle(self, context: RequestContext, responder: BaseResponder):
        """Handle static connection creation request."""

        connection_mgr = ConnectionManager(context)
        wallet: BaseWallet = await context.inject(BaseWallet)

        # Make our info for the connection
        my_info = await wallet.create_local_did()

        # Create connection record
        connection = ConnectionRecord(
            initiator=ConnectionRecord.INITIATOR_SELF,
            my_did=my_info.did,
            their_did=context.message.static_did,
            their_label=context.message.label,
            their_role=context.message.role if context.message.role else None,
            state=ConnectionRecord.STATE_ACTIVE,
            invitation_mode=ConnectionRecord.INVITATION_MODE_STATIC
        )

        # Construct their did doc from the basic components in message
        diddoc = DIDDoc(context.message.static_did)
        public_key = PublicKey(
            did=context.message.static_did,
            ident="1",
            value=context.message.static_key,
            pk_type=PublicKeyType.ED25519_SIG_2018,
            controller=context.message.static_did
        )
        service = Service(
            did=context.message.static_did,
            ident="indy",
            typ="IndyAgent",
            recip_keys=[public_key],
            routing_keys=[],
            endpoint=context.message.static_endpoint
        )
        diddoc.set(public_key)
        diddoc.set(service)

        # Save
        await connection_mgr.store_did_document(diddoc)
        await connection.save(
            context,
            reason='Created new static connection'
        )

        # Prepare response
        info = StaticConnectionInfo(
            did=my_info.did,
            key=my_info.verkey,
            endpoint=context.settings.get("default_endpoint")
        )
        info.assign_thread_from(context.message)
        await responder.send_reply(info)
        return
Ejemplo n.º 10
0
    async def test_handle(self):
        handler = test_module.ForwardInvitationHandler()

        responder = MockResponder()
        with async_mock.patch.object(test_module,
                                     "ConnectionManager",
                                     autospec=True) as mock_mgr:
            mock_mgr.return_value.receive_invitation = async_mock.CoroutineMock(
                return_value=ConnectionRecord(connection_id="dummy"))

            await handler.handle(self.context, responder)
            assert not (responder.messages)
Ejemplo n.º 11
0
    async def establish_inbound(self, connection: ConnectionRecord,
                                inbound_connection_id: str,
                                outbound_handler) -> str:
        """Assign the inbound routing connection for a connection record.

        Returns: the current routing state (request or done)

        """

        # The connection must have a verkey, but in the case of a received
        # invitation we might not have created one yet
        wallet: BaseWallet = await self.context.inject(BaseWallet)
        if connection.my_did:
            my_info = await wallet.get_local_did(connection.my_did)
        else:
            # Create new DID for connection
            my_info = await wallet.create_local_did()
            connection.my_did = my_info.did

        try:
            router = await ConnectionRecord.retrieve_by_id(
                self.context, inbound_connection_id)
        except StorageNotFoundError:
            raise ConnectionManagerError(
                f"Routing connection not found: {inbound_connection_id}")
        if not router.is_ready:
            raise ConnectionManagerError(
                f"Routing connection is not ready: {inbound_connection_id}")
        connection.inbound_connection_id = inbound_connection_id

        route_mgr = RoutingManager(self.context)

        await route_mgr.send_create_route(inbound_connection_id,
                                          my_info.verkey, outbound_handler)
        connection.routing_state = ConnectionRecord.ROUTING_STATE_REQUEST
        await connection.save(self.context)
        return connection.routing_state
Ejemplo n.º 12
0
 async def test_create_credential_null(self):
     connection = ConnectionRecord(my_did="1234-my", their_did="1234-their")
     with self.assertRaises(IssuerError):
         credential, _ = await self.issuer.create_credential(
             schema={"credential_type": "TestType"},
             credential_values={},
             credential_offer={},
             credential_request={"connection_record": connection},
         )
     with self.assertRaises(IssuerError):
         credential, _ = await self.issuer.create_credential(
             schema={},
             credential_values={},
             credential_offer={},
             credential_request={"connection_record": connection},
         )
def conn_record_to_message_repr(conn: ConnectionRecord) -> Dict[str, Any]:
    """Map ConnectionRecord onto Connection."""
    def _state_map(state: str) -> str:
        if state in ('active', 'response'):
            return 'active'
        if state == 'error':
            return 'error'
        return 'pending'

    return {
        'label': conn.their_label,
        'my_did': conn.my_did,
        'their_did': conn.their_did,
        'state': _state_map(conn.state),
        'role': conn.their_role,
        'connection_id': conn.connection_id,
        'raw_repr': conn.serialize()
    }
Ejemplo n.º 14
0
    async def create_invitation(
        self,
        my_label: str = None,
        my_endpoint: str = None,
        their_role: str = None,
        auto_accept: bool = None,
        public: bool = False,
        multi_use: bool = False,
        alias: str = None,
    ) -> Tuple[ConnectionRecord, ConnectionInvitation]:
        """
        Generate new connection invitation.

        This interaction represents an out-of-band communication channel. In the future
        and in practice, these sort of invitations will be received over any number of
        channels such as SMS, Email, QR Code, NFC, etc.

        Structure of an invite message:

        ::

            {
                "@type":
                    "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation",
                "label": "Alice",
                "did": "did:sov:QmWbsNYhMrjHiqZDTUTEJs"
            }

        Or, in the case of a peer DID:

        ::

            {
                "@type":
                    "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation",
                "label": "Alice",
                "did": "did:peer:oiSqsNYhMrjHiqZDTUthsw",
                "recipientKeys": ["8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K"],
                "serviceEndpoint": "https://example.com/endpoint"
            }

        Currently, only peer DID is supported.

        Args:
            my_label: label for this connection
            my_endpoint: endpoint where other party can reach me
            their_role: a role to assign the connection
            auto_accept: auto-accept a corresponding connection request
                (None to use config)
            public: set to create an invitation from the public DID
            multi_use: set to True to create an invitation for multiple use
            alias: optional alias to apply to connection for later use

        Returns:
            A tuple of the new `ConnectionRecord` and `ConnectionInvitation` instances

        """
        if not my_label:
            my_label = self.context.settings.get("default_label")
        wallet: BaseWallet = await self.context.inject(BaseWallet)

        if public:
            if not self.context.settings.get("public_invites"):
                raise ConnectionManagerError(
                    "Public invitations are not enabled")

            public_did = await wallet.get_public_did()
            if not public_did:
                raise ConnectionManagerError(
                    "Cannot create public invitation with no public DID")

            if multi_use:
                raise ConnectionManagerError(
                    "Cannot use public and multi_use at the same time")

            # FIXME - allow ledger instance to format public DID with prefix?
            invitation = ConnectionInvitation(label=my_label,
                                              did=f"did:sov:{public_did.did}")
            return None, invitation

        invitation_mode = ConnectionRecord.INVITATION_MODE_ONCE
        if multi_use:
            invitation_mode = ConnectionRecord.INVITATION_MODE_MULTI

        if not my_endpoint:
            my_endpoint = self.context.settings.get("default_endpoint")
        accept = (ConnectionRecord.ACCEPT_AUTO if
                  (auto_accept or
                   (auto_accept is None and
                    self.context.settings.get("debug.auto_accept_requests")))
                  else ConnectionRecord.ACCEPT_MANUAL)

        # Create and store new invitation key
        connection_key = await wallet.create_signing_key()

        # Create connection record
        connection = ConnectionRecord(
            initiator=ConnectionRecord.INITIATOR_SELF,
            invitation_key=connection_key.verkey,
            their_role=their_role,
            state=ConnectionRecord.STATE_INVITATION,
            accept=accept,
            invitation_mode=invitation_mode,
            alias=alias,
        )

        await connection.save(self.context, reason="Created new invitation")

        # Create connection invitation message
        # Note: Need to split this into two stages to support inbound routing of invites
        # Would want to reuse create_did_document and convert the result
        invitation = ConnectionInvitation(
            label=my_label,
            recipient_keys=[connection_key.verkey],
            endpoint=my_endpoint)
        await connection.attach_invitation(self.context, invitation)

        return connection, invitation
Ejemplo n.º 15
0
    async def create_static_connection(
        self,
        my_did: str = None,
        my_seed: str = None,
        their_did: str = None,
        their_seed: str = None,
        their_verkey: str = None,
        their_endpoint: str = None,
        their_role: str = None,
        their_label: str = None,
        alias: str = None,
    ) -> (DIDInfo, DIDInfo, ConnectionRecord):
        """
        Register a new static connection (for use by the test suite).

        Args:
            my_did: override the DID used in the connection
            my_seed: provide a seed used to generate our DID and keys
            their_did: provide the DID used by the other party
            their_seed: provide a seed used to generate their DID and keys
            their_verkey: provide the verkey used by the other party
            their_endpoint: their URL endpoint for routing messages
            their_role: their role in this connection
            alias: an alias for this connection record

        Returns:
            The new `ConnectionRecord` instance

        """
        wallet: BaseWallet = await self.context.inject(BaseWallet)

        # seed and DID optional
        my_info = await wallet.create_local_did(my_seed, my_did)

        # must provide their DID and verkey if the seed is not known
        if (not their_did or not their_verkey) and not their_seed:
            raise ConnectionManagerError(
                "Either a verkey or seed must be provided for the other party")
        if not their_did:
            their_did = seed_to_did(their_seed)
        if not their_verkey:
            their_verkey_bin, _ = create_keypair(their_seed.encode())
            their_verkey = bytes_to_b58(their_verkey_bin)
        their_info = DIDInfo(their_did, their_verkey, {})

        # Create connection record
        connection = ConnectionRecord(
            initiator=ConnectionRecord.INITIATOR_SELF,
            invitation_mode=ConnectionRecord.INVITATION_MODE_STATIC,
            my_did=my_info.did,
            their_did=their_info.did,
            their_role=their_role,
            their_label=their_label,
            state=ConnectionRecord.STATE_ACTIVE,
            alias=alias,
        )
        await connection.save(self.context,
                              reason="Created new static connection")

        # Synthesize their DID doc
        did_doc = await self.create_did_document(their_info, None,
                                                 [their_endpoint])
        await self.store_did_document(did_doc)

        return my_info, their_info, connection
Ejemplo n.º 16
0
    async def create_response(self,
                              connection: ConnectionRecord,
                              my_endpoint: str = None) -> ConnectionResponse:
        """
        Create a connection response for a received connection request.

        Args:
            connection: The `ConnectionRecord` with a pending connection request
            my_endpoint: The endpoint I can be reached at

        Returns:
            A tuple of the updated `ConnectionRecord` new `ConnectionResponse` message

        """
        ConnectionRecord.log_state(
            self.context,
            "Creating connection response",
            {"connection_id": connection.connection_id},
        )

        if connection.state not in (
                ConnectionRecord.STATE_REQUEST,
                ConnectionRecord.STATE_RESPONSE,
        ):
            raise ConnectionManagerError(
                "Connection is not in the request or response state")

        request = await connection.retrieve_request(self.context)
        wallet: BaseWallet = await self.context.inject(BaseWallet)
        if connection.my_did:
            my_info = await wallet.get_local_did(connection.my_did)
        else:
            my_info = await wallet.create_local_did()
            connection.my_did = my_info.did

        # Create connection response message
        if not my_endpoint:
            my_endpoints = []
            default_endpoint = self.context.settings.get("default_endpoint")
            if default_endpoint:
                my_endpoints.append(default_endpoint)
            my_endpoints.extend(
                self.context.settings.get("additional_endpoints", []))
        did_doc = await self.create_did_document(
            my_info, connection.inbound_connection_id, my_endpoints)
        response = ConnectionResponse(
            connection=ConnectionDetail(did=my_info.did, did_doc=did_doc))
        # Assign thread information
        response.assign_thread_from(request)
        response.assign_trace_from(request)
        # Sign connection field using the invitation key
        wallet: BaseWallet = await self.context.inject(BaseWallet)
        await response.sign_field("connection", connection.invitation_key,
                                  wallet)

        # Update connection state
        connection.state = ConnectionRecord.STATE_RESPONSE

        await connection.save(
            self.context,
            reason="Created connection response",
            log_params={"response": response},
        )
        return response
Ejemplo n.º 17
0
    async def receive_request(self, request: ConnectionRequest,
                              receipt: MessageReceipt) -> ConnectionRecord:
        """
        Receive and store a connection request.

        Args:
            request: The `ConnectionRequest` to accept
            receipt: The message receipt

        Returns:
            The new or updated `ConnectionRecord` instance

        """
        ConnectionRecord.log_state(self.context,
                                   "Receiving connection request",
                                   {"request": request})

        connection = None
        connection_key = None

        # Determine what key will need to sign the response
        if receipt.recipient_did_public:
            wallet: BaseWallet = await self.context.inject(BaseWallet)
            my_info = await wallet.get_local_did(receipt.recipient_did)
            connection_key = my_info.verkey
        else:
            connection_key = receipt.recipient_verkey
            try:
                connection = await ConnectionRecord.retrieve_by_invitation_key(
                    self.context, connection_key,
                    ConnectionRecord.INITIATOR_SELF)
            except StorageNotFoundError:
                raise ConnectionManagerError(
                    "No invitation found for pairwise connection")

        invitation = None
        if connection:
            invitation = await connection.retrieve_invitation(self.context)
            connection_key = connection.invitation_key
            ConnectionRecord.log_state(self.context, "Found invitation",
                                       {"invitation": invitation})

            if connection.is_multiuse_invitation:
                wallet: BaseWallet = await self.context.inject(BaseWallet)
                my_info = await wallet.create_local_did()
                new_connection = ConnectionRecord(
                    initiator=ConnectionRecord.INITIATOR_MULTIUSE,
                    invitation_key=connection_key,
                    my_did=my_info.did,
                    state=ConnectionRecord.STATE_INVITATION,
                    accept=connection.accept,
                    their_role=connection.their_role,
                )

                await new_connection.save(
                    self.context,
                    reason=
                    "Received connection request from multi-use invitation DID",
                )
                connection = new_connection

        conn_did_doc = request.connection.did_doc
        if not conn_did_doc:
            raise ConnectionManagerError(
                "No DIDDoc provided; cannot connect to public DID")
        if request.connection.did != conn_did_doc.did:
            raise ConnectionManagerError(
                "Connection DID does not match DIDDoc id",
                error_code=ProblemReportReason.REQUEST_NOT_ACCEPTED,
            )
        await self.store_did_document(conn_did_doc)

        if connection:
            connection.their_label = request.label
            connection.their_did = request.connection.did
            connection.state = ConnectionRecord.STATE_REQUEST
            await connection.save(
                self.context,
                reason="Received connection request from invitation")
        elif not self.context.settings.get("public_invites"):
            raise ConnectionManagerError("Public invitations are not enabled")
        else:
            my_info = await wallet.create_local_did()
            connection = ConnectionRecord(
                initiator=ConnectionRecord.INITIATOR_EXTERNAL,
                invitation_key=connection_key,
                my_did=my_info.did,
                their_did=request.connection.did,
                their_label=request.label,
                state=ConnectionRecord.STATE_REQUEST,
            )
            if self.context.settings.get("debug.auto_accept_requests"):
                connection.accept = ConnectionRecord.ACCEPT_AUTO

            await connection.save(
                self.context,
                reason="Received connection request from public DID")

        # Attach the connection request so it can be found and responded to
        await connection.attach_request(self.context, request)

        if connection.accept == ConnectionRecord.ACCEPT_AUTO:
            response = await self.create_response(connection)
            responder: BaseResponder = await self._context.inject(
                BaseResponder, required=False)
            if responder:
                await responder.send_reply(
                    response, connection_id=connection.connection_id)
                # refetch connection for accurate state
                connection = await ConnectionRecord.retrieve_by_id(
                    self._context, connection.connection_id)
        else:
            self._logger.debug("Connection request will await acceptance")

        return connection
Ejemplo n.º 18
0
    async def receive_invitation(
        self,
        invitation: ConnectionInvitation,
        their_role: str = None,
        auto_accept: bool = None,
        alias: str = None,
    ) -> ConnectionRecord:
        """
        Create a new connection record to track a received invitation.

        Args:
            invitation: The `ConnectionInvitation` to store
            their_role: The role assigned to this connection
            auto_accept: set to auto-accept the invitation (None to use config)
            alias: optional alias to set on the record

        Returns:
            The new `ConnectionRecord` instance

        """
        if not invitation.did:
            if not invitation.recipient_keys:
                raise ConnectionManagerError(
                    "Invitation must contain recipient key(s)")
            if not invitation.endpoint:
                raise ConnectionManagerError(
                    "Invitation must contain an endpoint")

        accept = (ConnectionRecord.ACCEPT_AUTO if
                  (auto_accept or
                   (auto_accept is None and
                    self.context.settings.get("debug.auto_accept_invites")))
                  else ConnectionRecord.ACCEPT_MANUAL)

        # Create connection record
        connection = ConnectionRecord(
            initiator=ConnectionRecord.INITIATOR_EXTERNAL,
            invitation_key=invitation.recipient_keys
            and invitation.recipient_keys[0],
            their_label=invitation.label,
            their_role=their_role,
            state=ConnectionRecord.STATE_INVITATION,
            accept=accept,
            alias=alias,
        )

        await connection.save(
            self.context,
            reason="Created new connection record from invitation",
            log_params={
                "invitation": invitation,
                "role": their_role
            },
        )

        # Save the invitation for later processing
        await connection.attach_invitation(self.context, invitation)

        if connection.accept == ConnectionRecord.ACCEPT_AUTO:
            request = await self.create_request(connection)
            responder: BaseResponder = await self._context.inject(
                BaseResponder, required=False)
            if responder:
                await responder.send(request,
                                     connection_id=connection.connection_id)
                # refetch connection for accurate state
                connection = await ConnectionRecord.retrieve_by_id(
                    self._context, connection.connection_id)
        else:
            self._logger.debug("Connection invitation will await acceptance")

        return connection