Migrating from AFJ 0.1.0 to 0.2.x
This document describes everything you need to know for updating AFJ 0.1.0 to 0.2.x. If you're not aware of how updating in AFJ works make sure to first read the guide on Updating AFJ.
First of all, update you dependencies to the 0.2.x versions. This will also update the needed peer depedencnies. Extension packages are not updated with this command. You need to update these manually, and make sure they're up to date with the latest version of AFJ.
- React Native
- Node
yarn add @aries-framework/react-native@^0.2.0 @aries-framework/core@^0.2.0 indy-sdk-react-native@^0.2.0
# or NPM
npn install @aries-framework/react-native@^0.2.0 @aries-framework/core@^0.2.0 indy-sdk-react-native@^0.2.0
yarn add @aries-framework/node@^0.2.0 @aries-framework/core@^0.2.0
# or NPM
npm install @aries-framework/node@^0.2.0 @aries-framework/core@^0.2.0
Breaking Code Changes
This section will list all breaking changes made to the public API of AFJ between version 0.1.0 and 0.2.0.
If you have custom modules take into account there could be a lot more breaking changes that aren't documented here. We try to make sure that the biggest breaking changes to the internal API are also documented here (e.g. see Updating Custom Messages to the New Message Type Objects), but it is possible some breaking changes are not documented here (feel free to open PRs).
Credentials Module
Module API Updates
With the addition of the issue credential v2 protocol and the preparation for multiple attachment formats (to be added in a later release), we've made some big changes to the credentials module API. Most changes are related to structure, so updating your code to the new API should be straightforward.
Basically for all methods in the credential module you should take the following steps to update your code:
- Move all function parameters into a single object. All module methods now take a single object that contain all properties.
- For methods that initiate a protocol (starting from offer, proposal), you should pass
protocolVersion: 'v1'
to indicate we should use the v1 protocol. (v2 is also supported, but this focuses on the breaking changes, not the new features). - All indy specific attributes (e.g. credentialDefinitionId) should be passed in the
credentialFormats.indy
object. - The preview should now be passed as only the preview attributes (the the full preview) and provided in the
credentialFormats.indy
object.
- 0.1.0
- 0.2.x
await agent.credentials.offerCredential('connectionId', {
autoAcceptCredential: AutoAcceptCredential.Always,
comment: 'hello',
credentialDefinitionId: 'credentialDefinitionId',
preview: new CredentialPreview({
attributes: [new CredentialPreviewAttribute({ name: 'key', value: 'value' })],
}),
})
await agent.credentials.offerCredential({
connectionId: 'connectionId',
protocolVersion: 'v1',
autoAcceptCredential: AutoAcceptCredential.Always,
comment: 'hello',
credentialFormats: {
indy: {
credentialDefinitionId: 'credentialDefinitionId',
attributes: [{ name: 'key', value: 'value' }],
},
},
})
Data from Received Messages only Stored in Record after Accepting
Previously when we received a message from another connection we would store the relevant data from the exchange in the credential record. The values we would store were the credentialDefinitionId
and schemaId
in the credential metadata, and the credentialAttributes
field.
Starting with AFJ 0.2.0 the values are not stored in the credential record until after the message content has been accepted (in the case of an offer this means after sending the request message). This is to avoid ambiguity of the values in the credential record. If I have sent a proposal and then receive an offer, should the credential record contain the values from the proposal or the values from the offer? The first one reflects our view on what the data should be, the second one reflects the latest data.
We decided to make the record properties always hold our view of what the data should be, and only update it after accepting the contents of a received message (either using auto accept, or by calling the acceptXXX
methods on the credential module).
This is an important change and requires some updates to how you extract the relevant data from the offer (or other messages such the proposal). We've added a new getFormatData
method on the credentials module that allows you to retrieve the attributes and format data for all messages in an exchange. One of the advantages of this approach is that we don't have to store all relevant data in the credential record anymore, which helps when adding new formats that don't match with the attributes used for indy credentials. In addition, the return value for this method is the same whether v1 or v2 of the protocol is used. This means your code should only care about the credential format (indy in this case) and doesn't have to worry about the protocol version.
- 0.1.0
- 0.2.x
agent.events.on<CredentialStateChangedEvent>(
CredentialEventTypes.CredentialStateChanged,
({ payload: { credentialRecord } }) => {
const indyCredentialMetadata = credentialRecord.metadata.get(CredentialMetadataKeys.IndyCredential)
// Get credential definition id, schema id and attributes from offer
const credentialDefinitionId = indyCredentialMetadata?.credentialDefinitionId
const schemaId = indyCredentialMetadata?.schemaId
const attributes = credentialRecord.credentialAttributes
}
)
agent.events.on<CredentialStateChangedEvent>(
CredentialEventTypes.CredentialStateChanged,
async ({ payload: { credentialRecord } }) => {
const formatData = await agent.credentials.getFormatData(credentialRecord.id)
// Get credential definition id, schema id and attributes from offer
const credentialDefinitionId = formatData.offer?.indy?.cred_def_id
const schemaId = formatData.offer?.indy?.schema_id
const attributes = formatData.offerAttributes
}
)
The return value of the getFormatData
method is fully typed an directly returns the format data as encoded in the attachment. It also returns the proposalAttributes
and offerAttributes
values which contain the attributes for the indy credential. This is not part of the attachment data itself, but can be seen as the format data for the credential.
{
proposalAttributes: [{ name: 'key', value: 'value' mimeType: 'text/plain' }],
proposal: {
indy: { } // indy proposal as described in RFC 0592
},
offerAttributes: [{ name: 'key', value: 'value' mimeType: 'text/plain' }],
offer: {
indy: { } // indy offer as described in RFC 0592
},
request: {
indy: { } // indy request as described in RFC 0592
}
credential: {
indy: { } // indy credential as described in RFC 0592
}
}
Messages Extracted from Credential Record
The DIDComm messages that were previously stored on the credential record, have been extracted to separate DIDComm message records. This makes it easier to work with multiple versions of the protocol internally, and keeps the credential exchange record agnostic of the protocol version. Instead of accessing the messages through the proposalMessage
, offerMessage
, requestMessage
and credentialMessage
parameters, we now expose dedicated methods on the credentials module to retrieve the message.
With the addition of the v2 messages, all v1 messages have been prefixed with V1
while v2 messages have been prefixed with V2
(V1ProposeCredentialMessage
and V2ProposeCredentialMessage
). If you were using these messages classes throughout your codebase, update them to use the V1
prefix.
- 0.1.0
- 0.2.x
const credentialRecord = await agent.credentials.getById('credentialRecordId')
const proposalMessage = credentialRecord.proposalMessage
const offerMessage = credentialRecord.offerMessage
const requestMessage = credentialRecord.requestMessage
const credentialMessage = credentialRecord.credentialMessage
const credentialRecord = await agent.credentials.getById('credentialRecordId')
const proposalMessage = await agent.credentials.findProposalMessage('credentialRecordId')
const offerMessage = await agent.credentials.findOfferMessage('credentialRecordId')
const requestMessage = await agent.credentials.findRequestMessage('credentialRecordId')
const credentialMessage = await agent.credentials.findCredentialMessage('credentialRecordId')
Because AFJ now also supports the issue credential v2 protocol, the return type of this protocol has been changed to V1XXXMessage | V2XXXMessage | null
. Take this into account when working with the messages.
You can check if a message is a specific version by using the instanceof
operator:
if (proposalMessage instanceof V1ProposeCredentialMessage) {
// do something
}
Shared properties (e.g. the proposal messages for v1 and v2 both have the credentialPreview
property) can be accessed without doing an instance check.
Connections Module
Version 0.2.0 added support for the Out of Band protocol with support for the DID Exchange protocol. Internally AFJ now uses out of band invitations for all connections, even if you're connecting using the old invitations from the Connection protocol.
Creating a Legacy Invitation
The createInvitation
method on the connections module has been moved to the out of band module and renamed to createLegacyInvitation
. The method is not planned to be removed in the near future, the legacy merely indicates this will create an RFC 0160 connection invitation. Internally AFJ creates an out of band invitation and transforms it into a legacy invitation. If you want to create an out of band invitation instead, you should use oob.createInvitation
.
- 0.1.0
- 0.2.x
const { connectionRecord, invitation } = await agent.connections.createInvitation({
/* config */
})
const invitationUrl = invitation.toUrl({ domain: 'https://example.com' })
const { outOfBandRecord, invitation } = await agent.oob.createLegacyInvitation({
/* config */
})
const invitationUrl = invitation.toUrl({ domain: 'https://example.com' })
Important thing to note here is that the oob.createLegacyInvitation
does not return a connection record, but rather an out of band record. Because out of band also supports connection-less scenarios, a connection record is not created until a connection request is received (or sent in the case of the invitee role).
You can listen for connection state change events that are associated with a specific out of band id. This allows you to link a connection to an invitation you created.
// Listen for connection state changed events associated with the out of band record
agent.events.on<ConnectionStateChangedEvent>(ConnectionEventTypes.ConnectionStateChanged, (event) => {
if (event.payload.connectionRecord.outOfBandId === outOfBandRecord.id) {
console.log(`Connection state changed for connection with out of band id ${outOfBandRecord.id}`)
}
})
It is also possible to retrieve all connection records associated with an out of band invitation. Because of multi use invitations, there could be multiple connection records associated with a single out of band invitation. Instead of having a separate connection record that will always stay in the invited state, the out of band record will now handle the multi use capabilities of an invitation.
// Retrieve all connections associated with an out of band id
const connections = await agent.connections.findAllByOutOfBandId(outOfBandRecord.id)
Receiving a Legacy Invitation
The receiveInvitation
and receiveInvitationFromUrl
methods on the connections module have also been moved to the out of band module. Both methods support the new out of band invitations and the legacy RFC 0160 connection invitations. Internally AFJ converts the old invitations to out of band invitations.
- 0.1.0
- 0.2.x
const invitationUrl = 'https://example.com?c_i=eyXXX'
// Receive invitation directly from url
const connectionRecord = await agent.connections.receiveInvitationFromUrl(invitationUrl, {
/* config */
})
// Parse invitation and receive invitation
const parsedInvitation = await ConnectionInvitationMessage.fromUrl(invitationUrl)
const connectionRecord = await agent.connections.receiveInvitation(parsedInvitation, {
/* config */
})
const invitationUrl = 'https://example.com?c_i=eyXXX'
// Receive invitation directly from url
const { outOfBandRecord, connectionRecord } = await agent.oob.receiveInvitationFromUrl(invitationUrl, {
/* config */
})
// Parse invitation and receive invitation
const parsedInvitation = await agent.oob.parseInvitation(invitationUrl)
const secondConnectionRecord = await agent.oob.receiveInvitation(parsedInvitation, {
/* config */
})
The new receive invitation methods on the out of band module won't always return a connection record anymore. The out of band invitation may not contain any handshake protocols. In the case of receiving a connection invitation you will always receive a connection record though, as you can't use it for connection-less invitations.
Updating to use DidExchangeState
The ConnectionState
that was previously used for the state of the ConnectionRecord
has been changed to use the DidExchangeState
for both connections made using the RFC 0160 Connection Protocol, as well as the RFC 0023 DID Exchange Protocol.
The DidExchangeState
has the following values:
DidExchangeState.Start
,DidExchangeState.InvitationSent
,DidExchangeState.InvitationReceived
,DidExchangeState.RequestSent
,DidExchangeState.RequestReceived
,DidExchangeState.ResponseSent
,DidExchangeState.ResponseReceived
,DidExchangeState.Abandoned
,DidExchangeState.Completed
If you still need to access the old ConnectionState
you can do so by accessing the computed connectionRecord.rfc0160State
property. This will return the old ConnectionState
value.
Updating Custom Messages to the New Message Type Objects
Although this isn't a breaking change to the public API of the framework, it is something that you will need to take into account when creating custom modules. Starting from AFJ 0.2.0 we now support handling messages with different minor versions (e.g. receive a message with @type
version 1.1 while we only support 1.0). With this change messages must now declare the message type as an ParsedMessageType
object. We've added an parseMessageType
util method that can help with this.
- 0.1.0
- 0.2.x
import { AgentMessage } from '@aries-framework/core'
import { Equals } from 'class-validator'
class MyMessage extends AgentMessage {
@Equals(MyMessage.type)
public readonly type = MyMessage.type
public static readonly type = 'https://didcomm.org/my-protocol/1.0/my-type'
}
import { AgentMessage, IsValidMessageType, parseMessageType } from '@aries-framework/core'
import { Equals } from 'class-validator'
class MyMessage extends AgentMessage {
// use IsValidMessageType instead of Equals
@IsValidMessageType(MyMessage.type)
// append .messageTypeUri to get the actual URI when instantiating a message
public readonly type = MyMessage.type.messageTypeUri
// use parseMessageType to get the message type object from a type. You can declare the object yourself, but this is the recommended way
public static readonly type = parseMessageType('https://didcomm.org/my-protocol/1.0/my-type')
}
Breaking Storage Changes
The 0.2.0 release is heavy on breaking changes to the storage format. This is not what we intend to do with every release. But as there's not that many people yet using the framework in production, and there were a lot of changes needed to keep the API straightforward, we decided to bundle a lot of breaking changes in this one release.
Below all breaking storage changes are explained in as much detail as possible. The update assistant provides all tools to migrate without a hassle, but it is important to know what has changed. All examples only show the keys that have changed, unrelated keys in records have been omitted.
See the Update Assistant documentation for a guide on how to use the update assistant.
The following config can be provided to the update assistant to migrate from 0.1.0 to 0.2.0:
{
"v0_1ToV0_2": {
"mediationRoleUpdateStrategy": "<mediationRoleUpdateStrategy>"
}
}
Credential Metadata
The credential record had a custom metadata
property in pre-0.1.0 storage that contained the requestMetadata
, schemaId
and credentialDefinition
properties. Later a generic metadata API was added that only allows objects to be stored. Therefore the properties were moved into a different structure.
- 0.1.0
- 0.2.x
{
"requestMetadata": <value of requestMetadata>,
"schemaId": "<value of schemaId>",
"credentialDefinitionId": "<value of credential definition id>"
}
{
"_internal/indyRequest": <value of requestMetadata>,
"_internal/indyCredential": {
"schemaId": "<value of schemaId>",
"credentialDefinitionId": "<value of credential definition id>"
}
}
Accessing the credentialDefinitionId
and schemaId
properties will now be done by retrieving the CredentialMetadataKeys.IndyCredential
metadata key.
const indyCredential = credentialRecord.metadata.get(CredentialMetadataKeys.IndyCredential)
// both properties are optional
indyCredential?.credentialDefinitionId
indyCredential?.schemaId
Migrate Credential Record Properties
In 0.2.0 the v1 DIDComm messages have been moved out of the credential record into separate records using the DidCommMessageRepository. The migration scripts extracts all messages (proposalMessage, offerMessage, requestMessage, credentialMessage) and moves them into the DidCommMessageRepository.
With the addition of support for different protocol versions the credential record now stores the protocol version. With the addition of issue credential v2 support, other credential formats than indy can be used, and multiple credentials can be issued at once. To account for this the credentialId
has been replaced by the credentials
array. This is an array of objects containing the credentialRecordId
and the credentialRecordType
. For all current credentials the credentialRecordType
will always be indy
.
- 0.1.0
- 0.2.x
{
"credentialId": "09e46da9-a575-4909-b016-040e96c3c539",
"proposalMessage": { ... },
"offerMessage": { ... },
"requestMessage": { ... },
"credentialMessage": { ... },
}
{
"protocolVersion": "v1",
"credentials": [
{
"credentialRecordId": "09e46da9-a575-4909-b016-040e96c3c539",
"credentialRecordType": "indy"
}
]
}
Mediation Record Role
The role in the mediation record was always being set to MediationRole.Mediator
for both mediators and recipients. This didn't cause any issues, but would return the wrong role for recipients.
In 0.2 a check is added to make sure the role of a mediation record matches with actions (e.g. a recipient can't grant mediation), which means it will throw an error if the role is not set correctly.
Because it's not always possible detect whether the role should actually be mediator or recipient, a number of configuration options are provided on how the role should be updated using the v0_1ToV0_2.mediationRoleUpdateStrategy
option:
allMediator
: The role is set toMediationRole.Mediator
for both mediators and recipientsallRecipient
: The role is set toMediationRole.Recipient
for both mediators and recipientsrecipientIfEndpoint
(default): The role is set toMediationRole.Recipient
if their is anendpoint
configured on the record. The endpoint is not set when running as a mediator. There is one case where this could be problematic when the role should be recipient, if the mediation grant hasn't actually occurred (meaning the endpoint is not set). This is probably the best approach otherwise it is set toMediationRole.Mediator
doNotChange
: The role is not changed
Most agents only act as either the role of mediator or recipient, in which case the allMediator
or allRecipient
configuration is the most appropriate. If your agent acts as both a recipient and mediator, the recipientIfEndpoint
configuration is the most appropriate. The doNotChange
options is not recommended and can lead to errors if the role is not set correctly.
Extracting Did Documents to Did Repository
The connection record previously stored both did documents from a connection in the connection record itself. Version 0.2.0 added a generic did storage that can be used for numerous usages, one of which is the storage of did documents for connection records.
The migration script extracts the did documents from the didDoc
and theirDidDoc
properties from the connection record, updates them to did documents compliant with the did core spec, and stores them in the did repository. By doing so it also updates the unqualified dids in the did
and theirDid
fields generated by the indy-sdk to fully qualified did:peer
dids compliant with the Peer DID Method Specification.
To account for the fact that the mechanism to migrate legacy did document to peer did documents is not defined yet, the legacy did and did document are stored in the did record metadata. This will be deleted later if we can be certain the did doc conversion to a did:peer
did document is correct.
- 0.1.0
- 0.2.x
{
"did": "BBPoJqRKatdcfLEAFL7exC",
"theirDid": "UppcJ5APts7ot5WX25943F",
"verkey": "GAb4NUvpBcHVCvtP45vTVa5Bp74vFg3iXzdp1Gbd68Wf",
"didDoc": <legacyDidDoc>,
"theirDidDoc": <legacyTheirDidDoc>,
}
{
"did": "did:peer:1zQmXUaPPhPCbUVZ3hGYmQmGxWTwyDfhqESXCpMFhKaF9Y2A",
"theirDid": "did:peer:1zQmZMygzYqNwU6Uhmewx5Xepf2VLp5S4HLSwwgf2aiKZuwa"
}
Migrating to the Out of Band Record
With the addition of the out of band protocol, invitations are now stored in the OutOfBandRecord
. In addition a new field invitationDid
is added to the connection record that is generated based on the invitation service or did. This allows to reuse existing connections.
The migration script extracts the invitation and other relevant data into a separate OutOfBandRecord
. By doing so it converts the old connection protocol invitation into the new Out of band invitation message. Based on the service or did of the invitation, the invitationDid
is populated.
Previously when creating a multi use invitation, a connection record would be created with the multiUseInvitation
set to true. The connection record would always be in state invited
. If a request for the multi use invitation came in, a new connection record would be created. With the addition of the out of band module, no connection records are created until a request is received. So for multi use invitation this means that the connection record with multiUseInvitation=true will be deleted, and instead all connections created using that out of band invitation will contain the outOfBandId
of the multi use invitation.
- 0.1.0
- 0.2.x
{
"invitation": {
"@type": "https://didcomm.org/connections/1.0/invitation",
"@id": "04a2c382-999e-4de9-a1d2-9dec0b2fa5e4",
"recipientKeys": ["E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu"],
"serviceEndpoint": "https://example.com",
"label": "test"
},
"multiUseInvitation": "false"
}
{
"invitationDid": "did:peer:2.Ez6MksYU4MHtfmNhNm1uGMvANr9j4CBv2FymjiJtRgA36bSVH.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSJ9",
"outOfBandId": "04a2c382-999e-4de9-a1d2-9dec0b2fa5e4"
}
Unifying Connection States and Roles
With the addition of the did exchange protocol there are now two states and roles related to the connection record; for the did exchange protocol and for the connection protocol. To keep it easy to work with the connection record, all state and role values are updated to those of the DidExchangeRole
and DidExchangeState
enums.
The migration script transforms all connection record state and role values to their respective values of the DidExchangeRole
and DidExchangeState
enums. For convenience a getter
property rfc0160ConnectionState
is added to the connection record which returns the ConnectionState
value.
- 0.1.0
- 0.2.0
{
"state": "invited",
"role": "inviter"
}
{
"state": "invitation-sent",
"role": "responder"
}