Skip to content

Commit 0831b9b

Browse files
authored
feat: 0.2.0 migration script for connections (#773)
Signed-off-by: Timo Glastra <timo@animo.id>
1 parent c8ca091 commit 0831b9b

29 files changed

Lines changed: 3746 additions & 55 deletions

docs/migration/0.1-to-0.2.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,89 @@ Because it's not always possible detect whether the role should actually be medi
7373
- `doNotChange`: The role is not changed
7474

7575
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.
76+
77+
### Extracting Did Documents to Did Repository
78+
79+
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.
80+
81+
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](https://identity.foundation/peer-did-method-spec/).
82+
83+
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.
84+
85+
The following 0.1.0 connection record structure (unrelated keys omitted):
86+
87+
```json
88+
{
89+
"did": "BBPoJqRKatdcfLEAFL7exC",
90+
"theirDid": "UppcJ5APts7ot5WX25943F",
91+
"verkey": "GAb4NUvpBcHVCvtP45vTVa5Bp74vFg3iXzdp1Gbd68Wf",
92+
"didDoc": <legacyDidDoc>,
93+
"theirDidDoc": <legacyTheirDidDoc>,
94+
}
95+
```
96+
97+
Will be transformed into the following 0.2.0 structure (unrelated keys omitted):
98+
99+
```json
100+
{
101+
"did": "did:peer:1zQmXUaPPhPCbUVZ3hGYmQmGxWTwyDfhqESXCpMFhKaF9Y2A",
102+
"theirDid": "did:peer:1zQmZMygzYqNwU6Uhmewx5Xepf2VLp5S4HLSwwgf2aiKZuwa"
103+
}
104+
```
105+
106+
### Migrating to the Out of Band Record
107+
108+
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.
109+
110+
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.
111+
112+
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.
113+
114+
The following 0.1.0 connection record structure (unrelated keys omitted):
115+
116+
```json
117+
{
118+
"invitation": {
119+
"@type": "https://didcomm.org/connections/1.0/invitation",
120+
"@id": "04a2c382-999e-4de9-a1d2-9dec0b2fa5e4",
121+
"recipientKeys": ["E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu"],
122+
"serviceEndpoint": "https://example.com",
123+
"label": "test"
124+
},
125+
"multiUseInvitation": "false"
126+
}
127+
```
128+
129+
Will be transformed into the following 0.2.0 structure (unrelated keys omitted):
130+
131+
```json
132+
{
133+
"invitationDid": "did:peer:2.Ez6MksYU4MHtfmNhNm1uGMvANr9j4CBv2FymjiJtRgA36bSVH.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSJ9",
134+
"outOfBandId": "04a2c382-999e-4de9-a1d2-9dec0b2fa5e4"
135+
}
136+
```
137+
138+
### Unifying Connection States and Roles
139+
140+
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.
141+
142+
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
143+
property `rfc0160ConnectionState` is added to the connection record which returns the `ConnectionState` value.
144+
145+
The following 0.1.0 connection record structure (unrelated keys omitted):
146+
147+
```json
148+
{
149+
"state": "invited",
150+
"role": "inviter"
151+
}
152+
```
153+
154+
Will be transformed into the following 0.2.0 structure (unrelated keys omitted):
155+
156+
```json
157+
{
158+
"state": "invitation-sent",
159+
"role": "responder"
160+
}
161+
```

packages/core/src/agent/MessageReceiver.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { Lifecycle, scoped } from 'tsyringe'
1010

1111
import { AriesFrameworkError } from '../error'
1212
import { ConnectionsModule } from '../modules/connections'
13-
import { OutOfBandService } from '../modules/oob/OutOfBandService'
1413
import { ProblemReportError, ProblemReportMessage, ProblemReportReason } from '../modules/problem-reports'
1514
import { isValidJweStructure } from '../utils/JWE'
1615
import { JsonTransformer } from '../utils/JsonTransformer'
@@ -35,23 +34,20 @@ export class MessageReceiver {
3534
private logger: Logger
3635
private connectionsModule: ConnectionsModule
3736
public readonly inboundTransports: InboundTransport[] = []
38-
private outOfBandService: OutOfBandService
3937

4038
public constructor(
4139
config: AgentConfig,
4240
envelopeService: EnvelopeService,
4341
transportService: TransportService,
4442
messageSender: MessageSender,
4543
connectionsModule: ConnectionsModule,
46-
outOfBandService: OutOfBandService,
4744
dispatcher: Dispatcher
4845
) {
4946
this.config = config
5047
this.envelopeService = envelopeService
5148
this.transportService = transportService
5249
this.messageSender = messageSender
5350
this.connectionsModule = connectionsModule
54-
this.outOfBandService = outOfBandService
5551
this.dispatcher = dispatcher
5652
this.logger = this.config.logger
5753
}
@@ -96,7 +92,6 @@ export class MessageReceiver {
9692
)
9793

9894
const connection = await this.findConnectionByMessageKeys(decryptedMessage)
99-
const outOfBand = (recipientKey && (await this.outOfBandService.findByRecipientKey(recipientKey))) || undefined
10095

10196
const message = await this.transformAndValidate(plaintextMessage, connection)
10297

@@ -126,7 +121,6 @@ export class MessageReceiver {
126121
// with mediators when you don't have a public endpoint yet.
127122
session.connection = connection ?? undefined
128123
messageContext.sessionId = session.id
129-
session.outOfBand = outOfBand
130124
this.transportService.saveSession(session)
131125
} else if (session) {
132126
// No need to wait for session to stay open if we're not actually going to respond to the message.

packages/core/src/agent/MessageSender.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,7 @@ export class MessageSender {
189189
}
190190
if (!session) {
191191
// Try to send to already open session
192-
session =
193-
this.transportService.findSessionByConnectionId(connection.id) ||
194-
(outOfBand && this.transportService.findSessionByOutOfBandId(outOfBand.id))
192+
session = this.transportService.findSessionByConnectionId(connection.id)
195193
}
196194

197195
if (session?.inboundMessage?.hasReturnRouting(payload.threadId)) {

packages/core/src/agent/TransportService.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { ConnectionRecord } from '../modules/connections/repository'
22
import type { DidDocument } from '../modules/dids'
3-
import type { OutOfBandRecord } from '../modules/oob/repository'
43
import type { EncryptedMessage } from '../types'
54
import type { AgentMessage } from './AgentMessage'
65
import type { EnvelopeKeys } from './EnvelopeService'
@@ -21,10 +20,6 @@ export class TransportService {
2120
return Object.values(this.transportSessionTable).find((session) => session?.connection?.id === connectionId)
2221
}
2322

24-
public findSessionByOutOfBandId(outOfBandId: string) {
25-
return Object.values(this.transportSessionTable).find((session) => session?.outOfBand?.id === outOfBandId)
26-
}
27-
2823
public hasInboundEndpoint(didDocument: DidDocument): boolean {
2924
return Boolean(didDocument.service?.find((s) => s.serviceEndpoint !== DID_COMM_TRANSPORT_QUEUE))
3025
}
@@ -48,7 +43,6 @@ export interface TransportSession {
4843
keys?: EnvelopeKeys
4944
inboundMessage?: AgentMessage
5045
connection?: ConnectionRecord
51-
outOfBand?: OutOfBandRecord
5246
send(encryptedMessage: EncryptedMessage): Promise<void>
5347
close(): Promise<void>
5448
}

packages/core/src/modules/connections/DidExchangeProtocol.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ export class DidExchangeProtocol {
8787
alias,
8888
state: DidExchangeState.InvitationReceived,
8989
theirLabel: outOfBandInvitation.label,
90-
multiUseInvitation: false,
9190
did,
9291
mediatorId,
9392
autoAcceptConnection: outOfBandRecord.autoAcceptConnection,
@@ -200,7 +199,6 @@ export class DidExchangeProtocol {
200199
protocol: HandshakeProtocol.DidExchange,
201200
role: DidExchangeRole.Responder,
202201
state: DidExchangeState.RequestReceived,
203-
multiUseInvitation: false,
204202
did,
205203
mediatorId,
206204
autoAcceptConnection: outOfBandRecord.autoAcceptConnection,
@@ -314,7 +312,7 @@ export class DidExchangeProtocol {
314312

315313
const didDocument = await this.extractDidDocument(
316314
message,
317-
outOfBandRecord.getRecipientKeys().map((key) => key.publicKeyBase58)
315+
outOfBandRecord.outOfBandInvitation.getRecipientKeys().map((key) => key.publicKeyBase58)
318316
)
319317
const didRecord = new DidRecord({
320318
id: message.did,

packages/core/src/modules/connections/__tests__/ConnectionService.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,6 @@ describe('ConnectionService', () => {
228228
id: 'test',
229229
state: DidExchangeState.InvitationSent,
230230
role: DidExchangeRole.Responder,
231-
multiUseInvitation: true,
232231
})
233232

234233
const theirDid = 'their-did'

packages/core/src/modules/connections/models/did/__tests__/diddoc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"@context": "https: //w3id.org/did/v1",
2+
"@context": "https://w3id.org/did/v1",
33
"id": "did:sov:LjgpST2rjsoxYegQDRm7EL",
44
"publicKey": [
55
{

packages/core/src/modules/connections/repository/ConnectionRecord.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export interface ConnectionRecordProps {
1919
threadId?: string
2020
tags?: CustomConnectionTags
2121
imageUrl?: string
22-
multiUseInvitation?: boolean
2322
mediatorId?: string
2423
errorMessage?: string
2524
protocol?: HandshakeProtocol
@@ -36,6 +35,7 @@ export type DefaultConnectionTags = {
3635
did: string
3736
theirDid?: string
3837
outOfBandId?: string
38+
invitationDid?: string
3939
}
4040

4141
export class ConnectionRecord
@@ -53,7 +53,6 @@ export class ConnectionRecord
5353
public alias?: string
5454
public autoAcceptConnection?: boolean
5555
public imageUrl?: string
56-
public multiUseInvitation!: boolean
5756

5857
public threadId?: string
5958
public mediatorId?: string
@@ -82,7 +81,6 @@ export class ConnectionRecord
8281
this._tags = props.tags ?? {}
8382
this.threadId = props.threadId
8483
this.imageUrl = props.imageUrl
85-
this.multiUseInvitation = props.multiUseInvitation || false
8684
this.mediatorId = props.mediatorId
8785
this.errorMessage = props.errorMessage
8886
this.protocol = props.protocol
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { DidExchangeRole, DidExchangeState } from '../../models'
2+
import { ConnectionRecord } from '../ConnectionRecord'
3+
4+
describe('ConnectionRecord', () => {
5+
describe('getTags', () => {
6+
it('should return default tags', () => {
7+
const connectionRecord = new ConnectionRecord({
8+
state: DidExchangeState.Completed,
9+
role: DidExchangeRole.Requester,
10+
threadId: 'a-thread-id',
11+
mediatorId: 'a-mediator-id',
12+
did: 'a-did',
13+
theirDid: 'a-their-did',
14+
outOfBandId: 'a-out-of-band-id',
15+
invitationDid: 'a-invitation-did',
16+
})
17+
18+
expect(connectionRecord.getTags()).toEqual({
19+
state: DidExchangeState.Completed,
20+
role: DidExchangeRole.Requester,
21+
threadId: 'a-thread-id',
22+
mediatorId: 'a-mediator-id',
23+
did: 'a-did',
24+
theirDid: 'a-their-did',
25+
outOfBandId: 'a-out-of-band-id',
26+
invitationDid: 'a-invitation-did',
27+
})
28+
})
29+
})
30+
})

packages/core/src/modules/connections/services/ConnectionService.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { AgentMessage } from '../../../agent/AgentMessage'
22
import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext'
33
import type { Logger } from '../../../logger'
44
import type { AckMessage } from '../../common'
5-
import type { DidDocument } from '../../dids'
65
import type { OutOfBandDidCommService } from '../../oob/domain/OutOfBandDidCommService'
76
import type { OutOfBandRecord } from '../../oob/repository'
87
import type { ConnectionStateChangedEvent } from '../ConnectionEvents'
@@ -26,6 +25,7 @@ import { DidDocumentRole } from '../../dids/domain/DidDocumentRole'
2625
import { didKeyToVerkey } from '../../dids/helpers'
2726
import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1'
2827
import { DidRepository, DidRecord } from '../../dids/repository'
28+
import { DidRecordMetadataKeys } from '../../dids/repository/didRecordMetadataTypes'
2929
import { OutOfBandRole } from '../../oob/domain/OutOfBandRole'
3030
import { OutOfBandState } from '../../oob/domain/OutOfBandState'
3131
import { ConnectionEventTypes } from '../ConnectionEvents'
@@ -112,14 +112,13 @@ export class ConnectionService {
112112
did,
113113
mediatorId,
114114
autoAcceptConnection: config?.autoAcceptConnection,
115-
multiUseInvitation: false,
116115
outOfBandId: outOfBandRecord.id,
117116
invitationDid,
118117
})
119118

120119
const { did: peerDid } = await this.createDid({
121120
role: DidDocumentRole.Created,
122-
didDocument: convertToNewDidDocument(didDoc),
121+
didDoc,
123122
})
124123

125124
const { label, imageUrl, autoAcceptConnection } = config
@@ -172,15 +171,14 @@ export class ConnectionService {
172171
protocol: HandshakeProtocol.Connections,
173172
role: DidExchangeRole.Responder,
174173
state: DidExchangeState.RequestReceived,
175-
multiUseInvitation: false,
176174
did,
177175
mediatorId,
178176
autoAcceptConnection: outOfBandRecord.autoAcceptConnection,
179177
})
180178

181179
const { did: peerDid } = await this.createDid({
182180
role: DidDocumentRole.Received,
183-
didDocument: convertToNewDidDocument(message.connection.didDoc),
181+
didDoc: message.connection.didDoc,
184182
})
185183

186184
connectionRecord.theirDid = peerDid
@@ -234,7 +232,7 @@ export class ConnectionService {
234232

235233
const { did: peerDid } = await this.createDid({
236234
role: DidDocumentRole.Created,
237-
didDocument: convertToNewDidDocument(didDoc),
235+
didDoc,
238236
})
239237

240238
const connection = new Connection({
@@ -333,7 +331,7 @@ export class ConnectionService {
333331

334332
const { did: peerDid } = await this.createDid({
335333
role: DidDocumentRole.Received,
336-
didDocument: convertToNewDidDocument(connection.didDoc),
334+
didDoc: connection.didDoc,
337335
})
338336

339337
connectionRecord.theirDid = peerDid
@@ -619,7 +617,6 @@ export class ConnectionService {
619617
mediatorId?: string
620618
theirLabel?: string
621619
autoAcceptConnection?: boolean
622-
multiUseInvitation: boolean
623620
tags?: CustomConnectionTags
624621
imageUrl?: string
625622
protocol?: HandshakeProtocol
@@ -635,7 +632,6 @@ export class ConnectionService {
635632
theirLabel: options.theirLabel,
636633
autoAcceptConnection: options.autoAcceptConnection,
637634
imageUrl: options.imageUrl,
638-
multiUseInvitation: options.multiUseInvitation,
639635
mediatorId: options.mediatorId,
640636
protocol: options.protocol,
641637
outOfBandId: options.outOfBandId,
@@ -645,7 +641,10 @@ export class ConnectionService {
645641
return connectionRecord
646642
}
647643

648-
private async createDid({ role, didDocument }: { role: DidDocumentRole; didDocument: DidDocument }) {
644+
private async createDid({ role, didDoc }: { role: DidDocumentRole; didDoc: DidDoc }) {
645+
// Convert the legacy did doc to a new did document
646+
const didDocument = convertToNewDidDocument(didDoc)
647+
649648
const peerDid = didDocumentJsonToNumAlgo1Did(didDocument.toJSON())
650649
didDocument.id = peerDid
651650
const didRecord = new DidRecord({
@@ -659,6 +658,13 @@ export class ConnectionService {
659658
},
660659
})
661660

661+
// Store the unqualified did with the legacy did document in the metadata
662+
// Can be removed at a later stage if we know for sure we don't need it anymore
663+
didRecord.metadata.set(DidRecordMetadataKeys.LegacyDid, {
664+
unqualifiedDid: didDoc.id,
665+
didDocumentString: JsonTransformer.serialize(didDoc),
666+
})
667+
662668
this.logger.debug('Saving DID record', {
663669
id: didRecord.id,
664670
role: didRecord.role,

0 commit comments

Comments
 (0)