From 92bc60070424e30a20f867322ae6acfe4ef610ed Mon Sep 17 00:00:00 2001 From: andresfv95 Date: Fri, 13 Sep 2024 12:28:09 -0500 Subject: [PATCH] feat: redis queue for input message processing (#15) --- README.md | 2 + examples/chatbot/docker-compose.yml | 20 ++ examples/phonenumbervs/docker-compose.yml | 20 ++ package.json | 2 + src/app.module.ts | 12 + src/controllers/message/MessageController.ts | 323 ++---------------- src/controllers/message/MessageService.ts | 321 +++++++++++++++++ src/controllers/message/index.ts | 6 + .../message/services/CoreMessageService.ts | 17 + .../message/services/MessageServiceFactory.ts | 22 ++ .../message/services/RedisMessageService.ts | 23 ++ src/events/MessageEvents.ts | 35 +- src/modules/redis.module.ts | 34 ++ yarn.lock | 180 ++++++++++ 14 files changed, 706 insertions(+), 311 deletions(-) create mode 100644 src/controllers/message/MessageService.ts create mode 100644 src/controllers/message/index.ts create mode 100644 src/controllers/message/services/CoreMessageService.ts create mode 100644 src/controllers/message/services/MessageServiceFactory.ts create mode 100644 src/controllers/message/services/RedisMessageService.ts create mode 100644 src/modules/redis.module.ts diff --git a/README.md b/README.md index fda6bfc..940214d 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ but likely needed for production and test deployments. | POSTGRES_PASSWORD | PosgreSQL database password | None | | POSTGRES_ADMIN_USER | PosgreSQL database admin user | None | | POSTGRES_ADMIN_PASSWORD | PosgreSQL database admin password | None | +| REDIS_HOST | Redis database host user. This system will only function if this variable is defined. (Recommended for production mode) | None | +| REDIS_PASSWORD | Redis database password | None | > **Note**: While not mandatory, it is recommended to set an agent public DID matching external hostname (e.g. if your Service Agent instance is accessable in `https://myagent.com:3000` you must set AGENT_PUBLIC_DID to `did:web:myagent.com%3A3000`), which will make possible for the agent to create its own creadential types and therefore issue credentials. Note that you'll need HTTPS in order to fully support did:web specification. diff --git a/examples/chatbot/docker-compose.yml b/examples/chatbot/docker-compose.yml index 2babfa8..dbd0306 100644 --- a/examples/chatbot/docker-compose.yml +++ b/examples/chatbot/docker-compose.yml @@ -33,5 +33,25 @@ services: - PORT=5000 - SERVICE_AGENT_ADMIN_BASE_URL=http://chatbot-service-agent:3000 - PNVS_SERVICE_AGENT_ADMIN_BASE_URL=http://10.82.14.12:3100 + + redis: + image: redis:alpine + restart: always + networks: + - chatbot + ports: + - 6379:6379 + command: redis-server --maxmemory 64mb --maxmemory-policy allkeys-lru + + postgres: + image: postgres:15.2 + restart: always + networks: + - chatbot + ports: + - 5432:5432 + environment: + - POSTGRES_PASSWORD=64270demo + - POSTGRES_USER=emailvs networks: chatbot: diff --git a/examples/phonenumbervs/docker-compose.yml b/examples/phonenumbervs/docker-compose.yml index a4692e9..7c77d50 100644 --- a/examples/phonenumbervs/docker-compose.yml +++ b/examples/phonenumbervs/docker-compose.yml @@ -33,5 +33,25 @@ services: environment: - PORT=5000 - SERVICE_AGENT_ADMIN_BASE_URL=http://phonenumbervs-service-agent:3000 + + redis: + image: redis:alpine + restart: always + networks: + - phonenumbervs + ports: + - 6379:6379 + command: redis-server --maxmemory 64mb --maxmemory-policy allkeys-lru + + postgres: + image: postgres:15.2 + restart: always + networks: + - phonenumbervs + ports: + - 5432:5432 + environment: + - POSTGRES_PASSWORD=64270demo + - POSTGRES_USER=emailvs networks: phonenumbervs: diff --git a/package.json b/package.json index df3c85c..9c3a542 100644 --- a/package.json +++ b/package.json @@ -66,12 +66,14 @@ "@credo-ts/question-answer": "^0.5.2", "@hyperledger/anoncreds-nodejs": "^0.2.2", "@hyperledger/aries-askar-nodejs": "^0.2.1", + "@nestjs/bull": "^10.2.1", "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.0", "@types/qrcode": "^1.5.0", "body-parser": "^1.20.0", + "bull": "^4.16.2", "cors": "^2.8.5", "credo-ts-didweb-anoncreds": "^0.0.1-alpha.10", "credo-ts-media-sharing": "^0.0.1-alpha.9", diff --git a/src/app.module.ts b/src/app.module.ts index 59e5b56..7a7a02b 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,9 +5,16 @@ import { ConnectionController } from './controllers/connections/ConnectionContro import { CredentialTypesController } from './controllers/credentials/CredentialTypeController' import { InvitationController } from './controllers/invitation/InvitationController' import { QrController } from './controllers/invitation/QrController' +import { + CoreMessageService, + MessageService, + MessageServiceFactory, + RedisMessageService, +} from './controllers/message' import { MessageController } from './controllers/message/MessageController' import { PresentationsController } from './controllers/presentations/PresentationsController' import { VCAuthNController } from './controllers/vcauthn/VCAuthNController' +import { HandledRedisModule } from './modules/redis.module' import { AgentService } from './services/AgentService' import { UrlShorteningService } from './services/UrlShorteningService' import { ServiceAgent } from './utils/ServiceAgent' @@ -17,6 +24,7 @@ export class ServiceAgentModule { static register(agent: ServiceAgent): DynamicModule { return { module: ServiceAgentModule, + imports: [HandledRedisModule.forRoot()], controllers: [ AgentController, ConnectionController, @@ -34,6 +42,10 @@ export class ServiceAgentModule { }, AgentService, UrlShorteningService, + MessageService, + RedisMessageService, + CoreMessageService, + MessageServiceFactory, ], exports: [AgentService], } diff --git a/src/controllers/message/MessageController.ts b/src/controllers/message/MessageController.ts index 93e7bc3..d55e77b 100644 --- a/src/controllers/message/MessageController.ts +++ b/src/controllers/message/MessageController.ts @@ -1,41 +1,13 @@ -import { ActionMenuRole, ActionMenuOption } from '@credo-ts/action-menu' -import { AnonCredsRequestedAttribute } from '@credo-ts/anoncreds' -import { - JsonTransformer, - AutoAcceptCredential, - AutoAcceptProof, - utils, - MessageSender, - OutboundMessageContext, - OutOfBandRepository, - OutOfBandInvitation, - DidExchangeState, -} from '@credo-ts/core' -import { QuestionAnswerRepository, ValidResponse } from '@credo-ts/question-answer' +import { DidExchangeState, utils } from '@credo-ts/core' import { Body, Controller, HttpException, HttpStatus, Logger, Post } from '@nestjs/common' import { ApiBody, ApiTags } from '@nestjs/swagger' -import { - TextMessage, - ReceiptsMessage, - IdentityProofRequestMessage, - MenuDisplayMessage, - CredentialIssuanceMessage, - ContextualMenuUpdateMessage, - InvitationMessage, - ProfileMessage, - MediaMessage, - IBaseMessage, - didcommReceiptFromServiceAgentReceipt, - IdentityProofResultMessage, - TerminateConnectionMessage, -} from '../../model' -import { VerifiableCredentialRequestedProofItem } from '../../model/messages/proofs/vc/VerifiableCredentialRequestedProofItem' +import { IBaseMessage } from '../../model' import { AgentService } from '../../services/AgentService' -import { parsePictureData } from '../../utils/parsers' -import { RequestedCredential } from '../types' +import { ServiceAgent } from '../../utils/ServiceAgent' import { MessageDto } from './MessageDto' +import { MessageServiceFactory } from './services/MessageServiceFactory' @ApiTags('message') @Controller({ @@ -45,7 +17,10 @@ import { MessageDto } from './MessageDto' export class MessageController { private readonly logger = new Logger(MessageController.name) - constructor(private readonly agentService: AgentService) {} + constructor( + private readonly messageServiceFactory: MessageServiceFactory, + private readonly agentService: AgentService, + ) {} @Post('/') @ApiBody({ @@ -76,11 +51,7 @@ export class MessageController { public async sendMessage(@Body() message: IBaseMessage): Promise<{ id: string }> { try { const agent = await this.agentService.getAgent() - - let messageId: string | undefined - const messageType = message.type - this.logger.debug!(`Message submitted. ${JSON.stringify(message)}`) - + await this.checkForDuplicateId(agent, message) const connection = await agent.connections.findById(message.connectionId) if (!connection) throw new Error(`Connection with id ${message.connectionId} not found`) @@ -88,271 +59,16 @@ export class MessageController { if (connection.state === DidExchangeState.Completed && (!connection.did || !connection.theirDid)) { throw new Error(`This connection has been terminated. No further messages are possible`) } + const messageId = message.id ?? utils.uuid() + message.id = messageId - if (messageType === TextMessage.type) { - const textMsg = JsonTransformer.fromJSON(message, TextMessage) - const record = await agent.basicMessages.sendMessage(textMsg.connectionId, textMsg.content) - messageId = record.threadId - } else if (messageType === MediaMessage.type) { - const mediaMsg = JsonTransformer.fromJSON(message, MediaMessage) - const mediaRecord = await agent.modules.media.create({ connectionId: mediaMsg.connectionId }) - const record = await agent.modules.media.share({ - recordId: mediaRecord.id, - description: mediaMsg.description, - items: mediaMsg.items.map(item => ({ - id: item.id, - uri: item.uri, - description: item.description, - mimeType: item.mimeType, - byteCount: item.byteCount, - ciphering: item.ciphering?.algorithm - ? { ...item.ciphering, parameters: item.ciphering.parameters ?? {} } - : undefined, - fileName: item.filename, - metadata: { - preview: item.preview, - width: item.width, - height: item.height, - duration: item.duration, - title: item.title, - icon: item.icon, - openingMode: item.openingMode, - screenOrientation: item.screenOrientation, - }, - })), - }) - messageId = record.threadId - } else if (messageType === ReceiptsMessage.type) { - const textMsg = JsonTransformer.fromJSON(message, ReceiptsMessage) - await agent.modules.receipts.send({ - connectionId: textMsg.connectionId, - receipts: textMsg.receipts.map(didcommReceiptFromServiceAgentReceipt), - }) - } else if (messageType === MenuDisplayMessage.type) { - const msg = JsonTransformer.fromJSON(message, MenuDisplayMessage) - - const record = await agent.modules.questionAnswer.sendQuestion(msg.connectionId, { - question: msg.prompt, - validResponses: msg.menuItems.map(item => new ValidResponse({ text: item.text })), - }) - messageId = record.threadId - - // Add id-text mapping so we can recover it when receiving an answer - record.metadata.add( - 'text-id-mapping', - msg.menuItems.reduce( - (acc, curr) => ((acc[curr.text] = curr.id), acc), - {} as Record, - ), - ) - await agent.dependencyManager.resolve(QuestionAnswerRepository).update(agent.context, record) - } else if (messageType === ContextualMenuUpdateMessage.type) { - const msg = JsonTransformer.fromJSON(message, ContextualMenuUpdateMessage) - - await agent.modules.actionMenu.clearActiveMenu({ - connectionId: msg.connectionId, - role: ActionMenuRole.Responder, - }) - await agent.modules.actionMenu.sendMenu({ - connectionId: msg.connectionId, - menu: { - title: msg.title, - description: msg.description ?? '', - options: msg.options.map( - item => - new ActionMenuOption({ - title: item.title, - name: item.id, - description: item.description ?? '', - }), - ), - }, - }) - } else if (messageType === IdentityProofRequestMessage.type) { - const msg = JsonTransformer.fromJSON(message, IdentityProofRequestMessage) - - for (const item of msg.requestedProofItems) { - if (item.type === 'verifiable-credential') { - const vcItem = item as VerifiableCredentialRequestedProofItem - - const credentialDefinitionId = vcItem.credentialDefinitionId as string - let attributes = vcItem.attributes as string[] - - if (!credentialDefinitionId) { - throw Error('Verifiable credential request must include credentialDefinitionId') - } - - if (attributes && !Array.isArray(attributes)) { - throw new Error('Received attributes is not an array') - } - - const { credentialDefinition } = - await agent.modules.anoncreds.getCredentialDefinition(credentialDefinitionId) - - if (!credentialDefinition) { - throw Error(`Cannot find information about credential definition ${credentialDefinitionId}.`) - } - - // Verify that requested attributes are present in credential definition - const { schema } = await agent.modules.anoncreds.getSchema(credentialDefinition.schemaId) - - if (!schema) { - throw Error(`Cannot find information about schema ${credentialDefinition.schemaId}.`) - } - - // If no attributes are specified, request all of them - if (!attributes) { - attributes = schema.attrNames - } - - if (!attributes.every(item => schema.attrNames.includes(item))) { - throw new Error( - `Some attributes are not present in the requested credential type: Requested: ${attributes}, Present: ${schema.attrNames}`, - ) - } - - const requestedAttributes: Record = {} - - requestedAttributes[schema.name] = { - names: attributes, - restrictions: [{ cred_def_id: credentialDefinitionId }], - } - - const record = await agent.proofs.requestProof({ - comment: vcItem.description as string, - connectionId: msg.connectionId, - proofFormats: { - anoncreds: { - name: 'proof-request', - version: '1.0', - requested_attributes: requestedAttributes, - }, - }, - protocolVersion: 'v2', - parentThreadId: msg.threadId, - autoAcceptProof: AutoAcceptProof.Never, - }) - messageId = record.threadId - record.metadata.set('_2060/requestedCredentials', { - credentialDefinitionId, - attributes, - } as RequestedCredential) - await agent.proofs.update(record) - } - } - } else if (messageType === IdentityProofResultMessage.type) { - throw new Error(`Identity proof Result not supported`) - } else if (messageType === CredentialIssuanceMessage.type) { - const msg = JsonTransformer.fromJSON(message, CredentialIssuanceMessage) - - const credential = (await agent.credentials.getAll()).find(item => item.threadId === message.threadId) - if (credential) { - await agent.credentials.acceptProposal({ - credentialRecordId: credential.id, - autoAcceptCredential: AutoAcceptCredential.Always, - }) - } else { - if (msg.claims && msg.credentialDefinitionId) { - const record = await agent.credentials.offerCredential({ - connectionId: msg.connectionId, - credentialFormats: { - anoncreds: { - attributes: msg.claims.map(item => { - return { name: item.name, mimeType: item.mimeType, value: item.value } - }), - credentialDefinitionId: msg.credentialDefinitionId, - }, - }, - protocolVersion: 'v2', - autoAcceptCredential: AutoAcceptCredential.Always, - }) - messageId = record.threadId - } else { - throw new Error( - 'Claims and credentialDefinitionId attributes must be present if a credential without related thread is to be issued', - ) - } - } - } else if (messageType === InvitationMessage.type) { - const msg = JsonTransformer.fromJSON(message, InvitationMessage) - const { label, imageUrl, did } = msg - - const messageSender = agent.context.dependencyManager.resolve(MessageSender) - - if (did) { - // FIXME: This is a workaround due to an issue found in AFJ validator. Replace with class when fixed - const json = { - '@type': OutOfBandInvitation.type.messageTypeUri, - '@id': did, - label: label ?? '', - imageUrl: imageUrl, - services: [did], - handshake_protocols: ['https://didcomm.org/didexchange/1.0'], - } - - const invitation = OutOfBandInvitation.fromJson(json) - - /*const invitation = new OutOfBandInvitation({ - id: did, - label: label ?? '', imageUrl, - services: [did], - handshakeProtocols: [HandshakeProtocol.DidExchange], - })*/ - - await messageSender.sendMessage( - new OutboundMessageContext(invitation, { - agentContext: agent.context, - connection, - }), - ) - - messageId = invitation.id - } else { - const outOfBandRecord = await agent.oob.createInvitation({ - label, - imageUrl, - }) - outOfBandRecord.setTag('parentConnectionId', connection.id) - await agent.dependencyManager.resolve(OutOfBandRepository).update(agent.context, outOfBandRecord) - - await messageSender.sendMessage( - new OutboundMessageContext(outOfBandRecord.outOfBandInvitation, { - agentContext: agent.context, - connection, - }), - ) - - messageId = outOfBandRecord.id - } - } else if (messageType === ProfileMessage.type) { - const msg = JsonTransformer.fromJSON(message, ProfileMessage) - const { displayImageUrl, displayName, displayIconUrl } = msg - - await agent.modules.userProfile.sendUserProfile({ - connectionId: connection.id, - profileData: { - displayName: displayName ?? undefined, - displayPicture: displayImageUrl ? parsePictureData(displayImageUrl) : undefined, - displayIcon: displayIconUrl ? parsePictureData(displayIconUrl) : undefined, - }, - }) - - // FIXME: No message id is returned here - } else if (messageType === TerminateConnectionMessage.type) { - JsonTransformer.fromJSON(message, TerminateConnectionMessage) - - await agent.connections.hangup({ connectionId: connection.id }) - - // FIXME: No message id is returned here - } - - this.logger.debug!(`messageId: ${messageId}`) - return { id: messageId ?? utils.uuid() } // TODO: persistant mapping between AFJ records and Service Agent flows. Support external message id setting + await this.messageServiceFactory.processMessage(message, connection) + return { id: messageId } } catch (error) { this.logger.error(`Error: ${error.stack}`) throw new HttpException( { - statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + statusCode: error.statusCode ?? HttpStatus.INTERNAL_SERVER_ERROR, error: `something went wrong: ${error}`, }, HttpStatus.INTERNAL_SERVER_ERROR, @@ -362,4 +78,15 @@ export class MessageController { ) } } + + private async checkForDuplicateId(agent: ServiceAgent, message: IBaseMessage): Promise { + const records = message.id + ? await agent.genericRecords.findAllByQuery({ + messageId: message.id, + connectionId: message.connectionId, + }) + : null + + if (records && records.length > 0) throw new Error(`Duplicated ID: ${message.id}`) + } } diff --git a/src/controllers/message/MessageService.ts b/src/controllers/message/MessageService.ts new file mode 100644 index 0000000..2207fba --- /dev/null +++ b/src/controllers/message/MessageService.ts @@ -0,0 +1,321 @@ +import { ActionMenuRole, ActionMenuOption } from '@credo-ts/action-menu' +import { AnonCredsRequestedAttribute } from '@credo-ts/anoncreds' +import { + JsonTransformer, + AutoAcceptCredential, + AutoAcceptProof, + utils, + MessageSender, + OutboundMessageContext, + OutOfBandRepository, + OutOfBandInvitation, + ConnectionRecord, +} from '@credo-ts/core' +import { QuestionAnswerRepository, ValidResponse } from '@credo-ts/question-answer' +import { Injectable, Logger } from '@nestjs/common' + +import { + TextMessage, + ReceiptsMessage, + IdentityProofRequestMessage, + MenuDisplayMessage, + CredentialIssuanceMessage, + ContextualMenuUpdateMessage, + InvitationMessage, + ProfileMessage, + MediaMessage, + IBaseMessage, + didcommReceiptFromServiceAgentReceipt, + IdentityProofResultMessage, + TerminateConnectionMessage, +} from '../../model' +import { VerifiableCredentialRequestedProofItem } from '../../model/messages/proofs/vc/VerifiableCredentialRequestedProofItem' +import { AgentService } from '../../services/AgentService' +import { parsePictureData } from '../../utils/parsers' +import { RequestedCredential } from '../types' + +@Injectable() +export class MessageService { + private readonly logger = new Logger(MessageService.name) + + constructor(private readonly agentService: AgentService) {} + + public async sendMessage(message: IBaseMessage, connection: ConnectionRecord): Promise<{ id: string }> { + try { + const agent = await this.agentService.getAgent() + + let messageId: string | undefined + const messageType = message.type + this.logger.debug!(`Message submitted. ${JSON.stringify(message)}`) + + if (messageType === TextMessage.type) { + const textMsg = JsonTransformer.fromJSON(message, TextMessage) + const record = await agent.basicMessages.sendMessage(textMsg.connectionId, textMsg.content) + messageId = record.threadId + } else if (messageType === MediaMessage.type) { + const mediaMsg = JsonTransformer.fromJSON(message, MediaMessage) + const mediaRecord = await agent.modules.media.create({ connectionId: mediaMsg.connectionId }) + const record = await agent.modules.media.share({ + recordId: mediaRecord.id, + description: mediaMsg.description, + items: mediaMsg.items.map(item => ({ + id: item.id, + uri: item.uri, + description: item.description, + mimeType: item.mimeType, + byteCount: item.byteCount, + ciphering: item.ciphering?.algorithm + ? { ...item.ciphering, parameters: item.ciphering.parameters ?? {} } + : undefined, + fileName: item.filename, + metadata: { + preview: item.preview, + width: item.width, + height: item.height, + duration: item.duration, + title: item.title, + icon: item.icon, + openingMode: item.openingMode, + screenOrientation: item.screenOrientation, + }, + })), + }) + messageId = record.threadId + } else if (messageType === ReceiptsMessage.type) { + const textMsg = JsonTransformer.fromJSON(message, ReceiptsMessage) + await agent.modules.receipts.send({ + connectionId: textMsg.connectionId, + receipts: textMsg.receipts.map(didcommReceiptFromServiceAgentReceipt), + }) + } else if (messageType === MenuDisplayMessage.type) { + const msg = JsonTransformer.fromJSON(message, MenuDisplayMessage) + + const record = await agent.modules.questionAnswer.sendQuestion(msg.connectionId, { + question: msg.prompt, + validResponses: msg.menuItems.map(item => new ValidResponse({ text: item.text })), + }) + messageId = record.threadId + + // Add id-text mapping so we can recover it when receiving an answer + record.metadata.add( + 'text-id-mapping', + msg.menuItems.reduce( + (acc, curr) => ((acc[curr.text] = curr.id), acc), + {} as Record, + ), + ) + await agent.dependencyManager.resolve(QuestionAnswerRepository).update(agent.context, record) + } else if (messageType === ContextualMenuUpdateMessage.type) { + const msg = JsonTransformer.fromJSON(message, ContextualMenuUpdateMessage) + + await agent.modules.actionMenu.clearActiveMenu({ + connectionId: msg.connectionId, + role: ActionMenuRole.Responder, + }) + await agent.modules.actionMenu.sendMenu({ + connectionId: msg.connectionId, + menu: { + title: msg.title, + description: msg.description ?? '', + options: msg.options.map( + item => + new ActionMenuOption({ + title: item.title, + name: item.id, + description: item.description ?? '', + }), + ), + }, + }) + } else if (messageType === IdentityProofRequestMessage.type) { + const msg = JsonTransformer.fromJSON(message, IdentityProofRequestMessage) + + for (const item of msg.requestedProofItems) { + if (item.type === 'verifiable-credential') { + const vcItem = item as VerifiableCredentialRequestedProofItem + + const credentialDefinitionId = vcItem.credentialDefinitionId as string + let attributes = vcItem.attributes as string[] + + if (!credentialDefinitionId) { + throw Error('Verifiable credential request must include credentialDefinitionId') + } + + if (attributes && !Array.isArray(attributes)) { + throw new Error('Received attributes is not an array') + } + + const { credentialDefinition } = + await agent.modules.anoncreds.getCredentialDefinition(credentialDefinitionId) + + if (!credentialDefinition) { + throw Error(`Cannot find information about credential definition ${credentialDefinitionId}.`) + } + + // Verify that requested attributes are present in credential definition + const { schema } = await agent.modules.anoncreds.getSchema(credentialDefinition.schemaId) + + if (!schema) { + throw Error(`Cannot find information about schema ${credentialDefinition.schemaId}.`) + } + + // If no attributes are specified, request all of them + if (!attributes) { + attributes = schema.attrNames + } + + if (!attributes.every(item => schema.attrNames.includes(item))) { + throw new Error( + `Some attributes are not present in the requested credential type: Requested: ${attributes}, Present: ${schema.attrNames}`, + ) + } + + const requestedAttributes: Record = {} + + requestedAttributes[schema.name] = { + names: attributes, + restrictions: [{ cred_def_id: credentialDefinitionId }], + } + + const record = await agent.proofs.requestProof({ + comment: vcItem.description as string, + connectionId: msg.connectionId, + proofFormats: { + anoncreds: { + name: 'proof-request', + version: '1.0', + requested_attributes: requestedAttributes, + }, + }, + protocolVersion: 'v2', + parentThreadId: msg.threadId, + autoAcceptProof: AutoAcceptProof.Never, + }) + messageId = record.threadId + record.metadata.set('_2060/requestedCredentials', { + credentialDefinitionId, + attributes, + } as RequestedCredential) + await agent.proofs.update(record) + } + } + } else if (messageType === IdentityProofResultMessage.type) { + throw new Error(`Identity proof Result not supported`) + } else if (messageType === CredentialIssuanceMessage.type) { + const msg = JsonTransformer.fromJSON(message, CredentialIssuanceMessage) + + const credential = (await agent.credentials.getAll()).find(item => item.threadId === message.threadId) + if (credential) { + await agent.credentials.acceptProposal({ + credentialRecordId: credential.id, + autoAcceptCredential: AutoAcceptCredential.Always, + }) + } else { + if (msg.claims && msg.credentialDefinitionId) { + const record = await agent.credentials.offerCredential({ + connectionId: msg.connectionId, + credentialFormats: { + anoncreds: { + attributes: msg.claims.map(item => { + return { name: item.name, mimeType: item.mimeType, value: item.value } + }), + credentialDefinitionId: msg.credentialDefinitionId, + }, + }, + protocolVersion: 'v2', + autoAcceptCredential: AutoAcceptCredential.Always, + }) + messageId = record.threadId + } else { + throw new Error( + 'Claims and credentialDefinitionId attributes must be present if a credential without related thread is to be issued', + ) + } + } + } else if (messageType === InvitationMessage.type) { + const msg = JsonTransformer.fromJSON(message, InvitationMessage) + const { label, imageUrl, did } = msg + + const messageSender = agent.context.dependencyManager.resolve(MessageSender) + + if (did) { + // FIXME: This is a workaround due to an issue found in AFJ validator. Replace with class when fixed + const json = { + '@type': OutOfBandInvitation.type.messageTypeUri, + '@id': did, + label: label ?? '', + imageUrl: imageUrl, + services: [did], + handshake_protocols: ['https://didcomm.org/didexchange/1.0'], + } + + const invitation = OutOfBandInvitation.fromJson(json) + + /*const invitation = new OutOfBandInvitation({ + id: did, + label: label ?? '', imageUrl, + services: [did], + handshakeProtocols: [HandshakeProtocol.DidExchange], + })*/ + + await messageSender.sendMessage( + new OutboundMessageContext(invitation, { + agentContext: agent.context, + connection, + }), + ) + + messageId = invitation.id + } else { + const outOfBandRecord = await agent.oob.createInvitation({ + label, + imageUrl, + }) + outOfBandRecord.setTag('parentConnectionId', connection.id) + await agent.dependencyManager.resolve(OutOfBandRepository).update(agent.context, outOfBandRecord) + + await messageSender.sendMessage( + new OutboundMessageContext(outOfBandRecord.outOfBandInvitation, { + agentContext: agent.context, + connection, + }), + ) + + messageId = outOfBandRecord.id + } + } else if (messageType === ProfileMessage.type) { + const msg = JsonTransformer.fromJSON(message, ProfileMessage) + const { displayImageUrl, displayName, displayIconUrl } = msg + + await agent.modules.userProfile.sendUserProfile({ + connectionId: connection.id, + profileData: { + displayName: displayName ?? undefined, + displayPicture: displayImageUrl ? parsePictureData(displayImageUrl) : undefined, + displayIcon: displayIconUrl ? parsePictureData(displayIconUrl) : undefined, + }, + }) + + // FIXME: No message id is returned here + } else if (messageType === TerminateConnectionMessage.type) { + JsonTransformer.fromJSON(message, TerminateConnectionMessage) + + await agent.connections.hangup({ connectionId: connection.id }) + + // FIXME: No message id is returned here + } + + if (messageId) + await agent.genericRecords.save({ + id: messageId, + content: {}, + tags: { messageId: message.id, connectionId: message.connectionId }, + }) + this.logger.debug!(`messageId: ${messageId}`) + return { id: messageId ?? utils.uuid() } // TODO: persistant mapping between AFJ records and Service Agent flows. Support external message id setting + } catch (error) { + this.logger.error(`Error: ${error.stack}`) + throw new Error(`something went wrong: ${error}`) + } + } +} diff --git a/src/controllers/message/index.ts b/src/controllers/message/index.ts new file mode 100644 index 0000000..3d2b9ed --- /dev/null +++ b/src/controllers/message/index.ts @@ -0,0 +1,6 @@ +export * from './MessageController' +export * from './MessageDto' +export * from './MessageService' +export * from './services/CoreMessageService' +export * from './services/MessageServiceFactory' +export * from './services/RedisMessageService' diff --git a/src/controllers/message/services/CoreMessageService.ts b/src/controllers/message/services/CoreMessageService.ts new file mode 100644 index 0000000..1b81215 --- /dev/null +++ b/src/controllers/message/services/CoreMessageService.ts @@ -0,0 +1,17 @@ +import { ConnectionRecord, utils } from '@credo-ts/core' +import { Injectable, Logger } from '@nestjs/common' + +import { IBaseMessage } from '../../../model' +import { MessageService } from '../MessageService' + +@Injectable() +export class CoreMessageService { + private readonly logger = new Logger(CoreMessageService.name) + constructor(private readonly messageService: MessageService) {} + + async processMessage(message: IBaseMessage, connection: ConnectionRecord): Promise<{ id: string }> { + this.logger.log(`Sending message directly: ${message.id}`) + await this.messageService.sendMessage(message, connection) + return { id: message.id ?? utils.uuid() } + } +} diff --git a/src/controllers/message/services/MessageServiceFactory.ts b/src/controllers/message/services/MessageServiceFactory.ts new file mode 100644 index 0000000..d78f23f --- /dev/null +++ b/src/controllers/message/services/MessageServiceFactory.ts @@ -0,0 +1,22 @@ +import { ConnectionRecord } from '@credo-ts/core' +import { InjectQueue } from '@nestjs/bull' +import { Injectable, Optional } from '@nestjs/common' +import { Queue } from 'bull' + +import { IBaseMessage } from '../../../model' + +import { CoreMessageService } from './CoreMessageService' + +@Injectable() +export class MessageServiceFactory { + constructor( + @Optional() @InjectQueue('message') private messageQueue: Queue, + private readonly coreMessageService: CoreMessageService, + ) {} + + async processMessage(message: IBaseMessage, connection: ConnectionRecord) { + return process.env.REDIS_HOST !== undefined + ? await this.messageQueue.add({ message, connection }) + : await this.coreMessageService.processMessage(message, connection) + } +} diff --git a/src/controllers/message/services/RedisMessageService.ts b/src/controllers/message/services/RedisMessageService.ts new file mode 100644 index 0000000..3c7b812 --- /dev/null +++ b/src/controllers/message/services/RedisMessageService.ts @@ -0,0 +1,23 @@ +import { ConnectionRecord, utils } from '@credo-ts/core' +import { Process, Processor } from '@nestjs/bull' +import { Logger } from '@nestjs/common' +import { Job } from 'bull' + +import { IBaseMessage } from '../../../model' +import { MessageService } from '../MessageService' + +@Processor('message') +export class RedisMessageService { + private readonly logger = new Logger(RedisMessageService.name) + constructor(private readonly messageService: MessageService) {} + + @Process() + async processMessage( + job: Job<{ message: IBaseMessage; connection: ConnectionRecord }>, + ): Promise<{ id: string }> { + const { message, connection } = job.data + this.logger.debug!(`Queuing message with Bull: ${message.id}`) + await this.messageService.sendMessage(message, connection) + return { id: message.id ?? utils.uuid() } + } +} diff --git a/src/events/MessageEvents.ts b/src/events/MessageEvents.ts index 1956745..ebbb45f 100644 --- a/src/events/MessageEvents.ts +++ b/src/events/MessageEvents.ts @@ -65,7 +65,7 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = timestamp: new Date(), // It can take also 'sentTime' to be related to the origin }) - await sendMessageReceivedEvent(msg, msg.timestamp, config) + await sendMessageReceivedEvent(agent, msg, msg.timestamp, config) } // Action Menu protocol messages @@ -76,7 +76,7 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = timestamp: new Date(), }) - await sendMessageReceivedEvent(msg, msg.timestamp, config) + await sendMessageReceivedEvent(agent, msg, msg.timestamp, config) } if (message.type === PerformMessage.type.messageTypeUri) { @@ -87,7 +87,7 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = timestamp: new Date(), }) - await sendMessageReceivedEvent(msg, msg.timestamp, config) + await sendMessageReceivedEvent(agent, msg, msg.timestamp, config) } // Question Answer protocol messages @@ -113,7 +113,7 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = id: message.id, }) - await sendMessageReceivedEvent(msg, msg.timestamp, config) + await sendMessageReceivedEvent(agent, msg, msg.timestamp, config) } if ( @@ -141,7 +141,7 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = timestamp: record.updatedAt, }) - await sendMessageReceivedEvent(msg, msg.timestamp, config) + await sendMessageReceivedEvent(agent, msg, msg.timestamp, config) } catch (error) { config.logger.error(`Error processing presentaion problem report: ${error}`) } @@ -196,7 +196,7 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = timestamp: record.updatedAt, }) - await sendMessageReceivedEvent(msg, msg.timestamp, config) + await sendMessageReceivedEvent(agent, msg, msg.timestamp, config) } catch (error) { config.logger.error(`Error processing presentaion message: ${error}`) } @@ -227,7 +227,7 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = timestamp: record.createdAt, }) - await sendMessageReceivedEvent(message, message.timestamp, config) + await sendMessageReceivedEvent(agent, message, message.timestamp, config) } else if ( [CredentialState.Declined, CredentialState.Done, CredentialState.Abandoned].includes(record.state) ) { @@ -240,7 +240,7 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = ? CredentialState.Declined : record.state, }) - await sendMessageReceivedEvent(message, message.timestamp, config) + await sendMessageReceivedEvent(agent, message, message.timestamp, config) } }, ) @@ -279,7 +279,7 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = })), }) - await sendMessageReceivedEvent(message, message.timestamp, config) + await sendMessageReceivedEvent(agent, message, message.timestamp, config) } } }) @@ -295,7 +295,7 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = receipts.forEach(receipt => { const { messageId, timestamp, state } = receipt - sendMessageStateUpdatedEvent({ messageId, connectionId, state, timestamp, config }) + sendMessageStateUpdatedEvent({ agent, messageId, connectionId, state, timestamp, config }) }) }, ) @@ -317,7 +317,14 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = }) } -const sendMessageReceivedEvent = async (message: BaseMessage, timestamp: Date, config: ServerConfig) => { +const sendMessageReceivedEvent = async ( + agent: ServiceAgent, + message: BaseMessage, + timestamp: Date, + config: ServerConfig, +) => { + const recordId = await agent.genericRecords.findById(message.id) + if (recordId?.getTag('messageId') as string) message.id = recordId?.getTag('messageId') as string const body = { timestamp, type: 'message-received', @@ -328,17 +335,19 @@ const sendMessageReceivedEvent = async (message: BaseMessage, timestamp: Date, c } const sendMessageStateUpdatedEvent = async (options: { + agent: ServiceAgent messageId: string connectionId: string state: string timestamp: Date config: ServerConfig }) => { - const { messageId, connectionId, state, timestamp, config } = options + const { agent, messageId, connectionId, state, timestamp, config } = options + const recordId = await agent.genericRecords.findById(messageId) const body = { type: 'message-state-updated', - messageId, + messageId: (recordId?.getTag('messageId') as string) ?? messageId, state, timestamp, connectionId, diff --git a/src/modules/redis.module.ts b/src/modules/redis.module.ts new file mode 100644 index 0000000..540bca2 --- /dev/null +++ b/src/modules/redis.module.ts @@ -0,0 +1,34 @@ +import { BullModule, BullModuleOptions } from '@nestjs/bull' +import { DynamicModule, Module } from '@nestjs/common' + +@Module({}) +export class HandledRedisModule { + static forRoot(): DynamicModule { + const imports = [] + + if (process.env.REDIS_HOST) { + const bullOptions: BullModuleOptions = { + redis: { + host: process.env.REDIS_HOST, + port: 6379, + password: process.env.REDIS_PASSWORD, + maxRetriesPerRequest: 3, + connectTimeout: 5000, + }, + } + + imports.push( + BullModule.forRoot(bullOptions), + BullModule.registerQueue({ + name: 'message', + }), + ) + } + + return { + module: HandledRedisModule, + imports, + exports: process.env.REDIS_HOST ? [BullModule] : [], + } + } +} diff --git a/yarn.lock b/yarn.lock index f2893d4..e50c6e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1068,6 +1068,11 @@ dependencies: buffer "^6.0.3" +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -1451,11 +1456,56 @@ resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz#c3ec604a0b54b9a9b87e9735dfc59e1a5da6a5fb" integrity sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug== +"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz#9edec61b22c3082018a79f6d1c30289ddf3d9d11" + integrity sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw== + +"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz#33677a275204898ad8acbf62734fc4dc0b6a4855" + integrity sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw== + +"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz#19edf7cdc2e7063ee328403c1d895a86dd28f4bb" + integrity sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg== + +"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz#94fb0543ba2e28766c3fc439cabbe0440ae70159" + integrity sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw== + +"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz#4a0609ab5fe44d07c9c60a11e4484d3c38bbd6e3" + integrity sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg== + +"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz#0aa5502d547b57abfc4ac492de68e2006e417242" + integrity sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ== + "@multiformats/base-x@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@multiformats/base-x/-/base-x-4.0.1.tgz#95ff0fa58711789d53aefb2590a8b7a4e715d121" integrity sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw== +"@nestjs/bull-shared@^10.2.1": + version "10.2.1" + resolved "https://registry.yarnpkg.com/@nestjs/bull-shared/-/bull-shared-10.2.1.tgz#64123b663ff1cda7e5635291fc552c9a2ec232bb" + integrity sha512-zvnTvSq6OJ92omcsFUwaUmPbM3PRgWkIusHPB5TE3IFS7nNdM3OwF+kfe56sgKjMtQQMe/56lok0S04OtPMX5Q== + dependencies: + tslib "2.6.3" + +"@nestjs/bull@^10.2.1": + version "10.2.1" + resolved "https://registry.yarnpkg.com/@nestjs/bull/-/bull-10.2.1.tgz#808568a4db1d848def3ff435a59ad8ec326bc6f2" + integrity sha512-y7AII50p+FoyAZP3OKXgi0QCFUFGJ4X7G3L2Arw3BtLsEYvLxTaRB1DPv5a80LYp8Pj4xisIzF4gNv3hfkuErw== + dependencies: + "@nestjs/bull-shared" "^10.2.1" + tslib "2.6.3" + "@nestjs/cli@^10.0.0": version "10.3.2" resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.3.2.tgz#42d2764ead6633e278c55d42de871b4cc1db002b" @@ -2920,6 +2970,19 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +bull@^4.16.2: + version "4.16.2" + resolved "https://registry.yarnpkg.com/bull/-/bull-4.16.2.tgz#a4c8a75ac2f0c686420f8fad766b36617688b30a" + integrity sha512-VCy33UdPGiIoZHDTrslGXKXWxcIUHNH5Z82pihr8HicbIfAH4SHug1HxlwKEbibVv85hq8rJ9tKAW/cuxv2T0A== + dependencies: + cron-parser "^4.2.1" + get-port "^5.1.1" + ioredis "^5.3.2" + lodash "^4.17.21" + msgpackr "^1.10.1" + semver "^7.5.2" + uuid "^8.3.0" + busboy@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" @@ -3159,6 +3222,11 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== +cluster-key-slot@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -3414,6 +3482,13 @@ credo-ts-user-profile@^0.0.1-alpha.6: tsyringe "^4.8.0" uuid "^9.0.0" +cron-parser@^4.2.1: + version "4.9.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.9.0.tgz#0340694af3e46a0894978c6f52a6dbb5c0f11ad5" + integrity sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q== + dependencies: + luxon "^3.2.1" + cross-fetch@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" @@ -3590,6 +3665,11 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -3605,6 +3685,11 @@ detect-libc@^2.0.0: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== +detect-libc@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -4668,6 +4753,11 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-port@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -5038,6 +5128,21 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" +ioredis@^5.3.2: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.4.1.tgz#1c56b70b759f01465913887375ed809134296f40" + integrity sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.1.0" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -5999,6 +6104,16 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== + lodash.memoize@4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -6053,6 +6168,11 @@ lru_map@^0.4.1: resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.4.1.tgz#f7b4046283c79fb7370c36f8fca6aee4324b0a98" integrity sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg== +luxon@^3.2.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.5.0.tgz#6b6f65c5cd1d61d1fd19dbf07ee87a50bf4b8e20" + integrity sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ== + luxon@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48" @@ -6239,6 +6359,27 @@ ms@2.1.3, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msgpackr-extract@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz#e9d87023de39ce714872f9e9504e3c1996d61012" + integrity sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA== + dependencies: + node-gyp-build-optional-packages "5.2.2" + optionalDependencies: + "@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.3" + +msgpackr@^1.10.1: + version "1.11.0" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.11.0.tgz#8321d52333048cadc749f56385e3231e65337091" + integrity sha512-I8qXuuALqJe5laEBYoFykChhSXLikZmUhccjGsPuSJ/7uPip2TJ7lwdIQwWSAi0jGZDXv4WOP8Qg65QZRuXxXw== + optionalDependencies: + msgpackr-extract "^3.0.2" + msrcrypto@^1.5.6: version "1.5.8" resolved "https://registry.yarnpkg.com/msrcrypto/-/msrcrypto-1.5.8.tgz#be419be4945bf134d8af52e9d43be7fa261f4a1c" @@ -6362,6 +6503,13 @@ node-fetch@^3.2.10: fetch-blob "^3.1.4" formdata-polyfill "^4.0.10" +node-gyp-build-optional-packages@5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz#522f50c2d53134d7f3a76cd7255de4ab6c96a3a4" + integrity sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw== + dependencies: + detect-libc "^2.0.1" + node-gyp-build@^4.2.1: version "4.5.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" @@ -6955,6 +7103,18 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== + dependencies: + redis-errors "^1.0.0" + ref-array-di@1.2.2, ref-array-di@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/ref-array-di/-/ref-array-di-1.2.2.tgz#ceee9d667d9c424b5a91bb813457cc916fb1f64d" @@ -7190,6 +7350,11 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== +semver@^7.5.2: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + semver@^7.5.3, semver@^7.5.4: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" @@ -7387,6 +7552,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + static-eval@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.0.2.tgz#2d1759306b1befa688938454c546b7871f806a42" @@ -7853,6 +8023,11 @@ tslib@2.6.2, tslib@^2.6.2: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tslib@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -8075,6 +8250,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@^8.3.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + uuid@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"