@veramo/did-comm
Version:
Veramo messaging plugin implementing DIDComm v2.
582 lines (538 loc) • 18.9 kB
text/typescript
import { IAgentContext, IDIDManager, IKeyManager } from '@veramo/core-types'
import { IMediationManager, MediationResponse, RecipientDid, RequesterDid } from '@veramo/mediation-manager'
import { AbstractMessageHandler, Message } from '@veramo/message-handler'
import Debug from 'debug'
import { v4 } from 'uuid'
import { IDIDComm } from '../types/IDIDComm.js'
import { DIDCommMessageMediaType, IDIDCommMessage } from '../types/message-types.js'
import { asArray } from '@veramo/utils'
const debug = Debug('veramo:did-comm:coordinate-mediation-message-handler')
const GRANTED = 'GRANTED'
const DENIED = 'DENIED'
type Context = IAgentContext<IDIDManager & IKeyManager & IDIDComm & IMediationManager>
/**
* @beta This API may change without a BREAKING CHANGE notice.
*
* Represents the actions (add or remove) that can be taken on a recipient did
*
* @see {@link @veramo/did-comm#CoordinateMediationV3MediatorMessageHandler}
*/
export enum UpdateAction {
ADD = 'add',
REMOVE = 'remove',
}
/**
* @beta This API may change without a BREAKING CHANGE notice.
*
* Represents the result of an update action
*
* @see {@link @veramo/did-comm#CoordinateMediationV3MediatorMessageHandler}
*/
export enum RecipientUpdateResult {
SUCCESS = 'success',
NO_CHANGE = 'no_change',
CLIENT_ERROR = 'client_error',
SERVER_ERROR = 'server_error',
}
/**
* @beta This API may change without a BREAKING CHANGE notice.
*
* Parameter options for the CoordinateMediationV3MediatorMessageHandler
* {@link @veramo/did-comm#CoordinateMediationV3MediatorMessageHandler}
*/
export interface CoordinateMediationV3MediatorMessageHandlerOptions {
isMediateDefaultGrantAll: boolean
}
/**
* @beta This API may change without a BREAKING CHANGE notice.
*
* Represents the structure of a specific update on RECIPIENT_UPDATE
*/
export interface Update {
recipient_did: RecipientDid
action: UpdateAction
}
/**
* @beta This API may change without a BREAKING CHANGE notice.
*
* Represents an update response on RECIPIENT_UPDATE_RESPONSE
*/
export interface UpdateResult extends Update {
result: RecipientUpdateResult
}
interface Query {
limit: number
offset: number
}
interface MediateRequestMessage extends Message {
to: string
from: RequesterDid
type: CoordinateMediation.MEDIATE_REQUEST
}
interface RecipientUpdateMessage extends Message {
to: string
from: RequesterDid
type: CoordinateMediation.RECIPIENT_UPDATE
body: { updates: Update[] }
return_route: 'all'
}
interface RecipientQueryMessage extends Message {
to: string
from: RequesterDid
type: CoordinateMediation.RECIPIENT_QUERY
body: { paginate?: Query }
}
/**
* @beta This API may change without a BREAKING CHANGE notice.
*
* Represents the types of messages that can be sent and received by the Mediator Coordinator protocol
*
* @see {@link @veramo/did-comm#CoordinateMediationV3MediatorMessageHandler}
* @see {@link @veramo/did-comm#CoordinateMediationRecipientMessageHandler}
*/
export enum CoordinateMediation {
MEDIATE_REQUEST = 'https://didcomm.org/coordinate-mediation/3.0/mediate-request',
MEDIATE_GRANT = 'https://didcomm.org/coordinate-mediation/3.0/mediate-grant',
MEDIATE_DENY = 'https://didcomm.org/coordinate-mediation/3.0/mediate-deny',
RECIPIENT_UPDATE = 'https://didcomm.org/coordinate-mediation/3.0/recipient-update',
RECIPIENT_UPDATE_RESPONSE = 'https://didcomm.org/coordinate-mediation/3.0/recipient-update-response',
RECIPIENT_QUERY = 'https://didcomm.org/coordinate-mediation/3.0/recipient-query',
RECIPIENT_QUERY_RESPONSE = 'https://didcomm.org/coordinate-mediation/3.0/recipient',
}
/**
* @beta This API may change without a BREAKING CHANGE notice.
*/
export enum MessagePickup {
STATUS_REQUEST_MESSAGE_TYPE = 'https://didcomm.org/messagepickup/3.0/status-request',
DELIVERY_REQUEST_MESSAGE_TYPE = 'https://didcomm.org/messagepickup/3.0/delivery-request',
}
/**
* @beta This API may change without a BREAKING CHANGE notice.
*/
export function createV3MediateGrantMessage(
recipientDidUrl: string,
mediatorDidUrl: string,
thid: string,
): IDIDCommMessage {
return {
type: CoordinateMediation.MEDIATE_GRANT,
from: mediatorDidUrl,
to: [recipientDidUrl],
id: v4(),
thid: thid,
body: { routing_did: [mediatorDidUrl] },
created_time: new Date().toISOString(),
}
}
/**
* @beta This API may change without a BREAKING CHANGE notice.
*/
export const createV3MediateDenyMessage = (
recipientDidUrl: string,
mediatorDidUrl: string,
thid: string,
): IDIDCommMessage => {
return {
type: CoordinateMediation.MEDIATE_DENY,
from: mediatorDidUrl,
to: [recipientDidUrl],
id: v4(),
thid: thid,
created_time: new Date().toISOString(),
body: null,
}
}
/**
* @beta This API may change without a BREAKING CHANGE notice.
* @see {@link @veramo/did-comm#CoordinateMediationV3MediatorMessageHandler}
*/
export function createV3RecipientUpdateResponseMessage(
recipientDidUrl: string,
mediatorDidUrl: string,
thid: string,
updates: UpdateResult[],
): IDIDCommMessage {
return {
type: CoordinateMediation.RECIPIENT_UPDATE_RESPONSE,
from: mediatorDidUrl,
to: [recipientDidUrl],
id: v4(),
thid: thid,
body: { updates },
created_time: new Date().toISOString(),
}
}
/**
* @beta This API may change without a BREAKING CHANGE notice.
* @see {@link @veramo/did-comm#CoordinateMediationV3MediatorMessageHandler}
*/
export const createV3RecipientQueryResponseMessage = (
recipientDidUrl: string,
mediatorDidUrl: string,
thid: string,
dids: Record<'recipient_did', RecipientDid>[],
): IDIDCommMessage => {
return {
type: CoordinateMediation.RECIPIENT_QUERY_RESPONSE,
from: mediatorDidUrl,
to: [recipientDidUrl],
id: v4(),
thid: thid,
body: { dids },
created_time: new Date().toISOString(),
}
}
/**
* @beta This API may change without a BREAKING CHANGE notice.
*
* @returns a structured message for the Mediator Coordinator protocol
* @see {@link @veramo/did-comm#CoordinateMediationV3MediatorMessageHandler}
*/
export function createV3MediateRequestMessage(
recipientDidUrl: string,
mediatorDidUrl: string,
): IDIDCommMessage {
return {
type: CoordinateMediation.MEDIATE_REQUEST,
from: recipientDidUrl,
to: [mediatorDidUrl],
id: v4(),
created_time: new Date().toISOString(),
body: {},
}
}
/**
* @beta This API may change without a BREAKING CHANGE notice.
*/
export const createV3StatusRequestMessage = (
recipientDidUrl: string,
mediatorDidUrl: string,
): IDIDCommMessage => {
return {
id: v4(),
type: MessagePickup.STATUS_REQUEST_MESSAGE_TYPE,
to: [mediatorDidUrl],
from: recipientDidUrl,
return_route: 'all',
body: {},
}
}
/**
* @beta This API may change without a BREAKING CHANGE notice.
*
* @returns a structured upate message for the Mediator Coordinator protocol
* @see {@link @veramo/did-comm#CoordinateMediationV3MediatorMessageHandler}
*/
export const createV3RecipientUpdateMessage = (
recipientDidUrl: string,
mediatorDidUrl: string,
updates: Update[],
): IDIDCommMessage => {
return {
type: CoordinateMediation.RECIPIENT_UPDATE,
from: recipientDidUrl,
to: [mediatorDidUrl],
id: v4(),
created_time: new Date().toISOString(),
body: { updates },
return_route: 'all',
}
}
/**
* @beta This API may change without a BREAKING CHANGE notice.
*
* @returns a structured query message for the Mediator Coordinator protocol
* @see {@link @veramo/did-comm#CoordinateMediationV3MediatorMessageHandler}
*/
export const createV3RecipientQueryMessage = (
recipientDidUrl: string,
mediatorDidUrl: string,
): IDIDCommMessage => {
return {
type: CoordinateMediation.RECIPIENT_QUERY,
from: recipientDidUrl,
to: [mediatorDidUrl],
id: v4(),
created_time: new Date().toISOString(),
body: {},
}
}
/**
* @beta This API may change without a BREAKING CHANGE notice.
*/
export const createV3DeliveryRequestMessage = (
recipientDidUrl: string,
mediatorDidUrl: string,
): IDIDCommMessage => {
return {
id: v4(),
type: MessagePickup.DELIVERY_REQUEST_MESSAGE_TYPE,
to: [mediatorDidUrl],
from: recipientDidUrl,
return_route: 'all',
body: { limit: 2 },
}
}
/**
* Handler Type Guards
*/
const isMediateRequest = (message: Message): message is MediateRequestMessage => {
if (message.type !== CoordinateMediation.MEDIATE_REQUEST) return false
if (!message.from) throw new Error('invalid_argument: MediateRequest received without `from` set')
if (!message.to) throw new Error('invalid_argument: MediateRequest received without `to` set')
return true
}
const isRecipientUpdate = (message: Message): message is RecipientUpdateMessage => {
if (message.type !== CoordinateMediation.RECIPIENT_UPDATE) return false
if (!message.from) throw new Error('invalid_argument: RecipientUpdate received without `from` set')
if (!message.to) throw new Error('invalid_argument: RecipientUpdate received without `to` set')
if (!('data' in message)) throw new Error('invalid_argument: RecipientUpdate received without `body` set')
if (!message.data || !message.data.updates) {
throw new Error('invalid_argument: RecipientUpdate received without `updates` set')
}
return true
}
const isRecipientQuery = (message: Message): message is RecipientQueryMessage => {
if (message.type !== CoordinateMediation.RECIPIENT_QUERY) return false
if (!message.from) throw new Error('invalid_argument: RecipientQuery received without `from` set')
if (!message.to) throw new Error('invalid_argument: RecipientQuery received without `to` set')
return true
}
/**
* A plugin for the {@link @veramo/message-handler#MessageHandler} that handles Mediator Coordinator messages for the
* mediator role.
* @beta This API may change without a BREAKING CHANGE notice.
*/
export class CoordinateMediationV3MediatorMessageHandler extends AbstractMessageHandler {
constructor() {
super()
}
private async grantOrDenyMediation(
{ from: requesterDid }: Message,
context: Context,
): Promise<MediationResponse> {
if (!requesterDid) return DENIED
const policy = await context.agent.mediationManagerGetMediationPolicy({ requesterDid })
if (await context.agent.isMediateDefaultGrantAll()) {
return policy === 'DENY' ? DENIED : GRANTED
} else {
return policy === 'ALLOW' ? GRANTED : DENIED
}
}
private async handleMediateRequest(message: MediateRequestMessage, context: Context): Promise<Message> {
try {
debug('MediateRequest Message Received')
const requesterDid = message.from
const status = await this.grantOrDenyMediation(message, context)
await context.agent.mediationManagerSaveMediation({ status, requesterDid })
const getResponse = status === GRANTED ? createV3MediateGrantMessage : createV3MediateDenyMessage
const response = getResponse(message.from, message.to, message.id)
const packedResponse = await context.agent.packDIDCommMessage({
message: response,
packing: 'authcrypt',
})
const returnResponse = {
id: response.id,
message: packedResponse.message,
contentType: DIDCommMessageMediaType.ENCRYPTED,
}
message.addMetaData({ type: 'ReturnRouteResponse', value: JSON.stringify(returnResponse) })
// Save message to track recipients
await context.agent.dataStoreSaveMessage({
message: {
type: response.type,
from: response.from,
to: asArray(response.to)[0],
id: response.id,
threadId: response.thid,
data: response.body,
createdAt: response.created_time,
},
})
} catch (error) {
debug(error)
}
return message
}
/**
* Used to notify the mediator of DIDs in use by the recipient
**/
private async handleRecipientUpdate(message: RecipientUpdateMessage, context: Context): Promise<Message> {
try {
debug('MediateRecipientUpdate Message Received')
const updates: Update[] = message.data.updates
const applyUpdate = async (requesterDid: RequesterDid, update: Update) => {
const { recipient_did: recipientDid } = update
try {
if (update.action === UpdateAction.ADD) {
await context.agent.mediationManagerAddRecipientDid({ requesterDid, recipientDid })
return { ...update, result: RecipientUpdateResult.SUCCESS }
} else if (update.action === UpdateAction.REMOVE) {
const result = await context.agent.mediationManagerRemoveRecipientDid({ recipientDid })
if (result) return { ...update, result: RecipientUpdateResult.SUCCESS }
return { ...update, result: RecipientUpdateResult.NO_CHANGE }
}
return { ...update, result: RecipientUpdateResult.CLIENT_ERROR }
} catch (ex) {
debug(ex)
return { ...update, result: RecipientUpdateResult.SERVER_ERROR }
}
}
const updated = await Promise.all(updates.map(async (u) => await applyUpdate(message.from, u)))
const response = createV3RecipientUpdateResponseMessage(message.from, message.to, message.id, updated)
const packedResponse = await context.agent.packDIDCommMessage({
message: response,
packing: 'authcrypt',
})
const returnResponse = {
id: response.id,
message: packedResponse.message,
contentType: DIDCommMessageMediaType.ENCRYPTED,
}
message.addMetaData({ type: 'ReturnRouteResponse', value: JSON.stringify(returnResponse) })
await context.agent.dataStoreSaveMessage({
message: {
type: response.type,
from: response.from,
to: asArray(response.to)[0],
id: response.id,
threadId: response.thid,
data: response.body,
createdAt: response.created_time,
},
})
} catch (error) {
debug(error)
}
return message
}
/**
* Query mediator for a list of DIDs registered for this connection
**/
private async handleRecipientQuery(message: RecipientQueryMessage, context: Context): Promise<Message> {
try {
const dids = await context.agent.mediationManagerListRecipientDids({ requesterDid: message.from })
const response = createV3RecipientQueryResponseMessage(
message.from,
message.to,
message.id,
dids.map((did) => ({ recipient_did: did })),
)
const packedResponse = await context.agent.packDIDCommMessage({
message: response,
packing: 'authcrypt',
})
const returnResponse = {
id: response.id,
message: packedResponse.message,
contentType: DIDCommMessageMediaType.ENCRYPTED,
}
message.addMetaData({ type: 'ReturnRouteResponse', value: JSON.stringify(returnResponse) })
await context.agent.dataStoreSaveMessage({
message: {
type: response.type,
from: response.from,
to: asArray(response.to)[0],
id: response.id,
threadId: response.thid,
data: response.body,
createdAt: response.created_time,
},
})
} catch (error) {
debug(error)
}
return message
}
/**
* Handles a Mediator Coordinator messages for the mediator role
* https://didcomm.org/mediator-coordination/3.0/
*/
public async handle(message: Message, context: Context): Promise<Message> {
if (isMediateRequest(message)) return this.handleMediateRequest(message, context)
if (isRecipientUpdate(message)) return this.handleRecipientUpdate(message, context)
if (isRecipientQuery(message)) return this.handleRecipientQuery(message, context)
return super.handle(message, context)
}
}
/**
* A plugin for the {@link @veramo/message-handler#MessageHandler} that handles Mediator Coordinator messages for the
* recipient role.
* @beta This API may change without a BREAKING CHANGE notice.
*/
export class CoordinateMediationV3RecipientMessageHandler extends AbstractMessageHandler {
constructor() {
super()
}
/**
* Handles a Mediator Coordinator messages for the recipient role
* https://didcomm.org/mediator-coordination/2.0/
*/
public async handle(message: Message, context: Context): Promise<Message> {
if (message.type === CoordinateMediation.MEDIATE_GRANT) {
debug('MediateGrant Message Received')
try {
const { from, to, data, threadId } = message
if (!from) {
throw new Error('invalid_argument: MediateGrant received without `from` set')
}
if (!to) {
throw new Error('invalid_argument: MediateGrant received without `to` set')
}
if (!threadId) {
throw new Error('invalid_argument: MediateGrant received without `thid` set')
}
if (!data.routing_did || data.routing_did.length === 0) {
throw new Error('invalid_argument: MediateGrant received with invalid routing_did')
}
// If mediate request was previously sent, add service to DID document
const prevRequestMsg = await context.agent.dataStoreGetMessage({ id: threadId })
if (prevRequestMsg.from === to && prevRequestMsg.to === from) {
const service = {
id: 'didcomm-mediator',
type: 'DIDCommMessaging',
serviceEndpoint: [
{
uri: data.routing_did[0],
},
],
}
await context.agent.didManagerAddService({
did: to,
service: service,
})
message.addMetaData({ type: 'DIDCommMessagingServiceAdded', value: JSON.stringify(service) })
}
} catch (ex) {
debug(ex)
}
return message
} else if (message.type === CoordinateMediation.MEDIATE_DENY) {
debug('MediateDeny Message Received')
try {
const { from, to } = message
if (!from) {
throw new Error('invalid_argument: MediateGrant received without `from` set')
}
if (!to) {
throw new Error('invalid_argument: MediateGrant received without `to` set')
}
// Delete service if it exists
const did = await context.agent.didManagerGet({
did: to,
})
const existingService = did.services.find(
(s) =>
s.serviceEndpoint === from ||
(Array.isArray(s.serviceEndpoint) && s.serviceEndpoint.includes(from)),
)
if (existingService) {
await context.agent.didManagerRemoveService({ did: to, id: existingService.id })
}
} catch (ex) {
debug(ex)
}
}
return super.handle(message, context)
}
}