Migrating from AFJ 0.2.x to 0.3.x
This document describes everything you need to know for updating AFJ 0.2.x to 0.3.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.3.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.3.0 @aries-framework/core@^0.3.0 indy-sdk-react-native@^0.3.0
# or NPM
npn install @aries-framework/react-native@^0.3.0 @aries-framework/core@^0.3.0 indy-sdk-react-native@^0.3.0
yarn add @aries-framework/node@^0.3.0 @aries-framework/core@^0.3.0
# or NPM
npm install @aries-framework/node@^0.3.0 @aries-framework/core@^0.3.0
Breaking Code Changes
This section will list all breaking changes made to the public API of AFJ between version 0.2.x and 0.3.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 Modules to the Plugin API), but it is possible some breaking changes are not documented here (feel free to open PRs).
Agent creation
The agent constructor has been updated to a single AgentOptions
object that contains the config and dependencies properties.
- 0.2.x
- 0.3.x
const agent = new Agent(agentConfig, agentDependencies)
const agent = new Agent({ config: agentConfig, dependencies: agentDependencies })
This object contains:
- config: Agent's initial configuration
- dependencies: platform-specific Agent dependencies
- modules: optional field for internal module configuration and custom module registration
For easy migration, you can simply construct AgentOptions
by putting current InitConfig into config
key and agentDependencies into dependencies
key.
Note that, if you are defining indyLedgers
configuration, you should set the indyNamespace for every ledger, as explained in Agent Config tutorial.
did:key usage in protocols
In accordance with Aries RFC 0360, since 0.2.5 there is a configuration parameter called useDidKeyInProtocols
which, when enabled, will encode keys in did:key instead of previous base58 format, unless the other party has started a protocol and is using base58.
This parameter was previously disabled by default and now it is enabled. If your agent only interacts with modern agents (e.g. AFJ 0.2.5 and newer) this will not represent any issue. Otherwise it is safer to explicitly set it to false
. However, keep in mind that we expect this setting to be deprecated in the future, so we encourage you to update all your agents to use did:key.
Modules extracted from the core
In this release two modules were extracted from the core and published as separate, optional packages:
- actionMenu has been moved to @aries-framework/action-menu
- questionAnswer has been moved to @aries-framework/question-answer
If you want to use them, you can integrate in an Agent instance by injecting them in constructor, as follows:
import { ActionMenuModule } from '@aries-framework/action-menu'
import { QuestionAnswerModule } from '@aries-framework/question-answer'
const agent = new Agent({
config: {
/* config */
},
dependencies: agentDependencies,
modules: {
actionMenu: new ActionMenuModule(),
questionAnswer: new QuestionAnswerModule(),
/* other custom modules */
},
})
As they are now considered custom modules, their API can be accessed in modules
namespace, so you should add it to every call to them.
- 0.2.x
- 0.3.x
await agent.questionAnswer.sendQuestion(connectionId, {
question: 'Do you want to play?',
validResponses: [{ text: 'Yes' }, { text: 'No' }],
})
await agent.questionAnswer.sendAnswer(questionAnswerRecordId, 'Yes')
await agent.modules.questionAnswer.sendQuestion(connectionId, {
question: 'Do you want to play?',
validResponses: [{ text: 'Yes' }, { text: 'No' }],
})
await agent.modules.questionAnswer.sendAnswer(questionAnswerRecordId, 'Yes')
Discover Features Module
This module now supports both Discover Features V1 and V2, and as it happened to other modules, queryFeatures
method parameters have been unified to a single object and requires to specify the version of Discover Features protocol to be used. Note that query
property has been replaced by the more general queries
which accepts multiple features to be search for. To convert a query to this new format you simply need to create a single-object array whose unique object whose featureType
field is 'protocol' and match
field is the query itself.
- 0.2.x
- 0.3.x
await agent.discovery.queryFeatures(connectionId, {
query: 'https://didcomm.org/messagepickup/2.0',
comment: 'Detect if protocol is supported',
})
await agent.discovery.queryFeatures({
connectionId,
protocolVersion: 'v1',
queries: [{ featureType: 'protocol', match: 'https://didcomm.org/messagepickup/2.0' }],
comment: 'Detect if protocol is supported',
})
The convenience method isProtocolSupported has been replaced by the more general synchronous mode of queryFeatures, which works when awaitDisclosures in options is set. Instead of returning a boolean, it returns an object with matching features:
- 0.2.x
- 0.3.x
const isPickUpV2Supported = await agent.discovery.isProtocolSupported(connectionId, StatusRequestMessage)
const discloseForPickupV2 = await agent.discovery.queryFeatures({
connectionId: connectionId,
protocolVersion: 'v1',
queries: [{ featureType: 'protocol', match: StatusMessage.type.protocolUri }],
awaitDisclosures: true,
awaitDisclosuresTimeoutMs: 7000,
})
const isPickUpV2Supported = discloseForPickupV2.features?.length === 1
Discover Features module does not rely anymore on Agent Dispatcher
to determine protocol support. Instead, it uses the new Feature Registry, where any custom modules implementing protocols must register them.
This procedure can be done in module's register(dependencyManager, featureRegistry)
.
Ledger Module
Apart from the aforementioned indyLedgers configuration, you should also note a slight change in behaviour when attempting to register credential definitions that already exists on the ledger but not in the wallet.
Proofs Module
Module API Updates
Much in the same way as in 0.2.0 release when Issue Credential V2 protocol has been added, now that Present Proof V2 is supported, we introduced changes to proofs module.
Basically, for all methods in the proofs 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 proposals, requests or presentations (
proposeProof
,acceptProposal
,requestProof
,acceptPresentation
, etc.), you should passprotocolVersion: 'v1'
to indicate we should use the v1 protocol - All indy specific attributes (e.g. Presentation Preview) should be passed in the
proofFormats.indy
object. - Some indy objects, as the preview should now be passed only as their attributes (i.e. no need of creating the object instance) and provided in the
proofFormats.indy
object.
- 0.2.x
- 0.3.x
await agent.proofs.proposeProof(
'connectionId',
new PresentationPreview({
attributes: [new PresentationPreviewAttribute({ name: 'key', value: 'value' })],
predicates: [
new PresentationPreviewPredicate({
name: 'age',
credentialDefinitionId,
predicate: PredicateType.GreaterThanOrEqualTo,
threshold: 50,
}),
],
})
)
await agent.proofs.proposeProof({
connectionId: connection.id,
protocolVersion: 'v1',
proofFormats: {
indy: {
attributes: [{ name: 'key', value: 'value' }],
predicates: [{name: 'age', credentialDefinitionId, predicate: PredicateType.GreaterThanOrEqualTo, threshold: 50, ]
},
},
comment: 'Propose proof comment',
})
Messages Extracted from Proof Exchange Record
The DIDComm messages that were previously stored on the proof 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 proof exchange record agnostic of the protocol version. Instead of accessing the messages through the proposalMessage
, requestMessage
and presentationMessage
parameters, we now expose dedicated methods on the proofs 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
(V1RequestPresentationMessage
and V2RequestPresentationMessage
). If you were using these messages classes throughout your codebase, update them to use the V1
prefix.
- 0.2.x
- 0.3.x
const proofRecord = await agent.proofs.getById('proofRecordId')
const proposalMessage = proofRecord.proposalMessage
const requestMessage = proofRecord.requestMessage
const presentationMessage = proofRecord.presentationMessage
const proofRecord = await agent.proofs.getById('proofRecordId')
const proposalMessage = await agent.proofs.findProposalMessage('proofRecordId')
const requestMessage = await agent.proofs.findRequestMessage('proofRecordId')
const presentationMessage = await agent.proofs.findPresentationMessage('proofRecordId')
Because AFJ now also supports the present proof 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 V1RequestPresentationMessage) {
// do something
}
Out Of Band Proofs and Credentials
With the addition of the out of band module, the creation of connection-less messages has been split into two steps, allowing for better control and flexibility. The previous agent.proofs.createOutOfBandRequest
has been replaced by the agent.proofs.createRequest
method. This new method creates a proof request that is not tied to any connection.
What you can now do is call agent.oob.createLegacyConnectionlessInvitation
to attach the service decorator to the message and get a legacy connectionless message.
- 0.2.x
- 0.3.x
const { requestMessage, proofRecord } = await agent.proofs.createOutOfBandRequest({
requestedAttributes: {
group1: {
name: 'dateOfBirth',
restrictions: [{ schemaId: 'F72i3Y3Q4i466efjYJYCHM:2:aha_cert:4.1.1' }],
},
},
})
const { message, proofRecord } = await agent.proofs.createRequest({
protocolVersion: 'v1',
proofFormats: {
indy: {
requestedAttributes: {
group1: {
name: 'dateOfBirth',
restrictions: [
{
schemaId: 'F72i3Y3Q4i466efjYJYCHM:2:aha_cert:4.1.1',
},
],
},
},
},
},
})
const { invitationUrl, message: messageWithServiceDecorator } = await agent.oob.createLegacyConnectionlessInvitation({
recordId: proofRecord.id,
domain: 'https://google.com',
message,
})
Out of band invitations are the new way to send messages out of band. You can use it for connection-less exchanges, but also for exchanges that you want to establish a connection for first. Here's an example on how to use the out of band module to create a connection-less invitation for a proof request:
const outOfBandRecord = await agent.oob.createInvitation({
handshake: false, // set to true if you want to create a connection
messages: [message],
})
const invitationUrl = outOfBandRecord.outOfBandInvitation.toUrl({
domain: 'https://afj.com',
})
As you can see, there's now a lot more ways to use a message not tied to a connection. By splitting the creation of the message from the creation of the invitation, we can now create a message not bound to a connection (at time of creation) for multiple use cases.
Updating Custom Modules to the new Plugin API
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 if you have custom modules and want to upgrade them to make compatible with AFJ 0.3.0.
Renaming handler classes
Handler
has been have been renamed to MessageHandler
to be be more descriptive, along with related types and methods. This means:
Handler
is nowMessageHandler
HandlerInboundMessage
is nowMessageHandlerInboundMessage
Dispatcher.registerHandler
is nowDispatcher.registerMessageHandler
and is marked as deprecated. The recommended way of registering handlers is by using the newMessageHandlerRegistry
object by callingMessageHandlerRegistry.registerMessageHandler
.
If your custom module include message handlers, you must update them accordingly.
- 0.2.x
- 0.3.x
export class MyHandler implements Handler {
public supportedMessages = [MyMessage]
public async handle(inboundMessage: HandlerInboundMessage<MyHandler>) {
...
}
}
export class MyHandler implements MessageHandler {
public supportedMessages = [MyMessage]
public async handle(inboundMessage: MessageHandlerInboundMessage<MyHandler>) {
...
}
}
Using AgentContext
First of all, it's worth noting that now all services and repositories have been made stateless. A new AgentContext
is introduced that holds the current context, which is passed to each method call. Therefore, you'll need to update every call to services, repositories and also eventEmitter methods to pass AgentContext
object as first argument.
AgentContext can be obtained from either:
- MessageContext used by message handlers (accesed as messageContext.agentContext)
- Injected in your API constructor: you can store the instance and pass it to all your service and repository calls
- 0.2.x
- 0.3.x
public async createRequest(options: CreateRequestOptions) {
const message = new RequestMessage({
parentThreadId: options.parentThreadId,
})
const record = new MyRecord({
connectionId: options.connectionRecord.id,
threadId: message.id,
parentThreadId: options.parentThreadId,
})
await this.myRecordRepository.save(record)
this.eventEmitter.emit<MyRecordStateChangedEvent>({
type: MyRecordEventTypes.StateChanged,
payload: {
myRecord: record,
previousState: null,
},
})
return { record, message }
}
public async processRequest(messageContext: HandlerInboundMessage<RequestHandler>) {
const { message } = messageContext
const record = new MyRecord({
connectionId: connection.id,
threadId: messageContext.message.id,
parentThreadId: messageContext.message.thread?.parentThreadId,
})
await this.myRepository.save(record)
return record
}
public async createRequest(agentContext: AgentContext, options: CreateRequestOptions) {
const message = new RequestMessage({
parentThreadId: options.parentThreadId,
})
const record = new MyRecord({
connectionId: options.connectionRecord.id,
threadId: message.id,
parentThreadId: options.parentThreadId,
})
await this.myRecordRepository.save(agentContext, record)
this.eventEmitter.emit<MyRecordStateChangedEvent>(agentContext, {
type: MyRecordEventTypes.StateChanged,
payload: {
myRecord: record,
previousState: null,
},
})
return { record, message }
}
public async processRequest(messageContext: MessageHandlerInboundMessage<RequestHandler>) {
const { message } = messageContext
const record = new MyRecord({
connectionId: connection.id,
threadId: messageContext.message.id,
parentThreadId: messageContext.message.thread?.parentThreadId,
})
await this.myRepository.save(messageContext.agentContext, record)
return record
}
Using OutboundMessageContext
If your module implements a protocol that sends messages to other agents, you will notice that Agent's MessageSender
now receives the more generic OutboundMessageContext
class, which replaces previous helper method createOutboundMessage
.
You can take advantage of this new mechanism to associate a record to the context, in order to do specific actions to it when outbound message state changes (e.g. a MessageSendingError
is thrown or AgentMessageSentEvent
is emitted).
- 0.2.x
- 0.3.x
import { createOutboundMessage } from '@aries-framework/core'
const outboundMessage = createOutboundMessage(connection, message)
await this.messageSender.sendMessage(outboundMessage)
import { OutboundMessageContext } from '@aries-framework/core'
const outboundMessageContext = new OutboundMessageContext(message, {
agentContext: this.agentContext,
connection,
// optional, if you want to link the message to a related record
associatedRecord: record,
})
await this.messageSender.sendMessage(outboundMessageContext)
Updating module structure to register in new Plugin API
Existing modules can benefit from the new Plugin API mechanism by doing the following modifications:
- Rename Module class (e.g. MyModule) to API class (MyApi) and add @injectable decorator. Inject AgentContext in order to pass it to any services or repositories it might call. For instance:
import { injectable } from '@aries-framework/core'
@injectable() // <-- Add this
export class MyApi {
private messageSender: MessageSender
private myService: MyService
private connectionService: ConnectionService
private agentContext: AgentContext // <-- Add this
public constructor(
messageHandlerRegistry: MessageHandlerRegistry, // <-- use this instead of Dispatcher
messageSender: MessageSender,
myService: MyService,
connectionService: ConnectionService,
agentContext: AgentContext // <-- Add this
) {
this.messageSender = messageSender
this.myService = myService
this.connectionService = connectionService
this.agentContext = agentContext // <-- Add this
this.registerHandlers(messageHandlerRegistry) // <-- use messageHandlerRegistry instead of dispatcher
}
- Create a new Module class that implements Module interface and registers the dependencies and features. For instance:
import type { DependencyManager, FeatureRegistry, Module } from '@aries-framework/core'
import { Protocol } from '@aries-framework/core'
export class MyModule implements Module {
public readonly api = MyApi // the one we've just renamed from MyModule
public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) {
// Api
dependencyManager.registerContextScoped(MyApi)
// Services
dependencyManager.registerSingleton(MyService)
// Repositories
dependencyManager.registerSingleton(MyRepository)
// Feature Registry: don't forget to register your protocols and other features your module may add
featureRegistry.register(
new Protocol({
id: 'https://didcomm.org/my-protocol/1.0',
roles: [MyRole.Sender, MyRole.Receiver],
})
)
}
After doing this, you can add your module to agent constructor like this:
const agent = new Agent({
config: {
/* config */
},
dependencies: agentDependencies,
modules: {
myModule: new MyModule(),
/* other custom modules */
},
})
// MyModule API can be accessed in agent.modules namespace
await agent.modules.myModule.doSomething()
await agent.modules.myModule.doAnotherThing()
Breaking Storage Changes
The 0.3.0 release introduces some breaking changes to the storage format, mainly related to Proof Exchanges.
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.
There are no config parameters to be provided to the update assistant to migrate from 0.2.x to 0.3.x.
Migrate Proof Record Properties
In 0.3.0 the v1 DIDComm messages have been moved out of the proof record into separate records using the DidCommMessageRepository. The migration scripts extracts all messages (proposalMessage, requestMessage, presentationMessage) and moves them into the DidCommMessageRepository. With the addition of support for different protocol versions the proof record now stores the protocol version.
- 0.2.x
- 0.3.x
{
"proposalMessage": { ... },
"requestMessage": { ... },
"presentationMessage": { ... },
}
{
"protocolVersion": "v1"
}
Migrate Connection Record properties
The recently introduced connectionType
tag has been pluralized to reflect the fact that more than a single connection type can be defined for a given connection. Also, it is now available as a direct record property (e.g. can be queried and set by using connectionRecord.connectionTypes
) apart from the tag for efficient search.
The migration script renames connectionType
to connectionTypes
in all connections, and also searches for any mediation connection and adds ConnectionType.Mediator
as one of its types.
Migrate Did Record properties
The didRecord.id
was previously the did itself. However to allow for connecting with self, where multiple did records are created for the same did, the id property is now an uuid and a separate did property is added.
The migration script generates a new ID for each did record and stores its did into didRecord.did property.
- 0.2.x
- 0.3.x
{
"id": "did"
}
{
"id": "uuid",
"did": "did"
}