Establishing Connections
Accept connection invitations, establish peer connections, and manage pending connections.
In Decentralized Identity, establishing an end-to-end encrypted communication channels with a peer is an essential step before receiving or proving credentials via DIDComm/Aries. As such, the agent's ConnectionExchangeModule
provides all functionality needed to establish those connections with peers, and manage pending connections.
The functionality of the ConnectionExchangeModule
is accessed via the agent
's fields: agent.connections.exchange
. The functionality provided is described below.
Receive a Connection Invitation
A ConnectionExchange
, representing a pending connection, will be created by the agent once a connection invitation is received. To have the agent 'receive' an invitation message, this can be achieved in multiple ways. An invitation JSON payload message can be pre-processed and passed to the agent via the receiveMessage
API. Or, if the invitation is in its URL form (as it typically is when scanned from a QR Code), our URL Message Source can be used to process and receive that payload.
After an invitation is received by the agent, a ConnectionExchange
is created in the agent's wallet and can be accessed and used by the other APIs mentioned below. If the agent is subscribed to agent events, a new ConnectionExchange
will invoke a connection update event.
A ConnectionExchange
created as a result of receiving an invitation will have a role of INVITEE
.
Create a Connection Invitation
Alternatively, the Edge Agent can create connection invitations to be received by other agents. The module's createInvitation
API can be used to create connection invitations. This API takes a configuration, which has variants depending on the format of the invitation which should be created.
After calling the createInvitation
API, a CreatedInvitation
data structure will be returned, which contains the created invitation URL encoding, and the newly created ConnectionExchange
in the INVITATION
state. The ID of this ConnectionExchange
object can be used to track updates in it's state (e.g. an incoming connection request from a peer who receives the invitation).
A ConnectionExchange
created as a result of creating an invitation will have a role of INVITER
.
Creating an Out of Band Invitation
To create a pairwise single-use Aries Out of Band Connection Invitation (RFC 0434), the createInvitation
API should be used with the "pairwise" variant of the CreateInvitationConfiguration
input configuration. This configuration takes the following input:
A
Routing
object, which is an item that the agent will use to inform the peer about how this agent should be contacted. This object notably includes aserviceEndpoint
(a HTTP endpoint where the peer should send DIDComm messages to), and an optional list ofroutingVerkeys
, which (if using a mediator) describes what layers of encryption the peer should wrap their messages in. If the agent is being used with the Sudo DI Relay, then a relay postbox can be used to create aRouting
for thatPostbox
(see below). If a custom message source is being used,Routing
will need to be manually constructed.overrideLabel
- a connection label which the inviter should present to the peer as a nickname for themself. If this input is provided, it will override the globally configured label.overrideBaseUrl
- the base URL for the encoded invitation URL (see RFC 0160). If this input is provided, it will override the globally configured invitation base URL.
let agent: SudoDIEdgeAgent
let routing: Routing
let label = "Alice"
let baseUrl = "https://example.com"
do {
let createdInvitation = try await agent.connections.exchange.createInvitation(
configuration: .pairwise(
routing: routing,
overrideLabel: label,
overrideBaseUrl: baseUrl
)
)
} catch {
// handle error
}
Aries AIP1 Connection Invitations (RFC 0160) have been superseded by AIP2 invitations and are now deprecated. However they can still be created using the .LegacyPairwise
variant of the configuration used above. Deprecated APIs will be removed eventually, so please migrate to use AIP2 invitations as seen above.
Accepting a Connection
Once a connection invitation is received (INVITATION
state in the INVITEE
role), or a connection request is received in response to a connection created by the Edge Agent (REQUEST
state in the INVITER
role) the agent has different options for how they can "accept" that connection.
The agent may choose to create a new connection (Establish a New Connection) or, in certain situations as an Invitee, the connection may be reused to avoid creating a duplicate connection with a peer (Reusing an Existing Connection).
Establish a New Connection
The agent can "accept" an exchange by creating a new connection. This will begin the next steps of the Aries protocol to establish an end-to-end encrypted channel with that peer.
To accept a new connection, the acceptConnection
API is used to with a AcceptConnectionConfiguration.NewConnection
configuration variant. The configuration takes the following:
A
Routing
object, which is an item that the agent will use to inform the peer about how this agent should be contacted. This object notably includes aserviceEndpoint
(a HTTP endpoint where the peer should send DIDComm messages to), and an optional list ofroutingVerkeys
, which (if using a mediator) describes what layers of encryption the peer should wrap their messages in. If the agent is being used with the Sudo DI Relay, then a relay postbox can be used to create aRouting
for thatPostbox
(see below). If a custom message source is being used,Routing
will need to be manually constructed.Optionally, a
PeerConnectionConfiguration
object. Which includes configuration for how the agent should present itself to the peer. Any provided configuration here will override the global setting for this specific connection.
let connectionExchangeId: String // The identifier of the [ConnectionExchange]
let routingVerkeys: [String] // any additional routing verkeys that
// the peer should encrypt their messages with before sending.
// In the case of the sudo relay, this is an emptyList(), whereas mediator
// services may specify routing keys.
let routing = Routing(serviceEndpoint: "https://sample.endpoint.com/abc", routingVerkeys: routingVerkeys)
do {
try await agent.connections.exchange.acceptConnection(
connectionExchangeId: connectionExchangeId,
configuration: .newConnection(
routing: routing,
peerConfiguration: PeerConnectionConfiguration()
)
)
} catch {
// handle error
}
Reusing an Existing Connection
If a ConnectionExchange
is in the invitation state as an invitee (recipient of an invitation), the agent SDK will determine if there are already established connections with the inviter. If there are, then the reusableConnectionIds
field of the ConnectionExchange
will be populated with a list of established Connection
IDs (see Manage Connections) that were detected to originate from the same Inviter.
When a ConnectionExchange
is populated with reusableConnectionIds
, then the desired ID from this list may be used to attempt a "connection reuse". Doing so will request that the inviter use the desired original connection instead of trying to establish a new connection, the exchange now enters the "reuse request" state. Once/if the peer responds, the exchange will complete; successfully re-using the connection. If the peer does not respond, a new connection can be established with this exchange instead (Establish a New Connection).
To accept a connection via reuse, the acceptConnection
API is used to with a AcceptConnectionConfiguration.ReuseExistingConnection
configuration variant. The configuration takes the following:
connectionId
, the ID of theConnection
to request reuse from
let connectionExchangeId: String // The identifier of the [ConnectionExchange]
let connectionId: String // the identifier of the [Connection] to request reuse
do {
try await agent.connections.exchange.acceptConnection(
connectionExchangeId: connectionExchangeId,
configuration: .reuseExistingConnection(connectionId: connectionId)
)
} catch {
// handle error
}
Creating a Connection Route from the Sudo Relay SDK
When integrating with the Sudo Relay SDK (i.e. via the relay message source), the Routing
for the acceptConnection
API can be easily created with the routingFromPostbox
helper API on the relay message source.
let connectionExchangeId: String // The identifier of the [ConnectionExchange]
let postbox: Postbox // A new postbox created by a sudo relay client.
let relayRouting = SudoDIRelayMessageSource.routingFromPostbox(postbox: postbox)
do {
try await agent.connections.exchange.acceptConnection(
connectionExchangeId: connectionExchangeId,
configuration: .newConnection(routing: relayRouting)
)
} catch {
// handle error
}
To avoid any chance of peers correlating different connections to a specific relay endpoint, it is recommended that a new Postbox
(and therefore new Postbox.serviceEndpoint
and corresponding Routing
) is created per connection.
Get a Pending Connection by ID
As mentioned above, a pending connection is represented by ConnectionExchange
objects. To retrieve the current state of a specific ConnectionExchange
in the agent's wallet, the getById
API can be used. If a connection exchange cannot be found by the given ID, then null
is returned:
let id: String // id of the ConnectionExchange
do {
let connectionExchange = try await agent.connections.exchange.getById(connectionExchangeId: id)
} catch {
// handle error
}
Delete a Pending Connection by ID
Similarly, a ConnectionExchange
in the wallet can be easily deleted via the deleteById
API:
let id: String // id of the ConnectionExchange
do {
try await agent.connections.exchange.deleteById(connectionExchangeId: id)
} catch {
// handle error
}
Updating the Metadata of a Pending Connection
ConnectionExchange
objects contain some metadata that can be controlled by SDK consumers, allowing custom information to be attached to each ConnectionExchange
, and allowing custom listing functionality to be leveraged.
Each ConnectionExchange
contains a list of RecordTag
(ConnectionExchange.tags
) attached to it, where a RecordTag
is simply a name-value pair stored with the record. By default, some tags are attached to a new ConnectionExchange
, this includes:
tag-name:
~started_timestamp
tag-value: The UNIX epoch seconds which this connection began establishment
The tags on a ConnectionExchange
can be replaced or updated by using the updateConnectionExchange
API, and providing a new set to update. This will replace whatever the current set of tags is:
let id: String // ID of the connection exchange to update
// add a 'category' of 'work' to this connection, and a 'priority' of '1'
let update = ConnectionExchangeUpdate(tags: [
RecordTag(name: "category", value: "work"),
RecordTag(name: "~priority", value: "1")
])
do {
try await agent.connections.exchange.updateConnectionExchange(connectionExchangeId: id, connectionExchangeUpdate: update)
} catch {
// handle error
}
Listing Pending Connections
To list all pending connections in the agent's wallet, the listAll
API can be used:
do {
let conns = try await agent.connections.exchange.listAll(options: nil)
} catch {
// handle error
}
Filtered Listing
More complicated ConnectionExchange
list queries can also be achieved by utilizing the ListConnectionExchangeFilters
.
These filters allow for the list of ConnectionExchange
to be filtered by their state
, tags
, or both together.
Filtering by state
can be achieved as follows:
// get all the pending connections which are waiting for their invitation to be accepted
let filters = ListConnectionExchangeFilters(state: ConnectionExchangeState.invitation)
let options = ListConnectionExchangeOptions(filters: filters)
do {
let conns = try await agent.connections.exchange.listAll(options: options)
} catch {
// handle error
}
To filter by tags
applied to the ConnectionExchange
(i.e. applied via the update API), the tagFilter
field of ListConnectionExchangeFilters
should be used. This field takes a String
in compliance with a Wallet Query Language (WQL) Query.
Continuing from the example in the Update section:
// WQL Query, filter for 'category' == 'work'
let wqlQuery = "{ \"category\": \"work\" }"
let filters = ListConnectionExchangeFilters(tagFilter: wqlQuery)
let options = ListConnectionExchangeOptions(filters: filters)
do {
let pendingWorkConnections = try await agent.connections.exchange.listAll(options: options)
} catch {
// handle error
}
// WQL Query, filter for priority < 2 (e.g. 'high' priority items)
let wqlQuery = "{ \"~priority\": { \"$lt\": \"2\" } }"
let filters = ListConnectionExchangeFilters(tagFilter: wqlQuery)
let options = ListConnectionExchangeOptions(filters: filters)
do {
let highPriorityPendingConnections = try await agent.connections.exchange.listAll(options: options)
} catch {
// Handle error
}
Last updated