Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support new didcomm v2 service type #1902

Merged
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
3 changes: 3 additions & 0 deletions packages/core/src/modules/dids/domain/DidDocumentBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export class DidDocumentBuilder {
if (typeof this.didDocument.context === 'string') {
this.didDocument.context = [this.didDocument.context, context]
} else {
// If already included, no need to add again
if (this.didDocument.context.includes(context)) return this

this.didDocument.context.push(context)
}

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. In ')
}
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved

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
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved
),
},
},
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
Loading