@veramo/did-comm
Version:
Veramo messaging plugin implementing DIDComm v2.
287 lines (261 loc) • 9.62 kB
text/typescript
import {
IAgentContext,
IDIDManager,
IKeyManager,
IDataStore,
IDataStoreORM,
IMessageHandler,
Where,
TMessageColumns,
} from '@veramo/core-types'
import { AbstractMessageHandler, Message } from '@veramo/message-handler'
import Debug from 'debug'
import { v4 } from 'uuid'
import { IDIDComm } from '../types/IDIDComm.js'
import { QUEUE_MESSAGE_TYPE } from './routing-message-handler.js'
import { IDIDCommMessage, DIDCommMessageMediaType, IDIDCommMessageAttachment } from '../types/message-types.js'
const debug = Debug('veramo:did-comm:messagepickup-message-handler')
type IContext = IAgentContext<
IDIDManager & IKeyManager & IDIDComm & IDataStore & IDataStoreORM & IMessageHandler
>
export const STATUS_REQUEST_MESSAGE_TYPE = 'https://didcomm.org/messagepickup/3.0/status-request'
export const STATUS_MESSAGE_TYPE = 'https://didcomm.org/messagepickup/3.0/status'
export const DELIVERY_REQUEST_MESSAGE_TYPE = 'https://didcomm.org/messagepickup/3.0/delivery-request'
export const DELIVERY_MESSAGE_TYPE = 'https://didcomm.org/messagepickup/3.0/delivery'
export const MESSAGES_RECEIVED_MESSAGE_TYPE = 'https://didcomm.org/messagepickup/3.0/messages-received'
function generateGetMessagesWhereQuery(from: string, recipientKey?: string): Where<TMessageColumns>[] {
return [
{
column: 'type',
value: [QUEUE_MESSAGE_TYPE],
op: 'In',
},
recipientKey
? {
column: 'to',
value: [recipientKey],
op: 'In',
}
: {
column: 'to',
value: [`${from}%`],
op: 'Like',
},
]
}
/**
* A plugin for the {@link @veramo/message-handler#MessageHandler} that handles Pickup messages for the mediator role.
* @beta This API may change without a BREAKING CHANGE notice.
*/
export class PickupMediatorMessageHandler extends AbstractMessageHandler {
constructor() {
super()
}
/**
* Handles messages for Pickup protocol and mediator role
* https://didcomm.org/pickup/3.0/
*/
public async handle(message: Message, context: IContext): Promise<Message> {
if (message.type === STATUS_REQUEST_MESSAGE_TYPE) {
debug('Status Request Message Received')
try {
await this.replyWithStatusMessage(message, context)
} catch (ex) {
debug(ex)
}
return message
} else if (message.type === DELIVERY_REQUEST_MESSAGE_TYPE) {
debug('Delivery Request Message Received')
try {
const { returnRoute, data, from, to } = message
if (!to) {
throw new Error('invalid_argument: DeliveryRequest received without `to` set')
}
if (!from) {
throw new Error('invalid_argument: DeliveryRequest received without `from` set')
}
if (!data.limit || Number.isNaN(data.limit)) {
throw new Error('invalid_argument: DeliveryRequest received without `body.limit` set')
}
if (returnRoute === 'all') {
const queuedMessages = await context.agent.dataStoreORMGetMessages({
where: generateGetMessagesWhereQuery(from, data.recipient_key),
take: data.limit,
})
if (queuedMessages.length == 0) {
await this.replyWithStatusMessage(message, context)
return message
}
const attachments: IDIDCommMessageAttachment[] = queuedMessages.map((message) => {
return {
id: message.id,
media_type: DIDCommMessageMediaType.ENCRYPTED,
data: {
json: JSON.parse(message.raw!),
},
}
})
const replyRecipientKey = data.recipient_key ? { recipient_key: data.recipient_key } : {}
const replyMessage: IDIDCommMessage = {
type: DELIVERY_MESSAGE_TYPE,
from: to,
to: [from],
id: v4(),
thid: message.threadId ?? message.id,
created_time: new Date().toISOString(),
body: {
...replyRecipientKey,
},
attachments,
}
const packedResponse = await context.agent.packDIDCommMessage({
message: replyMessage,
packing: 'authcrypt',
})
const returnResponse = {
id: replyMessage.id,
message: packedResponse.message,
contentType: DIDCommMessageMediaType.ENCRYPTED,
}
message.addMetaData({ type: 'ReturnRouteResponse', value: JSON.stringify(returnResponse) })
} else {
throw new Error('No return_route found for DeliveryRequest')
}
} catch (ex) {
debug(ex)
}
return message
} else if (message.type === MESSAGES_RECEIVED_MESSAGE_TYPE) {
debug('MessagesReceived Message Received')
try {
const { data, from } = message
if (!from) {
throw new Error('invalid_argument: MessagesReceived received without `from` set')
}
if (!data.message_id_list || !Array.isArray(data.message_id_list)) {
throw new Error('invalid_argument: MessagesReceived received without `body.message_id_list` set')
}
await Promise.all(
data.message_id_list.map(async (messageId: string) => {
const message = await context.agent.dataStoreGetMessage({ id: messageId })
// Delete message if meant for recipient
if (message.to?.startsWith(`${from}#`)) {
await context.agent.dataStoreDeleteMessage({ id: messageId })
context.agent.emit('DIDCommV2Message-forwardMessageDequeued', messageId)
}
}),
)
await this.replyWithStatusMessage(message, context)
} catch (ex) {
debug(ex)
}
}
return super.handle(message, context)
}
private async replyWithStatusMessage(message: Message, context: IContext) {
const { returnRoute, data, from, to } = message
if (!to) {
throw new Error('invalid_argument: StatusRequest received without `to` set')
}
if (!from) {
throw new Error('invalid_argument: StatusRequest received without `from` set')
}
if (returnRoute === 'all') {
const queuedMessageCount = await context.agent.dataStoreORMGetMessagesCount({
where: generateGetMessagesWhereQuery(from, data.recipient_key),
})
const replyRecipientKey = data.recipient_key ? { recipient_key: data.recipient_key } : {}
const replyMessage: IDIDCommMessage = {
type: STATUS_MESSAGE_TYPE,
from: to,
to: [from],
id: v4(),
thid: message.threadId ?? message.id,
created_time: new Date().toISOString(),
body: {
message_count: queuedMessageCount,
live_delivery: false,
...replyRecipientKey,
},
}
const packedResponse = await context.agent.packDIDCommMessage({
message: replyMessage,
packing: 'authcrypt',
})
const returnResponse = {
id: replyMessage.id,
message: packedResponse.message,
contentType: DIDCommMessageMediaType.ENCRYPTED,
}
message.addMetaData({ type: 'ReturnRouteResponse', value: JSON.stringify(returnResponse) })
} else {
throw new Error('No return_route found for StatusRequest')
}
}
}
/**
* A plugin for the {@link @veramo/message-handler#MessageHandler} that handles Pickup messages for the mediator role.
* @beta This API may change without a BREAKING CHANGE notice.
*/
export class PickupRecipientMessageHandler extends AbstractMessageHandler {
constructor() {
super()
}
/**
* Handles messages for Pickup protocol and recipient role
* https://didcomm.org/pickup/3.0/
*/
public async handle(message: Message, context: IContext): Promise<Message> {
if (message.type === DELIVERY_MESSAGE_TYPE) {
debug('Message Delivery batch Received')
try {
const { attachments, to, from } = message
if (!to) {
throw new Error('invalid_argument: StatusRequest received without `to` set')
}
if (!from) {
throw new Error('invalid_argument: StatusRequest received without `from` set')
}
if (!attachments) {
throw new Error('invalid_argument: MessagesDelivery received without `attachments` set')
}
// 1. Handle batch of messages
const messageIds = await Promise.all(
attachments.map(async (attachment) => {
await context.agent.handleMessage({
raw: JSON.stringify(attachment.data.json),
metaData: [{ type: 'didCommMsgFromMediator', value: attachment.id }],
})
return attachment.id
}),
)
// 2. Reply with messages-received
const replyMessage: IDIDCommMessage = {
type: MESSAGES_RECEIVED_MESSAGE_TYPE,
from: to,
to: [from],
id: v4(),
thid: message.threadId ?? message.id,
created_time: new Date().toISOString(),
return_route: 'all',
body: {
message_id_list: messageIds,
},
}
const packedResponse = await context.agent.packDIDCommMessage({
message: replyMessage,
packing: 'authcrypt',
})
await context.agent.sendDIDCommMessage({
packedMessage: packedResponse,
messageId: replyMessage.id,
recipientDidUrl: from,
})
} catch (ex) {
debug(ex)
}
return message
}
return super.handle(message, context)
}
}