Skip to content

Commit

Permalink
feat: support new didcomm v2 service type (#1902)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra authored Jun 24, 2024
1 parent eef0660 commit d548fa4
Show file tree
Hide file tree
Showing 29 changed files with 571 additions and 138 deletions.
7 changes: 7 additions & 0 deletions .changeset/sharp-masks-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@credo-ts/indy-vdr': patch
'@credo-ts/cheqd': patch
'@credo-ts/core': patch
---

feat: support new 'DIDCommMessaging' didcomm v2 service type (in addition to older 'DIDComm' service type)
2 changes: 1 addition & 1 deletion packages/cheqd/tests/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function validVerificationMethod(did: string) {
export function validService(did: string) {
return new DidDocumentService({
id: did + '#service-1',
type: 'DIDCommMessaging',
type: 'CustomType',
serviceEndpoint: 'https://rand.io',
})
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/agent/TransportService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class TransportService {
}

public hasInboundEndpoint(didDocument: DidDocument): boolean {
return Boolean(didDocument.service?.find((s) => s.serviceEndpoint !== DID_COMM_TRANSPORT_QUEUE))
return Boolean(didDocument.didCommServices?.find((s) => s.serviceEndpoint !== DID_COMM_TRANSPORT_QUEUE))
}

public findSessionById(sessionId: string) {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/agent/__tests__/MessageSender.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import type { ConnectionRecord } from '../../modules/connections'
import type { ResolvedDidCommService } from '../../modules/didcomm'
import type { DidDocumentService } from '../../modules/dids'
import type { DidDocumentService, IndyAgentService } from '../../modules/dids'
import type { MessagePickupRepository } from '../../modules/message-pickup/storage'
import type { OutboundTransport } from '../../transport'
import type { EncryptedMessage } from '../../types'
Expand Down Expand Up @@ -693,7 +693,7 @@ function getMockDidDocument({ service }: { service: DidDocumentService[] }) {
})
}

function getMockResolvedDidService(service: DidDocumentService): ResolvedDidCommService {
function getMockResolvedDidService(service: DidCommV1Service | IndyAgentService): ResolvedDidCommService {
return {
id: service.id,
serviceEndpoint: service.serviceEndpoint,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"@context": ["https://w3id.org/did/v1"],
"id": "did:example:123",
"service": [
{
"id": "did:example:123#service-1",
"type": "DIDCommMessaging",
"serviceEndpoint": {
"uri": "did:sov:Q4zqM7aXqm7gDQkUVLng9h",
"routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"]
}
},
{
"id": "did:example:123#service-2",
"type": "DIDComm",
"serviceEndpoint": "https://agent.com/did-comm",
"routingKeys": ["DADEajsDSaksLng9h"]
},
{
"id": "did:example:123#service-3",
"type": "DIDCommMessaging",
"serviceEndpoint": [
{
"uri": "did:sov:Q4zqM7aXqm7gDQkUVLng9h",
"routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"]
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"service": [
{
"id": "did:example:123#service-1",
"type": "Mediator"
"type": "Mediator",
"serviceEndpoint": "uri:uri"
},
{
"id": "did:example:123#service-2",
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/modules/dids/domain/DidDocumentBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { DidDocumentService } from './service'

import { asArray } from '../../../utils'

import { DidDocument } from './DidDocument'
import { VerificationMethod } from './verificationMethod'

Expand All @@ -13,12 +15,10 @@ export class DidDocumentBuilder {
}

public addContext(context: string) {
if (typeof this.didDocument.context === 'string') {
this.didDocument.context = [this.didDocument.context, context]
} else {
this.didDocument.context.push(context)
}
const currentContexts = asArray(this.didDocument.context)
if (currentContexts.includes(context)) return this

this.didDocument.context = [...currentContexts, context]
return this
}

Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/modules/dids/domain/service/DidCommV1Service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ArrayNotEmpty, IsOptional, IsString } from 'class-validator'

import { IsUri } from '../../../../utils'
import { getProtocolScheme } from '../../../../utils/uri'

import { DidDocumentService } from './DidDocumentService'

export class DidCommV1Service extends DidDocumentService {
Expand All @@ -23,6 +26,14 @@ export class DidCommV1Service extends DidDocumentService {

public static type = 'did-communication'

public get protocolScheme(): string {
return getProtocolScheme(this.serviceEndpoint)
}

@IsString()
@IsUri()
public serviceEndpoint!: string

@ArrayNotEmpty()
@IsString({ each: true })
public recipientKeys!: string[]
Expand Down
33 changes: 31 additions & 2 deletions packages/core/src/modules/dids/domain/service/DidCommV2Service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
import { IsOptional, IsString } from 'class-validator'

import { IsUri } from '../../../../utils'

import { DidDocumentService } from './DidDocumentService'
import { NewDidCommV2Service, NewDidCommV2ServiceEndpoint } from './NewDidCommV2Service'

export interface DidCommV2ServiceOptions {
id: string
serviceEndpoint: string
routingKeys?: string[]
accept?: string[]
}

/**
* @deprecated use `NewDidCommV2Service` instead. Will be renamed to `LegacyDidCommV2Service` in 0.6
*/
export class DidCommV2Service extends DidDocumentService {
public constructor(options: { id: string; serviceEndpoint: string; routingKeys?: string[]; accept?: string[] }) {
public constructor(options: DidCommV2ServiceOptions) {
super({ ...options, type: DidCommV2Service.type })

if (options) {
this.routingKeys = options.routingKeys
this.serviceEndpoint = options.serviceEndpoint
this.accept = options.accept
this.routingKeys = options.routingKeys
}
}

Expand All @@ -21,4 +35,19 @@ export class DidCommV2Service extends DidDocumentService {
@IsString({ each: true })
@IsOptional()
public accept?: string[]

@IsUri()
@IsString()
public serviceEndpoint!: string

public toNewDidCommV2(): NewDidCommV2Service {
return new NewDidCommV2Service({
id: this.id,
serviceEndpoint: new NewDidCommV2ServiceEndpoint({
uri: this.serviceEndpoint,
accept: this.accept,
routingKeys: this.routingKeys,
}),
})
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,63 @@
import { IsString } from 'class-validator'
import type { ValidationOptions } from 'class-validator'

import { buildMessage, isString, IsString, ValidateBy } from 'class-validator'

import { CredoError } from '../../../../error'
import { isJsonObject, SingleOrArray } from '../../../../utils'
import { getProtocolScheme } from '../../../../utils/uri'

type ServiceEndpointType = SingleOrArray<string | Record<string, unknown>>

export class DidDocumentService {
public constructor(options: { id: string; serviceEndpoint: string; type: string }) {
public constructor(options: { id: string; serviceEndpoint: ServiceEndpointType; type: string }) {
if (options) {
this.id = options.id
this.serviceEndpoint = options.serviceEndpoint
this.type = options.type
}
}

/**
* @deprecated will be removed in 0.6, as it's not possible from the base did document service class to determine
* the protocol scheme. It needs to be implemented on a specific did document service class.
*/
public get protocolScheme(): string {
if (typeof this.serviceEndpoint !== 'string') {
throw new CredoError('Unable to extract protocol scheme from serviceEndpoint as it is not a string.')
}

return getProtocolScheme(this.serviceEndpoint)
}

@IsString()
public id!: string

@IsString()
public serviceEndpoint!: string
@IsStringOrJsonObjectSingleOrArray()
public serviceEndpoint!: SingleOrArray<string | Record<string, unknown>>

@IsString()
public type!: string
}

/**
* Checks if a given value is a string, a json object, or an array of strings and json objects
*/
function IsStringOrJsonObjectSingleOrArray(validationOptions?: Omit<ValidationOptions, 'each'>): PropertyDecorator {
return ValidateBy(
{
name: 'isStringOrJsonObjectSingleOrArray',
validator: {
validate: (value): boolean =>
isString(value) ||
isJsonObject(value) ||
(Array.isArray(value) && value.every((v) => isString(v) || isJsonObject(v))),
defaultMessage: buildMessage(
(eachPrefix) =>
eachPrefix + '$property must be a string, JSON object, or an array consisting of strings and JSON objects',
validationOptions
),
},
},
validationOptions
)
}
11 changes: 11 additions & 0 deletions packages/core/src/modules/dids/domain/service/IndyAgentService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ArrayNotEmpty, IsOptional, IsString } from 'class-validator'

import { IsUri } from '../../../../utils'
import { getProtocolScheme } from '../../../../utils/uri'

import { DidDocumentService } from './DidDocumentService'

export class IndyAgentService extends DidDocumentService {
Expand All @@ -21,6 +24,14 @@ export class IndyAgentService extends DidDocumentService {

public static type = 'IndyAgent'

public get protocolScheme(): string {
return getProtocolScheme(this.serviceEndpoint)
}

@IsString()
@IsUri()
public serviceEndpoint!: string

@ArrayNotEmpty()
@IsString({ each: true })
public recipientKeys!: string[]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Type } from 'class-transformer'
import { IsOptional, IsString, ValidateNested } from 'class-validator'

import { CredoError } from '../../../../error'
import { SingleOrArray, IsInstanceOrArrayOfInstances, IsUri } from '../../../../utils'

import { DidDocumentService } from './DidDocumentService'

export interface NewDidCommV2ServiceEndpointOptions {
uri: string
routingKeys?: string[]
accept?: string[]
}

export class NewDidCommV2ServiceEndpoint {
public constructor(options: NewDidCommV2ServiceEndpointOptions) {
if (options) {
this.uri = options.uri
this.routingKeys = options.routingKeys
this.accept = options.accept
}
}

@IsString()
@IsUri()
public uri!: string

@IsString({ each: true })
@IsOptional()
public routingKeys?: string[]

@IsString({ each: true })
@IsOptional()
public accept?: string[];

[key: string]: unknown | undefined
}

export interface DidCommV2ServiceOptions {
id: string
serviceEndpoint: SingleOrArray<NewDidCommV2ServiceEndpoint>
}

/**
* Will be renamed to `DidCommV2Service` in 0.6 (and replace the current `DidCommV2Service`)
*/
export class NewDidCommV2Service extends DidDocumentService {
public constructor(options: DidCommV2ServiceOptions) {
super({ ...options, type: NewDidCommV2Service.type })

if (options) {
this.serviceEndpoint = options.serviceEndpoint
}
}

public static type = 'DIDCommMessaging'

@IsInstanceOrArrayOfInstances({ classType: [NewDidCommV2ServiceEndpoint] })
@ValidateNested()
@Type(() => NewDidCommV2ServiceEndpoint)
public serviceEndpoint!: SingleOrArray<NewDidCommV2ServiceEndpoint>

public get firstServiceEndpointUri(): string {
if (Array.isArray(this.serviceEndpoint)) {
if (this.serviceEndpoint.length === 0) {
throw new CredoError('No entries in serviceEndpoint array')
}

return this.serviceEndpoint[0].uri
}

return this.serviceEndpoint.uri
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import type { ClassConstructor } from 'class-transformer'

import { Transform, plainToInstance } from 'class-transformer'
import { Transform } from 'class-transformer'

import { JsonTransformer } from '../../../../utils'

import { DidCommV1Service } from './DidCommV1Service'
import { DidCommV2Service } from './DidCommV2Service'
import { DidDocumentService } from './DidDocumentService'
import { IndyAgentService } from './IndyAgentService'
import { NewDidCommV2Service } from './NewDidCommV2Service'

export const serviceTypes: { [key: string]: unknown | undefined } = {
[IndyAgentService.type]: IndyAgentService,
[DidCommV1Service.type]: DidCommV1Service,
[NewDidCommV2Service.type]: NewDidCommV2Service,
[DidCommV2Service.type]: DidCommV2Service,
}

Expand All @@ -26,9 +30,20 @@ export function ServiceTransformer() {
return Transform(
({ value }: { value?: Array<{ type: string }> }) => {
return value?.map((serviceJson) => {
const serviceClass = (serviceTypes[serviceJson.type] ??
let serviceClass = (serviceTypes[serviceJson.type] ??
DidDocumentService) as ClassConstructor<DidDocumentService>
const service = plainToInstance<DidDocumentService, unknown>(serviceClass, serviceJson)

// NOTE: deal with `DIDCommMessaging` type but using `serviceEndpoint` string value, parse it using the
// legacy class type
if (
serviceJson.type === NewDidCommV2Service.type &&
'serviceEndpoint' in serviceJson &&
typeof serviceJson.serviceEndpoint === 'string'
) {
serviceClass = DidCommV2Service
}

const service = JsonTransformer.fromJSON(serviceJson, serviceClass)

return service
})
Expand Down
Loading

0 comments on commit d548fa4

Please sign in to comment.