From 7e6970db1a94a1afe702e49c7a91dd85e190fa28 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 14 Nov 2023 22:39:07 -0300 Subject: [PATCH 01/15] feat!: initial live mode support + message repository refactor Signed-off-by: Ariel Gentile --- packages/core/src/agent/Agent.ts | 6 +- packages/core/src/agent/MessageSender.ts | 22 ++- packages/core/src/agent/TransportService.ts | 26 ++- .../core/src/agent/__tests__/Agent.test.ts | 11 +- .../src/agent/__tests__/MessageSender.test.ts | 18 +- .../agent/__tests__/TransportService.test.ts | 7 +- packages/core/src/constants.ts | 2 +- packages/core/src/index.ts | 2 +- .../MessagePickupApi.ts" | 83 +++++++++- .../MessagePickupApiOptions.ts" | 19 ++- .../MessagePickupEvents.ts" | 22 +++ .../MessagePickupModule.ts" | 13 +- .../MessagePickupModuleConfig.ts" | 12 +- .../MessagePickupSession.ts" | 12 ++ .../modules/message-p\303\254ckup/index.ts" | 1 + .../protocol/BaseMessagePickupProtocol.ts" | 19 ++- .../protocol/MessagePickupProtocol.ts" | 19 ++- .../protocol/MessagePickupProtocolOptions.ts" | 19 +++ .../protocol/v1/V1MessagePickupProtocol.ts" | 67 ++++++-- .../protocol/v2/V2MessagePickupProtocol.ts" | 154 ++++++++++++++---- .../V2MessagePickupProtocol.test.ts" | 103 +++++++----- .../handlers/V2LiveDeliveryChangeHandler.ts" | 19 +++ .../protocol/v2/handlers/index.ts" | 1 + .../messages/V2LiveDeliveryChangeMessage.ts" | 31 ++++ .../v2/messages/V2MessageDeliveryMessage.ts" | 10 +- .../v2/messages/V2MessagesReceivedMessage.ts" | 5 +- .../protocol/v2/messages/index.ts" | 1 + .../services/MessagePickupSessionService.ts" | 97 +++++++++++ .../message-p\303\254ckup/services/index.ts" | 1 + .../InMemoryMessagePickupRepository.ts" | 65 ++++++++ .../storage/MessagePickupRepository.ts" | 14 ++ .../MessagePickupRepositoryOptions.ts" | 27 +++ .../storage/QueuedMessage.ts" | 7 + .../storage/QueuedMessageState.ts" | 5 + .../message-p\303\254ckup/storage/index.ts" | 5 + .../modules/routing/MediationRecipientApi.ts | 68 ++++---- .../core/src/modules/routing/MediatorApi.ts | 18 +- .../modules/routing/MediatorModuleConfig.ts | 19 +++ .../modules/routing/MediatorPickupStrategy.ts | 8 +- .../routing/MessageForwardingStrategy.ts | 13 ++ .../routing/handlers/ForwardHandler.ts | 27 +-- .../routing/services/MediatorService.ts | 48 +++++- .../__tests__/MediatorService.test.ts | 7 + .../src/storage/InMemoryMessageRepository.ts | 41 ----- .../core/src/storage/MessageRepository.ts | 11 -- .../core/src/transport/TransportEventTypes.ts | 17 ++ 46 files changed, 924 insertions(+), 278 deletions(-) create mode 100644 "packages/core/src/modules/message-p\303\254ckup/MessagePickupEvents.ts" create mode 100644 "packages/core/src/modules/message-p\303\254ckup/MessagePickupSession.ts" create mode 100644 "packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2LiveDeliveryChangeHandler.ts" create mode 100644 "packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts" create mode 100644 "packages/core/src/modules/message-p\303\254ckup/services/MessagePickupSessionService.ts" create mode 100644 "packages/core/src/modules/message-p\303\254ckup/services/index.ts" create mode 100644 "packages/core/src/modules/message-p\303\254ckup/storage/InMemoryMessagePickupRepository.ts" create mode 100644 "packages/core/src/modules/message-p\303\254ckup/storage/MessagePickupRepository.ts" create mode 100644 "packages/core/src/modules/message-p\303\254ckup/storage/MessagePickupRepositoryOptions.ts" create mode 100644 "packages/core/src/modules/message-p\303\254ckup/storage/QueuedMessage.ts" create mode 100644 "packages/core/src/modules/message-p\303\254ckup/storage/QueuedMessageState.ts" create mode 100644 "packages/core/src/modules/message-p\303\254ckup/storage/index.ts" create mode 100644 packages/core/src/modules/routing/MessageForwardingStrategy.ts delete mode 100644 packages/core/src/storage/InMemoryMessageRepository.ts delete mode 100644 packages/core/src/storage/MessageRepository.ts diff --git a/packages/core/src/agent/Agent.ts b/packages/core/src/agent/Agent.ts index 952e610c82..e3c0fda86d 100644 --- a/packages/core/src/agent/Agent.ts +++ b/packages/core/src/agent/Agent.ts @@ -16,7 +16,6 @@ import { JwsService } from '../crypto/JwsService' import { AriesFrameworkError } from '../error' import { DependencyManager } from '../plugins' import { DidCommMessageRepository, StorageUpdateService, StorageVersionRepository } from '../storage' -import { InMemoryMessageRepository } from '../storage/InMemoryMessageRepository' import { AgentConfig } from './AgentConfig' import { extendModulesWithDefaultModules } from './AgentModules' @@ -90,9 +89,6 @@ export class Agent extends BaseAge "Missing required dependency: 'StorageService'. You can register it using one of the provided modules such as the AskarModule or the IndySdkModule, or implement your own." ) } - if (!dependencyManager.isRegistered(InjectionSymbols.MessageRepository)) { - dependencyManager.registerSingleton(InjectionSymbols.MessageRepository, InMemoryMessageRepository) - } // TODO: contextCorrelationId for base wallet // Bind the default agent context to the container for use in modules etc. @@ -197,6 +193,8 @@ export class Agent extends BaseAge ) await this.mediationRecipient.provision(mediationConnection) } + + await this.messagePickup.initialize() await this.mediator.initialize() await this.mediationRecipient.initialize() diff --git a/packages/core/src/agent/MessageSender.ts b/packages/core/src/agent/MessageSender.ts index 0faf514b83..1ac095b556 100644 --- a/packages/core/src/agent/MessageSender.ts +++ b/packages/core/src/agent/MessageSender.ts @@ -5,21 +5,21 @@ import type { TransportSession } from './TransportService' import type { AgentContext } from './context' import type { ConnectionRecord } from '../modules/connections' import type { ResolvedDidCommService } from '../modules/didcomm' -import type { DidDocument } from '../modules/dids' import type { OutOfBandRecord } from '../modules/oob/repository' import type { OutboundTransport } from '../transport/OutboundTransport' -import type { OutboundPackage, EncryptedMessage } from '../types' +import type { EncryptedMessage, OutboundPackage } from '../types' import { DID_COMM_TRANSPORT_QUEUE, InjectionSymbols } from '../constants' import { ReturnRouteTypes } from '../decorators/transport/TransportDecorator' import { AriesFrameworkError, MessageSendingError } from '../error' import { Logger } from '../logger' import { DidCommDocumentService } from '../modules/didcomm' +import { DidKey, type DidDocument } from '../modules/dids' import { getKeyFromVerificationMethod } from '../modules/dids/domain/key-type' import { didKeyToInstanceOfKey } from '../modules/dids/helpers' import { DidResolverService } from '../modules/dids/services/DidResolverService' +import { MessagePickupRepository } from '../modules/message-pìckup/storage' import { inject, injectable } from '../plugins' -import { MessageRepository } from '../storage/MessageRepository' import { MessageValidator } from '../utils/MessageValidator' import { getProtocolScheme } from '../utils/uri' @@ -38,7 +38,7 @@ export interface TransportPriorityOptions { export class MessageSender { private envelopeService: EnvelopeService private transportService: TransportService - private messageRepository: MessageRepository + private messagePickupRepository: MessagePickupRepository private logger: Logger private didResolverService: DidResolverService private didCommDocumentService: DidCommDocumentService @@ -48,7 +48,7 @@ export class MessageSender { public constructor( envelopeService: EnvelopeService, transportService: TransportService, - @inject(InjectionSymbols.MessageRepository) messageRepository: MessageRepository, + @inject(InjectionSymbols.MessagePickupRepository) messagePickupRepository: MessagePickupRepository, @inject(InjectionSymbols.Logger) logger: Logger, didResolverService: DidResolverService, didCommDocumentService: DidCommDocumentService, @@ -56,7 +56,7 @@ export class MessageSender { ) { this.envelopeService = envelopeService this.transportService = transportService - this.messageRepository = messageRepository + this.messagePickupRepository = messagePickupRepository this.logger = logger this.didResolverService = didResolverService this.didCommDocumentService = didCommDocumentService @@ -176,7 +176,7 @@ export class MessageSender { // If the other party shared a queue service endpoint in their did doc we queue the message if (queueService) { this.logger.debug(`Queue packed message for connection ${connection.id} (${connection.theirLabel})`) - await this.messageRepository.add(connection.id, encryptedMessage) + await this.messagePickupRepository.addMessage({ connectionId: connection.id, payload: encryptedMessage }) return } @@ -327,7 +327,13 @@ export class MessageSender { } const encryptedMessage = await this.envelopeService.packMessage(agentContext, message, keys) - await this.messageRepository.add(connection.id, encryptedMessage) + for (const recipientKey of keys.recipientKeys) { + await this.messagePickupRepository.addMessage({ + connectionId: connection.id, + recipientKey: new DidKey(recipientKey).did, + payload: encryptedMessage, + }) + } this.emitMessageSentEvent(outboundMessageContext, OutboundMessageSendStatus.QueuedForPickup) diff --git a/packages/core/src/agent/TransportService.ts b/packages/core/src/agent/TransportService.ts index 4095cb7e9b..cf281b77b9 100644 --- a/packages/core/src/agent/TransportService.ts +++ b/packages/core/src/agent/TransportService.ts @@ -1,16 +1,27 @@ import type { AgentMessage } from './AgentMessage' import type { EnvelopeKeys } from './EnvelopeService' -import type { AgentContext } from './context' import type { DidDocument } from '../modules/dids' +import type { TransportSessionRemovedEvent, TransportSessionSavedEvent } from '../transport' import type { EncryptedMessage } from '../types' import { DID_COMM_TRANSPORT_QUEUE } from '../constants' import { AriesFrameworkError } from '../error' import { injectable } from '../plugins' +import { TransportEventTypes } from '../transport' + +import { EventEmitter } from './EventEmitter' +import { AgentContext } from './context' @injectable() export class TransportService { public transportSessionTable: TransportSessionTable = {} + private agentContext: AgentContext + private eventEmitter: EventEmitter + + public constructor(agentContext: AgentContext, eventEmitter: EventEmitter) { + this.agentContext = agentContext + this.eventEmitter = eventEmitter + } public saveSession(session: TransportSession) { if (session.connectionId) { @@ -22,6 +33,13 @@ export class TransportService { }) } this.transportSessionTable[session.id] = session + + this.eventEmitter.emit(this.agentContext, { + type: TransportEventTypes.TransportSessionSaved, + payload: { + session, + }, + }) } public findSessionByConnectionId(connectionId: string) { @@ -47,6 +65,12 @@ export class TransportService { public removeSession(session: TransportSession) { delete this.transportSessionTable[session.id] + this.eventEmitter.emit(this.agentContext, { + type: TransportEventTypes.TransportSessionRemoved, + payload: { + session, + }, + }) } private getExistingSessionsForConnectionIdAndType(connectionId: string, type: string) { diff --git a/packages/core/src/agent/__tests__/Agent.test.ts b/packages/core/src/agent/__tests__/Agent.test.ts index d11331d6ff..6493a7ebab 100644 --- a/packages/core/src/agent/__tests__/Agent.test.ts +++ b/packages/core/src/agent/__tests__/Agent.test.ts @@ -13,7 +13,7 @@ import { ConnectionService } from '../../modules/connections/services/Connection import { TrustPingService } from '../../modules/connections/services/TrustPingService' import { CredentialRepository } from '../../modules/credentials' import { CredentialsApi } from '../../modules/credentials/CredentialsApi' -import { MessagePickupApi } from '../../modules/message-pìckup' +import { MessagePickupApi, InMemoryMessagePickupRepository } from '../../modules/message-pìckup' import { ProofRepository } from '../../modules/proofs' import { ProofsApi } from '../../modules/proofs/ProofsApi' import { @@ -24,7 +24,6 @@ import { MediationRecipientApi, MediationRecipientModule, } from '../../modules/routing' -import { InMemoryMessageRepository } from '../../storage/InMemoryMessageRepository' import { WalletError } from '../../wallet/error' import { Agent } from '../Agent' import { Dispatcher } from '../Dispatcher' @@ -179,7 +178,9 @@ describe('Agent', () => { // Symbols, interface based expect(container.resolve(InjectionSymbols.Logger)).toBe(agentOptions.config.logger) - expect(container.resolve(InjectionSymbols.MessageRepository)).toBeInstanceOf(InMemoryMessageRepository) + expect(container.resolve(InjectionSymbols.MessagePickupRepository)).toBeInstanceOf( + InMemoryMessagePickupRepository + ) // Agent expect(container.resolve(MessageSender)).toBeInstanceOf(MessageSender) @@ -217,8 +218,8 @@ describe('Agent', () => { // Symbols, interface based expect(container.resolve(InjectionSymbols.Logger)).toBe(container.resolve(InjectionSymbols.Logger)) - expect(container.resolve(InjectionSymbols.MessageRepository)).toBe( - container.resolve(InjectionSymbols.MessageRepository) + expect(container.resolve(InjectionSymbols.MessagePickupRepository)).toBe( + container.resolve(InjectionSymbols.MessagePickupRepository) ) expect(container.resolve(InjectionSymbols.StorageService)).toBe( container.resolve(InjectionSymbols.StorageService) diff --git a/packages/core/src/agent/__tests__/MessageSender.test.ts b/packages/core/src/agent/__tests__/MessageSender.test.ts index 6c96bf1008..04ff6450a2 100644 --- a/packages/core/src/agent/__tests__/MessageSender.test.ts +++ b/packages/core/src/agent/__tests__/MessageSender.test.ts @@ -2,7 +2,7 @@ import type { ConnectionRecord } from '../../modules/connections' import type { ResolvedDidCommService } from '../../modules/didcomm' import type { DidDocumentService } from '../../modules/dids' -import type { MessageRepository } from '../../storage/MessageRepository' +import type { MessagePickupRepository } from '../../modules/message-pìckup/storage' import type { OutboundTransport } from '../../transport' import type { EncryptedMessage } from '../../types' import type { AgentMessageSentEvent } from '../Events' @@ -24,7 +24,7 @@ import { DidCommDocumentService } from '../../modules/didcomm' import { DidResolverService, DidDocument, VerificationMethod } from '../../modules/dids' import { DidCommV1Service } from '../../modules/dids/domain/service/DidCommV1Service' import { verkeyToInstanceOfKey } from '../../modules/dids/helpers' -import { InMemoryMessageRepository } from '../../storage/InMemoryMessageRepository' +import { InMemoryMessagePickupRepository } from '../../modules/message-pìckup/storage' import { EnvelopeService as EnvelopeServiceImpl } from '../EnvelopeService' import { EventEmitter } from '../EventEmitter' import { AgentEventTypes } from '../Events' @@ -114,7 +114,7 @@ describe('MessageSender', () => { sessionWithoutKeys.inboundMessage = inboundMessage sessionWithoutKeys.send = jest.fn() - const transportService = new TransportService() + const transportService = new TransportService(getAgentContext(), eventEmitter) const transportServiceFindSessionMock = mockFunction(transportService.findSessionByConnectionId) const transportServiceFindSessionByIdMock = mockFunction(transportService.findSessionById) const transportServiceHasInboundEndpoint = mockFunction(transportService.hasInboundEndpoint) @@ -132,7 +132,7 @@ describe('MessageSender', () => { let messageSender: MessageSender let outboundTransport: OutboundTransport - let messageRepository: MessageRepository + let messagePickupRepository: MessagePickupRepository let connection: ConnectionRecord let outboundMessageContext: OutboundMessageContext const agentConfig = getAgentConfig('MessageSender') @@ -147,11 +147,11 @@ describe('MessageSender', () => { eventEmitter.on(AgentEventTypes.AgentMessageSent, eventListenerMock) outboundTransport = new DummyHttpOutboundTransport() - messageRepository = new InMemoryMessageRepository(agentConfig.logger) + messagePickupRepository = new InMemoryMessagePickupRepository(agentConfig.logger) messageSender = new MessageSender( enveloperService, transportService, - messageRepository, + messagePickupRepository, logger, didResolverService, didCommDocumentService, @@ -496,7 +496,7 @@ describe('MessageSender', () => { messageSender = new MessageSender( enveloperService, transportService, - new InMemoryMessageRepository(agentConfig.logger), + new InMemoryMessagePickupRepository(agentConfig.logger), logger, didResolverService, didCommDocumentService, @@ -635,11 +635,11 @@ describe('MessageSender', () => { describe('packMessage', () => { beforeEach(() => { outboundTransport = new DummyHttpOutboundTransport() - messageRepository = new InMemoryMessageRepository(agentConfig.logger) + messagePickupRepository = new InMemoryMessagePickupRepository(agentConfig.logger) messageSender = new MessageSender( enveloperService, transportService, - messageRepository, + messagePickupRepository, logger, didResolverService, didCommDocumentService, diff --git a/packages/core/src/agent/__tests__/TransportService.test.ts b/packages/core/src/agent/__tests__/TransportService.test.ts index f46707fa3d..fdfbc57bf9 100644 --- a/packages/core/src/agent/__tests__/TransportService.test.ts +++ b/packages/core/src/agent/__tests__/TransportService.test.ts @@ -1,5 +1,8 @@ -import { getMockConnection } from '../../../tests/helpers' +import { Subject } from 'rxjs' + +import { agentDependencies, getAgentContext, getMockConnection } from '../../../tests/helpers' import { DidExchangeRole } from '../../modules/connections' +import { EventEmitter } from '../EventEmitter' import { TransportService } from '../TransportService' import { DummyTransportSession } from './stubs' @@ -9,7 +12,7 @@ describe('TransportService', () => { let transportService: TransportService beforeEach(() => { - transportService = new TransportService() + transportService = new TransportService(getAgentContext(), new EventEmitter(agentDependencies, new Subject())) }) test(`remove session saved for a given connection`, () => { diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 0c67d367f6..f7d43876ab 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -1,5 +1,5 @@ export const InjectionSymbols = { - MessageRepository: Symbol('MessageRepository'), + MessagePickupRepository: Symbol('MessagePickupRepository'), StorageService: Symbol('StorageService'), Logger: Symbol('Logger'), AgentContextProvider: Symbol('AgentContextProvider'), diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 46b4dbddc1..2e4a214579 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -33,7 +33,7 @@ export { DidCommMimeType, KeyDerivationMethod } from './types' export type { FileSystem, DownloadToFileOptions } from './storage/FileSystem' export * from './storage/BaseRecord' export { DidCommMessageRecord, DidCommMessageRole, DidCommMessageRepository } from './storage/didcomm' -export { InMemoryMessageRepository } from './storage/InMemoryMessageRepository' +export { InMemoryMessagePickupRepository } from './modules/message-pìckup' export { Repository } from './storage/Repository' export * from './storage/RepositoryEvents' export { StorageService, Query, SimpleQuery, BaseRecordConstructor } from './storage/StorageService' diff --git "a/packages/core/src/modules/message-p\303\254ckup/MessagePickupApi.ts" "b/packages/core/src/modules/message-p\303\254ckup/MessagePickupApi.ts" index d47521dc44..65d87b2ed8 100644 --- "a/packages/core/src/modules/message-p\303\254ckup/MessagePickupApi.ts" +++ "b/packages/core/src/modules/message-p\303\254ckup/MessagePickupApi.ts" @@ -1,12 +1,15 @@ import type { + DeliverQueuedMessagesOptions, PickupMessagesOptions, PickupMessagesReturnType, QueueMessageOptions, QueueMessageReturnType, + SetLiveDeliveryModeOptions, + SetLiveDeliveryModeReturnType, } from './MessagePickupApiOptions' import type { V1MessagePickupProtocol, V2MessagePickupProtocol } from './protocol' import type { MessagePickupProtocol } from './protocol/MessagePickupProtocol' -import type { MessageRepository } from '../../storage/MessageRepository' +import type { MessagePickupRepository } from './storage/MessagePickupRepository' import { AgentContext } from '../../agent' import { MessageSender } from '../../agent/MessageSender' @@ -18,6 +21,7 @@ import { inject, injectable } from '../../plugins' import { ConnectionService } from '../connections/services' import { MessagePickupModuleConfig } from './MessagePickupModuleConfig' +import { MessagePickupSessionService } from './services/MessagePickupSessionService' export interface MessagePickupApi { queueMessage(options: QueueMessageOptions): Promise @@ -33,12 +37,14 @@ export class MessagePickupApi, @inject(InjectionSymbols.Logger) logger: Logger ) { @@ -46,9 +52,14 @@ export class MessagePickupApi(protocolVersion: MPP): MessagePickupProtocol { const protocol = this.config.protocols.find((protocol) => protocol.version === protocolVersion) @@ -66,13 +77,52 @@ export class MessagePickupApi { this.logger.debug('Queuing message...') - const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) + const { connectionId, message, recipientKey } = options + const connectionRecord = await this.connectionService.getById(this.agentContext, connectionId) - const messageRepository = this.agentContext.dependencyManager.resolve( - InjectionSymbols.MessageRepository + const messagePickupRepository = this.agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository ) - await messageRepository.add(connectionRecord.id, options.message) + await messagePickupRepository.addMessage({ connectionId: connectionRecord.id, recipientKey, payload: message }) + } + + /** + * Deliver messages in the Message Pickup Queue for a given connection and key (if specified). + * + * Note that this is only available when there is an active session with the recipient. Message + * Pickup protocol messages themselves are not added to pickup queue + * + */ + public async deliverQueuedMessages(options: DeliverQueuedMessagesOptions) { + this.logger.debug('Deliverying queried message...') + + const { connectionId, recipientKey } = options + const connectionRecord = await this.connectionService.getById(this.agentContext, connectionId) + + const activePickupSession = this.messagePickupSessionService.getLiveSession(this.agentContext, { + connectionId: connectionRecord.id, + }) + + if (activePickupSession) { + const protocol = this.getProtocol(activePickupSession.protocolVersion) + + const deliverMessagesReturn = await protocol.deliverMessages(this.agentContext, { + connectionRecord, + recipientKey, + }) + + if (deliverMessagesReturn) { + await this.messageSender.sendMessage( + new OutboundMessageContext(deliverMessagesReturn.message, { + agentContext: this.agentContext, + connection: connectionRecord, + }) + ) + } + } else { + this.logger.debug('No live mode session') + } } /** @@ -98,4 +148,27 @@ export class MessagePickupApi { + const connectionRecord = await this.connectionService.getById(this.agentContext, options.connectionId) + + const protocol = this.getProtocol(options.protocolVersion) + const { message } = await protocol.setLiveDeliveryMode(this.agentContext, { + connectionRecord, + liveDelivery: options.liveDelivery, + }) + + await this.messageSender.sendMessage( + new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection: connectionRecord, + }) + ) + } } diff --git "a/packages/core/src/modules/message-p\303\254ckup/MessagePickupApiOptions.ts" "b/packages/core/src/modules/message-p\303\254ckup/MessagePickupApiOptions.ts" index 1f8d54e264..ada988c09c 100644 --- "a/packages/core/src/modules/message-p\303\254ckup/MessagePickupApiOptions.ts" +++ "b/packages/core/src/modules/message-p\303\254ckup/MessagePickupApiOptions.ts" @@ -2,15 +2,22 @@ import type { MessagePickupProtocol } from './protocol/MessagePickupProtocol' import type { EncryptedMessage } from '../../types' /** - * Get the supported protocol versions based on the provided discover features services. + * Get the supported protocol versions based on the provided message pickup protocols */ export type MessagePickupProtocolVersionType = MPPs[number]['version'] export interface QueueMessageOptions { connectionId: string + recipientKey?: string message: EncryptedMessage } +export interface DeliverQueuedMessagesOptions { + connectionId: string + recipientKey?: string + batchSize?: number +} + export interface PickupMessagesOptions { connectionId: string protocolVersion: MessagePickupProtocolVersionType @@ -18,6 +25,16 @@ export interface PickupMessagesOptions { + connectionId: string + protocolVersion: MessagePickupProtocolVersionType + liveDelivery: boolean +} + export type QueueMessageReturnType = void export type PickupMessagesReturnType = void + +export type DeliverMessagesReturnType = void + +export type SetLiveDeliveryModeReturnType = void diff --git "a/packages/core/src/modules/message-p\303\254ckup/MessagePickupEvents.ts" "b/packages/core/src/modules/message-p\303\254ckup/MessagePickupEvents.ts" new file mode 100644 index 0000000000..ff0e893629 --- /dev/null +++ "b/packages/core/src/modules/message-p\303\254ckup/MessagePickupEvents.ts" @@ -0,0 +1,22 @@ +import type { MessagePickupSession } from './MessagePickupSession' +import type { QueuedMessage } from './storage' +import type { BaseEvent } from '../../agent/Events' + +export enum MessagePickupEventTypes { + LiveSessionSaved = 'LiveSessionSaved', + LiveSessionRemoved = 'LiveSessionRemoved', +} + +export interface MessagePickupLiveSessionSavedEvent extends BaseEvent { + type: typeof MessagePickupEventTypes.LiveSessionSaved + payload: { + session: MessagePickupSession + } +} + +export interface MessagePickupLiveSessionRemovedEvent extends BaseEvent { + type: typeof MessagePickupEventTypes.LiveSessionRemoved + payload: { + session: MessagePickupSession + } +} diff --git "a/packages/core/src/modules/message-p\303\254ckup/MessagePickupModule.ts" "b/packages/core/src/modules/message-p\303\254ckup/MessagePickupModule.ts" index 5cf4540625..364959dd2c 100644 --- "a/packages/core/src/modules/message-p\303\254ckup/MessagePickupModule.ts" +++ "b/packages/core/src/modules/message-p\303\254ckup/MessagePickupModule.ts" @@ -10,6 +10,7 @@ import { InjectionSymbols } from '../../constants' import { MessagePickupApi } from './MessagePickupApi' import { MessagePickupModuleConfig } from './MessagePickupModuleConfig' import { V1MessagePickupProtocol, V2MessagePickupProtocol } from './protocol' +import { InMemoryMessagePickupRepository } from './storage' /** * Default protocols that will be registered if the `protocols` property is not configured. @@ -38,7 +39,7 @@ export class MessagePickupModule { @@ -50,8 +50,8 @@ export class MessagePickupModuleConfig = { + connectionId: string + protocolVersion: MessagePickupProtocolVersionType + role: MessagePickupSessionRole +} diff --git "a/packages/core/src/modules/message-p\303\254ckup/index.ts" "b/packages/core/src/modules/message-p\303\254ckup/index.ts" index b4745b6037..86b3c1915f 100644 --- "a/packages/core/src/modules/message-p\303\254ckup/index.ts" +++ "b/packages/core/src/modules/message-p\303\254ckup/index.ts" @@ -3,3 +3,4 @@ export * from './MessagePickupApiOptions' export * from './MessagePickupModule' export * from './MessagePickupModuleConfig' export * from './protocol' +export * from './storage' diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/BaseMessagePickupProtocol.ts" "b/packages/core/src/modules/message-p\303\254ckup/protocol/BaseMessagePickupProtocol.ts" index ebbd6fde39..a6d5f52703 100644 --- "a/packages/core/src/modules/message-p\303\254ckup/protocol/BaseMessagePickupProtocol.ts" +++ "b/packages/core/src/modules/message-p\303\254ckup/protocol/BaseMessagePickupProtocol.ts" @@ -1,5 +1,12 @@ import type { MessagePickupProtocol } from './MessagePickupProtocol' -import type { PickupMessagesProtocolOptions, PickupMessagesProtocolReturnType } from './MessagePickupProtocolOptions' +import type { + DeliverMessagesProtocolOptions, + DeliverMessagesProtocolReturnType, + PickupMessagesProtocolOptions, + PickupMessagesProtocolReturnType, + SetLiveDeliveryModeProtocolOptions, + SetLiveDeliveryModeProtocolReturnType, +} from './MessagePickupProtocolOptions' import type { AgentContext } from '../../../agent' import type { AgentMessage } from '../../../agent/AgentMessage' import type { FeatureRegistry } from '../../../agent/FeatureRegistry' @@ -17,5 +24,15 @@ export abstract class BaseMessagePickupProtocol implements MessagePickupProtocol options: PickupMessagesProtocolOptions ): Promise> + public abstract deliverMessages( + agentContext: AgentContext, + options: DeliverMessagesProtocolOptions + ): Promise | void> + + public abstract setLiveDeliveryMode( + agentContext: AgentContext, + options: SetLiveDeliveryModeProtocolOptions + ): Promise> + public abstract register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void } diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/MessagePickupProtocol.ts" "b/packages/core/src/modules/message-p\303\254ckup/protocol/MessagePickupProtocol.ts" index 9acdcf5e4d..d4ea5f8861 100644 --- "a/packages/core/src/modules/message-p\303\254ckup/protocol/MessagePickupProtocol.ts" +++ "b/packages/core/src/modules/message-p\303\254ckup/protocol/MessagePickupProtocol.ts" @@ -1,4 +1,11 @@ -import type { PickupMessagesProtocolOptions, PickupMessagesProtocolReturnType } from './MessagePickupProtocolOptions' +import type { + DeliverMessagesProtocolOptions, + DeliverMessagesProtocolReturnType, + PickupMessagesProtocolOptions, + PickupMessagesProtocolReturnType, + SetLiveDeliveryModeProtocolOptions, + SetLiveDeliveryModeProtocolReturnType, +} from './MessagePickupProtocolOptions' import type { AgentContext } from '../../../agent' import type { AgentMessage } from '../../../agent/AgentMessage' import type { FeatureRegistry } from '../../../agent/FeatureRegistry' @@ -12,5 +19,15 @@ export interface MessagePickupProtocol { options: PickupMessagesProtocolOptions ): Promise> + deliverMessages( + agentContext: AgentContext, + options: DeliverMessagesProtocolOptions + ): Promise | void> + + setLiveDeliveryMode( + agentContext: AgentContext, + options: SetLiveDeliveryModeProtocolOptions + ): Promise> + register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void } diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/MessagePickupProtocolOptions.ts" "b/packages/core/src/modules/message-p\303\254ckup/protocol/MessagePickupProtocolOptions.ts" index 9f3f252c6a..37b8feac30 100644 --- "a/packages/core/src/modules/message-p\303\254ckup/protocol/MessagePickupProtocolOptions.ts" +++ "b/packages/core/src/modules/message-p\303\254ckup/protocol/MessagePickupProtocolOptions.ts" @@ -7,6 +7,25 @@ export interface PickupMessagesProtocolOptions { batchSize?: number } +export interface DeliverMessagesProtocolOptions { + connectionRecord: ConnectionRecord + recipientKey?: string + batchSize?: number +} + +export interface SetLiveDeliveryModeProtocolOptions { + connectionRecord: ConnectionRecord + liveDelivery: boolean +} + export type PickupMessagesProtocolReturnType = { message: MessageType } + +export type DeliverMessagesProtocolReturnType = { + message: MessageType +} + +export type SetLiveDeliveryModeProtocolReturnType = { + message: MessageType +} diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v1/V1MessagePickupProtocol.ts" "b/packages/core/src/modules/message-p\303\254ckup/protocol/v1/V1MessagePickupProtocol.ts" index 581d0d31a7..33954964ea 100644 --- "a/packages/core/src/modules/message-p\303\254ckup/protocol/v1/V1MessagePickupProtocol.ts" +++ "b/packages/core/src/modules/message-p\303\254ckup/protocol/v1/V1MessagePickupProtocol.ts" @@ -3,11 +3,19 @@ import type { AgentMessage } from '../../../../agent/AgentMessage' import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' import type { DependencyManager } from '../../../../plugins' -import type { MessageRepository } from '../../../../storage/MessageRepository' -import type { PickupMessagesProtocolOptions, PickupMessagesProtocolReturnType } from '../MessagePickupProtocolOptions' +import type { MessagePickupRepository } from '../../storage/MessagePickupRepository' +import type { + DeliverMessagesProtocolOptions, + DeliverMessagesProtocolReturnType, + PickupMessagesProtocolOptions, + PickupMessagesProtocolReturnType, + SetLiveDeliveryModeProtocolOptions, + SetLiveDeliveryModeProtocolReturnType, +} from '../MessagePickupProtocolOptions' import { OutboundMessageContext, Protocol } from '../../../../agent/models' import { InjectionSymbols } from '../../../../constants' +import { AriesFrameworkError } from '../../../../error' import { injectable } from '../../../../plugins' import { MessagePickupModuleConfig } from '../../MessagePickupModuleConfig' import { BaseMessagePickupProtocol } from '../BaseMessagePickupProtocol' @@ -17,10 +25,6 @@ import { V1BatchMessage, BatchMessageMessage, V1BatchPickupMessage } from './mes @injectable() export class V1MessagePickupProtocol extends BaseMessagePickupProtocol { - public constructor() { - super() - } - /** * The version of the message pickup protocol this class supports */ @@ -55,24 +59,63 @@ export class V1MessagePickupProtocol extends BaseMessagePickupProtocol { return { message } } + public async deliverMessages( + agentContext: AgentContext, + options: DeliverMessagesProtocolOptions + ): Promise | void> { + const { connectionRecord, batchSize } = options + connectionRecord.assertReady() + + const pickupMessageQueue = agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository + ) + + const messages = await pickupMessageQueue.takeFromQueue({ + connectionId: connectionRecord.id, + limit: batchSize, // TODO: Define as config parameter for message holder side + }) + + const batchMessages = messages.map( + (msg) => + new BatchMessageMessage({ + id: msg.id, + message: msg.encryptedMessage, + }) + ) + + if (messages.length > 0) { + const message = new V1BatchMessage({ + messages: batchMessages, + }) + + return { message } + } + } + + public async setLiveDeliveryMode( + agentContext: AgentContext, + options: SetLiveDeliveryModeProtocolOptions + ): Promise> { + throw new AriesFrameworkError('Live Delivery mode not supported in Message Pickup V1 protocol') + } + public async processBatchPickup(messageContext: InboundMessageContext) { // Assert ready connection const connection = messageContext.assertReadyConnection() const { message } = messageContext - const messageRepository = messageContext.agentContext.dependencyManager.resolve( - InjectionSymbols.MessageRepository + const pickupMessageQueue = messageContext.agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository ) - const messages = await messageRepository.takeFromQueue(connection.id, message.batchSize) + const messages = await pickupMessageQueue.takeFromQueue({ connectionId: connection.id, limit: message.batchSize }) - // TODO: each message should be stored with an id. to be able to conform to the id property - // of batch message const batchMessages = messages.map( (msg) => new BatchMessageMessage({ - message: msg, + id: msg.id, + message: msg.encryptedMessage, }) ) diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/V2MessagePickupProtocol.ts" "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/V2MessagePickupProtocol.ts" index b9dc22ae7e..00e8818812 100644 --- "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/V2MessagePickupProtocol.ts" +++ "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/V2MessagePickupProtocol.ts" @@ -4,9 +4,16 @@ import type { AgentMessageReceivedEvent } from '../../../../agent/Events' import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' import type { DependencyManager } from '../../../../plugins' -import type { MessageRepository } from '../../../../storage/MessageRepository' import type { EncryptedMessage } from '../../../../types' -import type { PickupMessagesProtocolOptions, PickupMessagesProtocolReturnType } from '../MessagePickupProtocolOptions' +import type { MessagePickupRepository } from '../../storage/MessagePickupRepository' +import type { + DeliverMessagesProtocolOptions, + DeliverMessagesProtocolReturnType, + PickupMessagesProtocolOptions, + PickupMessagesProtocolReturnType, + SetLiveDeliveryModeProtocolOptions, + SetLiveDeliveryModeProtocolReturnType, +} from '../MessagePickupProtocolOptions' import { EventEmitter } from '../../../../agent/EventEmitter' import { AgentEventTypes } from '../../../../agent/Events' @@ -14,16 +21,19 @@ import { MessageSender } from '../../../../agent/MessageSender' import { OutboundMessageContext, Protocol } from '../../../../agent/models' import { InjectionSymbols } from '../../../../constants' import { Attachment } from '../../../../decorators/attachment/Attachment' -import { AriesFrameworkError } from '../../../../error' import { injectable } from '../../../../plugins' import { ConnectionService } from '../../../connections' +import { verkeyToDidKey } from '../../../dids/helpers' import { ProblemReportError } from '../../../problem-reports' import { RoutingProblemReportReason } from '../../../routing/error' import { MessagePickupModuleConfig } from '../../MessagePickupModuleConfig' +import { MessagePickupSessionRole } from '../../MessagePickupSession' +import { MessagePickupSessionService } from '../../services' import { BaseMessagePickupProtocol } from '../BaseMessagePickupProtocol' import { V2DeliveryRequestHandler, + V2LiveDeliveryChangeHandler, V2MessageDeliveryHandler, V2MessagesReceivedHandler, V2StatusHandler, @@ -35,14 +45,11 @@ import { V2DeliveryRequestMessage, V2MessagesReceivedMessage, V2StatusRequestMessage, + V2LiveDeliveryChangeMessage, } from './messages' @injectable() export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { - public constructor() { - super() - } - /** * The version of the message pickup protocol this class supports */ @@ -58,6 +65,7 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { new V2MessagesReceivedHandler(this), new V2StatusHandler(this), new V2MessageDeliveryHandler(this), + new V2LiveDeliveryChangeHandler(this), ]) featureRegistry.register( @@ -82,21 +90,73 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { return { message } } + public async deliverMessages( + agentContext: AgentContext, + options: DeliverMessagesProtocolOptions + ): Promise | void> { + const { connectionRecord, recipientKey } = options + connectionRecord.assertReady() + + const messagePickupRepository = agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository + ) + + // Get available messages from queue, but don't delete them + const messages = await messagePickupRepository.takeFromQueue({ + connectionId: connectionRecord.id, + recipientKey: recipientKey, + limit: 10, // TODO: Define as config parameter + keepMessages: true, + }) + + const attachments = messages.map( + (msg) => + new Attachment({ + id: msg.id, + data: { + json: msg.encryptedMessage, + }, + }) + ) + + if (messages.length > 0) { + return { + message: new V2MessageDeliveryMessage({ + attachments, + }), + } + } + } + + public async setLiveDeliveryMode( + agentContext: AgentContext, + options: SetLiveDeliveryModeProtocolOptions + ): Promise> { + const { connectionRecord, liveDelivery } = options + connectionRecord.assertReady() + return { + message: new V2LiveDeliveryChangeMessage({ + liveDelivery, + }), + } + } + public async processStatusRequest(messageContext: InboundMessageContext) { // Assert ready connection const connection = messageContext.assertReadyConnection() + const recipientKey = messageContext.message.recipientKey - const messageRepository = messageContext.agentContext.dependencyManager.resolve( - InjectionSymbols.MessageRepository + const messagePickupRepository = messageContext.agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository ) - if (messageContext.message.recipientKey) { - throw new AriesFrameworkError('recipient_key parameter not supported') - } - const statusMessage = new V2StatusMessage({ threadId: messageContext.message.threadId, - messageCount: await messageRepository.getAvailableMessageCount(connection.id), + recipientKey, + messageCount: await messagePickupRepository.getAvailableMessageCount({ + connectionId: connection.id, + recipientKey: recipientKey ? verkeyToDidKey(recipientKey) : undefined, + }), }) return new OutboundMessageContext(statusMessage, { agentContext: messageContext.agentContext, connection }) @@ -105,27 +165,28 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { public async processDeliveryRequest(messageContext: InboundMessageContext) { // Assert ready connection const connection = messageContext.assertReadyConnection() - - if (messageContext.message.recipientKey) { - throw new AriesFrameworkError('recipient_key parameter not supported') - } + const recipientKey = messageContext.message.recipientKey const { message } = messageContext - const messageRepository = messageContext.agentContext.dependencyManager.resolve( - InjectionSymbols.MessageRepository + const messagePickupRepository = messageContext.agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository ) // Get available messages from queue, but don't delete them - const messages = await messageRepository.takeFromQueue(connection.id, message.limit, true) + const messages = await messagePickupRepository.takeFromQueue({ + connectionId: connection.id, + recipientKey: recipientKey ? verkeyToDidKey(recipientKey) : undefined, + limit: message.limit, + keepMessages: true, + }) - // TODO: each message should be stored with an id. to be able to conform to the id property - // of delivery message const attachments = messages.map( (msg) => new Attachment({ + id: msg.id, data: { - json: msg, + json: msg.encryptedMessage, }, }) ) @@ -134,10 +195,12 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { messages.length > 0 ? new V2MessageDeliveryMessage({ threadId: messageContext.message.threadId, + recipientKey, attachments, }) : new V2StatusMessage({ threadId: messageContext.message.threadId, + recipientKey, messageCount: 0, }) @@ -150,19 +213,17 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { const { message } = messageContext - const messageRepository = messageContext.agentContext.dependencyManager.resolve( - InjectionSymbols.MessageRepository + const messageRepository = messageContext.agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository ) - // TODO: Add Queued Message ID - await messageRepository.takeFromQueue( - connection.id, - message.messageIdList ? message.messageIdList.length : undefined - ) + if (message.messageIdList.length) { + await messageRepository.removeMessages({ connectionId: connection.id, messageIds: message.messageIdList }) + } const statusMessage = new V2StatusMessage({ threadId: messageContext.message.threadId, - messageCount: await messageRepository.getAvailableMessageCount(connection.id), + messageCount: await messageRepository.getAvailableMessageCount({ connectionId: connection.id }), }) return new OutboundMessageContext(statusMessage, { agentContext: messageContext.agentContext, connection }) @@ -221,6 +282,35 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { return deliveryRequestMessage } + public async processLiveDeliveryChange(messageContext: InboundMessageContext) { + const { agentContext, message } = messageContext + + const connection = messageContext.assertReadyConnection() + + const messagePickupRepository = messageContext.agentContext.dependencyManager.resolve( + InjectionSymbols.MessagePickupRepository + ) + const sessionService = messageContext.agentContext.dependencyManager.resolve(MessagePickupSessionService) + + if (message.liveDelivery) { + sessionService.saveLiveSession(agentContext, { + connectionId: connection.id, + protocolVersion: 'v2', + role: MessagePickupSessionRole.MessageHolder, + }) + } else { + sessionService.removeLiveSession(agentContext, { connectionId: connection.id }) + } + + const statusMessage = new V2StatusMessage({ + threadId: message.threadId, + liveDelivery: message.liveDelivery, + messageCount: await messagePickupRepository.getAvailableMessageCount({ connectionId: connection.id }), + }) + + return new OutboundMessageContext(statusMessage, { agentContext: messageContext.agentContext, connection }) + } + public async processDelivery(messageContext: InboundMessageContext) { messageContext.assertReadyConnection() diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts" "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts" index 50476217f9..424552bb00 100644 --- "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts" +++ "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts" @@ -8,11 +8,12 @@ import { InboundMessageContext } from '../../../../../agent/models/InboundMessag import { InjectionSymbols } from '../../../../../constants' import { Attachment } from '../../../../../decorators/attachment/Attachment' import { AriesFrameworkError } from '../../../../../error' -import { InMemoryMessageRepository } from '../../../../../storage/InMemoryMessageRepository' import { uuid } from '../../../../../utils/uuid' import { DidExchangeState, TrustPingMessage } from '../../../../connections' import { ConnectionService } from '../../../../connections/services/ConnectionService' +import { verkeyToDidKey } from '../../../../dids/helpers' import { MessagePickupModuleConfig } from '../../../MessagePickupModuleConfig' +import { InMemoryMessagePickupRepository } from '../../../storage/InMemoryMessagePickupRepository' import { V1MessagePickupProtocol } from '../../v1' import { V2MessagePickupProtocol } from '../V2MessagePickupProtocol' import { @@ -28,13 +29,13 @@ const mockConnection = getMockConnection({ }) // Mock classes -jest.mock('../../../../../storage/InMemoryMessageRepository') +jest.mock('../../../storage/InMemoryMessagePickupQueue') jest.mock('../../../../../agent/EventEmitter') jest.mock('../../../../../agent/MessageSender') jest.mock('../../../../connections/services/ConnectionService') // Mock typed object -const InMessageRepositoryMock = InMemoryMessageRepository as jest.Mock +const InMessageRepositoryMock = InMemoryMessagePickupRepository as jest.Mock const EventEmitterMock = EventEmitter as jest.Mock const MessageSenderMock = MessageSender as jest.Mock const ConnectionServiceMock = ConnectionService as jest.Mock @@ -46,11 +47,11 @@ const messagePickupModuleConfig = new MessagePickupModuleConfig({ const messageSender = new MessageSenderMock() const eventEmitter = new EventEmitterMock() const connectionService = new ConnectionServiceMock() -const messageRepository = new InMessageRepositoryMock() +const messagePickupRepository = new InMessageRepositoryMock() const agentContext = getAgentContext({ registerInstances: [ - [InjectionSymbols.MessageRepository, messageRepository], + [InjectionSymbols.MessagePickupRepository, messagePickupRepository], [EventEmitter, eventEmitter], [MessageSender, messageSender], [ConnectionService, connectionService], @@ -64,7 +65,11 @@ const encryptedMessage: EncryptedMessage = { ciphertext: 'base64url', tag: 'base64url', } -const queuedMessages = [encryptedMessage, encryptedMessage, encryptedMessage] +const queuedMessages = [ + { id: '1', encryptedMessage }, + { id: '2', encryptedMessage }, + { id: '3', encryptedMessage }, +] describe('V2MessagePickupService', () => { let pickupProtocol: V2MessagePickupProtocol @@ -75,7 +80,7 @@ describe('V2MessagePickupService', () => { describe('processStatusRequest', () => { test('no available messages in queue', async () => { - mockFunction(messageRepository.getAvailableMessageCount).mockResolvedValue(0) + mockFunction(messagePickupRepository.getAvailableMessageCount).mockResolvedValue(0) const statusRequest = new V2StatusRequestMessage({}) @@ -91,11 +96,11 @@ describe('V2MessagePickupService', () => { messageCount: 0, }) ) - expect(messageRepository.getAvailableMessageCount).toHaveBeenCalledWith(mockConnection.id) + expect(messagePickupRepository.getAvailableMessageCount).toHaveBeenCalledWith({ connectionId: mockConnection.id }) }) test('multiple messages in queue', async () => { - mockFunction(messageRepository.getAvailableMessageCount).mockResolvedValue(5) + mockFunction(messagePickupRepository.getAvailableMessageCount).mockResolvedValue(5) const statusRequest = new V2StatusRequestMessage({}) const messageContext = new InboundMessageContext(statusRequest, { connection: mockConnection, agentContext }) @@ -110,27 +115,26 @@ describe('V2MessagePickupService', () => { messageCount: 5, }) ) - expect(messageRepository.getAvailableMessageCount).toHaveBeenCalledWith(mockConnection.id) + expect(messagePickupRepository.getAvailableMessageCount).toHaveBeenCalledWith({ connectionId: mockConnection.id }) }) test('status request specifying recipient key', async () => { - mockFunction(messageRepository.getAvailableMessageCount).mockResolvedValue(10) + mockFunction(messagePickupRepository.getAvailableMessageCount).mockResolvedValue(10) const statusRequest = new V2StatusRequestMessage({ - recipientKey: 'recipientKey', + recipientKey: '79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', }) const messageContext = new InboundMessageContext(statusRequest, { connection: mockConnection, agentContext }) - await expect(pickupProtocol.processStatusRequest(messageContext)).rejects.toThrowError( - 'recipient_key parameter not supported' - ) + await pickupProtocol.processStatusRequest(messageContext) + expect(messagePickupRepository.getAvailableMessageCount).toHaveBeenCalledWith({ connectionId: mockConnection.id }) }) }) describe('processDeliveryRequest', () => { test('no available messages in queue', async () => { - mockFunction(messageRepository.takeFromQueue).mockReturnValue([]) + mockFunction(messagePickupRepository.takeFromQueue).mockReturnValue([]) const deliveryRequest = new V2DeliveryRequestMessage({ limit: 10 }) @@ -146,11 +150,15 @@ describe('V2MessagePickupService', () => { messageCount: 0, }) ) - expect(messageRepository.takeFromQueue).toHaveBeenCalledWith(mockConnection.id, 10, true) + expect(messagePickupRepository.takeFromQueue).toHaveBeenCalledWith({ + connectionId: mockConnection.id, + limit: 10, + keepMessages: true, + }) }) test('less messages in queue than limit', async () => { - mockFunction(messageRepository.takeFromQueue).mockReturnValue(queuedMessages) + mockFunction(messagePickupRepository.takeFromQueue).mockReturnValue(queuedMessages) const deliveryRequest = new V2DeliveryRequestMessage({ limit: 10 }) @@ -166,18 +174,23 @@ describe('V2MessagePickupService', () => { expect.arrayContaining( queuedMessages.map((msg) => expect.objectContaining({ + id: msg.id, data: { - json: msg, + json: msg.encryptedMessage, }, }) ) ) ) - expect(messageRepository.takeFromQueue).toHaveBeenCalledWith(mockConnection.id, 10, true) + expect(messagePickupRepository.takeFromQueue).toHaveBeenCalledWith({ + connectionId: mockConnection.id, + limit: 10, + keepMessages: true, + }) }) test('more messages in queue than limit', async () => { - mockFunction(messageRepository.takeFromQueue).mockReturnValue(queuedMessages.slice(0, 2)) + mockFunction(messagePickupRepository.takeFromQueue).mockReturnValue(queuedMessages.slice(0, 2)) const deliveryRequest = new V2DeliveryRequestMessage({ limit: 2 }) @@ -193,36 +206,46 @@ describe('V2MessagePickupService', () => { expect.arrayContaining( queuedMessages.slice(0, 2).map((msg) => expect.objectContaining({ + id: msg.id, data: { - json: msg, + json: msg.encryptedMessage, }, }) ) ) ) - expect(messageRepository.takeFromQueue).toHaveBeenCalledWith(mockConnection.id, 2, true) + expect(messagePickupRepository.takeFromQueue).toHaveBeenCalledWith({ + connectionId: mockConnection.id, + limit: 2, + keepMessages: true, + }) }) test('delivery request specifying recipient key', async () => { - mockFunction(messageRepository.takeFromQueue).mockReturnValue(queuedMessages) + mockFunction(messagePickupRepository.takeFromQueue).mockReturnValue(queuedMessages) - const statusRequest = new V2DeliveryRequestMessage({ + const deliveryRequest = new V2DeliveryRequestMessage({ limit: 10, recipientKey: 'recipientKey', }) - const messageContext = new InboundMessageContext(statusRequest, { connection: mockConnection, agentContext }) + const messageContext = new InboundMessageContext(deliveryRequest, { connection: mockConnection, agentContext }) - await expect(pickupProtocol.processStatusRequest(messageContext)).rejects.toThrowError( - 'recipient_key parameter not supported' - ) + await pickupProtocol.processDeliveryRequest(messageContext) + + expect(messagePickupRepository.takeFromQueue).toHaveBeenCalledWith({ + connectionId: mockConnection.id, + limit: 10, + recipientKey: verkeyToDidKey('recipientKey'), + keepMessages: true, + }) }) }) describe('processMessagesReceived', () => { test('messages received partially', async () => { - mockFunction(messageRepository.takeFromQueue).mockReturnValue(queuedMessages) - mockFunction(messageRepository.getAvailableMessageCount).mockResolvedValue(4) + mockFunction(messagePickupRepository.takeFromQueue).mockReturnValue(queuedMessages) + mockFunction(messagePickupRepository.getAvailableMessageCount).mockResolvedValue(4) const messagesReceived = new V2MessagesReceivedMessage({ messageIdList: ['1', '2'], @@ -240,13 +263,16 @@ describe('V2MessagePickupService', () => { messageCount: 4, }) ) - expect(messageRepository.getAvailableMessageCount).toHaveBeenCalledWith(mockConnection.id) - expect(messageRepository.takeFromQueue).toHaveBeenCalledWith(mockConnection.id, 2) + expect(messagePickupRepository.getAvailableMessageCount).toHaveBeenCalledWith({ connectionId: mockConnection.id }) + expect(messagePickupRepository.removeMessages).toHaveBeenCalledWith({ + connectionId: mockConnection.id, + messageIds: ['1', '2'], + }) }) test('all messages have been received', async () => { - mockFunction(messageRepository.takeFromQueue).mockReturnValue(queuedMessages) - mockFunction(messageRepository.getAvailableMessageCount).mockResolvedValue(0) + mockFunction(messagePickupRepository.takeFromQueue).mockReturnValue(queuedMessages) + mockFunction(messagePickupRepository.getAvailableMessageCount).mockResolvedValue(0) const messagesReceived = new V2MessagesReceivedMessage({ messageIdList: ['1', '2'], @@ -265,8 +291,11 @@ describe('V2MessagePickupService', () => { }) ) - expect(messageRepository.getAvailableMessageCount).toHaveBeenCalledWith(mockConnection.id) - expect(messageRepository.takeFromQueue).toHaveBeenCalledWith(mockConnection.id, 2) + expect(messagePickupRepository.getAvailableMessageCount).toHaveBeenCalledWith({ connectionId: mockConnection.id }) + expect(messagePickupRepository.removeMessages).toHaveBeenCalledWith({ + connectionId: mockConnection.id, + messageIds: ['1', '2'], + }) }) }) diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2LiveDeliveryChangeHandler.ts" "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2LiveDeliveryChangeHandler.ts" new file mode 100644 index 0000000000..30eeaf035f --- /dev/null +++ "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2LiveDeliveryChangeHandler.ts" @@ -0,0 +1,19 @@ +import type { MessageHandler } from '../../../../../agent/MessageHandler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { V2MessagePickupProtocol } from '../V2MessagePickupProtocol' + +import { V2LiveDeliveryChangeMessage } from '../messages' + +export class V2LiveDeliveryChangeHandler implements MessageHandler { + public supportedMessages = [V2LiveDeliveryChangeMessage] + private messagePickupService: V2MessagePickupProtocol + + public constructor(messagePickupService: V2MessagePickupProtocol) { + this.messagePickupService = messagePickupService + } + + public async handle(messageContext: InboundMessageContext) { + messageContext.assertReadyConnection() + return this.messagePickupService.processLiveDeliveryChange(messageContext) + } +} diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/index.ts" "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/index.ts" index 5f54b56ac7..f8a173669b 100644 --- "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/index.ts" +++ "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/index.ts" @@ -1,4 +1,5 @@ export * from './V2DeliveryRequestHandler' +export * from './V2LiveDeliveryChangeHandler' export * from './V2MessageDeliveryHandler' export * from './V2MessagesReceivedHandler' export * from './V2StatusHandler' diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts" "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts" new file mode 100644 index 0000000000..4d38fc2409 --- /dev/null +++ "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts" @@ -0,0 +1,31 @@ +import { Expose } from 'class-transformer' +import { IsBoolean } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { ReturnRouteTypes } from '../../../../../decorators/transport/TransportDecorator' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export interface V2LiveDeliveryChangeMessageOptions { + id?: string + liveDelivery: boolean +} + +export class V2LiveDeliveryChangeMessage extends AgentMessage { + public constructor(options: V2LiveDeliveryChangeMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.liveDelivery = options.liveDelivery + } + this.setReturnRouting(ReturnRouteTypes.all) + } + + @IsValidMessageType(V2LiveDeliveryChangeMessage.type) + public readonly type = V2LiveDeliveryChangeMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/2.0/live-delivery-change') + + @IsBoolean() + @Expose({ name: 'live_delivery' }) + public liveDelivery!: boolean +} diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2MessageDeliveryMessage.ts" "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2MessageDeliveryMessage.ts" index 48783f634b..073efa1819 100644 --- "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2MessageDeliveryMessage.ts" +++ "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2MessageDeliveryMessage.ts" @@ -10,7 +10,7 @@ import { IsValidMessageType, parseMessageType } from '../../../../../utils/messa export interface V2MessageDeliveryMessageOptions { id?: string recipientKey?: string - threadId: string + threadId?: string attachments: Attachment[] } @@ -22,9 +22,11 @@ export class V2MessageDeliveryMessage extends AgentMessage { this.id = options.id || this.generateId() this.recipientKey = options.recipientKey this.appendedAttachments = options.attachments - this.setThread({ - threadId: options.threadId, - }) + if (this.threadId) { + this.setThread({ + threadId: options.threadId, + }) + } } this.setReturnRouting(ReturnRouteTypes.all) } diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2MessagesReceivedMessage.ts" "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2MessagesReceivedMessage.ts" index 23da433de6..f6e9d49d41 100644 --- "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2MessagesReceivedMessage.ts" +++ "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2MessagesReceivedMessage.ts" @@ -1,5 +1,5 @@ import { Expose } from 'class-transformer' -import { IsArray, IsOptional } from 'class-validator' +import { IsArray } from 'class-validator' import { AgentMessage } from '../../../../../agent/AgentMessage' import { ReturnRouteTypes } from '../../../../../decorators/transport/TransportDecorator' @@ -26,7 +26,6 @@ export class V2MessagesReceivedMessage extends AgentMessage { public static readonly type = parseMessageType('https://didcomm.org/messagepickup/2.0/messages-received') @IsArray() - @IsOptional() @Expose({ name: 'message_id_list' }) - public messageIdList?: string[] + public messageIdList!: string[] } diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/index.ts" "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/index.ts" index 4746216ec0..df70290a6f 100644 --- "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/index.ts" +++ "b/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/index.ts" @@ -1,4 +1,5 @@ export * from './V2DeliveryRequestMessage' +export * from './V2LiveDeliveryChangeMessage' export * from './V2MessageDeliveryMessage' export * from './V2MessagesReceivedMessage' export * from './V2StatusMessage' diff --git "a/packages/core/src/modules/message-p\303\254ckup/services/MessagePickupSessionService.ts" "b/packages/core/src/modules/message-p\303\254ckup/services/MessagePickupSessionService.ts" new file mode 100644 index 0000000000..cde670e090 --- /dev/null +++ "b/packages/core/src/modules/message-p\303\254ckup/services/MessagePickupSessionService.ts" @@ -0,0 +1,97 @@ +import type { AgentContext } from '../../../agent' +import type { TransportSessionRemovedEvent } from '../../../transport' +import type { MessagePickupLiveSessionRemovedEvent } from '../MessagePickupEvents' +import type { MessagePickupSession, MessagePickupSessionRole } from '../MessagePickupSession' + +import { takeUntil, type Subject } from 'rxjs' + +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { injectable } from '../../../plugins' +import { TransportEventTypes } from '../../../transport' +import { MessagePickupEventTypes } from '../MessagePickupEvents' + +/** + * @internal + * The Message Pickup session service keeps track of all {@link MessagePickupSession} + * + * It is initially intended for Message Holder/Mediator role, where only Live Mode sessions are + * considered. + */ +@injectable() +export class MessagePickupSessionService { + private sessions: MessagePickupSession[] + + public constructor() { + this.sessions = [] + } + + public start(agentContext: AgentContext) { + const stop$ = agentContext.dependencyManager.resolve>(InjectionSymbols.Stop$) + const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + this.sessions = [] + + eventEmitter + .observable(TransportEventTypes.TransportSessionRemoved) + .pipe(takeUntil(stop$)) + .subscribe({ + next: (e) => { + const connectionId = e.payload.session.connectionId + if (connectionId) this.removeLiveSession(agentContext, { connectionId }) + }, + }) + } + + public getLiveSession( + agentContext: AgentContext, + options: { connectionId: string; role?: MessagePickupSessionRole } + ) { + const { connectionId, role } = options + + return this.sessions.find( + (session) => session.connectionId === connectionId && (role === undefined || role === session.role) + ) + } + + public saveLiveSession( + agentContext: AgentContext, + options: { connectionId: string; protocolVersion: string; role: MessagePickupSessionRole } + ) { + const { connectionId, protocolVersion, role } = options + + // First remove any live session for the given connection Id + this.removeLiveSession(agentContext, { connectionId }) + + const session = { + connectionId, + protocolVersion, + role, + } + + this.sessions.push(session) + + const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + eventEmitter.emit(agentContext, { + type: MessagePickupEventTypes.LiveSessionRemoved, + payload: { + session, + }, + }) + } + + public removeLiveSession(agentContext: AgentContext, options: { connectionId: string }) { + const itemIndex = this.sessions.findIndex((session) => session.connectionId === options.connectionId) + + if (itemIndex > -1) { + const [session] = this.sessions.splice(itemIndex, 1) + const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + + eventEmitter.emit(agentContext, { + type: MessagePickupEventTypes.LiveSessionRemoved, + payload: { + session, + }, + }) + } + } +} diff --git "a/packages/core/src/modules/message-p\303\254ckup/services/index.ts" "b/packages/core/src/modules/message-p\303\254ckup/services/index.ts" new file mode 100644 index 0000000000..e91435bfaf --- /dev/null +++ "b/packages/core/src/modules/message-p\303\254ckup/services/index.ts" @@ -0,0 +1 @@ +export * from './MessagePickupSessionService' diff --git "a/packages/core/src/modules/message-p\303\254ckup/storage/InMemoryMessagePickupRepository.ts" "b/packages/core/src/modules/message-p\303\254ckup/storage/InMemoryMessagePickupRepository.ts" new file mode 100644 index 0000000000..4aa657d92e --- /dev/null +++ "b/packages/core/src/modules/message-p\303\254ckup/storage/InMemoryMessagePickupRepository.ts" @@ -0,0 +1,65 @@ +import type { MessagePickupRepository } from './MessagePickupRepository' +import type { + AddMessageOptions, + GetAvailableMessageCountOptions, + RemoveMessagesOptions, + TakeFromQueueOptions, +} from './MessagePickupRepositoryOptions' +import type { QueuedMessage } from './QueuedMessage' + +import { InjectionSymbols } from '../../../constants' +import { Logger } from '../../../logger' +import { injectable, inject } from '../../../plugins' +import { uuid } from '../../../utils/uuid' + +@injectable() +export class InMemoryMessagePickupRepository implements MessagePickupRepository { + private logger: Logger + private messages: { [key: string]: QueuedMessage[] } = {} + + public constructor(@inject(InjectionSymbols.Logger) logger: Logger) { + this.logger = logger + } + + public getAvailableMessageCount(options: GetAvailableMessageCountOptions): number | Promise { + const { connectionId } = options + return this.messages[connectionId] ? this.messages[connectionId].length : 0 + } + + public takeFromQueue(options: TakeFromQueueOptions) { + const { connectionId, limit, keepMessages } = options + + if (!this.messages[connectionId]) { + return [] + } + + const messagesToTake = limit ?? this.messages[connectionId].length + this.logger.debug(`Taking ${messagesToTake} messages from queue for connection ${connectionId}`) + + return keepMessages + ? this.messages[connectionId].slice(0, messagesToTake) + : this.messages[connectionId].splice(0, messagesToTake) + } + + public addMessage(options: AddMessageOptions) { + const { connectionId, payload } = options + if (!this.messages[connectionId]) { + this.messages[connectionId] = [] + } + + const id = uuid() + this.messages[connectionId].push({ id, encryptedMessage: payload }) + return id + } + + public removeMessages(options: RemoveMessagesOptions): void | Promise { + const { connectionId, messageIds } = options + + if (!this.messages[connectionId]) return + + for (const messageId of messageIds) { + const messageIndex = this.messages[connectionId].findIndex((item) => item.id === messageId) + if (messageIndex > -1) this.messages[connectionId].splice(messageIndex, 1) + } + } +} diff --git "a/packages/core/src/modules/message-p\303\254ckup/storage/MessagePickupRepository.ts" "b/packages/core/src/modules/message-p\303\254ckup/storage/MessagePickupRepository.ts" new file mode 100644 index 0000000000..6b234918ce --- /dev/null +++ "b/packages/core/src/modules/message-p\303\254ckup/storage/MessagePickupRepository.ts" @@ -0,0 +1,14 @@ +import type { + AddMessageOptions, + GetAvailableMessageCountOptions, + RemoveMessagesOptions, + TakeFromQueueOptions, +} from './MessagePickupRepositoryOptions' +import type { QueuedMessage } from './QueuedMessage' + +export interface MessagePickupRepository { + getAvailableMessageCount(options: GetAvailableMessageCountOptions): number | Promise + takeFromQueue(options: TakeFromQueueOptions): QueuedMessage[] | Promise + addMessage(options: AddMessageOptions): string | Promise + removeMessages(options: RemoveMessagesOptions): void | Promise +} diff --git "a/packages/core/src/modules/message-p\303\254ckup/storage/MessagePickupRepositoryOptions.ts" "b/packages/core/src/modules/message-p\303\254ckup/storage/MessagePickupRepositoryOptions.ts" new file mode 100644 index 0000000000..90dbfef93e --- /dev/null +++ "b/packages/core/src/modules/message-p\303\254ckup/storage/MessagePickupRepositoryOptions.ts" @@ -0,0 +1,27 @@ +import type { QueuedMessageState } from './QueuedMessageState' +import type { EncryptedMessage } from '../../../types' + +export interface GetAvailableMessageCountOptions { + connectionId: string + recipientKey?: string + state?: QueuedMessageState +} + +export interface TakeFromQueueOptions { + connectionId: string + recipientKey?: string + limit?: number + keepMessages?: boolean +} + +export interface AddMessageOptions { + connectionId: string + recipientKey?: string + payload: EncryptedMessage +} + +export interface RemoveMessagesOptions { + connectionId: string + recipientKey?: string + messageIds: string[] +} diff --git "a/packages/core/src/modules/message-p\303\254ckup/storage/QueuedMessage.ts" "b/packages/core/src/modules/message-p\303\254ckup/storage/QueuedMessage.ts" new file mode 100644 index 0000000000..dae90e7d5f --- /dev/null +++ "b/packages/core/src/modules/message-p\303\254ckup/storage/QueuedMessage.ts" @@ -0,0 +1,7 @@ +import type { EncryptedMessage } from '../../../types' + +export type QueuedMessage = { + id: string + encryptedMessage: EncryptedMessage + state?: string +} diff --git "a/packages/core/src/modules/message-p\303\254ckup/storage/QueuedMessageState.ts" "b/packages/core/src/modules/message-p\303\254ckup/storage/QueuedMessageState.ts" new file mode 100644 index 0000000000..dc4065d85a --- /dev/null +++ "b/packages/core/src/modules/message-p\303\254ckup/storage/QueuedMessageState.ts" @@ -0,0 +1,5 @@ +export enum QueuedMessageState { + Pending = 'pending', + Sending = 'sending', + Delivered = 'delivered', +} diff --git "a/packages/core/src/modules/message-p\303\254ckup/storage/index.ts" "b/packages/core/src/modules/message-p\303\254ckup/storage/index.ts" new file mode 100644 index 0000000000..6c733de292 --- /dev/null +++ "b/packages/core/src/modules/message-p\303\254ckup/storage/index.ts" @@ -0,0 +1,5 @@ +export * from './InMemoryMessagePickupRepository' +export * from './MessagePickupRepository' +export * from './MessagePickupRepositoryOptions' +export * from './QueuedMessage' +export * from './QueuedMessageState' diff --git a/packages/core/src/modules/routing/MediationRecipientApi.ts b/packages/core/src/modules/routing/MediationRecipientApi.ts index dc8a1023c1..3a1c9242e9 100644 --- a/packages/core/src/modules/routing/MediationRecipientApi.ts +++ b/packages/core/src/modules/routing/MediationRecipientApi.ts @@ -149,7 +149,14 @@ export class MediationRecipientApi { ) } - private async openWebSocketAndPickUp(mediator: MediationRecord, pickupStrategy: MediatorPickupStrategy) { + /** + * Keep track of a persistent transport session with a mediator, trying to reconnect to it as + * soon as it is disconnected, using a recursive back-off strategy + * + * @param mediator mediation record + * @param pickupStrategy chosen pick up strategy (should be Implicit or PickUp in Live Mode) + */ + private async monitorMediatorWebSocketEvents(mediator: MediationRecord, pickupStrategy: MediatorPickupStrategy) { const { baseMediatorReconnectionIntervalMs, maximumMediatorReconnectionIntervalMs } = this.config let interval = baseMediatorReconnectionIntervalMs @@ -197,11 +204,11 @@ export class MediationRecipientApi { `Websocket connection to mediator with connectionId '${mediator.connectionId}' is closed, attempting to reconnect...` ) try { - if (pickupStrategy === MediatorPickupStrategy.PickUpV2) { - // Start Pickup v2 protocol to receive messages received while websocket offline - await this.messagePickupApi.pickupMessages({ + if (pickupStrategy === MediatorPickupStrategy.PickUpV2LiveMode) { + // Start Pickup v2 protocol in live mode (retrieve any queued message before) + await this.messagePickupApi.setLiveDeliveryMode({ connectionId: mediator.connectionId, - batchSize: this.config.maximumMessagePickup, + liveDelivery: true, protocolVersion: 'v2', }) } else { @@ -213,13 +220,6 @@ export class MediationRecipientApi { }, complete: () => this.logger.info(`Stopping pickup of messages from mediator '${mediator.id}'`), }) - try { - if (pickupStrategy === MediatorPickupStrategy.Implicit) { - await this.openMediationWebSocket(mediator) - } - } catch (error) { - this.logger.warn('Unable to open websocket connection to mediator', { error }) - } } /** @@ -242,18 +242,10 @@ export class MediationRecipientApi { const mediatorConnection = await this.connectionService.getById(this.agentContext, mediatorRecord.connectionId) switch (mediatorPickupStrategy) { - case MediatorPickupStrategy.PickUpV2: - this.logger.info(`Starting pickup of messages from mediator '${mediatorRecord.id}'`) - await this.openWebSocketAndPickUp(mediatorRecord, mediatorPickupStrategy) - await this.messagePickupApi.pickupMessages({ - connectionId: mediatorConnection.id, - batchSize: this.config.maximumMessagePickup, - protocolVersion: 'v2', - }) - break - case MediatorPickupStrategy.PickUpV1: { + case MediatorPickupStrategy.PickUpV1: + case MediatorPickupStrategy.PickUpV2: { const stopConditions$ = merge(this.stop$, this.stopMessagePickup$).pipe() - // Explicit means polling every X seconds with batch message + // PickUpV1/PickUpV2 means polling every X seconds with batch message this.logger.info(`Starting explicit (batch) pickup of messages from mediator '${mediatorRecord.id}'`) const subscription = interval(mediatorPollingInterval) .pipe(takeUntil(stopConditions$)) @@ -262,18 +254,30 @@ export class MediationRecipientApi { await this.messagePickupApi.pickupMessages({ connectionId: mediatorConnection.id, batchSize: this.config.maximumMessagePickup, - protocolVersion: 'v1', + protocolVersion: mediatorPickupStrategy === MediatorPickupStrategy.PickUpV2 ? 'v2' : 'v1', }) }, complete: () => this.logger.info(`Stopping pickup of messages from mediator '${mediatorRecord.id}'`), }) return subscription } + case MediatorPickupStrategy.PickUpV2LiveMode: + // PickUp V2 in Live Mode will retrieve queued messages and then set up live delivery mode + this.logger.info(`Starting pickup of messages from mediator '${mediatorRecord.id}'`) + await this.monitorMediatorWebSocketEvents(mediatorRecord, mediatorPickupStrategy) + await this.messagePickupApi.setLiveDeliveryMode({ + connectionId: mediatorConnection.id, + liveDelivery: true, + protocolVersion: 'v2', + }) + + break case MediatorPickupStrategy.Implicit: // Implicit means sending ping once and keeping connection open. This requires a long-lived transport // such as WebSockets to work this.logger.info(`Starting implicit pickup of messages from mediator '${mediatorRecord.id}'`) - await this.openWebSocketAndPickUp(mediatorRecord, mediatorPickupStrategy) + await this.monitorMediatorWebSocketEvents(mediatorRecord, mediatorPickupStrategy) + await this.openMediationWebSocket(mediatorRecord) break default: this.logger.info(`Skipping pickup of messages from mediator '${mediatorRecord.id}' due to pickup strategy none`) @@ -329,20 +333,6 @@ export class MediationRecipientApi { return this.mediationRecipientService.discoverMediation(this.agentContext) } - /** - * @deprecated Use `MessagePickupApi.pickupMessages` instead. - * */ - public async pickupMessages(mediatorConnection: ConnectionRecord, pickupStrategy?: MediatorPickupStrategy) { - mediatorConnection.assertReady() - - const messagePickupApi = this.agentContext.dependencyManager.resolve(MessagePickupApi) - - await messagePickupApi.pickupMessages({ - connectionId: mediatorConnection.id, - protocolVersion: pickupStrategy === MediatorPickupStrategy.PickUpV2 ? 'v2' : 'v1', - }) - } - public async setDefaultMediator(mediatorRecord: MediationRecord) { return this.mediationRecipientService.setDefaultMediator(this.agentContext, mediatorRecord) } diff --git a/packages/core/src/modules/routing/MediatorApi.ts b/packages/core/src/modules/routing/MediatorApi.ts index af1bf29e79..62c456b31c 100644 --- a/packages/core/src/modules/routing/MediatorApi.ts +++ b/packages/core/src/modules/routing/MediatorApi.ts @@ -1,5 +1,4 @@ import type { MediationRecord } from './repository' -import type { EncryptedMessage } from '../../types' import { AgentContext } from '../../agent' import { MessageHandlerRegistry } from '../../agent/MessageHandlerRegistry' @@ -7,7 +6,6 @@ import { MessageSender } from '../../agent/MessageSender' import { OutboundMessageContext } from '../../agent/models' import { injectable } from '../../plugins' import { ConnectionService } from '../connections/services' -import { MessagePickupApi } from '../message-pìckup' import { MediatorModuleConfig } from './MediatorModuleConfig' import { ForwardHandler, KeylistUpdateHandler } from './handlers' @@ -52,8 +50,8 @@ export class MediatorApi { } } - public async grantRequestedMediation(mediatorId: string): Promise { - const record = await this.mediatorService.getById(this.agentContext, mediatorId) + public async grantRequestedMediation(mediationRecordId: string): Promise { + const record = await this.mediatorService.getById(this.agentContext, mediationRecordId) const connectionRecord = await this.connectionService.getById(this.agentContext, record.connectionId) const { message, mediationRecord } = await this.mediatorService.createGrantMediationMessage( @@ -71,19 +69,9 @@ export class MediatorApi { return mediationRecord } - /** - * @deprecated Use `MessagePickupApi.queueMessage` instead. - * */ - public queueMessage(connectionId: string, message: EncryptedMessage) { - const messagePickupApi = this.agentContext.dependencyManager.resolve(MessagePickupApi) - return messagePickupApi.queueMessage({ connectionId, message }) - } - private registerMessageHandlers(messageHandlerRegistry: MessageHandlerRegistry) { messageHandlerRegistry.registerMessageHandler(new KeylistUpdateHandler(this.mediatorService)) - messageHandlerRegistry.registerMessageHandler( - new ForwardHandler(this.mediatorService, this.connectionService, this.messageSender) - ) + messageHandlerRegistry.registerMessageHandler(new ForwardHandler(this.mediatorService)) messageHandlerRegistry.registerMessageHandler(new MediationRequestHandler(this.mediatorService, this.config)) } } diff --git a/packages/core/src/modules/routing/MediatorModuleConfig.ts b/packages/core/src/modules/routing/MediatorModuleConfig.ts index 8b70d9591a..0f72359782 100644 --- a/packages/core/src/modules/routing/MediatorModuleConfig.ts +++ b/packages/core/src/modules/routing/MediatorModuleConfig.ts @@ -1,3 +1,5 @@ +import { MessageForwardingStrategy } from './MessageForwardingStrategy' + /** * MediatorModuleConfigOptions defines the interface for the options of the MediatorModuleConfig class. * This can contain optional parameters that have default values in the config class itself. @@ -9,6 +11,18 @@ export interface MediatorModuleConfigOptions { * @default false */ autoAcceptMediationRequests?: boolean + + /** + * Strategy to use when a Forward message is received. + * + * + * - `MessageForwardingStrategy.QueueOnly` - simply queue encrypted message into MessagePickupRepository. It will be in charge of manually trigering MessagePickupApi.deliver() afterwards. + * - `MessageForwardingStrategy.QueueAndDeliver` - Queue message into MessagePickupRepository and deliver it (along any other queued message). + * - `MessageForwardingStrategy.DeliverOnly` - Deliver message directly. Do not add into queue (it might be manually added after, e.g. in case of failure) + * + * @default MessageForwardingStrategy.QueueAndDeliver + */ + messageForwardingStrategy?: MessageForwardingStrategy } export class MediatorModuleConfig { @@ -22,4 +36,9 @@ export class MediatorModuleConfig { public get autoAcceptMediationRequests() { return this.options.autoAcceptMediationRequests ?? false } + + /** See {@link MediatorModuleConfigOptions.messageForwardingStrategy} */ + public get messageForwardingStrategy() { + return this.options.messageForwardingStrategy ?? MessageForwardingStrategy.QueueAndDeliver + } } diff --git a/packages/core/src/modules/routing/MediatorPickupStrategy.ts b/packages/core/src/modules/routing/MediatorPickupStrategy.ts index d4889b6ac9..1104abf7cb 100644 --- a/packages/core/src/modules/routing/MediatorPickupStrategy.ts +++ b/packages/core/src/modules/routing/MediatorPickupStrategy.ts @@ -1,10 +1,14 @@ export enum MediatorPickupStrategy { - // Explicit pickup strategy means picking up messages using the pickup protocol + // Use PickUp v1 protocol to periodically retrieve messages PickUpV1 = 'PickUpV1', - // Supports pickup v2 + // Use PickUp v2 protocol to periodically retrieve messages PickUpV2 = 'PickUpV2', + // Use PickUp v2 protocol in Live Mode to get incoming messages as soon as they arrive + // to mediator + PickUpV2LiveMode = 'PickUpV2LiveMode', + // Implicit pickup strategy means picking up messages only using return route // decorator. This is what ACA-Py currently uses Implicit = 'Implicit', diff --git a/packages/core/src/modules/routing/MessageForwardingStrategy.ts b/packages/core/src/modules/routing/MessageForwardingStrategy.ts new file mode 100644 index 0000000000..c3e253f038 --- /dev/null +++ b/packages/core/src/modules/routing/MessageForwardingStrategy.ts @@ -0,0 +1,13 @@ +export enum MessageForwardingStrategy { + // When a forward is received, simply queue encrypted message. MessagePickupRepository + // will be in charge of manually triggering MessagePickupApi.deliver() + QueueOnly = 'QueueOnly', + + // Queue message into MessagePickupRepository and, if a live mode session si active, + // deliver it along any other queued message + QueueAndDeliver = 'QueueAndDeliver', + + // Attempt to deliver message directly. Do not add into pickup queue (it might be manually + // added after, e.g. in case of failure) + DeliverOnly = 'DeliverOnly', +} diff --git a/packages/core/src/modules/routing/handlers/ForwardHandler.ts b/packages/core/src/modules/routing/handlers/ForwardHandler.ts index 960237fc45..2ff27a0dae 100644 --- a/packages/core/src/modules/routing/handlers/ForwardHandler.ts +++ b/packages/core/src/modules/routing/handlers/ForwardHandler.ts @@ -1,40 +1,17 @@ import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' -import type { MessageSender } from '../../../agent/MessageSender' -import type { ConnectionService } from '../../connections/services' import type { MediatorService } from '../services' import { ForwardMessage } from '../messages' export class ForwardHandler implements MessageHandler { private mediatorService: MediatorService - private connectionService: ConnectionService - private messageSender: MessageSender - public supportedMessages = [ForwardMessage] - public constructor( - mediatorService: MediatorService, - connectionService: ConnectionService, - messageSender: MessageSender - ) { + public constructor(mediatorService: MediatorService) { this.mediatorService = mediatorService - this.connectionService = connectionService - this.messageSender = messageSender } public async handle(messageContext: MessageHandlerInboundMessage) { - const { encryptedMessage, mediationRecord } = await this.mediatorService.processForwardMessage(messageContext) - - const connectionRecord = await this.connectionService.getById( - messageContext.agentContext, - mediationRecord.connectionId - ) - - // The message inside the forward message is packed so we just send the packed - // message to the connection associated with it - await this.messageSender.sendPackage(messageContext.agentContext, { - connection: connectionRecord, - encryptedMessage, - }) + await this.mediatorService.processForwardMessage(messageContext) } } diff --git a/packages/core/src/modules/routing/services/MediatorService.ts b/packages/core/src/modules/routing/services/MediatorService.ts index 7b3df9b861..a8d3c1dc87 100644 --- a/packages/core/src/modules/routing/services/MediatorService.ts +++ b/packages/core/src/modules/routing/services/MediatorService.ts @@ -1,12 +1,12 @@ import type { AgentContext } from '../../../agent' import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' import type { Query } from '../../../storage/StorageService' -import type { EncryptedMessage } from '../../../types' import type { ConnectionRecord } from '../../connections' import type { MediationStateChangedEvent } from '../RoutingEvents' import type { ForwardMessage, MediationRequestMessage } from '../messages' import { EventEmitter } from '../../../agent/EventEmitter' +import { MessageSender } from '../../../agent/MessageSender' import { InjectionSymbols } from '../../../constants' import { KeyType } from '../../../crypto' import { AriesFrameworkError, RecordDuplicateError } from '../../../error' @@ -15,6 +15,9 @@ import { injectable, inject } from '../../../plugins' import { ConnectionService } from '../../connections' import { ConnectionMetadataKeys } from '../../connections/repository/ConnectionMetadataTypes' import { didKeyToVerkey, isDidKey, verkeyToDidKey } from '../../dids/helpers' +import { MessagePickupApi } from '../../message-pìckup' +import { MediatorModuleConfig } from '../MediatorModuleConfig' +import { MessageForwardingStrategy } from '../MessageForwardingStrategy' import { RoutingEventTypes } from '../RoutingEvents' import { KeylistUpdateMessage, @@ -36,18 +39,21 @@ export class MediatorService { private logger: Logger private mediationRepository: MediationRepository private mediatorRoutingRepository: MediatorRoutingRepository + private messagePickupApi: MessagePickupApi private eventEmitter: EventEmitter private connectionService: ConnectionService public constructor( mediationRepository: MediationRepository, mediatorRoutingRepository: MediatorRoutingRepository, + messagePickupApi: MessagePickupApi, eventEmitter: EventEmitter, @inject(InjectionSymbols.Logger) logger: Logger, connectionService: ConnectionService ) { this.mediationRepository = mediationRepository this.mediatorRoutingRepository = mediatorRoutingRepository + this.messagePickupApi = messagePickupApi this.eventEmitter = eventEmitter this.logger = logger this.connectionService = connectionService @@ -64,10 +70,9 @@ export class MediatorService { throw new AriesFrameworkError(`Mediator has not been initialized yet.`) } - public async processForwardMessage( - messageContext: InboundMessageContext - ): Promise<{ mediationRecord: MediationRecord; encryptedMessage: EncryptedMessage }> { - const { message } = messageContext + public async processForwardMessage(messageContext: InboundMessageContext): Promise { + const { message, agentContext } = messageContext + const connection = messageContext.assertReadyConnection() // TODO: update to class-validator validation if (!message.to) { @@ -83,9 +88,36 @@ export class MediatorService { mediationRecord.assertReady() mediationRecord.assertRole(MediationRole.Mediator) - return { - encryptedMessage: message.message, - mediationRecord, + const messageForwardingStrategy = + agentContext.dependencyManager.resolve(MediatorModuleConfig).messageForwardingStrategy + const messageSender = agentContext.dependencyManager.resolve(MessageSender) + + switch (messageForwardingStrategy) { + case MessageForwardingStrategy.QueueOnly: + await this.messagePickupApi.queueMessage({ + connectionId: mediationRecord.connectionId, + recipientKey: verkeyToDidKey(message.to), + message: message.message, + }) + break + case MessageForwardingStrategy.QueueAndDeliver: + await this.messagePickupApi.queueMessage({ + connectionId: mediationRecord.connectionId, + recipientKey: verkeyToDidKey(message.to), + message: message.message, + }) + await this.messagePickupApi.deliverQueuedMessages({ + connectionId: mediationRecord.connectionId, + recipientKey: verkeyToDidKey(message.to), + }) + break + case MessageForwardingStrategy.DeliverOnly: + // The message inside the forward message is packed so we just send the packed + // message to the connection associated with it + await messageSender.sendPackage(agentContext, { + connection: connection, + encryptedMessage: message.message, + }) } } diff --git a/packages/core/src/modules/routing/services/__tests__/MediatorService.test.ts b/packages/core/src/modules/routing/services/__tests__/MediatorService.test.ts index 017de44042..baff063ec2 100644 --- a/packages/core/src/modules/routing/services/__tests__/MediatorService.test.ts +++ b/packages/core/src/modules/routing/services/__tests__/MediatorService.test.ts @@ -5,6 +5,7 @@ import { EventEmitter } from '../../../../agent/EventEmitter' import { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' import { ConnectionService, DidExchangeState } from '../../../connections' import { isDidKey } from '../../../dids/helpers' +import { MessagePickupApi } from '../../../message-pìckup' import { KeylistUpdateAction, KeylistUpdateMessage, KeylistUpdateResult } from '../../messages' import { MediationRole, MediationState } from '../../models' import { MediationRecord, MediatorRoutingRecord } from '../../repository' @@ -21,9 +22,13 @@ const MediatorRoutingRepositoryMock = MediatorRoutingRepository as jest.Mock +jest.mock('../../../connections/services/ConnectionService') +const MessagePickupApiMock = MessagePickupApi as jest.Mock + const mediationRepository = new MediationRepositoryMock() const mediatorRoutingRepository = new MediatorRoutingRepositoryMock() const connectionService = new ConnectionServiceMock() +const mediationPickupApi = new MessagePickupApiMock() const mockConnection = getMockConnection({ state: DidExchangeState.Completed, @@ -39,6 +44,7 @@ describe('MediatorService - default config', () => { const mediatorService = new MediatorService( mediationRepository, mediatorRoutingRepository, + mediationPickupApi, new EventEmitter(agentConfig.agentDependencies, new Subject()), agentConfig.logger, connectionService @@ -165,6 +171,7 @@ describe('MediatorService - useDidKeyInProtocols set to false', () => { const mediatorService = new MediatorService( mediationRepository, mediatorRoutingRepository, + mediationPickupApi, new EventEmitter(agentConfig.agentDependencies, new Subject()), agentConfig.logger, connectionService diff --git a/packages/core/src/storage/InMemoryMessageRepository.ts b/packages/core/src/storage/InMemoryMessageRepository.ts deleted file mode 100644 index cf98440a7d..0000000000 --- a/packages/core/src/storage/InMemoryMessageRepository.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { MessageRepository } from './MessageRepository' -import type { EncryptedMessage } from '../types' - -import { InjectionSymbols } from '../constants' -import { Logger } from '../logger' -import { injectable, inject } from '../plugins' - -@injectable() -export class InMemoryMessageRepository implements MessageRepository { - private logger: Logger - private messages: { [key: string]: EncryptedMessage[] } = {} - - public constructor(@inject(InjectionSymbols.Logger) logger: Logger) { - this.logger = logger - } - - public getAvailableMessageCount(connectionId: string): number | Promise { - return this.messages[connectionId] ? this.messages[connectionId].length : 0 - } - - public takeFromQueue(connectionId: string, limit?: number, keepMessages?: boolean) { - if (!this.messages[connectionId]) { - return [] - } - - const messagesToTake = limit ?? this.messages[connectionId].length - this.logger.debug(`Taking ${messagesToTake} messages from queue for connection ${connectionId}`) - - return keepMessages - ? this.messages[connectionId].slice(0, messagesToTake) - : this.messages[connectionId].splice(0, messagesToTake) - } - - public add(connectionId: string, payload: EncryptedMessage) { - if (!this.messages[connectionId]) { - this.messages[connectionId] = [] - } - - this.messages[connectionId].push(payload) - } -} diff --git a/packages/core/src/storage/MessageRepository.ts b/packages/core/src/storage/MessageRepository.ts deleted file mode 100644 index d12c7b6c07..0000000000 --- a/packages/core/src/storage/MessageRepository.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { EncryptedMessage } from '../types' - -export interface MessageRepository { - getAvailableMessageCount(connectionId: string): number | Promise - takeFromQueue( - connectionId: string, - limit?: number, - keepMessages?: boolean - ): EncryptedMessage[] | Promise - add(connectionId: string, payload: EncryptedMessage): void | Promise -} diff --git a/packages/core/src/transport/TransportEventTypes.ts b/packages/core/src/transport/TransportEventTypes.ts index 8916724e86..25dc5aef5e 100644 --- a/packages/core/src/transport/TransportEventTypes.ts +++ b/packages/core/src/transport/TransportEventTypes.ts @@ -1,8 +1,11 @@ import type { BaseEvent } from '../agent/Events' +import type { TransportSession } from '../agent/TransportService' export enum TransportEventTypes { OutboundWebSocketClosedEvent = 'OutboundWebSocketClosedEvent', OutboundWebSocketOpenedEvent = 'OutboundWebSocketOpenedEvent', + TransportSessionSaved = 'TransportSessionSaved ', + TransportSessionRemoved = 'TransportSessionSaved ', } export interface OutboundWebSocketClosedEvent extends BaseEvent { @@ -20,3 +23,17 @@ export interface OutboundWebSocketOpenedEvent extends BaseEvent { connectionId?: string } } + +export interface TransportSessionSavedEvent extends BaseEvent { + type: typeof TransportEventTypes.TransportSessionSaved + payload: { + session: TransportSession + } +} + +export interface TransportSessionRemovedEvent extends BaseEvent { + type: typeof TransportEventTypes.TransportSessionRemoved + payload: { + session: TransportSession + } +} From 2f40c6abf3f1e56990cb2bc944491cad3d5f0ed6 Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht <61358536+berendsliedrecht@users.noreply.github.com> Date: Tue, 21 Nov 2023 16:18:42 +0100 Subject: [PATCH 02/15] fix(core): allow string for did document controller (#1644) Signed-off-by: Berend Sliedrecht Signed-off-by: Ariel Gentile --- packages/core/src/modules/dids/domain/DidDocument.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/modules/dids/domain/DidDocument.ts b/packages/core/src/modules/dids/domain/DidDocument.ts index 2080d7dfb4..08963613af 100644 --- a/packages/core/src/modules/dids/domain/DidDocument.ts +++ b/packages/core/src/modules/dids/domain/DidDocument.ts @@ -26,7 +26,7 @@ interface DidDocumentOptions { context?: string | string[] id: string alsoKnownAs?: string[] - controller?: string[] + controller?: string | string[] verificationMethod?: VerificationMethod[] service?: DidDocumentService[] authentication?: Array From 664c891ddd758aa40ac25ad2c1a7bf8eb4ac6a98 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 21 Nov 2023 23:32:16 -0300 Subject: [PATCH 03/15] feat(anoncreds): issue revocable credentials (#1427) Signed-off-by: Ariel Gentile --- demo/src/Faber.ts | 1 + .../src/services/AnonCredsRsIssuerService.ts | 190 +++- .../services/AnonCredsRsVerifierService.ts | 101 +- .../tests/InMemoryTailsFileService.ts | 58 + .../anoncreds-rs/tests/LocalDidResolver.ts | 30 + .../anoncreds-rs/tests/anoncreds-flow.test.ts | 555 +++++---- packages/anoncreds-rs/tests/anoncredsSetup.ts | 561 +++++++++ .../v2-credential-revocation.e2e.test.ts | 237 ++++ .../tests/v2-credentials.e2e.test.ts | 674 +++++++++++ .../anoncreds-rs/tests/v2-proofs.e2e.test.ts | 1007 +++++++++++++++++ packages/anoncreds/src/AnonCredsApi.ts | 279 ++++- packages/anoncreds/src/AnonCredsApiOptions.ts | 28 +- packages/anoncreds/src/AnonCredsModule.ts | 4 + .../anoncreds/src/AnonCredsModuleConfig.ts | 14 + .../src/__tests__/AnonCredsModule.test.ts | 8 +- .../src/formats/AnonCredsCredentialFormat.ts | 4 + .../AnonCredsCredentialFormatService.ts | 105 +- .../src/formats/AnonCredsProofFormat.ts | 1 + .../formats/AnonCredsProofFormatService.ts | 4 +- .../formats/LegacyIndyProofFormatService.ts | 3 +- ...vocationRegistryDefinitionPrivateRecord.ts | 59 + ...tionRegistryDefinitionPrivateRepository.ts | 36 + ...CredsRevocationRegistryDefinitionRecord.ts | 46 + ...sRevocationRegistryDefinitionRepository.ts | 31 + ...onRegistryDefinitionRecordMetadataTypes.ts | 11 + packages/anoncreds/src/repository/index.ts | 4 + .../src/services/AnonCredsIssuerService.ts | 21 +- .../services/AnonCredsIssuerServiceOptions.ts | 46 +- packages/anoncreds/src/services/index.ts | 1 + .../services/registry/AnonCredsRegistry.ts | 30 +- .../RevocationRegistryDefinitionOptions.ts | 43 +- .../registry/RevocationStatusListOptions.ts | 48 +- .../services/tails/BasicTailsFileService.ts | 88 ++ .../src/services/tails/TailsFileService.ts | 47 + .../anoncreds/src/services/tails/index.ts | 2 + .../src/utils/createRequestFromPreview.ts | 9 +- .../src/utils/getRevocationRegistries.ts | 63 +- packages/anoncreds/src/utils/index.ts | 2 +- packages/anoncreds/src/utils/tails.ts | 57 - packages/anoncreds/src/utils/timestamp.ts | 2 + .../tests/InMemoryAnonCredsRegistry.ts | 111 +- packages/anoncreds/tests/anoncreds.test.ts | 1 + .../anoncreds/tests/legacyAnonCredsSetup.ts | 4 +- .../services/CheqdAnonCredsRegistry.ts | 10 + .../src/modules/credentials/CredentialsApi.ts | 54 +- .../credentials/CredentialsApiOptions.ts | 11 + .../services/RevocationNotificationService.ts | 38 +- .../RevocationNotificationServiceOptions.ts | 6 + .../revocation-notification/services/index.ts | 1 + .../util/revocationIdentifier.ts | 5 + packages/core/tests/helpers.ts | 42 + .../services/IndySdkAnonCredsRegistry.ts | 11 + .../services/IndySdkIssuerService.ts | 36 +- .../src/anoncreds/IndyVdrAnonCredsRegistry.ts | 13 +- samples/tails/.gitignore | 1 + samples/tails/FullTailsFileService.ts | 41 + samples/tails/README.md | 5 + samples/tails/package.json | 27 + samples/tails/server.ts | 132 +++ samples/tails/tsconfig.json | 6 + samples/tails/yarn.lock | 335 ++++++ yarn.lock | 57 +- 62 files changed, 5039 insertions(+), 418 deletions(-) create mode 100644 packages/anoncreds-rs/tests/InMemoryTailsFileService.ts create mode 100644 packages/anoncreds-rs/tests/LocalDidResolver.ts create mode 100644 packages/anoncreds-rs/tests/anoncredsSetup.ts create mode 100644 packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts create mode 100644 packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts create mode 100644 packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts create mode 100644 packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRepository.ts create mode 100644 packages/anoncreds/src/repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes.ts create mode 100644 packages/anoncreds/src/services/tails/BasicTailsFileService.ts create mode 100644 packages/anoncreds/src/services/tails/TailsFileService.ts create mode 100644 packages/anoncreds/src/services/tails/index.ts delete mode 100644 packages/anoncreds/src/utils/tails.ts create mode 100644 packages/anoncreds/src/utils/timestamp.ts create mode 100644 packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationServiceOptions.ts create mode 100644 samples/tails/.gitignore create mode 100644 samples/tails/FullTailsFileService.ts create mode 100644 samples/tails/README.md create mode 100644 samples/tails/package.json create mode 100644 samples/tails/server.ts create mode 100644 samples/tails/tsconfig.json create mode 100644 samples/tails/yarn.lock diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts index 91befb8918..a0bfaff828 100644 --- a/demo/src/Faber.ts +++ b/demo/src/Faber.ts @@ -176,6 +176,7 @@ export class Faber extends BaseAgent { issuerId: this.anonCredsIssuerId, tag: 'latest', }, + supportRevocation: false, options: { endorserMode: 'internal', endorserDid: this.anonCredsIssuerId, diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts index 383c6e94e7..f4c666de56 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts @@ -10,6 +10,12 @@ import type { AnonCredsCredentialDefinition, CreateCredentialDefinitionReturn, AnonCredsCredential, + CreateRevocationRegistryDefinitionOptions, + CreateRevocationRegistryDefinitionReturn, + AnonCredsRevocationRegistryDefinition, + CreateRevocationStatusListOptions, + AnonCredsRevocationStatusList, + UpdateRevocationStatusListOptions, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { CredentialDefinitionPrivate, JsonObject, KeyCorrectnessProof } from '@hyperledger/anoncreds-shared' @@ -22,9 +28,21 @@ import { AnonCredsKeyCorrectnessProofRepository, AnonCredsCredentialDefinitionPrivateRepository, AnonCredsCredentialDefinitionRepository, + AnonCredsRevocationRegistryDefinitionRepository, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryState, } from '@aries-framework/anoncreds' import { injectable, AriesFrameworkError } from '@aries-framework/core' -import { Credential, CredentialDefinition, CredentialOffer, Schema } from '@hyperledger/anoncreds-shared' +import { + RevocationStatusList, + RevocationRegistryDefinitionPrivate, + RevocationRegistryDefinition, + CredentialRevocationConfig, + Credential, + CredentialDefinition, + CredentialOffer, + Schema, +} from '@hyperledger/anoncreds-shared' import { AnonCredsRsError } from '../errors/AnonCredsRsError' @@ -83,6 +101,118 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { } } + public async createRevocationRegistryDefinition( + agentContext: AgentContext, + options: CreateRevocationRegistryDefinitionOptions + ): Promise { + const { tag, issuerId, credentialDefinition, credentialDefinitionId, maximumCredentialNumber, tailsDirectoryPath } = + options + + let createReturnObj: + | { + revocationRegistryDefinition: RevocationRegistryDefinition + revocationRegistryDefinitionPrivate: RevocationRegistryDefinitionPrivate + } + | undefined + try { + createReturnObj = RevocationRegistryDefinition.create({ + credentialDefinition: credentialDefinition as unknown as JsonObject, + credentialDefinitionId, + issuerId, + maximumCredentialNumber, + revocationRegistryType: 'CL_ACCUM', + tag, + tailsDirectoryPath, + }) + + return { + revocationRegistryDefinition: + createReturnObj.revocationRegistryDefinition.toJson() as unknown as AnonCredsRevocationRegistryDefinition, + revocationRegistryDefinitionPrivate: createReturnObj.revocationRegistryDefinitionPrivate.toJson(), + } + } finally { + createReturnObj?.revocationRegistryDefinition.handle.clear() + createReturnObj?.revocationRegistryDefinitionPrivate.handle.clear() + } + } + + public async createRevocationStatusList( + agentContext: AgentContext, + options: CreateRevocationStatusListOptions + ): Promise { + const { issuerId, revocationRegistryDefinitionId, revocationRegistryDefinition } = options + + const credentialDefinitionRecord = await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, revocationRegistryDefinition.credDefId) + + const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) + + let revocationStatusList: RevocationStatusList | undefined + try { + revocationStatusList = RevocationStatusList.create({ + issuanceByDefault: true, + revocationRegistryDefinitionId, + credentialDefinition: credentialDefinitionRecord.credentialDefinition as unknown as JsonObject, + revocationRegistryDefinition: revocationRegistryDefinition as unknown as JsonObject, + revocationRegistryDefinitionPrivate: revocationRegistryDefinitionPrivateRecord.value as unknown as JsonObject, + issuerId, + }) + + return revocationStatusList.toJson() as unknown as AnonCredsRevocationStatusList + } finally { + revocationStatusList?.handle.clear() + } + } + + public async updateRevocationStatusList( + agentContext: AgentContext, + options: UpdateRevocationStatusListOptions + ): Promise { + const { revocationStatusList, revocationRegistryDefinition, issued, revoked, timestamp, tailsFilePath } = options + + let updatedRevocationStatusList: RevocationStatusList | undefined + let revocationRegistryDefinitionObj: RevocationRegistryDefinition | undefined + + try { + updatedRevocationStatusList = RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject) + + if (timestamp && !issued && !revoked) { + updatedRevocationStatusList.updateTimestamp({ + timestamp, + }) + } else { + const credentialDefinitionRecord = await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, revocationRegistryDefinition.credDefId) + + const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationStatusList.revRegDefId) + + revocationRegistryDefinitionObj = RevocationRegistryDefinition.fromJson({ + ...revocationRegistryDefinition, + value: { ...revocationRegistryDefinition.value, tailsLocation: tailsFilePath }, + } as unknown as JsonObject) + updatedRevocationStatusList.update({ + credentialDefinition: credentialDefinitionRecord.credentialDefinition as unknown as JsonObject, + revocationRegistryDefinition: revocationRegistryDefinitionObj, + revocationRegistryDefinitionPrivate: revocationRegistryDefinitionPrivateRecord.value, + issued: options.issued, + revoked: options.revoked, + timestamp: timestamp ?? -1, // FIXME: this should be fixed in anoncreds-rs wrapper + }) + } + + return updatedRevocationStatusList.toJson() as unknown as AnonCredsRevocationStatusList + } finally { + updatedRevocationStatusList?.handle.clear() + revocationRegistryDefinitionObj?.handle.clear() + } + } + public async createCredentialOffer( agentContext: AgentContext, options: CreateCredentialOfferOptions @@ -132,14 +262,28 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { agentContext: AgentContext, options: CreateCredentialOptions ): Promise { - const { tailsFilePath, credentialOffer, credentialRequest, credentialValues, revocationRegistryId } = options + const { + credentialOffer, + credentialRequest, + credentialValues, + revocationRegistryDefinitionId, + revocationStatusList, + revocationRegistryIndex, + } = options + + const definedRevocationOptions = [ + revocationRegistryDefinitionId, + revocationStatusList, + revocationRegistryIndex, + ].filter((e) => e !== undefined) + if (definedRevocationOptions.length > 0 && definedRevocationOptions.length < 3) { + throw new AriesFrameworkError( + 'Revocation requires all of revocationRegistryDefinitionId, revocationRegistryIndex and revocationStatusList' + ) + } let credential: Credential | undefined try { - if (revocationRegistryId || tailsFilePath) { - throw new AriesFrameworkError('Revocation not supported yet') - } - const attributeRawValues: Record = {} const attributeEncodedValues: Record = {} @@ -172,14 +316,46 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService { } } + let revocationConfiguration: CredentialRevocationConfig | undefined + if (revocationRegistryDefinitionId && revocationStatusList && revocationRegistryIndex) { + const revocationRegistryDefinitionRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) + + const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) + + if ( + revocationRegistryIndex >= revocationRegistryDefinitionRecord.revocationRegistryDefinition.value.maxCredNum + ) { + revocationRegistryDefinitionPrivateRecord.state = AnonCredsRevocationRegistryState.Full + } + + revocationConfiguration = new CredentialRevocationConfig({ + registryDefinition: RevocationRegistryDefinition.fromJson( + revocationRegistryDefinitionRecord.revocationRegistryDefinition as unknown as JsonObject + ), + registryDefinitionPrivate: RevocationRegistryDefinitionPrivate.fromJson( + revocationRegistryDefinitionPrivateRecord.value + ), + statusList: RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject), + registryIndex: revocationRegistryIndex, + }) + } credential = Credential.create({ credentialDefinition: credentialDefinitionRecord.credentialDefinition as unknown as JsonObject, credentialOffer: credentialOffer as unknown as JsonObject, credentialRequest: credentialRequest as unknown as JsonObject, - revocationRegistryId, + revocationRegistryId: revocationRegistryDefinitionId, attributeEncodedValues, attributeRawValues, credentialDefinitionPrivate: credentialDefinitionPrivateRecord.value, + revocationConfiguration, + // FIXME: duplicated input parameter? + revocationStatusList: revocationStatusList + ? RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject) + : undefined, }) return { diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts index 26573309ff..d9f615a964 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts @@ -1,7 +1,14 @@ -import type { AnonCredsVerifierService, VerifyProofOptions } from '@aries-framework/anoncreds' +import type { + AnonCredsNonRevokedInterval, + AnonCredsProof, + AnonCredsProofRequest, + AnonCredsVerifierService, + VerifyProofOptions, +} from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' -import type { JsonObject } from '@hyperledger/anoncreds-shared' +import type { JsonObject, NonRevokedIntervalOverride } from '@hyperledger/anoncreds-shared' +import { AnonCredsRegistryService } from '@aries-framework/anoncreds' import { injectable } from '@aries-framework/core' import { Presentation } from '@hyperledger/anoncreds-shared' @@ -12,6 +19,16 @@ export class AnonCredsRsVerifierService implements AnonCredsVerifierService { let presentation: Presentation | undefined try { + // Check that provided timestamps correspond to the active ones from the VDR. If they are and differ from the originally + // requested ones, create overrides for anoncreds-rs to consider them valid + const { verified, nonRevokedIntervalOverrides } = await this.verifyTimestamps(agentContext, proof, proofRequest) + + // No need to call anoncreds-rs as we already know that the proof will not be valid + if (!verified) { + agentContext.config.logger.debug('Invalid timestamps for provided identifiers') + return false + } + presentation = Presentation.fromJson(proof as unknown as JsonObject) const rsCredentialDefinitions: Record = {} @@ -41,9 +58,89 @@ export class AnonCredsRsVerifierService implements AnonCredsVerifierService { schemas: rsSchemas, revocationRegistryDefinitions, revocationStatusLists: lists, + nonRevokedIntervalOverrides, }) } finally { presentation?.handle.clear() } } + + private async verifyTimestamps( + agentContext: AgentContext, + proof: AnonCredsProof, + proofRequest: AnonCredsProofRequest + ): Promise<{ verified: boolean; nonRevokedIntervalOverrides?: NonRevokedIntervalOverride[] }> { + const nonRevokedIntervalOverrides: NonRevokedIntervalOverride[] = [] + + // Override expected timestamps if the requested ones don't exacly match the values from VDR + const globalNonRevokedInterval = proofRequest.non_revoked + + const requestedNonRevokedRestrictions: { + nonRevokedInterval: AnonCredsNonRevokedInterval + schemaId?: string + credentialDefinitionId?: string + revocationRegistryDefinitionId?: string + }[] = [] + + for (const value of [ + ...Object.values(proofRequest.requested_attributes), + ...Object.values(proofRequest.requested_predicates), + ]) { + const nonRevokedInterval = value.non_revoked ?? globalNonRevokedInterval + if (nonRevokedInterval) { + value.restrictions?.forEach((restriction) => + requestedNonRevokedRestrictions.push({ + nonRevokedInterval, + schemaId: restriction.schema_id, + credentialDefinitionId: restriction.cred_def_id, + revocationRegistryDefinitionId: restriction.rev_reg_id, + }) + ) + } + } + + for (const identifier of proof.identifiers) { + if (!identifier.timestamp || !identifier.rev_reg_id) { + continue + } + const relatedNonRevokedRestrictionItem = requestedNonRevokedRestrictions.find( + (item) => + item.revocationRegistryDefinitionId === item.revocationRegistryDefinitionId || + item.credentialDefinitionId === identifier.cred_def_id || + item.schemaId === item.schemaId + ) + + const requestedFrom = relatedNonRevokedRestrictionItem?.nonRevokedInterval.from + if (requestedFrom && requestedFrom > identifier.timestamp) { + // Check VDR if the active revocation status list at requestedFrom was the one from provided timestamp. + // If it matches, add to the override list + const registry = agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier(agentContext, identifier.rev_reg_id) + const { revocationStatusList } = await registry.getRevocationStatusList( + agentContext, + identifier.rev_reg_id, + requestedFrom + ) + const vdrTimestamp = revocationStatusList?.timestamp + if (vdrTimestamp && vdrTimestamp === identifier.timestamp) { + nonRevokedIntervalOverrides.push({ + overrideRevocationStatusListTimestamp: identifier.timestamp, + requestedFromTimestamp: requestedFrom, + revocationRegistryDefinitionId: identifier.rev_reg_id, + }) + } else { + agentContext.config.logger.debug( + `VDR timestamp for ${requestedFrom} does not correspond to the one provided in proof identifiers. Expected: ${identifier.timestamp} and received ${vdrTimestamp}` + ) + return { verified: false } + } + } + } + + return { + verified: true, + nonRevokedIntervalOverrides: nonRevokedIntervalOverrides.length ? nonRevokedIntervalOverrides : undefined, + } + } } diff --git a/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts b/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts new file mode 100644 index 0000000000..32cb2d48f4 --- /dev/null +++ b/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts @@ -0,0 +1,58 @@ +import type { AnonCredsRevocationRegistryDefinition } from '@aries-framework/anoncreds' +import type { AgentContext, FileSystem } from '@aries-framework/core' + +import { BasicTailsFileService } from '@aries-framework/anoncreds' +import { InjectionSymbols } from '@aries-framework/core' + +export class InMemoryTailsFileService extends BasicTailsFileService { + private tailsFilePaths: Record = {} + + public async uploadTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ): Promise { + this.tailsFilePaths[options.revocationRegistryDefinition.value.tailsHash] = + options.revocationRegistryDefinition.value.tailsLocation + + return options.revocationRegistryDefinition.value.tailsHash + } + + public async getTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ): Promise { + const { revocationRegistryDefinition } = options + const { tailsLocation, tailsHash } = revocationRegistryDefinition.value + + try { + agentContext.config.logger.debug( + `Checking to see if tails file for URL ${revocationRegistryDefinition.value.tailsLocation} has been stored in the FileSystem` + ) + + // hash is used as file identifier + const tailsExists = await this.tailsFileExists(agentContext, tailsHash) + const tailsFilePath = await this.getTailsFilePath(agentContext, tailsHash) + agentContext.config.logger.debug( + `Tails file for ${tailsLocation} ${tailsExists ? 'is stored' : 'is not stored'} at ${tailsFilePath}` + ) + + if (!tailsExists) { + agentContext.config.logger.debug(`Retrieving tails file from URL ${tailsLocation}`) + const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) + await fileSystem.downloadToFile(tailsLocation, tailsFilePath) + agentContext.config.logger.debug(`Saved tails file to FileSystem at path ${tailsFilePath}`) + } + + return tailsFilePath + } catch (error) { + agentContext.config.logger.error(`Error while retrieving tails file from URL ${tailsLocation}`, { + error, + }) + throw error + } + } +} diff --git a/packages/anoncreds-rs/tests/LocalDidResolver.ts b/packages/anoncreds-rs/tests/LocalDidResolver.ts new file mode 100644 index 0000000000..1f50c1b3aa --- /dev/null +++ b/packages/anoncreds-rs/tests/LocalDidResolver.ts @@ -0,0 +1,30 @@ +import type { DidResolutionResult, DidResolver, AgentContext } from '@aries-framework/core' + +import { DidsApi } from '@aries-framework/core' + +export class LocalDidResolver implements DidResolver { + public readonly supportedMethods = ['sov', 'indy'] + + public async resolve(agentContext: AgentContext, did: string): Promise { + const didDocumentMetadata = {} + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + + const didRecord = (await didsApi.getCreatedDids()).find((record) => record.did === did) + if (!didRecord) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}'`, + }, + } + } + return { + didDocument: didRecord.didDocument ?? null, + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } +} diff --git a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts index f3ac963ab2..06481055f4 100644 --- a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts +++ b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts @@ -2,6 +2,11 @@ import type { AnonCredsCredentialRequest } from '@aries-framework/anoncreds' import type { Wallet } from '@aries-framework/core' import { + AnonCredsRevocationRegistryDefinitionPrivateRecord, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRecord, + AnonCredsRevocationRegistryDefinitionRepository, + AnonCredsRevocationRegistryState, AnonCredsModuleConfig, AnonCredsHolderServiceSymbol, AnonCredsIssuerServiceSymbol, @@ -31,15 +36,20 @@ import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' +import { dateToTimestamp } from '../../anoncreds/src/utils/timestamp' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers' import { AnonCredsRsHolderService } from '../src/services/AnonCredsRsHolderService' import { AnonCredsRsIssuerService } from '../src/services/AnonCredsRsIssuerService' import { AnonCredsRsVerifierService } from '../src/services/AnonCredsRsVerifierService' +import { InMemoryTailsFileService } from './InMemoryTailsFileService' + const registry = new InMemoryAnonCredsRegistry() +const tailsFileService = new InMemoryTailsFileService() const anonCredsModuleConfig = new AnonCredsModuleConfig({ registries: [registry], + tailsFileService, }) const agentConfig = getAgentConfig('AnonCreds format services using anoncreds-rs') @@ -54,6 +64,7 @@ const agentContext = getAgentContext({ registerInstances: [ [InjectionSymbols.Stop$, new Subject()], [InjectionSymbols.AgentDependencies, agentDependencies], + [InjectionSymbols.FileSystem, new agentDependencies.FileSystem()], [InjectionSymbols.StorageService, inMemoryStorageService], [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], [AnonCredsHolderServiceSymbol, anonCredsHolderService], @@ -71,289 +82,373 @@ const anoncredsProofFormatService = new AnonCredsProofFormatService() const indyDid = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' describe('AnonCreds format services using anoncreds-rs', () => { + afterEach(() => { + inMemoryStorageService.records = {} + }) + test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => { - const schema = await anonCredsIssuerService.createSchema(agentContext, { - attrNames: ['name', 'age'], - issuerId: indyDid, - name: 'Employee Credential', - version: '1.0.0', + await anonCredsFlowTest({ issuerId: indyDid, revocable: false }) + }) + + test('issuance and verification flow starting from proposal without negotiation and with revocation', async () => { + await anonCredsFlowTest({ issuerId: indyDid, revocable: true }) + }) +}) + +async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean }) { + const { issuerId, revocable } = options + + const schema = await anonCredsIssuerService.createSchema(agentContext, { + attrNames: ['name', 'age'], + issuerId, + name: 'Employee Credential', + version: '1.0.0', + }) + + const { schemaState } = await registry.registerSchema(agentContext, { + schema, + options: {}, + }) + + if (!schemaState.schema || !schemaState.schemaId) { + throw new Error('Failed to create schema') + } + + await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save( + agentContext, + new AnonCredsSchemaRecord({ + schema: schemaState.schema, + schemaId: schemaState.schemaId, + methodName: 'inMemory', }) + ) - const { schemaState } = await registry.registerSchema(agentContext, { + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = + await anonCredsIssuerService.createCredentialDefinition(agentContext, { + issuerId: indyDid, + schemaId: schemaState.schemaId as string, schema, - options: {}, + tag: 'Employee Credential', + supportRevocation: revocable, + }) + + const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { + credentialDefinition, + options: {}, + }) + + if (!credentialDefinitionState.credentialDefinition || !credentialDefinitionState.credentialDefinitionId) { + throw new Error('Failed to create credential definition') + } + + if (!credentialDefinitionPrivate || !keyCorrectnessProof) { + throw new Error('Failed to get private part of credential definition') + } + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( + agentContext, + new AnonCredsCredentialDefinitionRecord({ + credentialDefinition: credentialDefinitionState.credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + methodName: 'inMemory', + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save( + agentContext, + new AnonCredsCredentialDefinitionPrivateRecord({ + value: credentialDefinitionPrivate, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + }) + ) + + await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save( + agentContext, + new AnonCredsKeyCorrectnessProofRecord({ + value: keyCorrectnessProof, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, }) + ) - const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = - await anonCredsIssuerService.createCredentialDefinition(agentContext, { + let revocationRegistryDefinitionId: string | undefined + if (revocable) { + const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } = + await anonCredsIssuerService.createRevocationRegistryDefinition(agentContext, { issuerId: indyDid, - schemaId: schemaState.schemaId as string, - schema, - tag: 'Employee Credential', - supportRevocation: false, + credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + maximumCredentialNumber: 100, + tailsDirectoryPath: await tailsFileService.getTailsBasePath(agentContext), + tag: 'default', }) - const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { - credentialDefinition, + // At this moment, tails file should be published and a valid public URL will be received + const localTailsFilePath = revocationRegistryDefinition.value.tailsLocation + + const { revocationRegistryDefinitionState } = await registry.registerRevocationRegistryDefinition(agentContext, { + revocationRegistryDefinition, options: {}, }) - if ( - !credentialDefinitionState.credentialDefinition || - !credentialDefinitionState.credentialDefinitionId || - !schemaState.schema || - !schemaState.schemaId - ) { - throw new Error('Failed to create schema or credential definition') - } + revocationRegistryDefinitionId = revocationRegistryDefinitionState.revocationRegistryDefinitionId if ( - !credentialDefinitionState.credentialDefinition || - !credentialDefinitionState.credentialDefinitionId || - !schemaState.schema || - !schemaState.schemaId + !revocationRegistryDefinitionState.revocationRegistryDefinition || + !revocationRegistryDefinitionId || + !revocationRegistryDefinitionPrivate ) { - throw new Error('Failed to create schema or credential definition') - } - - if (!credentialDefinitionPrivate || !keyCorrectnessProof) { - throw new Error('Failed to get private part of credential definition') + throw new Error('Failed to create revocation registry') } - await agentContext.dependencyManager.resolve(AnonCredsSchemaRepository).save( + await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionRepository).save( agentContext, - new AnonCredsSchemaRecord({ - schema: schemaState.schema, - schemaId: schemaState.schemaId, - methodName: 'inMemory', + new AnonCredsRevocationRegistryDefinitionRecord({ + revocationRegistryDefinition: revocationRegistryDefinitionState.revocationRegistryDefinition, + revocationRegistryDefinitionId, }) ) - await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository).save( + await agentContext.dependencyManager.resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository).save( agentContext, - new AnonCredsCredentialDefinitionRecord({ - credentialDefinition: credentialDefinitionState.credentialDefinition, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - methodName: 'inMemory', + new AnonCredsRevocationRegistryDefinitionPrivateRecord({ + state: AnonCredsRevocationRegistryState.Active, + value: revocationRegistryDefinitionPrivate, + credentialDefinitionId: revocationRegistryDefinitionState.revocationRegistryDefinition.credDefId, + revocationRegistryDefinitionId, }) ) - await agentContext.dependencyManager.resolve(AnonCredsCredentialDefinitionPrivateRepository).save( - agentContext, - new AnonCredsCredentialDefinitionPrivateRecord({ - value: credentialDefinitionPrivate, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }) - ) + const createdRevocationStatusList = await anonCredsIssuerService.createRevocationStatusList(agentContext, { + issuerId: indyDid, + revocationRegistryDefinition, + revocationRegistryDefinitionId, + tailsFilePath: localTailsFilePath, + }) - await agentContext.dependencyManager.resolve(AnonCredsKeyCorrectnessProofRepository).save( - agentContext, - new AnonCredsKeyCorrectnessProofRecord({ - value: keyCorrectnessProof, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }) - ) + const { revocationStatusListState } = await registry.registerRevocationStatusList(agentContext, { + revocationStatusList: createdRevocationStatusList, + options: {}, + }) - const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' }) - expect(linkSecret.linkSecretId).toBe('linkSecretId') + if (!revocationStatusListState.revocationStatusList || !revocationStatusListState.timestamp) { + throw new Error('Failed to create revocation status list') + } + } - await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save( - agentContext, - new AnonCredsLinkSecretRecord({ - value: linkSecret.linkSecretValue, - linkSecretId: linkSecret.linkSecretId, - }) - ) + const linkSecret = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'linkSecretId' }) + expect(linkSecret.linkSecretId).toBe('linkSecretId') - const holderCredentialRecord = new CredentialExchangeRecord({ - protocolVersion: 'v1', - state: CredentialState.ProposalSent, - threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + await agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository).save( + agentContext, + new AnonCredsLinkSecretRecord({ + value: linkSecret.linkSecretValue, + linkSecretId: linkSecret.linkSecretId, }) + ) - const issuerCredentialRecord = new CredentialExchangeRecord({ - protocolVersion: 'v1', - state: CredentialState.ProposalReceived, - threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', - }) + const holderCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalSent, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) + + const issuerCredentialRecord = new CredentialExchangeRecord({ + protocolVersion: 'v1', + state: CredentialState.ProposalReceived, + threadId: 'f365c1a5-2baf-4873-9432-fa87c888a0aa', + }) - const credentialAttributes = [ - new CredentialPreviewAttribute({ - name: 'name', - value: 'John', - }), - new CredentialPreviewAttribute({ - name: 'age', - value: '25', - }), - ] - - // Holder creates proposal - holderCredentialRecord.credentialAttributes = credentialAttributes - const { attachment: proposalAttachment } = await anoncredsCredentialFormatService.createProposal(agentContext, { - credentialRecord: holderCredentialRecord, - credentialFormats: { - anoncreds: { - attributes: credentialAttributes, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }, + const credentialAttributes = [ + new CredentialPreviewAttribute({ + name: 'name', + value: 'John', + }), + new CredentialPreviewAttribute({ + name: 'age', + value: '25', + }), + ] + + // Holder creates proposal + holderCredentialRecord.credentialAttributes = credentialAttributes + const { attachment: proposalAttachment } = await anoncredsCredentialFormatService.createProposal(agentContext, { + credentialRecord: holderCredentialRecord, + credentialFormats: { + anoncreds: { + attributes: credentialAttributes, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, }, - }) + }, + }) - // Issuer processes and accepts proposal - await anoncredsCredentialFormatService.processProposal(agentContext, { - credentialRecord: issuerCredentialRecord, - attachment: proposalAttachment, - }) - // Set attributes on the credential record, this is normally done by the protocol service - issuerCredentialRecord.credentialAttributes = credentialAttributes - const { attachment: offerAttachment } = await anoncredsCredentialFormatService.acceptProposal(agentContext, { - credentialRecord: issuerCredentialRecord, - proposalAttachment: proposalAttachment, - }) + // Issuer processes and accepts proposal + await anoncredsCredentialFormatService.processProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: proposalAttachment, + }) + // Set attributes on the credential record, this is normally done by the protocol service + issuerCredentialRecord.credentialAttributes = credentialAttributes + + // If revocable, specify revocation registry definition id and index + const credentialFormats = revocable + ? { anoncreds: { revocationRegistryDefinitionId, revocationRegistryIndex: 1 } } + : undefined + + const { attachment: offerAttachment } = await anoncredsCredentialFormatService.acceptProposal(agentContext, { + credentialRecord: issuerCredentialRecord, + proposalAttachment: proposalAttachment, + credentialFormats, + }) - // Holder processes and accepts offer - await anoncredsCredentialFormatService.processOffer(agentContext, { - credentialRecord: holderCredentialRecord, - attachment: offerAttachment, - }) - const { attachment: requestAttachment } = await anoncredsCredentialFormatService.acceptOffer(agentContext, { - credentialRecord: holderCredentialRecord, - offerAttachment, - credentialFormats: { - anoncreds: { - linkSecretId: linkSecret.linkSecretId, - }, + // Holder processes and accepts offer + await anoncredsCredentialFormatService.processOffer(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: offerAttachment, + }) + const { attachment: requestAttachment } = await anoncredsCredentialFormatService.acceptOffer(agentContext, { + credentialRecord: holderCredentialRecord, + offerAttachment, + credentialFormats: { + anoncreds: { + linkSecretId: linkSecret.linkSecretId, }, - }) + }, + }) - // Make sure the request contains an entropy and does not contain a prover_did field - expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).entropy).toBeDefined() - expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).prover_did).toBeUndefined() + // Make sure the request contains an entropy and does not contain a prover_did field + expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).entropy).toBeDefined() + expect((requestAttachment.getDataAsJson() as AnonCredsCredentialRequest).prover_did).toBeUndefined() - // Issuer processes and accepts request - await anoncredsCredentialFormatService.processRequest(agentContext, { - credentialRecord: issuerCredentialRecord, - attachment: requestAttachment, - }) - const { attachment: credentialAttachment } = await anoncredsCredentialFormatService.acceptRequest(agentContext, { - credentialRecord: issuerCredentialRecord, - requestAttachment, - offerAttachment, - }) + // Issuer processes and accepts request + await anoncredsCredentialFormatService.processRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + attachment: requestAttachment, + }) + const { attachment: credentialAttachment } = await anoncredsCredentialFormatService.acceptRequest(agentContext, { + credentialRecord: issuerCredentialRecord, + requestAttachment, + offerAttachment, + }) - // Holder processes and accepts credential - await anoncredsCredentialFormatService.processCredential(agentContext, { - credentialRecord: holderCredentialRecord, - attachment: credentialAttachment, - requestAttachment, - }) + // Holder processes and accepts credential + await anoncredsCredentialFormatService.processCredential(agentContext, { + credentialRecord: holderCredentialRecord, + attachment: credentialAttachment, + requestAttachment, + }) - expect(holderCredentialRecord.credentials).toEqual([ - { credentialRecordType: 'anoncreds', credentialRecordId: expect.any(String) }, - ]) + expect(holderCredentialRecord.credentials).toEqual([ + { credentialRecordType: 'anoncreds', credentialRecordId: expect.any(String) }, + ]) - const credentialId = holderCredentialRecord.credentials[0].credentialRecordId - const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { - credentialId, - }) + const credentialId = holderCredentialRecord.credentials[0].credentialRecordId + const anonCredsCredential = await anonCredsHolderService.getCredential(agentContext, { + credentialId, + }) - expect(anonCredsCredential).toEqual({ - credentialId, - attributes: { - age: '25', - name: 'John', - }, - schemaId: schemaState.schemaId, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - revocationRegistryId: null, - credentialRevocationId: undefined, // FIXME: should be null? - methodName: 'inMemory', - }) + expect(anonCredsCredential).toEqual({ + credentialId, + attributes: { + age: '25', + name: 'John', + }, + schemaId: schemaState.schemaId, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + revocationRegistryId: revocable ? revocationRegistryDefinitionId : null, + credentialRevocationId: revocable ? '1' : undefined, + methodName: 'inMemory', + }) - expect(holderCredentialRecord.metadata.data).toEqual({ - '_anoncreds/credential': { + const expectedCredentialMetadata = revocable + ? { schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }, - '_anoncreds/credentialRequest': { - link_secret_blinding_data: expect.any(Object), - link_secret_name: expect.any(String), - nonce: expect.any(String), - }, - }) - - expect(issuerCredentialRecord.metadata.data).toEqual({ - '_anoncreds/credential': { + revocationRegistryId: revocationRegistryDefinitionId, + credentialRevocationId: '1', + } + : { schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - }, - }) + } + expect(holderCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': expectedCredentialMetadata, + '_anoncreds/credentialRequest': { + link_secret_blinding_data: expect.any(Object), + link_secret_name: expect.any(String), + nonce: expect.any(String), + }, + }) - const holderProofRecord = new ProofExchangeRecord({ - protocolVersion: 'v1', - state: ProofState.ProposalSent, - threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', - }) - const verifierProofRecord = new ProofExchangeRecord({ - protocolVersion: 'v1', - state: ProofState.ProposalReceived, - threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', - }) + expect(issuerCredentialRecord.metadata.data).toEqual({ + '_anoncreds/credential': expectedCredentialMetadata, + }) - const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, { - proofFormats: { - anoncreds: { - attributes: [ - { - name: 'name', - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - value: 'John', - referent: '1', - }, - ], - predicates: [ - { - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, - name: 'age', - predicate: '>=', - threshold: 18, - }, - ], - name: 'Proof Request', - version: '1.0', - }, - }, - proofRecord: holderProofRecord, - }) + const holderProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalSent, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) + const verifierProofRecord = new ProofExchangeRecord({ + protocolVersion: 'v1', + state: ProofState.ProposalReceived, + threadId: '4f5659a4-1aea-4f42-8c22-9a9985b35e38', + }) - await anoncredsProofFormatService.processProposal(agentContext, { - attachment: proofProposalAttachment, - proofRecord: verifierProofRecord, - }) + const nrpRequestedTime = dateToTimestamp(new Date()) + + const { attachment: proofProposalAttachment } = await anoncredsProofFormatService.createProposal(agentContext, { + proofFormats: { + anoncreds: { + attributes: [ + { + name: 'name', + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + value: 'John', + referent: '1', + }, + ], + predicates: [ + { + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + name: 'age', + predicate: '>=', + threshold: 18, + }, + ], + name: 'Proof Request', + version: '1.0', + nonRevokedInterval: { from: nrpRequestedTime, to: nrpRequestedTime }, + }, + }, + proofRecord: holderProofRecord, + }) - const { attachment: proofRequestAttachment } = await anoncredsProofFormatService.acceptProposal(agentContext, { - proofRecord: verifierProofRecord, - proposalAttachment: proofProposalAttachment, - }) + await anoncredsProofFormatService.processProposal(agentContext, { + attachment: proofProposalAttachment, + proofRecord: verifierProofRecord, + }) - await anoncredsProofFormatService.processRequest(agentContext, { - attachment: proofRequestAttachment, - proofRecord: holderProofRecord, - }) + const { attachment: proofRequestAttachment } = await anoncredsProofFormatService.acceptProposal(agentContext, { + proofRecord: verifierProofRecord, + proposalAttachment: proofProposalAttachment, + }) - const { attachment: proofAttachment } = await anoncredsProofFormatService.acceptRequest(agentContext, { - proofRecord: holderProofRecord, - requestAttachment: proofRequestAttachment, - proposalAttachment: proofProposalAttachment, - }) + await anoncredsProofFormatService.processRequest(agentContext, { + attachment: proofRequestAttachment, + proofRecord: holderProofRecord, + }) - const isValid = await anoncredsProofFormatService.processPresentation(agentContext, { - attachment: proofAttachment, - proofRecord: verifierProofRecord, - requestAttachment: proofRequestAttachment, - }) + const { attachment: proofAttachment } = await anoncredsProofFormatService.acceptRequest(agentContext, { + proofRecord: holderProofRecord, + requestAttachment: proofRequestAttachment, + proposalAttachment: proofProposalAttachment, + }) - expect(isValid).toBe(true) + const isValid = await anoncredsProofFormatService.processPresentation(agentContext, { + attachment: proofAttachment, + proofRecord: verifierProofRecord, + requestAttachment: proofRequestAttachment, }) -}) + + expect(isValid).toBe(true) +} diff --git a/packages/anoncreds-rs/tests/anoncredsSetup.ts b/packages/anoncreds-rs/tests/anoncredsSetup.ts new file mode 100644 index 0000000000..5f3cbd85ab --- /dev/null +++ b/packages/anoncreds-rs/tests/anoncredsSetup.ts @@ -0,0 +1,561 @@ +import type { + AnonCredsRegisterCredentialDefinitionOptions, + AnonCredsRequestedAttribute, + AnonCredsRequestedPredicate, + AnonCredsOfferCredentialFormat, + AnonCredsSchema, + RegisterCredentialDefinitionReturnStateFinished, + RegisterSchemaReturnStateFinished, + AnonCredsRegistry, + AnonCredsRegisterRevocationRegistryDefinitionOptions, + RegisterRevocationRegistryDefinitionReturnStateFinished, + AnonCredsRegisterRevocationStatusListOptions, + RegisterRevocationStatusListReturnStateFinished, +} from '../../anoncreds/src' +import type { EventReplaySubject } from '../../core/tests' +import type { AutoAcceptProof, ConnectionRecord } from '@aries-framework/core' + +import { + DidDocumentBuilder, + CacheModule, + InMemoryLruCache, + Agent, + AriesFrameworkError, + AutoAcceptCredential, + CredentialEventTypes, + CredentialsModule, + CredentialState, + ProofEventTypes, + ProofsModule, + ProofState, + V2CredentialProtocol, + V2ProofProtocol, + DidsModule, +} from '@aries-framework/core' +import { anoncreds } from '@hyperledger/anoncreds-nodejs' +import { randomUUID } from 'crypto' + +import { AnonCredsCredentialFormatService, AnonCredsProofFormatService, AnonCredsModule } from '../../anoncreds/src' +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { AskarModule } from '../../askar/src' +import { askarModuleConfig } from '../../askar/tests/helpers' +import { sleep } from '../../core/src/utils/sleep' +import { setupSubjectTransports, setupEventReplaySubjects } from '../../core/tests' +import { + getAgentOptions, + makeConnection, + waitForCredentialRecordSubject, + waitForProofExchangeRecordSubject, +} from '../../core/tests/helpers' +import testLogger from '../../core/tests/logger' +import { AnonCredsRsModule } from '../src' + +import { InMemoryTailsFileService } from './InMemoryTailsFileService' +import { LocalDidResolver } from './LocalDidResolver' + +// Helper type to get the type of the agents (with the custom modules) for the credential tests +export type AnonCredsTestsAgent = Agent< + ReturnType & { mediationRecipient?: any; mediator?: any } +> + +export const getAnonCredsModules = ({ + autoAcceptCredentials, + autoAcceptProofs, + registries, +}: { + autoAcceptCredentials?: AutoAcceptCredential + autoAcceptProofs?: AutoAcceptProof + registries?: [AnonCredsRegistry, ...AnonCredsRegistry[]] +} = {}) => { + const anonCredsCredentialFormatService = new AnonCredsCredentialFormatService() + const anonCredsProofFormatService = new AnonCredsProofFormatService() + + const modules = { + credentials: new CredentialsModule({ + autoAcceptCredentials, + credentialProtocols: [ + new V2CredentialProtocol({ + credentialFormats: [anonCredsCredentialFormatService], + }), + ], + }), + proofs: new ProofsModule({ + autoAcceptProofs, + proofProtocols: [ + new V2ProofProtocol({ + proofFormats: [anonCredsProofFormatService], + }), + ], + }), + anoncreds: new AnonCredsModule({ + registries: registries ?? [new InMemoryAnonCredsRegistry()], + tailsFileService: new InMemoryTailsFileService(), + }), + anoncredsRs: new AnonCredsRsModule({ + anoncreds, + }), + dids: new DidsModule({ + resolvers: [new LocalDidResolver()], + }), + askar: new AskarModule(askarModuleConfig), + cache: new CacheModule({ + cache: new InMemoryLruCache({ limit: 100 }), + }), + } as const + + return modules +} + +export async function presentAnonCredsProof({ + verifierAgent, + verifierReplay, + + holderAgent, + holderReplay, + + verifierHolderConnectionId, + + request: { attributes, predicates }, +}: { + holderAgent: AnonCredsTestsAgent + holderReplay: EventReplaySubject + + verifierAgent: AnonCredsTestsAgent + verifierReplay: EventReplaySubject + + verifierHolderConnectionId: string + request: { + attributes?: Record + predicates?: Record + } +}) { + let holderProofExchangeRecordPromise = waitForProofExchangeRecordSubject(holderReplay, { + state: ProofState.RequestReceived, + }) + + let verifierProofExchangeRecord = await verifierAgent.proofs.requestProof({ + connectionId: verifierHolderConnectionId, + proofFormats: { + anoncreds: { + name: 'Test Proof Request', + requested_attributes: attributes, + requested_predicates: predicates, + version: '1.0', + }, + }, + protocolVersion: 'v2', + }) + + let holderProofExchangeRecord = await holderProofExchangeRecordPromise + + const selectedCredentials = await holderAgent.proofs.selectCredentialsForRequest({ + proofRecordId: holderProofExchangeRecord.id, + }) + + const verifierProofExchangeRecordPromise = waitForProofExchangeRecordSubject(verifierReplay, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await holderAgent.proofs.acceptRequest({ + proofRecordId: holderProofExchangeRecord.id, + proofFormats: { anoncreds: selectedCredentials.proofFormats.anoncreds }, + }) + + verifierProofExchangeRecord = await verifierProofExchangeRecordPromise + + // assert presentation is valid + expect(verifierProofExchangeRecord.isVerified).toBe(true) + + holderProofExchangeRecordPromise = waitForProofExchangeRecordSubject(holderReplay, { + threadId: holderProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + verifierProofExchangeRecord = await verifierAgent.proofs.acceptPresentation({ + proofRecordId: verifierProofExchangeRecord.id, + }) + holderProofExchangeRecord = await holderProofExchangeRecordPromise + + return { + verifierProofExchangeRecord, + holderProofExchangeRecord, + } +} + +export async function issueAnonCredsCredential({ + issuerAgent, + issuerReplay, + + holderAgent, + holderReplay, + + issuerHolderConnectionId, + revocationRegistryDefinitionId, + offer, +}: { + issuerAgent: AnonCredsTestsAgent + issuerReplay: EventReplaySubject + + holderAgent: AnonCredsTestsAgent + holderReplay: EventReplaySubject + + issuerHolderConnectionId: string + revocationRegistryDefinitionId?: string + offer: AnonCredsOfferCredentialFormat +}) { + let issuerCredentialExchangeRecord = await issuerAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: issuerHolderConnectionId, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { ...offer, revocationRegistryDefinitionId, revocationRegistryIndex: 1 }, + }, + autoAcceptCredential: AutoAcceptCredential.ContentApproved, + }) + + let holderCredentialExchangeRecord = await waitForCredentialRecordSubject(holderReplay, { + threadId: issuerCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + await holderAgent.credentials.acceptOffer({ + credentialRecordId: holderCredentialExchangeRecord.id, + autoAcceptCredential: AutoAcceptCredential.ContentApproved, + }) + + // Because we use auto-accept it can take a while to have the whole credential flow finished + // Both parties need to interact with the ledger and sign/verify the credential + holderCredentialExchangeRecord = await waitForCredentialRecordSubject(holderReplay, { + threadId: issuerCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + issuerCredentialExchangeRecord = await waitForCredentialRecordSubject(issuerReplay, { + threadId: issuerCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + + return { + issuerCredentialExchangeRecord, + holderCredentialExchangeRecord, + } +} + +interface SetupAnonCredsTestsReturn { + issuerAgent: AnonCredsTestsAgent + issuerReplay: EventReplaySubject + + holderAgent: AnonCredsTestsAgent + holderReplay: EventReplaySubject + + issuerHolderConnectionId: CreateConnections extends true ? string : undefined + holderIssuerConnectionId: CreateConnections extends true ? string : undefined + + verifierHolderConnectionId: CreateConnections extends true + ? VerifierName extends string + ? string + : undefined + : undefined + holderVerifierConnectionId: CreateConnections extends true + ? VerifierName extends string + ? string + : undefined + : undefined + + verifierAgent: VerifierName extends string ? AnonCredsTestsAgent : undefined + verifierReplay: VerifierName extends string ? EventReplaySubject : undefined + + schemaId: string + credentialDefinitionId: string + revocationRegistryDefinitionId?: string + revocationStatusListTimestamp?: number +} + +export async function setupAnonCredsTests< + VerifierName extends string | undefined = undefined, + CreateConnections extends boolean = true +>({ + issuerId, + issuerName, + holderName, + verifierName, + autoAcceptCredentials, + autoAcceptProofs, + attributeNames, + createConnections, + supportRevocation, + registries, +}: { + issuerId: string + issuerName: string + holderName: string + verifierName?: VerifierName + autoAcceptCredentials?: AutoAcceptCredential + autoAcceptProofs?: AutoAcceptProof + attributeNames: string[] + createConnections?: CreateConnections + supportRevocation?: boolean + registries?: [AnonCredsRegistry, ...AnonCredsRegistry[]] +}): Promise> { + const issuerAgent = new Agent( + getAgentOptions( + issuerName, + { + endpoints: ['rxjs:issuer'], + }, + getAnonCredsModules({ + autoAcceptCredentials, + autoAcceptProofs, + registries, + }) + ) + ) + + const holderAgent = new Agent( + getAgentOptions( + holderName, + { + endpoints: ['rxjs:holder'], + }, + getAnonCredsModules({ + autoAcceptCredentials, + autoAcceptProofs, + registries, + }) + ) + ) + + const verifierAgent = verifierName + ? new Agent( + getAgentOptions( + verifierName, + { + endpoints: ['rxjs:verifier'], + }, + getAnonCredsModules({ + autoAcceptCredentials, + autoAcceptProofs, + registries, + }) + ) + ) + : undefined + + setupSubjectTransports(verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent]) + const [issuerReplay, holderReplay, verifierReplay] = setupEventReplaySubjects( + verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent], + [CredentialEventTypes.CredentialStateChanged, ProofEventTypes.ProofStateChanged] + ) + + await issuerAgent.initialize() + await holderAgent.initialize() + if (verifierAgent) await verifierAgent.initialize() + + // Create default link secret for holder + await holderAgent.modules.anoncreds.createLinkSecret({ + linkSecretId: 'default', + setAsDefault: true, + }) + + const { credentialDefinition, revocationRegistryDefinition, revocationStatusList, schema } = + await prepareForAnonCredsIssuance(issuerAgent, { + issuerId, + attributeNames, + supportRevocation, + }) + + let issuerHolderConnection: ConnectionRecord | undefined + let holderIssuerConnection: ConnectionRecord | undefined + let verifierHolderConnection: ConnectionRecord | undefined + let holderVerifierConnection: ConnectionRecord | undefined + + if (createConnections ?? true) { + ;[issuerHolderConnection, holderIssuerConnection] = await makeConnection(issuerAgent, holderAgent) + + if (verifierAgent) { + ;[holderVerifierConnection, verifierHolderConnection] = await makeConnection(holderAgent, verifierAgent) + } + } + + return { + issuerAgent, + issuerReplay, + + holderAgent, + holderReplay, + + verifierAgent: verifierName ? verifierAgent : undefined, + verifierReplay: verifierName ? verifierReplay : undefined, + + revocationRegistryDefinitionId: revocationRegistryDefinition?.revocationRegistryDefinitionId, + revocationStatusListTimestamp: revocationStatusList.revocationStatusList?.timestamp, + credentialDefinitionId: credentialDefinition.credentialDefinitionId, + schemaId: schema.schemaId, + + issuerHolderConnectionId: issuerHolderConnection?.id, + holderIssuerConnectionId: holderIssuerConnection?.id, + holderVerifierConnectionId: holderVerifierConnection?.id, + verifierHolderConnectionId: verifierHolderConnection?.id, + } as unknown as SetupAnonCredsTestsReturn +} + +export async function prepareForAnonCredsIssuance( + agent: Agent, + { + attributeNames, + supportRevocation, + issuerId, + }: { attributeNames: string[]; supportRevocation?: boolean; issuerId: string } +) { + //const key = await agent.wallet.createKey({ keyType: KeyType.Ed25519 }) + + const didDocument = new DidDocumentBuilder(issuerId).build() + + await agent.dids.import({ did: issuerId, didDocument }) + + const schema = await registerSchema(agent, { + // TODO: update attrNames to attributeNames + attrNames: attributeNames, + name: `Schema ${randomUUID()}`, + version: '1.0', + issuerId, + }) + + // Wait some time pass to let ledger settle the object + await sleep(1000) + + const credentialDefinition = await registerCredentialDefinition( + agent, + { + schemaId: schema.schemaId, + issuerId, + tag: 'default', + }, + supportRevocation + ) + + // Wait some time pass to let ledger settle the object + await sleep(1000) + + let revocationRegistryDefinition + let revocationStatusList + if (supportRevocation) { + revocationRegistryDefinition = await registerRevocationRegistryDefinition(agent, { + issuerId, + tag: 'default', + credentialDefinitionId: credentialDefinition.credentialDefinitionId, + maximumCredentialNumber: 10, + }) + + // Wait some time pass to let ledger settle the object + await sleep(1000) + + revocationStatusList = await registerRevocationStatusList(agent, { + revocationRegistryDefinitionId: revocationRegistryDefinition?.revocationRegistryDefinitionId, + issuerId, + }) + + // Wait some time pass to let ledger settle the object + await sleep(1000) + } + + return { + schema: { + ...schema, + schemaId: schema.schemaId, + }, + credentialDefinition: { + ...credentialDefinition, + credentialDefinitionId: credentialDefinition.credentialDefinitionId, + }, + revocationRegistryDefinition: { + ...revocationRegistryDefinition, + revocationRegistryDefinitionId: revocationRegistryDefinition?.revocationRegistryDefinitionId, + }, + revocationStatusList: { + ...revocationStatusList, + }, + } +} + +async function registerSchema( + agent: AnonCredsTestsAgent, + schema: AnonCredsSchema +): Promise { + const { schemaState } = await agent.modules.anoncreds.registerSchema({ + schema, + options: {}, + }) + + testLogger.test(`created schema with id ${schemaState.schemaId}`, schema) + + if (schemaState.state !== 'finished') { + throw new AriesFrameworkError( + `Schema not created: ${schemaState.state === 'failed' ? schemaState.reason : 'Not finished'}` + ) + } + + return schemaState +} + +async function registerCredentialDefinition( + agent: AnonCredsTestsAgent, + credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions, + supportRevocation?: boolean +): Promise { + const { credentialDefinitionState } = await agent.modules.anoncreds.registerCredentialDefinition({ + credentialDefinition, + supportRevocation: supportRevocation ?? false, + options: {}, + }) + + if (credentialDefinitionState.state !== 'finished') { + throw new AriesFrameworkError( + `Credential definition not created: ${ + credentialDefinitionState.state === 'failed' ? credentialDefinitionState.reason : 'Not finished' + }` + ) + } + + return credentialDefinitionState +} + +async function registerRevocationRegistryDefinition( + agent: AnonCredsTestsAgent, + revocationRegistryDefinition: AnonCredsRegisterRevocationRegistryDefinitionOptions +): Promise { + const { revocationRegistryDefinitionState } = await agent.modules.anoncreds.registerRevocationRegistryDefinition({ + revocationRegistryDefinition, + options: {}, + }) + + if (revocationRegistryDefinitionState.state !== 'finished') { + throw new AriesFrameworkError( + `Revocation registry definition not created: ${ + revocationRegistryDefinitionState.state === 'failed' ? revocationRegistryDefinitionState.reason : 'Not finished' + }` + ) + } + + return revocationRegistryDefinitionState +} + +async function registerRevocationStatusList( + agent: AnonCredsTestsAgent, + revocationStatusList: AnonCredsRegisterRevocationStatusListOptions +): Promise { + const { revocationStatusListState } = await agent.modules.anoncreds.registerRevocationStatusList({ + revocationStatusList, + options: {}, + }) + + if (revocationStatusListState.state !== 'finished') { + throw new AriesFrameworkError( + `Revocation status list not created: ${ + revocationStatusListState.state === 'failed' ? revocationStatusListState.reason : 'Not finished' + }` + ) + } + + return revocationStatusListState +} diff --git a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts new file mode 100644 index 0000000000..f3db7e54cf --- /dev/null +++ b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts @@ -0,0 +1,237 @@ +import type { AnonCredsTestsAgent } from './anoncredsSetup' +import type { EventReplaySubject } from '../../core/tests' + +import { + DidCommMessageRepository, + JsonTransformer, + CredentialState, + CredentialExchangeRecord, + V2CredentialPreview, + V2OfferCredentialMessage, +} from '@aries-framework/core' + +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { waitForCredentialRecordSubject } from '../../core/tests' +import { waitForRevocationNotification } from '../../core/tests/helpers' +import testLogger from '../../core/tests/logger' + +import { setupAnonCredsTests } from './anoncredsSetup' + +const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', +}) + +describe('IC v2 credential revocation', () => { + let faberAgent: AnonCredsTestsAgent + let aliceAgent: AnonCredsTestsAgent + let credentialDefinitionId: string + let revocationRegistryDefinitionId: string | undefined + let aliceConnectionId: string + + let faberReplay: EventReplaySubject + let aliceReplay: EventReplaySubject + + const inMemoryRegistry = new InMemoryAnonCredsRegistry() + + const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + revocationRegistryDefinitionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerId, + issuerName: 'Faber Agent Credentials v2', + holderName: 'Alice Agent Credentials v2', + attributeNames: ['name', 'age', 'x-ray', 'profile_picture'], + supportRevocation: true, + registries: [inMemoryRegistry], + })) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with V2 credential proposal to Faber', async () => { + testLogger.test('Alice sends (v2) credential proposal to Faber') + + const credentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', + }, + }, + comment: 'v2 propose credential test', + }) + + expect(credentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + state: CredentialState.ProposalSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: credentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Proposal', + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + revocationRegistryDefinitionId, + revocationRegistryIndex: 1, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + const didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) + const offerMessage = await didCommMessageRepository.findAgentMessage(faberAgent.context, { + associatedRecordId: faberCredentialRecord.id, + messageClass: V2OfferCredentialMessage, + }) + + expect(JsonTransformer.toJSON(offerMessage)).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/2.0/offer-credential', + comment: 'V2 AnonCreds Proposal', + credential_preview: { + '@type': 'https://didcomm.org/issue-credential/2.0/credential-preview', + attributes: [ + { + name: 'name', + 'mime-type': 'text/plain', + value: 'John', + }, + { + name: 'age', + 'mime-type': 'text/plain', + value: '99', + }, + { + name: 'x-ray', + 'mime-type': 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + 'mime-type': 'text/plain', + value: 'profile picture', + }, + ], + }, + 'offers~attach': expect.any(Array), + }) + + expect(aliceCredentialRecord).toMatchObject({ + id: expect.any(String), + connectionId: expect.any(String), + type: CredentialExchangeRecord.type, + }) + + // below values are not in json object + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: faberCredentialRecord.threadId, + connectionId: aliceCredentialRecord.connectionId, + state: aliceCredentialRecord.state, + credentialIds: [], + }) + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + state: CredentialState.RequestSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + await aliceAgent.credentials.acceptCredential({ + credentialRecordId: aliceCredentialRecord.id, + }) + + testLogger.test('Faber waits for state done') + const doneCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + // Now revoke the credential + const credentialRevocationRegistryDefinitionId = doneCredentialRecord.getTag( + 'anonCredsRevocationRegistryId' + ) as string + const credentialRevocationIndex = doneCredentialRecord.getTag('anonCredsCredentialRevocationId') as string + + expect(credentialRevocationRegistryDefinitionId).toBeDefined() + expect(credentialRevocationIndex).toBeDefined() + expect(credentialRevocationRegistryDefinitionId).toEqual(revocationRegistryDefinitionId) + + await faberAgent.modules.anoncreds.updateRevocationStatusList({ + revocationRegistryDefinitionId: credentialRevocationRegistryDefinitionId, + revokedCredentialIndexes: [Number(credentialRevocationIndex)], + }) + + await faberAgent.credentials.sendRevocationNotification({ + credentialRecordId: doneCredentialRecord.id, + revocationFormat: 'anoncreds', + revocationId: `${credentialRevocationRegistryDefinitionId}::${credentialRevocationIndex}`, + }) + + testLogger.test('Alice waits for credential revocation notification from Faber') + await waitForRevocationNotification(aliceAgent, { + threadId: faberCredentialRecord.threadId, + }) + }) +}) diff --git a/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts new file mode 100644 index 0000000000..e198a16828 --- /dev/null +++ b/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts @@ -0,0 +1,674 @@ +import type { AnonCredsTestsAgent } from './anoncredsSetup' +import type { EventReplaySubject } from '../../core/tests' +import type { AnonCredsHolderService, AnonCredsProposeCredentialFormat } from '@aries-framework/anoncreds' + +import { AnonCredsHolderServiceSymbol } from '@aries-framework/anoncreds' +import { + DidCommMessageRepository, + JsonTransformer, + CredentialState, + CredentialExchangeRecord, + V2CredentialPreview, + V2IssueCredentialMessage, + V2OfferCredentialMessage, + V2ProposeCredentialMessage, + V2RequestCredentialMessage, +} from '@aries-framework/core' + +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { waitForCredentialRecord, waitForCredentialRecordSubject } from '../../core/tests' +import testLogger from '../../core/tests/logger' + +import { issueAnonCredsCredential, setupAnonCredsTests } from './anoncredsSetup' + +const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', +}) + +describe('IC V2 AnonCreds credentials', () => { + let faberAgent: AnonCredsTestsAgent + let aliceAgent: AnonCredsTestsAgent + let credentialDefinitionId: string + let faberConnectionId: string + let aliceConnectionId: string + + let faberReplay: EventReplaySubject + let aliceReplay: EventReplaySubject + + let anonCredsCredentialProposal: AnonCredsProposeCredentialFormat + + const inMemoryRegistry = new InMemoryAnonCredsRegistry() + + const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + + const newCredentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'another x-ray value', + profile_picture: 'another profile picture', + }) + + beforeAll(async () => { + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerId, + issuerName: 'Faber Agent Credentials v2', + holderName: 'Alice Agent Credentials v2', + attributeNames: ['name', 'age', 'x-ray', 'profile_picture'], + registries: [inMemoryRegistry], + })) + + anonCredsCredentialProposal = { + credentialDefinitionId: credentialDefinitionId, + schemaIssuerDid: issuerId, + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: `${issuerId}/q7ATwTYbQDgiigVijUAej:2:test:1.0`, + issuerDid: issuerId, + } + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with V2 credential proposal to Faber', async () => { + testLogger.test('Alice sends (v2) credential proposal to Faber') + + const credentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + schemaIssuerDid: issuerId, + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: `${issuerId}/q7ATwTYbQDgiigVijUAej:2:test:1.0`, + issuerDid: issuerId, + credentialDefinitionId: `${issuerId}/:3:CL:12:tag`, + }, + }, + comment: 'v2 propose credential test', + }) + + expect(credentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + state: CredentialState.ProposalSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: credentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Proposal', + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + const didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) + const offerMessage = await didCommMessageRepository.findAgentMessage(faberAgent.context, { + associatedRecordId: faberCredentialRecord.id, + messageClass: V2OfferCredentialMessage, + }) + + expect(JsonTransformer.toJSON(offerMessage)).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/2.0/offer-credential', + comment: 'V2 AnonCreds Proposal', + credential_preview: { + '@type': 'https://didcomm.org/issue-credential/2.0/credential-preview', + attributes: [ + { + name: 'name', + 'mime-type': 'text/plain', + value: 'John', + }, + { + name: 'age', + 'mime-type': 'text/plain', + value: '99', + }, + { + name: 'x-ray', + 'mime-type': 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + 'mime-type': 'text/plain', + value: 'profile picture', + }, + ], + }, + 'offers~attach': expect.any(Array), + }) + + expect(aliceCredentialRecord).toMatchObject({ + id: expect.any(String), + connectionId: expect.any(String), + type: CredentialExchangeRecord.type, + }) + + // below values are not in json object + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: faberCredentialRecord.threadId, + connectionId: aliceCredentialRecord.connectionId, + state: aliceCredentialRecord.state, + credentialIds: [], + }) + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + state: CredentialState.RequestSent, + threadId: expect.any(String), + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + await aliceAgent.credentials.acceptCredential({ + credentialRecordId: aliceCredentialRecord.id, + }) + + testLogger.test('Faber waits for state done') + await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + }) + + test('Faber issues credential which is then deleted from Alice`s wallet', async () => { + const { holderCredentialExchangeRecord } = await issueAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + issuerHolderConnectionId: faberConnectionId, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + offer: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }) + + // test that delete credential removes from both repository and wallet + // latter is tested by spying on holder service to + // see if deleteCredential is called + const holderService = aliceAgent.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + + const deleteCredentialSpy = jest.spyOn(holderService, 'deleteCredential') + await aliceAgent.credentials.deleteById(holderCredentialExchangeRecord.id, { + deleteAssociatedCredentials: true, + deleteAssociatedDidCommMessages: true, + }) + expect(deleteCredentialSpy).toHaveBeenNthCalledWith( + 1, + aliceAgent.context, + holderCredentialExchangeRecord.credentials[0].credentialRecordId + ) + + return expect(aliceAgent.credentials.getById(holderCredentialExchangeRecord.id)).rejects.toThrowError( + `CredentialRecord: record with id ${holderCredentialExchangeRecord.id} not found.` + ) + }) + + test('Alice starts with proposal, faber sends a counter offer, alice sends second proposal, faber sends second offer', async () => { + // proposeCredential -> negotiateProposal -> negotiateOffer -> negotiateProposal -> acceptOffer -> acceptRequest -> DONE (credential issued) + + let faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Alice sends credential proposal to Faber') + let aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + credentialFormats: { + anoncreds: { + ...anonCredsCredentialProposal, + attributes: credentialPreview.attributes, + }, + }, + comment: 'v2 propose credential test', + }) + expect(aliceCredentialExchangeRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await faberCredentialRecordPromise + + let aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + faberCredentialRecord = await faberAgent.credentials.negotiateProposal({ + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await aliceCredentialRecordPromise + + // Check if the state of the credential records did not change + faberCredentialRecord = await faberAgent.credentials.getById(faberCredentialRecord.id) + faberCredentialRecord.assertState(CredentialState.OfferSent) + + aliceCredentialRecord = await aliceAgent.credentials.getById(aliceCredentialRecord.id) + aliceCredentialRecord.assertState(CredentialState.OfferReceived) + + faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + // second proposal + aliceCredentialExchangeRecord = await aliceAgent.credentials.negotiateOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + anoncreds: { + ...anonCredsCredentialProposal, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + expect(aliceCredentialExchangeRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + faberCredentialRecord = await faberAgent.credentials.negotiateProposal({ + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + + aliceCredentialRecord = await aliceCredentialRecordPromise + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialExchangeRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + state: CredentialState.RequestSent, + protocolVersion: 'v2', + threadId: aliceCredentialExchangeRecord.threadId, + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.RequestReceived, + }) + testLogger.test('Faber sends credential to Alice') + + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + // testLogger.test('Alice sends credential ack to Faber') + await aliceAgent.credentials.acceptCredential({ credentialRecordId: aliceCredentialRecord.id }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: expect.any(String), + connectionId: expect.any(String), + state: CredentialState.CredentialReceived, + }) + }) + + test('Faber starts with offer, alice sends counter proposal, faber sends second offer, alice sends second proposal', async () => { + let aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + let faberCredentialRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + protocolVersion: 'v2', + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await aliceCredentialRecordPromise + + let faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + aliceCredentialRecord = await aliceAgent.credentials.negotiateOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + anoncreds: { + ...anonCredsCredentialProposal, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + expect(aliceCredentialRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + faberCredentialRecord = await faberAgent.credentials.negotiateProposal({ + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + + aliceCredentialRecord = await aliceCredentialRecordPromise + + faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + aliceCredentialRecord = await aliceAgent.credentials.negotiateOffer({ + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + anoncreds: { + ...anonCredsCredentialProposal, + attributes: newCredentialPreview.attributes, + }, + }, + }) + + expect(aliceCredentialRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Proposal', + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }, + }) + + testLogger.test('Alice waits for credential offer from Faber') + aliceCredentialRecord = await aliceCredentialRecordPromise + + faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + const offerCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + expect(offerCredentialExchangeRecord).toMatchObject({ + connectionId: aliceConnectionId, + state: CredentialState.RequestSent, + protocolVersion: 'v2', + }) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await faberCredentialRecordPromise + + aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Faber sends credential to Alice') + await faberAgent.credentials.acceptRequest({ + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 AnonCreds Credential', + }) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await aliceCredentialRecordPromise + + const proposalMessage = await aliceAgent.credentials.findProposalMessage(aliceCredentialRecord.id) + const offerMessage = await aliceAgent.credentials.findOfferMessage(aliceCredentialRecord.id) + const requestMessage = await aliceAgent.credentials.findRequestMessage(aliceCredentialRecord.id) + const credentialMessage = await aliceAgent.credentials.findCredentialMessage(aliceCredentialRecord.id) + + expect(proposalMessage).toBeInstanceOf(V2ProposeCredentialMessage) + expect(offerMessage).toBeInstanceOf(V2OfferCredentialMessage) + expect(requestMessage).toBeInstanceOf(V2RequestCredentialMessage) + expect(credentialMessage).toBeInstanceOf(V2IssueCredentialMessage) + + const formatData = await aliceAgent.credentials.getFormatData(aliceCredentialRecord.id) + expect(formatData).toMatchObject({ + proposalAttributes: [ + { + name: 'name', + mimeType: 'text/plain', + value: 'John', + }, + { + name: 'age', + mimeType: 'text/plain', + value: '99', + }, + { + name: 'x-ray', + mimeType: 'text/plain', + value: 'another x-ray value', + }, + { + name: 'profile_picture', + mimeType: 'text/plain', + value: 'another profile picture', + }, + ], + proposal: { + anoncreds: { + schema_issuer_did: expect.any(String), + schema_id: expect.any(String), + schema_name: expect.any(String), + schema_version: expect.any(String), + cred_def_id: expect.any(String), + issuer_did: expect.any(String), + }, + }, + offer: { + anoncreds: { + schema_id: expect.any(String), + cred_def_id: expect.any(String), + key_correctness_proof: expect.any(Object), + nonce: expect.any(String), + }, + }, + offerAttributes: [ + { + name: 'name', + mimeType: 'text/plain', + value: 'John', + }, + { + name: 'age', + mimeType: 'text/plain', + value: '99', + }, + { + name: 'x-ray', + mimeType: 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + mimeType: 'text/plain', + value: 'profile picture', + }, + ], + request: { + anoncreds: { + entropy: expect.any(String), + cred_def_id: expect.any(String), + blinded_ms: expect.any(Object), + blinded_ms_correctness_proof: expect.any(Object), + nonce: expect.any(String), + }, + }, + credential: { + anoncreds: { + schema_id: expect.any(String), + cred_def_id: expect.any(String), + rev_reg_id: null, + values: { + age: { raw: '99', encoded: '99' }, + profile_picture: { + raw: 'profile picture', + encoded: '28661874965215723474150257281172102867522547934697168414362313592277831163345', + }, + name: { + raw: 'John', + encoded: '76355713903561865866741292988746191972523015098789458240077478826513114743258', + }, + 'x-ray': { + raw: 'some x-ray', + encoded: '43715611391396952879378357808399363551139229809726238083934532929974486114650', + }, + }, + signature: expect.any(Object), + signature_correctness_proof: expect.any(Object), + rev_reg: null, + witness: null, + }, + }, + }) + }) + + test('Faber starts with V2 offer, alice declines the offer', async () => { + testLogger.test('Faber sends credential offer to Alice') + const faberCredentialExchangeRecord = await faberAgent.credentials.offerCredential({ + comment: 'some comment about credential', + connectionId: faberConnectionId, + credentialFormats: { + anoncreds: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credentialDefinitionId, + }, + }, + protocolVersion: 'v2', + }) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + expect(aliceCredentialRecord).toMatchObject({ + id: expect.any(String), + type: CredentialExchangeRecord.type, + }) + + testLogger.test('Alice declines offer') + aliceCredentialRecord = await aliceAgent.credentials.declineOffer(aliceCredentialRecord.id) + + expect(aliceCredentialRecord.state).toBe(CredentialState.Declined) + }) +}) diff --git a/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts b/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts new file mode 100644 index 0000000000..b304ef5e73 --- /dev/null +++ b/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts @@ -0,0 +1,1007 @@ +import type { AnonCredsTestsAgent } from './anoncredsSetup' +import type { EventReplaySubject } from '../../core/tests' +import type { AnonCredsRequestProofFormat } from '@aries-framework/anoncreds' +import type { CredentialExchangeRecord } from '@aries-framework/core' + +import { + Attachment, + AttachmentData, + LinkedAttachment, + ProofState, + ProofExchangeRecord, + V2ProposePresentationMessage, + V2RequestPresentationMessage, + V2PresentationMessage, +} from '@aries-framework/core' + +import { dateToTimestamp } from '../../anoncreds/src/utils/timestamp' +import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' +import { sleep } from '../../core/src/utils/sleep' +import { waitForProofExchangeRecord } from '../../core/tests' +import testLogger from '../../core/tests/logger' + +import { issueAnonCredsCredential, setupAnonCredsTests } from './anoncredsSetup' + +describe('PP V2 AnonCreds Proofs', () => { + let faberAgent: AnonCredsTestsAgent + let faberReplay: EventReplaySubject + let aliceAgent: AnonCredsTestsAgent + let aliceReplay: EventReplaySubject + let credentialDefinitionId: string + let revocationRegistryDefinitionId: string | undefined + let aliceConnectionId: string + let faberConnectionId: string + let faberProofExchangeRecord: ProofExchangeRecord + let aliceProofExchangeRecord: ProofExchangeRecord + let faberCredentialExchangeRecord: CredentialExchangeRecord + + const inMemoryRegistry = new InMemoryAnonCredsRegistry() + + const issuerId = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + revocationRegistryDefinitionId, + //revocationStatusListTimestamp, + issuerHolderConnectionId: faberConnectionId, + holderIssuerConnectionId: aliceConnectionId, + } = await setupAnonCredsTests({ + issuerId, + issuerName: 'Faber agent AnonCreds proofs', + holderName: 'Alice agent AnonCreds proofs', + attributeNames: ['name', 'age', 'image_0', 'image_1'], + registries: [inMemoryRegistry], + supportRevocation: true, + })) + ;({ issuerCredentialExchangeRecord: faberCredentialExchangeRecord } = await issueAnonCredsCredential({ + issuerAgent: faberAgent, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerReplay: faberReplay, + issuerHolderConnectionId: faberConnectionId, + revocationRegistryDefinitionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'John', + }, + { + name: 'age', + value: '99', + }, + ], + linkedAttachments: [ + new LinkedAttachment({ + name: 'image_0', + attachment: new Attachment({ + filename: 'picture-of-a-cat.png', + data: new AttachmentData({ base64: 'cGljdHVyZSBvZiBhIGNhdA==' }), + }), + }), + new LinkedAttachment({ + name: 'image_1', + attachment: new Attachment({ + filename: 'picture-of-a-dog.png', + data: new AttachmentData({ base64: 'UGljdHVyZSBvZiBhIGRvZw==' }), + }), + }), + ], + }, + })) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with proof proposal to Faber', async () => { + // Alice sends a presentation proposal to Faber + testLogger.test('Alice sends a presentation proposal to Faber') + + let faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + state: ProofState.ProposalReceived, + }) + + aliceProofExchangeRecord = await aliceAgent.proofs.proposeProof({ + connectionId: aliceConnectionId, + protocolVersion: 'v2', + proofFormats: { + anoncreds: { + name: 'abc', + version: '1.0', + attributes: [ + { + name: 'name', + value: 'Alice', + credentialDefinitionId, + }, + ], + predicates: [ + { + name: 'age', + predicate: '>=', + threshold: 50, + credentialDefinitionId, + }, + ], + }, + }, + }) + + // Faber waits for a presentation proposal from Alice + testLogger.test('Faber waits for a presentation proposal from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const proposal = await faberAgent.proofs.findProposalMessage(faberProofExchangeRecord.id) + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + proposalAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v2', + }) + + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber accepts the presentation proposal from Alice + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofExchangeRecord = await faberAgent.proofs.acceptProposal({ + proofRecordId: faberProofExchangeRecord.id, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: requestedCredentials.proofFormats.anoncreds }, + }) + + // Faber waits for the presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof@v1.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation provided by Alice + testLogger.test('Faber accepts the presentation provided by Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she received a presentation acknowledgement + testLogger.test('Alice waits until she receives a presentation acknowledgement') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + + const proposalMessage = await aliceAgent.proofs.findProposalMessage(aliceProofExchangeRecord.id) + const requestMessage = await aliceAgent.proofs.findRequestMessage(aliceProofExchangeRecord.id) + const presentationMessage = await aliceAgent.proofs.findPresentationMessage(aliceProofExchangeRecord.id) + + expect(proposalMessage).toBeInstanceOf(V2ProposePresentationMessage) + expect(requestMessage).toBeInstanceOf(V2RequestPresentationMessage) + expect(presentationMessage).toBeInstanceOf(V2PresentationMessage) + + const formatData = await aliceAgent.proofs.getFormatData(aliceProofExchangeRecord.id) + + expect(formatData).toMatchObject({ + proposal: { + anoncreds: { + name: 'abc', + version: '1.0', + nonce: expect.any(String), + requested_attributes: { + [Object.keys(formatData.proposal?.anoncreds?.requested_attributes ?? {})[0]]: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + [Object.keys(formatData.proposal?.anoncreds?.requested_predicates ?? {})[0]]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + request: { + anoncreds: { + name: 'abc', + version: '1.0', + nonce: expect.any(String), + requested_attributes: { + [Object.keys(formatData.request?.anoncreds?.requested_attributes ?? {})[0]]: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + [Object.keys(formatData.request?.anoncreds?.requested_predicates ?? {})[0]]: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + presentation: { + anoncreds: { + proof: { + proofs: [ + { + primary_proof: expect.any(Object), + non_revoc_proof: null, + }, + { + primary_proof: expect.any(Object), + non_revoc_proof: null, + }, + ], + aggregated_proof: { + c_hash: expect.any(String), + c_list: expect.any(Array), + }, + }, + requested_proof: expect.any(Object), + identifiers: expect.any(Array), + }, + }, + }) + }) + + test('Faber starts with proof request to Alice', async () => { + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: { + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + + expect(aliceProofExchangeRecord.id).not.toBeNull() + expect(aliceProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: requestedCredentials.proofFormats.anoncreds }, + }) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + const presentation = await faberAgent.proofs.findPresentationMessage(faberProofExchangeRecord.id) + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof@v1.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofExchangeRecord.threadId, + }, + }) + expect(faberProofExchangeRecord.id).not.toBeNull() + expect(faberProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation + testLogger.test('Faber accept the presentation from Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she receives a presentation acknowledgement + testLogger.test('Alice waits for acceptance by Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + type: ProofExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) + + test('Alice provides credentials via call to getRequestedCredentials', async () => { + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: { + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const retrievedCredentials = await aliceAgent.proofs.getCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + expect(retrievedCredentials).toMatchObject({ + proofFormats: { + anoncreds: { + attributes: { + name: [ + { + credentialId: expect.any(String), + revealed: true, + credentialInfo: { + credentialId: expect.any(String), + attributes: { + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + name: 'John', + age: '99', + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + credentialRevocationId: '1', + revocationRegistryId: revocationRegistryDefinitionId, + }, + }, + ], + image_0: [ + { + credentialId: expect.any(String), + revealed: true, + credentialInfo: { + credentialId: expect.any(String), + attributes: { + age: '99', + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + name: 'John', + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + credentialRevocationId: '1', + revocationRegistryId: revocationRegistryDefinitionId, + }, + }, + ], + }, + predicates: { + age: [ + { + credentialId: expect.any(String), + credentialInfo: { + credentialId: expect.any(String), + attributes: { + image_1: 'hl:zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', + image_0: 'hl:zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', + name: 'John', + age: '99', + }, + schemaId: expect.any(String), + credentialDefinitionId: expect.any(String), + credentialRevocationId: '1', + revocationRegistryId: revocationRegistryDefinitionId, + }, + }, + ], + }, + }, + }, + }) + }) + + test('Faber starts with proof request to Alice but gets Problem Reported', async () => { + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: { + name: 'proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + }, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + const request = await faberAgent.proofs.findRequestMessage(faberProofExchangeRecord.id) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'anoncreds/proof-request@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + + expect(aliceProofExchangeRecord.id).not.toBeNull() + expect(aliceProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Abandoned, + }) + + aliceProofExchangeRecord = await aliceAgent.proofs.sendProblemReport({ + description: 'Problem inside proof request', + proofRecordId: aliceProofExchangeRecord.id, + }) + + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Abandoned, + protocolVersion: 'v2', + }) + }) + + test('Credential is revoked after proof request and before presentation', async () => { + let aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + const nrpRequestedTime = dateToTimestamp(new Date()) + 1 + + const requestProofFormat: AnonCredsRequestProofFormat = { + non_revoked: { from: nrpRequestedTime, to: nrpRequestedTime }, + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + } + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: requestProofFormat, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + // Revoke the credential + const credentialRevocationRegistryDefinitionId = faberCredentialExchangeRecord.getTag( + 'anonCredsRevocationRegistryId' + ) as string + const credentialRevocationIndex = faberCredentialExchangeRecord.getTag('anonCredsCredentialRevocationId') as string + + expect(credentialRevocationRegistryDefinitionId).toBeDefined() + expect(credentialRevocationIndex).toBeDefined() + + // FIXME: do not use delays. Maybe we can add the timestamp to parameters? + // InMemoryAnonCredsRegistry would respect what we ask while actual VDRs will use their own + await sleep(2000) + await faberAgent.modules.anoncreds.updateRevocationStatusList({ + revocationRegistryDefinitionId: credentialRevocationRegistryDefinitionId, + revokedCredentialIndexes: [Number(credentialRevocationIndex)], + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: requestedCredentials.proofFormats.anoncreds }, + }) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Faber accepts the presentation + testLogger.test('Faber accept the presentation from Alice') + await faberAgent.proofs.acceptPresentation({ proofRecordId: faberProofExchangeRecord.id }) + + // Alice waits until she receives a presentation acknowledgement + testLogger.test('Alice waits for acceptance by Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + expect(faberProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofExchangeRecord).toMatchObject({ + threadId: faberProofExchangeRecord.threadId, + state: ProofState.Done, + }) + }) + + test.only('Credential is revoked before proof request', async () => { + // Revoke the credential + const credentialRevocationRegistryDefinitionId = faberCredentialExchangeRecord.getTag( + 'anonCredsRevocationRegistryId' + ) as string + const credentialRevocationIndex = faberCredentialExchangeRecord.getTag('anonCredsCredentialRevocationId') as string + + expect(credentialRevocationRegistryDefinitionId).toBeDefined() + expect(credentialRevocationIndex).toBeDefined() + + const { revocationStatusListState } = await faberAgent.modules.anoncreds.updateRevocationStatusList({ + revocationRegistryDefinitionId: credentialRevocationRegistryDefinitionId, + revokedCredentialIndexes: [Number(credentialRevocationIndex)], + }) + + expect(revocationStatusListState.revocationStatusList).toBeDefined() + const revokedTimestamp = revocationStatusListState.revocationStatusList?.timestamp + + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + const nrpRequestedTime = (revokedTimestamp ?? dateToTimestamp(new Date())) + 1 + + const requestProofFormat: AnonCredsRequestProofFormat = { + non_revoked: { from: nrpRequestedTime, to: nrpRequestedTime }, + name: 'Proof Request', + version: '1.0.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + image_0: { + name: 'image_0', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: { + age: { + name: 'age', + p_type: '>=', + p_value: 50, + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + } + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofExchangeRecord = await faberAgent.proofs.requestProof({ + protocolVersion: 'v2', + connectionId: faberConnectionId, + proofFormats: { + anoncreds: requestProofFormat, + }, + }) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.selectCredentialsForRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: { filterByNonRevocationRequirements: false } }, + }) + + const faberProofExchangeRecordPromise = waitForProofExchangeRecord(faberAgent, { + threadId: aliceProofExchangeRecord.threadId, + }) + + await aliceAgent.proofs.acceptRequest({ + proofRecordId: aliceProofExchangeRecord.id, + proofFormats: { anoncreds: requestedCredentials.proofFormats.anoncreds }, + }) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofExchangeRecord = await faberProofExchangeRecordPromise + + // Faber receives presentation and checks that it is not valid + expect(faberProofExchangeRecord).toMatchObject({ + threadId: aliceProofExchangeRecord.threadId, + isVerified: false, + state: ProofState.Abandoned, + }) + + // Faber will send a problem report, meaning for Alice that the proof state is abandoned + // as well + await waitForProofExchangeRecord(aliceAgent, { + threadId: aliceProofExchangeRecord.threadId, + state: ProofState.Abandoned, + }) + }) +}) diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index 5659f5a813..92d741308b 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -1,6 +1,9 @@ import type { AnonCredsCreateLinkSecretOptions, AnonCredsRegisterCredentialDefinitionOptions, + AnonCredsRegisterRevocationRegistryDefinitionOptions, + AnonCredsRegisterRevocationStatusListOptions, + AnonCredsUpdateRevocationStatusListOptions, } from './AnonCredsApiOptions' import type { AnonCredsCredentialDefinition, AnonCredsSchema } from './models' import type { @@ -12,6 +15,8 @@ import type { GetSchemaReturn, RegisterCredentialDefinitionReturn, RegisterSchemaReturn, + RegisterRevocationRegistryDefinitionReturn, + RegisterRevocationStatusListReturn, } from './services' import type { Extensible } from './services/registry/base' import type { SimpleQuery } from '@aries-framework/core' @@ -21,17 +26,23 @@ import { AgentContext, inject, injectable } from '@aries-framework/core' import { AnonCredsModuleConfig } from './AnonCredsModuleConfig' import { AnonCredsStoreRecordError } from './error' import { + AnonCredsRevocationRegistryDefinitionPrivateRecord, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRepository, AnonCredsCredentialDefinitionPrivateRecord, AnonCredsCredentialDefinitionPrivateRepository, AnonCredsKeyCorrectnessProofRecord, AnonCredsKeyCorrectnessProofRepository, AnonCredsLinkSecretRepository, + AnonCredsRevocationRegistryDefinitionRecord, + AnonCredsRevocationRegistryState, } from './repository' import { AnonCredsCredentialDefinitionRecord } from './repository/AnonCredsCredentialDefinitionRecord' import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository' import { AnonCredsSchemaRecord } from './repository/AnonCredsSchemaRecord' import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository' import { AnonCredsCredentialDefinitionRecordMetadataKeys } from './repository/anonCredsCredentialDefinitionRecordMetadataTypes' +import { AnonCredsRevocationRegistryDefinitionRecordMetadataKeys } from './repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes' import { AnonCredsHolderService, AnonCredsHolderServiceSymbol, @@ -39,7 +50,7 @@ import { AnonCredsIssuerServiceSymbol, } from './services' import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' -import { storeLinkSecret } from './utils' +import { dateToTimestamp, storeLinkSecret } from './utils' @injectable() export class AnonCredsApi { @@ -50,6 +61,8 @@ export class AnonCredsApi { private anonCredsSchemaRepository: AnonCredsSchemaRepository private anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository private anonCredsCredentialDefinitionPrivateRepository: AnonCredsCredentialDefinitionPrivateRepository + private anonCredsRevocationRegistryDefinitionRepository: AnonCredsRevocationRegistryDefinitionRepository + private anonCredsRevocationRegistryDefinitionPrivateRepository: AnonCredsRevocationRegistryDefinitionPrivateRepository private anonCredsKeyCorrectnessProofRepository: AnonCredsKeyCorrectnessProofRepository private anonCredsLinkSecretRepository: AnonCredsLinkSecretRepository private anonCredsIssuerService: AnonCredsIssuerService @@ -62,6 +75,8 @@ export class AnonCredsApi { @inject(AnonCredsIssuerServiceSymbol) anonCredsIssuerService: AnonCredsIssuerService, @inject(AnonCredsHolderServiceSymbol) anonCredsHolderService: AnonCredsHolderService, anonCredsSchemaRepository: AnonCredsSchemaRepository, + anonCredsRevocationRegistryDefinitionRepository: AnonCredsRevocationRegistryDefinitionRepository, + anonCredsRevocationRegistryDefinitionPrivateRepository: AnonCredsRevocationRegistryDefinitionPrivateRepository, anonCredsCredentialDefinitionRepository: AnonCredsCredentialDefinitionRepository, anonCredsCredentialDefinitionPrivateRepository: AnonCredsCredentialDefinitionPrivateRepository, anonCredsKeyCorrectnessProofRepository: AnonCredsKeyCorrectnessProofRepository, @@ -73,6 +88,8 @@ export class AnonCredsApi { this.anonCredsIssuerService = anonCredsIssuerService this.anonCredsHolderService = anonCredsHolderService this.anonCredsSchemaRepository = anonCredsSchemaRepository + this.anonCredsRevocationRegistryDefinitionRepository = anonCredsRevocationRegistryDefinitionRepository + this.anonCredsRevocationRegistryDefinitionPrivateRepository = anonCredsRevocationRegistryDefinitionPrivateRepository this.anonCredsCredentialDefinitionRepository = anonCredsCredentialDefinitionRepository this.anonCredsCredentialDefinitionPrivateRepository = anonCredsCredentialDefinitionPrivateRepository this.anonCredsKeyCorrectnessProofRepository = anonCredsKeyCorrectnessProofRepository @@ -258,7 +275,7 @@ export class AnonCredsApi { issuerId: options.credentialDefinition.issuerId, schemaId: options.credentialDefinition.schemaId, tag: options.credentialDefinition.tag, - supportRevocation: false, + supportRevocation: options.supportRevocation, schema: schemaResult.schema, }, // FIXME: Indy SDK requires the schema seq no to be passed in here. This is not ideal. @@ -338,6 +355,77 @@ export class AnonCredsApi { } } + public async registerRevocationRegistryDefinition(options: { + revocationRegistryDefinition: AnonCredsRegisterRevocationRegistryDefinitionOptions + options: Extensible + }): Promise { + const { issuerId, tag, credentialDefinitionId, maximumCredentialNumber } = options.revocationRegistryDefinition + + const tailsFileService = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + + const tailsDirectoryPath = await tailsFileService.getTailsBasePath(this.agentContext) + + const failedReturnBase = { + revocationRegistryDefinitionState: { + state: 'failed' as const, + reason: `Error registering revocation registry definition for issuerId ${issuerId}`, + }, + registrationMetadata: {}, + revocationRegistryDefinitionMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(issuerId) + if (!registry) { + failedReturnBase.revocationRegistryDefinitionState.reason = `Unable to register revocation registry definition. No registry found for issuerId ${issuerId}` + return failedReturnBase + } + + const { credentialDefinition } = await registry.getCredentialDefinition(this.agentContext, credentialDefinitionId) + + if (!credentialDefinition) { + failedReturnBase.revocationRegistryDefinitionState.reason = `Unable to register revocation registry definition. No credential definition found for id ${credentialDefinitionId}` + return failedReturnBase + } + try { + const { revocationRegistryDefinition, revocationRegistryDefinitionPrivate } = + await this.anonCredsIssuerService.createRevocationRegistryDefinition(this.agentContext, { + issuerId, + tag, + credentialDefinitionId, + credentialDefinition, + maximumCredentialNumber, + tailsDirectoryPath, + }) + + // At this moment, tails file should be published and a valid public URL will be received + const localTailsLocation = revocationRegistryDefinition.value.tailsLocation + + revocationRegistryDefinition.value.tailsLocation = await tailsFileService.uploadTailsFile(this.agentContext, { + revocationRegistryDefinition, + }) + + const result = await registry.registerRevocationRegistryDefinition(this.agentContext, { + revocationRegistryDefinition, + options: {}, + }) + await this.storeRevocationRegistryDefinitionRecord(result, revocationRegistryDefinitionPrivate) + + return { + ...result, + revocationRegistryDefinitionMetadata: { ...result.revocationRegistryDefinitionMetadata, localTailsLocation }, + } + } catch (error) { + // Storage failed + if (error instanceof AnonCredsStoreRecordError) { + failedReturnBase.revocationRegistryDefinitionState.reason = `Error storing revocation registry definition records: ${error.message}` + return failedReturnBase + } + + failedReturnBase.revocationRegistryDefinitionState.reason = `Error registering revocation registry definition: ${error.message}` + return failedReturnBase + } + } + /** * Retrieve the {@link AnonCredsRevocationStatusList} for the given {@link timestamp} from the registry associated * with the {@link revocationRegistryDefinitionId} @@ -374,6 +462,139 @@ export class AnonCredsApi { } } + public async registerRevocationStatusList(options: { + revocationStatusList: AnonCredsRegisterRevocationStatusListOptions + options: Extensible + }): Promise { + const { issuerId, revocationRegistryDefinitionId } = options.revocationStatusList + + const failedReturnBase = { + revocationStatusListState: { + state: 'failed' as const, + reason: `Error registering revocation status list for issuerId ${issuerId}`, + }, + registrationMetadata: {}, + revocationStatusListMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(issuerId) + if (!registry) { + failedReturnBase.revocationStatusListState.reason = `Unable to register revocation status list. No registry found for issuerId ${issuerId}` + return failedReturnBase + } + + const { revocationRegistryDefinition } = await registry.getRevocationRegistryDefinition( + this.agentContext, + revocationRegistryDefinitionId + ) + + if (!revocationRegistryDefinition) { + failedReturnBase.revocationStatusListState.reason = `Unable to register revocation status list. No revocation registry definition found for ${revocationRegistryDefinitionId}` + return failedReturnBase + } + const tailsFileService = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + const tailsFilePath = await tailsFileService.getTailsFile(this.agentContext, { + revocationRegistryDefinition, + }) + + try { + const revocationStatusList = await this.anonCredsIssuerService.createRevocationStatusList(this.agentContext, { + issuerId, + revocationRegistryDefinition, + revocationRegistryDefinitionId, + tailsFilePath, + }) + + const result = await registry.registerRevocationStatusList(this.agentContext, { + revocationStatusList, + options: {}, + }) + + return result + } catch (error) { + // Storage failed + if (error instanceof AnonCredsStoreRecordError) { + failedReturnBase.revocationStatusListState.reason = `Error storing revocation status list records: ${error.message}` + return failedReturnBase + } + + failedReturnBase.revocationStatusListState.reason = `Error registering revocation status list: ${error.message}` + return failedReturnBase + } + } + + public async updateRevocationStatusList( + options: AnonCredsUpdateRevocationStatusListOptions + ): Promise { + const { issuedCredentialIndexes, revokedCredentialIndexes, revocationRegistryDefinitionId } = options + + const failedReturnBase = { + revocationStatusListState: { + state: 'failed' as const, + reason: `Error updating revocation status list for revocation registry definition id ${options.revocationRegistryDefinitionId}`, + }, + registrationMetadata: {}, + revocationStatusListMetadata: {}, + } + + const registry = this.findRegistryForIdentifier(options.revocationRegistryDefinitionId) + if (!registry) { + failedReturnBase.revocationStatusListState.reason = `Unable to update revocation status list. No registry found for id ${options.revocationRegistryDefinitionId}` + return failedReturnBase + } + + const { revocationRegistryDefinition } = await registry.getRevocationRegistryDefinition( + this.agentContext, + revocationRegistryDefinitionId + ) + + if (!revocationRegistryDefinition) { + failedReturnBase.revocationStatusListState.reason = `Unable to update revocation status list. No revocation registry definition found for ${revocationRegistryDefinitionId}` + return failedReturnBase + } + + const { revocationStatusList: previousRevocationStatusList } = await this.getRevocationStatusList( + revocationRegistryDefinitionId, + dateToTimestamp(new Date()) + ) + + if (!previousRevocationStatusList) { + failedReturnBase.revocationStatusListState.reason = `Unable to update revocation status list. No previous revocation status list found for ${options.revocationRegistryDefinitionId}` + return failedReturnBase + } + + const tailsFileService = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + const tailsFilePath = await tailsFileService.getTailsFile(this.agentContext, { + revocationRegistryDefinition, + }) + + try { + const revocationStatusList = await this.anonCredsIssuerService.updateRevocationStatusList(this.agentContext, { + issued: issuedCredentialIndexes, + revoked: revokedCredentialIndexes, + revocationStatusList: previousRevocationStatusList, + revocationRegistryDefinition, + tailsFilePath, + }) + + const result = await registry.registerRevocationStatusList(this.agentContext, { + revocationStatusList, + options: {}, + }) + + return result + } catch (error) { + // Storage failed + if (error instanceof AnonCredsStoreRecordError) { + failedReturnBase.revocationStatusListState.reason = `Error storing revocation status list records: ${error.message}` + return failedReturnBase + } + + failedReturnBase.revocationStatusListState.reason = `Error registering revocation status list: ${error.message}` + return failedReturnBase + } + } + public async getCredential(credentialId: string) { return this.anonCredsHolderService.getCredential(this.agentContext, { credentialId }) } @@ -382,6 +603,59 @@ export class AnonCredsApi { return this.anonCredsHolderService.getCredentials(this.agentContext, options) } + private async storeRevocationRegistryDefinitionRecord( + result: RegisterRevocationRegistryDefinitionReturn, + revocationRegistryDefinitionPrivate?: Record + ): Promise { + try { + // If we have both the revocationRegistryDefinition and the revocationRegistryDefinitionId we will store a copy + // of the credential definition. We may need to handle an edge case in the future where we e.g. don't have the + // id yet, and it is registered through a different channel + if ( + result.revocationRegistryDefinitionState.revocationRegistryDefinition && + result.revocationRegistryDefinitionState.revocationRegistryDefinitionId + ) { + const revocationRegistryDefinitionRecord = new AnonCredsRevocationRegistryDefinitionRecord({ + revocationRegistryDefinitionId: result.revocationRegistryDefinitionState.revocationRegistryDefinitionId, + revocationRegistryDefinition: result.revocationRegistryDefinitionState.revocationRegistryDefinition, + }) + + // TODO: do we need to store this metadata? For indy, the registration metadata contains e.g. + // the indyLedgerSeqNo and the didIndyNamespace, but it can get quite big if complete transactions + // are stored in the metadata + revocationRegistryDefinitionRecord.metadata.set( + AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.RevocationRegistryDefinitionMetadata, + result.revocationRegistryDefinitionMetadata + ) + revocationRegistryDefinitionRecord.metadata.set( + AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.RevocationRegistryDefinitionRegistrationMetadata, + result.registrationMetadata + ) + + await this.anonCredsRevocationRegistryDefinitionRepository.save( + this.agentContext, + revocationRegistryDefinitionRecord + ) + + // Store Revocation Registry Definition private data (if provided by issuer service) + if (revocationRegistryDefinitionPrivate) { + const revocationRegistryDefinitionPrivateRecord = new AnonCredsRevocationRegistryDefinitionPrivateRecord({ + revocationRegistryDefinitionId: result.revocationRegistryDefinitionState.revocationRegistryDefinitionId, + credentialDefinitionId: result.revocationRegistryDefinitionState.revocationRegistryDefinition.credDefId, + value: revocationRegistryDefinitionPrivate, + state: AnonCredsRevocationRegistryState.Active, + }) + await this.anonCredsRevocationRegistryDefinitionPrivateRepository.save( + this.agentContext, + revocationRegistryDefinitionPrivateRecord + ) + } + } + } catch (error) { + throw new AnonCredsStoreRecordError(`Error storing revocation registry definition records`, { cause: error }) + } + } + private async storeCredentialDefinitionPrivateAndKeyCorrectnessRecord( result: RegisterCredentialDefinitionReturn, credentialDefinitionPrivate?: Record, @@ -482,6 +756,7 @@ export class AnonCredsApi { interface AnonCredsRegisterCredentialDefinition { credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions + supportRevocation: boolean options: T } diff --git a/packages/anoncreds/src/AnonCredsApiOptions.ts b/packages/anoncreds/src/AnonCredsApiOptions.ts index f70e422ec4..64bb1e1d61 100644 --- a/packages/anoncreds/src/AnonCredsApiOptions.ts +++ b/packages/anoncreds/src/AnonCredsApiOptions.ts @@ -1,10 +1,28 @@ -import type { AnonCredsCredentialDefinition } from './models' - export interface AnonCredsCreateLinkSecretOptions { linkSecretId?: string setAsDefault?: boolean } -export type AnonCredsRegisterCredentialDefinitionOptions = - | Omit - | AnonCredsCredentialDefinition +export interface AnonCredsRegisterCredentialDefinitionOptions { + issuerId: string + schemaId: string + tag: string +} + +export interface AnonCredsRegisterRevocationRegistryDefinitionOptions { + issuerId: string + tag: string + credentialDefinitionId: string + maximumCredentialNumber: number +} + +export interface AnonCredsRegisterRevocationStatusListOptions { + issuerId: string + revocationRegistryDefinitionId: string +} + +export interface AnonCredsUpdateRevocationStatusListOptions { + revokedCredentialIndexes?: number[] + issuedCredentialIndexes?: number[] + revocationRegistryDefinitionId: string +} diff --git a/packages/anoncreds/src/AnonCredsModule.ts b/packages/anoncreds/src/AnonCredsModule.ts index 873288348c..afc698beda 100644 --- a/packages/anoncreds/src/AnonCredsModule.ts +++ b/packages/anoncreds/src/AnonCredsModule.ts @@ -7,6 +7,8 @@ import { AnonCredsCredentialDefinitionPrivateRepository, AnonCredsKeyCorrectnessProofRepository, AnonCredsLinkSecretRepository, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRepository, } from './repository' import { AnonCredsCredentialDefinitionRepository } from './repository/AnonCredsCredentialDefinitionRepository' import { AnonCredsSchemaRepository } from './repository/AnonCredsSchemaRepository' @@ -36,6 +38,8 @@ export class AnonCredsModule implements Module { dependencyManager.registerSingleton(AnonCredsCredentialDefinitionPrivateRepository) dependencyManager.registerSingleton(AnonCredsKeyCorrectnessProofRepository) dependencyManager.registerSingleton(AnonCredsLinkSecretRepository) + dependencyManager.registerSingleton(AnonCredsRevocationRegistryDefinitionRepository) + dependencyManager.registerSingleton(AnonCredsRevocationRegistryDefinitionPrivateRepository) } public updates = [ diff --git a/packages/anoncreds/src/AnonCredsModuleConfig.ts b/packages/anoncreds/src/AnonCredsModuleConfig.ts index 9f7b971aab..c0cbbf9b48 100644 --- a/packages/anoncreds/src/AnonCredsModuleConfig.ts +++ b/packages/anoncreds/src/AnonCredsModuleConfig.ts @@ -1,4 +1,7 @@ import type { AnonCredsRegistry } from './services' +import type { TailsFileService } from './services/tails' + +import { BasicTailsFileService } from './services/tails' /** * @public @@ -9,6 +12,12 @@ export interface AnonCredsModuleConfigOptions { * A list of AnonCreds registries to make available to the AnonCreds module. */ registries: [AnonCredsRegistry, ...AnonCredsRegistry[]] + + /** + * Tails file service for download/uploading tails files + * @default BasicTailsFileService (only for downloading tails files) + */ + tailsFileService?: TailsFileService } /** @@ -25,4 +34,9 @@ export class AnonCredsModuleConfig { public get registries() { return this.options.registries } + + /** See {@link AnonCredsModuleConfigOptions.tailsFileService} */ + public get tailsFileService() { + return this.options.tailsFileService ?? new BasicTailsFileService() + } } diff --git a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts index f9c868c14c..02d0d13076 100644 --- a/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts +++ b/packages/anoncreds/src/__tests__/AnonCredsModule.test.ts @@ -9,6 +9,8 @@ import { AnonCredsCredentialDefinitionPrivateRepository, AnonCredsKeyCorrectnessProofRepository, AnonCredsLinkSecretRepository, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryDefinitionRepository, } from '../repository' import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' @@ -26,13 +28,17 @@ describe('AnonCredsModule', () => { }) anonCredsModule.register(dependencyManager) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(6) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(8) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsRegistryService) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsSchemaRepository) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsCredentialDefinitionRepository) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsCredentialDefinitionPrivateRepository) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsKeyCorrectnessProofRepository) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsLinkSecretRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(AnonCredsRevocationRegistryDefinitionRepository) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith( + AnonCredsRevocationRegistryDefinitionPrivateRepository + ) expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) expect(dependencyManager.registerInstance).toHaveBeenCalledWith(AnonCredsModuleConfig, anonCredsModule.config) diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts index dba5361a41..716265588a 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormat.ts @@ -42,6 +42,8 @@ export interface AnonCredsProposeCredentialFormat { */ export interface AnonCredsAcceptProposalFormat { credentialDefinitionId?: string + revocationRegistryDefinitionId?: string + revocationRegistryIndex?: number attributes?: CredentialPreviewAttributeOptions[] linkedAttachments?: LinkedAttachment[] } @@ -60,6 +62,8 @@ export interface AnonCredsAcceptOfferFormat { */ export interface AnonCredsOfferCredentialFormat { credentialDefinitionId: string + revocationRegistryDefinitionId?: string + revocationRegistryIndex?: number attributes: CredentialPreviewAttributeOptions[] linkedAttachments?: LinkedAttachment[] } diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index 28d7d47185..9a53590d12 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -43,8 +43,14 @@ import { import { AnonCredsError } from '../error' import { AnonCredsCredentialProposal } from '../models/AnonCredsCredentialProposal' +import { + AnonCredsCredentialDefinitionRepository, + AnonCredsRevocationRegistryDefinitionPrivateRepository, + AnonCredsRevocationRegistryState, +} from '../repository' import { AnonCredsIssuerServiceSymbol, AnonCredsHolderServiceSymbol } from '../services' import { AnonCredsRegistryService } from '../services/registry/AnonCredsRegistryService' +import { dateToTimestamp } from '../utils' import { convertAttributesToCredentialValues, assertCredentialValuesMatch, @@ -160,6 +166,8 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService attachmentId, attributes, credentialDefinitionId, + revocationRegistryDefinitionId: anoncredsFormat?.revocationRegistryDefinitionId, + revocationRegistryIndex: anoncredsFormat?.revocationRegistryIndex, linkedAttachments: anoncredsFormat?.linkedAttachments, }) @@ -188,6 +196,8 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService attachmentId, attributes: anoncredsFormat.attributes, credentialDefinitionId: anoncredsFormat.credentialDefinitionId, + revocationRegistryDefinitionId: anoncredsFormat.revocationRegistryDefinitionId, + revocationRegistryIndex: anoncredsFormat.revocationRegistryIndex, linkedAttachments: anoncredsFormat.linkedAttachments, }) @@ -303,23 +313,65 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService const credentialRequest = requestAttachment.getDataAsJson() if (!credentialRequest) throw new AriesFrameworkError('Missing anoncreds credential request in createCredential') - const { credential, credentialRevocationId } = await anonCredsIssuerService.createCredential(agentContext, { + // We check locally for credential definition info. If it supports revocation, we need to search locally for + // an active revocation registry + const credentialDefinition = ( + await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, credentialRequest.cred_def_id) + ).credentialDefinition.value + + let revocationRegistryDefinitionId + let revocationRegistryIndex + let revocationStatusList + + if (credentialDefinition.revocation) { + const credentialMetadata = + credentialRecord.metadata.get(AnonCredsCredentialMetadataKey) + revocationRegistryDefinitionId = credentialMetadata?.revocationRegistryId + if (credentialMetadata?.credentialRevocationId) { + revocationRegistryIndex = Number(credentialMetadata.credentialRevocationId) + } + + if (!revocationRegistryDefinitionId || !revocationRegistryIndex) { + throw new AriesFrameworkError( + 'Revocation registry definition id and revocation index are mandatory to issue AnonCreds revocable credentials' + ) + } + const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager + .resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository) + .getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId) + + if (revocationRegistryDefinitionPrivateRecord.state !== AnonCredsRevocationRegistryState.Active) { + throw new AriesFrameworkError( + `Revocation registry ${revocationRegistryDefinitionId} is in ${revocationRegistryDefinitionPrivateRecord.state} state` + ) + } + + const registryService = agentContext.dependencyManager.resolve(AnonCredsRegistryService) + const revocationStatusListResult = await registryService + .getRegistryForIdentifier(agentContext, revocationRegistryDefinitionId) + .getRevocationStatusList(agentContext, revocationRegistryDefinitionId, dateToTimestamp(new Date())) + + if (!revocationStatusListResult.revocationStatusList) { + throw new AriesFrameworkError( + `Unable to resolve revocation status list for ${revocationRegistryDefinitionId}: + ${revocationStatusListResult.resolutionMetadata.error} ${revocationStatusListResult.resolutionMetadata.message}` + ) + } + + revocationStatusList = revocationStatusListResult.revocationStatusList + } + + const { credential } = await anonCredsIssuerService.createCredential(agentContext, { credentialOffer, credentialRequest, credentialValues: convertAttributesToCredentialValues(credentialAttributes), + revocationRegistryDefinitionId, + revocationRegistryIndex, + revocationStatusList, }) - if (credential.rev_reg_id) { - credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, { - credentialRevocationId: credentialRevocationId, - revocationRegistryId: credential.rev_reg_id, - }) - credentialRecord.setTags({ - anonCredsRevocationRegistryId: credential.rev_reg_id, - anonCredsCredentialRevocationId: credentialRevocationId, - }) - } - const format = new CredentialFormatSpec({ attachmentId, format: ANONCREDS_CREDENTIAL, @@ -524,10 +576,14 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService credentialRecord, attachmentId, credentialDefinitionId, + revocationRegistryDefinitionId, + revocationRegistryIndex, attributes, linkedAttachments, }: { credentialDefinitionId: string + revocationRegistryDefinitionId?: string + revocationRegistryIndex?: number credentialRecord: CredentialExchangeRecord attachmentId?: string attributes: CredentialPreviewAttributeOptions[] @@ -554,9 +610,34 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService await this.assertPreviewAttributesMatchSchemaAttributes(agentContext, offer, previewAttributes) + // We check locally for credential definition info. If it supports revocation, revocationRegistryIndex + // and revocationRegistryDefinitionId are mandatory + const credentialDefinition = ( + await agentContext.dependencyManager + .resolve(AnonCredsCredentialDefinitionRepository) + .getByCredentialDefinitionId(agentContext, offer.cred_def_id) + ).credentialDefinition.value + + if (credentialDefinition.revocation) { + if (!revocationRegistryDefinitionId || !revocationRegistryIndex) { + throw new AriesFrameworkError( + 'AnonCreds revocable credentials require revocationRegistryDefinitionId and revocationRegistryIndex' + ) + } + + // Set revocation tags + credentialRecord.setTags({ + anonCredsRevocationRegistryId: revocationRegistryDefinitionId, + anonCredsCredentialRevocationId: revocationRegistryIndex.toString(), + }) + } + + // Set the metadata credentialRecord.metadata.set(AnonCredsCredentialMetadataKey, { schemaId: offer.schema_id, credentialDefinitionId: offer.cred_def_id, + credentialRevocationId: revocationRegistryIndex?.toString(), + revocationRegistryId: revocationRegistryDefinitionId, }) const attachment = this.getFormatData(offer, format.attachmentId) diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormat.ts b/packages/anoncreds/src/formats/AnonCredsProofFormat.ts index 0c326943f8..c25dd17bc8 100644 --- a/packages/anoncreds/src/formats/AnonCredsProofFormat.ts +++ b/packages/anoncreds/src/formats/AnonCredsProofFormat.ts @@ -34,6 +34,7 @@ export interface AnonCredsProposeProofFormat { version?: string attributes?: AnonCredsPresentationPreviewAttribute[] predicates?: AnonCredsPresentationPreviewPredicate[] + nonRevokedInterval?: AnonCredsNonRevokedInterval } /** diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts index 001aebb340..e996a16e6f 100644 --- a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts @@ -58,6 +58,7 @@ import { getRevocationRegistriesForRequest, getRevocationRegistriesForProof, } from '../utils' +import { dateToTimestamp } from '../utils/timestamp' const ANONCREDS_PRESENTATION_PROPOSAL = 'anoncreds/proof-request@v1.0' const ANONCREDS_PRESENTATION_REQUEST = 'anoncreds/proof-request@v1.0' @@ -86,6 +87,7 @@ export class AnonCredsProofFormatService implements ProofFormatService + index?: number + state?: AnonCredsRevocationRegistryState +} + +export type DefaultAnonCredsRevocationRegistryPrivateTags = { + revocationRegistryDefinitionId: string + credentialDefinitionId: string + state: AnonCredsRevocationRegistryState +} + +export class AnonCredsRevocationRegistryDefinitionPrivateRecord extends BaseRecord< + DefaultAnonCredsRevocationRegistryPrivateTags, + TagsBase +> { + public static readonly type = 'AnonCredsRevocationRegistryDefinitionPrivateRecord' + public readonly type = AnonCredsRevocationRegistryDefinitionPrivateRecord.type + + public readonly revocationRegistryDefinitionId!: string + public readonly credentialDefinitionId!: string + public readonly value!: Record // TODO: Define structure + + public state!: AnonCredsRevocationRegistryState + + public constructor(props: AnonCredsRevocationRegistryDefinitionPrivateRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.revocationRegistryDefinitionId = props.revocationRegistryDefinitionId + this.credentialDefinitionId = props.credentialDefinitionId + this.value = props.value + this.state = props.state ?? AnonCredsRevocationRegistryState.Created + } + } + + public getTags() { + return { + ...this._tags, + revocationRegistryDefinitionId: this.revocationRegistryDefinitionId, + credentialDefinitionId: this.credentialDefinitionId, + state: this.state, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts new file mode 100644 index 0000000000..0f571d2a1a --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionPrivateRepository.ts @@ -0,0 +1,36 @@ +import type { AnonCredsRevocationRegistryState } from './AnonCredsRevocationRegistryDefinitionPrivateRecord' +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' + +import { AnonCredsRevocationRegistryDefinitionPrivateRecord } from './AnonCredsRevocationRegistryDefinitionPrivateRecord' + +@injectable() +export class AnonCredsRevocationRegistryDefinitionPrivateRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) + storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsRevocationRegistryDefinitionPrivateRecord, storageService, eventEmitter) + } + + public async getByRevocationRegistryDefinitionId(agentContext: AgentContext, revocationRegistryDefinitionId: string) { + return this.getSingleByQuery(agentContext, { revocationRegistryDefinitionId }) + } + + public async findByRevocationRegistryDefinitionId( + agentContext: AgentContext, + revocationRegistryDefinitionId: string + ) { + return this.findSingleByQuery(agentContext, { revocationRegistryDefinitionId }) + } + + public async findAllByCredentialDefinitionIdAndState( + agentContext: AgentContext, + credentialDefinitionId: string, + state?: AnonCredsRevocationRegistryState + ) { + return this.findByQuery(agentContext, { credentialDefinitionId, state }) + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts new file mode 100644 index 0000000000..8a41227574 --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRecord.ts @@ -0,0 +1,46 @@ +import type { AnonCredsRevocationRegistryDefinitionRecordMetadata } from './anonCredsRevocationRegistryDefinitionRecordMetadataTypes' +import type { AnonCredsRevocationRegistryDefinition } from '../models' +import type { TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export interface AnonCredsRevocationRegistryDefinitionRecordProps { + id?: string + revocationRegistryDefinitionId: string + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition +} + +export type DefaultAnonCredsRevocationRegistryDefinitionTags = { + revocationRegistryDefinitionId: string + credentialDefinitionId: string +} + +export class AnonCredsRevocationRegistryDefinitionRecord extends BaseRecord< + DefaultAnonCredsRevocationRegistryDefinitionTags, + TagsBase, + AnonCredsRevocationRegistryDefinitionRecordMetadata +> { + public static readonly type = 'AnonCredsRevocationRegistryDefinitionRecord' + public readonly type = AnonCredsRevocationRegistryDefinitionRecord.type + + public readonly revocationRegistryDefinitionId!: string + public readonly revocationRegistryDefinition!: AnonCredsRevocationRegistryDefinition + + public constructor(props: AnonCredsRevocationRegistryDefinitionRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.revocationRegistryDefinitionId = props.revocationRegistryDefinitionId + this.revocationRegistryDefinition = props.revocationRegistryDefinition + } + } + + public getTags() { + return { + ...this._tags, + revocationRegistryDefinitionId: this.revocationRegistryDefinitionId, + credentialDefinitionId: this.revocationRegistryDefinition.credDefId, + } + } +} diff --git a/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRepository.ts b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRepository.ts new file mode 100644 index 0000000000..4b1890b09b --- /dev/null +++ b/packages/anoncreds/src/repository/AnonCredsRevocationRegistryDefinitionRepository.ts @@ -0,0 +1,31 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, InjectionSymbols, StorageService, EventEmitter, injectable, inject } from '@aries-framework/core' + +import { AnonCredsRevocationRegistryDefinitionRecord } from './AnonCredsRevocationRegistryDefinitionRecord' + +@injectable() +export class AnonCredsRevocationRegistryDefinitionRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) + storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(AnonCredsRevocationRegistryDefinitionRecord, storageService, eventEmitter) + } + + public async getByRevocationRegistryDefinitionId(agentContext: AgentContext, revocationRegistryDefinitionId: string) { + return this.getSingleByQuery(agentContext, { revocationRegistryDefinitionId }) + } + + public async findByRevocationRegistryDefinitionId( + agentContext: AgentContext, + revocationRegistryDefinitionId: string + ) { + return this.findSingleByQuery(agentContext, { revocationRegistryDefinitionId }) + } + + public async findAllByCredentialDefinitionId(agentContext: AgentContext, credentialDefinitionId: string) { + return this.findByQuery(agentContext, { credentialDefinitionId }) + } +} diff --git a/packages/anoncreds/src/repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes.ts b/packages/anoncreds/src/repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes.ts new file mode 100644 index 0000000000..7a960c2af1 --- /dev/null +++ b/packages/anoncreds/src/repository/anonCredsRevocationRegistryDefinitionRecordMetadataTypes.ts @@ -0,0 +1,11 @@ +import type { Extensible } from '../services/registry/base' + +export enum AnonCredsRevocationRegistryDefinitionRecordMetadataKeys { + RevocationRegistryDefinitionRegistrationMetadata = '_internal/anonCredsRevocationRegistryDefinitionRegistrationMetadata', + RevocationRegistryDefinitionMetadata = '_internal/anonCredsRevocationRegistryDefinitionMetadata', +} + +export type AnonCredsRevocationRegistryDefinitionRecordMetadata = { + [AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.RevocationRegistryDefinitionRegistrationMetadata]: Extensible + [AnonCredsRevocationRegistryDefinitionRecordMetadataKeys.RevocationRegistryDefinitionMetadata]: Extensible +} diff --git a/packages/anoncreds/src/repository/index.ts b/packages/anoncreds/src/repository/index.ts index c4fb3bbe80..8772b528e7 100644 --- a/packages/anoncreds/src/repository/index.ts +++ b/packages/anoncreds/src/repository/index.ts @@ -8,5 +8,9 @@ export * from './AnonCredsKeyCorrectnessProofRecord' export * from './AnonCredsKeyCorrectnessProofRepository' export * from './AnonCredsLinkSecretRecord' export * from './AnonCredsLinkSecretRepository' +export * from './AnonCredsRevocationRegistryDefinitionRecord' +export * from './AnonCredsRevocationRegistryDefinitionRepository' +export * from './AnonCredsRevocationRegistryDefinitionPrivateRecord' +export * from './AnonCredsRevocationRegistryDefinitionPrivateRepository' export * from './AnonCredsSchemaRecord' export * from './AnonCredsSchemaRepository' diff --git a/packages/anoncreds/src/services/AnonCredsIssuerService.ts b/packages/anoncreds/src/services/AnonCredsIssuerService.ts index 3090b1759b..00f6b5871e 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerService.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerService.ts @@ -5,9 +5,13 @@ import type { CreateCredentialReturn, CreateCredentialOptions, CreateCredentialDefinitionReturn, + CreateRevocationRegistryDefinitionOptions, + CreateRevocationRegistryDefinitionReturn, + CreateRevocationStatusListOptions, + UpdateRevocationStatusListOptions, } from './AnonCredsIssuerServiceOptions' import type { AnonCredsCredentialOffer } from '../models/exchange' -import type { AnonCredsSchema } from '../models/registry' +import type { AnonCredsRevocationStatusList, AnonCredsSchema } from '../models/registry' import type { AgentContext } from '@aries-framework/core' export const AnonCredsIssuerServiceSymbol = Symbol('AnonCredsIssuerService') @@ -23,6 +27,21 @@ export interface AnonCredsIssuerService { metadata?: Record ): Promise + createRevocationRegistryDefinition( + agentContext: AgentContext, + options: CreateRevocationRegistryDefinitionOptions + ): Promise + + createRevocationStatusList( + agentContext: AgentContext, + options: CreateRevocationStatusListOptions + ): Promise + + updateRevocationStatusList( + agentContext: AgentContext, + options: UpdateRevocationStatusListOptions + ): Promise + createCredentialOffer( agentContext: AgentContext, options: CreateCredentialOfferOptions diff --git a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts index c7da246b9b..936ea91af1 100644 --- a/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts +++ b/packages/anoncreds/src/services/AnonCredsIssuerServiceOptions.ts @@ -4,7 +4,12 @@ import type { AnonCredsCredentialRequest, AnonCredsCredentialValues, } from '../models/exchange' -import type { AnonCredsCredentialDefinition, AnonCredsSchema } from '../models/registry' +import type { + AnonCredsCredentialDefinition, + AnonCredsRevocationRegistryDefinition, + AnonCredsRevocationStatusList, + AnonCredsSchema, +} from '../models/registry' export interface CreateSchemaOptions { issuerId: string @@ -16,12 +21,36 @@ export interface CreateSchemaOptions { export interface CreateCredentialDefinitionOptions { issuerId: string tag: string - supportRevocation?: boolean - + supportRevocation: boolean schemaId: string schema: AnonCredsSchema } +export interface CreateRevocationRegistryDefinitionOptions { + issuerId: string + tag: string + credentialDefinitionId: string + credentialDefinition: AnonCredsCredentialDefinition + maximumCredentialNumber: number + tailsDirectoryPath: string +} + +export interface CreateRevocationStatusListOptions { + issuerId: string + revocationRegistryDefinitionId: string + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + tailsFilePath: string +} + +export interface UpdateRevocationStatusListOptions { + revocationStatusList: AnonCredsRevocationStatusList + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revoked?: number[] + issued?: number[] + timestamp?: number + tailsFilePath: string +} + export interface CreateCredentialOfferOptions { credentialDefinitionId: string } @@ -30,9 +59,9 @@ export interface CreateCredentialOptions { credentialOffer: AnonCredsCredentialOffer credentialRequest: AnonCredsCredentialRequest credentialValues: AnonCredsCredentialValues - revocationRegistryId?: string - // TODO: should this just be the tails file instead of a path? - tailsFilePath?: string + revocationRegistryDefinitionId?: string + revocationStatusList?: AnonCredsRevocationStatusList + revocationRegistryIndex?: number } export interface CreateCredentialReturn { @@ -45,3 +74,8 @@ export interface CreateCredentialDefinitionReturn { credentialDefinitionPrivate?: Record keyCorrectnessProof?: Record } + +export interface CreateRevocationRegistryDefinitionReturn { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionPrivate?: Record +} diff --git a/packages/anoncreds/src/services/index.ts b/packages/anoncreds/src/services/index.ts index fe7b176754..419436fde9 100644 --- a/packages/anoncreds/src/services/index.ts +++ b/packages/anoncreds/src/services/index.ts @@ -3,5 +3,6 @@ export * from './AnonCredsHolderServiceOptions' export * from './AnonCredsIssuerService' export * from './AnonCredsIssuerServiceOptions' export * from './registry' +export { TailsFileService, BasicTailsFileService } from './tails' export * from './AnonCredsVerifierService' export * from './AnonCredsVerifierServiceOptions' diff --git a/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts b/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts index 85bf72ba2b..d641befdef 100644 --- a/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts +++ b/packages/anoncreds/src/services/registry/AnonCredsRegistry.ts @@ -3,8 +3,16 @@ import type { RegisterCredentialDefinitionOptions, RegisterCredentialDefinitionReturn, } from './CredentialDefinitionOptions' -import type { GetRevocationRegistryDefinitionReturn } from './RevocationRegistryDefinitionOptions' -import type { GetRevocationStatusListReturn } from './RevocationStatusListOptions' +import type { + GetRevocationRegistryDefinitionReturn, + RegisterRevocationRegistryDefinitionOptions, + RegisterRevocationRegistryDefinitionReturn, +} from './RevocationRegistryDefinitionOptions' +import type { + GetRevocationStatusListReturn, + RegisterRevocationStatusListOptions, + RegisterRevocationStatusListReturn, +} from './RevocationStatusListOptions' import type { GetSchemaReturn, RegisterSchemaOptions, RegisterSchemaReturn } from './SchemaOptions' import type { AgentContext } from '@aries-framework/core' @@ -38,11 +46,10 @@ export interface AnonCredsRegistry { revocationRegistryDefinitionId: string ): Promise - // TODO: issuance of revocable credentials - // registerRevocationRegistryDefinition( - // agentContext: AgentContext, - // options: RegisterRevocationRegistryDefinitionOptions - // ): Promise + registerRevocationRegistryDefinition( + agentContext: AgentContext, + options: RegisterRevocationRegistryDefinitionOptions + ): Promise getRevocationStatusList( agentContext: AgentContext, @@ -50,9 +57,8 @@ export interface AnonCredsRegistry { timestamp: number ): Promise - // TODO: issuance of revocable credentials - // registerRevocationList( - // agentContext: AgentContext, - // options: RegisterRevocationListOptions - // ): Promise + registerRevocationStatusList( + agentContext: AgentContext, + options: RegisterRevocationStatusListOptions + ): Promise } diff --git a/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts index 6e9d1349fe..3f7a07ed77 100644 --- a/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts @@ -1,4 +1,10 @@ -import type { AnonCredsResolutionMetadata, Extensible } from './base' +import type { + AnonCredsOperationStateWait, + AnonCredsOperationStateFailed, + AnonCredsOperationStateFinished, + AnonCredsResolutionMetadata, + Extensible, +} from './base' import type { AnonCredsRevocationRegistryDefinition } from '../../models/registry' export interface GetRevocationRegistryDefinitionReturn { @@ -8,11 +14,32 @@ export interface GetRevocationRegistryDefinitionReturn { revocationRegistryDefinitionMetadata: Extensible } -// TODO: Support for issuance of revocable credentials -// export interface RegisterRevocationRegistryDefinitionOptions { -// revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition -// } +export interface RegisterRevocationRegistryDefinitionOptions { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + options: Extensible +} + +export interface RegisterRevocationRegistryDefinitionReturnStateFailed extends AnonCredsOperationStateFailed { + revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId?: string +} + +export interface RegisterRevocationRegistryDefinitionReturnStateFinished extends AnonCredsOperationStateFinished { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId: string +} + +export interface RegisterRevocationRegistryDefinitionReturnState extends AnonCredsOperationStateWait { + revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId?: string +} -// export interface RegisterRevocationRegistryDefinitionReturn { -// revocationRegistryDefinitionId: string -// } +export interface RegisterRevocationRegistryDefinitionReturn { + jobId?: string + revocationRegistryDefinitionState: + | RegisterRevocationRegistryDefinitionReturnStateFailed + | RegisterRevocationRegistryDefinitionReturnStateFinished + | RegisterRevocationRegistryDefinitionReturnState + revocationRegistryDefinitionMetadata: Extensible + registrationMetadata: Extensible +} diff --git a/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts index 6396fe6df0..05b1353801 100644 --- a/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts @@ -1,4 +1,10 @@ -import type { AnonCredsResolutionMetadata, Extensible } from './base' +import type { + AnonCredsOperationStateWait, + AnonCredsOperationStateFailed, + AnonCredsOperationStateFinished, + AnonCredsResolutionMetadata, + Extensible, +} from './base' import type { AnonCredsRevocationStatusList } from '../../models/registry' export interface GetRevocationStatusListReturn { @@ -7,12 +13,34 @@ export interface GetRevocationStatusListReturn { revocationStatusListMetadata: Extensible } -// TODO: Support for issuance of revocable credentials -// export interface RegisterRevocationListOptions { -// // Timestamp is often calculated by the ledger, otherwise method should just take current time -// // Return type does include the timestamp. -// revocationList: Omit -// } -// export interface RegisterRevocationListReturn { -// timestamp: string -// } +export interface RegisterRevocationStatusListOptions { + // Timestamp is often calculated by the ledger, otherwise method should just take current time + // Return type does include the timestamp. + revocationStatusList: Omit + options: Extensible +} + +export interface RegisterRevocationStatusListReturnStateFailed extends AnonCredsOperationStateFailed { + revocationStatusList?: AnonCredsRevocationStatusList + timestamp?: string +} + +export interface RegisterRevocationStatusListReturnStateFinished extends AnonCredsOperationStateFinished { + revocationStatusList: AnonCredsRevocationStatusList + timestamp: string +} + +export interface RegisterRevocationStatusListReturnState extends AnonCredsOperationStateWait { + revocationStatusList?: AnonCredsRevocationStatusList + timestamp?: string +} + +export interface RegisterRevocationStatusListReturn { + jobId?: string + revocationStatusListState: + | RegisterRevocationStatusListReturnStateFailed + | RegisterRevocationStatusListReturnStateFinished + | RegisterRevocationStatusListReturnState + revocationStatusListMetadata: Extensible + registrationMetadata: Extensible +} diff --git a/packages/anoncreds/src/services/tails/BasicTailsFileService.ts b/packages/anoncreds/src/services/tails/BasicTailsFileService.ts new file mode 100644 index 0000000000..f2cf3eeae1 --- /dev/null +++ b/packages/anoncreds/src/services/tails/BasicTailsFileService.ts @@ -0,0 +1,88 @@ +import type { TailsFileService } from './TailsFileService' +import type { AnonCredsRevocationRegistryDefinition } from '../../models' +import type { AgentContext, FileSystem } from '@aries-framework/core' + +import { AriesFrameworkError, InjectionSymbols, TypedArrayEncoder } from '@aries-framework/core' + +export class BasicTailsFileService implements TailsFileService { + private tailsDirectoryPath?: string + + public constructor(options?: { tailsDirectoryPath?: string; tailsServerBaseUrl?: string }) { + this.tailsDirectoryPath = options?.tailsDirectoryPath + } + + public async getTailsBasePath(agentContext: AgentContext) { + const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) + const basePath = `${this.tailsDirectoryPath ?? fileSystem.cachePath}/anoncreds/tails` + if (!(await fileSystem.exists(basePath))) { + await fileSystem.createDirectory(`${basePath}/file`) + } + return basePath + } + + public async uploadTailsFile( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ): Promise { + throw new AriesFrameworkError('BasicTailsFileService only supports tails file downloading') + } + + public async getTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ): Promise { + const { revocationRegistryDefinition } = options + const { tailsLocation, tailsHash } = revocationRegistryDefinition.value + + const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) + + try { + agentContext.config.logger.debug( + `Checking to see if tails file for URL ${revocationRegistryDefinition.value.tailsLocation} has been stored in the FileSystem` + ) + + // hash is used as file identifier + const tailsExists = await this.tailsFileExists(agentContext, tailsHash) + const tailsFilePath = await this.getTailsFilePath(agentContext, tailsHash) + agentContext.config.logger.debug( + `Tails file for ${tailsLocation} ${tailsExists ? 'is stored' : 'is not stored'} at ${tailsFilePath}` + ) + + if (!tailsExists) { + agentContext.config.logger.debug(`Retrieving tails file from URL ${tailsLocation}`) + + // download file and verify hash + await fileSystem.downloadToFile(tailsLocation, tailsFilePath, { + verifyHash: { + algorithm: 'sha256', + hash: TypedArrayEncoder.fromBase58(tailsHash), + }, + }) + agentContext.config.logger.debug(`Saved tails file to FileSystem at path ${tailsFilePath}`) + } + + return tailsFilePath + } catch (error) { + agentContext.config.logger.error(`Error while retrieving tails file from URL ${tailsLocation}`, { + error, + }) + throw error + } + } + + protected async getTailsFilePath(agentContext: AgentContext, tailsHash: string) { + return `${await this.getTailsBasePath(agentContext)}/${tailsHash}` + } + + protected async tailsFileExists(agentContext: AgentContext, tailsHash: string): Promise { + const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) + const tailsFilePath = await this.getTailsFilePath(agentContext, tailsHash) + return await fileSystem.exists(tailsFilePath) + } +} diff --git a/packages/anoncreds/src/services/tails/TailsFileService.ts b/packages/anoncreds/src/services/tails/TailsFileService.ts new file mode 100644 index 0000000000..d8e0dd7167 --- /dev/null +++ b/packages/anoncreds/src/services/tails/TailsFileService.ts @@ -0,0 +1,47 @@ +import type { AnonCredsRevocationRegistryDefinition } from '../../models' +import type { AgentContext } from '@aries-framework/core' + +export interface TailsFileService { + /** + * Retrieve base directory for tail file storage + * + * @param agentContext + */ + getTailsBasePath(agentContext: AgentContext): string | Promise + + /** + * Upload the tails file for a given revocation registry definition. + * + * Optionally, receives revocationRegistryDefinitionId in case the ID is + * known beforehand. + * + * Returns the published tail file URL + * @param agentContext + * @param options + */ + uploadTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId?: string + } + ): Promise + + /** + * Retrieve the tails file for a given revocation registry, downloading it + * from the tailsLocation URL if not present in internal cache + * + * Classes implementing this interface should verify integrity of the downloaded + * file. + * + * @param agentContext + * @param options + */ + getTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId?: string + } + ): Promise +} diff --git a/packages/anoncreds/src/services/tails/index.ts b/packages/anoncreds/src/services/tails/index.ts new file mode 100644 index 0000000000..104617b380 --- /dev/null +++ b/packages/anoncreds/src/services/tails/index.ts @@ -0,0 +1,2 @@ +export * from './BasicTailsFileService' +export * from './TailsFileService' diff --git a/packages/anoncreds/src/utils/createRequestFromPreview.ts b/packages/anoncreds/src/utils/createRequestFromPreview.ts index d1738d000e..5c08ebd83d 100644 --- a/packages/anoncreds/src/utils/createRequestFromPreview.ts +++ b/packages/anoncreds/src/utils/createRequestFromPreview.ts @@ -2,7 +2,7 @@ import type { AnonCredsPresentationPreviewAttribute, AnonCredsPresentationPreviewPredicate, } from '../formats/AnonCredsProofFormat' -import type { AnonCredsProofRequest } from '../models' +import type { AnonCredsNonRevokedInterval, AnonCredsProofRequest } from '../models' import { utils } from '@aries-framework/core' @@ -12,12 +12,14 @@ export function createRequestFromPreview({ nonce, attributes, predicates, + nonRevokedInterval, }: { name: string version: string nonce: string attributes: AnonCredsPresentationPreviewAttribute[] predicates: AnonCredsPresentationPreviewPredicate[] + nonRevokedInterval?: AnonCredsNonRevokedInterval }): AnonCredsProofRequest { const proofRequest: AnonCredsProofRequest = { name, @@ -85,5 +87,10 @@ export function createRequestFromPreview({ } } + // TODO: local non_revoked? + if (nonRevokedInterval) { + proofRequest.non_revoked = nonRevokedInterval + } + return proofRequest } diff --git a/packages/anoncreds/src/utils/getRevocationRegistries.ts b/packages/anoncreds/src/utils/getRevocationRegistries.ts index 0141bf257b..421fefe61a 100644 --- a/packages/anoncreds/src/utils/getRevocationRegistries.ts +++ b/packages/anoncreds/src/utils/getRevocationRegistries.ts @@ -4,10 +4,10 @@ import type { AgentContext } from '@aries-framework/core' import { AriesFrameworkError } from '@aries-framework/core' +import { AnonCredsModuleConfig } from '../AnonCredsModuleConfig' import { AnonCredsRegistryService } from '../services' import { assertBestPracticeRevocationInterval } from './revocationInterval' -import { downloadTailsFile } from './tails' export async function getRevocationRegistriesForRequest( agentContext: AgentContext, @@ -90,8 +90,10 @@ export async function getRevocationRegistriesForRequest( ) } - const { tailsLocation, tailsHash } = revocationRegistryDefinition.value - const { tailsFilePath } = await downloadTailsFile(agentContext, tailsLocation, tailsHash) + const tailsFileService = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService + const tailsFilePath = await tailsFileService.getTailsFile(agentContext, { + revocationRegistryDefinition, + }) // const tails = await this.indyUtilitiesService.downloadTails(tailsHash, tailsLocation) revocationRegistries[revocationRegistryId] = { @@ -100,42 +102,41 @@ export async function getRevocationRegistriesForRequest( revocationStatusLists: {}, } } - } - - revocationRegistryPromises.push(getRevocationRegistry()) - // In most cases we will have a timestamp, but if it's not defined, we use the nonRevoked.to value - const timestampToFetch = timestamp ?? nonRevoked.to + // In most cases we will have a timestamp, but if it's not defined, we use the nonRevoked.to value + const timestampToFetch = timestamp ?? nonRevoked.to - // Fetch revocation status list if we don't already have a revocation status list for the given timestamp - if (!revocationRegistries[revocationRegistryId].revocationStatusLists[timestampToFetch]) { - const { revocationStatusList, resolutionMetadata: statusListResolutionMetadata } = - await registry.getRevocationStatusList(agentContext, revocationRegistryId, timestampToFetch) + // Fetch revocation status list if we don't already have a revocation status list for the given timestamp + if (!revocationRegistries[revocationRegistryId].revocationStatusLists[timestampToFetch]) { + const { revocationStatusList, resolutionMetadata: statusListResolutionMetadata } = + await registry.getRevocationStatusList(agentContext, revocationRegistryId, timestampToFetch) - if (!revocationStatusList) { - throw new AriesFrameworkError( - `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${statusListResolutionMetadata.message}` - ) - } + if (!revocationStatusList) { + throw new AriesFrameworkError( + `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${statusListResolutionMetadata.message}` + ) + } - revocationRegistries[revocationRegistryId].revocationStatusLists[revocationStatusList.timestamp] = - revocationStatusList - - // If we don't have a timestamp on the selected credential, we set it to the timestamp of the revocation status list - // this way we know which revocation status list to use when creating the proof. - if (!timestamp) { - updatedSelectedCredentials = { - ...updatedSelectedCredentials, - [type]: { - ...updatedSelectedCredentials[type], - [referent]: { - ...updatedSelectedCredentials[type][referent], - timestamp: revocationStatusList.timestamp, + revocationRegistries[revocationRegistryId].revocationStatusLists[revocationStatusList.timestamp] = + revocationStatusList + + // If we don't have a timestamp on the selected credential, we set it to the timestamp of the revocation status list + // this way we know which revocation status list to use when creating the proof. + if (!timestamp) { + updatedSelectedCredentials = { + ...updatedSelectedCredentials, + [type]: { + ...updatedSelectedCredentials[type], + [referent]: { + ...updatedSelectedCredentials[type][referent], + timestamp: revocationStatusList.timestamp, + }, }, - }, + } } } } + revocationRegistryPromises.push(getRevocationRegistry()) } } // await all revocation registry statuses asynchronously diff --git a/packages/anoncreds/src/utils/index.ts b/packages/anoncreds/src/utils/index.ts index d995623bb0..b49440268b 100644 --- a/packages/anoncreds/src/utils/index.ts +++ b/packages/anoncreds/src/utils/index.ts @@ -2,13 +2,13 @@ export { createRequestFromPreview } from './createRequestFromPreview' export { sortRequestedCredentialsMatches } from './sortRequestedCredentialsMatches' export { assertNoDuplicateGroupsNamesInProofRequest } from './hasDuplicateGroupNames' export { areAnonCredsProofRequestsEqual } from './areRequestsEqual' -export { downloadTailsFile } from './tails' export { assertBestPracticeRevocationInterval } from './revocationInterval' export { getRevocationRegistriesForRequest, getRevocationRegistriesForProof } from './getRevocationRegistries' export { encodeCredentialValue, checkValidCredentialValueEncoding } from './credential' export { IsMap } from './isMap' export { composeCredentialAutoAccept, composeProofAutoAccept } from './composeAutoAccept' export { areCredentialPreviewAttributesEqual } from './credentialPreviewAttributes' +export { dateToTimestamp } from './timestamp' export { storeLinkSecret } from './linkSecret' export { unqualifiedCredentialDefinitionIdRegex, diff --git a/packages/anoncreds/src/utils/tails.ts b/packages/anoncreds/src/utils/tails.ts deleted file mode 100644 index e706f00914..0000000000 --- a/packages/anoncreds/src/utils/tails.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { AgentContext, FileSystem } from '@aries-framework/core' - -import { TypedArrayEncoder, InjectionSymbols } from '@aries-framework/core' - -const getTailsFilePath = (cachePath: string, tailsHash: string) => `${cachePath}/anoncreds/tails/${tailsHash}` - -export function tailsFileExists(agentContext: AgentContext, tailsHash: string): Promise { - const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) - const tailsFilePath = getTailsFilePath(fileSystem.cachePath, tailsHash) - - return fileSystem.exists(tailsFilePath) -} - -export async function downloadTailsFile( - agentContext: AgentContext, - tailsLocation: string, - tailsHashBase58: string -): Promise<{ - tailsFilePath: string -}> { - const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) - - try { - agentContext.config.logger.debug( - `Checking to see if tails file for URL ${tailsLocation} has been stored in the FileSystem` - ) - - // hash is used as file identifier - const tailsExists = await tailsFileExists(agentContext, tailsHashBase58) - const tailsFilePath = getTailsFilePath(fileSystem.cachePath, tailsHashBase58) - agentContext.config.logger.debug( - `Tails file for ${tailsLocation} ${tailsExists ? 'is stored' : 'is not stored'} at ${tailsFilePath}` - ) - - if (!tailsExists) { - agentContext.config.logger.debug(`Retrieving tails file from URL ${tailsLocation}`) - - // download file and verify hash - await fileSystem.downloadToFile(tailsLocation, tailsFilePath, { - verifyHash: { - algorithm: 'sha256', - hash: TypedArrayEncoder.fromBase58(tailsHashBase58), - }, - }) - agentContext.config.logger.debug(`Saved tails file to FileSystem at path ${tailsFilePath}`) - } - - return { - tailsFilePath, - } - } catch (error) { - agentContext.config.logger.error(`Error while retrieving tails file from URL ${tailsLocation}`, { - error, - }) - throw error - } -} diff --git a/packages/anoncreds/src/utils/timestamp.ts b/packages/anoncreds/src/utils/timestamp.ts new file mode 100644 index 0000000000..9386fe68e3 --- /dev/null +++ b/packages/anoncreds/src/utils/timestamp.ts @@ -0,0 +1,2 @@ +// Timestamps are expressed as Unix epoch time (seconds since 1/1/1970) +export const dateToTimestamp = (date: Date) => Math.floor(date.getTime() / 1000) diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index 9cb3a9adf3..da51aa12ec 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -13,14 +13,30 @@ import type { AnonCredsRevocationRegistryDefinition, AnonCredsSchema, AnonCredsCredentialDefinition, + RegisterRevocationRegistryDefinitionOptions, + RegisterRevocationRegistryDefinitionReturn, + RegisterRevocationStatusListReturn, + RegisterRevocationStatusListOptions, } from '../src' import type { AgentContext } from '@aries-framework/core' import { Hasher, TypedArrayEncoder } from '@aries-framework/core' import BigNumber from 'bn.js' -import { getDidIndyCredentialDefinitionId, getDidIndySchemaId } from '../../indy-sdk/src/anoncreds/utils/identifiers' -import { getUnqualifiedCredentialDefinitionId, getUnqualifiedSchemaId, parseIndyDid, parseIndySchemaId } from '../src' +import { + getDidIndyCredentialDefinitionId, + getDidIndyRevocationRegistryId, + getDidIndySchemaId, +} from '../../indy-sdk/src/anoncreds/utils/identifiers' +import { + parseIndyCredentialDefinitionId, + getUnqualifiedRevocationRegistryId, + getUnqualifiedCredentialDefinitionId, + getUnqualifiedSchemaId, + parseIndyDid, + parseIndySchemaId, +} from '../src' +import { dateToTimestamp } from '../src/utils/timestamp' /** * In memory implementation of the {@link AnonCredsRegistry} interface. Useful for testing. @@ -219,6 +235,54 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } } + public async registerRevocationRegistryDefinition( + agentContext: AgentContext, + options: RegisterRevocationRegistryDefinitionOptions + ): Promise { + const parsedCredentialDefinition = parseIndyCredentialDefinitionId(options.revocationRegistryDefinition.credDefId) + const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId( + parsedCredentialDefinition.namespaceIdentifier, + parsedCredentialDefinition.schemaSeqNo, + parsedCredentialDefinition.tag + ) + const indyLedgerSeqNo = getSeqNoFromSchemaId(legacyCredentialDefinitionId) + + const { namespace, namespaceIdentifier } = parseIndyDid(options.revocationRegistryDefinition.issuerId) + const legacyIssuerId = namespaceIdentifier + const didIndyRevocationRegistryDefinitionId = getDidIndyRevocationRegistryId( + namespace, + namespaceIdentifier, + indyLedgerSeqNo, + parsedCredentialDefinition.tag, + options.revocationRegistryDefinition.tag + ) + + this.revocationRegistryDefinitions[didIndyRevocationRegistryDefinitionId] = options.revocationRegistryDefinition + + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryId( + legacyIssuerId, + indyLedgerSeqNo, + parsedCredentialDefinition.tag, + options.revocationRegistryDefinition.tag + ) + + this.revocationRegistryDefinitions[legacyRevocationRegistryDefinitionId] = { + ...options.revocationRegistryDefinition, + issuerId: legacyIssuerId, + credDefId: legacyCredentialDefinitionId, + } + + return { + registrationMetadata: {}, + revocationRegistryDefinitionMetadata: {}, + revocationRegistryDefinitionState: { + state: 'finished', + revocationRegistryDefinition: options.revocationRegistryDefinition, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, + }, + } + } + public async getRevocationStatusList( agentContext: AgentContext, revocationRegistryId: string, @@ -226,7 +290,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { ): Promise { const revocationStatusLists = this.revocationStatusLists[revocationRegistryId] - if (!revocationStatusLists || !revocationStatusLists[timestamp]) { + if (!revocationStatusLists || Object.entries(revocationStatusLists).length === 0) { return { resolutionMetadata: { error: 'notFound', @@ -236,12 +300,51 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } } + const previousTimestamps = Object.keys(revocationStatusLists) + .filter((ts) => Number(ts) <= timestamp) + .sort() + + if (!previousTimestamps) { + return { + resolutionMetadata: { + error: 'notFound', + message: `No active Revocation status list found at ${timestamp} for revocation registry with id ${revocationRegistryId}`, + }, + revocationStatusListMetadata: {}, + } + } + return { resolutionMetadata: {}, - revocationStatusList: revocationStatusLists[timestamp], + revocationStatusList: revocationStatusLists[previousTimestamps[previousTimestamps.length - 1]], revocationStatusListMetadata: {}, } } + + public async registerRevocationStatusList( + agentContext: AgentContext, + options: RegisterRevocationStatusListOptions + ): Promise { + const timestamp = (options.options.timestamp as number) ?? dateToTimestamp(new Date()) + const revocationStatusList = { + ...options.revocationStatusList, + timestamp, + } satisfies AnonCredsRevocationStatusList + if (!this.revocationStatusLists[options.revocationStatusList.revRegDefId]) { + this.revocationStatusLists[options.revocationStatusList.revRegDefId] = {} + } + + this.revocationStatusLists[revocationStatusList.revRegDefId][timestamp.toString()] = revocationStatusList + return { + registrationMetadata: {}, + revocationStatusListMetadata: {}, + revocationStatusListState: { + state: 'finished', + revocationStatusList, + timestamp: timestamp.toString(), + }, + } + } } /** diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts index d56dc4b630..f96b116126 100644 --- a/packages/anoncreds/tests/anoncreds.test.ts +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -201,6 +201,7 @@ describe('AnonCreds API', () => { schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', tag: 'TAG', }, + supportRevocation: false, options: {}, }) diff --git a/packages/anoncreds/tests/legacyAnonCredsSetup.ts b/packages/anoncreds/tests/legacyAnonCredsSetup.ts index 39e9b53c47..9709b509ea 100644 --- a/packages/anoncreds/tests/legacyAnonCredsSetup.ts +++ b/packages/anoncreds/tests/legacyAnonCredsSetup.ts @@ -511,10 +511,12 @@ async function registerSchema( async function registerCredentialDefinition( agent: AnonCredsTestsAgent, - credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions + credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions, + supportRevocation?: boolean ): Promise { const { credentialDefinitionState } = await agent.modules.anoncreds.registerCredentialDefinition({ credentialDefinition, + supportRevocation: supportRevocation ?? false, options: {}, }) diff --git a/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts b/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts index d9e27c18e2..391ce13d92 100644 --- a/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts +++ b/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts @@ -9,6 +9,8 @@ import type { RegisterCredentialDefinitionReturn, RegisterSchemaReturn, RegisterSchemaOptions, + RegisterRevocationRegistryDefinitionReturn, + RegisterRevocationStatusListReturn, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' @@ -290,6 +292,10 @@ export class CheqdAnonCredsRegistry implements AnonCredsRegistry { } } + public async registerRevocationRegistryDefinition(): Promise { + throw new Error('Not implemented!') + } + // FIXME: this method doesn't retrieve the revocation status list at a specified time, it just resolves the revocation registry definition public async getRevocationStatusList( agentContext: AgentContext, @@ -344,4 +350,8 @@ export class CheqdAnonCredsRegistry implements AnonCredsRegistry { } } } + + public async registerRevocationStatusList(): Promise { + throw new Error('Not implemented!') + } } diff --git a/packages/core/src/modules/credentials/CredentialsApi.ts b/packages/core/src/modules/credentials/CredentialsApi.ts index 4a0a6ac349..8cdcc0f1f5 100644 --- a/packages/core/src/modules/credentials/CredentialsApi.ts +++ b/packages/core/src/modules/credentials/CredentialsApi.ts @@ -15,6 +15,7 @@ import type { ProposeCredentialOptions, SendCredentialProblemReportOptions, DeleteCredentialOptions, + SendRevocationNotificationOptions, } from './CredentialsApiOptions' import type { CredentialProtocol } from './protocol/CredentialProtocol' import type { CredentialFormatsFromProtocols } from './protocol/CredentialProtocolOptions' @@ -60,6 +61,9 @@ export interface CredentialsApi { // Issue Credential Methods acceptCredential(options: AcceptCredentialOptions): Promise + // Revoke Credential Methods + sendRevocationNotification(options: SendRevocationNotificationOptions): Promise + // out of band createOffer(options: CreateCredentialOfferOptions): Promise<{ message: AgentMessage @@ -96,6 +100,7 @@ export class CredentialsApi implements Credent private credentialRepository: CredentialRepository private agentContext: AgentContext private didCommMessageRepository: DidCommMessageRepository + private revocationNotificationService: RevocationNotificationService private routingService: RoutingService private logger: Logger @@ -107,9 +112,7 @@ export class CredentialsApi implements Credent credentialRepository: CredentialRepository, mediationRecipientService: RoutingService, didCommMessageRepository: DidCommMessageRepository, - // only injected so the handlers will be registered - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _revocationNotificationService: RevocationNotificationService, + revocationNotificationService: RevocationNotificationService, config: CredentialsModuleConfig ) { this.messageSender = messageSender @@ -118,6 +121,7 @@ export class CredentialsApi implements Credent this.routingService = mediationRecipientService this.agentContext = agentContext this.didCommMessageRepository = didCommMessageRepository + this.revocationNotificationService = revocationNotificationService this.logger = logger this.config = config } @@ -414,7 +418,7 @@ export class CredentialsApi implements Credent } const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) if (!offerMessage) { - throw new AriesFrameworkError(`No offer message found for proof record with id '${credentialRecord.id}'`) + throw new AriesFrameworkError(`No offer message found for credential record with id '${credentialRecord.id}'`) } const { message } = await protocol.acceptRequest(this.agentContext, { @@ -485,6 +489,48 @@ export class CredentialsApi implements Credent return credentialRecord } + /** + * Send a revocation notification for a credential exchange record. Currently Revocation Notification V2 protocol is supported + * + * @param credentialRecordId The id of the credential record for which to send revocation notification + */ + public async sendRevocationNotification(options: SendRevocationNotificationOptions): Promise { + const { credentialRecordId, revocationId, revocationFormat, comment, requestAck } = options + + const credentialRecord = await this.getById(credentialRecordId) + + const { message } = await this.revocationNotificationService.v2CreateRevocationNotification({ + credentialId: revocationId, + revocationFormat, + comment, + requestAck, + }) + const protocol = this.getProtocol(credentialRecord.protocolVersion) + + const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for credential record with id '${credentialRecord.id}'`) + } + + const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) + if (!offerMessage) { + throw new AriesFrameworkError(`No offer message found for credential record with id '${credentialRecord.id}'`) + } + + // Use connection if present + const connectionRecord = credentialRecord.connectionId + ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + : undefined + connectionRecord?.assertReady() + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: credentialRecord, + }) + await this.messageSender.sendMessage(outboundMessageContext) + } + /** * Send problem report message for a credential record * @param credentialRecordId The id of the credential record for which to send problem report diff --git a/packages/core/src/modules/credentials/CredentialsApiOptions.ts b/packages/core/src/modules/credentials/CredentialsApiOptions.ts index 19e9f17295..9f49b1ca98 100644 --- a/packages/core/src/modules/credentials/CredentialsApiOptions.ts +++ b/packages/core/src/modules/credentials/CredentialsApiOptions.ts @@ -121,6 +121,17 @@ export interface AcceptCredentialOptions { credentialRecordId: string } +/** + * Interface for CredentialsApi.sendRevocationNotification. Will send a revoke message + */ +export interface SendRevocationNotificationOptions { + credentialRecordId: string + revocationId: string // TODO: Get from record? + revocationFormat: string // TODO: Get from record? + comment?: string + requestAck?: boolean +} + /** * Interface for CredentialsApi.sendProblemReport. Will send a problem-report message */ diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.ts index 5efc958bc8..87717ce986 100644 --- a/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.ts +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.ts @@ -1,9 +1,9 @@ +import type { V2CreateRevocationNotificationMessageOptions } from './RevocationNotificationServiceOptions' import type { AgentContext } from '../../../../../agent' import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' import type { ConnectionRecord } from '../../../../connections' import type { RevocationNotificationReceivedEvent } from '../../../CredentialEvents' import type { V1RevocationNotificationMessage } from '../messages/V1RevocationNotificationMessage' -import type { V2RevocationNotificationMessage } from '../messages/V2RevocationNotificationMessage' import { EventEmitter } from '../../../../../agent/EventEmitter' import { MessageHandlerRegistry } from '../../../../../agent/MessageHandlerRegistry' @@ -15,7 +15,14 @@ import { CredentialEventTypes } from '../../../CredentialEvents' import { RevocationNotification } from '../../../models/RevocationNotification' import { CredentialRepository } from '../../../repository' import { V1RevocationNotificationHandler, V2RevocationNotificationHandler } from '../handlers' -import { v1ThreadRegex, v2IndyRevocationFormat, v2IndyRevocationIdentifierRegex } from '../util/revocationIdentifier' +import { V2RevocationNotificationMessage } from '../messages/V2RevocationNotificationMessage' +import { + v1ThreadRegex, + v2AnonCredsRevocationFormat, + v2AnonCredsRevocationIdentifierRegex, + v2IndyRevocationFormat, + v2IndyRevocationIdentifierRegex, +} from '../util/revocationIdentifier' @injectable() export class RevocationNotificationService { @@ -100,6 +107,26 @@ export class RevocationNotificationService { } } + /** + * Create a V2 Revocation Notification message + */ + + public async v2CreateRevocationNotification( + options: V2CreateRevocationNotificationMessageOptions + ): Promise<{ message: V2RevocationNotificationMessage }> { + const { credentialId, revocationFormat, comment, requestAck } = options + const message = new V2RevocationNotificationMessage({ + credentialId, + revocationFormat, + comment, + }) + if (requestAck) { + message.setPleaseAck() + } + + return { message } + } + /** * Process a received {@link V2RevocationNotificationMessage}. This will create a * {@link RevocationNotification} and store it in the corresponding {@link CredentialRecord} @@ -113,14 +140,15 @@ export class RevocationNotificationService { const credentialId = messageContext.message.credentialId - if (messageContext.message.revocationFormat !== v2IndyRevocationFormat) { + if (![v2IndyRevocationFormat, v2AnonCredsRevocationFormat].includes(messageContext.message.revocationFormat)) { throw new AriesFrameworkError( - `Unknown revocation format: ${messageContext.message.revocationFormat}. Supported formats are indy-anoncreds` + `Unknown revocation format: ${messageContext.message.revocationFormat}. Supported formats are indy-anoncreds and anoncreds` ) } try { - const credentialIdGroups = credentialId.match(v2IndyRevocationIdentifierRegex) + const credentialIdGroups = + credentialId.match(v2IndyRevocationIdentifierRegex) ?? credentialId.match(v2AnonCredsRevocationIdentifierRegex) if (!credentialIdGroups) { throw new AriesFrameworkError( `Incorrect revocation notification credentialId format: \n${credentialId}\ndoes not match\n"::"` diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationServiceOptions.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationServiceOptions.ts new file mode 100644 index 0000000000..406013b18b --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationServiceOptions.ts @@ -0,0 +1,6 @@ +export interface V2CreateRevocationNotificationMessageOptions { + credentialId: string + revocationFormat: string + comment?: string + requestAck?: boolean +} diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/services/index.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/services/index.ts index 5413d4da87..e5696a92e1 100644 --- a/packages/core/src/modules/credentials/protocol/revocation-notification/services/index.ts +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/services/index.ts @@ -1 +1,2 @@ export * from './RevocationNotificationService' +export * from './RevocationNotificationServiceOptions' diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.ts index 12f75569d7..c1bc1d35f2 100644 --- a/packages/core/src/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.ts +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/util/revocationIdentifier.ts @@ -9,3 +9,8 @@ export const v2IndyRevocationIdentifierRegex = /((?:[\dA-z]{21,22}):4:(?:[\dA-z]{21,22}):3:[Cc][Ll]:(?:(?:[1-9][0-9]*)|(?:[\dA-z]{21,22}:2:.+:[0-9.]+)):.+?:CL_ACCUM:(?:[\dA-z-]+))::(\d+)$/ export const v2IndyRevocationFormat = 'indy-anoncreds' + +// CredentialID = :: +export const v2AnonCredsRevocationIdentifierRegex = /([a-zA-Z0-9+\-.]+:.+)::(\d+)$/ + +export const v2AnonCredsRevocationFormat = 'anoncreds' diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 517c53a274..4ad2392ceb 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -14,6 +14,7 @@ import type { CredentialState, ConnectionStateChangedEvent, Buffer, + RevocationNotificationReceivedEvent, } from '../src' import type { AgentModulesInput, EmptyModuleMap } from '../src/agent/AgentModules' import type { TrustPingReceivedEvent, TrustPingResponseReceivedEvent } from '../src/modules/connections/TrustPingEvents' @@ -455,6 +456,47 @@ export async function waitForBasicMessage(agent: Agent, { content }: { content?: }) } +export async function waitForRevocationNotification( + agent: Agent, + options: { + threadId?: string + timeoutMs?: number + } +) { + const observable = agent.events.observable( + CredentialEventTypes.RevocationNotificationReceived + ) + + return waitForRevocationNotificationSubject(observable, options) +} + +export function waitForRevocationNotificationSubject( + subject: ReplaySubject | Observable, + { + threadId, + timeoutMs = 10000, + }: { + threadId?: string + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return firstValueFrom( + observable.pipe( + filter((e) => threadId === undefined || e.payload.credentialRecord.threadId === threadId), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `RevocationNotificationReceivedEvent event not emitted within specified timeout: { + threadId: ${threadId}, + }` + ) + }), + map((e) => e.payload.credentialRecord) + ) + ) +} + export function getMockConnection({ state = DidExchangeState.InvitationReceived, role = DidExchangeRole.Requester, diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts index 21ab95ab53..a386675b1e 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts @@ -10,6 +10,8 @@ import type { RegisterCredentialDefinitionReturn, RegisterSchemaOptions, RegisterSchemaReturn, + RegisterRevocationRegistryDefinitionReturn, + RegisterRevocationStatusListReturn, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { Schema as IndySdkSchema } from 'indy-sdk' @@ -23,6 +25,7 @@ import { parseIndyRevocationRegistryId, parseIndySchemaId, } from '@aries-framework/anoncreds' +import { AriesFrameworkError } from '@aries-framework/core' import { verificationKeyForIndyDid } from '../../dids/didIndyUtil' import { IndySdkError, isIndyError } from '../../error' @@ -468,6 +471,10 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { } } + public async registerRevocationRegistryDefinition(): Promise { + throw new AriesFrameworkError('Not implemented!') + } + public async getRevocationStatusList( agentContext: AgentContext, revocationRegistryId: string, @@ -569,6 +576,10 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { } } + public async registerRevocationStatusList(): Promise { + throw new AriesFrameworkError('Not implemented!') + } + private async fetchIndySchemaWithSeqNo(agentContext: AgentContext, pool: IndySdkPool, seqNo: number) { const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts index 01973d31dd..b303bed598 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts @@ -9,6 +9,8 @@ import type { AnonCredsCredentialOffer, AnonCredsSchema, CreateCredentialDefinitionReturn, + CreateRevocationRegistryDefinitionReturn, + AnonCredsRevocationStatusList, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' @@ -24,7 +26,6 @@ import { assertUnqualifiedCredentialRequest, assertUnqualifiedRevocationRegistryId, } from '../utils/assertUnqualified' -import { createTailsReader } from '../utils/tails' import { indySdkSchemaFromAnonCreds } from '../utils/transform' @injectable() @@ -35,6 +36,18 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { this.indySdk = indySdk } + public async createRevocationStatusList(): Promise { + throw new AriesFrameworkError('Method not implemented.') + } + + public async updateRevocationStatusList(): Promise { + throw new AriesFrameworkError('Method not implemented.') + } + + public async createRevocationRegistryDefinition(): Promise { + throw new AriesFrameworkError('Method not implemented.') + } + public async createSchema(agentContext: AgentContext, options: CreateSchemaOptions): Promise { // We only support passing qualified did:indy issuer ids in the indy issuer service when creating objects const { namespaceIdentifier } = parseIndyDid(options.issuerId) @@ -117,20 +130,23 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { agentContext: AgentContext, options: CreateCredentialOptions ): Promise { - const { tailsFilePath, credentialOffer, credentialRequest, credentialValues, revocationRegistryId } = options + const { + revocationStatusList, + credentialOffer, + credentialRequest, + credentialValues, + revocationRegistryDefinitionId, + } = options assertIndySdkWallet(agentContext.wallet) assertUnqualifiedCredentialOffer(options.credentialOffer) assertUnqualifiedCredentialRequest(options.credentialRequest) - if (options.revocationRegistryId) { - assertUnqualifiedRevocationRegistryId(options.revocationRegistryId) + if (options.revocationRegistryDefinitionId) { + assertUnqualifiedRevocationRegistryId(options.revocationRegistryDefinitionId) } try { - // Indy SDK requires tailsReaderHandle. Use null if no tailsFilePath is present - const tailsReaderHandle = tailsFilePath ? await createTailsReader(agentContext, tailsFilePath) : 0 - - if (revocationRegistryId || tailsFilePath) { + if (revocationRegistryDefinitionId || revocationStatusList) { throw new AriesFrameworkError('Revocation not supported yet') } @@ -142,8 +158,8 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { credentialOffer, { ...credentialRequest, prover_did: proverDid }, credentialValues, - revocationRegistryId ?? null, - tailsReaderHandle + revocationRegistryDefinitionId ?? null, + 0 ) return { diff --git a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts index fbfaa49a0a..5251362983 100644 --- a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts +++ b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts @@ -7,6 +7,8 @@ import type { GetRevocationStatusListReturn, GetRevocationRegistryDefinitionReturn, AnonCredsRevocationRegistryDefinition, + RegisterRevocationRegistryDefinitionReturn, + RegisterRevocationStatusListReturn, AnonCredsSchema, AnonCredsCredentialDefinition, RegisterSchemaReturnStateFailed, @@ -30,6 +32,7 @@ import { parseIndyRevocationRegistryId, parseIndySchemaId, } from '@aries-framework/anoncreds' +import { AriesFrameworkError } from '@aries-framework/core' import { GetSchemaRequest, SchemaRequest, @@ -312,7 +315,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { const { schemaId, issuerId, tag, value } = credentialDefinition try { - // This will throw an error if trying to register a credential defintion with a legacy indy identifier. We only support did:indy + // This will throw an error if trying to register a credential definition with a legacy indy identifier. We only support did:indy // identifiers for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. const { namespaceIdentifier, namespace } = parseIndyDid(issuerId) const { endorserDid, endorserMode } = options.options @@ -549,6 +552,10 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { } } + public async registerRevocationRegistryDefinition(): Promise { + throw new AriesFrameworkError('Not implemented!') + } + public async getRevocationStatusList( agentContext: AgentContext, revocationRegistryId: string, @@ -653,6 +660,10 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { } } + public async registerRevocationStatusList(): Promise { + throw new AriesFrameworkError('Not implemented!') + } + private async fetchIndySchemaWithSeqNo(agentContext: AgentContext, seqNo: number, did: string) { const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) diff --git a/samples/tails/.gitignore b/samples/tails/.gitignore new file mode 100644 index 0000000000..6c4291916e --- /dev/null +++ b/samples/tails/.gitignore @@ -0,0 +1 @@ +tails \ No newline at end of file diff --git a/samples/tails/FullTailsFileService.ts b/samples/tails/FullTailsFileService.ts new file mode 100644 index 0000000000..1ffa4c03c6 --- /dev/null +++ b/samples/tails/FullTailsFileService.ts @@ -0,0 +1,41 @@ +import type { AnonCredsRevocationRegistryDefinition } from '@aries-framework/anoncreds' +import type { AgentContext } from '@aries-framework/core' + +import { BasicTailsFileService } from '@aries-framework/anoncreds' +import { utils } from '@aries-framework/core' +import FormData from 'form-data' +import fs from 'fs' + +export class FullTailsFileService extends BasicTailsFileService { + private tailsServerBaseUrl?: string + public constructor(options?: { tailsDirectoryPath?: string; tailsServerBaseUrl?: string }) { + super(options) + this.tailsServerBaseUrl = options?.tailsServerBaseUrl + } + + public async uploadTailsFile( + agentContext: AgentContext, + options: { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + } + ): Promise { + const revocationRegistryDefinition = options.revocationRegistryDefinition + const localTailsFilePath = revocationRegistryDefinition.value.tailsLocation + + const tailsFileId = utils.uuid() + const data = new FormData() + const readStream = fs.createReadStream(localTailsFilePath) + data.append('file', readStream) + const response = await agentContext.config.agentDependencies.fetch( + `${this.tailsServerBaseUrl}/${encodeURIComponent(tailsFileId)}`, + { + method: 'PUT', + body: data, + } + ) + if (response.status !== 200) { + throw new Error('Cannot upload tails file') + } + return `${this.tailsServerBaseUrl}/${encodeURIComponent(tailsFileId)}` + } +} diff --git a/samples/tails/README.md b/samples/tails/README.md new file mode 100644 index 0000000000..838d207160 --- /dev/null +++ b/samples/tails/README.md @@ -0,0 +1,5 @@ +

Sample tails file server

+ +This is a very simple server that can be used to host AnonCreds tails files. It is intended to be used only for development purposes. + +It offers a single endpoint at the root that takes an URI-encoded `tailsFileId` as URL path and allows to upload (using PUT method and a through a multi-part encoded form) or retrieve a tails file (using GET method). diff --git a/samples/tails/package.json b/samples/tails/package.json new file mode 100644 index 0000000000..44cb263a53 --- /dev/null +++ b/samples/tails/package.json @@ -0,0 +1,27 @@ +{ + "name": "test-tails-file-server", + "version": "1.0.0", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/hyperledger/aries-framework-javascript", + "directory": "samples/tails/" + }, + "license": "Apache-2.0", + "scripts": { + "start": "ts-node server.ts" + }, + "devDependencies": { + "ts-node": "^10.4.0" + }, + "dependencies": { + "@aries-framework/anoncreds": "^0.4.0", + "@aries-framework/core": "^0.4.0", + "@types/express": "^4.17.13", + "@types/multer": "^1.4.7", + "@types/uuid": "^9.0.1", + "@types/ws": "^8.5.4", + "form-data": "^4.0.0", + "multer": "^1.4.5-lts.1" + } +} diff --git a/samples/tails/server.ts b/samples/tails/server.ts new file mode 100644 index 0000000000..b02d420d30 --- /dev/null +++ b/samples/tails/server.ts @@ -0,0 +1,132 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ConsoleLogger, LogLevel } from '@aries-framework/core' +import { createHash } from 'crypto' +import express from 'express' +import fs from 'fs' +import multer, { diskStorage } from 'multer' + +const port = process.env.AGENT_PORT ? Number(process.env.AGENT_PORT) : 3001 +const app = express() + +const baseFilePath = './tails' +const indexFilePath = `./${baseFilePath}/index.json` + +if (!fs.existsSync(baseFilePath)) { + fs.mkdirSync(baseFilePath, { recursive: true }) +} +const tailsIndex = ( + fs.existsSync(indexFilePath) ? JSON.parse(fs.readFileSync(indexFilePath, { encoding: 'utf-8' })) : {} +) as Record + +const logger = new ConsoleLogger(LogLevel.debug) + +function fileHash(filePath: string, algorithm = 'sha256') { + return new Promise((resolve, reject) => { + const shasum = createHash(algorithm) + try { + const s = fs.createReadStream(filePath) + s.on('data', function (data) { + shasum.update(data) + }) + // making digest + s.on('end', function () { + const hash = shasum.digest('hex') + return resolve(hash) + }) + } catch (error) { + return reject('error in calculation') + } + }) +} + +const fileStorage = diskStorage({ + filename: (req: any, file: { originalname: string }, cb: (arg0: null, arg1: string) => void) => { + cb(null, file.originalname + '-' + new Date().toISOString()) + }, +}) + +// Allow to create invitation, no other way to ask for invitation yet +app.get('/:tailsFileId', async (req, res) => { + logger.debug(`requested file`) + + const tailsFileId = req.params.tailsFileId + if (!tailsFileId) { + res.status(409).end() + return + } + + const fileName = tailsIndex[tailsFileId] + + if (!fileName) { + logger.debug(`no entry found for tailsFileId: ${tailsFileId}`) + res.status(404).end() + return + } + + const path = `${baseFilePath}/${fileName}` + try { + logger.debug(`reading file: ${path}`) + + if (!fs.existsSync(path)) { + logger.debug(`file not found: ${path}`) + res.status(404).end() + return + } + + const file = fs.createReadStream(path) + res.setHeader('Content-Disposition', `attachment: filename="${fileName}"`) + file.pipe(res) + } catch (error) { + logger.debug(`error reading file: ${path}`) + res.status(500).end() + } +}) + +app.put('/:tailsFileId', multer({ storage: fileStorage }).single('file'), async (req, res) => { + logger.info(`tails file upload: ${req.params.tailsFileId}`) + + const file = req.file + + if (!file) { + logger.info(`No file found: ${JSON.stringify(req.headers)}`) + return res.status(400).send('No files were uploaded.') + } + + const tailsFileId = req.params.tailsFileId + if (!tailsFileId) { + // Clean up temporary file + fs.rmSync(file.path) + return res.status(409).send('Missing tailsFileId') + } + + const item = tailsIndex[tailsFileId] + + if (item) { + logger.debug(`there is already an entry for: ${tailsFileId}`) + res.status(409).end() + return + } + + const hash = await fileHash(file.path) + const destinationPath = `${baseFilePath}/${hash}` + + if (fs.existsSync(destinationPath)) { + logger.warn('tails file already exists') + } else { + fs.copyFileSync(file.path, destinationPath) + fs.rmSync(file.path) + } + + // Store filename in index + tailsIndex[tailsFileId] = hash + fs.writeFileSync(indexFilePath, JSON.stringify(tailsIndex)) + + res.status(200).end() +}) + +const run = async () => { + app.listen(port) + logger.info(`server started at port ${port}`) +} + +void run() diff --git a/samples/tails/tsconfig.json b/samples/tails/tsconfig.json new file mode 100644 index 0000000000..46efe6f721 --- /dev/null +++ b/samples/tails/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/samples/tails/yarn.lock b/samples/tails/yarn.lock new file mode 100644 index 0000000000..bf3778cf7f --- /dev/null +++ b/samples/tails/yarn.lock @@ -0,0 +1,335 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.17.33" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543" + integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@*", "@types/express@^4.17.13": + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" + integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/mime@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" + integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== + +"@types/multer@^1.4.7": + version "1.4.7" + resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.7.tgz#89cf03547c28c7bbcc726f029e2a76a7232cc79e" + integrity sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA== + dependencies: + "@types/express" "*" + +"@types/node@*": + version "18.15.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" + integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q== + +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/serve-static@*": + version "1.15.1" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.1.tgz#86b1753f0be4f9a1bee68d459fcda5be4ea52b5d" + integrity sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ== + dependencies: + "@types/mime" "*" + "@types/node" "*" + +"@types/uuid@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.1.tgz#98586dc36aee8dacc98cc396dbca8d0429647aa6" + integrity sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA== + +"@types/ws@^8.5.4": + version "8.5.4" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5" + integrity sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg== + dependencies: + "@types/node" "*" + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +busboy@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.24: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +multer@^1.4.5-lts.1: + version "1.4.5-lts.1" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.5-lts.1.tgz#803e24ad1984f58edffbc79f56e305aec5cfd1ac" + integrity sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ== + dependencies: + append-field "^1.0.0" + busboy "^1.0.0" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + type-is "^1.6.4" + xtend "^4.0.0" + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +readable-stream@^2.2.2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +ts-node@^10.4.0: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +type-is@^1.6.4: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== diff --git a/yarn.lock b/yarn.lock index 2afc180e9f..abae25902c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2855,7 +2855,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@^4.17.13", "@types/express@^4.17.15": +"@types/express@*", "@types/express@^4.17.13", "@types/express@^4.17.15": version "4.17.18" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.18.tgz#efabf5c4495c1880df1bdffee604b143b29c4a95" integrity sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ== @@ -2954,6 +2954,13 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== +"@types/multer@^1.4.7": + version "1.4.7" + resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.7.tgz#89cf03547c28c7bbcc726f029e2a76a7232cc79e" + integrity sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA== + dependencies: + "@types/express" "*" + "@types/node@*", "@types/node@18.18.8", "@types/node@>=13.7.0", "@types/node@^18.18.8": version "18.18.8" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.8.tgz#2b285361f2357c8c8578ec86b5d097c7f464cfd6" @@ -3366,6 +3373,11 @@ appdirsjs@^1.2.4: resolved "https://registry.yarnpkg.com/appdirsjs/-/appdirsjs-1.2.7.tgz#50b4b7948a26ba6090d4aede2ae2dc2b051be3b3" integrity sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw== +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -3996,6 +4008,13 @@ builtins@^5.0.0: dependencies: semver "^7.0.0" +busboy@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + byte-size@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-7.0.0.tgz#36528cd1ca87d39bd9abd51f5715dc93b6ceb032" @@ -4510,6 +4529,16 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + concat-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" @@ -9005,7 +9034,7 @@ mkdirp-infer-owner@^2.0.0: infer-owner "^1.0.4" mkdirp "^1.0.3" -mkdirp@^0.5.1, mkdirp@^0.5.5: +mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@^0.5.5: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -9042,6 +9071,19 @@ msrcrypto@^1.5.6: resolved "https://registry.yarnpkg.com/msrcrypto/-/msrcrypto-1.5.8.tgz#be419be4945bf134d8af52e9d43be7fa261f4a1c" integrity sha512-ujZ0TRuozHKKm6eGbKHfXef7f+esIhEckmThVnz7RNyiOJd7a6MXj2JGBoL9cnPDW+JMG16MoTUh5X+XXjI66Q== +multer@^1.4.5-lts.1: + version "1.4.5-lts.1" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.5-lts.1.tgz#803e24ad1984f58edffbc79f56e305aec5cfd1ac" + integrity sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ== + dependencies: + append-field "^1.0.0" + busboy "^1.0.0" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + type-is "^1.6.4" + xtend "^4.0.0" + multiformats@^9.4.2, multiformats@^9.6.5, multiformats@^9.9.0: version "9.9.0" resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" @@ -10649,7 +10691,7 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stre string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^2.0.6, readable-stream@~2.3.6: +readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -11379,6 +11421,11 @@ str2buf@^1.3.0: resolved "https://registry.yarnpkg.com/str2buf/-/str2buf-1.3.0.tgz#a4172afff4310e67235178e738a2dbb573abead0" integrity sha512-xIBmHIUHYZDP4HyoXGHYNVmxlXLXDrtFHYT0eV6IOdEj3VO9ccaF1Ejl9Oq8iFjITllpT8FhaXb4KsNmw+3EuA== +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + strict-uri-encode@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" @@ -11960,7 +12007,7 @@ type-fest@^3.2.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.9.0.tgz#36a9e46e6583649f9e6098b267bc577275e9e4f4" integrity sha512-hR8JP2e8UiH7SME5JZjsobBlEiatFoxpzCP+R3ZeCo7kAaG1jXQE5X/buLzogM6GJu8le9Y4OcfNuIQX0rZskA== -type-is@~1.6.18: +type-is@^1.6.4, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -12517,7 +12564,7 @@ xstream@^11.14.0: globalthis "^1.0.1" symbol-observable "^2.0.3" -xtend@~4.0.1: +xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== From e5cb74e4688d694a5a108c4f365db1c780f3c67e Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 27 Nov 2023 22:07:16 +0700 Subject: [PATCH 04/15] refactor(anoncreds)!: move supportRevocation to options (#1648) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile --- demo/src/Faber.ts | 2 +- .../tests/InMemoryTailsFileService.ts | 8 ++++---- packages/anoncreds-rs/tests/anoncredsSetup.ts | 5 +++-- packages/anoncreds/src/AnonCredsApi.ts | 18 +++++++++++------- .../services/tails/BasicTailsFileService.ts | 6 +++--- .../src/services/tails/TailsFileService.ts | 4 ++-- .../src/utils/getRevocationRegistries.ts | 2 +- packages/anoncreds/tests/anoncreds.test.ts | 5 +++-- .../anoncreds/tests/legacyAnonCredsSetup.ts | 5 +++-- samples/tails/FullTailsFileService.ts | 4 ++-- 10 files changed, 33 insertions(+), 26 deletions(-) diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts index a0bfaff828..abf44c67dc 100644 --- a/demo/src/Faber.ts +++ b/demo/src/Faber.ts @@ -176,8 +176,8 @@ export class Faber extends BaseAgent { issuerId: this.anonCredsIssuerId, tag: 'latest', }, - supportRevocation: false, options: { + supportRevocation: false, endorserMode: 'internal', endorserDid: this.anonCredsIssuerId, }, diff --git a/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts b/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts index 32cb2d48f4..beac976687 100644 --- a/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts +++ b/packages/anoncreds-rs/tests/InMemoryTailsFileService.ts @@ -12,11 +12,11 @@ export class InMemoryTailsFileService extends BasicTailsFileService { options: { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition } - ): Promise { + ) { this.tailsFilePaths[options.revocationRegistryDefinition.value.tailsHash] = options.revocationRegistryDefinition.value.tailsLocation - return options.revocationRegistryDefinition.value.tailsHash + return { tailsFileUrl: options.revocationRegistryDefinition.value.tailsHash } } public async getTailsFile( @@ -24,7 +24,7 @@ export class InMemoryTailsFileService extends BasicTailsFileService { options: { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition } - ): Promise { + ) { const { revocationRegistryDefinition } = options const { tailsLocation, tailsHash } = revocationRegistryDefinition.value @@ -47,7 +47,7 @@ export class InMemoryTailsFileService extends BasicTailsFileService { agentContext.config.logger.debug(`Saved tails file to FileSystem at path ${tailsFilePath}`) } - return tailsFilePath + return { tailsFilePath } } catch (error) { agentContext.config.logger.error(`Error while retrieving tails file from URL ${tailsLocation}`, { error, diff --git a/packages/anoncreds-rs/tests/anoncredsSetup.ts b/packages/anoncreds-rs/tests/anoncredsSetup.ts index 5f3cbd85ab..2ce0a49fee 100644 --- a/packages/anoncreds-rs/tests/anoncredsSetup.ts +++ b/packages/anoncreds-rs/tests/anoncredsSetup.ts @@ -505,8 +505,9 @@ async function registerCredentialDefinition( ): Promise { const { credentialDefinitionState } = await agent.modules.anoncreds.registerCredentialDefinition({ credentialDefinition, - supportRevocation: supportRevocation ?? false, - options: {}, + options: { + supportRevocation: supportRevocation ?? false, + }, }) if (credentialDefinitionState.state !== 'finished') { diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index 92d741308b..5542680c84 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -227,7 +227,7 @@ export class AnonCredsApi { } } - public async registerCredentialDefinition( + public async registerCredentialDefinition( options: AnonCredsRegisterCredentialDefinition ): Promise { const failedReturnBase = { @@ -275,7 +275,7 @@ export class AnonCredsApi { issuerId: options.credentialDefinition.issuerId, schemaId: options.credentialDefinition.schemaId, tag: options.credentialDefinition.tag, - supportRevocation: options.supportRevocation, + supportRevocation: options.options.supportRevocation, schema: schemaResult.schema, }, // FIXME: Indy SDK requires the schema seq no to be passed in here. This is not ideal. @@ -400,9 +400,10 @@ export class AnonCredsApi { // At this moment, tails file should be published and a valid public URL will be received const localTailsLocation = revocationRegistryDefinition.value.tailsLocation - revocationRegistryDefinition.value.tailsLocation = await tailsFileService.uploadTailsFile(this.agentContext, { + const { tailsFileUrl } = await tailsFileService.uploadTailsFile(this.agentContext, { revocationRegistryDefinition, }) + revocationRegistryDefinition.value.tailsLocation = tailsFileUrl const result = await registry.registerRevocationRegistryDefinition(this.agentContext, { revocationRegistryDefinition, @@ -493,7 +494,7 @@ export class AnonCredsApi { return failedReturnBase } const tailsFileService = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService - const tailsFilePath = await tailsFileService.getTailsFile(this.agentContext, { + const { tailsFilePath } = await tailsFileService.getTailsFile(this.agentContext, { revocationRegistryDefinition, }) @@ -564,7 +565,7 @@ export class AnonCredsApi { } const tailsFileService = this.agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService - const tailsFilePath = await tailsFileService.getTailsFile(this.agentContext, { + const { tailsFilePath } = await tailsFileService.getTailsFile(this.agentContext, { revocationRegistryDefinition, }) @@ -754,10 +755,13 @@ export class AnonCredsApi { } } +export interface AnonCredsRegisterCredentialDefinitionApiOptions { + supportRevocation: boolean +} + interface AnonCredsRegisterCredentialDefinition { credentialDefinition: AnonCredsRegisterCredentialDefinitionOptions - supportRevocation: boolean - options: T + options: T & AnonCredsRegisterCredentialDefinitionApiOptions } interface AnonCredsRegisterSchema { diff --git a/packages/anoncreds/src/services/tails/BasicTailsFileService.ts b/packages/anoncreds/src/services/tails/BasicTailsFileService.ts index f2cf3eeae1..27da210476 100644 --- a/packages/anoncreds/src/services/tails/BasicTailsFileService.ts +++ b/packages/anoncreds/src/services/tails/BasicTailsFileService.ts @@ -27,7 +27,7 @@ export class BasicTailsFileService implements TailsFileService { options: { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition } - ): Promise { + ): Promise<{ tailsFileUrl: string }> { throw new AriesFrameworkError('BasicTailsFileService only supports tails file downloading') } @@ -36,7 +36,7 @@ export class BasicTailsFileService implements TailsFileService { options: { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition } - ): Promise { + ) { const { revocationRegistryDefinition } = options const { tailsLocation, tailsHash } = revocationRegistryDefinition.value @@ -67,7 +67,7 @@ export class BasicTailsFileService implements TailsFileService { agentContext.config.logger.debug(`Saved tails file to FileSystem at path ${tailsFilePath}`) } - return tailsFilePath + return { tailsFilePath } } catch (error) { agentContext.config.logger.error(`Error while retrieving tails file from URL ${tailsLocation}`, { error, diff --git a/packages/anoncreds/src/services/tails/TailsFileService.ts b/packages/anoncreds/src/services/tails/TailsFileService.ts index d8e0dd7167..8bae55c598 100644 --- a/packages/anoncreds/src/services/tails/TailsFileService.ts +++ b/packages/anoncreds/src/services/tails/TailsFileService.ts @@ -25,7 +25,7 @@ export interface TailsFileService { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition revocationRegistryDefinitionId?: string } - ): Promise + ): Promise<{ tailsFileUrl: string }> /** * Retrieve the tails file for a given revocation registry, downloading it @@ -43,5 +43,5 @@ export interface TailsFileService { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition revocationRegistryDefinitionId?: string } - ): Promise + ): Promise<{ tailsFilePath: string }> } diff --git a/packages/anoncreds/src/utils/getRevocationRegistries.ts b/packages/anoncreds/src/utils/getRevocationRegistries.ts index 421fefe61a..95145cca6b 100644 --- a/packages/anoncreds/src/utils/getRevocationRegistries.ts +++ b/packages/anoncreds/src/utils/getRevocationRegistries.ts @@ -91,7 +91,7 @@ export async function getRevocationRegistriesForRequest( } const tailsFileService = agentContext.dependencyManager.resolve(AnonCredsModuleConfig).tailsFileService - const tailsFilePath = await tailsFileService.getTailsFile(agentContext, { + const { tailsFilePath } = await tailsFileService.getTailsFile(agentContext, { revocationRegistryDefinition, }) diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts index f96b116126..521f38f345 100644 --- a/packages/anoncreds/tests/anoncreds.test.ts +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -201,8 +201,9 @@ describe('AnonCreds API', () => { schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', tag: 'TAG', }, - supportRevocation: false, - options: {}, + options: { + supportRevocation: false, + }, }) expect(credentialDefinitionResult).toEqual({ diff --git a/packages/anoncreds/tests/legacyAnonCredsSetup.ts b/packages/anoncreds/tests/legacyAnonCredsSetup.ts index 9709b509ea..baef3eac54 100644 --- a/packages/anoncreds/tests/legacyAnonCredsSetup.ts +++ b/packages/anoncreds/tests/legacyAnonCredsSetup.ts @@ -516,8 +516,9 @@ async function registerCredentialDefinition( ): Promise { const { credentialDefinitionState } = await agent.modules.anoncreds.registerCredentialDefinition({ credentialDefinition, - supportRevocation: supportRevocation ?? false, - options: {}, + options: { + supportRevocation: supportRevocation ?? false, + }, }) if (credentialDefinitionState.state !== 'finished') { diff --git a/samples/tails/FullTailsFileService.ts b/samples/tails/FullTailsFileService.ts index 1ffa4c03c6..8d5d2a0eec 100644 --- a/samples/tails/FullTailsFileService.ts +++ b/samples/tails/FullTailsFileService.ts @@ -18,7 +18,7 @@ export class FullTailsFileService extends BasicTailsFileService { options: { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition } - ): Promise { + ) { const revocationRegistryDefinition = options.revocationRegistryDefinition const localTailsFilePath = revocationRegistryDefinition.value.tailsLocation @@ -36,6 +36,6 @@ export class FullTailsFileService extends BasicTailsFileService { if (response.status !== 200) { throw new Error('Cannot upload tails file') } - return `${this.tailsServerBaseUrl}/${encodeURIComponent(tailsFileId)}` + return { tailsFileUrl: `${this.tailsServerBaseUrl}/${encodeURIComponent(tailsFileId)}` } } } From ff545954a9407ed1b9244adaa54c51f05801eca4 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 27 Nov 2023 12:08:06 -0300 Subject: [PATCH 05/15] refactor: move message-pickup directory (#1650) Signed-off-by: Ariel Gentile --- packages/core/src/agent/AgentModules.ts | 2 +- packages/core/src/agent/BaseAgent.ts | 4 ++-- packages/core/src/agent/__tests__/Agent.test.ts | 2 +- packages/core/src/agent/__tests__/AgentModules.test.ts | 2 +- .../core/src/modules/message-pickup/MessagePickupApi.ts | 0 .../src/modules/message-pickup/MessagePickupApiOptions.ts | 0 .../core/src/modules/message-pickup/MessagePickupEvents.ts | 0 .../core/src/modules/message-pickup/MessagePickupModule.ts | 0 .../src/modules/message-pickup/MessagePickupModuleConfig.ts | 0 .../core/src/modules/message-pickup/MessagePickupSession.ts | 0 .../message-pickup/__tests__/MessagePickupModule.test.ts | 0 .../src/modules/message-pickup/__tests__/pickup.test.ts | 0 .../core/src/modules/message-pickup/index.ts | 0 .../message-pickup/protocol/BaseMessagePickupProtocol.ts | 0 .../message-pickup/protocol/MessagePickupProtocol.ts | 0 .../message-pickup/protocol/MessagePickupProtocolOptions.ts | 0 .../core/src/modules/message-pickup/protocol/index.ts | 0 .../message-pickup/protocol/v1/V1MessagePickupProtocol.ts | 0 .../message-pickup/protocol/v1/handlers/V1BatchHandler.ts | 0 .../protocol/v1/handlers/V1BatchPickupHandler.ts | 0 .../modules/message-pickup/protocol/v1/handlers/index.ts | 0 .../core/src/modules/message-pickup/protocol/v1/index.ts | 0 .../message-pickup/protocol/v1/messages/V1BatchMessage.ts | 0 .../protocol/v1/messages/V1BatchPickupMessage.ts | 0 .../modules/message-pickup/protocol/v1/messages/index.ts | 0 .../message-pickup/protocol/v2/V2MessagePickupProtocol.ts | 0 .../protocol/v2/__tests__/V2MessagePickupProtocol.test.ts | 0 .../protocol/v2/handlers/V2DeliveryRequestHandler.ts | 0 .../protocol/v2/handlers/V2LiveDeliveryChangeHandler.ts | 0 .../protocol/v2/handlers/V2MessageDeliveryHandler.ts | 0 .../protocol/v2/handlers/V2MessagesReceivedHandler.ts | 0 .../message-pickup/protocol/v2/handlers/V2StatusHandler.ts | 0 .../protocol/v2/handlers/V2StatusRequestHandler.ts | 0 .../modules/message-pickup/protocol/v2/handlers/index.ts | 0 .../core/src/modules/message-pickup/protocol/v2/index.ts | 0 .../protocol/v2/messages/V2DeliveryRequestMessage.ts | 0 .../protocol/v2/messages/V2LiveDeliveryChangeMessage.ts | 0 .../protocol/v2/messages/V2MessageDeliveryMessage.ts | 0 .../protocol/v2/messages/V2MessagesReceivedMessage.ts | 0 .../message-pickup/protocol/v2/messages/V2StatusMessage.ts | 0 .../protocol/v2/messages/V2StatusRequestMessage.ts | 0 .../modules/message-pickup/protocol/v2/messages/index.ts | 0 .../message-pickup/services/MessagePickupSessionService.ts | 0 .../core/src/modules/message-pickup/services/index.ts | 0 .../storage/InMemoryMessagePickupRepository.ts | 0 .../message-pickup/storage/MessagePickupRepository.ts | 0 .../storage/MessagePickupRepositoryOptions.ts | 0 .../src/modules/message-pickup/storage/QueuedMessage.ts | 0 .../modules/message-pickup/storage/QueuedMessageState.ts | 0 .../core/src/modules/message-pickup/storage/index.ts | 0 packages/core/src/modules/routing/MediationRecipientApi.ts | 6 +++--- 51 files changed, 8 insertions(+), 8 deletions(-) rename "packages/core/src/modules/message-p\303\254ckup/MessagePickupApi.ts" => packages/core/src/modules/message-pickup/MessagePickupApi.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/MessagePickupApiOptions.ts" => packages/core/src/modules/message-pickup/MessagePickupApiOptions.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/MessagePickupEvents.ts" => packages/core/src/modules/message-pickup/MessagePickupEvents.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/MessagePickupModule.ts" => packages/core/src/modules/message-pickup/MessagePickupModule.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/MessagePickupModuleConfig.ts" => packages/core/src/modules/message-pickup/MessagePickupModuleConfig.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/MessagePickupSession.ts" => packages/core/src/modules/message-pickup/MessagePickupSession.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/__tests__/MessagePickupModule.test.ts" => packages/core/src/modules/message-pickup/__tests__/MessagePickupModule.test.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/__tests__/pickup.test.ts" => packages/core/src/modules/message-pickup/__tests__/pickup.test.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/index.ts" => packages/core/src/modules/message-pickup/index.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/BaseMessagePickupProtocol.ts" => packages/core/src/modules/message-pickup/protocol/BaseMessagePickupProtocol.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/MessagePickupProtocol.ts" => packages/core/src/modules/message-pickup/protocol/MessagePickupProtocol.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/MessagePickupProtocolOptions.ts" => packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/index.ts" => packages/core/src/modules/message-pickup/protocol/index.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v1/V1MessagePickupProtocol.ts" => packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v1/handlers/V1BatchHandler.ts" => packages/core/src/modules/message-pickup/protocol/v1/handlers/V1BatchHandler.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v1/handlers/V1BatchPickupHandler.ts" => packages/core/src/modules/message-pickup/protocol/v1/handlers/V1BatchPickupHandler.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v1/handlers/index.ts" => packages/core/src/modules/message-pickup/protocol/v1/handlers/index.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v1/index.ts" => packages/core/src/modules/message-pickup/protocol/v1/index.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v1/messages/V1BatchMessage.ts" => packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchMessage.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v1/messages/V1BatchPickupMessage.ts" => packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchPickupMessage.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v1/messages/index.ts" => packages/core/src/modules/message-pickup/protocol/v1/messages/index.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/V2MessagePickupProtocol.ts" => packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts" => packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2DeliveryRequestHandler.ts" => packages/core/src/modules/message-pickup/protocol/v2/handlers/V2DeliveryRequestHandler.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2LiveDeliveryChangeHandler.ts" => packages/core/src/modules/message-pickup/protocol/v2/handlers/V2LiveDeliveryChangeHandler.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2MessageDeliveryHandler.ts" => packages/core/src/modules/message-pickup/protocol/v2/handlers/V2MessageDeliveryHandler.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2MessagesReceivedHandler.ts" => packages/core/src/modules/message-pickup/protocol/v2/handlers/V2MessagesReceivedHandler.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2StatusHandler.ts" => packages/core/src/modules/message-pickup/protocol/v2/handlers/V2StatusHandler.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2StatusRequestHandler.ts" => packages/core/src/modules/message-pickup/protocol/v2/handlers/V2StatusRequestHandler.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/index.ts" => packages/core/src/modules/message-pickup/protocol/v2/handlers/index.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/index.ts" => packages/core/src/modules/message-pickup/protocol/v2/index.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2DeliveryRequestMessage.ts" => packages/core/src/modules/message-pickup/protocol/v2/messages/V2DeliveryRequestMessage.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts" => packages/core/src/modules/message-pickup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2MessageDeliveryMessage.ts" => packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessageDeliveryMessage.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2MessagesReceivedMessage.ts" => packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessagesReceivedMessage.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2StatusMessage.ts" => packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusMessage.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2StatusRequestMessage.ts" => packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusRequestMessage.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/index.ts" => packages/core/src/modules/message-pickup/protocol/v2/messages/index.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/services/MessagePickupSessionService.ts" => packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/services/index.ts" => packages/core/src/modules/message-pickup/services/index.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/storage/InMemoryMessagePickupRepository.ts" => packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/storage/MessagePickupRepository.ts" => packages/core/src/modules/message-pickup/storage/MessagePickupRepository.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/storage/MessagePickupRepositoryOptions.ts" => packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/storage/QueuedMessage.ts" => packages/core/src/modules/message-pickup/storage/QueuedMessage.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/storage/QueuedMessageState.ts" => packages/core/src/modules/message-pickup/storage/QueuedMessageState.ts (100%) rename "packages/core/src/modules/message-p\303\254ckup/storage/index.ts" => packages/core/src/modules/message-pickup/storage/index.ts (100%) diff --git a/packages/core/src/agent/AgentModules.ts b/packages/core/src/agent/AgentModules.ts index 805a147918..ed0afcede9 100644 --- a/packages/core/src/agent/AgentModules.ts +++ b/packages/core/src/agent/AgentModules.ts @@ -9,7 +9,7 @@ import { CredentialsModule } from '../modules/credentials' import { DidsModule } from '../modules/dids' import { DiscoverFeaturesModule } from '../modules/discover-features' import { GenericRecordsModule } from '../modules/generic-records' -import { MessagePickupModule } from '../modules/message-pìckup' +import { MessagePickupModule } from '../modules/message-pickup' import { OutOfBandModule } from '../modules/oob' import { ProofsModule } from '../modules/proofs' import { MediationRecipientModule, MediatorModule } from '../modules/routing' diff --git a/packages/core/src/agent/BaseAgent.ts b/packages/core/src/agent/BaseAgent.ts index 39ea8d521d..f265f6418a 100644 --- a/packages/core/src/agent/BaseAgent.ts +++ b/packages/core/src/agent/BaseAgent.ts @@ -3,7 +3,7 @@ import type { AgentApi, CustomOrDefaultApi, EmptyModuleMap, ModulesMap, WithoutD import type { TransportSession } from './TransportService' import type { Logger } from '../logger' import type { CredentialsModule } from '../modules/credentials' -import type { MessagePickupModule } from '../modules/message-pìckup' +import type { MessagePickupModule } from '../modules/message-pickup' import type { ProofsModule } from '../modules/proofs' import type { DependencyManager } from '../plugins' @@ -14,7 +14,7 @@ import { CredentialsApi } from '../modules/credentials' import { DidsApi } from '../modules/dids' import { DiscoverFeaturesApi } from '../modules/discover-features' import { GenericRecordsApi } from '../modules/generic-records' -import { MessagePickupApi } from '../modules/message-pìckup/MessagePickupApi' +import { MessagePickupApi } from '../modules/message-pickup/MessagePickupApi' import { OutOfBandApi } from '../modules/oob' import { ProofsApi } from '../modules/proofs' import { MediatorApi, MediationRecipientApi } from '../modules/routing' diff --git a/packages/core/src/agent/__tests__/Agent.test.ts b/packages/core/src/agent/__tests__/Agent.test.ts index 6493a7ebab..d00e94e845 100644 --- a/packages/core/src/agent/__tests__/Agent.test.ts +++ b/packages/core/src/agent/__tests__/Agent.test.ts @@ -13,7 +13,7 @@ import { ConnectionService } from '../../modules/connections/services/Connection import { TrustPingService } from '../../modules/connections/services/TrustPingService' import { CredentialRepository } from '../../modules/credentials' import { CredentialsApi } from '../../modules/credentials/CredentialsApi' -import { MessagePickupApi, InMemoryMessagePickupRepository } from '../../modules/message-pìckup' +import { MessagePickupApi, InMemoryMessagePickupRepository } from '../../modules/message-pickup' import { ProofRepository } from '../../modules/proofs' import { ProofsApi } from '../../modules/proofs/ProofsApi' import { diff --git a/packages/core/src/agent/__tests__/AgentModules.test.ts b/packages/core/src/agent/__tests__/AgentModules.test.ts index aa3d9af950..7717608581 100644 --- a/packages/core/src/agent/__tests__/AgentModules.test.ts +++ b/packages/core/src/agent/__tests__/AgentModules.test.ts @@ -7,7 +7,7 @@ import { CredentialsModule } from '../../modules/credentials' import { DidsModule } from '../../modules/dids' import { DiscoverFeaturesModule } from '../../modules/discover-features' import { GenericRecordsModule } from '../../modules/generic-records' -import { MessagePickupModule } from '../../modules/message-pìckup' +import { MessagePickupModule } from '../../modules/message-pickup' import { OutOfBandModule } from '../../modules/oob' import { ProofsModule } from '../../modules/proofs' import { MediationRecipientModule, MediatorModule } from '../../modules/routing' diff --git "a/packages/core/src/modules/message-p\303\254ckup/MessagePickupApi.ts" b/packages/core/src/modules/message-pickup/MessagePickupApi.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/MessagePickupApi.ts" rename to packages/core/src/modules/message-pickup/MessagePickupApi.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/MessagePickupApiOptions.ts" b/packages/core/src/modules/message-pickup/MessagePickupApiOptions.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/MessagePickupApiOptions.ts" rename to packages/core/src/modules/message-pickup/MessagePickupApiOptions.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/MessagePickupEvents.ts" b/packages/core/src/modules/message-pickup/MessagePickupEvents.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/MessagePickupEvents.ts" rename to packages/core/src/modules/message-pickup/MessagePickupEvents.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/MessagePickupModule.ts" b/packages/core/src/modules/message-pickup/MessagePickupModule.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/MessagePickupModule.ts" rename to packages/core/src/modules/message-pickup/MessagePickupModule.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/MessagePickupModuleConfig.ts" b/packages/core/src/modules/message-pickup/MessagePickupModuleConfig.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/MessagePickupModuleConfig.ts" rename to packages/core/src/modules/message-pickup/MessagePickupModuleConfig.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/MessagePickupSession.ts" b/packages/core/src/modules/message-pickup/MessagePickupSession.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/MessagePickupSession.ts" rename to packages/core/src/modules/message-pickup/MessagePickupSession.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/__tests__/MessagePickupModule.test.ts" b/packages/core/src/modules/message-pickup/__tests__/MessagePickupModule.test.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/__tests__/MessagePickupModule.test.ts" rename to packages/core/src/modules/message-pickup/__tests__/MessagePickupModule.test.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/__tests__/pickup.test.ts" b/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/__tests__/pickup.test.ts" rename to packages/core/src/modules/message-pickup/__tests__/pickup.test.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/index.ts" b/packages/core/src/modules/message-pickup/index.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/index.ts" rename to packages/core/src/modules/message-pickup/index.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/BaseMessagePickupProtocol.ts" b/packages/core/src/modules/message-pickup/protocol/BaseMessagePickupProtocol.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/BaseMessagePickupProtocol.ts" rename to packages/core/src/modules/message-pickup/protocol/BaseMessagePickupProtocol.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/MessagePickupProtocol.ts" b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocol.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/MessagePickupProtocol.ts" rename to packages/core/src/modules/message-pickup/protocol/MessagePickupProtocol.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/MessagePickupProtocolOptions.ts" b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/MessagePickupProtocolOptions.ts" rename to packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/index.ts" b/packages/core/src/modules/message-pickup/protocol/index.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/index.ts" rename to packages/core/src/modules/message-pickup/protocol/index.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v1/V1MessagePickupProtocol.ts" b/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v1/V1MessagePickupProtocol.ts" rename to packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v1/handlers/V1BatchHandler.ts" b/packages/core/src/modules/message-pickup/protocol/v1/handlers/V1BatchHandler.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v1/handlers/V1BatchHandler.ts" rename to packages/core/src/modules/message-pickup/protocol/v1/handlers/V1BatchHandler.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v1/handlers/V1BatchPickupHandler.ts" b/packages/core/src/modules/message-pickup/protocol/v1/handlers/V1BatchPickupHandler.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v1/handlers/V1BatchPickupHandler.ts" rename to packages/core/src/modules/message-pickup/protocol/v1/handlers/V1BatchPickupHandler.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v1/handlers/index.ts" b/packages/core/src/modules/message-pickup/protocol/v1/handlers/index.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v1/handlers/index.ts" rename to packages/core/src/modules/message-pickup/protocol/v1/handlers/index.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v1/index.ts" b/packages/core/src/modules/message-pickup/protocol/v1/index.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v1/index.ts" rename to packages/core/src/modules/message-pickup/protocol/v1/index.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v1/messages/V1BatchMessage.ts" b/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchMessage.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v1/messages/V1BatchMessage.ts" rename to packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchMessage.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v1/messages/V1BatchPickupMessage.ts" b/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchPickupMessage.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v1/messages/V1BatchPickupMessage.ts" rename to packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchPickupMessage.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v1/messages/index.ts" b/packages/core/src/modules/message-pickup/protocol/v1/messages/index.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v1/messages/index.ts" rename to packages/core/src/modules/message-pickup/protocol/v1/messages/index.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/V2MessagePickupProtocol.ts" b/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/V2MessagePickupProtocol.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts" b/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2DeliveryRequestHandler.ts" b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2DeliveryRequestHandler.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2DeliveryRequestHandler.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/handlers/V2DeliveryRequestHandler.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2LiveDeliveryChangeHandler.ts" b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2LiveDeliveryChangeHandler.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2LiveDeliveryChangeHandler.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/handlers/V2LiveDeliveryChangeHandler.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2MessageDeliveryHandler.ts" b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2MessageDeliveryHandler.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2MessageDeliveryHandler.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/handlers/V2MessageDeliveryHandler.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2MessagesReceivedHandler.ts" b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2MessagesReceivedHandler.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2MessagesReceivedHandler.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/handlers/V2MessagesReceivedHandler.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2StatusHandler.ts" b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2StatusHandler.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2StatusHandler.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/handlers/V2StatusHandler.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2StatusRequestHandler.ts" b/packages/core/src/modules/message-pickup/protocol/v2/handlers/V2StatusRequestHandler.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/V2StatusRequestHandler.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/handlers/V2StatusRequestHandler.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/index.ts" b/packages/core/src/modules/message-pickup/protocol/v2/handlers/index.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/handlers/index.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/handlers/index.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/index.ts" b/packages/core/src/modules/message-pickup/protocol/v2/index.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/index.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/index.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2DeliveryRequestMessage.ts" b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2DeliveryRequestMessage.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2DeliveryRequestMessage.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/messages/V2DeliveryRequestMessage.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts" b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2MessageDeliveryMessage.ts" b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessageDeliveryMessage.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2MessageDeliveryMessage.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessageDeliveryMessage.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2MessagesReceivedMessage.ts" b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessagesReceivedMessage.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2MessagesReceivedMessage.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessagesReceivedMessage.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2StatusMessage.ts" b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusMessage.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2StatusMessage.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusMessage.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2StatusRequestMessage.ts" b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusRequestMessage.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/V2StatusRequestMessage.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusRequestMessage.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/index.ts" b/packages/core/src/modules/message-pickup/protocol/v2/messages/index.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/protocol/v2/messages/index.ts" rename to packages/core/src/modules/message-pickup/protocol/v2/messages/index.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/services/MessagePickupSessionService.ts" b/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/services/MessagePickupSessionService.ts" rename to packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/services/index.ts" b/packages/core/src/modules/message-pickup/services/index.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/services/index.ts" rename to packages/core/src/modules/message-pickup/services/index.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/storage/InMemoryMessagePickupRepository.ts" b/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/storage/InMemoryMessagePickupRepository.ts" rename to packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/storage/MessagePickupRepository.ts" b/packages/core/src/modules/message-pickup/storage/MessagePickupRepository.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/storage/MessagePickupRepository.ts" rename to packages/core/src/modules/message-pickup/storage/MessagePickupRepository.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/storage/MessagePickupRepositoryOptions.ts" b/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/storage/MessagePickupRepositoryOptions.ts" rename to packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/storage/QueuedMessage.ts" b/packages/core/src/modules/message-pickup/storage/QueuedMessage.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/storage/QueuedMessage.ts" rename to packages/core/src/modules/message-pickup/storage/QueuedMessage.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/storage/QueuedMessageState.ts" b/packages/core/src/modules/message-pickup/storage/QueuedMessageState.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/storage/QueuedMessageState.ts" rename to packages/core/src/modules/message-pickup/storage/QueuedMessageState.ts diff --git "a/packages/core/src/modules/message-p\303\254ckup/storage/index.ts" b/packages/core/src/modules/message-pickup/storage/index.ts similarity index 100% rename from "packages/core/src/modules/message-p\303\254ckup/storage/index.ts" rename to packages/core/src/modules/message-pickup/storage/index.ts diff --git a/packages/core/src/modules/routing/MediationRecipientApi.ts b/packages/core/src/modules/routing/MediationRecipientApi.ts index 3a1c9242e9..c86d917638 100644 --- a/packages/core/src/modules/routing/MediationRecipientApi.ts +++ b/packages/core/src/modules/routing/MediationRecipientApi.ts @@ -23,9 +23,9 @@ import { ConnectionService } from '../connections/services' import { DidsApi } from '../dids' import { verkeyToDidKey } from '../dids/helpers' import { DiscoverFeaturesApi } from '../discover-features' -import { MessagePickupApi } from '../message-pìckup/MessagePickupApi' -import { V1BatchPickupMessage } from '../message-pìckup/protocol/v1' -import { V2StatusMessage } from '../message-pìckup/protocol/v2' +import { MessagePickupApi } from '../message-pickup/MessagePickupApi' +import { V1BatchPickupMessage } from '../message-pickup/protocol/v1' +import { V2StatusMessage } from '../message-pickup/protocol/v2' import { MediationRecipientModuleConfig } from './MediationRecipientModuleConfig' import { MediatorPickupStrategy } from './MediatorPickupStrategy' From 55ba318fb2b2b7a9c262e3df98d7759d4fb11780 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 28 Nov 2023 18:25:16 -0500 Subject: [PATCH 06/15] feat: addressing some feedback and fixes Signed-off-by: Ariel Gentile --- packages/core/src/agent/MessageSender.ts | 24 ++++---- packages/core/src/index.ts | 2 +- .../message-pickup/MessagePickupApi.ts | 39 ++++++------ .../message-pickup/MessagePickupApiOptions.ts | 2 +- .../message-pickup/MessagePickupEvents.ts | 1 - .../protocol/v2/V2MessagePickupProtocol.ts | 18 +++--- .../services/MessagePickupSessionService.ts | 6 +- .../InMemoryMessagePickupRepository.ts | 59 ++++++++++++------- .../storage/MessagePickupRepositoryOptions.ts | 4 +- .../routing/services/MediatorService.ts | 16 ++--- .../core/src/transport/TransportEventTypes.ts | 4 +- 11 files changed, 97 insertions(+), 78 deletions(-) diff --git a/packages/core/src/agent/MessageSender.ts b/packages/core/src/agent/MessageSender.ts index 1ac095b556..f89934fbd4 100644 --- a/packages/core/src/agent/MessageSender.ts +++ b/packages/core/src/agent/MessageSender.ts @@ -5,6 +5,7 @@ import type { TransportSession } from './TransportService' import type { AgentContext } from './context' import type { ConnectionRecord } from '../modules/connections' import type { ResolvedDidCommService } from '../modules/didcomm' +import type { DidDocument } from '../modules/dids' import type { OutOfBandRecord } from '../modules/oob/repository' import type { OutboundTransport } from '../transport/OutboundTransport' import type { EncryptedMessage, OutboundPackage } from '../types' @@ -14,9 +15,8 @@ import { ReturnRouteTypes } from '../decorators/transport/TransportDecorator' import { AriesFrameworkError, MessageSendingError } from '../error' import { Logger } from '../logger' import { DidCommDocumentService } from '../modules/didcomm' -import { DidKey, type DidDocument } from '../modules/dids' import { getKeyFromVerificationMethod } from '../modules/dids/domain/key-type' -import { didKeyToInstanceOfKey } from '../modules/dids/helpers' +import { didKeyToInstanceOfKey, verkeyToDidKey } from '../modules/dids/helpers' import { DidResolverService } from '../modules/dids/services/DidResolverService' import { MessagePickupRepository } from '../modules/message-pìckup/storage' import { inject, injectable } from '../plugins' @@ -113,9 +113,11 @@ export class MessageSender { { connection, encryptedMessage, + recipientKey, options, }: { connection: ConnectionRecord + recipientKey: string encryptedMessage: EncryptedMessage options?: { transportPriority?: TransportPriorityOptions } } @@ -176,7 +178,11 @@ export class MessageSender { // If the other party shared a queue service endpoint in their did doc we queue the message if (queueService) { this.logger.debug(`Queue packed message for connection ${connection.id} (${connection.theirLabel})`) - await this.messagePickupRepository.addMessage({ connectionId: connection.id, payload: encryptedMessage }) + await this.messagePickupRepository.addMessage({ + connectionId: connection.id, + recipientKeys: [recipientKey], + payload: encryptedMessage, + }) return } @@ -327,13 +333,11 @@ export class MessageSender { } const encryptedMessage = await this.envelopeService.packMessage(agentContext, message, keys) - for (const recipientKey of keys.recipientKeys) { - await this.messagePickupRepository.addMessage({ - connectionId: connection.id, - recipientKey: new DidKey(recipientKey).did, - payload: encryptedMessage, - }) - } + await this.messagePickupRepository.addMessage({ + connectionId: connection.id, + recipientKeys: keys.recipientKeys.map((item) => verkeyToDidKey(item.publicKeyBase58)), + payload: encryptedMessage, + }) this.emitMessageSentEvent(outboundMessageContext, OutboundMessageSendStatus.QueuedForPickup) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2e4a214579..3c07b50a33 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -33,7 +33,6 @@ export { DidCommMimeType, KeyDerivationMethod } from './types' export type { FileSystem, DownloadToFileOptions } from './storage/FileSystem' export * from './storage/BaseRecord' export { DidCommMessageRecord, DidCommMessageRole, DidCommMessageRepository } from './storage/didcomm' -export { InMemoryMessagePickupRepository } from './modules/message-pìckup' export { Repository } from './storage/Repository' export * from './storage/RepositoryEvents' export { StorageService, Query, SimpleQuery, BaseRecordConstructor } from './storage/StorageService' @@ -53,6 +52,7 @@ export * from './modules/basic-messages' export * from './modules/common' export * from './modules/credentials' export * from './modules/discover-features' +export * from './modules/message-pickup' export * from './modules/problem-reports' export * from './modules/proofs' export * from './modules/connections' diff --git a/packages/core/src/modules/message-pickup/MessagePickupApi.ts b/packages/core/src/modules/message-pickup/MessagePickupApi.ts index 65d87b2ed8..f854f7e0be 100644 --- a/packages/core/src/modules/message-pickup/MessagePickupApi.ts +++ b/packages/core/src/modules/message-pickup/MessagePickupApi.ts @@ -77,14 +77,14 @@ export class MessagePickupApi { this.logger.debug('Queuing message...') - const { connectionId, message, recipientKey } = options + const { connectionId, message, recipientKeys } = options const connectionRecord = await this.connectionService.getById(this.agentContext, connectionId) const messagePickupRepository = this.agentContext.dependencyManager.resolve( InjectionSymbols.MessagePickupRepository ) - await messagePickupRepository.addMessage({ connectionId: connectionRecord.id, recipientKey, payload: message }) + await messagePickupRepository.addMessage({ connectionId: connectionRecord.id, recipientKeys, payload: message }) } /** @@ -95,7 +95,7 @@ export class MessagePickupApi new Attachment({ @@ -119,12 +123,10 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { }) ) - if (messages.length > 0) { - return { - message: new V2MessageDeliveryMessage({ - attachments, - }), - } + return { + message: new V2MessageDeliveryMessage({ + attachments, + }), } } @@ -218,7 +220,7 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { ) if (message.messageIdList.length) { - await messageRepository.removeMessages({ connectionId: connection.id, messageIds: message.messageIdList }) + await messageRepository.removeMessages({ messageIds: message.messageIdList }) } const statusMessage = new V2StatusMessage({ diff --git a/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts b/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts index cde670e090..bfec964e2c 100644 --- a/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts +++ b/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts @@ -1,6 +1,6 @@ import type { AgentContext } from '../../../agent' import type { TransportSessionRemovedEvent } from '../../../transport' -import type { MessagePickupLiveSessionRemovedEvent } from '../MessagePickupEvents' +import type { MessagePickupLiveSessionRemovedEvent, MessagePickupLiveSessionSavedEvent } from '../MessagePickupEvents' import type { MessagePickupSession, MessagePickupSessionRole } from '../MessagePickupSession' import { takeUntil, type Subject } from 'rxjs' @@ -71,8 +71,8 @@ export class MessagePickupSessionService { this.sessions.push(session) const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) - eventEmitter.emit(agentContext, { - type: MessagePickupEventTypes.LiveSessionRemoved, + eventEmitter.emit(agentContext, { + type: MessagePickupEventTypes.LiveSessionSaved, payload: { session, }, diff --git a/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts b/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts index 4aa657d92e..efea72f80b 100644 --- a/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts +++ b/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts @@ -12,54 +12,69 @@ import { Logger } from '../../../logger' import { injectable, inject } from '../../../plugins' import { uuid } from '../../../utils/uuid' +interface InMemoryQueuedMessage extends QueuedMessage { + connectionId: string + recipientKeys: string[] +} + @injectable() export class InMemoryMessagePickupRepository implements MessagePickupRepository { private logger: Logger - private messages: { [key: string]: QueuedMessage[] } = {} + private messages: InMemoryQueuedMessage[] public constructor(@inject(InjectionSymbols.Logger) logger: Logger) { this.logger = logger + this.messages = [] } public getAvailableMessageCount(options: GetAvailableMessageCountOptions): number | Promise { - const { connectionId } = options - return this.messages[connectionId] ? this.messages[connectionId].length : 0 + const { connectionId, recipientKey } = options + + const messages = this.messages.filter( + (msg) => + msg.connectionId === connectionId && (recipientKey === undefined || msg.recipientKeys.includes(recipientKey)) + ) + return messages.length } public takeFromQueue(options: TakeFromQueueOptions) { - const { connectionId, limit, keepMessages } = options + const { connectionId, recipientKey, limit, keepMessages } = options - if (!this.messages[connectionId]) { - return [] - } + const messages = this.messages.filter( + (msg) => + msg.connectionId === connectionId && (recipientKey === undefined || msg.recipientKeys.includes(recipientKey)) + ) - const messagesToTake = limit ?? this.messages[connectionId].length + const messagesToTake = limit ?? messages.length this.logger.debug(`Taking ${messagesToTake} messages from queue for connection ${connectionId}`) - return keepMessages - ? this.messages[connectionId].slice(0, messagesToTake) - : this.messages[connectionId].splice(0, messagesToTake) + if (!keepMessages) { + this.removeMessages({ messageIds: messages.map((msg) => msg.id) }) + } + + return messages } public addMessage(options: AddMessageOptions) { - const { connectionId, payload } = options - if (!this.messages[connectionId]) { - this.messages[connectionId] = [] - } + const { connectionId, recipientKeys, payload } = options const id = uuid() - this.messages[connectionId].push({ id, encryptedMessage: payload }) + this.messages.push({ + id, + connectionId, + encryptedMessage: payload, + recipientKeys, + }) + return id } - public removeMessages(options: RemoveMessagesOptions): void | Promise { - const { connectionId, messageIds } = options - - if (!this.messages[connectionId]) return + public removeMessages(options: RemoveMessagesOptions) { + const { messageIds } = options for (const messageId of messageIds) { - const messageIndex = this.messages[connectionId].findIndex((item) => item.id === messageId) - if (messageIndex > -1) this.messages[connectionId].splice(messageIndex, 1) + const messageIndex = this.messages.findIndex((item) => item.id === messageId) + if (messageIndex > -1) this.messages.splice(messageIndex, 1) } } } diff --git a/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts b/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts index 90dbfef93e..1412dbddad 100644 --- a/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts +++ b/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts @@ -16,12 +16,10 @@ export interface TakeFromQueueOptions { export interface AddMessageOptions { connectionId: string - recipientKey?: string + recipientKeys: string[] payload: EncryptedMessage } export interface RemoveMessagesOptions { - connectionId: string - recipientKey?: string messageIds: string[] } diff --git a/packages/core/src/modules/routing/services/MediatorService.ts b/packages/core/src/modules/routing/services/MediatorService.ts index a8d3c1dc87..d54b3720c3 100644 --- a/packages/core/src/modules/routing/services/MediatorService.ts +++ b/packages/core/src/modules/routing/services/MediatorService.ts @@ -72,22 +72,21 @@ export class MediatorService { public async processForwardMessage(messageContext: InboundMessageContext): Promise { const { message, agentContext } = messageContext - const connection = messageContext.assertReadyConnection() // TODO: update to class-validator validation if (!message.to) { throw new AriesFrameworkError('Invalid Message: Missing required attribute "to"') } - const mediationRecord = await this.mediationRepository.getSingleByRecipientKey( - messageContext.agentContext, - message.to - ) + const mediationRecord = await this.mediationRepository.getSingleByRecipientKey(agentContext, message.to) // Assert mediation record is ready to be used mediationRecord.assertReady() mediationRecord.assertRole(MediationRole.Mediator) + const connection = await this.connectionService.getById(agentContext, mediationRecord.connectionId) + connection.assertReady() + const messageForwardingStrategy = agentContext.dependencyManager.resolve(MediatorModuleConfig).messageForwardingStrategy const messageSender = agentContext.dependencyManager.resolve(MessageSender) @@ -96,14 +95,14 @@ export class MediatorService { case MessageForwardingStrategy.QueueOnly: await this.messagePickupApi.queueMessage({ connectionId: mediationRecord.connectionId, - recipientKey: verkeyToDidKey(message.to), + recipientKeys: [verkeyToDidKey(message.to)], message: message.message, }) break case MessageForwardingStrategy.QueueAndDeliver: await this.messagePickupApi.queueMessage({ connectionId: mediationRecord.connectionId, - recipientKey: verkeyToDidKey(message.to), + recipientKeys: [verkeyToDidKey(message.to)], message: message.message, }) await this.messagePickupApi.deliverQueuedMessages({ @@ -115,7 +114,8 @@ export class MediatorService { // The message inside the forward message is packed so we just send the packed // message to the connection associated with it await messageSender.sendPackage(agentContext, { - connection: connection, + connection, + recipientKey: verkeyToDidKey(message.to), encryptedMessage: message.message, }) } diff --git a/packages/core/src/transport/TransportEventTypes.ts b/packages/core/src/transport/TransportEventTypes.ts index 25dc5aef5e..1dd34674d3 100644 --- a/packages/core/src/transport/TransportEventTypes.ts +++ b/packages/core/src/transport/TransportEventTypes.ts @@ -4,8 +4,8 @@ import type { TransportSession } from '../agent/TransportService' export enum TransportEventTypes { OutboundWebSocketClosedEvent = 'OutboundWebSocketClosedEvent', OutboundWebSocketOpenedEvent = 'OutboundWebSocketOpenedEvent', - TransportSessionSaved = 'TransportSessionSaved ', - TransportSessionRemoved = 'TransportSessionSaved ', + TransportSessionSaved = 'TransportSessionSaved', + TransportSessionRemoved = 'TransportSessionRemoved', } export interface OutboundWebSocketClosedEvent extends BaseEvent { From 8e7c1f8721d27524acc00d1f770e916f8e1d72a8 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 28 Nov 2023 19:47:57 -0500 Subject: [PATCH 07/15] fix: properly register session service Signed-off-by: Ariel Gentile --- .../src/modules/message-pickup/MessagePickupModule.ts | 4 ++++ .../message-pickup/__tests__/MessagePickupModule.test.ts | 9 +++++++++ packages/core/src/modules/message-pickup/index.ts | 2 ++ 3 files changed, 15 insertions(+) diff --git a/packages/core/src/modules/message-pickup/MessagePickupModule.ts b/packages/core/src/modules/message-pickup/MessagePickupModule.ts index 364959dd2c..5c8d869694 100644 --- a/packages/core/src/modules/message-pickup/MessagePickupModule.ts +++ b/packages/core/src/modules/message-pickup/MessagePickupModule.ts @@ -10,6 +10,7 @@ import { InjectionSymbols } from '../../constants' import { MessagePickupApi } from './MessagePickupApi' import { MessagePickupModuleConfig } from './MessagePickupModuleConfig' import { V1MessagePickupProtocol, V2MessagePickupProtocol } from './protocol' +import { MessagePickupSessionService } from './services' import { InMemoryMessagePickupRepository } from './storage' /** @@ -48,6 +49,9 @@ export class MessagePickupModule @@ -25,6 +28,12 @@ describe('MessagePickupModule', () => { expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) expect(dependencyManager.registerInstance).toHaveBeenCalledWith(MessagePickupModuleConfig, module.config) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith( + InjectionSymbols.MessagePickupRepository, + InMemoryMessagePickupRepository + ) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(MessagePickupSessionService) expect(featureRegistry.register).toHaveBeenCalledTimes(2) expect(featureRegistry.register).toHaveBeenCalledWith( new Protocol({ diff --git a/packages/core/src/modules/message-pickup/index.ts b/packages/core/src/modules/message-pickup/index.ts index 86b3c1915f..b2b05ba8ee 100644 --- a/packages/core/src/modules/message-pickup/index.ts +++ b/packages/core/src/modules/message-pickup/index.ts @@ -1,6 +1,8 @@ export * from './MessagePickupApi' export * from './MessagePickupApiOptions' +export * from './MessagePickupEvents' export * from './MessagePickupModule' export * from './MessagePickupModuleConfig' export * from './protocol' export * from './storage' +export { MessagePickupSessionService } from './services' From c198993bcedd0f5c3366739fb4f94a52f4d47fc2 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Wed, 29 Nov 2023 14:35:33 -0500 Subject: [PATCH 08/15] fix: don't send trust ping in V2 pickup protocol Signed-off-by: Ariel Gentile --- .../protocol/v2/V2MessagePickupProtocol.ts | 38 +------------------ .../InMemoryMessagePickupRepository.ts | 2 +- .../storage/MessagePickupRepositoryOptions.ts | 1 + 3 files changed, 4 insertions(+), 37 deletions(-) diff --git a/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts index a99c6a911b..78936f8b8f 100644 --- a/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts +++ b/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts @@ -17,12 +17,10 @@ import type { import { EventEmitter } from '../../../../agent/EventEmitter' import { AgentEventTypes } from '../../../../agent/Events' -import { MessageSender } from '../../../../agent/MessageSender' import { OutboundMessageContext, Protocol } from '../../../../agent/models' import { InjectionSymbols } from '../../../../constants' import { Attachment } from '../../../../decorators/attachment/Attachment' import { injectable } from '../../../../plugins' -import { ConnectionService } from '../../../connections' import { verkeyToDidKey } from '../../../dids/helpers' import { ProblemReportError } from '../../../problem-reports' import { RoutingProblemReportReason } from '../../../routing/error' @@ -220,7 +218,7 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { ) if (message.messageIdList.length) { - await messageRepository.removeMessages({ messageIds: message.messageIdList }) + await messageRepository.removeMessages({ connectionId: connection.id, messageIds: message.messageIdList }) } const statusMessage = new V2StatusMessage({ @@ -232,45 +230,13 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { } public async processStatus(messageContext: InboundMessageContext) { - const connection = messageContext.assertReadyConnection() const { message: statusMessage } = messageContext const { messageCount, recipientKey } = statusMessage - const connectionService = messageContext.agentContext.dependencyManager.resolve(ConnectionService) - const messageSender = messageContext.agentContext.dependencyManager.resolve(MessageSender) const messagePickupModuleConfig = messageContext.agentContext.dependencyManager.resolve(MessagePickupModuleConfig) - //No messages to be sent + //No messages to be retrieved if (messageCount === 0) { - const { message, connectionRecord } = await connectionService.createTrustPing( - messageContext.agentContext, - connection, - { - responseRequested: false, - } - ) - - // FIXME: check where this flow fits, as it seems very particular for the AFJ-ACA-Py combination - const websocketSchemes = ['ws', 'wss'] - - await messageSender.sendMessage( - new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: connectionRecord, - }), - { - transportPriority: { - schemes: websocketSchemes, - restrictive: true, - // TODO: add keepAlive: true to enforce through the public api - // we need to keep the socket alive. It already works this way, but would - // be good to make more explicit from the public facing API. - // This would also make it easier to change the internal API later on. - // keepAlive: true, - }, - } - ) - return null } const { maximumBatchSize: maximumMessagePickup } = messagePickupModuleConfig diff --git a/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts b/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts index efea72f80b..5346de4e2a 100644 --- a/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts +++ b/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts @@ -49,7 +49,7 @@ export class InMemoryMessagePickupRepository implements MessagePickupRepository this.logger.debug(`Taking ${messagesToTake} messages from queue for connection ${connectionId}`) if (!keepMessages) { - this.removeMessages({ messageIds: messages.map((msg) => msg.id) }) + this.removeMessages({ connectionId, messageIds: messages.map((msg) => msg.id) }) } return messages diff --git a/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts b/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts index 1412dbddad..108ccc6a3d 100644 --- a/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts +++ b/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts @@ -21,5 +21,6 @@ export interface AddMessageOptions { } export interface RemoveMessagesOptions { + connectionId: string messageIds: string[] } From 5afcd33495d8983b5724d13caca087e6ae560756 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Wed, 13 Dec 2023 19:36:18 -0300 Subject: [PATCH 09/15] fix: only remove session if it's not the new one Signed-off-by: Ariel Gentile feat: deliver messages individually, not fetching from the queue every time Signed-off-by: Ariel Gentile chore: revert to free runners (#1662) Signed-off-by: Ry Jones Signed-off-by: Ariel Gentile chore: create settings.yml (#1663) Signed-off-by: Ry Jones Signed-off-by: Ariel Gentile chore: fix ci and add note to readme (#1669) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile docs: update active maintainers (#1664) Signed-off-by: Karim Stekelenburg Signed-off-by: Ariel Gentile feat: did:peer:2 and did:peer:4 support in DID Exchange (#1550) Signed-off-by: Ariel Gentile feat(presentation-exchange): added PresentationExchangeService (#1672) Signed-off-by: Berend Sliedrecht Signed-off-by: Ariel Gentile chore: removed jan as maintainer (#1678) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile ci: change secret (#1679) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile chore: add meta to rxjs timeout errors (#1683) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile build(deps): bump ws and @types/ws (#1686) Signed-off-by: dependabot[bot] Signed-off-by: Ariel Gentile build(deps): bump follow-redirects from 1.15.2 to 1.15.4 (#1694) Signed-off-by: dependabot[bot] Signed-off-by: Ariel Gentile chore: update shared components libraries (#1691) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile fix: properly print key class (#1684) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile feat(present-proof): add support for aries RFC 510 (#1676) Signed-off-by: Berend Sliedrecht Signed-off-by: Ariel Gentile fix(present-proof): isolated tests (#1696) Signed-off-by: Ariel Gentile feat(indy-vdr): register revocation registry definitions and status list (#1693) Signed-off-by: Ariel Gentile chore: rename to credo-ts (#1703) Signed-off-by: Ry Jones Signed-off-by: Ariel Gentile ci: fix git checkout path and update setup-node (#1709) Signed-off-by: Ariel Gentile fix: remove check for DifPresentationExchangeService dependency (#1702) Signed-off-by: Sai Ranjit Tummalapalli Signed-off-by: Ariel Gentile docs: update zoom meeting link (#1706) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile refactor(oob)!: make label optional (#1680) Signed-off-by: Timo Glastra Co-authored-by: Ariel Gentile Signed-off-by: Ariel Gentile feat: support short legacy connectionless invitations (#1705) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile feat(dids)!: did caching (#1710) feat: did caching Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile fix: jsonld document loader node 18 (#1454) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile build(deps): bump amannn/action-semantic-pull-request from 5.3.0 to 5.4.0 (#1656) build(deps): bump amannn/action-semantic-pull-request Bumps [amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request) from 5.3.0 to 5.4.0. - [Release notes](https://github.com/amannn/action-semantic-pull-request/releases) - [Changelog](https://github.com/amannn/action-semantic-pull-request/blob/main/CHANGELOG.md) - [Commits](https://github.com/amannn/action-semantic-pull-request/compare/v5.3.0...v5.4.0) --- updated-dependencies: - dependency-name: amannn/action-semantic-pull-request dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Timo Glastra Signed-off-by: Ariel Gentile feat: did rotate (#1699) Signed-off-by: Ariel Gentile refactor: pickup protocol method names Signed-off-by: Ariel Gentile --- .github/settings.yml | 18 + .github/workflows/continuous-deployment.yml | 18 +- .github/workflows/continuous-integration.yml | 20 +- .github/workflows/lint-pr.yml | 2 +- CODEOWNERS | 4 +- MAINTAINERS.md | 4 +- README.md | 12 +- demo/package.json | 6 +- package.json | 2 +- packages/anoncreds-rs/package.json | 6 +- .../anoncreds-rs/tests/LocalDidResolver.ts | 1 + .../anoncreds-rs/tests/anoncreds-flow.test.ts | 2 +- packages/anoncreds/src/index.ts | 1 + packages/anoncreds/src/models/registry.ts | 2 +- .../RevocationRegistryDefinitionOptions.ts | 19 +- .../registry/RevocationStatusListOptions.ts | 31 +- .../utils/__tests__/indyIdentifiers.test.ts | 4 +- .../anoncreds/src/utils/indyIdentifiers.ts | 16 +- .../tests/InMemoryAnonCredsRegistry.ts | 26 +- packages/askar/package.json | 6 +- .../signature-suites/BbsBlsSignature2020.ts | 29 +- .../BbsBlsSignatureProof2020.ts | 27 +- .../src/types/CanonizeOptions.ts | 6 +- .../src/types/CreateProofOptions.ts | 4 - .../src/types/CreateVerifyDataOptions.ts | 7 +- .../src/types/DeriveProofOptions.ts | 6 +- .../src/types/SuiteSignOptions.ts | 5 +- .../src/types/VerifyProofOptions.ts | 4 - .../src/types/VerifySignatureOptions.ts | 4 - .../tests/bbs-signatures.e2e.test.ts | 4 +- .../tests/bbs-signing-provider.e2e.test.ts | 4 +- packages/bbs-signatures/tests/util.ts | 4 +- ...proof.credentials.propose-offerBbs.test.ts | 4 +- packages/cheqd/src/dids/CheqdDidResolver.ts | 1 + packages/core/package.json | 11 +- packages/core/src/agent/AgentModules.ts | 2 + packages/core/src/agent/TransportService.ts | 2 +- .../core/src/agent/__tests__/Agent.test.ts | 8 +- .../src/agent/__tests__/AgentModules.test.ts | 4 + packages/core/src/crypto/Key.ts | 9 + .../src/decorators/attachment/Attachment.ts | 5 +- packages/core/src/index.ts | 1 + .../src/modules/connections/ConnectionsApi.ts | 130 ++- .../modules/connections/ConnectionsModule.ts | 11 +- .../connections/ConnectionsModuleConfig.ts | 44 + .../connections/DidExchangeProtocol.ts | 389 ++++--- .../__tests__/ConnectionsModule.test.ts | 4 +- .../__tests__/InMemoryDidRegistry.ts | 105 ++ .../__tests__/did-rotate.e2e.test.ts | 385 +++++++ .../__tests__/didexchange-numalgo.e2e.test.ts | 193 ++++ .../handlers/DidRotateAckHandler.ts | 17 + .../connections/handlers/DidRotateHandler.ts | 26 + .../handlers/DidRotateProblemReportHandler.ts | 17 + .../connections/handlers/HangupHandler.ts | 17 + .../src/modules/connections/handlers/index.ts | 4 + .../messages/DidExchangeCompleteMessage.ts | 2 +- .../DidExchangeProblemReportMessage.ts | 2 +- .../messages/DidExchangeRequestMessage.ts | 2 +- .../messages/DidExchangeResponseMessage.ts | 11 +- .../messages/DidRotateAckMessage.ts | 20 + .../connections/messages/DidRotateMessage.ts | 38 + .../messages/DidRotateProblemReportMessage.ts | 19 + .../connections/messages/HangupMessage.ts | 30 + .../src/modules/connections/messages/index.ts | 4 + .../connections/models/DidRotateRole.ts | 4 + .../connections/models/HandshakeProtocol.ts | 2 +- .../src/modules/connections/models/index.ts | 1 + .../repository/ConnectionMetadataTypes.ts | 6 + .../repository/ConnectionRecord.ts | 10 + .../repository/ConnectionRepository.ts | 10 +- .../__tests__/ConnectionRecord.test.ts | 2 + .../connections/services/ConnectionService.ts | 5 +- .../connections/services/DidRotateService.ts | 276 +++++ .../modules/connections/services/helpers.ts | 59 +- .../src/modules/connections/services/index.ts | 1 + .../services/DidCommDocumentService.ts | 4 +- packages/core/src/modules/dids/DidsApi.ts | 3 + .../modules/dids/__tests__/DidsApi.test.ts | 46 +- .../src/modules/dids/domain/DidDocument.ts | 4 +- .../src/modules/dids/domain/DidResolver.ts | 2 + .../dids/methods/jwk/JwkDidResolver.ts | 5 + .../dids/methods/key/KeyDidResolver.ts | 5 + .../dids/methods/peer/PeerDidRegistrar.ts | 44 +- .../dids/methods/peer/PeerDidResolver.ts | 21 +- .../methods/peer/__tests__/DidPeer.test.ts | 20 +- .../peer/__tests__/PeerDidRegistrar.test.ts | 109 +- .../__fixtures__/didPeer4zQmUJdJ.json | 24 + .../__fixtures__/didPeer4zQmd8Cp.json | 39 + .../peer/__tests__/peerDidNumAlgo4.test.ts | 45 + .../peer/createPeerDidDocumentFromServices.ts | 6 +- .../src/modules/dids/methods/peer/didPeer.ts | 20 +- .../dids/methods/peer/peerDidNumAlgo4.ts | 138 +++ .../dids/methods/web/WebDidResolver.ts | 2 + .../src/modules/dids/repository/DidRecord.ts | 4 + .../modules/dids/repository/DidRepository.ts | 14 +- .../dids/services/DidResolverService.ts | 65 +- .../__tests__/DidResolverService.test.ts | 71 +- packages/core/src/modules/dids/types.ts | 25 +- .../DifPresentationExchangeError.ts | 13 + .../DifPresentationExchangeModule.ts | 25 + .../DifPresentationExchangeService.ts | 548 ++++++++++ .../dif-presentation-exchange/index.ts | 4 + .../models/DifPexCredentialsForRequest.ts | 119 +++ .../dif-presentation-exchange/models/index.ts | 11 + .../utils/credentialSelection.ts | 314 ++++++ .../dif-presentation-exchange/utils/index.ts | 2 + .../utils/transform.ts | 78 ++ .../discover-features/DiscoverFeaturesApi.ts | 5 +- .../message-pickup/MessagePickupApi.ts | 79 +- .../message-pickup/MessagePickupApiOptions.ts | 10 +- .../message-pickup/MessagePickupSession.ts | 1 + .../protocol/BaseMessagePickupProtocol.ts | 4 +- .../protocol/MessagePickupProtocol.ts | 4 +- .../protocol/MessagePickupProtocolOptions.ts | 2 + .../protocol/v1/V1MessagePickupProtocol.ts | 20 +- .../protocol/v2/V2MessagePickupProtocol.ts | 26 +- .../__tests__/V2MessagePickupProtocol.test.ts | 8 +- .../services/MessagePickupSessionService.ts | 8 +- .../InMemoryMessagePickupRepository.ts | 19 +- .../storage/MessagePickupRepositoryOptions.ts | 2 - .../message-pickup/storage/QueuedMessage.ts | 1 - .../storage/QueuedMessageState.ts | 5 - .../modules/message-pickup/storage/index.ts | 1 - packages/core/src/modules/oob/OutOfBandApi.ts | 11 +- .../core/src/modules/oob/OutOfBandService.ts | 1 - packages/core/src/modules/oob/helpers.ts | 6 +- .../oob/messages/OutOfBandInvitation.ts | 4 +- .../DifPresentationExchangeProofFormat.ts | 73 ++ ...fPresentationExchangeProofFormatService.ts | 367 +++++++ ...entationExchangeProofFormatService.test.ts | 204 ++++ .../dif-presentation-exchange/index.ts | 2 + .../core/src/modules/proofs/formats/index.ts | 2 + .../proofs/protocol/v2/V2ProofProtocol.ts | 2 +- .../proofs/protocol/v2/__tests__/fixtures.ts | 13 + ...entation-exchange-presentation.e2e.test.ts | 451 ++++++++ .../core/src/modules/proofs/utils/index.ts | 1 + .../modules/routing/MediationRecipientApi.ts | 5 +- .../services/MediationRecipientService.ts | 5 +- .../routing/services/MediatorService.ts | 16 +- .../src/modules/routing/services/helpers.ts | 13 + .../W3cJsonLdCredentialService.ts | 12 +- .../W3cJsonLdCredentialService.test.ts | 21 + .../modules/vc/data-integrity/deriveProof.ts | 8 +- .../modules/vc/data-integrity/jsonldUtil.ts | 8 +- .../data-integrity/models/GetProofsOptions.ts | 4 - .../data-integrity/models/GetTypeOptions.ts | 4 - .../CredentialIssuancePurpose.ts | 2 - .../JwsLinkedDataSignature.ts | 7 - .../ed25519/Ed25519Signature2018.ts | 7 - packages/core/src/modules/vc/index.ts | 2 +- .../presentation/W3cJsonPresentation.ts | 2 + .../vc/models/presentation/W3cPresentation.ts | 6 + .../__tests__/__snapshots__/0.1.test.ts.snap | 28 + .../0.1-0.2/__tests__/connection.test.ts | 14 + .../0.2-0.3/__tests__/connection.test.ts | 8 + ...nedUrl.test.ts => parseInvitation.test.ts} | 84 +- packages/core/src/utils/objectEquality.ts | 14 +- packages/core/src/utils/parseInvitation.ts | 52 +- packages/core/tests/helpers.ts | 58 +- packages/core/tests/jsonld.ts | 3 + packages/core/tests/oob.test.ts | 2 +- .../indy-sdk-to-askar-migration/package.json | 6 +- .../services/IndySdkAnonCredsRegistry.ts | 6 +- .../utils/__tests__/identifiers.test.ts | 4 +- .../src/anoncreds/utils/identifiers.ts | 10 +- .../src/dids/IndySdkIndyDidResolver.ts | 2 + .../src/dids/IndySdkSovDidResolver.ts | 2 + packages/indy-vdr/package.json | 6 +- .../src/anoncreds/IndyVdrAnonCredsRegistry.ts | 554 ++++++++-- .../utils/__tests__/identifiers.test.ts | 4 +- .../utils/__tests__/transform.test.ts | 164 +++ .../src/anoncreds/utils/identifiers.ts | 20 +- .../indy-vdr/src/anoncreds/utils/transform.ts | 116 ++- .../src/dids/IndyVdrIndyDidResolver.ts | 2 + .../src/dids/IndyVdrSovDidResolver.ts | 2 + .../indy-vdr/tests/__fixtures__/anoncreds.ts | 11 + .../indy-vdr-anoncreds-registry.e2e.test.ts | 489 ++++----- packages/sd-jwt-vc/package.json | 2 +- packages/tenants/tests/tenants.e2e.test.ts | 8 +- tests/transport/SubjectInboundTransport.ts | 11 +- yarn.lock | 961 +++++++++++++++++- 181 files changed, 7230 insertions(+), 894 deletions(-) create mode 100644 .github/settings.yml create mode 100644 packages/core/src/modules/connections/__tests__/InMemoryDidRegistry.ts create mode 100644 packages/core/src/modules/connections/__tests__/did-rotate.e2e.test.ts create mode 100644 packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts create mode 100644 packages/core/src/modules/connections/handlers/DidRotateAckHandler.ts create mode 100644 packages/core/src/modules/connections/handlers/DidRotateHandler.ts create mode 100644 packages/core/src/modules/connections/handlers/DidRotateProblemReportHandler.ts create mode 100644 packages/core/src/modules/connections/handlers/HangupHandler.ts create mode 100644 packages/core/src/modules/connections/messages/DidRotateAckMessage.ts create mode 100644 packages/core/src/modules/connections/messages/DidRotateMessage.ts create mode 100644 packages/core/src/modules/connections/messages/DidRotateProblemReportMessage.ts create mode 100644 packages/core/src/modules/connections/messages/HangupMessage.ts create mode 100644 packages/core/src/modules/connections/models/DidRotateRole.ts create mode 100644 packages/core/src/modules/connections/services/DidRotateService.ts create mode 100644 packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmUJdJ.json create mode 100644 packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmd8Cp.json create mode 100644 packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo4.test.ts create mode 100644 packages/core/src/modules/dids/methods/peer/peerDidNumAlgo4.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeError.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/index.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/models/index.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/utils/index.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/utils/transform.ts delete mode 100644 packages/core/src/modules/message-pickup/storage/QueuedMessageState.ts create mode 100644 packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts create mode 100644 packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts create mode 100644 packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts create mode 100644 packages/core/src/modules/proofs/formats/dif-presentation-exchange/index.ts create mode 100644 packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts create mode 100644 packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts create mode 100644 packages/core/src/modules/proofs/utils/index.ts create mode 100644 packages/core/src/modules/routing/services/helpers.ts rename packages/core/src/utils/__tests__/{shortenedUrl.test.ts => parseInvitation.test.ts} (69%) create mode 100644 packages/indy-vdr/src/anoncreds/utils/__tests__/transform.test.ts diff --git a/.github/settings.yml b/.github/settings.yml new file mode 100644 index 0000000000..e56ad92080 --- /dev/null +++ b/.github/settings.yml @@ -0,0 +1,18 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +repository: + name: credo-ts + description: Extension libraries for Aries Framework JavaScript + homepage: https://github.com/openwallet-foundation/credo-ts + default_branch: main + has_downloads: false + has_issues: true + has_projects: false + has_wiki: false + archived: false + private: false + allow_squash_merge: true + allow_merge_commit: false + allow_rebase_merge: true diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml index c7e209bfda..1bf351d22a 100644 --- a/.github/workflows/continuous-deployment.yml +++ b/.github/workflows/continuous-deployment.yml @@ -10,11 +10,11 @@ env: jobs: release-canary: - runs-on: aries-ubuntu-2004 + runs-on: ubuntu-20.04 name: Release Canary if: "!startsWith(github.event.head_commit.message, 'chore(release): v')" steps: - - name: Checkout aries-framework-javascript + - name: Checkout credo-ts uses: actions/checkout@v4 with: # pulls all commits (needed for lerna to correctly version) @@ -25,7 +25,7 @@ jobs: uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: 'yarn' @@ -40,8 +40,8 @@ jobs: export NEXT_VERSION_BUMP=$(./node_modules/.bin/ts-node ./scripts/get-next-bump.ts) yarn lerna publish --loglevel=verbose --canary $NEXT_VERSION_BUMP --exact --force-publish --yes --no-verify-access --dist-tag alpha env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_PUBLISH }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH }} - name: Get version number id: get-version @@ -61,7 +61,7 @@ jobs: git push origin v${{ steps.get-version.outputs.version }} --no-verify release-stable: - runs-on: aries-ubuntu-2004 + runs-on: ubuntu-20.04 name: Create Stable Release # Only run if the last pushed commit is a release commit if: "startsWith(github.event.head_commit.message, 'chore(release): v')" @@ -74,7 +74,7 @@ jobs: uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: 'yarn' @@ -109,5 +109,5 @@ jobs: - name: Release to NPM run: yarn lerna publish from-package --loglevel=verbose --yes --no-verify-access env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_PUBLISH }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH }} diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 57edd1bb1d..c057fe5d8b 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -12,7 +12,7 @@ env: TEST_AGENT_PUBLIC_DID_SEED: 000000000000000000000000Trustee9 ENDORSER_AGENT_PUBLIC_DID_SEED: 00000000000000000000000Endorser9 GENESIS_TXN_PATH: network/genesis/local-genesis.txn - LIB_INDY_STRG_POSTGRES: /home/runner/work/aries-framework-javascript/indy-sdk/experimental/plugins/postgres_storage/target/release # for Linux + LIB_INDY_STRG_POSTGRES: /home/runner/work/credo-ts/indy-sdk/experimental/plugins/postgres_storage/target/release # for Linux NODE_OPTIONS: --max_old_space_size=6144 # Make sure we're not running multiple release steps at the same time as this can give issues with determining the next npm version to release. @@ -28,7 +28,7 @@ jobs: # validation scripts. To still be able to run the CI we can manually trigger it by adding the 'ci-test' # label to the pull request ci-trigger: - runs-on: aries-ubuntu-2004 + runs-on: ubuntu-20.04 outputs: triggered: ${{ steps.check.outputs.triggered }} steps: @@ -47,10 +47,10 @@ jobs: echo triggered="${SHOULD_RUN}" >> "$GITHUB_OUTPUT" validate: - runs-on: aries-ubuntu-2004 + runs-on: ubuntu-20.04 name: Validate steps: - - name: Checkout aries-framework-javascript + - name: Checkout credo-ts uses: actions/checkout@v4 # setup dependencies @@ -58,7 +58,7 @@ jobs: uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: 'yarn' @@ -79,7 +79,7 @@ jobs: run: yarn build integration-test: - runs-on: aries-ubuntu-2004 + runs-on: ubuntu-20.04 name: Integration Tests strategy: @@ -111,7 +111,7 @@ jobs: uses: ./.github/actions/setup-postgres-wallet-plugin - name: Setup NodeJS - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'yarn' @@ -126,12 +126,12 @@ jobs: if: always() version-stable: - runs-on: aries-ubuntu-2004 + runs-on: ubuntu-20.04 name: Release stable needs: [integration-test, validate] if: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' steps: - - name: Checkout aries-framework-javascript + - name: Checkout agent-framework-javascript uses: actions/checkout@v4 with: # pulls all commits (needed for lerna to correctly version) @@ -143,7 +143,7 @@ jobs: uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: 'yarn' diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index f02bbae69d..50b29cb22e 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -14,7 +14,7 @@ jobs: steps: # Please look up the latest version from # https://github.com/amannn/action-semantic-pull-request/releases - - uses: amannn/action-semantic-pull-request@v5.3.0 + - uses: amannn/action-semantic-pull-request@v5.4.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/CODEOWNERS b/CODEOWNERS index 3d2299408b..f1b505c931 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,4 +1,4 @@ # SPDX-License-Identifier: Apache-2.0 -# Aries Framework Javascript maintainers -* @hyperledger/aries-framework-javascript-committers +# Agent Framework Javascript maintainers +* @openwallet-foundation/agent-framework-javascript-maintainers diff --git a/MAINTAINERS.md b/MAINTAINERS.md index ff84db7f6e..a5830b63f1 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -4,10 +4,8 @@ | name | Github | Discord | | ------------------ | ---------------------------------------------------------- | ---------------- | -| Berend Sliedrecht | [@blu3beri](https://github.com/blu3beri) | blu3beri#2230 | +| Berend Sliedrecht | [@berendsliedrecht](https://github.com/berendsliedrecht) | blu3beri#2230 | | Jakub Kočí | [@jakubkoci](https://github.com/jakubkoci) | jakubkoci#1481 | -| James Ebert | [@JamesKEbert](https://github.com/JamesKEbert) | JamesEbert#4350 | | Karim Stekelenburg | [@karimStekelenburg](https://github.com/karimStekelenburg) | ssi_karim#3505 | | Timo Glastra | [@TimoGlastra](https://github.com/TimoGlastra) | TimoGlastra#2988 | | Ariel Gentile | [@genaris](https://github.com/genaris) | GenAris#4962 | -| Jan Rietveld | [@janrtvld](https://github.com/janrtvld) | janrtvld#3868 | diff --git a/README.md b/README.md index c7a2d8cccf..2c2d8176f9 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,13 @@ Aries Framework JavaScript is a framework written in TypeScript for building **SSI Agents and DIDComm services** that aims to be **compliant and interoperable** with the standards defined in the [Aries RFCs](https://github.com/hyperledger/aries-rfcs). +> **Note** +> The Aries Framework JavaScript project has recently been moved from the Hyperledger Foundation to the Open Wallet Foundation. +> We are currently in the process of changing the name of the project, and updating all the documentation and links to reflect this change. +> You may encounter some broken links, or references to the old name, but we are working hard to fix this. Once the new name has been decided +> we will update this README and all the documentation to reflect this change. +> You can follow this discussion for updates about the name: https://github.com/openwallet-foundation/agent-framework-javascript/discussions/1668 + ## Features - 🏃 Runs in React Native & Node.JS @@ -186,10 +193,7 @@ Although Aries Framework JavaScript tries to follow the standards as described i If you would like to contribute to the framework, please read the [Framework Developers README](/DEVREADME.md) and the [CONTRIBUTING](/CONTRIBUTING.md) guidelines. These documents will provide more information to get you started! -The Aries Framework JavaScript call takes place every week at Thursday, 6AM Pacific Time. See [World Time Buddy](https://www.worldtimebuddy.com/?qm=1&lid=5,2759794,8&h=5&date=2023-5-19&sln=9-10&hf=1) for the time in your timezone. The meeting is held on [Zoom](https://zoom.us/j/99751084865?pwd=TW1rU0FDVTBqUlhnWnY2NERkd1diZz09). -This meeting is for contributors to groom and plan the backlog, and discuss issues. -Meeting agendas and recordings can be found [here](https://wiki.hyperledger.org/display/ARIES/Framework+JS+Meetings). -Feel free to join! +There are regular community working groups to discuss ongoing efforts within the framework, showcase items you've built with Credo, or ask questions. See [Meeting Information](https://github.com/openwallet-foundation/credo-ts/wiki/Meeting-Information) for up to date information on the meeting schedule. Everyone is welcome to join! ## License diff --git a/demo/package.json b/demo/package.json index 4ffbef505b..4cb9fe150f 100644 --- a/demo/package.json +++ b/demo/package.json @@ -14,9 +14,9 @@ "refresh": "rm -rf ./node_modules ./yarn.lock && yarn" }, "dependencies": { - "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.5", - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.4", - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", + "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.6", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.5", + "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", "inquirer": "^8.2.5" }, "devDependencies": { diff --git a/package.json b/package.json index 3f7c1efbac..df9133150f 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "next-version-bump": "ts-node ./scripts/get-next-bump.ts" }, "devDependencies": { - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", + "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", "@types/cors": "^2.8.10", "@types/eslint": "^8.21.2", "@types/express": "^4.17.13", diff --git a/packages/anoncreds-rs/package.json b/packages/anoncreds-rs/package.json index aaafaf228d..494d899a09 100644 --- a/packages/anoncreds-rs/package.json +++ b/packages/anoncreds-rs/package.json @@ -32,8 +32,8 @@ "tsyringe": "^4.8.0" }, "devDependencies": { - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.4", - "@hyperledger/anoncreds-shared": "^0.2.0-dev.4", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.5", + "@hyperledger/anoncreds-shared": "^0.2.0-dev.5", "@types/ref-array-di": "^1.2.6", "@types/ref-struct-di": "^1.1.10", "reflect-metadata": "^0.1.13", @@ -41,6 +41,6 @@ "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/anoncreds-shared": "^0.2.0-dev.4" + "@hyperledger/anoncreds-shared": "^0.2.0-dev.5" } } diff --git a/packages/anoncreds-rs/tests/LocalDidResolver.ts b/packages/anoncreds-rs/tests/LocalDidResolver.ts index 1f50c1b3aa..c10434f205 100644 --- a/packages/anoncreds-rs/tests/LocalDidResolver.ts +++ b/packages/anoncreds-rs/tests/LocalDidResolver.ts @@ -4,6 +4,7 @@ import { DidsApi } from '@aries-framework/core' export class LocalDidResolver implements DidResolver { public readonly supportedMethods = ['sov', 'indy'] + public readonly allowsCaching = false public async resolve(agentContext: AgentContext, did: string): Promise { const didDocumentMetadata = {} diff --git a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts index 06481055f4..01a71f1ae3 100644 --- a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts +++ b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts @@ -230,7 +230,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean options: {}, }) - if (!revocationStatusListState.revocationStatusList || !revocationStatusListState.timestamp) { + if (!revocationStatusListState.revocationStatusList) { throw new Error('Failed to create revocation status list') } } diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts index fad5355d54..4b8fcd8133 100644 --- a/packages/anoncreds/src/index.ts +++ b/packages/anoncreds/src/index.ts @@ -15,3 +15,4 @@ export { generateLegacyProverDidLikeString } from './utils/proverDid' export * from './utils/indyIdentifiers' export { assertBestPracticeRevocationInterval } from './utils/revocationInterval' export { storeLinkSecret } from './utils/linkSecret' +export { dateToTimestamp } from './utils' diff --git a/packages/anoncreds/src/models/registry.ts b/packages/anoncreds/src/models/registry.ts index bc4ad8a152..883fd859fe 100644 --- a/packages/anoncreds/src/models/registry.ts +++ b/packages/anoncreds/src/models/registry.ts @@ -37,7 +37,7 @@ export interface AnonCredsRevocationRegistryDefinition { export interface AnonCredsRevocationStatusList { issuerId: string revRegDefId: string - revocationList: number[] + revocationList: Array currentAccumulator: string timestamp: number } diff --git a/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts index 3f7a07ed77..fee5653c1e 100644 --- a/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts @@ -4,6 +4,7 @@ import type { AnonCredsOperationStateFinished, AnonCredsResolutionMetadata, Extensible, + AnonCredsOperationStateAction, } from './base' import type { AnonCredsRevocationRegistryDefinition } from '../../models/registry' @@ -19,27 +20,33 @@ export interface RegisterRevocationRegistryDefinitionOptions { options: Extensible } +export interface RegisterRevocationRegistryDefinitionReturnStateAction extends AnonCredsOperationStateAction { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId: string +} + export interface RegisterRevocationRegistryDefinitionReturnStateFailed extends AnonCredsOperationStateFailed { revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition revocationRegistryDefinitionId?: string } +export interface RegisterRevocationRegistryDefinitionReturnStateWait extends AnonCredsOperationStateWait { + revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId?: string +} + export interface RegisterRevocationRegistryDefinitionReturnStateFinished extends AnonCredsOperationStateFinished { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition revocationRegistryDefinitionId: string } -export interface RegisterRevocationRegistryDefinitionReturnState extends AnonCredsOperationStateWait { - revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition - revocationRegistryDefinitionId?: string -} - export interface RegisterRevocationRegistryDefinitionReturn { jobId?: string revocationRegistryDefinitionState: + | RegisterRevocationRegistryDefinitionReturnStateWait + | RegisterRevocationRegistryDefinitionReturnStateAction | RegisterRevocationRegistryDefinitionReturnStateFailed | RegisterRevocationRegistryDefinitionReturnStateFinished - | RegisterRevocationRegistryDefinitionReturnState revocationRegistryDefinitionMetadata: Extensible registrationMetadata: Extensible } diff --git a/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts index 05b1353801..21ea1bca95 100644 --- a/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts @@ -4,8 +4,10 @@ import type { AnonCredsOperationStateFinished, AnonCredsResolutionMetadata, Extensible, + AnonCredsOperationStateAction, } from './base' import type { AnonCredsRevocationStatusList } from '../../models/registry' +import type { Optional } from '@aries-framework/core' export interface GetRevocationStatusListReturn { revocationStatusList?: AnonCredsRevocationStatusList @@ -13,34 +15,39 @@ export interface GetRevocationStatusListReturn { revocationStatusListMetadata: Extensible } +// Timestamp is often calculated by the ledger, otherwise method should just take current time +// Return type does include the timestamp. +export type AnonCredsRevocationStatusListWithoutTimestamp = Omit +export type AnonCredsRevocationStatusListWithOptionalTimestamp = Optional + export interface RegisterRevocationStatusListOptions { - // Timestamp is often calculated by the ledger, otherwise method should just take current time - // Return type does include the timestamp. - revocationStatusList: Omit + revocationStatusList: AnonCredsRevocationStatusListWithoutTimestamp options: Extensible } +export interface RegisterRevocationStatusListReturnStateAction extends AnonCredsOperationStateAction { + revocationStatusList: AnonCredsRevocationStatusListWithOptionalTimestamp +} + export interface RegisterRevocationStatusListReturnStateFailed extends AnonCredsOperationStateFailed { - revocationStatusList?: AnonCredsRevocationStatusList - timestamp?: string + revocationStatusList?: AnonCredsRevocationStatusListWithOptionalTimestamp } -export interface RegisterRevocationStatusListReturnStateFinished extends AnonCredsOperationStateFinished { - revocationStatusList: AnonCredsRevocationStatusList - timestamp: string +export interface RegisterRevocationStatusListReturnStateWait extends AnonCredsOperationStateWait { + revocationStatusList?: AnonCredsRevocationStatusListWithOptionalTimestamp } -export interface RegisterRevocationStatusListReturnState extends AnonCredsOperationStateWait { - revocationStatusList?: AnonCredsRevocationStatusList - timestamp?: string +export interface RegisterRevocationStatusListReturnStateFinished extends AnonCredsOperationStateFinished { + revocationStatusList: AnonCredsRevocationStatusList } export interface RegisterRevocationStatusListReturn { jobId?: string revocationStatusListState: + | RegisterRevocationStatusListReturnStateWait + | RegisterRevocationStatusListReturnStateAction | RegisterRevocationStatusListReturnStateFailed | RegisterRevocationStatusListReturnStateFinished - | RegisterRevocationStatusListReturnState revocationStatusListMetadata: Extensible registrationMetadata: Extensible } diff --git a/packages/anoncreds/src/utils/__tests__/indyIdentifiers.test.ts b/packages/anoncreds/src/utils/__tests__/indyIdentifiers.test.ts index 7fba68562d..7f94486586 100644 --- a/packages/anoncreds/src/utils/__tests__/indyIdentifiers.test.ts +++ b/packages/anoncreds/src/utils/__tests__/indyIdentifiers.test.ts @@ -1,6 +1,6 @@ import { getUnqualifiedCredentialDefinitionId, - getUnqualifiedRevocationRegistryId, + getUnqualifiedRevocationRegistryDefinitionId, getUnqualifiedSchemaId, parseIndyCredentialDefinitionId, parseIndyRevocationRegistryId, @@ -67,7 +67,7 @@ describe('Legacy Indy Identifier Regex', () => { const credentialDefinitionTag = 'someTag' const tag = 'anotherTag' - expect(getUnqualifiedRevocationRegistryId(did, seqNo, credentialDefinitionTag, tag)).toEqual( + expect(getUnqualifiedRevocationRegistryDefinitionId(did, seqNo, credentialDefinitionTag, tag)).toEqual( '12345:4:12345:3:CL:420:someTag:CL_ACCUM:anotherTag' ) }) diff --git a/packages/anoncreds/src/utils/indyIdentifiers.ts b/packages/anoncreds/src/utils/indyIdentifiers.ts index 1e20f75c55..3cd050e7c8 100644 --- a/packages/anoncreds/src/utils/indyIdentifiers.ts +++ b/packages/anoncreds/src/utils/indyIdentifiers.ts @@ -18,7 +18,7 @@ export const didIndyCredentialDefinitionIdRegex = new RegExp( `^${didIndyAnonCredsBase.source}/CLAIM_DEF/([1-9][0-9]*)/(.+)$` ) -// :4::3:CL::CL_ACCUM: +// :4::3:CL:::CL_ACCUM: export const unqualifiedRevocationRegistryIdRegex = /^([a-zA-Z0-9]{21,22}):4:[a-zA-Z0-9]{21,22}:3:CL:([1-9][0-9]*):(.+):CL_ACCUM:(.+)$/ // did:indy::/anoncreds/v0/REV_REG_DEF/// @@ -32,18 +32,22 @@ export function getUnqualifiedSchemaId(unqualifiedDid: string, name: string, ver return `${unqualifiedDid}:2:${name}:${version}` } -export function getUnqualifiedCredentialDefinitionId(unqualifiedDid: string, seqNo: string | number, tag: string) { - return `${unqualifiedDid}:3:CL:${seqNo}:${tag}` +export function getUnqualifiedCredentialDefinitionId( + unqualifiedDid: string, + schemaSeqNo: string | number, + tag: string +) { + return `${unqualifiedDid}:3:CL:${schemaSeqNo}:${tag}` } // TZQuLp43UcYTdtc3HewcDz:4:TZQuLp43UcYTdtc3HewcDz:3:CL:98158:BaustellenzertifikateNU1:CL_ACCUM:1-100 -export function getUnqualifiedRevocationRegistryId( +export function getUnqualifiedRevocationRegistryDefinitionId( unqualifiedDid: string, - seqNo: string | number, + schemaSeqNo: string | number, credentialDefinitionTag: string, revocationRegistryTag: string ) { - return `${unqualifiedDid}:4:${unqualifiedDid}:3:CL:${seqNo}:${credentialDefinitionTag}:CL_ACCUM:${revocationRegistryTag}` + return `${unqualifiedDid}:4:${unqualifiedDid}:3:CL:${schemaSeqNo}:${credentialDefinitionTag}:CL_ACCUM:${revocationRegistryTag}` } export function isUnqualifiedCredentialDefinitionId(credentialDefinitionId: string) { diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index da51aa12ec..9c2e8bc42b 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import type { AnonCredsRegistry, GetSchemaReturn, @@ -25,12 +24,12 @@ import BigNumber from 'bn.js' import { getDidIndyCredentialDefinitionId, - getDidIndyRevocationRegistryId, + getDidIndyRevocationRegistryDefinitionId, getDidIndySchemaId, } from '../../indy-sdk/src/anoncreds/utils/identifiers' import { parseIndyCredentialDefinitionId, - getUnqualifiedRevocationRegistryId, + getUnqualifiedRevocationRegistryDefinitionId, getUnqualifiedCredentialDefinitionId, getUnqualifiedSchemaId, parseIndyDid, @@ -71,7 +70,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { this.revocationStatusLists = existingRevocationStatusLists } - public async getSchema(agentContext: AgentContext, schemaId: string): Promise { + public async getSchema(_agentContext: AgentContext, schemaId: string): Promise { const schema = this.schemas[schemaId] const parsed = parseIndySchemaId(schemaId) @@ -103,7 +102,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } public async registerSchema( - agentContext: AgentContext, + _agentContext: AgentContext, options: RegisterSchemaOptions ): Promise { const { namespace, namespaceIdentifier } = parseIndyDid(options.schema.issuerId) @@ -140,7 +139,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } public async getCredentialDefinition( - agentContext: AgentContext, + _agentContext: AgentContext, credentialDefinitionId: string ): Promise { const credentialDefinition = this.credentialDefinitions[credentialDefinitionId] @@ -165,7 +164,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } public async registerCredentialDefinition( - agentContext: AgentContext, + _agentContext: AgentContext, options: RegisterCredentialDefinitionOptions ): Promise { const parsedSchema = parseIndySchemaId(options.credentialDefinition.schemaId) @@ -211,7 +210,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } public async getRevocationRegistryDefinition( - agentContext: AgentContext, + _agentContext: AgentContext, revocationRegistryDefinitionId: string ): Promise { const revocationRegistryDefinition = this.revocationRegistryDefinitions[revocationRegistryDefinitionId] @@ -236,7 +235,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } public async registerRevocationRegistryDefinition( - agentContext: AgentContext, + _agentContext: AgentContext, options: RegisterRevocationRegistryDefinitionOptions ): Promise { const parsedCredentialDefinition = parseIndyCredentialDefinitionId(options.revocationRegistryDefinition.credDefId) @@ -249,7 +248,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { const { namespace, namespaceIdentifier } = parseIndyDid(options.revocationRegistryDefinition.issuerId) const legacyIssuerId = namespaceIdentifier - const didIndyRevocationRegistryDefinitionId = getDidIndyRevocationRegistryId( + const didIndyRevocationRegistryDefinitionId = getDidIndyRevocationRegistryDefinitionId( namespace, namespaceIdentifier, indyLedgerSeqNo, @@ -259,7 +258,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { this.revocationRegistryDefinitions[didIndyRevocationRegistryDefinitionId] = options.revocationRegistryDefinition - const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryId( + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( legacyIssuerId, indyLedgerSeqNo, parsedCredentialDefinition.tag, @@ -284,7 +283,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } public async getRevocationStatusList( - agentContext: AgentContext, + _agentContext: AgentContext, revocationRegistryId: string, timestamp: number ): Promise { @@ -322,7 +321,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } public async registerRevocationStatusList( - agentContext: AgentContext, + _agentContext: AgentContext, options: RegisterRevocationStatusListOptions ): Promise { const timestamp = (options.options.timestamp as number) ?? dateToTimestamp(new Date()) @@ -341,7 +340,6 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { revocationStatusListState: { state: 'finished', revocationStatusList, - timestamp: timestamp.toString(), }, } } diff --git a/packages/askar/package.json b/packages/askar/package.json index 6a88bf3449..dbce742314 100644 --- a/packages/askar/package.json +++ b/packages/askar/package.json @@ -32,8 +32,8 @@ "tsyringe": "^4.8.0" }, "devDependencies": { - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", - "@hyperledger/aries-askar-shared": "^0.2.0-dev.1", + "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", + "@hyperledger/aries-askar-shared": "^0.2.0-dev.5", "@types/bn.js": "^5.1.0", "@types/ref-array-di": "^1.2.6", "@types/ref-struct-di": "^1.1.10", @@ -42,6 +42,6 @@ "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/aries-askar-shared": "^0.2.0-dev.1" + "@hyperledger/aries-askar-shared": "^0.2.0-dev.5" } } diff --git a/packages/bbs-signatures/src/signature-suites/BbsBlsSignature2020.ts b/packages/bbs-signatures/src/signature-suites/BbsBlsSignature2020.ts index f1a4239007..5b517a1a8e 100644 --- a/packages/bbs-signatures/src/signature-suites/BbsBlsSignature2020.ts +++ b/packages/bbs-signatures/src/signature-suites/BbsBlsSignature2020.ts @@ -113,7 +113,7 @@ export class BbsBlsSignature2020 extends LinkedDataProof { * @returns {Promise} Resolves with the created proof object. */ public async createProof(options: CreateProofOptions): Promise> { - const { document, purpose, documentLoader, expansionMap, compactProof } = options + const { document, purpose, documentLoader, compactProof } = options let proof: JsonObject @@ -121,7 +121,6 @@ export class BbsBlsSignature2020 extends LinkedDataProof { if (this.proof) { proof = await jsonld.compact(this.proof, SECURITY_CONTEXT_URL, { documentLoader, - expansionMap, compactToRelative: true, }) } else { @@ -159,7 +158,6 @@ export class BbsBlsSignature2020 extends LinkedDataProof { document, suite: this, documentLoader, - expansionMap, }) // create data to sign @@ -168,7 +166,7 @@ export class BbsBlsSignature2020 extends LinkedDataProof { document, proof, documentLoader, - expansionMap, + compactProof, }) ).map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) @@ -179,7 +177,6 @@ export class BbsBlsSignature2020 extends LinkedDataProof { document, proof, documentLoader, - expansionMap, }) delete proof['@context'] @@ -192,7 +189,7 @@ export class BbsBlsSignature2020 extends LinkedDataProof { * @returns {Promise<{object}>} Resolves with the verification result. */ public async verifyProof(options: VerifyProofOptions): Promise> { - const { proof, document, documentLoader, expansionMap, purpose } = options + const { proof, document, documentLoader, purpose } = options try { // create data to verify @@ -201,7 +198,6 @@ export class BbsBlsSignature2020 extends LinkedDataProof { document, proof, documentLoader, - expansionMap, compactProof: false, }) ).map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) @@ -219,7 +215,6 @@ export class BbsBlsSignature2020 extends LinkedDataProof { document, proof, documentLoader, - expansionMap, }) if (!verified) { throw new Error('Invalid signature.') @@ -231,7 +226,6 @@ export class BbsBlsSignature2020 extends LinkedDataProof { suite: this, verificationMethod, documentLoader, - expansionMap, }) if (!valid) { throw error @@ -244,24 +238,22 @@ export class BbsBlsSignature2020 extends LinkedDataProof { } public async canonize(input: Record, options: CanonizeOptions): Promise { - const { documentLoader, expansionMap, skipExpansion } = options + const { documentLoader, skipExpansion } = options return jsonld.canonize(input, { algorithm: 'URDNA2015', format: 'application/n-quads', documentLoader, - expansionMap, skipExpansion, useNative: this.useNativeCanonize, }) } public async canonizeProof(proof: Record, options: CanonizeOptions): Promise { - const { documentLoader, expansionMap } = options + const { documentLoader } = options proof = { ...proof } delete proof[this.proofSignatureKey] return this.canonize(proof, { documentLoader, - expansionMap, skipExpansion: false, }) } @@ -272,17 +264,15 @@ export class BbsBlsSignature2020 extends LinkedDataProof { * @returns {Promise<{string[]>}. */ public async createVerifyData(options: CreateVerifyDataOptions): Promise { - const { proof, document, documentLoader, expansionMap } = options + const { proof, document, documentLoader } = options const proof2 = { ...proof, '@context': document['@context'] } const proofStatements = await this.createVerifyProofData(proof2, { documentLoader, - expansionMap, }) const documentStatements = await this.createVerifyDocumentData(document, { documentLoader, - expansionMap, }) // concatenate c14n proof options and c14n document @@ -297,11 +287,10 @@ export class BbsBlsSignature2020 extends LinkedDataProof { */ public async createVerifyProofData( proof: Record, - { documentLoader, expansionMap }: { documentLoader?: DocumentLoader; expansionMap?: () => void } + { documentLoader }: { documentLoader?: DocumentLoader } ): Promise { const c14nProofOptions = await this.canonizeProof(proof, { documentLoader, - expansionMap, }) return c14nProofOptions.split('\n').filter((_) => _.length > 0) @@ -315,11 +304,10 @@ export class BbsBlsSignature2020 extends LinkedDataProof { */ public async createVerifyDocumentData( document: Record, - { documentLoader, expansionMap }: { documentLoader?: DocumentLoader; expansionMap?: () => void } + { documentLoader }: { documentLoader?: DocumentLoader } ): Promise { const c14nDocument = await this.canonize(document, { documentLoader, - expansionMap, }) return c14nDocument.split('\n').filter((_) => _.length > 0) @@ -329,7 +317,6 @@ export class BbsBlsSignature2020 extends LinkedDataProof { * @param document {object} to be signed. * @param proof {object} * @param documentLoader {function} - * @param expansionMap {function} */ public async getVerificationMethod({ proof, diff --git a/packages/bbs-signatures/src/signature-suites/BbsBlsSignatureProof2020.ts b/packages/bbs-signatures/src/signature-suites/BbsBlsSignatureProof2020.ts index 59d0b37eb3..2d902c8591 100644 --- a/packages/bbs-signatures/src/signature-suites/BbsBlsSignatureProof2020.ts +++ b/packages/bbs-signatures/src/signature-suites/BbsBlsSignatureProof2020.ts @@ -62,7 +62,7 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { * @returns {Promise} Resolves with the derived proof object. */ public async deriveProof(options: DeriveProofOptions): Promise> { - const { document, proof, revealDocument, documentLoader, expansionMap } = options + const { document, proof, revealDocument, documentLoader } = options let { nonce } = options const proofType = proof.type @@ -98,7 +98,6 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { // use proof JSON-LD document passed to API derivedProof = await jsonld.compact(this.proof, SECURITY_CONTEXT_URL, { documentLoader, - expansionMap, compactToRelative: false, }) } else { @@ -112,13 +111,11 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { // Get the input document statements const documentStatements = await suite.createVerifyDocumentData(document, { documentLoader, - expansionMap, }) // Get the proof statements const proofStatements = await suite.createVerifyProofData(proof, { documentLoader, - expansionMap, }) // Transform any blank node identifiers for the input @@ -137,7 +134,6 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { // Canonicalize the resulting reveal document const revealDocumentStatements = await suite.createVerifyDocumentData(revealDocumentResult, { documentLoader, - expansionMap, }) //Get the indicies of the revealed statements from the transformed input document offset @@ -216,7 +212,7 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { * @returns {Promise<{object}>} Resolves with the verification result. */ public async verifyProof(options: VerifyProofOptions): Promise { - const { document, documentLoader, expansionMap, purpose } = options + const { document, documentLoader, purpose } = options const { proof } = options try { @@ -227,13 +223,11 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { // Get the proof statements const proofStatements = await this.createVerifyProofData(proofIncludingDocumentContext, { documentLoader, - expansionMap, }) // Get the document statements const documentStatements = await this.createVerifyProofData(document, { documentLoader, - expansionMap, }) // Transform the blank node identifier placeholders for the document statements @@ -278,7 +272,6 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { suite: this, verificationMethod, documentLoader, - expansionMap, }) if (!valid) { throw error @@ -291,19 +284,18 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { } public async canonize(input: JsonObject, options: CanonizeOptions): Promise { - const { documentLoader, expansionMap, skipExpansion } = options + const { documentLoader, skipExpansion } = options return jsonld.canonize(input, { algorithm: 'URDNA2015', format: 'application/n-quads', documentLoader, - expansionMap, skipExpansion, useNative: this.useNativeCanonize, }) } public async canonizeProof(proof: JsonObject, options: CanonizeOptions): Promise { - const { documentLoader, expansionMap } = options + const { documentLoader } = options proof = { ...proof } delete proof.nonce @@ -311,7 +303,6 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { return this.canonize(proof, { documentLoader, - expansionMap, skipExpansion: false, }) } @@ -322,15 +313,13 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { * @returns {Promise<{string[]>}. */ public async createVerifyData(options: CreateVerifyDataOptions): Promise { - const { proof, document, documentLoader, expansionMap } = options + const { proof, document, documentLoader } = options const proofStatements = await this.createVerifyProofData(proof, { documentLoader, - expansionMap, }) const documentStatements = await this.createVerifyDocumentData(document, { documentLoader, - expansionMap, }) // concatenate c14n proof options and c14n document @@ -345,11 +334,10 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { */ public async createVerifyProofData( proof: JsonObject, - { documentLoader, expansionMap }: { documentLoader?: DocumentLoader; expansionMap?: () => void } + { documentLoader }: { documentLoader?: DocumentLoader } ): Promise { const c14nProofOptions = await this.canonizeProof(proof, { documentLoader, - expansionMap, }) return c14nProofOptions.split('\n').filter((_) => _.length > 0) @@ -363,11 +351,10 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { */ public async createVerifyDocumentData( document: JsonObject, - { documentLoader, expansionMap }: { documentLoader?: DocumentLoader; expansionMap?: () => void } + { documentLoader }: { documentLoader?: DocumentLoader } ): Promise { const c14nDocument = await this.canonize(document, { documentLoader, - expansionMap, }) return c14nDocument.split('\n').filter((_) => _.length > 0) diff --git a/packages/bbs-signatures/src/types/CanonizeOptions.ts b/packages/bbs-signatures/src/types/CanonizeOptions.ts index e2a4af60c8..73a4217e8c 100644 --- a/packages/bbs-signatures/src/types/CanonizeOptions.ts +++ b/packages/bbs-signatures/src/types/CanonizeOptions.ts @@ -21,11 +21,7 @@ export interface CanonizeOptions { * Optional custom document loader */ documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - // eslint-disable-next-line - expansionMap?: () => void + /** * Indicates whether to skip expansion during canonization */ diff --git a/packages/bbs-signatures/src/types/CreateProofOptions.ts b/packages/bbs-signatures/src/types/CreateProofOptions.ts index d4acbbe0ba..e413649ced 100644 --- a/packages/bbs-signatures/src/types/CreateProofOptions.ts +++ b/packages/bbs-signatures/src/types/CreateProofOptions.ts @@ -29,10 +29,6 @@ export interface CreateProofOptions { * Optional custom document loader */ documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - expansionMap?: () => void /** * Indicates whether to compact the resulting proof */ diff --git a/packages/bbs-signatures/src/types/CreateVerifyDataOptions.ts b/packages/bbs-signatures/src/types/CreateVerifyDataOptions.ts index c163eca5c6..7aff485105 100644 --- a/packages/bbs-signatures/src/types/CreateVerifyDataOptions.ts +++ b/packages/bbs-signatures/src/types/CreateVerifyDataOptions.ts @@ -21,20 +21,17 @@ export interface CreateVerifyDataOptions { * Document to create the proof for */ readonly document: JsonObject + /** * The proof */ readonly proof: JsonObject + /** * Optional custom document loader */ - documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - expansionMap?: () => void /** * Indicates whether to compact the proof */ diff --git a/packages/bbs-signatures/src/types/DeriveProofOptions.ts b/packages/bbs-signatures/src/types/DeriveProofOptions.ts index 23fe427798..db62925292 100644 --- a/packages/bbs-signatures/src/types/DeriveProofOptions.ts +++ b/packages/bbs-signatures/src/types/DeriveProofOptions.ts @@ -34,11 +34,7 @@ export interface DeriveProofOptions { */ // eslint-disable-next-line documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - // eslint-disable-next-line - expansionMap?: () => void + /** * Nonce to include in the derived proof */ diff --git a/packages/bbs-signatures/src/types/SuiteSignOptions.ts b/packages/bbs-signatures/src/types/SuiteSignOptions.ts index 850587dc60..44420e7221 100644 --- a/packages/bbs-signatures/src/types/SuiteSignOptions.ts +++ b/packages/bbs-signatures/src/types/SuiteSignOptions.ts @@ -25,10 +25,7 @@ export interface SuiteSignOptions { * Optional custom document loader */ documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - expansionMap?: () => void + /** * The array of statements to sign */ diff --git a/packages/bbs-signatures/src/types/VerifyProofOptions.ts b/packages/bbs-signatures/src/types/VerifyProofOptions.ts index 9aa2a60ff4..decfc7a47a 100644 --- a/packages/bbs-signatures/src/types/VerifyProofOptions.ts +++ b/packages/bbs-signatures/src/types/VerifyProofOptions.ts @@ -33,8 +33,4 @@ export interface VerifyProofOptions { * Optional custom document loader */ documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - expansionMap?: () => void } diff --git a/packages/bbs-signatures/src/types/VerifySignatureOptions.ts b/packages/bbs-signatures/src/types/VerifySignatureOptions.ts index 07ea80c5b8..03a0ddfb80 100644 --- a/packages/bbs-signatures/src/types/VerifySignatureOptions.ts +++ b/packages/bbs-signatures/src/types/VerifySignatureOptions.ts @@ -37,8 +37,4 @@ export interface VerifySignatureOptions { * Optional custom document loader */ documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - expansionMap?: () => void } diff --git a/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts b/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts index 6aa8db303f..c5c20bf90c 100644 --- a/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts +++ b/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts @@ -31,7 +31,7 @@ import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' import { BbsBlsSignature2020, BbsBlsSignatureProof2020, Bls12381g2SigningProvider } from '../src' import { BbsBlsSignature2020Fixtures } from './fixtures' -import { describeSkipNode17And18 } from './util' +import { describeSkipNode18 } from './util' const { jsonldSignatures } = vcLibraries const { purposes } = jsonldSignatures @@ -61,7 +61,7 @@ const signingProviderRegistry = new SigningProviderRegistry([new Bls12381g2Signi const agentConfig = getAgentConfig('BbsSignaturesE2eTest') -describeSkipNode17And18('BBS W3cCredentialService', () => { +describeSkipNode18('BBS W3cCredentialService', () => { let wallet: Wallet let agentContext: AgentContext let w3cJsonLdCredentialService: W3cJsonLdCredentialService diff --git a/packages/bbs-signatures/tests/bbs-signing-provider.e2e.test.ts b/packages/bbs-signatures/tests/bbs-signing-provider.e2e.test.ts index 67e2112e96..a9b48d6352 100644 --- a/packages/bbs-signatures/tests/bbs-signing-provider.e2e.test.ts +++ b/packages/bbs-signatures/tests/bbs-signing-provider.e2e.test.ts @@ -14,7 +14,7 @@ import { IndySdkWallet } from '../../indy-sdk/src' import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' import { Bls12381g2SigningProvider } from '../src' -import { describeSkipNode17And18 } from './util' +import { describeSkipNode18 } from './util' // use raw key derivation method to speed up wallet creating / opening / closing between tests const walletConfig: WalletConfig = { @@ -24,7 +24,7 @@ const walletConfig: WalletConfig = { keyDerivationMethod: KeyDerivationMethod.Raw, } -describeSkipNode17And18('BBS Signing Provider', () => { +describeSkipNode18('BBS Signing Provider', () => { let wallet: Wallet const seed = TypedArrayEncoder.fromString('sample-seed-min-of-32-bytes-long') const message = TypedArrayEncoder.fromString('sample-message') diff --git a/packages/bbs-signatures/tests/util.ts b/packages/bbs-signatures/tests/util.ts index 208a6ce8ac..efe9f799bd 100644 --- a/packages/bbs-signatures/tests/util.ts +++ b/packages/bbs-signatures/tests/util.ts @@ -1,7 +1,7 @@ -export function describeSkipNode17And18(...parameters: Parameters) { +export function describeSkipNode18(...parameters: Parameters) { const version = process.version - if (version.startsWith('v17.') || version.startsWith('v18.')) { + if (version.startsWith('v18.')) { describe.skip(...parameters) } else { describe(...parameters) diff --git a/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts b/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts index 78d06d862a..feaead2f3e 100644 --- a/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts +++ b/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts @@ -9,7 +9,7 @@ import { CREDENTIALS_CONTEXT_V1_URL, SECURITY_CONTEXT_BBS_URL } from '../../core import { JsonTransformer } from '../../core/src/utils/JsonTransformer' import { waitForCredentialRecordSubject, setupJsonLdTests, testLogger } from '../../core/tests' -import { describeSkipNode17And18 } from './util' +import { describeSkipNode18 } from './util' let faberAgent: JsonLdTestsAgent let faberReplay: EventReplaySubject @@ -52,7 +52,7 @@ const signCredentialOptions = { }, } -describeSkipNode17And18('credentials, BBS+ signature', () => { +describeSkipNode18('credentials, BBS+ signature', () => { beforeAll(async () => { ;({ issuerAgent: faberAgent, diff --git a/packages/cheqd/src/dids/CheqdDidResolver.ts b/packages/cheqd/src/dids/CheqdDidResolver.ts index edd94c2aa1..0b87fe9061 100644 --- a/packages/cheqd/src/dids/CheqdDidResolver.ts +++ b/packages/cheqd/src/dids/CheqdDidResolver.ts @@ -19,6 +19,7 @@ import { filterResourcesByNameAndType, getClosestResourceVersion, renderResource export class CheqdDidResolver implements DidResolver { public readonly supportedMethods = ['cheqd'] + public readonly allowsCaching = true public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise { const didDocumentMetadata = {} diff --git a/packages/core/package.json b/packages/core/package.json index 363143ddcd..02c30fec80 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,13 +23,16 @@ "prepublishOnly": "yarn run build" }, "dependencies": { - "@digitalcredentials/jsonld": "^5.2.1", - "@digitalcredentials/jsonld-signatures": "^9.3.1", - "@digitalcredentials/vc": "^1.1.2", + "@digitalcredentials/jsonld": "^6.0.0", + "@digitalcredentials/jsonld-signatures": "^9.4.0", + "@digitalcredentials/vc": "^6.0.1", "@multiformats/base-x": "^4.0.1", "@stablelib/ed25519": "^1.0.2", "@stablelib/random": "^1.0.1", "@stablelib/sha256": "^1.0.1", + "@sphereon/pex": "^2.2.2", + "@sphereon/pex-models": "^2.1.2", + "@sphereon/ssi-types": "^0.17.5", "@types/ws": "^8.5.4", "abort-controller": "^3.0.0", "big-integer": "^1.6.51", @@ -38,6 +41,7 @@ "class-transformer": "0.5.1", "class-validator": "0.14.0", "did-resolver": "^4.1.0", + "jsonpath": "^1.1.1", "lru_map": "^0.4.1", "luxon": "^3.3.0", "make-error": "^1.3.6", @@ -52,6 +56,7 @@ }, "devDependencies": { "@types/events": "^3.0.0", + "@types/jsonpath": "^0.2.4", "@types/luxon": "^3.2.0", "@types/object-inspect": "^1.8.0", "@types/uuid": "^9.0.1", diff --git a/packages/core/src/agent/AgentModules.ts b/packages/core/src/agent/AgentModules.ts index ed0afcede9..faf87ecec7 100644 --- a/packages/core/src/agent/AgentModules.ts +++ b/packages/core/src/agent/AgentModules.ts @@ -7,6 +7,7 @@ import { CacheModule } from '../modules/cache' import { ConnectionsModule } from '../modules/connections' import { CredentialsModule } from '../modules/credentials' import { DidsModule } from '../modules/dids' +import { DifPresentationExchangeModule } from '../modules/dif-presentation-exchange' import { DiscoverFeaturesModule } from '../modules/discover-features' import { GenericRecordsModule } from '../modules/generic-records' import { MessagePickupModule } from '../modules/message-pickup' @@ -131,6 +132,7 @@ function getDefaultAgentModules() { oob: () => new OutOfBandModule(), w3cCredentials: () => new W3cCredentialsModule(), cache: () => new CacheModule(), + pex: () => new DifPresentationExchangeModule(), } as const } diff --git a/packages/core/src/agent/TransportService.ts b/packages/core/src/agent/TransportService.ts index cf281b77b9..bd42f892a5 100644 --- a/packages/core/src/agent/TransportService.ts +++ b/packages/core/src/agent/TransportService.ts @@ -27,7 +27,7 @@ export class TransportService { if (session.connectionId) { const oldSessions = this.getExistingSessionsForConnectionIdAndType(session.connectionId, session.type) oldSessions.forEach((oldSession) => { - if (oldSession) { + if (oldSession && oldSession.id !== session.id) { this.removeSession(oldSession) } }) diff --git a/packages/core/src/agent/__tests__/Agent.test.ts b/packages/core/src/agent/__tests__/Agent.test.ts index d00e94e845..22268ccb93 100644 --- a/packages/core/src/agent/__tests__/Agent.test.ts +++ b/packages/core/src/agent/__tests__/Agent.test.ts @@ -7,6 +7,7 @@ import { getAgentOptions } from '../../../tests/helpers' import { InjectionSymbols } from '../../constants' import { BasicMessageRepository, BasicMessageService } from '../../modules/basic-messages' import { BasicMessagesApi } from '../../modules/basic-messages/BasicMessagesApi' +import { DidRotateService } from '../../modules/connections' import { ConnectionsApi } from '../../modules/connections/ConnectionsApi' import { ConnectionRepository } from '../../modules/connections/repository/ConnectionRepository' import { ConnectionService } from '../../modules/connections/services/ConnectionService' @@ -157,6 +158,7 @@ describe('Agent', () => { expect(container.resolve(ConnectionsApi)).toBeInstanceOf(ConnectionsApi) expect(container.resolve(ConnectionService)).toBeInstanceOf(ConnectionService) expect(container.resolve(ConnectionRepository)).toBeInstanceOf(ConnectionRepository) + expect(container.resolve(DidRotateService)).toBeInstanceOf(DidRotateService) expect(container.resolve(TrustPingService)).toBeInstanceOf(TrustPingService) expect(container.resolve(ProofsApi)).toBeInstanceOf(ProofsApi) @@ -198,6 +200,7 @@ describe('Agent', () => { expect(container.resolve(ConnectionService)).toBe(container.resolve(ConnectionService)) expect(container.resolve(ConnectionRepository)).toBe(container.resolve(ConnectionRepository)) expect(container.resolve(TrustPingService)).toBe(container.resolve(TrustPingService)) + expect(container.resolve(DidRotateService)).toBe(container.resolve(DidRotateService)) expect(container.resolve(ProofsApi)).toBe(container.resolve(ProofsApi)) expect(container.resolve(ProofRepository)).toBe(container.resolve(ProofRepository)) @@ -247,7 +250,8 @@ describe('Agent', () => { 'https://didcomm.org/coordinate-mediation/1.0', 'https://didcomm.org/issue-credential/2.0', 'https://didcomm.org/present-proof/2.0', - 'https://didcomm.org/didexchange/1.0', + 'https://didcomm.org/didexchange/1.1', + 'https://didcomm.org/did-rotate/1.0', 'https://didcomm.org/discover-features/1.0', 'https://didcomm.org/discover-features/2.0', 'https://didcomm.org/messagepickup/1.0', @@ -257,6 +261,6 @@ describe('Agent', () => { 'https://didcomm.org/revocation_notification/2.0', ]) ) - expect(protocols.length).toEqual(13) + expect(protocols.length).toEqual(14) }) }) diff --git a/packages/core/src/agent/__tests__/AgentModules.test.ts b/packages/core/src/agent/__tests__/AgentModules.test.ts index 7717608581..a4c3be88a3 100644 --- a/packages/core/src/agent/__tests__/AgentModules.test.ts +++ b/packages/core/src/agent/__tests__/AgentModules.test.ts @@ -5,6 +5,7 @@ import { CacheModule } from '../../modules/cache' import { ConnectionsModule } from '../../modules/connections' import { CredentialsModule } from '../../modules/credentials' import { DidsModule } from '../../modules/dids' +import { DifPresentationExchangeModule } from '../../modules/dif-presentation-exchange' import { DiscoverFeaturesModule } from '../../modules/discover-features' import { GenericRecordsModule } from '../../modules/generic-records' import { MessagePickupModule } from '../../modules/message-pickup' @@ -62,6 +63,7 @@ describe('AgentModules', () => { mediationRecipient: expect.any(MediationRecipientModule), messagePickup: expect.any(MessagePickupModule), basicMessages: expect.any(BasicMessagesModule), + pex: expect.any(DifPresentationExchangeModule), genericRecords: expect.any(GenericRecordsModule), discovery: expect.any(DiscoverFeaturesModule), dids: expect.any(DidsModule), @@ -86,6 +88,7 @@ describe('AgentModules', () => { mediationRecipient: expect.any(MediationRecipientModule), messagePickup: expect.any(MessagePickupModule), basicMessages: expect.any(BasicMessagesModule), + pex: expect.any(DifPresentationExchangeModule), genericRecords: expect.any(GenericRecordsModule), discovery: expect.any(DiscoverFeaturesModule), dids: expect.any(DidsModule), @@ -113,6 +116,7 @@ describe('AgentModules', () => { mediationRecipient: expect.any(MediationRecipientModule), messagePickup: expect.any(MessagePickupModule), basicMessages: expect.any(BasicMessagesModule), + pex: expect.any(DifPresentationExchangeModule), genericRecords: expect.any(GenericRecordsModule), discovery: expect.any(DiscoverFeaturesModule), dids: expect.any(DidsModule), diff --git a/packages/core/src/crypto/Key.ts b/packages/core/src/crypto/Key.ts index 2ebe3651f2..ec44eead24 100644 --- a/packages/core/src/crypto/Key.ts +++ b/packages/core/src/crypto/Key.ts @@ -59,4 +59,13 @@ export class Key { public get supportsSigning() { return isSigningSupportedForKeyType(this.keyType) } + + // We return an object structure based on the key, so that when this object is + // serialized to JSON it will be nicely formatted instead of the bytes printed + private toJSON() { + return { + keyType: this.keyType, + publicKeyBase58: this.publicKeyBase58, + } + } } diff --git a/packages/core/src/decorators/attachment/Attachment.ts b/packages/core/src/decorators/attachment/Attachment.ts index 85996ff039..3a91065b56 100644 --- a/packages/core/src/decorators/attachment/Attachment.ts +++ b/packages/core/src/decorators/attachment/Attachment.ts @@ -4,6 +4,7 @@ import { Expose, Type } from 'class-transformer' import { IsDate, IsHash, IsInstance, IsInt, IsMimeType, IsOptional, IsString, ValidateNested } from 'class-validator' import { AriesFrameworkError } from '../../error' +import { JsonValue } from '../../types' import { JsonEncoder } from '../../utils/JsonEncoder' import { uuid } from '../../utils/uuid' @@ -19,7 +20,7 @@ export interface AttachmentOptions { export interface AttachmentDataOptions { base64?: string - json?: Record + json?: JsonValue links?: string[] jws?: JwsDetachedFormat | JwsFlattenedDetachedFormat sha256?: string @@ -40,7 +41,7 @@ export class AttachmentData { * Directly embedded JSON data, when representing content inline instead of via links, and when the content is natively conveyable as JSON. Optional. */ @IsOptional() - public json?: Record + public json?: JsonValue /** * A list of zero or more locations at which the content may be fetched. Optional. diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3c07b50a33..4b86373206 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -80,6 +80,7 @@ export * from './crypto/' // TODO: clean up util exports export { encodeAttachment, isLinkedAttachment } from './utils/attachment' +export type { Optional } from './utils' export { Hasher, HashName } from './utils/Hasher' export { MessageValidator } from './utils/MessageValidator' export { LinkedAttachment, LinkedAttachmentOptions } from './utils/LinkedAttachment' diff --git a/packages/core/src/modules/connections/ConnectionsApi.ts b/packages/core/src/modules/connections/ConnectionsApi.ts index 5ca73ae19d..fd1051bc06 100644 --- a/packages/core/src/modules/connections/ConnectionsApi.ts +++ b/packages/core/src/modules/connections/ConnectionsApi.ts @@ -15,6 +15,7 @@ import { DidResolverService } from '../dids' import { DidRepository } from '../dids/repository' import { OutOfBandService } from '../oob/OutOfBandService' import { RoutingService } from '../routing/services/RoutingService' +import { getMediationRecordForDidDocument } from '../routing/services/helpers' import { ConnectionsModuleConfig } from './ConnectionsModuleConfig' import { DidExchangeProtocol } from './DidExchangeProtocol' @@ -28,8 +29,13 @@ import { TrustPingMessageHandler, TrustPingResponseMessageHandler, ConnectionProblemReportHandler, + DidRotateHandler, + DidRotateAckHandler, + DidRotateProblemReportHandler, + HangupHandler, } from './handlers' import { HandshakeProtocol } from './models' +import { DidRotateService } from './services' import { ConnectionService } from './services/ConnectionService' import { TrustPingService } from './services/TrustPingService' @@ -47,6 +53,7 @@ export class ConnectionsApi { private didExchangeProtocol: DidExchangeProtocol private connectionService: ConnectionService + private didRotateService: DidRotateService private outOfBandService: OutOfBandService private messageSender: MessageSender private trustPingService: TrustPingService @@ -59,6 +66,7 @@ export class ConnectionsApi { messageHandlerRegistry: MessageHandlerRegistry, didExchangeProtocol: DidExchangeProtocol, connectionService: ConnectionService, + didRotateService: DidRotateService, outOfBandService: OutOfBandService, trustPingService: TrustPingService, routingService: RoutingService, @@ -70,6 +78,7 @@ export class ConnectionsApi { ) { this.didExchangeProtocol = didExchangeProtocol this.connectionService = connectionService + this.didRotateService = didRotateService this.outOfBandService = outOfBandService this.trustPingService = trustPingService this.routingService = routingService @@ -91,9 +100,14 @@ export class ConnectionsApi { imageUrl?: string protocol: HandshakeProtocol routing?: Routing + ourDid?: string } ) { - const { protocol, label, alias, imageUrl, autoAcceptConnection } = config + const { protocol, label, alias, imageUrl, autoAcceptConnection, ourDid } = config + + if (ourDid && config.routing) { + throw new AriesFrameworkError(`'routing' is disallowed when defining 'ourDid'`) + } const routing = config.routing || @@ -106,8 +120,13 @@ export class ConnectionsApi { alias, routing, autoAcceptConnection, + ourDid, }) } else if (protocol === HandshakeProtocol.Connections) { + if (ourDid) { + throw new AriesFrameworkError('Using an externally defined did for connections protocol is unsupported') + } + result = await this.connectionService.createRequest(this.agentContext, outOfBandRecord, { label, alias, @@ -268,6 +287,74 @@ export class ConnectionsApi { return message } + /** + * Rotate the DID used for a given connection, notifying the other party immediately. + * + * If `toDid` is not specified, a new peer did will be created. Optionally, routing + * configuration can be set. + * + * Note: any did created or imported in agent wallet can be used as `toDid`, as long as + * there are valid DIDComm services in its DID Document. + * + * @param options connectionId and optional target did and routing configuration + * @returns object containing the new did + */ + public async rotate(options: { connectionId: string; toDid?: string; routing?: Routing }) { + const { connectionId, toDid } = options + const connection = await this.connectionService.getById(this.agentContext, connectionId) + + if (toDid && options.routing) { + throw new AriesFrameworkError(`'routing' is disallowed when defining 'toDid'`) + } + + let routing = options.routing + if (!toDid && !routing) { + routing = await this.routingService.getRouting(this.agentContext, {}) + } + + const message = await this.didRotateService.createRotate(this.agentContext, { + connection, + toDid, + routing, + }) + + const outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + + return { newDid: message.toDid } + } + + /** + * Terminate a connection by sending a hang-up message to the other party. The connection record itself and any + * keys used for mediation will only be deleted if `deleteAfterHangup` flag is set. + * + * @param options connectionId + */ + public async hangup(options: { connectionId: string; deleteAfterHangup?: boolean }) { + const connection = await this.connectionService.getById(this.agentContext, options.connectionId) + + const connectionBeforeHangup = connection.clone() + + // Create Hangup message and update did in connection record + const message = await this.didRotateService.createHangup(this.agentContext, { connection }) + + const outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection: connectionBeforeHangup, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + + // After hang-up message submission, delete connection if required + if (options.deleteAfterHangup) { + await this.deleteById(connection.id) + } + } + public async returnWhenIsConnected(connectionId: string, options?: { timeoutMs: number }): Promise { return this.connectionService.returnWhenIsConnected(this.agentContext, connectionId, options?.timeoutMs) } @@ -384,6 +471,39 @@ export class ConnectionsApi { return this.connectionService.deleteById(this.agentContext, connectionId) } + /** + * Remove relationship of a connection with any previous did (either ours or theirs), preventing it from accepting + * messages from them. This is usually called when a DID Rotation flow has been succesful and we are sure that no + * more messages with older keys will arrive. + * + * It will remove routing keys from mediator if applicable. + * + * Note: this will not actually delete any DID from the wallet. + * + * @param connectionId + */ + public async removePreviousDids(options: { connectionId: string }) { + const connection = await this.connectionService.getById(this.agentContext, options.connectionId) + + for (const previousDid of connection.previousDids) { + const did = await this.didResolverService.resolve(this.agentContext, previousDid) + if (!did.didDocument) continue + const mediatorRecord = await getMediationRecordForDidDocument(this.agentContext, did.didDocument) + + if (mediatorRecord) { + await this.routingService.removeRouting(this.agentContext, { + recipientKeys: did.didDocument.recipientKeys, + mediatorId: mediatorRecord.id, + }) + } + } + + connection.previousDids = [] + connection.previousTheirDids = [] + + await this.connectionService.update(this.agentContext, connection) + } + public async findAllByOutOfBandId(outOfBandId: string) { return this.connectionService.findAllByOutOfBandId(this.agentContext, outOfBandId) } @@ -450,5 +570,13 @@ export class ConnectionsApi { messageHandlerRegistry.registerMessageHandler( new DidExchangeCompleteHandler(this.didExchangeProtocol, this.outOfBandService) ) + + messageHandlerRegistry.registerMessageHandler(new DidRotateHandler(this.didRotateService, this.connectionService)) + + messageHandlerRegistry.registerMessageHandler(new DidRotateAckHandler(this.didRotateService)) + + messageHandlerRegistry.registerMessageHandler(new HangupHandler(this.didRotateService)) + + messageHandlerRegistry.registerMessageHandler(new DidRotateProblemReportHandler(this.didRotateService)) } } diff --git a/packages/core/src/modules/connections/ConnectionsModule.ts b/packages/core/src/modules/connections/ConnectionsModule.ts index 537f7695a7..dcddf81da3 100644 --- a/packages/core/src/modules/connections/ConnectionsModule.ts +++ b/packages/core/src/modules/connections/ConnectionsModule.ts @@ -7,9 +7,9 @@ import { Protocol } from '../../agent/models' import { ConnectionsApi } from './ConnectionsApi' import { ConnectionsModuleConfig } from './ConnectionsModuleConfig' import { DidExchangeProtocol } from './DidExchangeProtocol' -import { ConnectionRole, DidExchangeRole } from './models' +import { ConnectionRole, DidExchangeRole, DidRotateRole } from './models' import { ConnectionRepository } from './repository' -import { ConnectionService, TrustPingService } from './services' +import { ConnectionService, DidRotateService, TrustPingService } from './services' export class ConnectionsModule implements Module { public readonly config: ConnectionsModuleConfig @@ -32,6 +32,7 @@ export class ConnectionsModule implements Module { // Services dependencyManager.registerSingleton(ConnectionService) dependencyManager.registerSingleton(DidExchangeProtocol) + dependencyManager.registerSingleton(DidRotateService) dependencyManager.registerSingleton(TrustPingService) // Repositories @@ -44,8 +45,12 @@ export class ConnectionsModule implements Module { roles: [ConnectionRole.Invitee, ConnectionRole.Inviter], }), new Protocol({ - id: 'https://didcomm.org/didexchange/1.0', + id: 'https://didcomm.org/didexchange/1.1', roles: [DidExchangeRole.Requester, DidExchangeRole.Responder], + }), + new Protocol({ + id: 'https://didcomm.org/did-rotate/1.0', + roles: [DidRotateRole.RotatingParty, DidRotateRole.ObservingParty], }) ) } diff --git a/packages/core/src/modules/connections/ConnectionsModuleConfig.ts b/packages/core/src/modules/connections/ConnectionsModuleConfig.ts index e3bd0c0408..a978241c70 100644 --- a/packages/core/src/modules/connections/ConnectionsModuleConfig.ts +++ b/packages/core/src/modules/connections/ConnectionsModuleConfig.ts @@ -1,3 +1,5 @@ +import { PeerDidNumAlgo } from '../dids' + /** * ConnectionsModuleConfigOptions defines the interface for the options of the ConnectionsModuleConfig class. * This can contain optional parameters that have default values in the config class itself. @@ -13,15 +15,35 @@ export interface ConnectionsModuleConfigOptions { * @default false */ autoAcceptConnections?: boolean + + /** + * Peer did num algo to use in requests for DID exchange protocol (RFC 0023). It will be also used by default + * in responses in case that the request does not use a peer did. + * + * @default PeerDidNumAlgo.GenesisDoc + */ + peerNumAlgoForDidExchangeRequests?: PeerDidNumAlgo + + /** + * Peer did num algo to use for DID rotation (RFC 0794). + * + * @default PeerDidNumAlgo.ShortFormAndLongForm + */ + peerNumAlgoForDidRotation?: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc | PeerDidNumAlgo.ShortFormAndLongForm } export class ConnectionsModuleConfig { #autoAcceptConnections?: boolean + #peerNumAlgoForDidExchangeRequests?: PeerDidNumAlgo + #peerNumAlgoForDidRotation?: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc | PeerDidNumAlgo.ShortFormAndLongForm + private options: ConnectionsModuleConfigOptions public constructor(options?: ConnectionsModuleConfigOptions) { this.options = options ?? {} this.#autoAcceptConnections = this.options.autoAcceptConnections + this.#peerNumAlgoForDidExchangeRequests = this.options.peerNumAlgoForDidExchangeRequests + this.#peerNumAlgoForDidRotation = this.options.peerNumAlgoForDidRotation } /** See {@link ConnectionsModuleConfigOptions.autoAcceptConnections} */ @@ -33,4 +55,26 @@ export class ConnectionsModuleConfig { public set autoAcceptConnections(autoAcceptConnections: boolean) { this.#autoAcceptConnections = autoAcceptConnections } + + /** See {@link ConnectionsModuleConfigOptions.peerNumAlgoForDidExchangeRequests} */ + public get peerNumAlgoForDidExchangeRequests() { + return this.#peerNumAlgoForDidExchangeRequests ?? PeerDidNumAlgo.GenesisDoc + } + + /** See {@link ConnectionsModuleConfigOptions.peerNumAlgoForDidExchangeRequests} */ + public set peerNumAlgoForDidExchangeRequests(peerNumAlgoForDidExchangeRequests: PeerDidNumAlgo) { + this.#peerNumAlgoForDidExchangeRequests = peerNumAlgoForDidExchangeRequests + } + + /** See {@link ConnectionsModuleConfigOptions.peerNumAlgoForDidRotation} */ + public get peerNumAlgoForDidRotation() { + return this.#peerNumAlgoForDidRotation ?? PeerDidNumAlgo.ShortFormAndLongForm + } + + /** See {@link ConnectionsModuleConfigOptions.peerNumAlgoForDidRotation} */ + public set peerNumAlgoForDidRotation( + peerNumAlgoForDidRotation: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc | PeerDidNumAlgo.ShortFormAndLongForm + ) { + this.#peerNumAlgoForDidRotation = peerNumAlgoForDidRotation + } } diff --git a/packages/core/src/modules/connections/DidExchangeProtocol.ts b/packages/core/src/modules/connections/DidExchangeProtocol.ts index b9da15c304..92eb8111d7 100644 --- a/packages/core/src/modules/connections/DidExchangeProtocol.ts +++ b/packages/core/src/modules/connections/DidExchangeProtocol.ts @@ -4,7 +4,6 @@ import type { AgentContext } from '../../agent' import type { InboundMessageContext } from '../../agent/models/InboundMessageContext' import type { ParsedMessageType } from '../../utils/messageType' import type { ResolvedDidCommService } from '../didcomm' -import type { PeerDidCreateOptions } from '../dids' import type { OutOfBandRecord } from '../oob/repository' import { InjectionSymbols } from '../../constants' @@ -16,60 +15,59 @@ import { Attachment, AttachmentData } from '../../decorators/attachment/Attachme import { AriesFrameworkError } from '../../error' import { Logger } from '../../logger' import { inject, injectable } from '../../plugins' -import { isDid } from '../../utils' +import { TypedArrayEncoder, isDid, Buffer } from '../../utils' import { JsonEncoder } from '../../utils/JsonEncoder' import { JsonTransformer } from '../../utils/JsonTransformer' import { base64ToBase64URL } from '../../utils/base64' import { DidDocument, - DidRegistrarService, - DidDocumentRole, - createPeerDidDocumentFromServices, DidKey, getNumAlgoFromPeerDid, PeerDidNumAlgo, + DidsApi, + isValidPeerDid, + getAlternativeDidsForPeerDid, } from '../dids' import { getKeyFromVerificationMethod } from '../dids/domain/key-type' import { tryParseDid } from '../dids/domain/parse' import { didKeyToInstanceOfKey } from '../dids/helpers' -import { DidRecord, DidRepository } from '../dids/repository' +import { DidRepository } from '../dids/repository' import { OutOfBandRole } from '../oob/domain/OutOfBandRole' import { OutOfBandState } from '../oob/domain/OutOfBandState' +import { getMediationRecordForDidDocument } from '../routing/services/helpers' +import { ConnectionsModuleConfig } from './ConnectionsModuleConfig' import { DidExchangeStateMachine } from './DidExchangeStateMachine' import { DidExchangeProblemReportError, DidExchangeProblemReportReason } from './errors' -import { DidExchangeCompleteMessage } from './messages/DidExchangeCompleteMessage' -import { DidExchangeRequestMessage } from './messages/DidExchangeRequestMessage' -import { DidExchangeResponseMessage } from './messages/DidExchangeResponseMessage' +import { DidExchangeRequestMessage, DidExchangeResponseMessage, DidExchangeCompleteMessage } from './messages' import { DidExchangeRole, DidExchangeState, HandshakeProtocol } from './models' import { ConnectionService } from './services' +import { createPeerDidFromServices, getDidDocumentForCreatedDid, routingToServices } from './services/helpers' interface DidExchangeRequestParams { label?: string alias?: string goal?: string goalCode?: string - routing: Routing + routing?: Routing autoAcceptConnection?: boolean + ourDid?: string } @injectable() export class DidExchangeProtocol { private connectionService: ConnectionService - private didRegistrarService: DidRegistrarService private jwsService: JwsService private didRepository: DidRepository private logger: Logger public constructor( connectionService: ConnectionService, - didRegistrarService: DidRegistrarService, didRepository: DidRepository, jwsService: JwsService, @inject(InjectionSymbols.Logger) logger: Logger ) { this.connectionService = connectionService - this.didRegistrarService = didRegistrarService this.didRepository = didRepository this.jwsService = jwsService this.logger = logger @@ -84,21 +82,56 @@ export class DidExchangeProtocol { outOfBandRecord, params, }) + const config = agentContext.dependencyManager.resolve(ConnectionsModuleConfig) const { outOfBandInvitation } = outOfBandRecord - const { alias, goal, goalCode, routing, autoAcceptConnection } = params - + const { alias, goal, goalCode, routing, autoAcceptConnection, ourDid: did } = params // TODO: We should store only one did that we'll use to send the request message with success. // We take just the first one for now. const [invitationDid] = outOfBandInvitation.invitationDids + // Create message + const label = params.label ?? agentContext.config.label + + let didDocument, mediatorId + + // If our did is specified, make sure we have all key material for it + if (did) { + didDocument = await getDidDocumentForCreatedDid(agentContext, did) + mediatorId = (await getMediationRecordForDidDocument(agentContext, didDocument))?.id + // Otherwise, create a did:peer based on the provided routing + } else { + if (!routing) throw new AriesFrameworkError(`'routing' must be defined if 'ourDid' is not specified`) + + didDocument = await createPeerDidFromServices( + agentContext, + routingToServices(routing), + config.peerNumAlgoForDidExchangeRequests + ) + mediatorId = routing.mediatorId + } + + const parentThreadId = outOfBandRecord.outOfBandInvitation.id + + const message = new DidExchangeRequestMessage({ label, parentThreadId, did: didDocument.id, goal, goalCode }) + + // Create sign attachment containing didDoc + if (isValidPeerDid(didDocument.id) && getNumAlgoFromPeerDid(didDocument.id) === PeerDidNumAlgo.GenesisDoc) { + const didDocAttach = await this.createSignedAttachment( + agentContext, + didDocument.toJSON(), + didDocument.recipientKeys.map((key) => key.publicKeyBase58) + ) + message.didDoc = didDocAttach + } + const connectionRecord = await this.connectionService.createConnection(agentContext, { protocol: HandshakeProtocol.DidExchange, role: DidExchangeRole.Requester, alias, state: DidExchangeState.InvitationReceived, theirLabel: outOfBandInvitation.label, - mediatorId: routing.mediatorId, + mediatorId, autoAcceptConnection: outOfBandRecord.autoAcceptConnection, outOfBandId: outOfBandRecord.id, invitationDid, @@ -107,21 +140,6 @@ export class DidExchangeProtocol { DidExchangeStateMachine.assertCreateMessageState(DidExchangeRequestMessage.type, connectionRecord) - // Create message - const label = params.label ?? agentContext.config.label - const didDocument = await this.createPeerDidDoc(agentContext, this.routingToServices(routing)) - const parentThreadId = outOfBandRecord.outOfBandInvitation.id - - const message = new DidExchangeRequestMessage({ label, parentThreadId, did: didDocument.id, goal, goalCode }) - - // Create sign attachment containing didDoc - if (getNumAlgoFromPeerDid(didDocument.id) === PeerDidNumAlgo.GenesisDoc) { - const didDocAttach = await this.createSignedAttachment(agentContext, didDocument, [ - routing.recipientKey.publicKeyBase58, - ]) - message.didDoc = didDocAttach - } - connectionRecord.did = didDocument.id connectionRecord.threadId = message.id @@ -141,7 +159,7 @@ export class DidExchangeProtocol { messageContext: InboundMessageContext, outOfBandRecord: OutOfBandRecord ): Promise { - this.logger.debug(`Process message ${DidExchangeRequestMessage.type.messageTypeUri} start`, { + this.logger.debug(`Process message ${messageContext.message.type} start`, { message: messageContext.message, }) @@ -150,7 +168,7 @@ export class DidExchangeProtocol { // TODO check there is no connection record for particular oob record - const { message } = messageContext + const { message, agentContext } = messageContext // Check corresponding invitation ID is the request's ~thread.pthid or pthid is a public did // TODO Maybe we can do it in handler, but that actually does not make sense because we try to find oob by parent thread ID there. @@ -165,41 +183,31 @@ export class DidExchangeProtocol { } // If the responder wishes to continue the exchange, they will persist the received information in their wallet. - if (!isDid(message.did, 'peer')) { - throw new DidExchangeProblemReportError( - `Message contains unsupported did ${message.did}. Supported dids are [did:peer]`, - { - problemCode: DidExchangeProblemReportReason.RequestNotAccepted, - } - ) - } - const numAlgo = getNumAlgoFromPeerDid(message.did) - if (numAlgo !== PeerDidNumAlgo.GenesisDoc) { - throw new DidExchangeProblemReportError( - `Unsupported numalgo ${numAlgo}. Supported numalgos are [${PeerDidNumAlgo.GenesisDoc}]`, - { - problemCode: DidExchangeProblemReportReason.RequestNotAccepted, - } - ) - } - // TODO: Move this into the didcomm module, and add a method called store received did document. - // This can be called from both the did exchange and the connection protocol. - const didDocument = await this.extractDidDocument(messageContext.agentContext, message) - const didRecord = new DidRecord({ - did: message.did, - role: DidDocumentRole.Received, + // Get DID Document either from message (if it is a supported did:peer) or resolve it externally + const didDocument = await this.resolveDidDocument(agentContext, message) + + // A DID Record must be stored in order to allow for searching for its recipient keys when receiving a message + const didRecord = await this.didRepository.storeReceivedDid(messageContext.agentContext, { + did: didDocument.id, // It is important to take the did document from the PeerDid class // as it will have the id property - didDocument, + didDocument: + !isValidPeerDid(didDocument.id) || getNumAlgoFromPeerDid(message.did) === PeerDidNumAlgo.GenesisDoc + ? didDocument + : undefined, tags: { // We need to save the recipientKeys, so we can find the associated did // of a key when we receive a message from another connection. recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + + // For did:peer, store any alternative dids (like short form did:peer:4), + // it may have in order to relate any message referencing it + alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(didDocument.id) : undefined, }, }) - this.logger.debug('Saving DID record', { + this.logger.debug('Saved DID record', { id: didRecord.id, did: didRecord.did, role: didRecord.role, @@ -207,8 +215,6 @@ export class DidExchangeProtocol { didDocument: 'omitted...', }) - await this.didRepository.save(messageContext.agentContext, didRecord) - const connectionRecord = await this.connectionService.createConnection(messageContext.agentContext, { protocol: HandshakeProtocol.DidExchange, role: DidExchangeRole.Responder, @@ -236,15 +242,21 @@ export class DidExchangeProtocol { this.logger.debug(`Create message ${DidExchangeResponseMessage.type.messageTypeUri} start`, connectionRecord) DidExchangeStateMachine.assertCreateMessageState(DidExchangeResponseMessage.type, connectionRecord) - const { threadId } = connectionRecord + const { threadId, theirDid } = connectionRecord + + const config = agentContext.dependencyManager.resolve(ConnectionsModuleConfig) if (!threadId) { throw new AriesFrameworkError('Missing threadId on connection record.') } + if (!theirDid) { + throw new AriesFrameworkError('Missing theirDid on connection record.') + } + let services: ResolvedDidCommService[] = [] if (routing) { - services = this.routingToServices(routing) + services = routingToServices(routing) } else if (outOfBandRecord) { const inlineServices = outOfBandRecord.outOfBandInvitation.getInlineServices() services = inlineServices.map((service) => ({ @@ -255,13 +267,32 @@ export class DidExchangeProtocol { })) } - const didDocument = await this.createPeerDidDoc(agentContext, services) + // Use the same num algo for response as received in request + const numAlgo = isValidPeerDid(theirDid) + ? getNumAlgoFromPeerDid(theirDid) + : config.peerNumAlgoForDidExchangeRequests + + const didDocument = await createPeerDidFromServices(agentContext, services, numAlgo) const message = new DidExchangeResponseMessage({ did: didDocument.id, threadId }) - if (getNumAlgoFromPeerDid(didDocument.id) === PeerDidNumAlgo.GenesisDoc) { - const didDocAttach = await this.createSignedAttachment( + if (numAlgo === PeerDidNumAlgo.GenesisDoc) { + message.didDoc = await this.createSignedAttachment( agentContext, - didDocument, + didDocument.toJSON(), + Array.from( + new Set( + services + .map((s) => s.recipientKeys) + .reduce((acc, curr) => acc.concat(curr), []) + .map((key) => key.publicKeyBase58) + ) + ) + ) + } else { + // We assume any other case is a resolvable did (e.g. did:peer:2 or did:peer:4) + message.didRotate = await this.createSignedAttachment( + agentContext, + didDocument.id, Array.from( new Set( services @@ -271,7 +302,6 @@ export class DidExchangeProtocol { ) ) ) - message.didDoc = didDocAttach } connectionRecord.did = didDocument.id @@ -292,7 +322,7 @@ export class DidExchangeProtocol { message: messageContext.message, }) - const { connection: connectionRecord, message } = messageContext + const { connection: connectionRecord, message, agentContext } = messageContext if (!connectionRecord) { throw new AriesFrameworkError('No connection record in message context.') @@ -306,51 +336,38 @@ export class DidExchangeProtocol { }) } - if (!isDid(message.did, 'peer')) { - throw new DidExchangeProblemReportError( - `Message contains unsupported did ${message.did}. Supported dids are [did:peer]`, - { - problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, - } - ) - } - const numAlgo = getNumAlgoFromPeerDid(message.did) - if (numAlgo !== PeerDidNumAlgo.GenesisDoc) { - throw new DidExchangeProblemReportError( - `Unsupported numalgo ${numAlgo}. Supported numalgos are [${PeerDidNumAlgo.GenesisDoc}]`, - { - problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, - } - ) - } - - const didDocument = await this.extractDidDocument( - messageContext.agentContext, + // Get DID Document either from message (if it is a did:peer) or resolve it externally + const didDocument = await this.resolveDidDocument( + agentContext, message, outOfBandRecord .getTags() .recipientKeyFingerprints.map((fingerprint) => Key.fromFingerprint(fingerprint).publicKeyBase58) ) - const didRecord = new DidRecord({ - did: message.did, - role: DidDocumentRole.Received, - didDocument, - tags: { - // We need to save the recipientKeys, so we can find the associated did - // of a key when we receive a message from another connection. - recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), - }, - }) - this.logger.debug('Saving DID record', { - id: didRecord.id, - did: didRecord.did, - role: didRecord.role, - tags: didRecord.getTags(), - didDocument: 'omitted...', - }) + if (isValidPeerDid(didDocument.id)) { + const didRecord = await this.didRepository.storeReceivedDid(messageContext.agentContext, { + did: didDocument.id, + didDocument: getNumAlgoFromPeerDid(message.did) === PeerDidNumAlgo.GenesisDoc ? didDocument : undefined, + tags: { + // We need to save the recipientKeys, so we can find the associated did + // of a key when we receive a message from another connection. + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + + // For did:peer, store any alternative dids (like short form did:peer:4), + // it may have in order to relate any message referencing it + alternativeDids: getAlternativeDidsForPeerDid(didDocument.id), + }, + }) - await this.didRepository.save(messageContext.agentContext, didRecord) + this.logger.debug('Saved DID record', { + id: didRecord.id, + did: didRecord.did, + role: didRecord.role, + tags: didRecord.getTags(), + didDocument: 'omitted...', + }) + } connectionRecord.theirDid = message.did @@ -433,36 +450,16 @@ export class DidExchangeProtocol { return this.connectionService.updateState(agentContext, connectionRecord, nextState) } - private async createPeerDidDoc(agentContext: AgentContext, services: ResolvedDidCommService[]) { - // Create did document without the id property - const didDocument = createPeerDidDocumentFromServices(services) - - // Register did:peer document. This will generate the id property and save it to a did record - const result = await this.didRegistrarService.create(agentContext, { - method: 'peer', - didDocument, - options: { - numAlgo: PeerDidNumAlgo.GenesisDoc, - }, - }) - - if (result.didState?.state !== 'finished') { - throw new AriesFrameworkError(`Did document creation failed: ${JSON.stringify(result.didState)}`) - } - - this.logger.debug(`Did document with did ${result.didState.did} created.`, { - did: result.didState.did, - didDocument: result.didState.didDocument, - }) - - return result.didState.didDocument - } - - private async createSignedAttachment(agentContext: AgentContext, didDoc: DidDocument, verkeys: string[]) { - const didDocAttach = new Attachment({ - mimeType: 'application/json', + private async createSignedAttachment( + agentContext: AgentContext, + data: string | Record, + verkeys: string[] + ) { + const signedAttach = new Attachment({ + mimeType: typeof data === 'string' ? undefined : 'application/json', data: new AttachmentData({ - base64: JsonEncoder.toBase64(didDoc), + base64: + typeof data === 'string' ? TypedArrayEncoder.toBase64URL(Buffer.from(data)) : JsonEncoder.toBase64(data), }), }) @@ -470,7 +467,7 @@ export class DidExchangeProtocol { verkeys.map(async (verkey) => { const key = Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) const kid = new DidKey(key).did - const payload = JsonEncoder.toBuffer(didDoc) + const payload = typeof data === 'string' ? TypedArrayEncoder.fromString(data) : JsonEncoder.toBuffer(data) const jws = await this.jwsService.createJws(agentContext, { payload, @@ -483,11 +480,114 @@ export class DidExchangeProtocol { jwk: getJwkFromKey(key), }, }) - didDocAttach.addJws(jws) + signedAttach.addJws(jws) }) ) - return didDocAttach + return signedAttach + } + + /** + * Resolves a did document from a given `request` or `response` message, verifying its signature or did rotate + * signature in case it is taken from message attachment. + * + * @param message DID request or DID response message + * @param invitationKeys array containing keys from connection invitation that could be used for signing of DID document + * @returns verified DID document content from message attachment + */ + + private async resolveDidDocument( + agentContext: AgentContext, + message: DidExchangeRequestMessage | DidExchangeResponseMessage, + invitationKeysBase58: string[] = [] + ) { + // The only supported case where we expect to receive a did-document attachment is did:peer algo 1 + return isDid(message.did, 'peer') && getNumAlgoFromPeerDid(message.did) === PeerDidNumAlgo.GenesisDoc + ? this.extractAttachedDidDocument(agentContext, message, invitationKeysBase58) + : this.extractResolvableDidDocument(agentContext, message, invitationKeysBase58) + } + + /** + * Extracts DID document from message (resolving it externally if required) and verifies did-rotate attachment signature + * if applicable + */ + private async extractResolvableDidDocument( + agentContext: AgentContext, + message: DidExchangeRequestMessage | DidExchangeResponseMessage, + invitationKeysBase58?: string[] + ) { + // Validate did-rotate attachment in case of DID Exchange response + if (message instanceof DidExchangeResponseMessage) { + const didRotateAttachment = message.didRotate + + if (!didRotateAttachment) { + throw new DidExchangeProblemReportError('DID Rotate attachment is missing.', { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + }) + } + + const jws = didRotateAttachment.data.jws + + if (!jws) { + throw new DidExchangeProblemReportError('DID Rotate signature is missing.', { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + }) + } + + if (!didRotateAttachment.data.base64) { + throw new AriesFrameworkError('DID Rotate attachment is missing base64 property for signed did.') + } + + // JWS payload must be base64url encoded + const base64UrlPayload = base64ToBase64URL(didRotateAttachment.data.base64) + const signedDid = TypedArrayEncoder.fromBase64(base64UrlPayload).toString() + + if (signedDid !== message.did) { + throw new AriesFrameworkError( + `DID Rotate attachment's did ${message.did} does not correspond to message did ${message.did}` + ) + } + + const { isValid, signerKeys } = await this.jwsService.verifyJws(agentContext, { + jws: { + ...jws, + payload: base64UrlPayload, + }, + jwkResolver: ({ jws: { header } }) => { + if (typeof header.kid !== 'string' || !isDid(header.kid, 'key')) { + throw new AriesFrameworkError('JWS header kid must be a did:key DID.') + } + + const didKey = DidKey.fromDid(header.kid) + return getJwkFromKey(didKey.key) + }, + }) + + if (!isValid || !signerKeys.every((key) => invitationKeysBase58?.includes(key.publicKeyBase58))) { + throw new DidExchangeProblemReportError( + `DID Rotate signature is invalid. isValid: ${isValid} signerKeys: ${JSON.stringify( + signerKeys + )} invitationKeys:${JSON.stringify(invitationKeysBase58)}`, + { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + } + ) + } + } + + // Now resolve the document related to the did (which can be either a public did or an inline did) + try { + return await agentContext.dependencyManager.resolve(DidsApi).resolveDidDocument(message.did) + } catch (error) { + const problemCode = + message instanceof DidExchangeRequestMessage + ? DidExchangeProblemReportReason.RequestNotAccepted + : DidExchangeProblemReportReason.ResponseNotAccepted + + throw new DidExchangeProblemReportError(error, { + problemCode, + }) + } } /** @@ -497,7 +597,7 @@ export class DidExchangeProtocol { * @param invitationKeys array containing keys from connection invitation that could be used for signing of DID document * @returns verified DID document content from message attachment */ - private async extractDidDocument( + private async extractAttachedDidDocument( agentContext: AgentContext, message: DidExchangeRequestMessage | DidExchangeResponseMessage, invitationKeysBase58: string[] = [] @@ -526,7 +626,6 @@ export class DidExchangeProtocol { // JWS payload must be base64url encoded const base64UrlPayload = base64ToBase64URL(didDocumentAttachment.data.base64) - const json = JsonEncoder.fromBase64(didDocumentAttachment.data.base64) const { isValid, signerKeys } = await this.jwsService.verifyJws(agentContext, { jws: { @@ -543,6 +642,7 @@ export class DidExchangeProtocol { }, }) + const json = JsonEncoder.fromBase64(didDocumentAttachment.data.base64) const didDocument = JsonTransformer.fromJSON(json, DidDocument) const didDocumentKeysBase58 = didDocument.authentication ?.map((authentication) => { @@ -567,13 +667,4 @@ export class DidExchangeProtocol { return didDocument } - - private routingToServices(routing: Routing): ResolvedDidCommService[] { - return routing.endpoints.map((endpoint, index) => ({ - id: `#inline-${index}`, - serviceEndpoint: endpoint, - recipientKeys: [routing.recipientKey], - routingKeys: routing.routingKeys, - })) - } } diff --git a/packages/core/src/modules/connections/__tests__/ConnectionsModule.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionsModule.test.ts index 34ebb476f7..8fe0127226 100644 --- a/packages/core/src/modules/connections/__tests__/ConnectionsModule.test.ts +++ b/packages/core/src/modules/connections/__tests__/ConnectionsModule.test.ts @@ -6,6 +6,7 @@ import { ConnectionsModuleConfig } from '../ConnectionsModuleConfig' import { DidExchangeProtocol } from '../DidExchangeProtocol' import { ConnectionRepository } from '../repository' import { ConnectionService, TrustPingService } from '../services' +import { DidRotateService } from '../services/DidRotateService' jest.mock('../../../plugins/DependencyManager') const DependencyManagerMock = DependencyManager as jest.Mock @@ -28,10 +29,11 @@ describe('ConnectionsModule', () => { expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) expect(dependencyManager.registerInstance).toHaveBeenCalledWith(ConnectionsModuleConfig, connectionsModule.config) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(4) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(5) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(ConnectionService) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidExchangeProtocol) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TrustPingService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidRotateService) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(ConnectionRepository) }) }) diff --git a/packages/core/src/modules/connections/__tests__/InMemoryDidRegistry.ts b/packages/core/src/modules/connections/__tests__/InMemoryDidRegistry.ts new file mode 100644 index 0000000000..ba17b1bb28 --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/InMemoryDidRegistry.ts @@ -0,0 +1,105 @@ +import type { AgentContext } from '../../../agent' +import type { + DidRegistrar, + DidResolver, + DidDocument, + DidCreateOptions, + DidCreateResult, + DidUpdateResult, + DidDeactivateResult, + DidResolutionResult, +} from '../../dids' + +import { DidRecord, DidDocumentRole, DidRepository } from '../../dids' + +export class InMemoryDidRegistry implements DidRegistrar, DidResolver { + public readonly supportedMethods = ['inmemory'] + + public readonly allowsCaching = false + + private dids: Record = {} + + public async create(agentContext: AgentContext, options: DidCreateOptions): Promise { + const { did, didDocument } = options + + if (!did || !didDocument) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'InMemoryDidRegistrar requires to specify both did and didDocument', + }, + } + } + + this.dids[did] = didDocument + + // Save the did so we know we created it and can use it for didcomm + const didRecord = new DidRecord({ + did: didDocument.id, + role: DidDocumentRole.Created, + didDocument, + tags: { + // We need to save the recipientKeys, so we can find the associated did + // of a key when we receive a message from another connection. + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + }, + }) + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + await didRepository.save(agentContext, didRecord) + + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: didDocument.id, + didDocument, + }, + } + } + + public async update(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:inmemory not implemented yet`, + }, + } + } + + public async deactivate(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:inmemory not implemented yet`, + }, + } + } + + public async resolve(agentContext: AgentContext, did: string): Promise { + const didDocument = this.dids[did] + + if (!didDocument) { + return { + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}'`, + }, + } + } + + return { + didDocument, + didDocumentMetadata: {}, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } +} diff --git a/packages/core/src/modules/connections/__tests__/did-rotate.e2e.test.ts b/packages/core/src/modules/connections/__tests__/did-rotate.e2e.test.ts new file mode 100644 index 0000000000..14c8496094 --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/did-rotate.e2e.test.ts @@ -0,0 +1,385 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +import type { ConnectionRecord } from '../repository' + +import { ReplaySubject, first, firstValueFrom, timeout } from 'rxjs' + +import { MessageSender } from '../../..//agent/MessageSender' +import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' +import { setupSubjectTransports, testLogger } from '../../../../tests' +import { + getAgentOptions, + makeConnection, + waitForAgentMessageProcessedEvent, + waitForBasicMessage, +} from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { getOutboundMessageContext } from '../../../agent/getOutboundMessageContext' +import { RecordNotFoundError } from '../../../error' +import { uuid } from '../../../utils/uuid' +import { BasicMessage } from '../../basic-messages' +import { createPeerDidDocumentFromServices } from '../../dids' +import { ConnectionsModule } from '../ConnectionsModule' +import { DidRotateProblemReportMessage, HangupMessage, DidRotateAckMessage } from '../messages' + +import { InMemoryDidRegistry } from './InMemoryDidRegistry' + +// This is the most common flow +describe('Rotation E2E tests', () => { + let aliceAgent: Agent + let bobAgent: Agent + let aliceBobConnection: ConnectionRecord | undefined + let bobAliceConnection: ConnectionRecord | undefined + + beforeEach(async () => { + const aliceAgentOptions = getAgentOptions( + 'DID Rotate Alice', + { + label: 'alice', + endpoints: ['rxjs:alice'], + logger: testLogger, + }, + { + ...getIndySdkModules(), + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + } + ) + const bobAgentOptions = getAgentOptions( + 'DID Rotate Bob', + { + label: 'bob', + endpoints: ['rxjs:bob'], + logger: testLogger, + }, + { + ...getIndySdkModules(), + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + } + ) + + aliceAgent = new Agent(aliceAgentOptions) + bobAgent = new Agent(bobAgentOptions) + + setupSubjectTransports([aliceAgent, bobAgent]) + await aliceAgent.initialize() + await bobAgent.initialize() + ;[aliceBobConnection, bobAliceConnection] = await makeConnection(aliceAgent, bobAgent) + }) + + afterEach(async () => { + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + await bobAgent.shutdown() + await bobAgent.wallet.delete() + }) + + describe('Rotation from did:peer:1 to did:peer:4', () => { + test('Rotate succesfully and send messages to new did afterwards', async () => { + const oldDid = aliceBobConnection!.did + expect(bobAliceConnection!.theirDid).toEqual(oldDid) + + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + // Do did rotate + const { newDid } = await aliceAgent.connections.rotate({ connectionId: aliceBobConnection!.id }) + + // Wait for acknowledge + await waitForAgentMessageProcessedEvent(aliceAgent, { messageType: DidRotateAckMessage.type.messageTypeUri }) + + // Check that new did is taken into account by both parties + const newAliceBobConnection = await aliceAgent.connections.getById(aliceBobConnection!.id) + const newBobAliceConnection = await bobAgent.connections.getById(bobAliceConnection!.id) + + expect(newAliceBobConnection.did).toEqual(newDid) + expect(newBobAliceConnection.theirDid).toEqual(newDid) + + // And also they store it into previous dids array + expect(newAliceBobConnection.previousDids).toContain(oldDid) + expect(newBobAliceConnection.previousTheirDids).toContain(oldDid) + + // Send message to new did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello new did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello new did', connectionId: aliceBobConnection!.id }) + }) + + test('Rotate succesfully and send messages to previous did afterwards', async () => { + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + const messageToPreviousDid = await getOutboundMessageContext(bobAgent.context, { + message: new BasicMessage({ content: 'Message to previous did' }), + connectionRecord: bobAliceConnection, + }) + + // Do did rotate + await aliceAgent.connections.rotate({ connectionId: aliceBobConnection!.id }) + + // Wait for acknowledge + await waitForAgentMessageProcessedEvent(aliceAgent, { messageType: DidRotateAckMessage.type.messageTypeUri }) + + // Send message to previous did + await bobAgent.dependencyManager.resolve(MessageSender).sendMessage(messageToPreviousDid) + + await waitForBasicMessage(aliceAgent, { + content: 'Message to previous did', + connectionId: aliceBobConnection!.id, + }) + }) + }) + + describe('Rotation specifying did and routing externally', () => { + test('Rotate succesfully and send messages to new did afterwards', async () => { + const oldDid = aliceBobConnection!.did + expect(bobAliceConnection!.theirDid).toEqual(oldDid) + + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + // Create a new external did + + // Make a common in-memory did registry for both agents + const didRegistry = new InMemoryDidRegistry() + aliceAgent.dids.config.addRegistrar(didRegistry) + aliceAgent.dids.config.addResolver(didRegistry) + bobAgent.dids.config.addRegistrar(didRegistry) + bobAgent.dids.config.addResolver(didRegistry) + + const didRouting = await aliceAgent.mediationRecipient.getRouting({}) + const did = `did:inmemory:${uuid()}` + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ]) + didDocument.id = did + + await aliceAgent.dids.create({ + did, + didDocument, + }) + + // Do did rotate + const { newDid } = await aliceAgent.connections.rotate({ + connectionId: aliceBobConnection!.id, + toDid: did, + }) + + // Wait for acknowledge + await waitForAgentMessageProcessedEvent(aliceAgent, { messageType: DidRotateAckMessage.type.messageTypeUri }) + + // Check that new did is taken into account by both parties + const newAliceBobConnection = await aliceAgent.connections.getById(aliceBobConnection!.id) + const newBobAliceConnection = await bobAgent.connections.getById(bobAliceConnection!.id) + + expect(newAliceBobConnection.did).toEqual(newDid) + expect(newBobAliceConnection.theirDid).toEqual(newDid) + + // And also they store it into previous dids array + expect(newAliceBobConnection.previousDids).toContain(oldDid) + expect(newBobAliceConnection.previousTheirDids).toContain(oldDid) + + // Send message to new did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello new did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello new did', connectionId: aliceBobConnection!.id }) + }) + + test('Rotate succesfully and send messages to previous did afterwards', async () => { + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + const messageToPreviousDid = await getOutboundMessageContext(bobAgent.context, { + message: new BasicMessage({ content: 'Message to previous did' }), + connectionRecord: bobAliceConnection, + }) + + // Create a new external did + + // Make a common in-memory did registry for both agents + const didRegistry = new InMemoryDidRegistry() + aliceAgent.dids.config.addRegistrar(didRegistry) + aliceAgent.dids.config.addResolver(didRegistry) + bobAgent.dids.config.addRegistrar(didRegistry) + bobAgent.dids.config.addResolver(didRegistry) + + const didRouting = await aliceAgent.mediationRecipient.getRouting({}) + const did = `did:inmemory:${uuid()}` + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ]) + didDocument.id = did + + await aliceAgent.dids.create({ + did, + didDocument, + }) + + // Do did rotate + await aliceAgent.connections.rotate({ connectionId: aliceBobConnection!.id, toDid: did }) + + // Wait for acknowledge + await waitForAgentMessageProcessedEvent(aliceAgent, { messageType: DidRotateAckMessage.type.messageTypeUri }) + + // Send message to previous did + await bobAgent.dependencyManager.resolve(MessageSender).sendMessage(messageToPreviousDid) + + await waitForBasicMessage(aliceAgent, { + content: 'Message to previous did', + connectionId: aliceBobConnection!.id, + }) + }) + + test('Rotate failed and send messages to previous did afterwards', async () => { + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + const messageToPreviousDid = await getOutboundMessageContext(bobAgent.context, { + message: new BasicMessage({ content: 'Message to previous did' }), + connectionRecord: bobAliceConnection, + }) + + // Create a new external did + + // Use custom registry only for Alice agent, in order to force an error on Bob side + const didRegistry = new InMemoryDidRegistry() + aliceAgent.dids.config.addRegistrar(didRegistry) + aliceAgent.dids.config.addResolver(didRegistry) + + const didRouting = await aliceAgent.mediationRecipient.getRouting({}) + const did = `did:inmemory:${uuid()}` + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ]) + didDocument.id = did + + await aliceAgent.dids.create({ + did, + didDocument, + }) + + // Do did rotate + await aliceAgent.connections.rotate({ connectionId: aliceBobConnection!.id, toDid: did }) + + // Wait for a problem report + await waitForAgentMessageProcessedEvent(aliceAgent, { + messageType: DidRotateProblemReportMessage.type.messageTypeUri, + }) + + // Send message to previous did + await bobAgent.dependencyManager.resolve(MessageSender).sendMessage(messageToPreviousDid) + + await waitForBasicMessage(aliceAgent, { + content: 'Message to previous did', + connectionId: aliceBobConnection!.id, + }) + + // Send message to stored did (should be the previous one) + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Message after did rotation failure') + + await waitForBasicMessage(aliceAgent, { + content: 'Message after did rotation failure', + connectionId: aliceBobConnection!.id, + }) + }) + }) + + describe('Hangup', () => { + test('Hangup without record deletion', async () => { + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + // Store an outbound context so we can attempt to send a message even if the connection is terminated. + // A bit hacky, but may happen in some cases where message retry mechanisms are being used + const messageBeforeHangup = await getOutboundMessageContext(bobAgent.context, { + message: new BasicMessage({ content: 'Message before hangup' }), + connectionRecord: bobAliceConnection!.clone(), + }) + + await aliceAgent.connections.hangup({ connectionId: aliceBobConnection!.id }) + + // Wait for hangup + await waitForAgentMessageProcessedEvent(bobAgent, { + messageType: HangupMessage.type.messageTypeUri, + }) + + // If Bob attempts to send a message to Alice after they received the hangup, framework should reject it + expect(bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Message after hangup')).rejects.toThrowError() + + // If Bob sends a message afterwards, Alice should still be able to receive it + await bobAgent.dependencyManager.resolve(MessageSender).sendMessage(messageBeforeHangup) + + await waitForBasicMessage(aliceAgent, { + content: 'Message before hangup', + connectionId: aliceBobConnection!.id, + }) + }) + + test('Hangup and delete connection record', async () => { + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + // Store an outbound context so we can attempt to send a message even if the connection is terminated. + // A bit hacky, but may happen in some cases where message retry mechanisms are being used + const messageBeforeHangup = await getOutboundMessageContext(bobAgent.context, { + message: new BasicMessage({ content: 'Message before hangup' }), + connectionRecord: bobAliceConnection!.clone(), + }) + + await aliceAgent.connections.hangup({ connectionId: aliceBobConnection!.id, deleteAfterHangup: true }) + + // Verify that alice connection has been effectively deleted + expect(aliceAgent.connections.getById(aliceBobConnection!.id)).rejects.toThrowError(RecordNotFoundError) + + // Wait for hangup + await waitForAgentMessageProcessedEvent(bobAgent, { + messageType: HangupMessage.type.messageTypeUri, + }) + + // If Bob sends a message afterwards, Alice should not receive it since the connection has been deleted + await bobAgent.dependencyManager.resolve(MessageSender).sendMessage(messageBeforeHangup) + + // An error is thrown by Alice agent and, after inspecting all basic messages, it cannot be found + // TODO: Update as soon as agent sends error events upon reception of messages + const observable = aliceAgent.events.observable('AgentReceiveMessageError') + const subject = new ReplaySubject(1) + observable.pipe(first(), timeout({ first: 10000 })).subscribe(subject) + await firstValueFrom(subject) + + const aliceBasicMessages = await aliceAgent.basicMessages.findAllByQuery({}) + expect(aliceBasicMessages.find((message) => message.content === 'Message before hangup')).toBeUndefined() + }) + }) +}) diff --git a/packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts b/packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts new file mode 100644 index 0000000000..50cf2742ee --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts @@ -0,0 +1,193 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { ConnectionStateChangedEvent } from '../ConnectionEvents' + +import { firstValueFrom } from 'rxjs' +import { filter, first, map, timeout } from 'rxjs/operators' + +import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' +import { setupSubjectTransports } from '../../../../tests' +import { getAgentOptions } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { uuid } from '../../../utils/uuid' +import { DidsModule, PeerDidNumAlgo, createPeerDidDocumentFromServices } from '../../dids' +import { ConnectionEventTypes } from '../ConnectionEvents' +import { ConnectionsModule } from '../ConnectionsModule' +import { DidExchangeState } from '../models' + +import { InMemoryDidRegistry } from './InMemoryDidRegistry' + +function waitForRequest(agent: Agent, theirLabel: string) { + return firstValueFrom( + agent.events.observable(ConnectionEventTypes.ConnectionStateChanged).pipe( + map((event) => event.payload.connectionRecord), + // Wait for request received + filter( + (connectionRecord) => + connectionRecord.state === DidExchangeState.RequestReceived && connectionRecord.theirLabel === theirLabel + ), + first(), + timeout(5000) + ) + ) +} + +function waitForResponse(agent: Agent, connectionId: string) { + return firstValueFrom( + agent.events.observable(ConnectionEventTypes.ConnectionStateChanged).pipe( + // Wait for response received + map((event) => event.payload.connectionRecord), + filter( + (connectionRecord) => + connectionRecord.state === DidExchangeState.ResponseReceived && connectionRecord.id === connectionId + ), + first(), + timeout(5000) + ) + ) +} + +describe('Did Exchange numalgo settings', () => { + test('Connect using default setting (numalgo 1)', async () => { + await didExchangeNumAlgoBaseTest({}) + }) + + test('Connect using default setting for requester and numalgo 2 for responder', async () => { + await didExchangeNumAlgoBaseTest({ responderNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc }) + }) + + test('Connect using numalgo 2 for requester and default setting for responder', async () => { + await didExchangeNumAlgoBaseTest({ requesterNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc }) + }) + + test('Connect using numalgo 2 for both requester and responder', async () => { + await didExchangeNumAlgoBaseTest({ + requesterNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, + responderNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, + }) + }) + + test('Connect using default setting for requester and numalgo 4 for responder', async () => { + await didExchangeNumAlgoBaseTest({ responderNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm }) + }) + + test('Connect using numalgo 4 for requester and default setting for responder', async () => { + await didExchangeNumAlgoBaseTest({ requesterNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm }) + }) + + test('Connect using numalgo 4 for both requester and responder', async () => { + await didExchangeNumAlgoBaseTest({ + requesterNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm, + responderNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm, + }) + }) + + test('Connect using an externally defined did for the requested', async () => { + await didExchangeNumAlgoBaseTest({ + createExternalDidForRequester: true, + }) + }) +}) + +async function didExchangeNumAlgoBaseTest(options: { + requesterNumAlgoSetting?: PeerDidNumAlgo + responderNumAlgoSetting?: PeerDidNumAlgo + createExternalDidForRequester?: boolean +}) { + // Make a common in-memory did registry for both agents + const didRegistry = new InMemoryDidRegistry() + + const aliceAgentOptions = getAgentOptions( + 'DID Exchange numalgo settings Alice', + { + label: 'alice', + endpoints: ['rxjs:alice'], + }, + { + ...getIndySdkModules(), + connections: new ConnectionsModule({ + autoAcceptConnections: false, + peerNumAlgoForDidExchangeRequests: options.requesterNumAlgoSetting, + }), + dids: new DidsModule({ registrars: [didRegistry], resolvers: [didRegistry] }), + } + ) + const faberAgentOptions = getAgentOptions( + 'DID Exchange numalgo settings Alice', + { + endpoints: ['rxjs:faber'], + }, + { + ...getIndySdkModules(), + connections: new ConnectionsModule({ + autoAcceptConnections: false, + peerNumAlgoForDidExchangeRequests: options.responderNumAlgoSetting, + }), + dids: new DidsModule({ registrars: [didRegistry], resolvers: [didRegistry] }), + } + ) + + const aliceAgent = new Agent(aliceAgentOptions) + const faberAgent = new Agent(faberAgentOptions) + + setupSubjectTransports([aliceAgent, faberAgent]) + await aliceAgent.initialize() + await faberAgent.initialize() + + const faberOutOfBandRecord = await faberAgent.oob.createInvitation({ + autoAcceptConnection: false, + multiUseInvitation: false, + }) + + const waitForAliceRequest = waitForRequest(faberAgent, 'alice') + + let ourDid, routing + if (options.createExternalDidForRequester) { + // Create did externally + const didRouting = await aliceAgent.mediationRecipient.getRouting({}) + ourDid = `did:inmemory:${uuid()}` + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ]) + didDocument.id = ourDid + + await aliceAgent.dids.create({ + did: ourDid, + didDocument, + }) + } + + let { connectionRecord: aliceConnectionRecord } = await aliceAgent.oob.receiveInvitation( + faberOutOfBandRecord.outOfBandInvitation, + { + autoAcceptInvitation: true, + autoAcceptConnection: false, + routing, + ourDid, + } + ) + + let faberAliceConnectionRecord = await waitForAliceRequest + + const waitForAliceResponse = waitForResponse(aliceAgent, aliceConnectionRecord!.id) + + await faberAgent.connections.acceptRequest(faberAliceConnectionRecord.id) + + aliceConnectionRecord = await waitForAliceResponse + await aliceAgent.connections.acceptResponse(aliceConnectionRecord!.id) + + aliceConnectionRecord = await aliceAgent.connections.returnWhenIsConnected(aliceConnectionRecord!.id) + faberAliceConnectionRecord = await faberAgent.connections.returnWhenIsConnected(faberAliceConnectionRecord!.id) + + expect(aliceConnectionRecord).toBeConnectedWith(faberAliceConnectionRecord) + + await aliceAgent.wallet.delete() + await aliceAgent.shutdown() + + await faberAgent.wallet.delete() + await faberAgent.shutdown() +} diff --git a/packages/core/src/modules/connections/handlers/DidRotateAckHandler.ts b/packages/core/src/modules/connections/handlers/DidRotateAckHandler.ts new file mode 100644 index 0000000000..ec05c71e8e --- /dev/null +++ b/packages/core/src/modules/connections/handlers/DidRotateAckHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidRotateService } from '../services' + +import { DidRotateAckMessage } from '../messages' + +export class DidRotateAckHandler implements MessageHandler { + private didRotateService: DidRotateService + public supportedMessages = [DidRotateAckMessage] + + public constructor(didRotateService: DidRotateService) { + this.didRotateService = didRotateService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + await this.didRotateService.processRotateAck(inboundMessage) + } +} diff --git a/packages/core/src/modules/connections/handlers/DidRotateHandler.ts b/packages/core/src/modules/connections/handlers/DidRotateHandler.ts new file mode 100644 index 0000000000..9533253572 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/DidRotateHandler.ts @@ -0,0 +1,26 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidRotateService } from '../services' +import type { ConnectionService } from '../services/ConnectionService' + +import { AriesFrameworkError } from '../../../error' +import { DidRotateMessage } from '../messages' + +export class DidRotateHandler implements MessageHandler { + private didRotateService: DidRotateService + private connectionService: ConnectionService + public supportedMessages = [DidRotateMessage] + + public constructor(didRotateService: DidRotateService, connectionService: ConnectionService) { + this.didRotateService = didRotateService + this.connectionService = connectionService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const { connection, recipientKey } = messageContext + if (!connection) { + throw new AriesFrameworkError(`Connection for verkey ${recipientKey?.fingerprint} not found!`) + } + + return this.didRotateService.processRotate(messageContext) + } +} diff --git a/packages/core/src/modules/connections/handlers/DidRotateProblemReportHandler.ts b/packages/core/src/modules/connections/handlers/DidRotateProblemReportHandler.ts new file mode 100644 index 0000000000..2f68e748bd --- /dev/null +++ b/packages/core/src/modules/connections/handlers/DidRotateProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidRotateService } from '../services' + +import { DidRotateProblemReportMessage } from '../messages' + +export class DidRotateProblemReportHandler implements MessageHandler { + private didRotateService: DidRotateService + public supportedMessages = [DidRotateProblemReportMessage] + + public constructor(didRotateService: DidRotateService) { + this.didRotateService = didRotateService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.didRotateService.processProblemReport(messageContext) + } +} diff --git a/packages/core/src/modules/connections/handlers/HangupHandler.ts b/packages/core/src/modules/connections/handlers/HangupHandler.ts new file mode 100644 index 0000000000..5e66ee2944 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/HangupHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidRotateService } from '../services' + +import { HangupMessage } from '../messages' + +export class HangupHandler implements MessageHandler { + private didRotateService: DidRotateService + public supportedMessages = [HangupMessage] + + public constructor(didRotateService: DidRotateService) { + this.didRotateService = didRotateService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + await this.didRotateService.processHangup(inboundMessage) + } +} diff --git a/packages/core/src/modules/connections/handlers/index.ts b/packages/core/src/modules/connections/handlers/index.ts index edd1a26766..0aeb955bdc 100644 --- a/packages/core/src/modules/connections/handlers/index.ts +++ b/packages/core/src/modules/connections/handlers/index.ts @@ -7,3 +7,7 @@ export * from './DidExchangeRequestHandler' export * from './DidExchangeResponseHandler' export * from './DidExchangeCompleteHandler' export * from './ConnectionProblemReportHandler' +export * from './DidRotateHandler' +export * from './DidRotateAckHandler' +export * from './DidRotateProblemReportHandler' +export * from './HangupHandler' diff --git a/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts index 3c142e76e8..754f049a71 100644 --- a/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts +++ b/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts @@ -26,5 +26,5 @@ export class DidExchangeCompleteMessage extends AgentMessage { @IsValidMessageType(DidExchangeCompleteMessage.type) public readonly type = DidExchangeCompleteMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/complete') + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/complete') } diff --git a/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts index ec8baf9880..3f948aa768 100644 --- a/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts +++ b/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts @@ -15,5 +15,5 @@ export class DidExchangeProblemReportMessage extends ProblemReportMessage { @IsValidMessageType(DidExchangeProblemReportMessage.type) public readonly type = DidExchangeProblemReportMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/problem-report') + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/problem-report') } diff --git a/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts index c22729a272..353ac079a2 100644 --- a/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts +++ b/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts @@ -42,7 +42,7 @@ export class DidExchangeRequestMessage extends AgentMessage { @IsValidMessageType(DidExchangeRequestMessage.type) public readonly type = DidExchangeRequestMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/request') + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/request') @IsString() public readonly label?: string diff --git a/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts index d0de9b6ec8..fe62a6393a 100644 --- a/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts +++ b/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts @@ -1,5 +1,5 @@ import { Type, Expose } from 'class-transformer' -import { IsString, ValidateNested } from 'class-validator' +import { IsOptional, IsString, ValidateNested } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' import { Attachment } from '../../../decorators/attachment/Attachment' @@ -36,13 +36,20 @@ export class DidExchangeResponseMessage extends AgentMessage { @IsValidMessageType(DidExchangeResponseMessage.type) public readonly type = DidExchangeResponseMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/response') + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/response') @IsString() public readonly did!: string @Expose({ name: 'did_doc~attach' }) + @IsOptional() @Type(() => Attachment) @ValidateNested() public didDoc?: Attachment + + @Expose({ name: 'did_rotate~attach' }) + @IsOptional() + @Type(() => Attachment) + @ValidateNested() + public didRotate?: Attachment } diff --git a/packages/core/src/modules/connections/messages/DidRotateAckMessage.ts b/packages/core/src/modules/connections/messages/DidRotateAckMessage.ts new file mode 100644 index 0000000000..363c7b1e45 --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidRotateAckMessage.ts @@ -0,0 +1,20 @@ +import type { AckMessageOptions } from '../../common/messages/AckMessage' + +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { AckMessage } from '../../common/messages/AckMessage' + +export type DidRotateAckMessageOptions = AckMessageOptions + +export class DidRotateAckMessage extends AckMessage { + /** + * Create new CredentialAckMessage instance. + * @param options + */ + public constructor(options: DidRotateAckMessageOptions) { + super(options) + } + + @IsValidMessageType(DidRotateAckMessage.type) + public readonly type = DidRotateAckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/did-rotate/1.0/ack') +} diff --git a/packages/core/src/modules/connections/messages/DidRotateMessage.ts b/packages/core/src/modules/connections/messages/DidRotateMessage.ts new file mode 100644 index 0000000000..a33db94a71 --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidRotateMessage.ts @@ -0,0 +1,38 @@ +import { Expose } from 'class-transformer' +import { IsString } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface DidRotateMessageOptions { + id?: string + toDid: string +} + +/** + * Message to communicate the DID a party wish to rotate to. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0794-did-rotate#rotate + */ +export class DidRotateMessage extends AgentMessage { + /** + * Create new RotateMessage instance. + * @param options + */ + public constructor(options: DidRotateMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.toDid = options.toDid + } + } + + @IsValidMessageType(DidRotateMessage.type) + public readonly type = DidRotateMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/did-rotate/1.0/rotate') + + @Expose({ name: 'to_did' }) + @IsString() + public readonly toDid!: string +} diff --git a/packages/core/src/modules/connections/messages/DidRotateProblemReportMessage.ts b/packages/core/src/modules/connections/messages/DidRotateProblemReportMessage.ts new file mode 100644 index 0000000000..2eaf0c027d --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidRotateProblemReportMessage.ts @@ -0,0 +1,19 @@ +import type { ProblemReportMessageOptions } from '../../problem-reports/messages/ProblemReportMessage' + +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { ProblemReportMessage } from '../../problem-reports/messages/ProblemReportMessage' + +export type DidRotateProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class DidRotateProblemReportMessage extends ProblemReportMessage { + public constructor(options: DidRotateProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(DidRotateProblemReportMessage.type) + public readonly type = DidRotateProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/did-rotate/1.0/problem-report') +} diff --git a/packages/core/src/modules/connections/messages/HangupMessage.ts b/packages/core/src/modules/connections/messages/HangupMessage.ts new file mode 100644 index 0000000000..b9ab2bd510 --- /dev/null +++ b/packages/core/src/modules/connections/messages/HangupMessage.ts @@ -0,0 +1,30 @@ +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface HangupMessageOptions { + id?: string +} + +/** + * This message is sent by the rotating_party to inform the observing_party that they are done + * with the relationship and will no longer be responding. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0794-did-rotate#hangup + */ +export class HangupMessage extends AgentMessage { + /** + * Create new HangupMessage instance. + * @param options + */ + public constructor(options: HangupMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + } + } + + @IsValidMessageType(HangupMessage.type) + public readonly type = HangupMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/did-rotate/1.0/hangup') +} diff --git a/packages/core/src/modules/connections/messages/index.ts b/packages/core/src/modules/connections/messages/index.ts index 7507e5ed56..1a3acdf7c8 100644 --- a/packages/core/src/modules/connections/messages/index.ts +++ b/packages/core/src/modules/connections/messages/index.ts @@ -8,3 +8,7 @@ export * from './DidExchangeRequestMessage' export * from './DidExchangeResponseMessage' export * from './DidExchangeCompleteMessage' export * from './DidExchangeProblemReportMessage' +export * from './DidRotateProblemReportMessage' +export * from './DidRotateMessage' +export * from './DidRotateAckMessage' +export * from './HangupMessage' diff --git a/packages/core/src/modules/connections/models/DidRotateRole.ts b/packages/core/src/modules/connections/models/DidRotateRole.ts new file mode 100644 index 0000000000..310c124ed4 --- /dev/null +++ b/packages/core/src/modules/connections/models/DidRotateRole.ts @@ -0,0 +1,4 @@ +export enum DidRotateRole { + RotatingParty = 'rotating_party', + ObservingParty = 'observing_party', +} diff --git a/packages/core/src/modules/connections/models/HandshakeProtocol.ts b/packages/core/src/modules/connections/models/HandshakeProtocol.ts index a433bd87f5..cee69c7fcd 100644 --- a/packages/core/src/modules/connections/models/HandshakeProtocol.ts +++ b/packages/core/src/modules/connections/models/HandshakeProtocol.ts @@ -1,4 +1,4 @@ export enum HandshakeProtocol { Connections = 'https://didcomm.org/connections/1.0', - DidExchange = 'https://didcomm.org/didexchange/1.0', + DidExchange = 'https://didcomm.org/didexchange/1.1', } diff --git a/packages/core/src/modules/connections/models/index.ts b/packages/core/src/modules/connections/models/index.ts index 69752df9c7..72a4635768 100644 --- a/packages/core/src/modules/connections/models/index.ts +++ b/packages/core/src/modules/connections/models/index.ts @@ -3,6 +3,7 @@ export * from './ConnectionRole' export * from './ConnectionState' export * from './DidExchangeState' export * from './DidExchangeRole' +export * from './DidRotateRole' export * from './HandshakeProtocol' export * from './did' export * from './ConnectionType' diff --git a/packages/core/src/modules/connections/repository/ConnectionMetadataTypes.ts b/packages/core/src/modules/connections/repository/ConnectionMetadataTypes.ts index 9609097515..f16ea2d043 100644 --- a/packages/core/src/modules/connections/repository/ConnectionMetadataTypes.ts +++ b/packages/core/src/modules/connections/repository/ConnectionMetadataTypes.ts @@ -1,9 +1,15 @@ export enum ConnectionMetadataKeys { UseDidKeysForProtocol = '_internal/useDidKeysForProtocol', + DidRotate = '_internal/didRotate', } export type ConnectionMetadata = { [ConnectionMetadataKeys.UseDidKeysForProtocol]: { [protocolUri: string]: boolean } + [ConnectionMetadataKeys.DidRotate]: { + did: string + threadId: string + mediatorId?: string + } } diff --git a/packages/core/src/modules/connections/repository/ConnectionRecord.ts b/packages/core/src/modules/connections/repository/ConnectionRecord.ts index f05106e5c9..2f0fa23059 100644 --- a/packages/core/src/modules/connections/repository/ConnectionRecord.ts +++ b/packages/core/src/modules/connections/repository/ConnectionRecord.ts @@ -27,6 +27,8 @@ export interface ConnectionRecordProps { outOfBandId?: string invitationDid?: string connectionTypes?: Array + previousDids?: Array + previousTheirDids?: Array } export type CustomConnectionTags = TagsBase @@ -40,6 +42,8 @@ export type DefaultConnectionTags = { outOfBandId?: string invitationDid?: string connectionTypes?: Array + previousDids?: Array + previousTheirDids?: Array } export class ConnectionRecord @@ -66,6 +70,8 @@ export class ConnectionRecord public invitationDid?: string public connectionTypes: string[] = [] + public previousDids: string[] = [] + public previousTheirDids: string[] = [] public static readonly type = 'ConnectionRecord' public readonly type = ConnectionRecord.type @@ -92,6 +98,8 @@ export class ConnectionRecord this.protocol = props.protocol this.outOfBandId = props.outOfBandId this.connectionTypes = props.connectionTypes ?? [] + this.previousDids = props.previousDids ?? [] + this.previousTheirDids = props.previousTheirDids ?? [] } } @@ -107,6 +115,8 @@ export class ConnectionRecord outOfBandId: this.outOfBandId, invitationDid: this.invitationDid, connectionTypes: this.connectionTypes, + previousDids: this.previousDids, + previousTheirDids: this.previousTheirDids, } } diff --git a/packages/core/src/modules/connections/repository/ConnectionRepository.ts b/packages/core/src/modules/connections/repository/ConnectionRepository.ts index 071d7e90cb..158c75e7a8 100644 --- a/packages/core/src/modules/connections/repository/ConnectionRepository.ts +++ b/packages/core/src/modules/connections/repository/ConnectionRepository.ts @@ -20,8 +20,14 @@ export class ConnectionRepository extends Repository { public async findByDids(agentContext: AgentContext, { ourDid, theirDid }: { ourDid: string; theirDid: string }) { return this.findSingleByQuery(agentContext, { - did: ourDid, - theirDid, + $or: [ + { + did: ourDid, + theirDid, + }, + { did: ourDid, previousTheirDids: [theirDid] }, + { previousDids: [ourDid], theirDid }, + ], }) } diff --git a/packages/core/src/modules/connections/repository/__tests__/ConnectionRecord.test.ts b/packages/core/src/modules/connections/repository/__tests__/ConnectionRecord.test.ts index 229a99d1e6..e052bfc594 100644 --- a/packages/core/src/modules/connections/repository/__tests__/ConnectionRecord.test.ts +++ b/packages/core/src/modules/connections/repository/__tests__/ConnectionRecord.test.ts @@ -25,6 +25,8 @@ describe('ConnectionRecord', () => { outOfBandId: 'a-out-of-band-id', invitationDid: 'a-invitation-did', connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) }) diff --git a/packages/core/src/modules/connections/services/ConnectionService.ts b/packages/core/src/modules/connections/services/ConnectionService.ts index 05ec71bc77..b87c4f6a33 100644 --- a/packages/core/src/modules/connections/services/ConnectionService.ts +++ b/packages/core/src/modules/connections/services/ConnectionService.ts @@ -898,7 +898,10 @@ export class ConnectionService { filterContextCorrelationId(agentContext.contextCorrelationId), map((e) => e.payload.connectionRecord), first(isConnected), // Do not wait for longer than specified timeout - timeout(timeoutMs) + timeout({ + first: timeoutMs, + meta: 'ConnectionService.returnWhenIsConnected', + }) ) .subscribe(subject) diff --git a/packages/core/src/modules/connections/services/DidRotateService.ts b/packages/core/src/modules/connections/services/DidRotateService.ts new file mode 100644 index 0000000000..23b9a5b7d5 --- /dev/null +++ b/packages/core/src/modules/connections/services/DidRotateService.ts @@ -0,0 +1,276 @@ +import type { Routing } from './ConnectionService' +import type { AgentContext } from '../../../agent' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { ConnectionRecord } from '../repository/ConnectionRecord' + +import { OutboundMessageContext } from '../../../agent/models' +import { InjectionSymbols } from '../../../constants' +import { AriesFrameworkError } from '../../../error' +import { Logger } from '../../../logger' +import { inject, injectable } from '../../../plugins' +import { AckStatus } from '../../common' +import { + DidRepository, + DidResolverService, + PeerDidNumAlgo, + getAlternativeDidsForPeerDid, + getNumAlgoFromPeerDid, + isValidPeerDid, +} from '../../dids' +import { getMediationRecordForDidDocument } from '../../routing/services/helpers' +import { ConnectionsModuleConfig } from '../ConnectionsModuleConfig' +import { DidRotateMessage, DidRotateAckMessage, DidRotateProblemReportMessage, HangupMessage } from '../messages' +import { ConnectionMetadataKeys } from '../repository/ConnectionMetadataTypes' + +import { ConnectionService } from './ConnectionService' +import { createPeerDidFromServices, getDidDocumentForCreatedDid, routingToServices } from './helpers' + +@injectable() +export class DidRotateService { + private didResolverService: DidResolverService + private logger: Logger + + public constructor(didResolverService: DidResolverService, @inject(InjectionSymbols.Logger) logger: Logger) { + this.didResolverService = didResolverService + this.logger = logger + } + + public async createRotate( + agentContext: AgentContext, + options: { connection: ConnectionRecord; toDid?: string; routing?: Routing } + ) { + const { connection, toDid, routing } = options + + const config = agentContext.dependencyManager.resolve(ConnectionsModuleConfig) + + // Do not allow to receive concurrent did rotation flows + const didRotateMetadata = connection.metadata.get(ConnectionMetadataKeys.DidRotate) + + if (didRotateMetadata) { + throw new AriesFrameworkError( + `There is already an existing opened did rotation flow for connection id ${connection.id}` + ) + } + + let didDocument, mediatorId + // If did is specified, make sure we have all key material for it + if (toDid) { + didDocument = await getDidDocumentForCreatedDid(agentContext, toDid) + mediatorId = (await getMediationRecordForDidDocument(agentContext, didDocument))?.id + + // Otherwise, create a did:peer based on the provided routing + } else { + if (!routing) { + throw new AriesFrameworkError('Routing configuration must be defined when rotating to a new peer did') + } + + didDocument = await createPeerDidFromServices( + agentContext, + routingToServices(routing), + config.peerNumAlgoForDidRotation + ) + mediatorId = routing.mediatorId + } + + const message = new DidRotateMessage({ toDid: didDocument.id }) + + // We set new info into connection metadata for further 'sealing' it once we receive an acknowledge + // All messages sent in-between will be using previous connection information + connection.metadata.set(ConnectionMetadataKeys.DidRotate, { + threadId: message.threadId, + did: didDocument.id, + mediatorId, + }) + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + + return message + } + + public async createHangup(agentContext: AgentContext, options: { connection: ConnectionRecord }) { + const { connection } = options + + const message = new HangupMessage({}) + + // Remove did to indicate termination status for this connection + if (connection.did) { + connection.previousDids = [...connection.previousDids, connection.did] + } + + connection.did = undefined + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + + return message + } + + /** + * Process a Hangup message and mark connection's theirDid as undefined so it is effectively terminated. + * Connection Record itself is not deleted (TODO: config parameter to automatically do so) + * + * Its previous did will be stored in record in order to be able to recognize any message received + * afterwards. + * + * @param messageContext + */ + public async processHangup(messageContext: InboundMessageContext) { + const connection = messageContext.assertReadyConnection() + const { agentContext } = messageContext + + if (connection.theirDid) { + connection.previousTheirDids = [...connection.previousTheirDids, connection.theirDid] + } + + connection.theirDid = undefined + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + } + + /** + * Process an incoming DID Rotate message and update connection if success. Any acknowledge + * or problem report will be sent to the prior DID, so the created context will take former + * connection record data + * + * @param param + * @param connection + * @returns + */ + public async processRotate(messageContext: InboundMessageContext) { + const connection = messageContext.assertReadyConnection() + const { message, agentContext } = messageContext + + // Check and store their new did + const newDid = message.toDid + + // DID Rotation not supported for peer:1 dids, as we need explicit did document information + if (isValidPeerDid(newDid) && getNumAlgoFromPeerDid(newDid) === PeerDidNumAlgo.GenesisDoc) { + this.logger.error(`Unable to resolve DID Document for '${newDid}`) + + const response = new DidRotateProblemReportMessage({ + description: { en: 'DID Method Unsupported', code: 'e.did.method_unsupported' }, + }) + return new OutboundMessageContext(response, { agentContext, connection }) + } + + const didDocument = (await this.didResolverService.resolve(agentContext, newDid)).didDocument + + // Cannot resolve did + if (!didDocument) { + this.logger.error(`Unable to resolve DID Document for '${newDid}`) + + const response = new DidRotateProblemReportMessage({ + description: { en: 'DID Unresolvable', code: 'e.did.unresolvable' }, + }) + return new OutboundMessageContext(response, { agentContext, connection }) + } + + // Did is resolved but no compatible DIDComm services found + if (!didDocument.didCommServices) { + const response = new DidRotateProblemReportMessage({ + description: { en: 'DID Document Unsupported', code: 'e.did.doc_unsupported' }, + }) + return new OutboundMessageContext(response, { agentContext, connection }) + } + + // Send acknowledge to previous did and persist new did. Previous did will be stored in connection record in + // order to still accept messages from it + const outboundMessageContext = new OutboundMessageContext( + new DidRotateAckMessage({ + threadId: message.threadId, + status: AckStatus.OK, + }), + { agentContext, connection: connection.clone() } + ) + + // Store received did and update connection for further message processing + await agentContext.dependencyManager.resolve(DidRepository).storeReceivedDid(agentContext, { + did: didDocument.id, + didDocument, + tags: { + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + + // For did:peer, store any alternative dids (like short form did:peer:4), + // it may have in order to relate any message referencing it + alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(didDocument.id) : undefined, + }, + }) + + if (connection.theirDid) { + connection.previousTheirDids = [...connection.previousTheirDids, connection.theirDid] + } + + connection.theirDid = newDid + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + + return outboundMessageContext + } + + public async processRotateAck(inboundMessage: InboundMessageContext) { + const { agentContext, message } = inboundMessage + + const connection = inboundMessage.assertReadyConnection() + + // Update connection info based on metadata set when creating the rotate message + const didRotateMetadata = connection.metadata.get(ConnectionMetadataKeys.DidRotate) + + if (!didRotateMetadata) { + throw new AriesFrameworkError(`No did rotation data found for connection with id '${connection.id}'`) + } + + if (didRotateMetadata.threadId !== message.threadId) { + throw new AriesFrameworkError( + `Existing did rotation flow thread id '${didRotateMetadata.threadId} does not match incoming message'` + ) + } + + // Store previous did in order to still accept out-of-order messages that arrived later using it + if (connection.did) connection.previousDids = [...connection.previousDids, connection.did] + + connection.did = didRotateMetadata.did + connection.mediatorId = didRotateMetadata.mediatorId + connection.metadata.delete(ConnectionMetadataKeys.DidRotate) + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + } + + /** + * Process a problem report related to did rotate protocol, by simply deleting any temporary metadata. + * + * No specific event is thrown other than generic message processing + * + * @param messageContext + */ + public async processProblemReport( + messageContext: InboundMessageContext + ): Promise { + const { message, agentContext } = messageContext + + const connection = messageContext.assertReadyConnection() + + this.logger.debug(`Processing problem report with id ${message.id}`) + + // Delete any existing did rotation metadata in order to 'reset' the connection + const didRotateMetadata = connection.metadata.get(ConnectionMetadataKeys.DidRotate) + + if (!didRotateMetadata) { + throw new AriesFrameworkError(`No did rotation data found for connection with id '${connection.id}'`) + } + + connection.metadata.delete(ConnectionMetadataKeys.DidRotate) + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + } + + public async clearDidRotationData(agentContext: AgentContext, connection: ConnectionRecord) { + const didRotateMetadata = connection.metadata.get(ConnectionMetadataKeys.DidRotate) + + if (!didRotateMetadata) { + throw new AriesFrameworkError(`No did rotation data found for connection with id '${connection.id}'`) + } + + connection.metadata.delete(ConnectionMetadataKeys.DidRotate) + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + } +} diff --git a/packages/core/src/modules/connections/services/helpers.ts b/packages/core/src/modules/connections/services/helpers.ts index 3dbcc9c837..e0dc91a93a 100644 --- a/packages/core/src/modules/connections/services/helpers.ts +++ b/packages/core/src/modules/connections/services/helpers.ts @@ -1,9 +1,20 @@ -import type { DidDocument } from '../../dids' +import type { Routing } from './ConnectionService' +import type { AgentContext } from '../../../agent' +import type { ResolvedDidCommService } from '../../didcomm' +import type { DidDocument, PeerDidNumAlgo } from '../../dids' import type { DidDoc, PublicKey } from '../models' import { Key, KeyType } from '../../../crypto' import { AriesFrameworkError } from '../../../error' -import { IndyAgentService, DidCommV1Service, DidDocumentBuilder, getEd25519VerificationKey2018 } from '../../dids' +import { + IndyAgentService, + DidCommV1Service, + DidDocumentBuilder, + getEd25519VerificationKey2018, + DidRepository, + DidsApi, + createPeerDidDocumentFromServices, +} from '../../dids' import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1' import { EmbeddedAuthentication } from '../models' @@ -109,3 +120,47 @@ function convertPublicKeyToVerificationMethod(publicKey: PublicKey) { controller: '#id', }) } + +export function routingToServices(routing: Routing): ResolvedDidCommService[] { + return routing.endpoints.map((endpoint, index) => ({ + id: `#inline-${index}`, + serviceEndpoint: endpoint, + recipientKeys: [routing.recipientKey], + routingKeys: routing.routingKeys, + })) +} + +export async function getDidDocumentForCreatedDid(agentContext: AgentContext, did: string) { + const didRecord = await agentContext.dependencyManager.resolve(DidRepository).findCreatedDid(agentContext, did) + + if (!didRecord?.didDocument) { + throw new AriesFrameworkError(`Could not get DidDocument for created did ${did}`) + } + return didRecord.didDocument +} + +export async function createPeerDidFromServices( + agentContext: AgentContext, + services: ResolvedDidCommService[], + numAlgo: PeerDidNumAlgo +) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + + // Create did document without the id property + const didDocument = createPeerDidDocumentFromServices(services) + // Register did:peer document. This will generate the id property and save it to a did record + + const result = await didsApi.create({ + method: 'peer', + didDocument, + options: { + numAlgo, + }, + }) + + if (result.didState?.state !== 'finished') { + throw new AriesFrameworkError(`Did document creation failed: ${JSON.stringify(result.didState)}`) + } + + return result.didState.didDocument +} diff --git a/packages/core/src/modules/connections/services/index.ts b/packages/core/src/modules/connections/services/index.ts index 3520b11146..db9fe13ac4 100644 --- a/packages/core/src/modules/connections/services/index.ts +++ b/packages/core/src/modules/connections/services/index.ts @@ -1,2 +1,3 @@ export * from './ConnectionService' +export * from './DidRotateService' export * from './TrustPingService' diff --git a/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts index ca1b1a88d4..e51a55c5ef 100644 --- a/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts +++ b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts @@ -29,7 +29,7 @@ export class DidCommDocumentService { // FIXME: we currently retrieve did documents for all didcomm services in the did document, and we don't have caching // yet so this will re-trigger ledger resolves for each one. Should we only resolve the first service, then the second service, etc...? for (const didCommService of didCommServices) { - if (didCommService instanceof IndyAgentService) { + if (didCommService.type === IndyAgentService.type) { // IndyAgentService (DidComm v0) has keys encoded as raw publicKeyBase58 (verkeys) resolvedServices.push({ id: didCommService.id, @@ -37,7 +37,7 @@ export class DidCommDocumentService { routingKeys: didCommService.routingKeys?.map(verkeyToInstanceOfKey) || [], serviceEndpoint: didCommService.serviceEndpoint, }) - } else if (didCommService instanceof DidCommV1Service) { + } else if (didCommService.type === DidCommV1Service.type) { // Resolve dids to DIDDocs to retrieve routingKeys const routingKeys = [] for (const routingKey of didCommService.routingKeys ?? []) { diff --git a/packages/core/src/modules/dids/DidsApi.ts b/packages/core/src/modules/dids/DidsApi.ts index e20ef573e2..4f0cf294bf 100644 --- a/packages/core/src/modules/dids/DidsApi.ts +++ b/packages/core/src/modules/dids/DidsApi.ts @@ -15,6 +15,7 @@ import { injectable } from '../../plugins' import { WalletKeyExistsError } from '../../wallet/error' import { DidsModuleConfig } from './DidsModuleConfig' +import { getAlternativeDidsForPeerDid, isValidPeerDid } from './methods' import { DidRepository } from './repository' import { DidRegistrarService, DidResolverService } from './services' @@ -157,6 +158,7 @@ export class DidsApi { existingDidRecord.didDocument = didDocument existingDidRecord.setTags({ recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(did) : undefined, }) await this.didRepository.update(this.agentContext, existingDidRecord) @@ -169,6 +171,7 @@ export class DidsApi { didDocument, tags: { recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(did) : undefined, }, }) } diff --git a/packages/core/src/modules/dids/__tests__/DidsApi.test.ts b/packages/core/src/modules/dids/__tests__/DidsApi.test.ts index 41993e9d43..8485899723 100644 --- a/packages/core/src/modules/dids/__tests__/DidsApi.test.ts +++ b/packages/core/src/modules/dids/__tests__/DidsApi.test.ts @@ -2,8 +2,16 @@ import { IndySdkModule } from '../../../../../indy-sdk/src' import { indySdk } from '../../../../tests' import { getAgentOptions } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' +import { isLongFormDidPeer4, isShortFormDidPeer4 } from '../methods/peer/peerDidNumAlgo4' -import { DidDocument, DidDocumentService, KeyType, TypedArrayEncoder } from '@aries-framework/core' +import { + DidDocument, + DidDocumentService, + KeyType, + PeerDidNumAlgo, + TypedArrayEncoder, + createPeerDidDocumentFromServices, +} from '@aries-framework/core' const agentOptions = getAgentOptions( 'DidsApi', @@ -227,4 +235,40 @@ describe('DidsApi', () => { ], }) }) + + test('create and resolve did:peer:4 in short and long form', async () => { + const routing = await agent.mediationRecipient.getRouting({}) + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [routing.recipientKey], + routingKeys: routing.routingKeys, + serviceEndpoint: routing.endpoints[0], + }, + ]) + + const result = await agent.dids.create({ + method: 'peer', + didDocument, + options: { + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm, + }, + }) + + const longFormDid = result.didState.did + const shortFormDid = result.didState.didDocument?.alsoKnownAs + ? result.didState.didDocument?.alsoKnownAs[0] + : undefined + + if (!longFormDid) fail('Long form did not defined') + if (!shortFormDid) fail('Short form did not defined') + + expect(isLongFormDidPeer4(longFormDid)).toBeTruthy() + expect(isShortFormDidPeer4(shortFormDid)).toBeTruthy() + + const didDocumentFromLongFormDid = await agent.dids.resolveDidDocument(longFormDid) + const didDocumentFromShortFormDid = await agent.dids.resolveDidDocument(shortFormDid) + + expect(didDocumentFromLongFormDid).toEqual(didDocumentFromShortFormDid) + }) }) diff --git a/packages/core/src/modules/dids/domain/DidDocument.ts b/packages/core/src/modules/dids/domain/DidDocument.ts index 08963613af..e67dbd6a1b 100644 --- a/packages/core/src/modules/dids/domain/DidDocument.ts +++ b/packages/core/src/modules/dids/domain/DidDocument.ts @@ -186,12 +186,12 @@ export class DidDocument { let recipientKeys: Key[] = [] for (const service of this.didCommServices) { - if (service instanceof IndyAgentService) { + if (service.type === IndyAgentService.type) { recipientKeys = [ ...recipientKeys, ...service.recipientKeys.map((publicKeyBase58) => Key.fromPublicKeyBase58(publicKeyBase58, KeyType.Ed25519)), ] - } else if (service instanceof DidCommV1Service) { + } else if (service.type === DidCommV1Service.type) { recipientKeys = [ ...recipientKeys, ...service.recipientKeys.map((recipientKey) => keyReferenceToKey(this, recipientKey)), diff --git a/packages/core/src/modules/dids/domain/DidResolver.ts b/packages/core/src/modules/dids/domain/DidResolver.ts index 050ea2cd97..6154030023 100644 --- a/packages/core/src/modules/dids/domain/DidResolver.ts +++ b/packages/core/src/modules/dids/domain/DidResolver.ts @@ -3,6 +3,8 @@ import type { ParsedDid, DidResolutionResult, DidResolutionOptions } from '../ty export interface DidResolver { readonly supportedMethods: string[] + readonly allowsCaching: boolean + resolve( agentContext: AgentContext, did: string, diff --git a/packages/core/src/modules/dids/methods/jwk/JwkDidResolver.ts b/packages/core/src/modules/dids/methods/jwk/JwkDidResolver.ts index 8207814895..5ac705c860 100644 --- a/packages/core/src/modules/dids/methods/jwk/JwkDidResolver.ts +++ b/packages/core/src/modules/dids/methods/jwk/JwkDidResolver.ts @@ -7,6 +7,11 @@ import { DidJwk } from './DidJwk' export class JwkDidResolver implements DidResolver { public readonly supportedMethods = ['jwk'] + /** + * No remote resolving done, did document is dynamically constructed. To not pollute the cache we don't allow caching + */ + public readonly allowsCaching = false + public async resolve(agentContext: AgentContext, did: string): Promise { const didDocumentMetadata = {} diff --git a/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts b/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts index 41f4a0e221..7719c29d1c 100644 --- a/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts +++ b/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts @@ -7,6 +7,11 @@ import { DidKey } from './DidKey' export class KeyDidResolver implements DidResolver { public readonly supportedMethods = ['key'] + /** + * No remote resolving done, did document is dynamically constructed. To not pollute the cache we don't allow caching + */ + public readonly allowsCaching = false + public async resolve(agentContext: AgentContext, did: string): Promise { const didDocumentMetadata = {} diff --git a/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts b/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts index fcae22119e..b1e9172dd9 100644 --- a/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts +++ b/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts @@ -9,19 +9,26 @@ import { DidDocument } from '../../domain' import { DidDocumentRole } from '../../domain/DidDocumentRole' import { DidRepository, DidRecord } from '../../repository' -import { PeerDidNumAlgo } from './didPeer' +import { PeerDidNumAlgo, getAlternativeDidsForPeerDid } from './didPeer' import { keyToNumAlgo0DidDocument } from './peerDidNumAlgo0' import { didDocumentJsonToNumAlgo1Did } from './peerDidNumAlgo1' import { didDocumentToNumAlgo2Did } from './peerDidNumAlgo2' +import { didDocumentToNumAlgo4Did } from './peerDidNumAlgo4' export class PeerDidRegistrar implements DidRegistrar { public readonly supportedMethods = ['peer'] public async create( agentContext: AgentContext, - options: PeerDidNumAlgo0CreateOptions | PeerDidNumAlgo1CreateOptions | PeerDidNumAlgo2CreateOptions + options: + | PeerDidNumAlgo0CreateOptions + | PeerDidNumAlgo1CreateOptions + | PeerDidNumAlgo2CreateOptions + | PeerDidNumAlgo4CreateOptions ): Promise { const didRepository = agentContext.dependencyManager.resolve(DidRepository) + + let did: string let didDocument: DidDocument try { @@ -50,16 +57,27 @@ export class PeerDidRegistrar implements DidRegistrar { // TODO: validate did:peer document didDocument = keyToNumAlgo0DidDocument(key) + did = didDocument.id } else if (isPeerDidNumAlgo1CreateOptions(options)) { const didDocumentJson = options.didDocument.toJSON() - const did = didDocumentJsonToNumAlgo1Did(didDocumentJson) + did = didDocumentJsonToNumAlgo1Did(didDocumentJson) didDocument = JsonTransformer.fromJSON({ ...didDocumentJson, id: did }, DidDocument) } else if (isPeerDidNumAlgo2CreateOptions(options)) { const didDocumentJson = options.didDocument.toJSON() - const did = didDocumentToNumAlgo2Did(options.didDocument) + did = didDocumentToNumAlgo2Did(options.didDocument) didDocument = JsonTransformer.fromJSON({ ...didDocumentJson, id: did }, DidDocument) + } else if (isPeerDidNumAlgo4CreateOptions(options)) { + const didDocumentJson = options.didDocument.toJSON() + + const { longFormDid, shortFormDid } = didDocumentToNumAlgo4Did(options.didDocument) + + did = longFormDid + didDocument = JsonTransformer.fromJSON( + { ...didDocumentJson, id: longFormDid, alsoKnownAs: [shortFormDid] }, + DidDocument + ) } else { return { didDocumentMetadata: {}, @@ -73,13 +91,14 @@ export class PeerDidRegistrar implements DidRegistrar { // Save the did so we know we created it and can use it for didcomm const didRecord = new DidRecord({ - did: didDocument.id, + did, role: DidDocumentRole.Created, didDocument: isPeerDidNumAlgo1CreateOptions(options) ? didDocument : undefined, tags: { // We need to save the recipientKeys, so we can find the associated did // of a key when we receive a message from another connection. recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + alternativeDids: getAlternativeDidsForPeerDid(did), }, }) await didRepository.save(agentContext, didRecord) @@ -149,10 +168,15 @@ function isPeerDidNumAlgo2CreateOptions(options: PeerDidCreateOptions): options return options.options.numAlgo === PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc } +function isPeerDidNumAlgo4CreateOptions(options: PeerDidCreateOptions): options is PeerDidNumAlgo4CreateOptions { + return options.options.numAlgo === PeerDidNumAlgo.ShortFormAndLongForm +} + export type PeerDidCreateOptions = | PeerDidNumAlgo0CreateOptions | PeerDidNumAlgo1CreateOptions | PeerDidNumAlgo2CreateOptions + | PeerDidNumAlgo4CreateOptions export interface PeerDidNumAlgo0CreateOptions extends DidCreateOptions { method: 'peer' @@ -188,6 +212,16 @@ export interface PeerDidNumAlgo2CreateOptions extends DidCreateOptions { secret?: undefined } +export interface PeerDidNumAlgo4CreateOptions extends DidCreateOptions { + method: 'peer' + did?: never + didDocument: DidDocument + options: { + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm + } + secret?: undefined +} + // Update and Deactivate not supported for did:peer export type PeerDidUpdateOptions = never export type PeerDidDeactivateOptions = never diff --git a/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts b/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts index fa3f2ed9d2..4e15bee8fd 100644 --- a/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts +++ b/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts @@ -9,10 +9,16 @@ import { DidRepository } from '../../repository' import { getNumAlgoFromPeerDid, isValidPeerDid, PeerDidNumAlgo } from './didPeer' import { didToNumAlgo0DidDocument } from './peerDidNumAlgo0' import { didToNumAlgo2DidDocument } from './peerDidNumAlgo2' +import { didToNumAlgo4DidDocument, isShortFormDidPeer4 } from './peerDidNumAlgo4' export class PeerDidResolver implements DidResolver { public readonly supportedMethods = ['peer'] + /** + * No remote resolving done, did document is fetched from storage. To not pollute the cache we don't allow caching + */ + public readonly allowsCaching = false + public async resolve(agentContext: AgentContext, did: string): Promise { const didRepository = agentContext.dependencyManager.resolve(DidRepository) @@ -48,9 +54,22 @@ export class PeerDidResolver implements DidResolver { didDocument = didDocumentRecord.didDocument } // For Method 2, generate from did - else { + else if (numAlgo === PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc) { didDocument = didToNumAlgo2DidDocument(did) } + // For Method 4, if short form is received, attempt to get the didDocument from stored record + else { + if (isShortFormDidPeer4(did)) { + const [didRecord] = await didRepository.findAllByDid(agentContext, did) + + if (!didRecord) { + throw new AriesFrameworkError(`No did record found for peer did ${did}.`) + } + didDocument = didToNumAlgo4DidDocument(didRecord.did) + } else { + didDocument = didToNumAlgo4DidDocument(did) + } + } return { didDocument, diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts index 99716995f5..160860bf03 100644 --- a/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts +++ b/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts @@ -9,10 +9,21 @@ describe('didPeer', () => { 'did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' ) ).toBe(true) + expect( + isValidPeerDid( + 'did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn:z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + ).toBe(true) + expect(isValidPeerDid('did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn')).toBe(true) + expect( + isValidPeerDid( + 'did:peer:4z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + ).toBe(false) expect( isValidPeerDid( - 'did:peer:4.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' + 'did:peer:5.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' ) ).toBe(false) }) @@ -35,6 +46,13 @@ describe('didPeer', () => { 'did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' ) ).toBe(PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc) + + // NumAlgo 4 + expect( + getNumAlgoFromPeerDid( + 'did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn:z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + ).toBe(PeerDidNumAlgo.ShortFormAndLongForm) }) }) }) diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts index 2daa05729b..2c1076f397 100644 --- a/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts +++ b/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts @@ -318,17 +318,110 @@ describe('DidRegistrar', () => { }) }) - it('should return an error state if an unsupported numAlgo is provided', async () => { - const result = await peerDidRegistrar.create( - agentContext, - // @ts-expect-error - this is not a valid numAlgo - { + describe('did:peer:4', () => { + const key = Key.fromFingerprint('z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz') + const verificationMethod = getEd25519VerificationKey2018({ + key, + controller: '#id', + // Use relative id for peer dids + id: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', + }) + + const didDocument = new DidDocumentBuilder('') + .addVerificationMethod(verificationMethod) + .addAuthentication(verificationMethod.id) + .addService( + new DidCommV1Service({ + id: '#service-0', + recipientKeys: [verificationMethod.id], + serviceEndpoint: 'https://example.com', + accept: ['didcomm/aip2;env=rfc19'], + }) + ) + .build() + + it('should correctly create a did:peer:4 document from a did document', async () => { + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + didDocument: didDocument, + options: { + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm, + }, + }) + + const longFormDid = + 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e' + const shortFormDid = 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4' + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: longFormDid, + didDocument: { + '@context': ['https://w3id.org/did/v1'], + id: longFormDid, + alsoKnownAs: [shortFormDid], + service: [ + { + serviceEndpoint: 'https://example.com', + type: 'did-communication', + priority: 0, + recipientKeys: ['#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16'], + accept: ['didcomm/aip2;env=rfc19'], + id: '#service-0', + }, + ], + verificationMethod: [ + { + id: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', + type: 'Ed25519VerificationKey2018', + controller: '#id', + publicKeyBase58: '7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE', + }, + ], + authentication: ['#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16'], + }, + secret: {}, + }, + }) + }) + + it('should store the did without the did document', async () => { + const longFormDid = + 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e' + const shortFormDid = 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4' + await peerDidRegistrar.create(agentContext, { method: 'peer', + didDocument, options: { - numAlgo: 4, + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm, + }, + }) + + expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) + const [, didRecord] = mockFunction(didRepositoryMock.save).mock.calls[0] + + expect(didRecord).toMatchObject({ + did: longFormDid, + role: DidDocumentRole.Created, + _tags: { + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + alternativeDids: [shortFormDid], }, - } - ) + didDocument: undefined, + }) + }) + }) + + it('should return an error state if an unsupported numAlgo is provided', async () => { + // @ts-expect-error - this is not a valid numAlgo + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + options: { + numAlgo: 5, + }, + }) expect(JsonTransformer.toJSON(result)).toMatchObject({ didDocumentMetadata: {}, diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmUJdJ.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmUJdJ.json new file mode 100644 index 0000000000..79a8c2a0d1 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmUJdJ.json @@ -0,0 +1,24 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "verificationMethod": [ + { + "id": "#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE", + "controller": "did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e" + } + ], + "service": [ + { + "id": "#service-0", + "serviceEndpoint": "https://example.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16"], + "accept": ["didcomm/aip2;env=rfc19"] + } + ], + "authentication": ["#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16"], + "id": "did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e", + "alsoKnownAs": ["did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4"] +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmd8Cp.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmd8Cp.json new file mode 100644 index 0000000000..fc0529d74e --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmd8Cp.json @@ -0,0 +1,39 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/x25519-2020/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "verificationMethod": [ + { + "id": "#6LSqPZfn", + "type": "X25519KeyAgreementKey2020", + "publicKeyMultibase": "z6LSqPZfn9krvgXma2icTMKf2uVcYhKXsudCmPoUzqGYW24U", + "controller": "did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd:z2M1k7h4psgp4CmJcnQn2Ljp7Pz7ktsd7oBhMU3dWY5s4fhFNj17qcRTQ427C7QHNT6cQ7T3XfRh35Q2GhaNFZmWHVFq4vL7F8nm36PA9Y96DvdrUiRUaiCuXnBFrn1o7mxFZAx14JL4t8vUWpuDPwQuddVo1T8myRiVH7wdxuoYbsva5x6idEpCQydJdFjiHGCpNc2UtjzPQ8awSXkctGCnBmgkhrj5gto3D4i3EREXYq4Z8r2cWGBr2UzbSmnxW2BuYddFo9Yfm6mKjtJyLpF74ytqrF5xtf84MnGFg1hMBmh1xVx1JwjZ2BeMJs7mNS8DTZhKC7KH38EgqDtUZzfjhpjmmUfkXg2KFEA3EGbbVm1DPqQXayPYKAsYPS9AyKkcQ3fzWafLPP93UfNhtUPL8JW5pMcSV3P8v6j3vPXqnnGknNyBprD6YGUVtgLiAqDBDUF3LSxFQJCVYYtghMTv8WuSw9h1a1SRFrDQLGHE4UrkgoRvwaGWr64aM87T1eVGkP5Dt4L1AbboeK2ceLArPScrdYGTpi3BpTkLwZCdjdiFSfTy9okL1YNRARqUf2wm8DvkVGUU7u5nQA3ZMaXWJAewk6k1YUxKd7LvofGUK4YEDtoxN5vb6r1Q2godrGqaPkjfL3RoYPpDYymf9XhcgG8Kx3DZaA6cyTs24t45KxYAfeCw4wqUpCH9HbpD78TbEUr9PPAsJgXBvBj2VVsxnr7FKbK4KykGcg1W8M1JPz21Z4Y72LWgGQCmixovrkHktcTX1uNHjAvKBqVD5C7XmVfHgXCHj7djCh3vzLNuVLtEED8J1hhqsB1oCBGiuh3xXr7fZ9wUjJCQ1HYHqxLJKdYKtoCiPmgKM7etVftXkmTFETZmpM19aRyih3bao76LdpQtbw636r7a3qt8v4WfxsXJetSL8c7t24SqQBcAY89FBsbEnFNrQCMK3JEseKHVaU388ctvRD45uQfe5GndFxthj4iSDomk4uRFd1uRbywoP1tRuabHTDX42UxPjz" + }, + { + "id": "#6MkrCD1c", + "type": "Ed25519VerificationKey2020", + "publicKeyMultibase": "z6MkrCD1csqtgdj8sjrsu8jxcbeyP6m7LiK87NzhfWqio5yr", + "controller": "did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd:z2M1k7h4psgp4CmJcnQn2Ljp7Pz7ktsd7oBhMU3dWY5s4fhFNj17qcRTQ427C7QHNT6cQ7T3XfRh35Q2GhaNFZmWHVFq4vL7F8nm36PA9Y96DvdrUiRUaiCuXnBFrn1o7mxFZAx14JL4t8vUWpuDPwQuddVo1T8myRiVH7wdxuoYbsva5x6idEpCQydJdFjiHGCpNc2UtjzPQ8awSXkctGCnBmgkhrj5gto3D4i3EREXYq4Z8r2cWGBr2UzbSmnxW2BuYddFo9Yfm6mKjtJyLpF74ytqrF5xtf84MnGFg1hMBmh1xVx1JwjZ2BeMJs7mNS8DTZhKC7KH38EgqDtUZzfjhpjmmUfkXg2KFEA3EGbbVm1DPqQXayPYKAsYPS9AyKkcQ3fzWafLPP93UfNhtUPL8JW5pMcSV3P8v6j3vPXqnnGknNyBprD6YGUVtgLiAqDBDUF3LSxFQJCVYYtghMTv8WuSw9h1a1SRFrDQLGHE4UrkgoRvwaGWr64aM87T1eVGkP5Dt4L1AbboeK2ceLArPScrdYGTpi3BpTkLwZCdjdiFSfTy9okL1YNRARqUf2wm8DvkVGUU7u5nQA3ZMaXWJAewk6k1YUxKd7LvofGUK4YEDtoxN5vb6r1Q2godrGqaPkjfL3RoYPpDYymf9XhcgG8Kx3DZaA6cyTs24t45KxYAfeCw4wqUpCH9HbpD78TbEUr9PPAsJgXBvBj2VVsxnr7FKbK4KykGcg1W8M1JPz21Z4Y72LWgGQCmixovrkHktcTX1uNHjAvKBqVD5C7XmVfHgXCHj7djCh3vzLNuVLtEED8J1hhqsB1oCBGiuh3xXr7fZ9wUjJCQ1HYHqxLJKdYKtoCiPmgKM7etVftXkmTFETZmpM19aRyih3bao76LdpQtbw636r7a3qt8v4WfxsXJetSL8c7t24SqQBcAY89FBsbEnFNrQCMK3JEseKHVaU388ctvRD45uQfe5GndFxthj4iSDomk4uRFd1uRbywoP1tRuabHTDX42UxPjz" + } + ], + "service": [ + { + "id": "#didcommmessaging-0", + "type": "DIDCommMessaging", + "serviceEndpoint": { + "uri": "didcomm:transport/queue", + "accept": ["didcomm/v2"], + "routingKeys": [] + } + } + ], + "authentication": ["#6MkrCD1c"], + "keyAgreement": ["#6LSqPZfn"], + "assertionMethod": ["#6MkrCD1c"], + "capabilityDelegation": ["#6MkrCD1c"], + "capabilityInvocation": ["#6MkrCD1c"], + "alsoKnownAs": ["did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd"], + "id": "did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd:z2M1k7h4psgp4CmJcnQn2Ljp7Pz7ktsd7oBhMU3dWY5s4fhFNj17qcRTQ427C7QHNT6cQ7T3XfRh35Q2GhaNFZmWHVFq4vL7F8nm36PA9Y96DvdrUiRUaiCuXnBFrn1o7mxFZAx14JL4t8vUWpuDPwQuddVo1T8myRiVH7wdxuoYbsva5x6idEpCQydJdFjiHGCpNc2UtjzPQ8awSXkctGCnBmgkhrj5gto3D4i3EREXYq4Z8r2cWGBr2UzbSmnxW2BuYddFo9Yfm6mKjtJyLpF74ytqrF5xtf84MnGFg1hMBmh1xVx1JwjZ2BeMJs7mNS8DTZhKC7KH38EgqDtUZzfjhpjmmUfkXg2KFEA3EGbbVm1DPqQXayPYKAsYPS9AyKkcQ3fzWafLPP93UfNhtUPL8JW5pMcSV3P8v6j3vPXqnnGknNyBprD6YGUVtgLiAqDBDUF3LSxFQJCVYYtghMTv8WuSw9h1a1SRFrDQLGHE4UrkgoRvwaGWr64aM87T1eVGkP5Dt4L1AbboeK2ceLArPScrdYGTpi3BpTkLwZCdjdiFSfTy9okL1YNRARqUf2wm8DvkVGUU7u5nQA3ZMaXWJAewk6k1YUxKd7LvofGUK4YEDtoxN5vb6r1Q2godrGqaPkjfL3RoYPpDYymf9XhcgG8Kx3DZaA6cyTs24t45KxYAfeCw4wqUpCH9HbpD78TbEUr9PPAsJgXBvBj2VVsxnr7FKbK4KykGcg1W8M1JPz21Z4Y72LWgGQCmixovrkHktcTX1uNHjAvKBqVD5C7XmVfHgXCHj7djCh3vzLNuVLtEED8J1hhqsB1oCBGiuh3xXr7fZ9wUjJCQ1HYHqxLJKdYKtoCiPmgKM7etVftXkmTFETZmpM19aRyih3bao76LdpQtbw636r7a3qt8v4WfxsXJetSL8c7t24SqQBcAY89FBsbEnFNrQCMK3JEseKHVaU388ctvRD45uQfe5GndFxthj4iSDomk4uRFd1uRbywoP1tRuabHTDX42UxPjz" +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo4.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo4.test.ts new file mode 100644 index 0000000000..60af15b4f2 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo4.test.ts @@ -0,0 +1,45 @@ +import { JsonTransformer } from '../../../../../utils' +import { OutOfBandDidCommService } from '../../../../oob/domain/OutOfBandDidCommService' +import { DidDocument } from '../../../domain' +import { didDocumentToNumAlgo4Did, didToNumAlgo4DidDocument, outOfBandServiceToNumAlgo4Did } from '../peerDidNumAlgo4' + +import didPeer4zQmUJdJ from './__fixtures__/didPeer4zQmUJdJ.json' +import didPeer4zQmd8Cp from './__fixtures__/didPeer4zQmd8Cp.json' + +describe('peerDidNumAlgo4', () => { + describe('didDocumentToNumAlgo4Did', () => { + test('transforms method 4 peer did to a did document', async () => { + expect(didToNumAlgo4DidDocument(didPeer4zQmd8Cp.id).toJSON()).toMatchObject(didPeer4zQmd8Cp) + }) + }) + + describe('didDocumentToNumAlgo4Did', () => { + test('transforms method 4 peer did document to a did', async () => { + const longFormDid = didPeer4zQmUJdJ.id + const shortFormDid = didPeer4zQmUJdJ.alsoKnownAs[0] + + const didDocument = JsonTransformer.fromJSON(didPeer4zQmUJdJ, DidDocument) + + expect(didDocumentToNumAlgo4Did(didDocument)).toEqual({ longFormDid, shortFormDid }) + }) + }) + + describe('outOfBandServiceToNumAlgo4Did', () => { + test('transforms a did comm service into a valid method 4 did', () => { + const service = new OutOfBandDidCommService({ + id: '#service-0', + serviceEndpoint: 'https://example.com/endpoint', + recipientKeys: ['did:key:z6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V'], + routingKeys: ['did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'], + accept: ['didcomm/v2', 'didcomm/aip2;env=rfc587'], + }) + const { longFormDid } = outOfBandServiceToNumAlgo4Did(service) + const peerDidDocument = didToNumAlgo4DidDocument(longFormDid) + + expect(longFormDid).toBe( + 'did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn:z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + expect(longFormDid).toBe(peerDidDocument.id) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts b/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts index 4e23c98d3d..7a194d4c4c 100644 --- a/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts +++ b/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts @@ -5,7 +5,6 @@ import { convertPublicKeyToX25519 } from '@stablelib/ed25519' import { Key } from '../../../../crypto/Key' import { KeyType } from '../../../../crypto/KeyType' import { AriesFrameworkError } from '../../../../error' -import { uuid } from '../../../../utils/uuid' import { getEd25519VerificationKey2018, getX25519KeyAgreementKey2019 } from '../../domain' import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder' import { DidCommV1Service } from '../../domain/service/DidCommV1Service' @@ -30,13 +29,14 @@ export function createPeerDidDocumentFromServices(services: ResolvedDidCommServi } const x25519Key = Key.fromPublicKey(convertPublicKeyToX25519(recipientKey.publicKey), KeyType.X25519) + // Remove prefix from id as it is not included in did peer identifiers const ed25519VerificationMethod = getEd25519VerificationKey2018({ - id: `#${uuid()}`, + id: `#${recipientKey.fingerprint.substring(1)}`, key: recipientKey, controller: '#id', }) const x25519VerificationMethod = getX25519KeyAgreementKey2019({ - id: `#${uuid()}`, + id: `#${x25519Key.fingerprint.substring(1)}`, key: x25519Key, controller: '#id', }) diff --git a/packages/core/src/modules/dids/methods/peer/didPeer.ts b/packages/core/src/modules/dids/methods/peer/didPeer.ts index 9aaf294ba5..622b5b6a9c 100644 --- a/packages/core/src/modules/dids/methods/peer/didPeer.ts +++ b/packages/core/src/modules/dids/methods/peer/didPeer.ts @@ -1,7 +1,9 @@ import { AriesFrameworkError } from '../../../../error' +import { getAlternativeDidsForNumAlgo4Did } from './peerDidNumAlgo4' + const PEER_DID_REGEX = new RegExp( - '^did:peer:(([01](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))|(2((.[AEVID](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))+(.(S)[0-9a-zA-Z=]*)?)))$' + '^did:peer:(([01](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))|(2((.[AEVID](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))+(.(S)[0-9a-zA-Z=]*)?))|([4](z[1-9a-km-zA-HJ-NP-Z]{46})(:z[1-9a-km-zA-HJ-NP-Z]{6,}){0,1}))$' ) export function isValidPeerDid(did: string): boolean { @@ -14,6 +16,7 @@ export enum PeerDidNumAlgo { InceptionKeyWithoutDoc = 0, GenesisDoc = 1, MultipleInceptionKeyWithoutDoc = 2, + ShortFormAndLongForm = 4, } export function getNumAlgoFromPeerDid(did: string) { @@ -22,10 +25,23 @@ export function getNumAlgoFromPeerDid(did: string) { if ( numAlgo !== PeerDidNumAlgo.InceptionKeyWithoutDoc && numAlgo !== PeerDidNumAlgo.GenesisDoc && - numAlgo !== PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc + numAlgo !== PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc && + numAlgo !== PeerDidNumAlgo.ShortFormAndLongForm ) { throw new AriesFrameworkError(`Invalid peer did numAlgo: ${numAlgo}`) } return numAlgo as PeerDidNumAlgo } + +/** + * Given a peer did, returns any alternative forms equivalent to it. + * + * @param did + * @returns array of alternative dids or undefined if not applicable + */ +export function getAlternativeDidsForPeerDid(did: string) { + if (getNumAlgoFromPeerDid(did) === PeerDidNumAlgo.ShortFormAndLongForm) { + return getAlternativeDidsForNumAlgo4Did(did) + } +} diff --git a/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo4.ts b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo4.ts new file mode 100644 index 0000000000..cb8d61598d --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo4.ts @@ -0,0 +1,138 @@ +import type { OutOfBandDidCommService } from '../../../oob/domain/OutOfBandDidCommService' + +import { AriesFrameworkError } from '../../../../error' +import { + JsonEncoder, + JsonTransformer, + MultiBaseEncoder, + MultiHashEncoder, + TypedArrayEncoder, + VarintEncoder, +} from '../../../../utils' +import { Buffer } from '../../../../utils/buffer' +import { DidDocument, DidCommV1Service } from '../../domain' +import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder' +import { parseDid } from '../../domain/parse' +import { DidKey } from '../key' + +const LONG_RE = new RegExp(`^did:peer:4(z[1-9a-km-zA-HJ-NP-Z]{46}):(z[1-9a-km-zA-HJ-NP-Z]{6,})$`) +const SHORT_RE = new RegExp(`^did:peer:4(z[1-9a-km-zA-HJ-NP-Z]{46})$`) +const JSON_MULTICODEC_VARINT = 0x0200 + +export const isShortFormDidPeer4 = (did: string) => SHORT_RE.test(did) +export const isLongFormDidPeer4 = (did: string) => LONG_RE.test(did) + +const hashEncodedDocument = (encodedDocument: string) => + MultiBaseEncoder.encode( + MultiHashEncoder.encode(TypedArrayEncoder.fromString(encodedDocument), 'sha2-256'), + 'base58btc' + ) + +export function getAlternativeDidsForNumAlgo4Did(did: string) { + const match = did.match(LONG_RE) + if (!match) return + const [, hash] = match + return [`did:peer:4${hash}`] +} + +export function didToNumAlgo4DidDocument(did: string) { + const parsed = parseDid(did) + + const match = parsed.did.match(LONG_RE) + if (!match) { + throw new AriesFrameworkError(`Invalid long form algo 4 did:peer: ${parsed.did}`) + } + const [, hash, encodedDocument] = match + if (hash !== hashEncodedDocument(encodedDocument)) { + throw new AriesFrameworkError(`Hash is invalid for did: ${did}`) + } + + const { data } = MultiBaseEncoder.decode(encodedDocument) + const [multiCodecValue] = VarintEncoder.decode(data.subarray(0, 2)) + if (multiCodecValue !== JSON_MULTICODEC_VARINT) { + throw new AriesFrameworkError(`Not a JSON multicodec data`) + } + const didDocumentJson = JsonEncoder.fromBuffer(data.subarray(2)) + + didDocumentJson.id = parsed.did + didDocumentJson.alsoKnownAs = [parsed.did.slice(0, did.lastIndexOf(':'))] + + // Populate all verification methods without controller + const addControllerIfNotPresent = (item: unknown) => { + if (Array.isArray(item)) item.forEach(addControllerIfNotPresent) + + if (item && typeof item === 'object' && (item as Record).controller === undefined) { + ;(item as Record).controller = parsed.did + } + } + + addControllerIfNotPresent(didDocumentJson.verificationMethod) + addControllerIfNotPresent(didDocumentJson.authentication) + addControllerIfNotPresent(didDocumentJson.assertionMethod) + addControllerIfNotPresent(didDocumentJson.keyAgreement) + addControllerIfNotPresent(didDocumentJson.capabilityDelegation) + addControllerIfNotPresent(didDocumentJson.capabilityInvocation) + + const didDocument = JsonTransformer.fromJSON(didDocumentJson, DidDocument) + return didDocument +} + +export function didDocumentToNumAlgo4Did(didDocument: DidDocument) { + const didDocumentJson = didDocument.toJSON() + + // Build input document based on did document, without any + // reference to controller + const deleteControllerIfPresent = (item: unknown) => { + if (Array.isArray(item)) { + item.forEach((method: { controller?: string }) => { + if (method.controller === '#id' || method.controller === didDocument.id) delete method.controller + }) + } + } + delete didDocumentJson.id + delete didDocumentJson.alsoKnownAs + deleteControllerIfPresent(didDocumentJson.verificationMethod) + deleteControllerIfPresent(didDocumentJson.authentication) + deleteControllerIfPresent(didDocumentJson.assertionMethod) + deleteControllerIfPresent(didDocumentJson.keyAgreement) + deleteControllerIfPresent(didDocumentJson.capabilityDelegation) + deleteControllerIfPresent(didDocumentJson.capabilityInvocation) + + // Construct encoded document by prefixing did document with multicodec prefix for JSON + const buffer = Buffer.concat([ + VarintEncoder.encode(JSON_MULTICODEC_VARINT), + Buffer.from(JSON.stringify(didDocumentJson)), + ]) + + const encodedDocument = MultiBaseEncoder.encode(buffer, 'base58btc') + + const shortFormDid = `did:peer:4${hashEncodedDocument(encodedDocument)}` + const longFormDid = `${shortFormDid}:${encodedDocument}` + + return { shortFormDid, longFormDid } +} + +export function outOfBandServiceToNumAlgo4Did(service: OutOfBandDidCommService) { + // FIXME: add the key entries for the recipientKeys to the did document. + const didDocument = new DidDocumentBuilder('') + .addService( + new DidCommV1Service({ + id: service.id, + serviceEndpoint: service.serviceEndpoint, + accept: service.accept, + // FIXME: this should actually be local key references, not did:key:123#456 references + recipientKeys: service.recipientKeys.map((recipientKey) => { + const did = DidKey.fromDid(recipientKey) + return `${did.did}#${did.key.fingerprint}` + }), + // Map did:key:xxx to actual did:key:xxx#123 + routingKeys: service.routingKeys?.map((routingKey) => { + const did = DidKey.fromDid(routingKey) + return `${did.did}#${did.key.fingerprint}` + }), + }) + ) + .build() + + return didDocumentToNumAlgo4Did(didDocument) +} diff --git a/packages/core/src/modules/dids/methods/web/WebDidResolver.ts b/packages/core/src/modules/dids/methods/web/WebDidResolver.ts index 77d9b1e295..f56dbebbfd 100644 --- a/packages/core/src/modules/dids/methods/web/WebDidResolver.ts +++ b/packages/core/src/modules/dids/methods/web/WebDidResolver.ts @@ -11,6 +11,8 @@ import { DidDocument } from '../../domain' export class WebDidResolver implements DidResolver { public readonly supportedMethods + public readonly allowsCaching = true + // FIXME: Would be nice if we don't have to provide a did resolver instance private _resolverInstance = new Resolver() private resolver = didWeb.getResolver() diff --git a/packages/core/src/modules/dids/repository/DidRecord.ts b/packages/core/src/modules/dids/repository/DidRecord.ts index 5f8b9cc375..3f22751648 100644 --- a/packages/core/src/modules/dids/repository/DidRecord.ts +++ b/packages/core/src/modules/dids/repository/DidRecord.ts @@ -23,6 +23,10 @@ export interface DidRecordProps { export interface CustomDidTags extends TagsBase { recipientKeyFingerprints?: string[] + + // Alternative forms of the did, allowed to be queried by them. + // Relationship must be verified both ways before setting this tag. + alternativeDids?: string[] } type DefaultDidTags = { diff --git a/packages/core/src/modules/dids/repository/DidRepository.ts b/packages/core/src/modules/dids/repository/DidRepository.ts index 11a6c60b9a..0851390d87 100644 --- a/packages/core/src/modules/dids/repository/DidRepository.ts +++ b/packages/core/src/modules/dids/repository/DidRepository.ts @@ -48,22 +48,28 @@ export class DidRepository extends Repository { } public findAllByDid(agentContext: AgentContext, did: string) { - return this.findByQuery(agentContext, { did }) + return this.findByQuery(agentContext, { $or: [{ alternativeDids: [did] }, { did }] }) } public findReceivedDid(agentContext: AgentContext, receivedDid: string) { - return this.findSingleByQuery(agentContext, { did: receivedDid, role: DidDocumentRole.Received }) + return this.findSingleByQuery(agentContext, { + $or: [{ alternativeDids: [receivedDid] }, { did: receivedDid }], + role: DidDocumentRole.Received, + }) } public findCreatedDid(agentContext: AgentContext, createdDid: string) { - return this.findSingleByQuery(agentContext, { did: createdDid, role: DidDocumentRole.Created }) + return this.findSingleByQuery(agentContext, { + $or: [{ alternativeDids: [createdDid] }, { did: createdDid }], + role: DidDocumentRole.Created, + }) } public getCreatedDids(agentContext: AgentContext, { method, did }: { method?: string; did?: string }) { return this.findByQuery(agentContext, { role: DidDocumentRole.Created, method, - did, + $or: did ? [{ alternativeDids: [did] }, { did }] : undefined, }) } diff --git a/packages/core/src/modules/dids/services/DidResolverService.ts b/packages/core/src/modules/dids/services/DidResolverService.ts index 7f97d3f9d1..a24c8908ba 100644 --- a/packages/core/src/modules/dids/services/DidResolverService.ts +++ b/packages/core/src/modules/dids/services/DidResolverService.ts @@ -6,7 +6,10 @@ import { InjectionSymbols } from '../../../constants' import { AriesFrameworkError } from '../../../error' import { Logger } from '../../../logger' import { injectable, inject } from '../../../plugins' +import { JsonTransformer } from '../../../utils' +import { CacheModuleConfig } from '../../cache' import { DidsModuleConfig } from '../DidsModuleConfig' +import { DidDocument } from '../domain' import { parseDid } from '../domain/parse' @injectable() @@ -53,9 +56,69 @@ export class DidResolverService { } } - return resolver.resolve(agentContext, parsed.did, parsed, options) + // extract caching options and set defaults + const { useCache = true, cacheDurationInSeconds = 300, persistInCache = true } = options + const cacheKey = `did:resolver:${parsed.did}` + + if (resolver.allowsCaching && useCache) { + const cache = agentContext.dependencyManager.resolve(CacheModuleConfig).cache + // FIXME: in multi-tenancy it can be that the same cache is used for different agent contexts + // This may become a problem when resolving dids, as you can get back a cache hit for a different + // tenant. did:peer has disabled caching, and I think we should just recommend disabling caching + // for these private dids + // We could allow providing a custom cache prefix in the resolver options, so that the cache key + // can be recognized in the cache implementation + const cachedDidDocument = await cache.get }>( + agentContext, + cacheKey + ) + + if (cachedDidDocument) { + return { + ...cachedDidDocument, + didDocument: JsonTransformer.fromJSON(cachedDidDocument.didDocument, DidDocument), + didResolutionMetadata: { + ...cachedDidDocument.didResolutionMetadata, + servedFromCache: true, + }, + } + } + } + + let resolutionResult = await resolver.resolve(agentContext, parsed.did, parsed, options) + // Avoid overwriting existing document + resolutionResult = { + ...resolutionResult, + didResolutionMetadata: { + ...resolutionResult.didResolutionMetadata, + resolutionTime: Date.now(), + // Did resolver implementation might use did method specific caching strategy + // We only set to false if not defined by the resolver + servedFromCache: resolutionResult.didResolutionMetadata.servedFromCache ?? false, + }, + } + + if (resolutionResult.didDocument && resolver.allowsCaching && persistInCache) { + const cache = agentContext.dependencyManager.resolve(CacheModuleConfig).cache + await cache.set( + agentContext, + cacheKey, + { + ...resolutionResult, + didDocument: resolutionResult.didDocument.toJSON(), + }, + // Set cache duration + cacheDurationInSeconds + ) + } + + return resolutionResult } + /** + * Resolve a did document. This uses the default resolution options, and thus + * will use caching if available. + */ public async resolveDidDocument(agentContext: AgentContext, did: string) { const { didDocument, diff --git a/packages/core/src/modules/dids/services/__tests__/DidResolverService.test.ts b/packages/core/src/modules/dids/services/__tests__/DidResolverService.test.ts index 00b17ad458..76573d0ed3 100644 --- a/packages/core/src/modules/dids/services/__tests__/DidResolverService.test.ts +++ b/packages/core/src/modules/dids/services/__tests__/DidResolverService.test.ts @@ -2,6 +2,7 @@ import type { DidResolver } from '../../domain' import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../tests/helpers' import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { CacheModuleConfig, InMemoryLruCache } from '../../../cache' import { DidsModuleConfig } from '../../DidsModuleConfig' import didKeyEd25519Fixture from '../../__tests__/__fixtures__/didKeyEd25519.json' import { DidDocument } from '../../domain' @@ -9,12 +10,16 @@ import { parseDid } from '../../domain/parse' import { DidResolverService } from '../DidResolverService' const didResolverMock = { + allowsCaching: true, supportedMethods: ['key'], resolve: jest.fn(), } as DidResolver +const cache = new InMemoryLruCache({ limit: 10 }) const agentConfig = getAgentConfig('DidResolverService') -const agentContext = getAgentContext() +const agentContext = getAgentContext({ + registerInstances: [[CacheModuleConfig, new CacheModuleConfig({ cache })]], +}) describe('DidResolverService', () => { const didResolverService = new DidResolverService( @@ -22,6 +27,10 @@ describe('DidResolverService', () => { new DidsModuleConfig({ resolvers: [didResolverMock] }) ) + afterEach(() => { + jest.clearAllMocks() + }) + it('should correctly find and call the correct resolver for a specified did', async () => { const returnValue = { didDocument: JsonTransformer.fromJSON(didKeyEd25519Fixture, DidDocument), @@ -33,7 +42,14 @@ describe('DidResolverService', () => { mockFunction(didResolverMock.resolve).mockResolvedValue(returnValue) const result = await didResolverService.resolve(agentContext, 'did:key:xxxx', { someKey: 'string' }) - expect(result).toEqual(returnValue) + expect(result).toEqual({ + ...returnValue, + didResolutionMetadata: { + ...returnValue.didResolutionMetadata, + resolutionTime: expect.any(Number), + servedFromCache: false, + }, + }) expect(didResolverMock.resolve).toHaveBeenCalledTimes(1) expect(didResolverMock.resolve).toHaveBeenCalledWith(agentContext, 'did:key:xxxx', parseDid('did:key:xxxx'), { @@ -41,6 +57,57 @@ describe('DidResolverService', () => { }) }) + it('should return cached did document when resolved multiple times within caching duration', async () => { + const returnValue = { + didDocument: JsonTransformer.fromJSON(didKeyEd25519Fixture, DidDocument), + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + } + mockFunction(didResolverMock.resolve).mockResolvedValue(returnValue) + + const result = await didResolverService.resolve(agentContext, 'did:key:cached', { someKey: 'string' }) + const cachedValue = await cache.get(agentContext, 'did:resolver:did:key:cached') + + expect(result).toEqual({ + ...returnValue, + didResolutionMetadata: { + ...returnValue.didResolutionMetadata, + resolutionTime: expect.any(Number), + servedFromCache: false, + }, + }) + + expect(cachedValue).toEqual({ + ...returnValue, + didDocument: returnValue.didDocument.toJSON(), + didResolutionMetadata: { + ...returnValue.didResolutionMetadata, + resolutionTime: expect.any(Number), + servedFromCache: false, + }, + }) + + expect(didResolverMock.resolve).toHaveBeenCalledTimes(1) + expect(didResolverMock.resolve).toHaveBeenCalledWith(agentContext, 'did:key:cached', parseDid('did:key:cached'), { + someKey: 'string', + }) + + const resultCached = await didResolverService.resolve(agentContext, 'did:key:cached', { someKey: 'string' }) + expect(resultCached).toEqual({ + ...returnValue, + didResolutionMetadata: { + ...returnValue.didResolutionMetadata, + resolutionTime: expect.any(Number), + servedFromCache: true, + }, + }) + + // Still called once because served from cache + expect(didResolverMock.resolve).toHaveBeenCalledTimes(1) + }) + it("should return an error with 'invalidDid' if the did string couldn't be parsed", async () => { const did = 'did:__Asd:asdfa' diff --git a/packages/core/src/modules/dids/types.ts b/packages/core/src/modules/dids/types.ts index 359616a818..ccbc53fa70 100644 --- a/packages/core/src/modules/dids/types.ts +++ b/packages/core/src/modules/dids/types.ts @@ -2,11 +2,34 @@ import type { DidDocument } from './domain' import type { DIDDocumentMetadata, DIDResolutionMetadata, DIDResolutionOptions, ParsedDID } from 'did-resolver' export type ParsedDid = ParsedDID -export type DidResolutionOptions = DIDResolutionOptions export type DidDocumentMetadata = DIDDocumentMetadata +export interface DidResolutionOptions extends DIDResolutionOptions { + /** + * Whether to resolve the did document from the cache. + * + * @default true + */ + useCache?: boolean + + /** + * Whether to persist the did document in the cache. + * + * @default true + */ + persistInCache?: boolean + + /** + * How many seconds to persist the resolved document + * + * @default 3600 + */ + cacheDurationInSeconds?: number +} + export interface DidResolutionMetadata extends DIDResolutionMetadata { message?: string + servedFromCache?: boolean } export interface DidResolutionResult { diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeError.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeError.ts new file mode 100644 index 0000000000..5c06ec420a --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeError.ts @@ -0,0 +1,13 @@ +import { AriesFrameworkError } from '../../error' + +export class DifPresentationExchangeError extends AriesFrameworkError { + public additionalMessages?: Array + + public constructor( + message: string, + { cause, additionalMessages }: { cause?: Error; additionalMessages?: Array } = {} + ) { + super(message, { cause }) + this.additionalMessages = additionalMessages + } +} diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts new file mode 100644 index 0000000000..7cb2c86c5a --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts @@ -0,0 +1,25 @@ +import type { DependencyManager, Module } from '../../plugins' + +import { AgentConfig } from '../../agent/AgentConfig' + +import { DifPresentationExchangeService } from './DifPresentationExchangeService' + +/** + * @public + */ +export class DifPresentationExchangeModule implements Module { + /** + * Registers the dependencies of the presentation-exchange module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Warn about experimental module + dependencyManager + .resolve(AgentConfig) + .logger.warn( + "The 'DifPresentationExchangeModule' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + ) + + // service + dependencyManager.registerSingleton(DifPresentationExchangeService) + } +} diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts new file mode 100644 index 0000000000..eab8642230 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -0,0 +1,548 @@ +import type { + DifPexInputDescriptorToCredentials, + DifPexCredentialsForRequest, + DifPresentationExchangeDefinition, + DifPresentationExchangeDefinitionV1, + DifPresentationExchangeSubmission, + DifPresentationExchangeDefinitionV2, +} from './models' +import type { AgentContext } from '../../agent' +import type { Query } from '../../storage/StorageService' +import type { VerificationMethod } from '../dids' +import type { W3cCredentialRecord, W3cVerifiableCredential, W3cVerifiablePresentation } from '../vc' +import type { PresentationSignCallBackParams, Validated, VerifiablePresentationResult } from '@sphereon/pex' +import type { InputDescriptorV2, PresentationDefinitionV1 } from '@sphereon/pex-models' +import type { OriginalVerifiableCredential, OriginalVerifiablePresentation } from '@sphereon/ssi-types' + +import { Status, PEVersion, PEX } from '@sphereon/pex' +import { injectable } from 'tsyringe' + +import { getJwkFromKey } from '../../crypto' +import { AriesFrameworkError } from '../../error' +import { JsonTransformer } from '../../utils' +import { DidsApi, getKeyFromVerificationMethod } from '../dids' +import { + ClaimFormat, + SignatureSuiteRegistry, + W3cCredentialRepository, + W3cCredentialService, + W3cPresentation, +} from '../vc' + +import { DifPresentationExchangeError } from './DifPresentationExchangeError' +import { DifPresentationExchangeSubmissionLocation } from './models' +import { + getCredentialsForRequest, + getSphereonOriginalVerifiableCredential, + getSphereonW3cVerifiablePresentation, + getW3cVerifiablePresentationInstance, +} from './utils' + +export type ProofStructure = Record>> + +@injectable() +export class DifPresentationExchangeService { + private pex = new PEX() + + public async getCredentialsForRequest( + agentContext: AgentContext, + presentationDefinition: DifPresentationExchangeDefinition + ): Promise { + const credentialRecords = await this.queryCredentialForPresentationDefinition(agentContext, presentationDefinition) + + // FIXME: why are we resolving all created dids here? + // If we want to do this we should extract all dids from the credential records and only + // fetch the dids for the queried credential records + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didRecords = await didsApi.getCreatedDids() + const holderDids = didRecords.map((didRecord) => didRecord.did) + + return getCredentialsForRequest(presentationDefinition, credentialRecords, holderDids) + } + + /** + * Selects the credentials to use based on the output from `getCredentialsForRequest` + * Use this method if you don't want to manually select the credentials yourself. + */ + public selectCredentialsForRequest( + credentialsForRequest: DifPexCredentialsForRequest + ): DifPexInputDescriptorToCredentials { + if (!credentialsForRequest.areRequirementsSatisfied) { + throw new AriesFrameworkError('Could not find the required credentials for the presentation submission') + } + + const credentials: DifPexInputDescriptorToCredentials = {} + + for (const requirement of credentialsForRequest.requirements) { + for (const submission of requirement.submissionEntry) { + if (!credentials[submission.inputDescriptorId]) { + credentials[submission.inputDescriptorId] = [] + } + + // We pick the first matching VC if we are auto-selecting + credentials[submission.inputDescriptorId].push(submission.verifiableCredentials[0].credential) + } + } + + return credentials + } + + public validatePresentationDefinition(presentationDefinition: DifPresentationExchangeDefinition) { + const validation = PEX.validateDefinition(presentationDefinition) + const errorMessages = this.formatValidated(validation) + if (errorMessages.length > 0) { + throw new DifPresentationExchangeError(`Invalid presentation definition`, { additionalMessages: errorMessages }) + } + } + + public validatePresentationSubmission(presentationSubmission: DifPresentationExchangeSubmission) { + const validation = PEX.validateSubmission(presentationSubmission) + const errorMessages = this.formatValidated(validation) + if (errorMessages.length > 0) { + throw new DifPresentationExchangeError(`Invalid presentation submission`, { additionalMessages: errorMessages }) + } + } + + public validatePresentation( + presentationDefinition: DifPresentationExchangeDefinition, + presentation: W3cVerifiablePresentation + ) { + const { errors } = this.pex.evaluatePresentation( + presentationDefinition, + presentation.encoded as OriginalVerifiablePresentation + ) + + if (errors) { + const errorMessages = this.formatValidated(errors as Validated) + if (errorMessages.length > 0) { + throw new DifPresentationExchangeError(`Invalid presentation`, { additionalMessages: errorMessages }) + } + } + } + + private formatValidated(v: Validated) { + const validated = Array.isArray(v) ? v : [v] + return validated + .filter((r) => r.tag === Status.ERROR) + .map((r) => r.message) + .filter((r): r is string => Boolean(r)) + } + + /** + * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the + * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. + */ + private async queryCredentialForPresentationDefinition( + agentContext: AgentContext, + presentationDefinition: DifPresentationExchangeDefinition + ): Promise> { + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const query: Array> = [] + const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition) + + if (!presentationDefinitionVersion.version) { + throw new DifPresentationExchangeError( + `Unable to determine the Presentation Exchange version from the presentation definition + `, + presentationDefinitionVersion.error ? { additionalMessages: [presentationDefinitionVersion.error] } : {} + ) + } + + if (presentationDefinitionVersion.version === PEVersion.v1) { + const pd = presentationDefinition as PresentationDefinitionV1 + + // The schema.uri can contain either an expanded type, or a context uri + for (const inputDescriptor of pd.input_descriptors) { + for (const schema of inputDescriptor.schema) { + query.push({ + $or: [{ expandedType: [schema.uri] }, { contexts: [schema.uri] }, { type: [schema.uri] }], + }) + } + } + } else if (presentationDefinitionVersion.version === PEVersion.v2) { + // FIXME: As PE version 2 does not have the `schema` anymore, we can't query by schema anymore. + // For now we retrieve ALL credentials, as we did the same for V1 with JWT credentials. We probably need + // to find some way to do initial filtering, hopefully if there's a filter on the `type` field or something. + } else { + throw new DifPresentationExchangeError( + `Unsupported presentation definition version ${presentationDefinitionVersion.version as unknown as string}` + ) + } + + // query the wallet ourselves first to avoid the need to query the pex library for all + // credentials for every proof request + const credentialRecords = + query.length > 0 + ? await w3cCredentialRepository.findByQuery(agentContext, { + $or: query, + }) + : await w3cCredentialRepository.getAll(agentContext) + + return credentialRecords + } + + private addCredentialToSubjectInputDescriptor( + subjectsToInputDescriptors: ProofStructure, + subjectId: string, + inputDescriptorId: string, + credential: W3cVerifiableCredential + ) { + const inputDescriptorsToCredentials = subjectsToInputDescriptors[subjectId] ?? {} + const credentials = inputDescriptorsToCredentials[inputDescriptorId] ?? [] + + credentials.push(credential) + inputDescriptorsToCredentials[inputDescriptorId] = credentials + subjectsToInputDescriptors[subjectId] = inputDescriptorsToCredentials + } + + private getPresentationFormat( + presentationDefinition: DifPresentationExchangeDefinition, + credentials: Array + ): ClaimFormat.JwtVp | ClaimFormat.LdpVp { + const allCredentialsAreJwtVc = credentials?.every((c) => typeof c === 'string') + const allCredentialsAreLdpVc = credentials?.every((c) => typeof c !== 'string') + + const inputDescriptorsNotSupportingJwtVc = ( + presentationDefinition.input_descriptors as Array + ).filter((d) => d.format && d.format.jwt_vc === undefined) + + const inputDescriptorsNotSupportingLdpVc = ( + presentationDefinition.input_descriptors as Array + ).filter((d) => d.format && d.format.ldp_vc === undefined) + + if ( + allCredentialsAreJwtVc && + (presentationDefinition.format === undefined || presentationDefinition.format.jwt_vc) && + inputDescriptorsNotSupportingJwtVc.length === 0 + ) { + return ClaimFormat.JwtVp + } else if ( + allCredentialsAreLdpVc && + (presentationDefinition.format === undefined || presentationDefinition.format.ldp_vc) && + inputDescriptorsNotSupportingLdpVc.length === 0 + ) { + return ClaimFormat.LdpVp + } else { + throw new DifPresentationExchangeError( + 'No suitable presentation format found for the given presentation definition, and credentials' + ) + } + } + + public async createPresentation( + agentContext: AgentContext, + options: { + credentialsForInputDescriptor: DifPexInputDescriptorToCredentials + presentationDefinition: DifPresentationExchangeDefinition + /** + * Defaults to {@link DifPresentationExchangeSubmissionLocation.PRESENTATION} + */ + presentationSubmissionLocation?: DifPresentationExchangeSubmissionLocation + challenge?: string + domain?: string + nonce?: string + } + ) { + const { presentationDefinition, challenge, nonce, domain, presentationSubmissionLocation } = options + + const proofStructure: ProofStructure = {} + + Object.entries(options.credentialsForInputDescriptor).forEach(([inputDescriptorId, credentials]) => { + credentials.forEach((credential) => { + const subjectId = credential.credentialSubjectIds[0] + if (!subjectId) { + throw new DifPresentationExchangeError('Missing required credential subject for creating the presentation.') + } + + this.addCredentialToSubjectInputDescriptor(proofStructure, subjectId, inputDescriptorId, credential) + }) + }) + + const verifiablePresentationResultsWithFormat: Array<{ + verifiablePresentationResult: VerifiablePresentationResult + format: ClaimFormat.LdpVp | ClaimFormat.JwtVp + }> = [] + + const subjectToInputDescriptors = Object.entries(proofStructure) + for (const [subjectId, subjectInputDescriptorsToCredentials] of subjectToInputDescriptors) { + // Determine a suitable verification method for the presentation + const verificationMethod = await this.getVerificationMethodForSubjectId(agentContext, subjectId) + + if (!verificationMethod) { + throw new DifPresentationExchangeError(`No verification method found for subject id '${subjectId}'.`) + } + + // We create a presentation for each subject + // Thus for each subject we need to filter all the related input descriptors and credentials + // FIXME: cast to V1, as tsc errors for strange reasons if not + const inputDescriptorsForSubject = (presentationDefinition as PresentationDefinitionV1).input_descriptors.filter( + (inputDescriptor) => inputDescriptor.id in subjectInputDescriptorsToCredentials + ) + + // Get all the credentials associated with the input descriptors + const credentialsForSubject = Object.values(subjectInputDescriptorsToCredentials) + .flat() + .map(getSphereonOriginalVerifiableCredential) + + const presentationDefinitionForSubject: DifPresentationExchangeDefinition = { + ...presentationDefinition, + input_descriptors: inputDescriptorsForSubject, + + // We remove the submission requirements, as it will otherwise fail to create the VP + submission_requirements: undefined, + } + + const format = this.getPresentationFormat(presentationDefinitionForSubject, credentialsForSubject) + + // FIXME: Q1: is holder always subject id, what if there are multiple subjects??? + // FIXME: Q2: What about proofType, proofPurpose verification method for multiple subjects? + const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( + presentationDefinitionForSubject, + credentialsForSubject, + this.getPresentationSignCallback(agentContext, verificationMethod, format), + { + holderDID: subjectId, + proofOptions: { challenge, domain, nonce }, + signatureOptions: { verificationMethod: verificationMethod?.id }, + presentationSubmissionLocation: + presentationSubmissionLocation ?? DifPresentationExchangeSubmissionLocation.PRESENTATION, + } + ) + + verifiablePresentationResultsWithFormat.push({ verifiablePresentationResult, format }) + } + + if (!verifiablePresentationResultsWithFormat[0]) { + throw new DifPresentationExchangeError('No verifiable presentations created') + } + + if (subjectToInputDescriptors.length !== verifiablePresentationResultsWithFormat.length) { + throw new DifPresentationExchangeError('Invalid amount of verifiable presentations created') + } + + const presentationSubmission: DifPresentationExchangeSubmission = { + id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id, + definition_id: + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.definition_id, + descriptor_map: [], + } + + for (const vpf of verifiablePresentationResultsWithFormat) { + const { verifiablePresentationResult } = vpf + presentationSubmission.descriptor_map.push(...verifiablePresentationResult.presentationSubmission.descriptor_map) + } + + return { + verifiablePresentations: verifiablePresentationResultsWithFormat.map((r) => + getW3cVerifiablePresentationInstance(r.verifiablePresentationResult.verifiablePresentation) + ), + presentationSubmission, + presentationSubmissionLocation: + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmissionLocation, + } + } + + private getSigningAlgorithmFromVerificationMethod( + verificationMethod: VerificationMethod, + suitableAlgorithms?: Array + ) { + const key = getKeyFromVerificationMethod(verificationMethod) + const jwk = getJwkFromKey(key) + + if (suitableAlgorithms) { + const possibleAlgorithms = jwk.supportedSignatureAlgorithms.filter((alg) => suitableAlgorithms?.includes(alg)) + if (!possibleAlgorithms || possibleAlgorithms.length === 0) { + throw new DifPresentationExchangeError( + [ + `Found no suitable signing algorithm.`, + `Algorithms supported by Verification method: ${jwk.supportedSignatureAlgorithms.join(', ')}`, + `Suitable algorithms: ${suitableAlgorithms.join(', ')}`, + ].join('\n') + ) + } + } + + const alg = jwk.supportedSignatureAlgorithms[0] + if (!alg) throw new DifPresentationExchangeError(`No supported algs for key type: ${key.keyType}`) + return alg + } + + private getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition: Array, + inputDescriptorAlgorithms: Array> + ) { + const allDescriptorAlgorithms = inputDescriptorAlgorithms.flat() + const algorithmsSatisfyingDescriptors = allDescriptorAlgorithms.filter((alg) => + inputDescriptorAlgorithms.every((descriptorAlgorithmSet) => descriptorAlgorithmSet.includes(alg)) + ) + + const algorithmsSatisfyingPdAndDescriptorRestrictions = algorithmsSatisfyingDefinition.filter((alg) => + algorithmsSatisfyingDescriptors.includes(alg) + ) + + if ( + algorithmsSatisfyingDefinition.length > 0 && + algorithmsSatisfyingDescriptors.length > 0 && + algorithmsSatisfyingPdAndDescriptorRestrictions.length === 0 + ) { + throw new DifPresentationExchangeError( + `No signature algorithm found for satisfying restrictions of the presentation definition and input descriptors` + ) + } + + if (allDescriptorAlgorithms.length > 0 && algorithmsSatisfyingDescriptors.length === 0) { + throw new DifPresentationExchangeError( + `No signature algorithm found for satisfying restrictions of the input descriptors` + ) + } + + let suitableAlgorithms: Array | undefined + if (algorithmsSatisfyingPdAndDescriptorRestrictions.length > 0) { + suitableAlgorithms = algorithmsSatisfyingPdAndDescriptorRestrictions + } else if (algorithmsSatisfyingDescriptors.length > 0) { + suitableAlgorithms = algorithmsSatisfyingDescriptors + } else if (algorithmsSatisfyingDefinition.length > 0) { + suitableAlgorithms = algorithmsSatisfyingDefinition + } + + return suitableAlgorithms + } + + private getSigningAlgorithmForJwtVc( + presentationDefinition: DifPresentationExchangeDefinitionV1 | DifPresentationExchangeDefinitionV2, + verificationMethod: VerificationMethod + ) { + const algorithmsSatisfyingDefinition = presentationDefinition.format?.jwt_vc?.alg ?? [] + + const inputDescriptorAlgorithms: Array> = presentationDefinition.input_descriptors + .map((descriptor) => (descriptor as InputDescriptorV2).format?.jwt_vc?.alg ?? []) + .filter((alg) => alg.length > 0) + + const suitableAlgorithms = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition, + inputDescriptorAlgorithms + ) + + return this.getSigningAlgorithmFromVerificationMethod(verificationMethod, suitableAlgorithms) + } + + private getProofTypeForLdpVc( + agentContext: AgentContext, + presentationDefinition: DifPresentationExchangeDefinitionV1 | DifPresentationExchangeDefinitionV2, + verificationMethod: VerificationMethod + ) { + const algorithmsSatisfyingDefinition = presentationDefinition.format?.ldp_vc?.proof_type ?? [] + + const inputDescriptorAlgorithms: Array> = presentationDefinition.input_descriptors + .map((descriptor) => (descriptor as InputDescriptorV2).format?.ldp_vc?.proof_type ?? []) + .filter((alg) => alg.length > 0) + + const suitableSignatureSuites = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition, + inputDescriptorAlgorithms + ) + + // For each of the supported algs, find the key types, then find the proof types + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + + const supportedSignatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) + if (!supportedSignatureSuite) { + throw new DifPresentationExchangeError( + `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'` + ) + } + + if (suitableSignatureSuites) { + if (suitableSignatureSuites.includes(supportedSignatureSuite.proofType) === false) { + throw new DifPresentationExchangeError( + [ + 'No possible signature suite found for the given verification method.', + `Verification method type: ${verificationMethod.type}`, + `SupportedSignatureSuite '${supportedSignatureSuite.proofType}'`, + `SuitableSignatureSuites: ${suitableSignatureSuites.join(', ')}`, + ].join('\n') + ) + } + + return supportedSignatureSuite.proofType + } + + return supportedSignatureSuite.proofType + } + + public getPresentationSignCallback( + agentContext: AgentContext, + verificationMethod: VerificationMethod, + vpFormat: ClaimFormat.LdpVp | ClaimFormat.JwtVp + ) { + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + + return async (callBackParams: PresentationSignCallBackParams) => { + // The created partial proof and presentation, as well as original supplied options + const { presentation: presentationJson, options, presentationDefinition } = callBackParams + const { challenge, domain, nonce } = options.proofOptions ?? {} + const { verificationMethod: verificationMethodId } = options.signatureOptions ?? {} + + if (verificationMethodId && verificationMethodId !== verificationMethod.id) { + throw new DifPresentationExchangeError( + `Verification method from signing options ${verificationMethodId} does not match verification method ${verificationMethod.id}` + ) + } + + let signedPresentation: W3cVerifiablePresentation + if (vpFormat === 'jwt_vp') { + signedPresentation = await w3cCredentialService.signPresentation(agentContext, { + format: ClaimFormat.JwtVp, + alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), + verificationMethod: verificationMethod.id, + presentation: JsonTransformer.fromJSON(presentationJson, W3cPresentation), + challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), + domain, + }) + } else if (vpFormat === 'ldp_vp') { + signedPresentation = await w3cCredentialService.signPresentation(agentContext, { + format: ClaimFormat.LdpVp, + proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod), + proofPurpose: 'authentication', + verificationMethod: verificationMethod.id, + presentation: JsonTransformer.fromJSON(presentationJson, W3cPresentation), + challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), + domain, + }) + } else { + throw new DifPresentationExchangeError( + `Only JWT credentials or JSONLD credentials are supported for a single presentation` + ) + } + + return getSphereonW3cVerifiablePresentation(signedPresentation) + } + } + + private async getVerificationMethodForSubjectId(agentContext: AgentContext, subjectId: string) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + + if (!subjectId.startsWith('did:')) { + throw new DifPresentationExchangeError( + `Only dids are supported as credentialSubject id. ${subjectId} is not a valid did` + ) + } + + const didDocument = await didsApi.resolveDidDocument(subjectId) + + if (!didDocument.authentication || didDocument.authentication.length === 0) { + throw new DifPresentationExchangeError( + `No authentication verificationMethods found for did ${subjectId} in did document` + ) + } + + // the signature suite to use for the presentation is dependant on the credentials we share. + // 1. Get the verification method for this given proof purpose in this DID document + let [verificationMethod] = didDocument.authentication + if (typeof verificationMethod === 'string') { + verificationMethod = didDocument.dereferenceKey(verificationMethod, ['authentication']) + } + + return verificationMethod + } +} diff --git a/packages/core/src/modules/dif-presentation-exchange/index.ts b/packages/core/src/modules/dif-presentation-exchange/index.ts new file mode 100644 index 0000000000..4f4e4b3923 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/index.ts @@ -0,0 +1,4 @@ +export * from './DifPresentationExchangeError' +export * from './DifPresentationExchangeModule' +export * from './DifPresentationExchangeService' +export * from './models' diff --git a/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts new file mode 100644 index 0000000000..ec2e83d17e --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts @@ -0,0 +1,119 @@ +import type { W3cCredentialRecord, W3cVerifiableCredential } from '../../vc' + +export interface DifPexCredentialsForRequest { + /** + * Whether all requirements have been satisfied by the credentials in the wallet. + */ + areRequirementsSatisfied: boolean + + /** + * The requirements for the presentation definition. If the `areRequirementsSatisfied` value + * is `false`, this list will still be populated with requirements, but won't contain credentials + * for all requirements. This can be useful to display the missing credentials for a presentation + * definition to be satisfied. + * + * NOTE: Presentation definition requirements can be really complex as there's a lot of different + * combinations that are possible. The structure doesn't include all possible combinations yet that + * could satisfy a presentation definition. + */ + requirements: DifPexCredentialsForRequestRequirement[] + + /** + * Name of the presentation definition + */ + name?: string + + /** + * Purpose of the presentation definition. + */ + purpose?: string +} + +/** + * A requirement for the presentation submission. A requirement + * is a group of input descriptors that together fulfill a requirement + * from the presentation definition. + * + * Each submission represents a input descriptor. + */ +export interface DifPexCredentialsForRequestRequirement { + /** + * Whether the requirement is satisfied. + * + * If the requirement is not satisfied, the submission will still contain + * entries, but the `verifiableCredentials` list will be empty. + */ + isRequirementSatisfied: boolean + + /** + * Name of the requirement + */ + name?: string + + /** + * Purpose of the requirement + */ + purpose?: string + + /** + * Array of objects, where each entry contains one or more credentials that will be part + * of the submission. + * + * NOTE: if the `isRequirementSatisfied` is `false` the submission list will + * contain entries where the verifiable credential list is empty. In this case it could also + * contain more entries than are actually needed (as you sometimes can choose from + * e.g. 4 types of credentials and need to submit at least two). If + * `isRequirementSatisfied` is `false`, make sure to check the `needsCount` value + * to see how many of those submissions needed. + */ + submissionEntry: DifPexCredentialsForRequestSubmissionEntry[] + + /** + * The number of submission entries that are needed to fulfill the requirement. + * If `isRequirementSatisfied` is `true`, the submission list will always be equal + * to the number of `needsCount`. If `isRequirementSatisfied` is `false` the list of + * submissions could be longer. + */ + needsCount: number + + /** + * The rule that is used to select the credentials for the submission. + * If the rule is `pick`, the user can select which credentials to use for the submission. + * If the rule is `all`, all credentials that satisfy the input descriptor will be used. + */ + rule: 'pick' | 'all' +} + +/** + * A submission entry that satisfies a specific input descriptor from the + * presentation definition. + */ +export interface DifPexCredentialsForRequestSubmissionEntry { + /** + * The id of the input descriptor + */ + inputDescriptorId: string + + /** + * Name of the input descriptor + */ + name?: string + + /** + * Purpose of the input descriptor + */ + purpose?: string + + /** + * The verifiable credentials that satisfy the input descriptor. + * + * If the value is an empty list, it means the input descriptor could + * not be satisfied. + */ + verifiableCredentials: W3cCredentialRecord[] +} + +/** + * Mapping of selected credentials for an input descriptor + */ +export type DifPexInputDescriptorToCredentials = Record> diff --git a/packages/core/src/modules/dif-presentation-exchange/models/index.ts b/packages/core/src/modules/dif-presentation-exchange/models/index.ts new file mode 100644 index 0000000000..01ce9d6767 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/models/index.ts @@ -0,0 +1,11 @@ +export * from './DifPexCredentialsForRequest' +import type { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models' + +import { PresentationSubmissionLocation } from '@sphereon/pex' + +// Re-export some types from sphereon library, but under more explicit names +export type DifPresentationExchangeDefinition = PresentationDefinitionV1 | PresentationDefinitionV2 +export type DifPresentationExchangeDefinitionV1 = PresentationDefinitionV1 +export type DifPresentationExchangeDefinitionV2 = PresentationDefinitionV2 +export type DifPresentationExchangeSubmission = PresentationSubmission +export { PresentationSubmissionLocation as DifPresentationExchangeSubmissionLocation } diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts new file mode 100644 index 0000000000..1fca34b943 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts @@ -0,0 +1,314 @@ +import type { W3cCredentialRecord } from '../../vc' +import type { + DifPexCredentialsForRequest, + DifPexCredentialsForRequestRequirement, + DifPexCredentialsForRequestSubmissionEntry, +} from '../models' +import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' +import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' + +import { PEX } from '@sphereon/pex' +import { Rules } from '@sphereon/pex-models' +import { default as jp } from 'jsonpath' + +import { deepEquality } from '../../../utils' +import { DifPresentationExchangeError } from '../DifPresentationExchangeError' + +import { getSphereonOriginalVerifiableCredential } from './transform' + +export async function getCredentialsForRequest( + presentationDefinition: IPresentationDefinition, + credentialRecords: Array, + holderDIDs: Array +): Promise { + if (!presentationDefinition) { + throw new DifPresentationExchangeError('Presentation Definition is required to select credentials for submission.') + } + + const pex = new PEX() + + const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential)) + + // FIXME: there is a function for this in the VP library, but it is not usable atm + const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, { + holderDIDs, + // limitDisclosureSignatureSuites: [], + // restrictToDIDMethods, + // restrictToFormats + }) + + const selectResults = { + ...selectResultsRaw, + // Map the encoded credential to their respective w3c credential record + verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded) => { + const credentialRecord = credentialRecords.find((record) => { + const originalVc = getSphereonOriginalVerifiableCredential(record.credential) + return deepEquality(originalVc, encoded) + }) + + if (!credentialRecord) { + throw new DifPresentationExchangeError('Unable to find credential in credential records.') + } + + return credentialRecord + }), + } + + const presentationSubmission: DifPexCredentialsForRequest = { + requirements: [], + areRequirementsSatisfied: false, + name: presentationDefinition.name, + purpose: presentationDefinition.purpose, + } + + // If there's no submission requirements, ALL input descriptors MUST be satisfied + if (!presentationDefinition.submission_requirements || presentationDefinition.submission_requirements.length === 0) { + presentationSubmission.requirements = getSubmissionRequirementsForAllInputDescriptors( + presentationDefinition.input_descriptors, + selectResults + ) + } else { + presentationSubmission.requirements = getSubmissionRequirements(presentationDefinition, selectResults) + } + + // There may be no requirements if we filter out all optional ones. To not makes things too complicated, we see it as an error + // for now if a request is made that has no required requirements (but only e.g. min: 0, which means we don't need to disclose anything) + // I see this more as the fault of the presentation definition, as it should have at least some requirements. + if (presentationSubmission.requirements.length === 0) { + throw new DifPresentationExchangeError( + 'Presentation Definition does not require any credentials. Optional credentials are not included in the presentation submission.' + ) + } + if (selectResultsRaw.areRequiredCredentialsPresent === 'error') { + return presentationSubmission + } + + return { + ...presentationSubmission, + + // If all requirements are satisfied, the presentation submission is satisfied + areRequirementsSatisfied: presentationSubmission.requirements.every( + (requirement) => requirement.isRequirementSatisfied + ), + } +} + +function getSubmissionRequirements( + presentationDefinition: IPresentationDefinition, + selectResults: W3cCredentialRecordSelectResults +): Array { + const submissionRequirements: Array = [] + + // There are submission requirements, so we need to select the input_descriptors + // based on the submission requirements + for (const submissionRequirement of presentationDefinition.submission_requirements ?? []) { + // Check: if the submissionRequirement uses `from_nested`, as we don't support this yet + if (submissionRequirement.from_nested) { + throw new DifPresentationExchangeError( + "Presentation definition contains requirement using 'from_nested', which is not supported yet." + ) + } + + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) { + throw new DifPresentationExchangeError("Missing 'from' in submission requirement match") + } + + if (submissionRequirement.rule === Rules.All) { + const selectedSubmission = getSubmissionRequirementRuleAll( + submissionRequirement, + presentationDefinition, + selectResults + ) + submissionRequirements.push(selectedSubmission) + } else { + const selectedSubmission = getSubmissionRequirementRulePick( + submissionRequirement, + presentationDefinition, + selectResults + ) + + submissionRequirements.push(selectedSubmission) + } + } + + // Submission may have requirement that doesn't require a credential to be submitted (e.g. min: 0) + // We use minimization strategy, and thus only disclose the minimum amount of information + const requirementsWithCredentials = submissionRequirements.filter((requirement) => requirement.needsCount > 0) + + return requirementsWithCredentials +} + +function getSubmissionRequirementsForAllInputDescriptors( + inputDescriptors: Array | Array, + selectResults: W3cCredentialRecordSelectResults +): Array { + const submissionRequirements: Array = [] + + for (const inputDescriptor of inputDescriptors) { + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + submissionRequirements.push({ + rule: Rules.Pick, + needsCount: 1, // Every input descriptor is a distinct requirement, so the count is always 1, + submissionEntry: [submission], + isRequirementSatisfied: submission.verifiableCredentials.length >= 1, + }) + } + + return submissionRequirements +} + +function getSubmissionRequirementRuleAll( + submissionRequirement: SubmissionRequirement, + presentationDefinition: IPresentationDefinition, + selectResults: W3cCredentialRecordSelectResults +) { + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) + throw new DifPresentationExchangeError("Missing 'from' in submission requirement match.") + + const selectedSubmission: DifPexCredentialsForRequestRequirement = { + rule: Rules.All, + needsCount: 0, + name: submissionRequirement.name, + purpose: submissionRequirement.purpose, + submissionEntry: [], + isRequirementSatisfied: false, + } + + for (const inputDescriptor of presentationDefinition.input_descriptors) { + // We only want to get the submission if the input descriptor belongs to the group + if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue + + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + // Rule ALL, so for every input descriptor that matches in this group, we need to add it + selectedSubmission.needsCount += 1 + selectedSubmission.submissionEntry.push(submission) + } + + return { + ...selectedSubmission, + + // If all submissions have a credential, the requirement is satisfied + isRequirementSatisfied: selectedSubmission.submissionEntry.every( + (submission) => submission.verifiableCredentials.length >= 1 + ), + } +} + +function getSubmissionRequirementRulePick( + submissionRequirement: SubmissionRequirement, + presentationDefinition: IPresentationDefinition, + selectResults: W3cCredentialRecordSelectResults +) { + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) { + throw new DifPresentationExchangeError("Missing 'from' in submission requirement match.") + } + + const selectedSubmission: DifPexCredentialsForRequestRequirement = { + rule: Rules.Pick, + needsCount: submissionRequirement.count ?? submissionRequirement.min ?? 1, + name: submissionRequirement.name, + purpose: submissionRequirement.purpose, + // If there's no count, min, or max we assume one credential is required for submission + // however, the exact behavior is not specified in the spec + submissionEntry: [], + isRequirementSatisfied: false, + } + + const satisfiedSubmissions: Array = [] + const unsatisfiedSubmissions: Array = [] + + for (const inputDescriptor of presentationDefinition.input_descriptors) { + // We only want to get the submission if the input descriptor belongs to the group + if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue + + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + if (submission.verifiableCredentials.length >= 1) { + satisfiedSubmissions.push(submission) + } else { + unsatisfiedSubmissions.push(submission) + } + + // If we have found enough credentials to satisfy the requirement, we could stop + // but the user may not want the first x that match, so we continue and return all matches + // if (satisfiedSubmissions.length === selectedSubmission.needsCount) break + } + + return { + ...selectedSubmission, + + // If there are enough satisfied submissions, the requirement is satisfied + isRequirementSatisfied: satisfiedSubmissions.length >= selectedSubmission.needsCount, + + // if the requirement is satisfied, we only need to return the satisfied submissions + // however if the requirement is not satisfied, we include all entries so the wallet could + // render which credentials are missing. + submission: + satisfiedSubmissions.length >= selectedSubmission.needsCount + ? satisfiedSubmissions + : [...satisfiedSubmissions, ...unsatisfiedSubmissions], + } +} + +function getSubmissionForInputDescriptor( + inputDescriptor: InputDescriptorV1 | InputDescriptorV2, + selectResults: W3cCredentialRecordSelectResults +): DifPexCredentialsForRequestSubmissionEntry { + // https://github.com/Sphereon-Opensource/PEX/issues/116 + // If the input descriptor doesn't contain a name, the name of the match will be the id of the input descriptor that satisfied it + const matchesForInputDescriptor = selectResults.matches?.filter( + (m) => + m.name === inputDescriptor.id || + // FIXME: this is not collision proof as the name doesn't have to be unique + m.name === inputDescriptor.name + ) + + const submissionEntry: DifPexCredentialsForRequestSubmissionEntry = { + inputDescriptorId: inputDescriptor.id, + name: inputDescriptor.name, + purpose: inputDescriptor.purpose, + verifiableCredentials: [], + } + + // return early if no matches. + if (!matchesForInputDescriptor?.length) return submissionEntry + + // FIXME: This can return multiple credentials for multiple input_descriptors, + // which I think is a bug in the PEX library + // Extract all credentials from the match + const verifiableCredentials = matchesForInputDescriptor.flatMap((matchForInputDescriptor) => + extractCredentialsFromMatch(matchForInputDescriptor, selectResults.verifiableCredential) + ) + + submissionEntry.verifiableCredentials = verifiableCredentials + + return submissionEntry +} + +function extractCredentialsFromMatch( + match: SubmissionRequirementMatch, + availableCredentials?: Array +) { + const verifiableCredentials: Array = [] + + for (const vcPath of match.vc_path) { + const [verifiableCredential] = jp.query({ verifiableCredential: availableCredentials }, vcPath) as [ + W3cCredentialRecord + ] + verifiableCredentials.push(verifiableCredential) + } + + return verifiableCredentials +} + +/** + * Custom SelectResults that include the W3cCredentialRecord instead of the encoded verifiable credential + */ +export type W3cCredentialRecordSelectResults = Omit & { + verifiableCredential?: Array +} diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/index.ts b/packages/core/src/modules/dif-presentation-exchange/utils/index.ts new file mode 100644 index 0000000000..aaf44fa1b6 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/utils/index.ts @@ -0,0 +1,2 @@ +export * from './transform' +export * from './credentialSelection' diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts new file mode 100644 index 0000000000..e4d5f694c9 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts @@ -0,0 +1,78 @@ +import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '../../vc' +import type { + OriginalVerifiableCredential as SphereonOriginalVerifiableCredential, + W3CVerifiableCredential as SphereonW3cVerifiableCredential, + W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, +} from '@sphereon/ssi-types' + +import { JsonTransformer } from '../../../utils' +import { + W3cJsonLdVerifiableCredential, + W3cJsonLdVerifiablePresentation, + W3cJwtVerifiableCredential, + W3cJwtVerifiablePresentation, + ClaimFormat, +} from '../../vc' +import { DifPresentationExchangeError } from '../DifPresentationExchangeError' + +export function getSphereonOriginalVerifiableCredential( + w3cVerifiableCredential: W3cVerifiableCredential +): SphereonOriginalVerifiableCredential { + if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { + return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonOriginalVerifiableCredential + } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { + return w3cVerifiableCredential.serializedJwt + } else { + throw new DifPresentationExchangeError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} + +export function getSphereonW3cVerifiableCredential( + w3cVerifiableCredential: W3cVerifiableCredential +): SphereonW3cVerifiableCredential { + if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { + return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonW3cVerifiableCredential + } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { + return w3cVerifiableCredential.serializedJwt + } else { + throw new DifPresentationExchangeError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} + +export function getSphereonW3cVerifiablePresentation( + w3cVerifiablePresentation: W3cVerifiablePresentation +): SphereonW3cVerifiablePresentation { + if (w3cVerifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { + return JsonTransformer.toJSON(w3cVerifiablePresentation) as SphereonW3cVerifiablePresentation + } else if (w3cVerifiablePresentation instanceof W3cJwtVerifiablePresentation) { + return w3cVerifiablePresentation.serializedJwt + } else { + throw new DifPresentationExchangeError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} + +export function getW3cVerifiablePresentationInstance( + w3cVerifiablePresentation: SphereonW3cVerifiablePresentation +): W3cVerifiablePresentation { + if (typeof w3cVerifiablePresentation === 'string') { + return W3cJwtVerifiablePresentation.fromSerializedJwt(w3cVerifiablePresentation) + } else { + return JsonTransformer.fromJSON(w3cVerifiablePresentation, W3cJsonLdVerifiablePresentation) + } +} + +export function getW3cVerifiableCredentialInstance( + w3cVerifiableCredential: SphereonW3cVerifiableCredential +): W3cVerifiableCredential { + if (typeof w3cVerifiableCredential === 'string') { + return W3cJwtVerifiableCredential.fromSerializedJwt(w3cVerifiableCredential) + } else { + return JsonTransformer.fromJSON(w3cVerifiableCredential, W3cJsonLdVerifiableCredential) + } +} diff --git a/packages/core/src/modules/discover-features/DiscoverFeaturesApi.ts b/packages/core/src/modules/discover-features/DiscoverFeaturesApi.ts index 3d074f9d18..2ab4550b52 100644 --- a/packages/core/src/modules/discover-features/DiscoverFeaturesApi.ts +++ b/packages/core/src/modules/discover-features/DiscoverFeaturesApi.ts @@ -121,7 +121,10 @@ export class DiscoverFeaturesApi< // Return disclosures map((e) => e.payload.disclosures), // If we don't have an answer in timeoutMs miliseconds (no response, not supported, etc...) error - timeout(options.awaitDisclosuresTimeoutMs ?? 7000), // TODO: Harmonize default timeouts across the framework + timeout({ + first: options.awaitDisclosuresTimeoutMs ?? 7000, + meta: 'DiscoverFeaturesApi.queryFeatures', + }), // TODO: Harmonize default timeouts across the framework // We want to return false if an error occurred catchError(() => of([])) ) diff --git a/packages/core/src/modules/message-pickup/MessagePickupApi.ts b/packages/core/src/modules/message-pickup/MessagePickupApi.ts index f854f7e0be..b2e1950090 100644 --- a/packages/core/src/modules/message-pickup/MessagePickupApi.ts +++ b/packages/core/src/modules/message-pickup/MessagePickupApi.ts @@ -1,5 +1,6 @@ import type { - DeliverQueuedMessagesOptions, + DeliverMessagesOptions, + DeliverMessagesFromQueueOptions, PickupMessagesOptions, PickupMessagesReturnType, QueueMessageOptions, @@ -7,6 +8,7 @@ import type { SetLiveDeliveryModeOptions, SetLiveDeliveryModeReturnType, } from './MessagePickupApiOptions' +import type { MessagePickupSessionRole } from './MessagePickupSession' import type { V1MessagePickupProtocol, V2MessagePickupProtocol } from './protocol' import type { MessagePickupProtocol } from './protocol/MessagePickupProtocol' import type { MessagePickupRepository } from './storage/MessagePickupRepository' @@ -88,32 +90,79 @@ export class MessagePickupApi { connectionId: string protocolVersion: MessagePickupProtocolVersionType diff --git a/packages/core/src/modules/message-pickup/MessagePickupSession.ts b/packages/core/src/modules/message-pickup/MessagePickupSession.ts index eddca7ad4f..26cf49ff1c 100644 --- a/packages/core/src/modules/message-pickup/MessagePickupSession.ts +++ b/packages/core/src/modules/message-pickup/MessagePickupSession.ts @@ -6,6 +6,7 @@ export enum MessagePickupSessionRole { MessageHolder = 'MessageHolder', } export type MessagePickupSession = { + id: string connectionId: string protocolVersion: MessagePickupProtocolVersionType role: MessagePickupSessionRole diff --git a/packages/core/src/modules/message-pickup/protocol/BaseMessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/BaseMessagePickupProtocol.ts index a6d5f52703..686cdccc90 100644 --- a/packages/core/src/modules/message-pickup/protocol/BaseMessagePickupProtocol.ts +++ b/packages/core/src/modules/message-pickup/protocol/BaseMessagePickupProtocol.ts @@ -19,12 +19,12 @@ import type { DependencyManager } from '../../../plugins' export abstract class BaseMessagePickupProtocol implements MessagePickupProtocol { public abstract readonly version: string - public abstract pickupMessages( + public abstract createPickupMessage( agentContext: AgentContext, options: PickupMessagesProtocolOptions ): Promise> - public abstract deliverMessages( + public abstract createDeliveryMessage( agentContext: AgentContext, options: DeliverMessagesProtocolOptions ): Promise | void> diff --git a/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocol.ts index d4ea5f8861..df11b80547 100644 --- a/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocol.ts +++ b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocol.ts @@ -14,12 +14,12 @@ import type { DependencyManager } from '../../../plugins' export interface MessagePickupProtocol { readonly version: string - pickupMessages( + createPickupMessage( agentContext: AgentContext, options: PickupMessagesProtocolOptions ): Promise> - deliverMessages( + createDeliveryMessage( agentContext: AgentContext, options: DeliverMessagesProtocolOptions ): Promise | void> diff --git a/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts index 37b8feac30..41391f3286 100644 --- a/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts +++ b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts @@ -1,5 +1,6 @@ import type { AgentMessage } from '../../../agent/AgentMessage' import type { ConnectionRecord } from '../../connections' +import type { QueuedMessage } from '../storage' export interface PickupMessagesProtocolOptions { connectionRecord: ConnectionRecord @@ -9,6 +10,7 @@ export interface PickupMessagesProtocolOptions { export interface DeliverMessagesProtocolOptions { connectionRecord: ConnectionRecord + messages?: QueuedMessage[] recipientKey?: string batchSize?: number } diff --git a/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts index 33954964ea..0a9b5711ba 100644 --- a/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts +++ b/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts @@ -44,7 +44,7 @@ export class V1MessagePickupProtocol extends BaseMessagePickupProtocol { ) } - public async pickupMessages( + public async createPickupMessage( agentContext: AgentContext, options: PickupMessagesProtocolOptions ): Promise> { @@ -59,23 +59,25 @@ export class V1MessagePickupProtocol extends BaseMessagePickupProtocol { return { message } } - public async deliverMessages( + public async createDeliveryMessage( agentContext: AgentContext, options: DeliverMessagesProtocolOptions ): Promise | void> { - const { connectionRecord, batchSize } = options + const { connectionRecord, batchSize, messages } = options connectionRecord.assertReady() const pickupMessageQueue = agentContext.dependencyManager.resolve( InjectionSymbols.MessagePickupRepository ) - const messages = await pickupMessageQueue.takeFromQueue({ - connectionId: connectionRecord.id, - limit: batchSize, // TODO: Define as config parameter for message holder side - }) + const messagesToDeliver = + messages ?? + (await pickupMessageQueue.takeFromQueue({ + connectionId: connectionRecord.id, + limit: batchSize, // TODO: Define as config parameter for message holder side + })) - const batchMessages = messages.map( + const batchMessages = messagesToDeliver.map( (msg) => new BatchMessageMessage({ id: msg.id, @@ -83,7 +85,7 @@ export class V1MessagePickupProtocol extends BaseMessagePickupProtocol { }) ) - if (messages.length > 0) { + if (messagesToDeliver.length > 0) { const message = new V1BatchMessage({ messages: batchMessages, }) diff --git a/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts index 78936f8b8f..38a0194baa 100644 --- a/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts +++ b/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts @@ -74,7 +74,7 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { ) } - public async pickupMessages( + public async createPickupMessage( agentContext: AgentContext, options: PickupMessagesProtocolOptions ): Promise> { @@ -88,11 +88,11 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { return { message } } - public async deliverMessages( + public async createDeliveryMessage( agentContext: AgentContext, options: DeliverMessagesProtocolOptions ): Promise | void> { - const { connectionRecord, recipientKey } = options + const { connectionRecord, recipientKey, messages } = options connectionRecord.assertReady() const messagePickupRepository = agentContext.dependencyManager.resolve( @@ -100,18 +100,20 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { ) // Get available messages from queue, but don't delete them - const messages = await messagePickupRepository.takeFromQueue({ - connectionId: connectionRecord.id, - recipientKey, - limit: 10, // TODO: Define as config parameter - keepMessages: true, - }) - - if (messages.length === 0) { + const messagesToDeliver = + messages ?? + (await messagePickupRepository.takeFromQueue({ + connectionId: connectionRecord.id, + recipientKey, + limit: 10, // TODO: Define as config parameter + keepMessages: true, + })) + + if (messagesToDeliver.length === 0) { return } - const attachments = messages.map( + const attachments = messagesToDeliver.map( (msg) => new Attachment({ id: msg.id, diff --git a/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts b/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts index 424552bb00..ec8fd4b62d 100644 --- a/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts +++ b/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts @@ -29,7 +29,7 @@ const mockConnection = getMockConnection({ }) // Mock classes -jest.mock('../../../storage/InMemoryMessagePickupQueue') +jest.mock('../../../storage/InMemoryMessagePickupRepository') jest.mock('../../../../../agent/EventEmitter') jest.mock('../../../../../agent/MessageSender') jest.mock('../../../../connections/services/ConnectionService') @@ -71,7 +71,7 @@ const queuedMessages = [ { id: '3', encryptedMessage }, ] -describe('V2MessagePickupService', () => { +describe('V2MessagePickupProtocol', () => { let pickupProtocol: V2MessagePickupProtocol beforeEach(async () => { @@ -299,9 +299,9 @@ describe('V2MessagePickupService', () => { }) }) - describe('pickupMessages', () => { + describe('createPickupMessage', () => { it('creates a status request message', async () => { - const { message: statusRequestMessage } = await pickupProtocol.pickupMessages(agentContext, { + const { message: statusRequestMessage } = await pickupProtocol.createPickupMessage(agentContext, { connectionRecord: mockConnection, recipientKey: 'a-key', }) diff --git a/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts b/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts index bfec964e2c..7e726c7c8a 100644 --- a/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts +++ b/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts @@ -9,6 +9,7 @@ import { EventEmitter } from '../../../agent/EventEmitter' import { InjectionSymbols } from '../../../constants' import { injectable } from '../../../plugins' import { TransportEventTypes } from '../../../transport' +import { uuid } from '../../../utils/uuid' import { MessagePickupEventTypes } from '../MessagePickupEvents' /** @@ -42,7 +43,11 @@ export class MessagePickupSessionService { }) } - public getLiveSession( + public getLiveSession(agentContext: AgentContext, sessionId: string) { + return this.sessions.find((session) => session.id === sessionId) + } + + public getLiveSessionByConnectionId( agentContext: AgentContext, options: { connectionId: string; role?: MessagePickupSessionRole } ) { @@ -63,6 +68,7 @@ export class MessagePickupSessionService { this.removeLiveSession(agentContext, { connectionId }) const session = { + id: uuid(), connectionId, protocolVersion, role, diff --git a/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts b/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts index 5346de4e2a..84a3557f85 100644 --- a/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts +++ b/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts @@ -15,6 +15,7 @@ import { uuid } from '../../../utils/uuid' interface InMemoryQueuedMessage extends QueuedMessage { connectionId: string recipientKeys: string[] + state: 'pending' | 'sending' } @injectable() @@ -37,17 +38,28 @@ export class InMemoryMessagePickupRepository implements MessagePickupRepository return messages.length } - public takeFromQueue(options: TakeFromQueueOptions) { + public takeFromQueue(options: TakeFromQueueOptions): QueuedMessage[] { const { connectionId, recipientKey, limit, keepMessages } = options - const messages = this.messages.filter( + let messages = this.messages.filter( (msg) => - msg.connectionId === connectionId && (recipientKey === undefined || msg.recipientKeys.includes(recipientKey)) + msg.connectionId === connectionId && + msg.state === 'pending' && + (recipientKey === undefined || msg.recipientKeys.includes(recipientKey)) ) const messagesToTake = limit ?? messages.length + + messages = messages.slice(0, messagesToTake) + this.logger.debug(`Taking ${messagesToTake} messages from queue for connection ${connectionId}`) + // Mark taken messages in order to prevent them of being retrieved again + messages.forEach((msg) => { + const index = this.messages.findIndex((item) => item.id === msg.id) + if (index !== -1) this.messages[index].state = 'sending' + }) + if (!keepMessages) { this.removeMessages({ connectionId, messageIds: messages.map((msg) => msg.id) }) } @@ -64,6 +76,7 @@ export class InMemoryMessagePickupRepository implements MessagePickupRepository connectionId, encryptedMessage: payload, recipientKeys, + state: 'pending', }) return id diff --git a/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts b/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts index 108ccc6a3d..e580949c32 100644 --- a/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts +++ b/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts @@ -1,10 +1,8 @@ -import type { QueuedMessageState } from './QueuedMessageState' import type { EncryptedMessage } from '../../../types' export interface GetAvailableMessageCountOptions { connectionId: string recipientKey?: string - state?: QueuedMessageState } export interface TakeFromQueueOptions { diff --git a/packages/core/src/modules/message-pickup/storage/QueuedMessage.ts b/packages/core/src/modules/message-pickup/storage/QueuedMessage.ts index dae90e7d5f..b554e08184 100644 --- a/packages/core/src/modules/message-pickup/storage/QueuedMessage.ts +++ b/packages/core/src/modules/message-pickup/storage/QueuedMessage.ts @@ -3,5 +3,4 @@ import type { EncryptedMessage } from '../../../types' export type QueuedMessage = { id: string encryptedMessage: EncryptedMessage - state?: string } diff --git a/packages/core/src/modules/message-pickup/storage/QueuedMessageState.ts b/packages/core/src/modules/message-pickup/storage/QueuedMessageState.ts deleted file mode 100644 index dc4065d85a..0000000000 --- a/packages/core/src/modules/message-pickup/storage/QueuedMessageState.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum QueuedMessageState { - Pending = 'pending', - Sending = 'sending', - Delivered = 'delivered', -} diff --git a/packages/core/src/modules/message-pickup/storage/index.ts b/packages/core/src/modules/message-pickup/storage/index.ts index 6c733de292..1894b67d72 100644 --- a/packages/core/src/modules/message-pickup/storage/index.ts +++ b/packages/core/src/modules/message-pickup/storage/index.ts @@ -2,4 +2,3 @@ export * from './InMemoryMessagePickupRepository' export * from './MessagePickupRepository' export * from './MessagePickupRepositoryOptions' export * from './QueuedMessage' -export * from './QueuedMessageState' diff --git a/packages/core/src/modules/oob/OutOfBandApi.ts b/packages/core/src/modules/oob/OutOfBandApi.ts index b6f48b97e2..1a0ae3cf4a 100644 --- a/packages/core/src/modules/oob/OutOfBandApi.ts +++ b/packages/core/src/modules/oob/OutOfBandApi.ts @@ -77,6 +77,7 @@ interface BaseReceiveOutOfBandInvitationConfig { routing?: Routing acceptInvitationTimeoutMs?: number isImplicit?: boolean + ourDid?: string } export type ReceiveOutOfBandInvitationConfig = Omit @@ -479,6 +480,7 @@ export class OutOfBandApi { reuseConnection, routing, timeoutMs: config.acceptInvitationTimeoutMs, + ourDid: config.ourDid, }) } @@ -514,12 +516,13 @@ export class OutOfBandApi { */ routing?: Routing timeoutMs?: number + ourDid?: string } ) { const outOfBandRecord = await this.outOfBandService.getById(this.agentContext, outOfBandId) const { outOfBandInvitation } = outOfBandRecord - const { label, alias, imageUrl, autoAcceptConnection, reuseConnection } = config + const { label, alias, imageUrl, autoAcceptConnection, reuseConnection, ourDid } = config const services = outOfBandInvitation.getServices() const messages = outOfBandInvitation.getRequests() const timeoutMs = config.timeoutMs ?? 20000 @@ -585,6 +588,7 @@ export class OutOfBandApi { autoAcceptConnection, protocol: handshakeProtocol, routing, + ourDid, }) } @@ -871,7 +875,10 @@ export class OutOfBandApi { ), // If the event is found, we return the value true map(() => true), - timeout(15000), + timeout({ + first: 15000, + meta: 'OutOfBandApi.handleHandshakeReuse', + }), // If timeout is reached, we return false catchError(() => of(false)) ) diff --git a/packages/core/src/modules/oob/OutOfBandService.ts b/packages/core/src/modules/oob/OutOfBandService.ts index 1884cb694a..93fbb26c9e 100644 --- a/packages/core/src/modules/oob/OutOfBandService.ts +++ b/packages/core/src/modules/oob/OutOfBandService.ts @@ -67,7 +67,6 @@ export class OutOfBandService { // initiating the flow const outOfBandInvitation = new OutOfBandInvitation({ id: did, - label: '', services: [did], handshakeProtocols, }) diff --git a/packages/core/src/modules/oob/helpers.ts b/packages/core/src/modules/oob/helpers.ts index ccd35e7a31..be2fe3f0b4 100644 --- a/packages/core/src/modules/oob/helpers.ts +++ b/packages/core/src/modules/oob/helpers.ts @@ -45,7 +45,8 @@ export function convertToOldInvitation(newInvitation: OutOfBandInvitation) { if (typeof service === 'string') { options = { id: newInvitation.id, - label: newInvitation.label, + // label is optional + label: newInvitation.label ?? '', did: service, imageUrl: newInvitation.imageUrl, appendedAttachments: newInvitation.appendedAttachments, @@ -53,7 +54,8 @@ export function convertToOldInvitation(newInvitation: OutOfBandInvitation) { } else { options = { id: newInvitation.id, - label: newInvitation.label, + // label is optional + label: newInvitation.label ?? '', recipientKeys: service.recipientKeys.map(didKeyToVerkey), routingKeys: service.routingKeys?.map(didKeyToVerkey), serviceEndpoint: service.serviceEndpoint, diff --git a/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts index 80118119a0..d900284329 100644 --- a/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts +++ b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts @@ -17,7 +17,7 @@ import { OutOfBandDidCommService } from '../domain/OutOfBandDidCommService' export interface OutOfBandInvitationOptions { id?: string - label: string + label?: string goalCode?: string goal?: string accept?: string[] @@ -124,7 +124,7 @@ export class OutOfBandInvitation extends AgentMessage { public readonly type = OutOfBandInvitation.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/out-of-band/1.1/invitation') - public readonly label!: string + public readonly label?: string @Expose({ name: 'goal_code' }) public readonly goalCode?: string diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts new file mode 100644 index 0000000000..ca7e908a76 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts @@ -0,0 +1,73 @@ +import type { + DifPexInputDescriptorToCredentials, + DifPexCredentialsForRequest, + DifPresentationExchangeDefinitionV1, +} from '../../../dif-presentation-exchange' +import type { W3cJsonPresentation } from '../../../vc/models/presentation/W3cJsonPresentation' +import type { ProofFormat } from '../ProofFormat' + +export type DifPresentationExchangeProposal = DifPresentationExchangeDefinitionV1 + +export type DifPresentationExchangeRequest = { + options?: { + challenge?: string + domain?: string + } + presentation_definition: DifPresentationExchangeDefinitionV1 +} + +export type DifPresentationExchangePresentation = + | W3cJsonPresentation + // NOTE: this is not spec compliant, as it doesn't describe how to submit + // JWT VPs but to support JWT VPs we also allow the value to be a string + | string + +export interface DifPresentationExchangeProofFormat extends ProofFormat { + formatKey: 'presentationExchange' + + proofFormats: { + createProposal: { + presentationDefinition: DifPresentationExchangeDefinitionV1 + } + + acceptProposal: { + options?: { + challenge?: string + domain?: string + } + } + + createRequest: { + presentationDefinition: DifPresentationExchangeDefinitionV1 + options?: { + challenge?: string + domain?: string + } + } + + acceptRequest: { + credentials?: DifPexInputDescriptorToCredentials + } + + getCredentialsForRequest: { + input: never + // Presentation submission details which the options that are available + output: DifPexCredentialsForRequest + } + + selectCredentialsForRequest: { + input: never + // Input descriptor to credentials specifically details which credentials + // should be used for which input descriptor + output: { + credentials: DifPexInputDescriptorToCredentials + } + } + } + + formatData: { + proposal: DifPresentationExchangeProposal + request: DifPresentationExchangeRequest + presentation: DifPresentationExchangePresentation + } +} diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts new file mode 100644 index 0000000000..6e3a85438a --- /dev/null +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -0,0 +1,367 @@ +import type { + DifPresentationExchangePresentation, + DifPresentationExchangeProofFormat, + DifPresentationExchangeProposal, + DifPresentationExchangeRequest, +} from './DifPresentationExchangeProofFormat' +import type { AgentContext } from '../../../../agent' +import type { JsonValue } from '../../../../types' +import type { DifPexInputDescriptorToCredentials } from '../../../dif-presentation-exchange' +import type { W3cVerifiablePresentation, W3cVerifyPresentationResult } from '../../../vc' +import type { W3cJsonPresentation } from '../../../vc/models/presentation/W3cJsonPresentation' +import type { ProofFormatService } from '../ProofFormatService' +import type { + ProofFormatCreateProposalOptions, + ProofFormatCreateReturn, + ProofFormatProcessOptions, + ProofFormatAcceptProposalOptions, + FormatCreateRequestOptions, + ProofFormatAcceptRequestOptions, + ProofFormatProcessPresentationOptions, + ProofFormatGetCredentialsForRequestOptions, + ProofFormatSelectCredentialsForRequestOptions, + ProofFormatAutoRespondProposalOptions, + ProofFormatAutoRespondRequestOptions, + ProofFormatAutoRespondPresentationOptions, +} from '../ProofFormatServiceOptions' + +import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' +import { AriesFrameworkError } from '../../../../error' +import { deepEquality, JsonTransformer } from '../../../../utils' +import { DifPresentationExchangeService } from '../../../dif-presentation-exchange' +import { + W3cCredentialService, + ClaimFormat, + W3cJsonLdVerifiablePresentation, + W3cJwtVerifiablePresentation, +} from '../../../vc' +import { ProofFormatSpec } from '../../models' + +const PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL = 'dif/presentation-exchange/definitions@v1.0' +const PRESENTATION_EXCHANGE_PRESENTATION_REQUEST = 'dif/presentation-exchange/definitions@v1.0' +const PRESENTATION_EXCHANGE_PRESENTATION = 'dif/presentation-exchange/submission@v1.0' + +export class PresentationExchangeProofFormatService implements ProofFormatService { + public readonly formatKey = 'presentationExchange' as const + + private presentationExchangeService(agentContext: AgentContext) { + return agentContext.dependencyManager.resolve(DifPresentationExchangeService) + } + + public supportsFormat(formatIdentifier: string): boolean { + return [ + PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL, + PRESENTATION_EXCHANGE_PRESENTATION_REQUEST, + PRESENTATION_EXCHANGE_PRESENTATION, + ].includes(formatIdentifier) + } + + public async createProposal( + agentContext: AgentContext, + { proofFormats, attachmentId }: ProofFormatCreateProposalOptions + ): Promise { + const ps = this.presentationExchangeService(agentContext) + + const pexFormat = proofFormats.presentationExchange + if (!pexFormat) { + throw new AriesFrameworkError('Missing Presentation Exchange format in create proposal attachment format') + } + + const { presentationDefinition } = pexFormat + + ps.validatePresentationDefinition(presentationDefinition) + + const format = new ProofFormatSpec({ format: PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL, attachmentId }) + + const attachment = this.getFormatData(presentationDefinition, format.attachmentId) + + return { format, attachment } + } + + public async processProposal(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { + const ps = this.presentationExchangeService(agentContext) + const proposal = attachment.getDataAsJson() + ps.validatePresentationDefinition(proposal) + } + + public async acceptProposal( + agentContext: AgentContext, + { + attachmentId, + proposalAttachment, + proofFormats, + }: ProofFormatAcceptProposalOptions + ): Promise { + const ps = this.presentationExchangeService(agentContext) + + const presentationExchangeFormat = proofFormats?.presentationExchange + + const format = new ProofFormatSpec({ + format: PRESENTATION_EXCHANGE_PRESENTATION_REQUEST, + attachmentId, + }) + + const presentationDefinition = proposalAttachment.getDataAsJson() + ps.validatePresentationDefinition(presentationDefinition) + + const attachment = this.getFormatData( + { + presentation_definition: presentationDefinition, + options: { + // NOTE: we always want to include a challenge to prevent replay attacks + challenge: presentationExchangeFormat?.options?.challenge ?? (await agentContext.wallet.generateNonce()), + domain: presentationExchangeFormat?.options?.domain, + }, + } satisfies DifPresentationExchangeRequest, + format.attachmentId + ) + + return { format, attachment } + } + + public async createRequest( + agentContext: AgentContext, + { attachmentId, proofFormats }: FormatCreateRequestOptions + ): Promise { + const ps = this.presentationExchangeService(agentContext) + + const presentationExchangeFormat = proofFormats.presentationExchange + if (!presentationExchangeFormat) { + throw Error('Missing presentation exchange format in create request attachment format') + } + + const { presentationDefinition, options } = presentationExchangeFormat + + ps.validatePresentationDefinition(presentationDefinition) + + const format = new ProofFormatSpec({ + format: PRESENTATION_EXCHANGE_PRESENTATION_REQUEST, + attachmentId, + }) + + const attachment = this.getFormatData( + { + presentation_definition: presentationDefinition, + options: { + // NOTE: we always want to include a challenge to prevent replay attacks + challenge: options?.challenge ?? (await agentContext.wallet.generateNonce()), + domain: options?.domain, + }, + } satisfies DifPresentationExchangeRequest, + format.attachmentId + ) + + return { attachment, format } + } + + public async processRequest(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { + const ps = this.presentationExchangeService(agentContext) + const { presentation_definition: presentationDefinition } = + attachment.getDataAsJson() + ps.validatePresentationDefinition(presentationDefinition) + } + + public async acceptRequest( + agentContext: AgentContext, + { + attachmentId, + requestAttachment, + proofFormats, + }: ProofFormatAcceptRequestOptions + ): Promise { + const ps = this.presentationExchangeService(agentContext) + + const format = new ProofFormatSpec({ + format: PRESENTATION_EXCHANGE_PRESENTATION, + attachmentId, + }) + + const { presentation_definition: presentationDefinition, options } = + requestAttachment.getDataAsJson() + + const credentials: DifPexInputDescriptorToCredentials = proofFormats?.presentationExchange?.credentials ?? {} + if (Object.keys(credentials).length === 0) { + const { areRequirementsSatisfied, requirements } = await ps.getCredentialsForRequest( + agentContext, + presentationDefinition + ) + + if (!areRequirementsSatisfied) { + throw new AriesFrameworkError('Requirements of the presentation definition could not be satisfied') + } + + requirements.forEach((r) => { + r.submissionEntry.forEach((r) => { + credentials[r.inputDescriptorId] = r.verifiableCredentials.map((c) => c.credential) + }) + }) + } + + const presentation = await ps.createPresentation(agentContext, { + presentationDefinition, + credentialsForInputDescriptor: credentials, + challenge: options?.challenge, + domain: options?.domain, + }) + + if (presentation.verifiablePresentations.length > 1) { + throw new AriesFrameworkError('Invalid amount of verifiable presentations. Only one is allowed.') + } + + const firstPresentation = presentation.verifiablePresentations[0] + const attachmentData = firstPresentation.encoded as DifPresentationExchangePresentation + const attachment = this.getFormatData(attachmentData, format.attachmentId) + + return { attachment, format } + } + + public async processPresentation( + agentContext: AgentContext, + { requestAttachment, attachment }: ProofFormatProcessPresentationOptions + ): Promise { + const ps = this.presentationExchangeService(agentContext) + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + + const request = requestAttachment.getDataAsJson() + const presentation = attachment.getDataAsJson() + let parsedPresentation: W3cVerifiablePresentation + let jsonPresentation: W3cJsonPresentation + + // TODO: we should probably move this transformation logic into the VC module, so it + // can be reused in AFJ when we need to go from encoded -> parsed + if (typeof presentation === 'string') { + parsedPresentation = W3cJwtVerifiablePresentation.fromSerializedJwt(presentation) + jsonPresentation = parsedPresentation.presentation.toJSON() + } else { + parsedPresentation = JsonTransformer.fromJSON(presentation, W3cJsonLdVerifiablePresentation) + jsonPresentation = parsedPresentation.toJSON() + } + + if (!jsonPresentation.presentation_submission) { + agentContext.config.logger.error( + 'Received presentation in PEX proof format without presentation submission. This should not happen.' + ) + return false + } + + if (!request.options?.challenge) { + agentContext.config.logger.error( + 'Received presentation in PEX proof format without challenge. This should not happen.' + ) + return false + } + + try { + ps.validatePresentationDefinition(request.presentation_definition) + ps.validatePresentationSubmission(jsonPresentation.presentation_submission) + ps.validatePresentation(request.presentation_definition, parsedPresentation) + + let verificationResult: W3cVerifyPresentationResult + + // FIXME: for some reason it won't accept the input if it doesn't know + // whether it's a JWT or JSON-LD VP even though the input is the same. + // Not sure how to fix + if (parsedPresentation.claimFormat === ClaimFormat.JwtVp) { + verificationResult = await w3cCredentialService.verifyPresentation(agentContext, { + presentation: parsedPresentation, + challenge: request.options.challenge, + domain: request.options.domain, + }) + } else { + verificationResult = await w3cCredentialService.verifyPresentation(agentContext, { + presentation: parsedPresentation, + challenge: request.options.challenge, + domain: request.options.domain, + }) + } + + if (!verificationResult.isValid) { + agentContext.config.logger.error( + `Received presentation in PEX proof format that could not be verified: ${verificationResult.error}`, + { verificationResult } + ) + return false + } + + return true + } catch (e) { + agentContext.config.logger.error(`Failed to verify presentation in PEX proof format service: ${e.message}`, { + cause: e, + }) + return false + } + } + + public async getCredentialsForRequest( + agentContext: AgentContext, + { requestAttachment }: ProofFormatGetCredentialsForRequestOptions + ) { + const ps = this.presentationExchangeService(agentContext) + const { presentation_definition: presentationDefinition } = + requestAttachment.getDataAsJson() + + ps.validatePresentationDefinition(presentationDefinition) + + const presentationSubmission = await ps.getCredentialsForRequest(agentContext, presentationDefinition) + return presentationSubmission + } + + public async selectCredentialsForRequest( + agentContext: AgentContext, + { requestAttachment }: ProofFormatSelectCredentialsForRequestOptions + ) { + const ps = this.presentationExchangeService(agentContext) + const { presentation_definition: presentationDefinition } = + requestAttachment.getDataAsJson() + + const credentialsForRequest = await ps.getCredentialsForRequest(agentContext, presentationDefinition) + return { credentials: ps.selectCredentialsForRequest(credentialsForRequest) } + } + + public async shouldAutoRespondToProposal( + _agentContext: AgentContext, + { requestAttachment, proposalAttachment }: ProofFormatAutoRespondProposalOptions + ): Promise { + const proposalData = proposalAttachment.getDataAsJson() + const requestData = requestAttachment.getDataAsJson() + + return deepEquality(requestData.presentation_definition, proposalData) + } + + public async shouldAutoRespondToRequest( + _agentContext: AgentContext, + { requestAttachment, proposalAttachment }: ProofFormatAutoRespondRequestOptions + ): Promise { + const proposalData = proposalAttachment.getDataAsJson() + const requestData = requestAttachment.getDataAsJson() + + return deepEquality(requestData.presentation_definition, proposalData) + } + + /** + * + * The presentation is already verified in processPresentation, so we can just return true here. + * It's only an ack, so it's just that we received the presentation. + * + */ + public async shouldAutoRespondToPresentation( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _options: ProofFormatAutoRespondPresentationOptions + ): Promise { + return true + } + + private getFormatData(data: unknown, id: string): Attachment { + const attachment = new Attachment({ + id, + mimeType: 'application/json', + data: new AttachmentData({ + json: data as JsonValue, + }), + }) + + return attachment + } +} diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts new file mode 100644 index 0000000000..316927aaf8 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts @@ -0,0 +1,204 @@ +import type { DifPresentationExchangeDefinitionV1 } from '../../../../dif-presentation-exchange' +import type { ProofFormatService } from '../../ProofFormatService' +import type { DifPresentationExchangeProofFormat } from '../DifPresentationExchangeProofFormat' + +import { PresentationSubmissionLocation } from '@sphereon/pex' + +import { getIndySdkModules } from '../../../../../../../indy-sdk/tests/setupIndySdkModule' +import { agentDependencies, getAgentConfig } from '../../../../../../tests' +import { Agent } from '../../../../../agent/Agent' +import { DifPresentationExchangeModule, DifPresentationExchangeService } from '../../../../dif-presentation-exchange' +import { + W3cJsonLdVerifiableCredential, + W3cCredentialRecord, + W3cCredentialRepository, + CREDENTIALS_CONTEXT_V1_URL, + W3cJsonLdVerifiablePresentation, +} from '../../../../vc' +import { ProofsModule } from '../../../ProofsModule' +import { ProofState } from '../../../models' +import { V2ProofProtocol } from '../../../protocol' +import { ProofExchangeRecord } from '../../../repository' +import { PresentationExchangeProofFormatService } from '../DifPresentationExchangeProofFormatService' + +const mockProofRecord = () => + new ProofExchangeRecord({ + state: ProofState.ProposalSent, + threadId: 'add7e1a0-109e-4f37-9caa-cfd0fcdfe540', + protocolVersion: 'v2', + }) + +const mockPresentationDefinition = (): DifPresentationExchangeDefinitionV1 => ({ + id: '32f54163-7166-48f1-93d8-ff217bdb0653', + input_descriptors: [ + { + schema: [{ uri: 'https://www.w3.org/2018/credentials/examples/v1' }], + id: 'wa_driver_license', + name: 'Washington State Business License', + purpose: 'We can only allow licensed Washington State business representatives into the WA Business Conference', + constraints: { + fields: [ + { + path: ['$.credentialSubject.id'], + }, + ], + }, + }, + ], +}) + +const mockCredentialRecord = new W3cCredentialRecord({ + tags: {}, + credential: new W3cJsonLdVerifiableCredential({ + id: 'did:some:id', + context: [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + }, + proof: { + type: 'Ed25519Signature2020', + created: '2021-11-13T18:19:39Z', + verificationMethod: 'https://example.edu/issuers/14#key-1', + proofPurpose: 'assertionMethod', + proofValue: 'z58DAdFfa9SkqZMVPxAQpic7ndSayn1PzZs6ZjWp1CktyGesjuTSwRdoWhAfGFCF5bppETSTojQCrfFPP2oumHKtz', + }, + }), +}) + +const presentationSubmission = { id: 'did:id', definition_id: 'my-id', descriptor_map: [] } +jest.spyOn(W3cCredentialRepository.prototype, 'findByQuery').mockResolvedValue([mockCredentialRecord]) +jest.spyOn(DifPresentationExchangeService.prototype, 'createPresentation').mockResolvedValue({ + presentationSubmission, + verifiablePresentations: [ + new W3cJsonLdVerifiablePresentation({ + verifiableCredential: [mockCredentialRecord.credential], + proof: { + type: 'Ed25519Signature2020', + created: '2021-11-13T18:19:39Z', + verificationMethod: 'https://example.edu/issuers/14#key-1', + proofPurpose: 'assertionMethod', + proofValue: 'z58DAdFfa9SkqZMVPxAQpic7ndSayn1PzZs6ZjWp1CktyGesjuTSwRdoWhAfGFCF5bppETSTojQCrfFPP2oumHKtz', + }, + }), + ], + presentationSubmissionLocation: PresentationSubmissionLocation.PRESENTATION, +}) + +describe('Presentation Exchange ProofFormatService', () => { + let pexFormatService: ProofFormatService + let agent: Agent + + beforeAll(async () => { + agent = new Agent({ + config: getAgentConfig('PresentationExchangeProofFormatService'), + modules: { + someModule: new DifPresentationExchangeModule(), + proofs: new ProofsModule({ + proofProtocols: [new V2ProofProtocol({ proofFormats: [new PresentationExchangeProofFormatService()] })], + }), + ...getIndySdkModules(), + }, + dependencies: agentDependencies, + }) + + await agent.initialize() + + pexFormatService = agent.dependencyManager.resolve(PresentationExchangeProofFormatService) + }) + + describe('Create Presentation Exchange Proof Proposal / Request', () => { + test('Creates Presentation Exchange Proposal', async () => { + const presentationDefinition = mockPresentationDefinition() + const { format, attachment } = await pexFormatService.createProposal(agent.context, { + proofRecord: mockProofRecord(), + proofFormats: { presentationExchange: { presentationDefinition } }, + }) + + expect(attachment).toMatchObject({ + id: expect.any(String), + mimeType: 'application/json', + data: { + json: presentationDefinition, + }, + }) + + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }) + }) + + test('Creates Presentation Exchange Request', async () => { + const presentationDefinition = mockPresentationDefinition() + const { format, attachment } = await pexFormatService.createRequest(agent.context, { + proofRecord: mockProofRecord(), + proofFormats: { presentationExchange: { presentationDefinition } }, + }) + + expect(attachment).toMatchObject({ + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + options: { + challenge: expect.any(String), + }, + presentation_definition: presentationDefinition, + }, + }, + }) + + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }) + }) + }) + + describe('Accept Proof Request', () => { + test('Accept a Presentation Exchange Proof Request', async () => { + const presentationDefinition = mockPresentationDefinition() + const { attachment: requestAttachment } = await pexFormatService.createRequest(agent.context, { + proofRecord: mockProofRecord(), + proofFormats: { presentationExchange: { presentationDefinition } }, + }) + + const { attachment, format } = await pexFormatService.acceptRequest(agent.context, { + proofRecord: mockProofRecord(), + requestAttachment, + }) + + expect(attachment).toMatchObject({ + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + '@context': expect.any(Array), + type: expect.any(Array), + verifiableCredential: [ + { + '@context': expect.any(Array), + id: expect.any(String), + type: expect.any(Array), + issuer: expect.any(String), + issuanceDate: expect.any(String), + credentialSubject: { + id: expect.any(String), + }, + proof: expect.any(Object), + }, + ], + }, + }, + }) + + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/submission@v1.0', + }) + }) + }) +}) diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/index.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/index.ts new file mode 100644 index 0000000000..b8a8c35e4e --- /dev/null +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/index.ts @@ -0,0 +1,2 @@ +export * from './DifPresentationExchangeProofFormat' +export * from './DifPresentationExchangeProofFormatService' diff --git a/packages/core/src/modules/proofs/formats/index.ts b/packages/core/src/modules/proofs/formats/index.ts index a28e77d623..a2cc952c57 100644 --- a/packages/core/src/modules/proofs/formats/index.ts +++ b/packages/core/src/modules/proofs/formats/index.ts @@ -2,6 +2,8 @@ export * from './ProofFormat' export * from './ProofFormatService' export * from './ProofFormatServiceOptions' +export * from './dif-presentation-exchange' + import * as ProofFormatServiceOptions from './ProofFormatServiceOptions' export { ProofFormatServiceOptions } diff --git a/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts b/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts index ffec69b8a3..3ce7e2c9d8 100644 --- a/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts +++ b/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts @@ -42,7 +42,7 @@ import { ProofsModuleConfig } from '../../ProofsModuleConfig' import { PresentationProblemReportReason } from '../../errors/PresentationProblemReportReason' import { AutoAcceptProof, ProofState } from '../../models' import { ProofExchangeRecord, ProofRepository } from '../../repository' -import { composeAutoAccept } from '../../utils/composeAutoAccept' +import { composeAutoAccept } from '../../utils' import { BaseProofProtocol } from '../BaseProofProtocol' import { ProofFormatCoordinator } from './ProofFormatCoordinator' diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts new file mode 100644 index 0000000000..0b3d8c39b9 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts @@ -0,0 +1,13 @@ +import type { InputDescriptorV1 } from '@sphereon/pex-models' + +export const TEST_INPUT_DESCRIPTORS_CITIZENSHIP = { + constraints: { + fields: [ + { + path: ['$.credentialSubject.degree.type'], + }, + ], + }, + id: 'citizenship_input_1', + schema: [{ uri: 'https://www.w3.org/2018/credentials/examples/v1' }], +} satisfies InputDescriptorV1 diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts new file mode 100644 index 0000000000..a0901029a6 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts @@ -0,0 +1,451 @@ +import type { getJsonLdModules } from '../../../../../../tests' +import type { Agent } from '../../../../../agent/Agent' + +import { waitForCredentialRecord, setupJsonLdTests, waitForProofExchangeRecord } from '../../../../../../tests' +import testLogger from '../../../../../../tests/logger' +import { KeyType } from '../../../../../crypto' +import { DidCommMessageRepository } from '../../../../../storage' +import { TypedArrayEncoder } from '../../../../../utils' +import { AutoAcceptCredential, CredentialState } from '../../../../credentials' +import { CREDENTIALS_CONTEXT_V1_URL } from '../../../../vc' +import { ProofState } from '../../../models/ProofState' +import { V2PresentationMessage, V2RequestPresentationMessage } from '../messages' +import { V2ProposePresentationMessage } from '../messages/V2ProposePresentationMessage' + +import { TEST_INPUT_DESCRIPTORS_CITIZENSHIP } from './fixtures' + +const jsonld = { + credential: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + }, + options: { + proofType: 'Ed25519Signature2018', + proofPurpose: 'assertionMethod', + }, +} + +describe('Present Proof', () => { + let proverAgent: Agent> + let issuerAgent: Agent> + let verifierAgent: Agent> + + let issuerProverConnectionId: string + let proverVerifierConnectionId: string + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + holderAgent: proverAgent, + issuerAgent, + verifierAgent, + issuerHolderConnectionId: issuerProverConnectionId, + holderVerifierConnectionId: proverVerifierConnectionId, + } = await setupJsonLdTests({ + holderName: 'presentation exchange prover agent', + issuerName: 'presentation exchange issuer agent', + verifierName: 'presentation exchange verifier agent', + createConnections: true, + autoAcceptCredentials: AutoAcceptCredential.Always, + })) + + await issuerAgent.wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + keyType: KeyType.Ed25519, + }) + + await proverAgent.wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + keyType: KeyType.Ed25519, + }) + + await issuerAgent.credentials.offerCredential({ + connectionId: issuerProverConnectionId, + protocolVersion: 'v2', + credentialFormats: { jsonld }, + }) + + await waitForCredentialRecord(proverAgent, { state: CredentialState.Done }) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await proverAgent.shutdown() + await proverAgent.wallet.delete() + await verifierAgent.shutdown() + await verifierAgent.wallet.delete() + }) + + test(`Prover Creates and sends Proof Proposal to a Verifier`, async () => { + testLogger.test('Prover sends proof proposal to a Verifier') + + const verifierPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + state: ProofState.ProposalReceived, + }) + + await proverAgent.proofs.proposeProof({ + connectionId: proverVerifierConnectionId, + protocolVersion: 'v2', + proofFormats: { + presentationExchange: { + presentationDefinition: { + id: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + input_descriptors: [TEST_INPUT_DESCRIPTORS_CITIZENSHIP], + }, + }, + }, + comment: 'V2 Presentation Exchange propose proof test', + }) + + testLogger.test('Verifier waits for presentation from the Prover') + const verifierProofExchangeRecord = await verifierPresentationRecordPromise + + const didCommMessageRepository = + proverAgent.dependencyManager.resolve(DidCommMessageRepository) + + const proposal = await didCommMessageRepository.findAgentMessage(verifierAgent.context, { + associatedRecordId: verifierProofExchangeRecord.id, + messageClass: V2ProposePresentationMessage, + }) + + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }, + ], + proposalAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + input_descriptors: expect.any(Array), + }, + }, + }, + ], + id: expect.any(String), + comment: 'V2 Presentation Exchange propose proof test', + }) + expect(verifierProofExchangeRecord.id).not.toBeNull() + expect(verifierProofExchangeRecord).toMatchObject({ + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v2', + }) + }) + + test(`Verifier accepts the Proposal send by the Prover`, async () => { + testLogger.test('Prover sends proof proposal to a Verifier') + + let proverProofExchangeRecord = await proverAgent.proofs.proposeProof({ + connectionId: proverVerifierConnectionId, + protocolVersion: 'v2', + proofFormats: { + presentationExchange: { + presentationDefinition: { + id: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + input_descriptors: [TEST_INPUT_DESCRIPTORS_CITIZENSHIP], + }, + }, + }, + comment: 'V2 Presentation Exchange propose proof test', + }) + + const verifierPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + state: ProofState.ProposalReceived, + }) + + const proverPresentationRecordPromise = waitForProofExchangeRecord(proverAgent, { + threadId: proverProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Verifier accepts presentation proposal from the Prover') + let verifierProofExchangeRecord = await verifierPresentationRecordPromise + verifierProofExchangeRecord = await verifierAgent.proofs.acceptProposal({ + proofRecordId: verifierProofExchangeRecord.id, + }) + + testLogger.test('Prover waits for proof request from the Verifier') + proverProofExchangeRecord = await proverPresentationRecordPromise + + const didCommMessageRepository = + proverAgent.dependencyManager.resolve(DidCommMessageRepository) + + const request = await didCommMessageRepository.findAgentMessage(proverAgent.context, { + associatedRecordId: proverProofExchangeRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + id: expect.any(String), + formats: [ + { + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + presentation_definition: { + id: expect.any(String), + input_descriptors: [ + { + id: TEST_INPUT_DESCRIPTORS_CITIZENSHIP.id, + constraints: { + fields: TEST_INPUT_DESCRIPTORS_CITIZENSHIP.constraints.fields, + }, + }, + ], + }, + }, + }, + }, + ], + }) + + expect(proverProofExchangeRecord).toMatchObject({ + id: expect.any(String), + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + }) + + test(`Prover accepts presentation request from the Verifier`, async () => { + testLogger.test('Prover sends proof proposal to a Verifier') + + let proverProofExchangeRecord = await proverAgent.proofs.proposeProof({ + connectionId: proverVerifierConnectionId, + protocolVersion: 'v2', + proofFormats: { + presentationExchange: { + presentationDefinition: { + id: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + input_descriptors: [TEST_INPUT_DESCRIPTORS_CITIZENSHIP], + }, + }, + }, + comment: 'V2 Presentation Exchange propose proof test', + }) + + const verifierProposalReceivedPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + state: ProofState.ProposalReceived, + }) + + const proverPresentationRecordPromise = waitForProofExchangeRecord(proverAgent, { + threadId: proverProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Verifier accepts presentation proposal from the Prover') + let verifierProofExchangeRecord = await verifierProposalReceivedPresentationRecordPromise + verifierProofExchangeRecord = await verifierAgent.proofs.acceptProposal({ + proofRecordId: verifierProofExchangeRecord.id, + }) + + testLogger.test('Prover waits for proof request from the Verifier') + proverProofExchangeRecord = await proverPresentationRecordPromise + + // Prover retrieves the requested credentials and accepts the presentation request + testLogger.test('Prover accepts presentation request from Verifier') + + const verifierPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await proverAgent.proofs.acceptRequest({ + proofRecordId: proverProofExchangeRecord.id, + }) + + // Verifier waits for the presentation from the Prover + testLogger.test('Verifier waits for presentation from the Prover') + verifierProofExchangeRecord = await verifierPresentationRecordPromise + + const didCommMessageRepository = + verifierAgent.dependencyManager.resolve(DidCommMessageRepository) + + const presentation = await didCommMessageRepository.findAgentMessage(verifierAgent.context, { + associatedRecordId: verifierProofExchangeRecord.id, + messageClass: V2PresentationMessage, + }) + + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/submission@v1.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + '@context': expect.any(Array), + type: expect.any(Array), + presentation_submission: { + id: expect.any(String), + definition_id: expect.any(String), + descriptor_map: [ + { + id: 'citizenship_input_1', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + ], + }, + verifiableCredential: [ + { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://www.w3.org/2018/credentials/examples/v1', + ], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: expect.any(String), + issuanceDate: expect.any(String), + credentialSubject: { + id: expect.any(String), + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + proof: { + verificationMethod: expect.any(String), + type: 'Ed25519Signature2018', + created: expect.any(String), + proofPurpose: 'assertionMethod', + jws: expect.any(String), + }, + }, + ], + proof: { + verificationMethod: expect.any(String), + type: 'Ed25519Signature2018', + created: expect.any(String), + proofPurpose: 'authentication', + challenge: expect.any(String), + jws: expect.any(String), + }, + }, + }, + }, + ], + id: expect.any(String), + thread: { + threadId: verifierProofExchangeRecord.threadId, + }, + }) + + expect(verifierProofExchangeRecord.id).not.toBeNull() + expect(verifierProofExchangeRecord).toMatchObject({ + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + }) + + test(`Verifier accepts the presentation provided by the Prover`, async () => { + testLogger.test('Prover sends proof proposal to a Verifier') + + let proverProofExchangeRecord = await proverAgent.proofs.proposeProof({ + connectionId: proverVerifierConnectionId, + protocolVersion: 'v2', + proofFormats: { + presentationExchange: { + presentationDefinition: { + id: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + input_descriptors: [TEST_INPUT_DESCRIPTORS_CITIZENSHIP], + }, + }, + }, + comment: 'V2 Presentation Exchange propose proof test', + }) + + const verifierProposalReceivedPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + state: ProofState.ProposalReceived, + }) + + const proverPresentationRecordPromise = waitForProofExchangeRecord(proverAgent, { + threadId: proverProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Verifier accepts presentation proposal from the Prover') + let verifierProofExchangeRecord = await verifierProposalReceivedPresentationRecordPromise + verifierProofExchangeRecord = await verifierAgent.proofs.acceptProposal({ + proofRecordId: verifierProofExchangeRecord.id, + }) + + testLogger.test('Prover waits for proof request from the Verifier') + proverProofExchangeRecord = await proverPresentationRecordPromise + + // Prover retrieves the requested credentials and accepts the presentation request + testLogger.test('Prover accepts presentation request from Verifier') + + const verifierPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await proverAgent.proofs.acceptRequest({ + proofRecordId: proverProofExchangeRecord.id, + }) + + // Verifier waits for the presentation from the Prover + testLogger.test('Verifier waits for presentation from the Prover') + verifierProofExchangeRecord = await verifierPresentationRecordPromise + + const proverProofExchangeRecordPromise = waitForProofExchangeRecord(proverAgent, { + threadId: proverProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Verifier accepts the presentation provided by by the Prover + testLogger.test('Verifier accepts the presentation provided by the Prover') + await verifierAgent.proofs.acceptPresentation({ proofRecordId: verifierProofExchangeRecord.id }) + + // Prover waits until she received a presentation acknowledgement + testLogger.test('Prover waits until she receives a presentation acknowledgement') + proverProofExchangeRecord = await proverProofExchangeRecordPromise + + expect(verifierProofExchangeRecord).toMatchObject({ + id: expect.any(String), + createdAt: expect.any(Date), + threadId: proverProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(proverProofExchangeRecord).toMatchObject({ + id: expect.any(String), + createdAt: expect.any(Date), + threadId: verifierProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) +}) diff --git a/packages/core/src/modules/proofs/utils/index.ts b/packages/core/src/modules/proofs/utils/index.ts new file mode 100644 index 0000000000..e9685c62fe --- /dev/null +++ b/packages/core/src/modules/proofs/utils/index.ts @@ -0,0 +1 @@ +export * from './composeAutoAccept' diff --git a/packages/core/src/modules/routing/MediationRecipientApi.ts b/packages/core/src/modules/routing/MediationRecipientApi.ts index c86d917638..be8e5d137d 100644 --- a/packages/core/src/modules/routing/MediationRecipientApi.ts +++ b/packages/core/src/modules/routing/MediationRecipientApi.ts @@ -418,7 +418,10 @@ export class MediationRecipientApi { // Only wait for first event that matches the criteria first(), // Do not wait for longer than specified timeout - timeout(timeoutMs) + timeout({ + first: timeoutMs, + meta: 'MediationRecipientApi.requestAndAwaitGrant', + }) ) .subscribe(subject) diff --git a/packages/core/src/modules/routing/services/MediationRecipientService.ts b/packages/core/src/modules/routing/services/MediationRecipientService.ts index c61e50bd54..dc464289b8 100644 --- a/packages/core/src/modules/routing/services/MediationRecipientService.ts +++ b/packages/core/src/modules/routing/services/MediationRecipientService.ts @@ -186,7 +186,10 @@ export class MediationRecipientService { // Only wait for first event that matches the criteria first(), // Do not wait for longer than specified timeout - timeout(timeoutMs) + timeout({ + first: timeoutMs, + meta: 'MediationRecipientService.keylistUpdateAndAwait', + }) ) .subscribe(subject) diff --git a/packages/core/src/modules/routing/services/MediatorService.ts b/packages/core/src/modules/routing/services/MediatorService.ts index d54b3720c3..5645df7ba2 100644 --- a/packages/core/src/modules/routing/services/MediatorService.ts +++ b/packages/core/src/modules/routing/services/MediatorService.ts @@ -15,7 +15,8 @@ import { injectable, inject } from '../../../plugins' import { ConnectionService } from '../../connections' import { ConnectionMetadataKeys } from '../../connections/repository/ConnectionMetadataTypes' import { didKeyToVerkey, isDidKey, verkeyToDidKey } from '../../dids/helpers' -import { MessagePickupApi } from '../../message-pìckup' +import { MessagePickupApi } from '../../message-pickup' +import { MessagePickupSessionRole } from '../../message-pickup/MessagePickupSession' import { MediatorModuleConfig } from '../MediatorModuleConfig' import { MessageForwardingStrategy } from '../MessageForwardingStrategy' import { RoutingEventTypes } from '../RoutingEvents' @@ -99,17 +100,24 @@ export class MediatorService { message: message.message, }) break - case MessageForwardingStrategy.QueueAndDeliver: + case MessageForwardingStrategy.QueueAndDeliver: { await this.messagePickupApi.queueMessage({ connectionId: mediationRecord.connectionId, recipientKeys: [verkeyToDidKey(message.to)], message: message.message, }) - await this.messagePickupApi.deliverQueuedMessages({ + const session = await this.messagePickupApi.getLiveModeSession({ connectionId: mediationRecord.connectionId, - recipientKey: verkeyToDidKey(message.to), + role: MessagePickupSessionRole.MessageHolder, }) + if (session) { + await this.messagePickupApi.deliverMessagesFromQueue({ + pickupSessionId: session.id, + recipientKey: verkeyToDidKey(message.to), + }) + } break + } case MessageForwardingStrategy.DeliverOnly: // The message inside the forward message is packed so we just send the packed // message to the connection associated with it diff --git a/packages/core/src/modules/routing/services/helpers.ts b/packages/core/src/modules/routing/services/helpers.ts new file mode 100644 index 0000000000..0a3bb4fe42 --- /dev/null +++ b/packages/core/src/modules/routing/services/helpers.ts @@ -0,0 +1,13 @@ +import type { AgentContext } from '../../../agent' +import type { DidDocument } from '../../dids' + +import { MediationRecipientService } from './MediationRecipientService' + +export async function getMediationRecordForDidDocument(agentContext: AgentContext, didDocument: DidDocument) { + const [mediatorRecord] = await agentContext.dependencyManager + .resolve(MediationRecipientService) + .findAllMediatorsByQuery(agentContext, { + recipientKeys: didDocument.recipientKeys.map((key) => key.publicKeyBase58), + }) + return mediatorRecord +} diff --git a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts index 841271fa39..f2777463dc 100644 --- a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts +++ b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts @@ -9,6 +9,7 @@ import type { W3cJsonLdVerifyPresentationOptions, } from '../W3cCredentialServiceOptions' import type { W3cVerifyCredentialResult, W3cVerifyPresentationResult } from '../models' +import type { W3cJsonCredential } from '../models/credential/W3cJsonCredential' import { createWalletKeyPairClass } from '../../../crypto/WalletKeyPair' import { AriesFrameworkError } from '../../../error' @@ -108,8 +109,9 @@ export class W3cJsonLdCredentialService { credential: JsonTransformer.toJSON(options.credential), suite: suites, documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), - checkStatus: () => { - if (verifyCredentialStatus) { + checkStatus: ({ credential }: { credential: W3cJsonCredential }) => { + // Only throw error if credentialStatus is present + if (verifyCredentialStatus && 'credentialStatus' in credential) { throw new AriesFrameworkError( 'Verifying credential status for JSON-LD credentials is currently not supported' ) @@ -129,6 +131,12 @@ export class W3cJsonLdCredentialService { const { verified: isValid, ...remainingResult } = result + if (!isValid) { + agentContext.config.logger.debug(`Credential verification failed: ${result.error?.message}`, { + stack: result.error?.stack, + }) + } + // We map the result to our own result type to make it easier to work with // however, for now we just add a single vcJs validation result as we don't // have access to the internal validation results of vc-js diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts b/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts index 80f4e42526..0e1a416dd2 100644 --- a/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts +++ b/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts @@ -156,6 +156,27 @@ describe('W3cJsonLdCredentialsService', () => { vcJs: { isValid: true, results: expect.any(Array), + log: [ + { + id: 'expiration', + valid: true, + }, + { + id: 'valid_signature', + valid: true, + }, + { + id: 'issuer_did_resolves', + valid: true, + }, + { + id: 'revocation_status', + valid: true, + }, + ], + statusResult: { + verified: true, + }, }, }, }) diff --git a/packages/core/src/modules/vc/data-integrity/deriveProof.ts b/packages/core/src/modules/vc/data-integrity/deriveProof.ts index a98bf1a064..fe89595115 100644 --- a/packages/core/src/modules/vc/data-integrity/deriveProof.ts +++ b/packages/core/src/modules/vc/data-integrity/deriveProof.ts @@ -38,7 +38,7 @@ export interface W3cJsonLdDeriveProofOptions { export const deriveProof = async ( proofDocument: JsonObject, revealDocument: JsonObject, - { suite, skipProofCompaction, documentLoader, expansionMap, nonce }: any + { suite, skipProofCompaction, documentLoader, nonce }: any ): Promise => { if (!suite) { throw new TypeError('"options.suite" is required.') @@ -52,7 +52,6 @@ export const deriveProof = async ( document: proofDocument, proofType: suite.supportedDeriveProofType, documentLoader, - expansionMap, }) if (proofs.length === 0) { @@ -64,7 +63,7 @@ export const deriveProof = async ( proof: proofs[0], revealDocument, documentLoader, - expansionMap, + nonce, }) @@ -82,7 +81,6 @@ export const deriveProof = async ( proof, revealDocument, documentLoader, - expansionMap, }) derivedProof.proof.push(additionalDerivedProofValue.proof) } @@ -99,7 +97,6 @@ export const deriveProof = async ( // account for type-scoped `proof` definition by getting document types const { types, alias } = await getTypeInfo(derivedProof.document, { documentLoader, - expansionMap, }) expandedProof['@type'] = types @@ -108,7 +105,6 @@ export const deriveProof = async ( const compactProof = await jsonld.compact(expandedProof, ctx, { documentLoader, - expansionMap, compactToRelative: false, }) diff --git a/packages/core/src/modules/vc/data-integrity/jsonldUtil.ts b/packages/core/src/modules/vc/data-integrity/jsonldUtil.ts index 7fbd3a753c..6975e92c99 100644 --- a/packages/core/src/modules/vc/data-integrity/jsonldUtil.ts +++ b/packages/core/src/modules/vc/data-integrity/jsonldUtil.ts @@ -67,7 +67,7 @@ const PROOF_PROPERTY = 'proof' * @returns {GetProofsResult} An object containing the matched proofs and the JSON-LD document */ export const getProofs = async (options: GetProofsOptions): Promise => { - const { proofType, skipProofCompaction, documentLoader, expansionMap } = options + const { proofType, skipProofCompaction, documentLoader } = options let { document } = options let proofs @@ -76,7 +76,6 @@ export const getProofs = async (options: GetProofsOptions): Promise => { - const { documentLoader, expansionMap } = options + const { documentLoader } = options // determine `@type` alias, if any // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -124,7 +123,6 @@ export const getTypeInfo = async ( const compacted = await jsonld.compact({ '@type': '_:b0' }, context, { documentLoader, - expansionMap, }) delete compacted['@context'] @@ -139,7 +137,7 @@ export const getTypeInfo = async ( // @ts-ignore - needed because getValues is not part of the public API. toExpand['@type'] = jsonld.getValues(document, '@type').concat(jsonld.getValues(document, alias)) - const expanded = (await jsonld.expand(toExpand, { documentLoader, expansionMap }))[0] || {} + const expanded = (await jsonld.expand(toExpand, { documentLoader }))[0] || {} // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - needed because getValues is not part of the public API. diff --git a/packages/core/src/modules/vc/data-integrity/models/GetProofsOptions.ts b/packages/core/src/modules/vc/data-integrity/models/GetProofsOptions.ts index 0ed1214404..76e9dfccb5 100644 --- a/packages/core/src/modules/vc/data-integrity/models/GetProofsOptions.ts +++ b/packages/core/src/modules/vc/data-integrity/models/GetProofsOptions.ts @@ -30,10 +30,6 @@ export interface GetProofsOptions { * Optional custom document loader */ documentLoader?(): DocumentLoader - /** - * Optional expansion map - */ - expansionMap?(): () => void /** * Optional property to indicate whether to skip compacting the resulting proof */ diff --git a/packages/core/src/modules/vc/data-integrity/models/GetTypeOptions.ts b/packages/core/src/modules/vc/data-integrity/models/GetTypeOptions.ts index f39854f02b..da40540e38 100644 --- a/packages/core/src/modules/vc/data-integrity/models/GetTypeOptions.ts +++ b/packages/core/src/modules/vc/data-integrity/models/GetTypeOptions.ts @@ -21,8 +21,4 @@ export interface GetTypeOptions { * Optional custom document loader */ documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - expansionMap?: () => void } diff --git a/packages/core/src/modules/vc/data-integrity/proof-purposes/CredentialIssuancePurpose.ts b/packages/core/src/modules/vc/data-integrity/proof-purposes/CredentialIssuancePurpose.ts index 950f5b3790..3f79fe92b7 100644 --- a/packages/core/src/modules/vc/data-integrity/proof-purposes/CredentialIssuancePurpose.ts +++ b/packages/core/src/modules/vc/data-integrity/proof-purposes/CredentialIssuancePurpose.ts @@ -40,7 +40,6 @@ export class CredentialIssuancePurpose extends AssertionProofPurpose { * @param {string} options.verificationMethod - Key id URL to the paired * public key. * @param {object} [options.documentLoader] - A document loader. - * @param {object} [options.expansionMap] - An expansion map. * * @throws {Error} If verification method not authorized by controller. * @throws {Error} If proof's created timestamp is out of range. @@ -54,7 +53,6 @@ export class CredentialIssuancePurpose extends AssertionProofPurpose { suite: typeof LinkedDataProof verificationMethod: string documentLoader?: DocumentLoader - expansionMap?: () => void } // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise<{ valid: boolean; error?: any }> { diff --git a/packages/core/src/modules/vc/data-integrity/signature-suites/JwsLinkedDataSignature.ts b/packages/core/src/modules/vc/data-integrity/signature-suites/JwsLinkedDataSignature.ts index cf1f25ad06..db012f8555 100644 --- a/packages/core/src/modules/vc/data-integrity/signature-suites/JwsLinkedDataSignature.ts +++ b/packages/core/src/modules/vc/data-integrity/signature-suites/JwsLinkedDataSignature.ts @@ -208,11 +208,6 @@ export class JwsLinkedDataSignature extends LinkedDataSignature { * recommended to use one that provides static known documents, instead of * fetching from the web) for returning contexts, controller documents, * keys, and other relevant URLs needed for the proof. - * @param [options.expansionMap] - A custom expansion map that is - * passed to the JSON-LD processor; by default a function that will throw - * an error when unmapped properties are detected in the input, use `false` - * to turn this off and allow unmapped properties to be dropped or use a - * custom function. * * @returns Whether a match for the proof was found. */ @@ -222,14 +217,12 @@ export class JwsLinkedDataSignature extends LinkedDataSignature { // eslint-disable-next-line @typescript-eslint/no-explicit-any purpose: any documentLoader?: DocumentLoader - expansionMap?: () => void }) { const proofMatches = await super.matchProof({ proof: options.proof, document: options.document, purpose: options.purpose, documentLoader: options.documentLoader, - expansionMap: options.expansionMap, }) if (!proofMatches) { return false diff --git a/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2018.ts b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2018.ts index 329b71c47c..3feed55441 100644 --- a/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2018.ts +++ b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2018.ts @@ -155,11 +155,6 @@ export class Ed25519Signature2018 extends JwsLinkedDataSignature { * recommended to use one that provides static known documents, instead of * fetching from the web) for returning contexts, controller documents, * keys, and other relevant URLs needed for the proof. - * @param {Function} [options.expansionMap] - A custom expansion map that is - * passed to the JSON-LD processor; by default a function that will throw - * an error when unmapped properties are detected in the input, use `false` - * to turn this off and allow unmapped properties to be dropped or use a - * custom function. * * @returns {Promise} Whether a match for the proof was found. */ @@ -169,7 +164,6 @@ export class Ed25519Signature2018 extends JwsLinkedDataSignature { // eslint-disable-next-line @typescript-eslint/no-explicit-any purpose: any documentLoader?: DocumentLoader - expansionMap?: () => void }) { if (!_includesCompatibleContext({ document: options.document })) { return false @@ -179,7 +173,6 @@ export class Ed25519Signature2018 extends JwsLinkedDataSignature { document: options.document, purpose: options.purpose, documentLoader: options.documentLoader, - expansionMap: options.expansionMap, }) } } diff --git a/packages/core/src/modules/vc/index.ts b/packages/core/src/modules/vc/index.ts index 47571cdecb..84a0da17c7 100644 --- a/packages/core/src/modules/vc/index.ts +++ b/packages/core/src/modules/vc/index.ts @@ -1,6 +1,6 @@ export * from './W3cCredentialService' export * from './W3cCredentialServiceOptions' -export * from './repository/W3cCredentialRecord' +export * from './repository' export * from './W3cCredentialsModule' export * from './W3cCredentialsApi' export * from './models' diff --git a/packages/core/src/modules/vc/models/presentation/W3cJsonPresentation.ts b/packages/core/src/modules/vc/models/presentation/W3cJsonPresentation.ts index 5625627ef8..a47b3e90dc 100644 --- a/packages/core/src/modules/vc/models/presentation/W3cJsonPresentation.ts +++ b/packages/core/src/modules/vc/models/presentation/W3cJsonPresentation.ts @@ -1,4 +1,5 @@ import type { JsonObject } from '../../../../types' +import type { DifPresentationExchangeSubmission } from '../../../dif-presentation-exchange' import type { W3cJsonCredential } from '../credential/W3cJsonCredential' export interface W3cJsonPresentation { @@ -7,5 +8,6 @@ export interface W3cJsonPresentation { type: Array holder: string | { id?: string } verifiableCredential: Array + presentation_submission?: DifPresentationExchangeSubmission [key: string]: unknown } diff --git a/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts b/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts index 299d0fbbc4..cf37fc434b 100644 --- a/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts +++ b/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts @@ -1,4 +1,5 @@ import type { W3cHolderOptions } from './W3cHolder' +import type { W3cJsonPresentation } from './W3cJsonPresentation' import type { JsonObject } from '../../../../types' import type { W3cVerifiableCredential } from '../credential/W3cVerifiableCredential' import type { ValidationOptions } from 'class-validator' @@ -6,6 +7,7 @@ import type { ValidationOptions } from 'class-validator' import { Expose } from 'class-transformer' import { ValidateNested, buildMessage, IsOptional, ValidateBy } from 'class-validator' +import { JsonTransformer } from '../../../../utils' import { SingleOrArray } from '../../../../utils/type' import { IsUri, IsInstanceOrArrayOfInstances } from '../../../../utils/validators' import { CREDENTIALS_CONTEXT_V1_URL, VERIFIABLE_PRESENTATION_TYPE } from '../../constants' @@ -64,6 +66,10 @@ export class W3cPresentation { return this.holder instanceof W3cHolder ? this.holder.id : this.holder } + + public toJSON() { + return JsonTransformer.toJSON(this) as W3cJsonPresentation + } } // Custom validators diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap index 83449887ab..c06c6515c6 100644 --- a/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap @@ -1256,6 +1256,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationKey": "EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF", "mediatorId": undefined, "outOfBandId": "3-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "request-received", "theirDid": "did:peer:1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ", @@ -1273,6 +1275,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3QxdHNwMTVjbkREN3dCQ0ZnZWhpUjJTeEhYMWFQeHQ0c3VlRTI0dHdIOUJkI3o2TWt0MXRzcDE1Y25ERDd3QkNGZ2VoaVIyU3hIWDFhUHh0NHN1ZUUyNHR3SDlCZCJdLCJyIjpbXX0", "metadata": {}, "outOfBandId": "3-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "request-received", "theirDid": "did:peer:1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ", @@ -1290,6 +1294,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationKey": "2G8JohwyJtj69ruWFC3hBkdoaJW5eg31E66ohceVWCvy", "mediatorId": undefined, "outOfBandId": "1-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "requester", "state": "completed", "theirDid": "did:peer:1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX", @@ -1307,6 +1313,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationDid": "did:peer:2.SeyJzIjoicnhqczpmYWJlciIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa2ZpUE1QeENRZVNEWkdNa0N2bTFZMnJCb1BzbXc0WkhNdjcxalh0Y1dSUmlNI3o2TWtmaVBNUHhDUWVTRFpHTWtDdm0xWTJyQm9Qc213NFpITXY3MWpYdGNXUlJpTSJdLCJyIjpbXX0", "metadata": {}, "outOfBandId": "1-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "requester", "state": "completed", "theirDid": "did:peer:1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX", @@ -1324,6 +1332,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationKey": "EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq", "mediatorId": undefined, "outOfBandId": "2-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "completed", "theirDid": "did:peer:1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU", @@ -1340,6 +1350,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3RDWkFRTkd2V2I0V0hBandCcVB0WGhaZERZb3JiU0prR1c5dmoxdWh3MUhEI3o2TWt0Q1pBUU5HdldiNFdIQWp3QnFQdFhoWmREWW9yYlNKa0dXOXZqMXVodzFIRCJdLCJyIjpbXX0", "metadata": {}, "outOfBandId": "2-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "completed", "theirDid": "did:peer:1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU", @@ -1369,6 +1381,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationKey": "G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe", "mediatorId": undefined, "outOfBandId": "7-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "invitation-sent", "theirDid": undefined, @@ -1385,6 +1399,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3VXVEVtSDFtVW82Vzk2elNXeUg2MTJoRkhvd1J6TkVzY1BZQkwyQ0NNeUMyI3o2TWt1V1RFbUgxbVVvNlc5NnpTV3lINjEyaEZIb3dSek5Fc2NQWUJMMkNDTXlDMiJdLCJyIjpbXX0", "metadata": {}, "outOfBandId": "7-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "invitation-sent", "updatedAt": "2022-01-21T22:50:20.522Z", @@ -1399,6 +1415,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationKey": "9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS", "mediatorId": undefined, "outOfBandId": "6-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "response-sent", "theirDid": "did:peer:1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga", @@ -1416,6 +1434,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa28zMURORTNncU1SWmoxSk5odjJCSGIxY2FRc2hjZDluamdLa0VRWHNnRlJwI3o2TWtvMzFETkUzZ3FNUlpqMUpOaHYyQkhiMWNhUXNoY2Q5bmpnS2tFUVhzZ0ZScCJdLCJyIjpbXX0", "metadata": {}, "outOfBandId": "6-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "response-sent", "theirDid": "did:peer:1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga", @@ -2395,6 +2415,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationKey": "5m3HUGs6wFndaEk51zTBXuFwZza2tnGj4NzT5EkUiWaU", "mediatorId": undefined, "outOfBandId": "5-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "requester", "state": "response-received", "theirDid": "did:peer:1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui", @@ -2412,6 +2434,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationDid": "did:peer:2.SeyJzIjoicnhqczpmYWJlciIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa2pESkw0WDdZR29INmdqYW1oWlIyTnpvd1BacXRKZlg1a1B1TnVXaVZkak1yI3o2TWtqREpMNFg3WUdvSDZnamFtaFpSMk56b3dQWnF0SmZYNWtQdU51V2lWZGpNciJdLCJyIjpbXX0", "metadata": {}, "outOfBandId": "5-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "requester", "state": "response-received", "theirDid": "did:peer:1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui", @@ -2429,6 +2453,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationKey": "8MN6LZnM8t1HmzMNw5Sp8kUVfQkFK1nCUMRSfQBoSNAC", "mediatorId": undefined, "outOfBandId": "4-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "requester", "state": "request-sent", "theirDid": undefined, @@ -2444,6 +2470,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationDid": "did:peer:2.SeyJzIjoicnhqczpmYWJlciIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa21vZDh2cDJuVVJWa3RWQzVjZVFleXIyVlV6MjZpdTJaQU5MTlZnOXBNYXdhI3o2TWttb2Q4dnAyblVSVmt0VkM1Y2VRZXlyMlZVejI2aXUyWkFOTE5WZzlwTWF3YSJdLCJyIjpbXX0", "metadata": {}, "outOfBandId": "4-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "requester", "state": "request-sent", "theirLabel": "Agent: PopulateWallet2", diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/connection.test.ts b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/connection.test.ts index 70c72c9c2a..af1886a094 100644 --- a/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/connection.test.ts +++ b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/connection.test.ts @@ -142,6 +142,8 @@ describe('0.1-0.2 | Connection', () => { theirDid: didPeer4kgVt6CidfKgo1MoWMqsQX.id, outOfBandId: expect.any(String), connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) }) @@ -173,6 +175,8 @@ describe('0.1-0.2 | Connection', () => { label: 'test', }, connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) }) @@ -202,6 +206,8 @@ describe('0.1-0.2 | Connection', () => { label: 'test', }, connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) @@ -275,6 +281,8 @@ describe('0.1-0.2 | Connection', () => { metadata: {}, _tags: {}, connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) @@ -346,6 +354,8 @@ describe('0.1-0.2 | Connection', () => { label: 'test', }, connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) }) @@ -373,6 +383,8 @@ describe('0.1-0.2 | Connection', () => { autoAcceptConnection: true, mediatorId: 'a-mediator-id', connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) @@ -499,6 +511,8 @@ describe('0.1-0.2 | Connection', () => { mediatorId: 'a-mediator-id', outOfBandId: outOfBandRecord.id, connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) diff --git a/packages/core/src/storage/migration/updates/0.2-0.3/__tests__/connection.test.ts b/packages/core/src/storage/migration/updates/0.2-0.3/__tests__/connection.test.ts index 0e397e3419..64ada8c5d2 100644 --- a/packages/core/src/storage/migration/updates/0.2-0.3/__tests__/connection.test.ts +++ b/packages/core/src/storage/migration/updates/0.2-0.3/__tests__/connection.test.ts @@ -104,6 +104,8 @@ describe('0.2-0.3 | Connection', () => { _tags: { connectionType: undefined, }, + previousDids: [], + previousTheirDids: [], metadata: {}, }) }) @@ -128,6 +130,8 @@ describe('0.2-0.3 | Connection', () => { _tags: { connectionType: undefined, }, + previousDids: [], + previousTheirDids: [], metadata: {}, }) }) @@ -146,6 +150,8 @@ describe('0.2-0.3 | Connection', () => { expect(connectionRecord.toJSON()).toEqual({ ...connectionRecordProps, connectionTypes: [], + previousDids: [], + previousTheirDids: [], _tags: { connectionType: undefined, }, @@ -174,6 +180,8 @@ describe('0.2-0.3 | Connection', () => { connectionType: undefined, }, metadata: {}, + previousDids: [], + previousTheirDids: [], }) }) }) diff --git a/packages/core/src/utils/__tests__/shortenedUrl.test.ts b/packages/core/src/utils/__tests__/parseInvitation.test.ts similarity index 69% rename from packages/core/src/utils/__tests__/shortenedUrl.test.ts rename to packages/core/src/utils/__tests__/parseInvitation.test.ts index ffe4a2934a..b9966f52cd 100644 --- a/packages/core/src/utils/__tests__/shortenedUrl.test.ts +++ b/packages/core/src/utils/__tests__/parseInvitation.test.ts @@ -1,9 +1,11 @@ +import { agentDependencies } from '../../../tests' import { ConnectionInvitationMessage } from '../../modules/connections' import { InvitationType, OutOfBandInvitation } from '../../modules/oob' import { convertToNewInvitation } from '../../modules/oob/helpers' +import { JsonEncoder } from '../JsonEncoder' import { JsonTransformer } from '../JsonTransformer' import { MessageValidator } from '../MessageValidator' -import { oobInvitationFromShortUrl } from '../parseInvitation' +import { oobInvitationFromShortUrl, parseInvitationShortUrl } from '../parseInvitation' const mockOobInvite = { '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.0/invitation', @@ -21,6 +23,16 @@ const mockConnectionInvite = { recipientKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'], } +const mockLegacyConnectionless = { + '@id': '035b6404-f496-4cb6-a2b5-8bd09e8c92c1', + '@type': 'https://didcomm.org/some-protocol/1.0/some-message', + '~service': { + recipientKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'], + routingKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'], + serviceEndpoint: 'https://example.com/endpoint', + }, +} + const header = new Headers() const dummyHeader = new Headers() @@ -49,6 +61,13 @@ const mockedResponseOobUrl = { dummyHeader.forEach(mockedResponseOobUrl.headers.append) +const mockedLegacyConnectionlessInvitationJson = { + status: 200, + ok: true, + json: async () => mockLegacyConnectionless, + headers: header, +} as Response + const mockedResponseConnectionJson = { status: 200, ok: true, @@ -103,15 +122,78 @@ describe('shortened urls resolving to oob invitations', () => { }) }) +describe('legacy connectionless', () => { + test('parse url containing d_m ', async () => { + const parsed = await parseInvitationShortUrl( + `https://example.com?d_m=${JsonEncoder.toBase64URL(mockLegacyConnectionless)}`, + agentDependencies + ) + expect(parsed.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + label: undefined, + 'requests~attach': [ + { + '@id': expect.any(String), + data: { + base64: + 'eyJAaWQiOiIwMzViNjQwNC1mNDk2LTRjYjYtYTJiNS04YmQwOWU4YzkyYzEiLCJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvc29tZS1wcm90b2NvbC8xLjAvc29tZS1tZXNzYWdlIn0=', + }, + 'mime-type': 'application/json', + }, + ], + services: [ + { + id: expect.any(String), + recipientKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + routingKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + }, + ], + }) + }) + + test('parse short url returning legacy connectionless invitation to out of band invitation', async () => { + const parsed = await oobInvitationFromShortUrl(mockedLegacyConnectionlessInvitationJson) + expect(parsed.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + label: undefined, + 'requests~attach': [ + { + '@id': expect.any(String), + data: { + base64: + 'eyJAaWQiOiIwMzViNjQwNC1mNDk2LTRjYjYtYTJiNS04YmQwOWU4YzkyYzEiLCJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvc29tZS1wcm90b2NvbC8xLjAvc29tZS1tZXNzYWdlIn0=', + }, + 'mime-type': 'application/json', + }, + ], + services: [ + { + id: expect.any(String), + recipientKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + routingKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + }, + ], + }) + }) +}) + describe('shortened urls resolving to connection invitations', () => { test('Resolve a mocked response in the form of a connection invitation as a json object', async () => { const short = await oobInvitationFromShortUrl(mockedResponseConnectionJson) expect(short).toEqual(connectionInvitationToNew) }) + test('Resolve a mocked Response in the form of a connection invitation encoded in an url c_i query parameter', async () => { const short = await oobInvitationFromShortUrl(mockedResponseConnectionUrl) expect(short).toEqual(connectionInvitationToNew) }) + test('Resolve a mocked Response in the form of a connection invitation encoded in an url oob query parameter', async () => { const mockedResponseConnectionInOobUrl = { status: 200, diff --git a/packages/core/src/utils/objectEquality.ts b/packages/core/src/utils/objectEquality.ts index 5288d1a52d..33db64084a 100644 --- a/packages/core/src/utils/objectEquality.ts +++ b/packages/core/src/utils/objectEquality.ts @@ -1,14 +1,16 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function areObjectsEqual(a: any, b: any): boolean { +export function areObjectsEqual(a: A, b: B): boolean { if (typeof a == 'object' && a != null && typeof b == 'object' && b != null) { - if (Object.keys(a).length !== Object.keys(b).length) return false - for (const key in a) { - if (!(key in b) || !areObjectsEqual(a[key], b[key])) { + const definedA = Object.fromEntries(Object.entries(a).filter(([, value]) => value !== undefined)) + const definedB = Object.fromEntries(Object.entries(b).filter(([, value]) => value !== undefined)) + if (Object.keys(definedA).length !== Object.keys(definedB).length) return false + for (const key in definedA) { + if (!(key in definedB) || !areObjectsEqual(definedA[key], definedB[key])) { return false } } - for (const key in b) { - if (!(key in a) || !areObjectsEqual(b[key], a[key])) { + for (const key in definedB) { + if (!(key in definedA) || !areObjectsEqual(definedB[key], definedA[key])) { return false } } diff --git a/packages/core/src/utils/parseInvitation.ts b/packages/core/src/utils/parseInvitation.ts index 1fb9d99b03..4b5b5232a8 100644 --- a/packages/core/src/utils/parseInvitation.ts +++ b/packages/core/src/utils/parseInvitation.ts @@ -59,6 +59,9 @@ export const parseInvitationJson = (invitationJson: Record): Ou const outOfBandInvitation = convertToNewInvitation(invitation) outOfBandInvitation.invitationType = InvitationType.Connection return outOfBandInvitation + } else if (invitationJson['~service']) { + // This is probably a legacy connectionless invitation + return transformLegacyConnectionlessInvitationToOutOfBandInvitation(invitationJson) } else { throw new AriesFrameworkError(`Invitation with '@type' ${parsedMessageType.messageTypeUri} not supported.`) } @@ -105,6 +108,30 @@ export const oobInvitationFromShortUrl = async (response: Response): Promise) { + const agentMessage = JsonTransformer.fromJSON(messageJson, AgentMessage) + + // ~service is required for legacy connectionless invitations + if (!agentMessage.service) { + throw new AriesFrameworkError('Invalid legacy connectionless invitation url. Missing ~service decorator.') + } + + // This destructuring removes the ~service property from the message, and + // we can can use messageWithoutService to create the out of band invitation + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { '~service': service, ...messageWithoutService } = messageJson + + // transform into out of band invitation + const invitation = new OutOfBandInvitation({ + services: [OutOfBandDidCommService.fromResolvedDidCommService(agentMessage.service.resolvedDidCommService)], + }) + + invitation.invitationType = InvitationType.Connectionless + invitation.addRequest(JsonTransformer.fromJSON(messageWithoutService, AgentMessage)) + + return invitation +} + /** * Parses URL containing encoded invitation and returns invitation message. Compatible with * parsing short Urls @@ -126,30 +153,7 @@ export const parseInvitationShortUrl = async ( // Legacy connectionless invitation else if (parsedUrl['d_m']) { const messageJson = JsonEncoder.fromBase64(parsedUrl['d_m'] as string) - const agentMessage = JsonTransformer.fromJSON(messageJson, AgentMessage) - - // ~service is required for legacy connectionless invitations - if (!agentMessage.service) { - throw new AriesFrameworkError('Invalid legacy connectionless invitation url. Missing ~service decorator.') - } - - // This destructuring removes the ~service property from the message, and - // we can can use messageWithoutService to create the out of band invitation - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { '~service': service, ...messageWithoutService } = messageJson - - // transform into out of band invitation - const invitation = new OutOfBandInvitation({ - // The label is currently required by the OutOfBandInvitation class, but not according to the specification. - // FIXME: In 0.5.0 we will make this optional: https://github.com/hyperledger/aries-framework-javascript/issues/1524 - label: '', - services: [OutOfBandDidCommService.fromResolvedDidCommService(agentMessage.service.resolvedDidCommService)], - }) - - invitation.invitationType = InvitationType.Connectionless - invitation.addRequest(JsonTransformer.fromJSON(messageWithoutService, AgentMessage)) - - return invitation + return transformLegacyConnectionlessInvitationToOutOfBandInvitation(messageJson) } else { try { const outOfBandInvitation = await oobInvitationFromShortUrl(await fetchShortUrl(invitationUrl, dependencies)) diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 4ad2392ceb..b1312050a8 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -15,6 +15,8 @@ import type { ConnectionStateChangedEvent, Buffer, RevocationNotificationReceivedEvent, + AgentMessageProcessedEvent, + AgentMessageReceivedEvent, } from '../src' import type { AgentModulesInput, EmptyModuleMap } from '../src/agent/AgentModules' import type { TrustPingReceivedEvent, TrustPingResponseReceivedEvent } from '../src/modules/connections/TrustPingEvents' @@ -45,6 +47,7 @@ import { InjectionSymbols, ProofEventTypes, TrustPingEventTypes, + AgentEventTypes, } from '../src' import { Key, KeyType } from '../src/crypto' import { DidKey } from '../src/modules/dids/methods/key' @@ -218,6 +221,8 @@ const isTrustPingReceivedEvent = (e: BaseEvent): e is TrustPingReceivedEvent => e.type === TrustPingEventTypes.TrustPingReceivedEvent const isTrustPingResponseReceivedEvent = (e: BaseEvent): e is TrustPingResponseReceivedEvent => e.type === TrustPingEventTypes.TrustPingResponseReceivedEvent +const isAgentMessageProcessedEvent = (e: BaseEvent): e is AgentMessageProcessedEvent => + e.type === AgentEventTypes.AgentMessageProcessed export function waitForProofExchangeRecordSubject( subject: ReplaySubject | Observable, @@ -344,6 +349,50 @@ export function waitForTrustPingResponseReceivedEventSubject( ) } +export async function waitForAgentMessageProcessedEvent( + agent: Agent, + options: { + threadId?: string + messageType?: string + timeoutMs?: number + } +) { + const observable = agent.events.observable(AgentEventTypes.AgentMessageProcessed) + + return waitForAgentMessageProcessedEventSubject(observable, options) +} + +export function waitForAgentMessageProcessedEventSubject( + subject: ReplaySubject | Observable, + { + threadId, + timeoutMs = 10000, + messageType, + }: { + threadId?: string + messageType?: string + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return firstValueFrom( + observable.pipe( + filter(isAgentMessageProcessedEvent), + filter((e) => threadId === undefined || e.payload.message.threadId === threadId), + filter((e) => messageType === undefined || e.payload.message.type === messageType), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `AgentMessageProcessedEvent event not emitted within specified timeout: ${timeoutMs} + threadId: ${threadId}, messageType: ${messageType} +}` + ) + }), + map((e) => e.payload.message) + ) + ) +} + export function waitForCredentialRecordSubject( subject: ReplaySubject | Observable, { @@ -440,12 +489,17 @@ export async function waitForConnectionRecord( return waitForConnectionRecordSubject(observable, options) } -export async function waitForBasicMessage(agent: Agent, { content }: { content?: string }): Promise { +export async function waitForBasicMessage( + agent: Agent, + { content, connectionId }: { content?: string; connectionId?: string } +): Promise { return new Promise((resolve) => { const listener = (event: BasicMessageStateChangedEvent) => { const contentMatches = content === undefined || event.payload.message.content === content + const connectionIdMatches = + connectionId === undefined || event.payload.basicMessageRecord.connectionId === connectionId - if (contentMatches) { + if (contentMatches && connectionIdMatches) { agent.events.off(BasicMessageEventTypes.BasicMessageStateChanged, listener) resolve(event.payload.message) diff --git a/packages/core/tests/jsonld.ts b/packages/core/tests/jsonld.ts index 9b0f211097..1cfb263ab0 100644 --- a/packages/core/tests/jsonld.ts +++ b/packages/core/tests/jsonld.ts @@ -5,6 +5,8 @@ import { BbsModule } from '../../bbs-signatures/src/BbsModule' import { IndySdkModule } from '../../indy-sdk/src' import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' import { + PresentationExchangeProofFormatService, + V2ProofProtocol, CacheModule, CredentialEventTypes, InMemoryLruCache, @@ -38,6 +40,7 @@ export const getJsonLdModules = ({ }), proofs: new ProofsModule({ autoAcceptProofs, + proofProtocols: [new V2ProofProtocol({ proofFormats: [new PresentationExchangeProofFormatService()] })], }), cache: new CacheModule({ cache: new InMemoryLruCache({ limit: 100 }), diff --git a/packages/core/tests/oob.test.ts b/packages/core/tests/oob.test.ts index 2911d9d406..fddbb253ba 100644 --- a/packages/core/tests/oob.test.ts +++ b/packages/core/tests/oob.test.ts @@ -692,7 +692,7 @@ describe('out of band', () => { await expect(aliceAgent.oob.receiveInvitation(outOfBandInvitation, receiveInvitationConfig)).rejects.toEqual( new AriesFrameworkError( - `Handshake protocols [${unsupportedProtocol}] are not supported. Supported protocols are [https://didcomm.org/didexchange/1.0,https://didcomm.org/connections/1.0]` + `Handshake protocols [${unsupportedProtocol}] are not supported. Supported protocols are [https://didcomm.org/didexchange/1.1,https://didcomm.org/connections/1.0]` ) ) }) diff --git a/packages/indy-sdk-to-askar-migration/package.json b/packages/indy-sdk-to-askar-migration/package.json index a8e1c6e0a4..1f8cf4d3f9 100644 --- a/packages/indy-sdk-to-askar-migration/package.json +++ b/packages/indy-sdk-to-askar-migration/package.json @@ -31,13 +31,13 @@ }, "devDependencies": { "@aries-framework/indy-sdk": "0.4.2", - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", - "@hyperledger/aries-askar-shared": "^0.2.0-dev.1", + "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", + "@hyperledger/aries-askar-shared": "^0.2.0-dev.5", "indy-sdk": "^1.16.0-dev-1655", "rimraf": "^4.4.0", "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/aries-askar-shared": "^0.2.0-dev.1" + "@hyperledger/aries-askar-shared": "^0.2.0-dev.5" } } diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts index a386675b1e..0b9fdb0718 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts @@ -18,7 +18,7 @@ import type { Schema as IndySdkSchema } from 'indy-sdk' import { getUnqualifiedCredentialDefinitionId, - getUnqualifiedRevocationRegistryId, + getUnqualifiedRevocationRegistryDefinitionId, getUnqualifiedSchemaId, parseIndyCredentialDefinitionId, parseIndyDid, @@ -394,7 +394,7 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { `Using ledger '${pool.didIndyNamespace}' to retrieve revocation registry definition '${revocationRegistryDefinitionId}'` ) - const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryId( + const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryDefinitionId( namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, @@ -492,7 +492,7 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { `Using ledger '${pool.didIndyNamespace}' to retrieve revocation registry deltas with revocation registry definition id '${revocationRegistryId}' until ${timestamp}` ) - const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryId( + const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryDefinitionId( namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, diff --git a/packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts b/packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts index 9b9a54ba83..546579330b 100644 --- a/packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts +++ b/packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts @@ -1,6 +1,6 @@ import { getDidIndyCredentialDefinitionId, - getDidIndyRevocationRegistryId, + getDidIndyRevocationRegistryDefinitionId, getDidIndySchemaId, indySdkAnonCredsRegistryIdentifierRegex, } from '../identifiers' @@ -72,7 +72,7 @@ describe('identifiers', () => { const credentialDefinitionTag = 'someTag' const tag = 'anotherTag' - expect(getDidIndyRevocationRegistryId(namespace, did, seqNo, credentialDefinitionTag, tag)).toEqual( + expect(getDidIndyRevocationRegistryDefinitionId(namespace, did, seqNo, credentialDefinitionTag, tag)).toEqual( 'did:indy:sovrin:test:12345/anoncreds/v0/REV_REG_DEF/420/someTag/anotherTag' ) }) diff --git a/packages/indy-sdk/src/anoncreds/utils/identifiers.ts b/packages/indy-sdk/src/anoncreds/utils/identifiers.ts index 4cedb11ff4..ccc7433e34 100644 --- a/packages/indy-sdk/src/anoncreds/utils/identifiers.ts +++ b/packages/indy-sdk/src/anoncreds/utils/identifiers.ts @@ -46,18 +46,18 @@ export function getDidIndySchemaId(namespace: string, unqualifiedDid: string, na export function getDidIndyCredentialDefinitionId( namespace: string, unqualifiedDid: string, - seqNo: string | number, + schemaSeqNo: string | number, tag: string ) { - return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/CLAIM_DEF/${seqNo}/${tag}` + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/${tag}` } -export function getDidIndyRevocationRegistryId( +export function getDidIndyRevocationRegistryDefinitionId( namespace: string, unqualifiedDid: string, - seqNo: string | number, + schemaSeqNo: string | number, credentialDefinitionTag: string, revocationRegistryTag: string ) { - return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/REV_REG_DEF/${seqNo}/${credentialDefinitionTag}/${revocationRegistryTag}` + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/REV_REG_DEF/${schemaSeqNo}/${credentialDefinitionTag}/${revocationRegistryTag}` } diff --git a/packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts b/packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts index 1c486eb3aa..c84999a8c1 100644 --- a/packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts +++ b/packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts @@ -16,6 +16,8 @@ import { addServicesFromEndpointsAttrib } from './didSovUtil' export class IndySdkIndyDidResolver implements DidResolver { public readonly supportedMethods = ['indy'] + public readonly allowsCaching = true + public async resolve(agentContext: AgentContext, did: string): Promise { const didDocumentMetadata = {} diff --git a/packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts b/packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts index ff6afc9571..3a881db50e 100644 --- a/packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts +++ b/packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts @@ -12,6 +12,8 @@ import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovU export class IndySdkSovDidResolver implements DidResolver { public readonly supportedMethods = ['sov'] + public readonly allowsCaching = true + public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise { const didDocumentMetadata = {} diff --git a/packages/indy-vdr/package.json b/packages/indy-vdr/package.json index e49286f040..d558c83dd0 100644 --- a/packages/indy-vdr/package.json +++ b/packages/indy-vdr/package.json @@ -28,8 +28,8 @@ "@aries-framework/core": "0.4.2" }, "devDependencies": { - "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.5", - "@hyperledger/indy-vdr-shared": "^0.2.0-dev.5", + "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.6", + "@hyperledger/indy-vdr-shared": "^0.2.0-dev.6", "@stablelib/ed25519": "^1.0.2", "@types/ref-array-di": "^1.2.6", "@types/ref-struct-di": "^1.1.10", @@ -38,6 +38,6 @@ "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/indy-vdr-shared": "^0.2.0-dev.5" + "@hyperledger/indy-vdr-shared": "^0.2.0-dev.6" } } diff --git a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts index 5251362983..15db83beee 100644 --- a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts +++ b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts @@ -1,3 +1,4 @@ +import type { RevocationRegistryDelta } from './utils/transform' import type { AnonCredsRegistry, GetCredentialDefinitionReturn, @@ -19,21 +20,33 @@ import type { RegisterCredentialDefinitionReturnStateWait, RegisterCredentialDefinitionReturnStateFinished, RegisterCredentialDefinitionReturnStateFailed, + RegisterRevocationRegistryDefinitionReturnStateFinished, + RegisterRevocationRegistryDefinitionReturnStateFailed, + RegisterRevocationRegistryDefinitionReturnStateWait, + RegisterRevocationRegistryDefinitionReturnStateAction, + RegisterRevocationStatusListReturnStateFinished, + RegisterRevocationStatusListReturnStateFailed, + RegisterRevocationStatusListReturnStateWait, + RegisterRevocationStatusListReturnStateAction, + RegisterRevocationStatusListOptions, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { SchemaResponse } from '@hyperledger/indy-vdr-shared' import { getUnqualifiedCredentialDefinitionId, - getUnqualifiedRevocationRegistryId, + getUnqualifiedRevocationRegistryDefinitionId, getUnqualifiedSchemaId, parseIndyCredentialDefinitionId, parseIndyDid, parseIndyRevocationRegistryId, parseIndySchemaId, + dateToTimestamp, } from '@aries-framework/anoncreds' import { AriesFrameworkError } from '@aries-framework/core' import { + RevocationRegistryEntryRequest, + RevocationRegistryDefinitionRequest, GetSchemaRequest, SchemaRequest, GetCredentialDefinitionRequest, @@ -52,8 +65,10 @@ import { indyVdrAnonCredsRegistryIdentifierRegex, getDidIndySchemaId, getDidIndyCredentialDefinitionId, + getDidIndyRevocationRegistryDefinitionId, + getDidIndyRevocationRegistryEntryId, } from './utils/identifiers' -import { anonCredsRevocationStatusListFromIndyVdr } from './utils/transform' +import { indyVdrCreateLatestRevocationDelta, anonCredsRevocationStatusListFromIndyVdr } from './utils/transform' export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { public readonly methodName = 'indy' @@ -277,7 +292,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { : schema.schema.schemaId return { - credentialDefinitionId: credentialDefinitionId, + credentialDefinitionId, credentialDefinition: { issuerId: did, schemaId, @@ -329,7 +344,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { let writeRequest: CustomRequest let didIndyCredentialDefinitionId: string - let seqNo: number + let schemaSeqNo: number const endorsedTransaction = options.options.endorsedTransaction if (endorsedTransaction) { @@ -340,8 +355,13 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { writeRequest = new CustomRequest({ customRequest: endorsedTransaction }) const operation = JSON.parse(endorsedTransaction)?.operation // extract the seqNo from the endorsed transaction, which is contained in the ref field of the operation - seqNo = Number(operation?.ref) - didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId(namespace, namespaceIdentifier, seqNo, tag) + schemaSeqNo = Number(operation?.ref) + didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId( + namespace, + namespaceIdentifier, + schemaSeqNo, + tag + ) } else { // TODO: this will bypass caching if done on a higher level. const { schemaMetadata, resolutionMetadata } = await this.getSchema(agentContext, schemaId) @@ -359,20 +379,31 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { }, } } - seqNo = schemaMetadata.indyLedgerSeqNo - - const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId(issuerId, seqNo, tag) - didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId(namespace, namespaceIdentifier, seqNo, tag) + schemaSeqNo = schemaMetadata.indyLedgerSeqNo + + // FIXME: we need to check if schemaId has same namespace as issuerId and it should not be a legacy identifier + // FIXME: in the other methods we also need to add checks. E.g. when creating a revocation + // status list, you can only create a revocation status list for a credential definition registry that is created + // under the same namespace and by the same issuer id (you can create a cred def for a schema created by another issuer + // but you can't create a revocation registry based on a cred def created by another issuer. We need to add these checks + // to all register methods in this file) + const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId(issuerId, schemaSeqNo, tag) + didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId( + namespace, + namespaceIdentifier, + schemaSeqNo, + tag + ) const credentialDefinitionRequest = new CredentialDefinitionRequest({ submitterDid: namespaceIdentifier, credentialDefinition: { ver: '1.0', id: legacyCredentialDefinitionId, - schemaId: `${seqNo}`, + schemaId: schemaSeqNo.toString(), type: 'CL', - tag: tag, - value: value, + tag, + value, }, }) @@ -457,7 +488,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { `Using ledger '${pool.indyNamespace}' to retrieve revocation registry definition '${revocationRegistryDefinitionId}'` ) - const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryId( + const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryDefinitionId( namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, @@ -483,7 +514,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { return { resolutionMetadata: { error: 'notFound', - message: `unable to resolve revocation registry definition`, + message: 'unable to resolve revocation registry definition', }, revocationRegistryDefinitionId, revocationRegistryDefinitionMetadata: {}, @@ -552,48 +583,178 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { } } - public async registerRevocationRegistryDefinition(): Promise { - throw new AriesFrameworkError('Not implemented!') - } - - public async getRevocationStatusList( + public async registerRevocationRegistryDefinition( agentContext: AgentContext, - revocationRegistryId: string, - timestamp: number - ): Promise { + { options, revocationRegistryDefinition }: IndyVdrRegisterRevocationRegistryDefinition + ): Promise { try { - const indySdkPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) - - const { did, namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, revocationRegistryTag } = - parseIndyRevocationRegistryId(revocationRegistryId) - const { pool } = await indySdkPoolService.getPoolForDid(agentContext, did) + // This will throw an error if trying to register a credential definition with a legacy indy identifier. We only support did:indy + // identifiers for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. + const { namespaceIdentifier, namespace } = parseIndyDid(revocationRegistryDefinition.issuerId) + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + const pool = indyVdrPoolService.getPoolForNamespace(namespace) agentContext.config.logger.debug( - `Using ledger '${pool.indyNamespace}' to retrieve revocation registry deltas with revocation registry definition id '${revocationRegistryId}' until ${timestamp}` + `Registering revocation registry definition on ledger '${namespace}' with did '${revocationRegistryDefinition.issuerId}'`, + revocationRegistryDefinition ) - const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryId( - namespaceIdentifier, - schemaSeqNo, - credentialDefinitionTag, - revocationRegistryTag - ) - const request = new GetRevocationRegistryDeltaRequest({ - revocationRegistryId: legacyRevocationRegistryId, - toTs: timestamp, - }) + let writeRequest: CustomRequest + let didIndyRevocationRegistryDefinitionId: string - agentContext.config.logger.trace( - `Submitting get revocation registry delta request for revocation registry '${revocationRegistryId}' to ledger` + const { schemaSeqNo, tag: credentialDefinitionTag } = parseIndyCredentialDefinitionId( + revocationRegistryDefinition.credDefId ) - const response = await pool.submitRequest(request) + const { endorsedTransaction, endorserDid, endorserMode } = options + if (endorsedTransaction) { + agentContext.config.logger.debug( + `Preparing endorsed tx '${endorsedTransaction}' for submission on ledger '${namespace}' with did '${revocationRegistryDefinition.issuerId}'`, + revocationRegistryDefinition + ) + writeRequest = new CustomRequest({ customRequest: endorsedTransaction }) + didIndyRevocationRegistryDefinitionId = getDidIndyRevocationRegistryDefinitionId( + namespace, + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryDefinition.tag + ) + } else { + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryDefinition.tag + ) + + didIndyRevocationRegistryDefinitionId = getDidIndyRevocationRegistryDefinitionId( + namespace, + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryDefinition.tag + ) + + const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag + ) + + const revocationRegistryDefinitionRequest = new RevocationRegistryDefinitionRequest({ + submitterDid: namespaceIdentifier, + revocationRegistryDefinitionV1: { + id: legacyRevocationRegistryDefinitionId, + ver: '1.0', + credDefId: legacyCredentialDefinitionId, + tag: revocationRegistryDefinition.tag, + revocDefType: revocationRegistryDefinition.revocDefType, + value: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + ...revocationRegistryDefinition.value, + }, + }, + }) + + const submitterKey = await verificationKeyForIndyDid(agentContext, revocationRegistryDefinition.issuerId) + writeRequest = await pool.prepareWriteRequest( + agentContext, + revocationRegistryDefinitionRequest, + submitterKey, + endorserDid !== revocationRegistryDefinition.issuerId ? endorserDid : undefined + ) + + if (endorserMode === 'external') { + return { + jobId: didIndyRevocationRegistryDefinitionId, + revocationRegistryDefinitionState: { + state: 'action', + action: 'endorseIndyTransaction', + revocationRegistryDefinition, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, + revocationRegistryDefinitionRequest: writeRequest.body, + }, + registrationMetadata: {}, + revocationRegistryDefinitionMetadata: {}, + } + } + + if (endorserMode === 'internal' && endorserDid !== revocationRegistryDefinition.issuerId) { + const endorserKey = await verificationKeyForIndyDid(agentContext, endorserDid as string) + await multiSignRequest(agentContext, writeRequest, endorserKey, parseIndyDid(endorserDid).namespaceIdentifier) + } + } + + const response = await pool.submitRequest(writeRequest) agentContext.config.logger.debug( - `Got revocation registry deltas '${revocationRegistryId}' until timestamp ${timestamp} from ledger` + `Registered revocation registry definition '${didIndyRevocationRegistryDefinitionId}' on ledger '${pool.indyNamespace}'`, + { + response, + revocationRegistryDefinition, + } ) + return { + revocationRegistryDefinitionMetadata: {}, + revocationRegistryDefinitionState: { + revocationRegistryDefinition, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + } + } catch (error) { + agentContext.config.logger.error( + `Error registering revocation registry definition for credential definition '${revocationRegistryDefinition.credDefId}'`, + { + error, + did: revocationRegistryDefinition.issuerId, + revocationRegistryDefinition, + } + ) + + return { + revocationRegistryDefinitionMetadata: {}, + registrationMetadata: {}, + revocationRegistryDefinitionState: { + revocationRegistryDefinition, + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async getRevocationStatusList( + agentContext: AgentContext, + revocationRegistryDefinitionId: string, + timestamp: number + ): Promise { + try { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const { did } = parseIndyRevocationRegistryId(revocationRegistryDefinitionId) + const { pool } = await indyVdrPoolService.getPoolForDid(agentContext, did) + + const revocationDelta = await this.fetchIndyRevocationDelta( + agentContext, + revocationRegistryDefinitionId, + timestamp + ) + + if (!revocationDelta) { + return { + resolutionMetadata: { + error: 'notFound', + message: `Error retrieving revocation registry delta '${revocationRegistryDefinitionId}' from ledger, potentially revocation interval ends before revocation registry creation`, + }, + revocationStatusListMetadata: {}, + } + } + const { revocationRegistryDefinition, resolutionMetadata, revocationRegistryDefinitionMetadata } = - await this.getRevocationRegistryDefinition(agentContext, revocationRegistryId) + await this.getRevocationRegistryDefinition(agentContext, revocationRegistryDefinitionId) if ( !revocationRegistryDefinition || @@ -602,7 +763,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { ) { return { resolutionMetadata: { - error: `error resolving revocation registry definition with id ${revocationRegistryId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, + error: `error resolving revocation registry definition with id ${revocationRegistryDefinitionId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, }, revocationStatusListMetadata: { didIndyNamespace: pool.indyNamespace, @@ -612,29 +773,12 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { const isIssuanceByDefault = revocationRegistryDefinitionMetadata.issuanceType === 'ISSUANCE_BY_DEFAULT' - if (!response.result.data) { - return { - resolutionMetadata: { - error: 'notFound', - message: `Error retrieving revocation registry delta '${revocationRegistryId}' from ledger, potentially revocation interval ends before revocation registry creation`, - }, - revocationStatusListMetadata: {}, - } - } - - const revocationRegistryDelta = { - accum: response.result.data.value.accum_to.value.accum, - issued: response.result.data.value.issued, - revoked: response.result.data.value.revoked, - } - return { resolutionMetadata: {}, revocationStatusList: anonCredsRevocationStatusListFromIndyVdr( - revocationRegistryId, + revocationRegistryDefinitionId, revocationRegistryDefinition, - revocationRegistryDelta, - response.result.data.value.accum_to.txnTime, + revocationDelta, isIssuanceByDefault ), revocationStatusListMetadata: { @@ -643,25 +787,178 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { } } catch (error) { agentContext.config.logger.error( - `Error retrieving revocation registry delta '${revocationRegistryId}' from ledger, potentially revocation interval ends before revocation registry creation?"`, + `Error retrieving revocation registry delta '${revocationRegistryDefinitionId}' from ledger, potentially revocation interval ends before revocation registry creation?"`, { error, - revocationRegistryId: revocationRegistryId, + revocationRegistryId: revocationRegistryDefinitionId, } ) return { resolutionMetadata: { error: 'notFound', - message: `Error retrieving revocation registry delta '${revocationRegistryId}' from ledger, potentially revocation interval ends before revocation registry creation: ${error.message}`, + message: `Error retrieving revocation registry delta '${revocationRegistryDefinitionId}' from ledger, potentially revocation interval ends before revocation registry creation: ${error.message}`, }, revocationStatusListMetadata: {}, } } } - public async registerRevocationStatusList(): Promise { - throw new AriesFrameworkError('Not implemented!') + public async registerRevocationStatusList( + agentContext: AgentContext, + { options, revocationStatusList }: IndyVdrRegisterRevocationStatusList + ): Promise { + try { + // This will throw an error if trying to register a revocation status list with a legacy indy identifier. We only support did:indy + // identifiers for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. + const { endorsedTransaction, endorserDid, endorserMode } = options + const { namespaceIdentifier, namespace } = parseIndyDid(revocationStatusList.issuerId) + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + const pool = indyVdrPoolService.getPoolForNamespace(namespace) + + agentContext.config.logger.debug( + `Registering revocation status list on ledger '${namespace}' with did '${revocationStatusList.issuerId}'`, + revocationStatusList + ) + + let writeRequest: CustomRequest + + // Parse the revocation registry id + const { + schemaSeqNo, + credentialDefinitionTag, + namespaceIdentifier: revocationRegistryNamespaceIdentifier, + revocationRegistryTag, + namespace: revocationRegistryNamespace, + } = parseIndyRevocationRegistryId(revocationStatusList.revRegDefId) + + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + + const didIndyRevocationRegistryEntryId = getDidIndyRevocationRegistryEntryId( + namespace, + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + + if (revocationRegistryNamespace && revocationRegistryNamespace !== namespace) { + throw new AriesFrameworkError( + `Issued id '${revocationStatusList.issuerId}' does not have the same namespace (${namespace}) as the revocation registry definition '${revocationRegistryNamespace}'` + ) + } + + if (revocationRegistryNamespaceIdentifier !== namespaceIdentifier) { + throw new AriesFrameworkError( + `Cannot register revocation registry definition using a different DID. Revocation registry definition contains '${revocationRegistryNamespaceIdentifier}', but DID used was '${namespaceIdentifier}'` + ) + } + + if (endorsedTransaction) { + agentContext.config.logger.debug( + `Preparing endorsed tx '${endorsedTransaction}' for submission on ledger '${namespace}' with did '${revocationStatusList.issuerId}'`, + revocationStatusList + ) + + writeRequest = new CustomRequest({ customRequest: endorsedTransaction }) + } else { + const previousDelta = await this.fetchIndyRevocationDelta( + agentContext, + legacyRevocationRegistryDefinitionId, + // Fetch revocation delta for current timestamp + dateToTimestamp(new Date()) + ) + + const revocationRegistryDefinitionEntryValue = indyVdrCreateLatestRevocationDelta( + revocationStatusList.currentAccumulator, + revocationStatusList.revocationList, + previousDelta ?? undefined + ) + + const revocationRegistryDefinitionRequest = new RevocationRegistryEntryRequest({ + submitterDid: namespaceIdentifier, + revocationRegistryEntry: { + ver: '1.0', + value: revocationRegistryDefinitionEntryValue, + }, + revocationRegistryDefinitionType: 'CL_ACCUM', + revocationRegistryDefinitionId: legacyRevocationRegistryDefinitionId, + }) + + const submitterKey = await verificationKeyForIndyDid(agentContext, revocationStatusList.issuerId) + writeRequest = await pool.prepareWriteRequest( + agentContext, + revocationRegistryDefinitionRequest, + submitterKey, + endorserDid !== revocationStatusList.issuerId ? endorserDid : undefined + ) + + if (endorserMode === 'external') { + return { + jobId: didIndyRevocationRegistryEntryId, + revocationStatusListState: { + state: 'action', + action: 'endorseIndyTransaction', + revocationStatusList, + revocationStatusListRequest: writeRequest.body, + }, + registrationMetadata: {}, + revocationStatusListMetadata: {}, + } + } + + if (endorserMode === 'internal' && endorserDid !== revocationStatusList.issuerId) { + const endorserKey = await verificationKeyForIndyDid(agentContext, endorserDid as string) + await multiSignRequest(agentContext, writeRequest, endorserKey, parseIndyDid(endorserDid).namespaceIdentifier) + } + } + + const response = await pool.submitRequest( + writeRequest as RevocationRegistryEntryRequest + ) + agentContext.config.logger.debug( + `Registered revocation status list '${didIndyRevocationRegistryEntryId}' on ledger '${pool.indyNamespace}'`, + { + response, + revocationStatusList, + } + ) + + return { + revocationStatusListMetadata: {}, + revocationStatusListState: { + revocationStatusList: { + ...revocationStatusList, + timestamp: response.result.txnMetadata.txnTime, + }, + state: 'finished', + }, + registrationMetadata: {}, + } + } catch (error) { + agentContext.config.logger.error( + `Error registering revocation status list for revocation registry definition '${revocationStatusList.revRegDefId}}'`, + { + error, + did: revocationStatusList.issuerId, + } + ) + + return { + registrationMetadata: {}, + revocationStatusListMetadata: {}, + revocationStatusListState: { + revocationStatusList, + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } } private async fetchIndySchemaWithSeqNo(agentContext: AgentContext, seqNo: number, did: string) { @@ -697,6 +994,56 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { indyNamespace: pool.indyNamespace, } } + + private async fetchIndyRevocationDelta( + agentContext: AgentContext, + revocationRegistryDefinitionId: string, + toTs: number + ): Promise { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const { did, namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, revocationRegistryTag } = + parseIndyRevocationRegistryId(revocationRegistryDefinitionId) + const { pool } = await indyVdrPoolService.getPoolForDid(agentContext, did) + + agentContext.config.logger.debug( + `Using ledger '${pool.indyNamespace}' to retrieve revocation registry deltas with revocation registry definition id '${revocationRegistryDefinitionId}' until ${toTs}` + ) + + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + + const deltaRequest = new GetRevocationRegistryDeltaRequest({ + toTs, + submitterDid: namespaceIdentifier, + revocationRegistryId: legacyRevocationRegistryDefinitionId, + }) + + agentContext.config.logger.trace(`Submitting get transaction request to ledger '${pool.indyNamespace}'`) + const response = await pool.submitRequest(deltaRequest) + const { + result: { data, type, txnTime }, + } = response + + // Indicating there are no deltas + if (type !== '117' || data === null || !txnTime) { + agentContext.config.logger.warn( + `Could not get any deltas from ledger for revocation registry definition '${revocationRegistryDefinitionId}' from ledger '${pool.indyNamespace}'` + ) + return null + } + + return { + revoked: data.value.revoked, + issued: data.value.issued, + accum: data.value.accum_to.value.accum, + txnTime, + } + } } interface SchemaType { @@ -781,3 +1128,72 @@ export type IndyVdrRegisterCredentialDefinition = | IndyVdrRegisterCredentialDefinitionExternalSubmitOptions export type IndyVdrRegisterCredentialDefinitionOptions = IndyVdrRegisterCredentialDefinition['options'] + +export interface IndyVdrRegisterRevocationRegistryDefinitionInternalOptions { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + options: InternalEndorsement +} + +export interface IndyVdrRegisterRevocationRegistryDefinitionExternalCreateOptions { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + options: ExternalEndorsementCreate +} + +export interface IndyVdrRegisterRevocationRegistryDefinitionExternalSubmitOptions { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + options: ExternalEndorsementSubmit +} + +export interface IndyVdrRegisterRevocationRegistryDefinitionReturnStateAction + extends RegisterRevocationRegistryDefinitionReturnStateAction { + action: 'endorseIndyTransaction' + revocationRegistryDefinitionRequest: string +} + +export interface IndyVdrRegisterRevocationRegistryDefinitionReturn extends RegisterRevocationRegistryDefinitionReturn { + revocationRegistryDefinitionState: + | IndyVdrRegisterRevocationRegistryDefinitionReturnStateAction + | RegisterRevocationRegistryDefinitionReturnStateWait + | RegisterRevocationRegistryDefinitionReturnStateFinished + | RegisterRevocationRegistryDefinitionReturnStateFailed +} + +export type IndyVdrRegisterRevocationRegistryDefinition = + | IndyVdrRegisterRevocationRegistryDefinitionInternalOptions + | IndyVdrRegisterRevocationRegistryDefinitionExternalCreateOptions + | IndyVdrRegisterRevocationRegistryDefinitionExternalSubmitOptions + +export type IndyVdrRegisterRevocationRegistryDefinitionOptions = IndyVdrRegisterRevocationRegistryDefinition['options'] + +export interface IndyVdrRegisterRevocationStatusListInternalOptions extends RegisterRevocationStatusListOptions { + options: InternalEndorsement +} + +export interface IndyVdrRegisterRevocationStatusListExternalCreateOptions extends RegisterRevocationStatusListOptions { + options: ExternalEndorsementCreate +} + +export interface IndyVdrRegisterRevocationStatusListExternalSubmitOptions extends RegisterRevocationStatusListOptions { + options: ExternalEndorsementSubmit +} + +export interface IndyVdrRegisterRevocationStatusListReturnStateAction + extends RegisterRevocationStatusListReturnStateAction { + action: 'endorseIndyTransaction' + revocationStatusListRequest: string +} + +export interface IndyVdrRegisterRevocationStatusListReturn extends RegisterRevocationStatusListReturn { + revocationStatusListState: + | IndyVdrRegisterRevocationStatusListReturnStateAction + | RegisterRevocationStatusListReturnStateWait + | RegisterRevocationStatusListReturnStateFinished + | RegisterRevocationStatusListReturnStateFailed +} + +export type IndyVdrRegisterRevocationStatusList = + | IndyVdrRegisterRevocationStatusListInternalOptions + | IndyVdrRegisterRevocationStatusListExternalCreateOptions + | IndyVdrRegisterRevocationStatusListExternalSubmitOptions + +export type IndyVdrRegisterRevocationStatusListOptions = IndyVdrRegisterRevocationStatusList['options'] diff --git a/packages/indy-vdr/src/anoncreds/utils/__tests__/identifiers.test.ts b/packages/indy-vdr/src/anoncreds/utils/__tests__/identifiers.test.ts index b96720611b..0028a3bfa2 100644 --- a/packages/indy-vdr/src/anoncreds/utils/__tests__/identifiers.test.ts +++ b/packages/indy-vdr/src/anoncreds/utils/__tests__/identifiers.test.ts @@ -1,6 +1,6 @@ import { getDidIndyCredentialDefinitionId, - getDidIndyRevocationRegistryId, + getDidIndyRevocationRegistryDefinitionId, getDidIndySchemaId, indyVdrAnonCredsRegistryIdentifierRegex, } from '../identifiers' @@ -72,7 +72,7 @@ describe('identifiers', () => { const credentialDefinitionTag = 'someTag' const tag = 'anotherTag' - expect(getDidIndyRevocationRegistryId(namespace, did, seqNo, credentialDefinitionTag, tag)).toEqual( + expect(getDidIndyRevocationRegistryDefinitionId(namespace, did, seqNo, credentialDefinitionTag, tag)).toEqual( 'did:indy:sovrin:test:12345/anoncreds/v0/REV_REG_DEF/420/someTag/anotherTag' ) }) diff --git a/packages/indy-vdr/src/anoncreds/utils/__tests__/transform.test.ts b/packages/indy-vdr/src/anoncreds/utils/__tests__/transform.test.ts new file mode 100644 index 0000000000..c5f71e8868 --- /dev/null +++ b/packages/indy-vdr/src/anoncreds/utils/__tests__/transform.test.ts @@ -0,0 +1,164 @@ +import type { RevocationRegistryDelta } from '../transform' +import type { AnonCredsRevocationRegistryDefinition } from '@aries-framework/anoncreds' + +import { indyVdrCreateLatestRevocationDelta, anonCredsRevocationStatusListFromIndyVdr } from '../transform' + +const createRevocationRegistryDefinition = (maxCreds: number): AnonCredsRevocationRegistryDefinition => ({ + value: { + tailsHash: 'hash', + maxCredNum: maxCreds, + publicKeys: { + accumKey: { + z: 'key', + }, + }, + tailsLocation: 'nowhere', + }, + revocDefType: 'CL_ACCUM', + tag: 'REV_TAG', + issuerId: 'does:not:matter', + credDefId: 'does:not:matter', +}) + +describe('transform', () => { + const accum = 'does not matter' + const revocationRegistryDefinitionId = 'does:not:matter' + + describe('indy vdr delta to anoncreds revocation status list', () => { + test('issued and revoked are filled', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [0, 1, 2, 3, 4], + revoked: [5, 6, 7, 8, 9], + txnTime: 1, + } + + const revocationRegistryDefinition = createRevocationRegistryDefinition(10) + + const statusList = anonCredsRevocationStatusListFromIndyVdr( + revocationRegistryDefinitionId, + revocationRegistryDefinition, + delta, + true + ) + + expect(statusList.revocationList).toStrictEqual([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) + }) + + test('issued and revoked are empty (issuance by default)', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [], + revoked: [], + txnTime: 1, + } + + const revocationRegistryDefinition = createRevocationRegistryDefinition(10) + + const statusList = anonCredsRevocationStatusListFromIndyVdr( + revocationRegistryDefinitionId, + revocationRegistryDefinition, + delta, + true + ) + + expect(statusList.revocationList).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + }) + + test('issued and revoked are empty (issuance on demand)', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [], + revoked: [], + txnTime: 1, + } + + const revocationRegistryDefinition = createRevocationRegistryDefinition(10) + + const statusList = anonCredsRevocationStatusListFromIndyVdr( + revocationRegistryDefinitionId, + revocationRegistryDefinition, + delta, + false + ) + + expect(statusList.revocationList).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + }) + + test('issued index is too high', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [200], + revoked: [5, 6, 7, 8, 9], + txnTime: 1, + } + + const revocationRegistryDefinition = createRevocationRegistryDefinition(10) + + expect(() => + anonCredsRevocationStatusListFromIndyVdr( + revocationRegistryDefinitionId, + revocationRegistryDefinition, + delta, + true + ) + ).toThrowError() + }) + }) + + describe('create latest indy vdr delta from status list and previous delta', () => { + test('delta and status list are equal', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [0, 1, 2, 3, 4], + revoked: [5, 6, 7, 8, 9], + txnTime: 1, + } + + const revocationStatusList = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1] + + const { revoked, issued } = indyVdrCreateLatestRevocationDelta(accum, revocationStatusList, delta) + + expect(revoked).toStrictEqual([]) + expect(issued).toStrictEqual([]) + }) + + test('no previous delta', () => { + const revocationStatusList = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1] + + const { revoked, issued } = indyVdrCreateLatestRevocationDelta(accum, revocationStatusList) + + expect(issued).toStrictEqual([0, 1, 2, 3, 4]) + expect(revoked).toStrictEqual([5, 6, 7, 8, 9]) + }) + + test('status list and previous delta are out of sync', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [0], + revoked: [5], + txnTime: 1, + } + + const revocationStatusList = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1] + + const { revoked, issued } = indyVdrCreateLatestRevocationDelta(accum, revocationStatusList, delta) + + expect(issued).toStrictEqual([1, 2, 3, 4]) + expect(revoked).toStrictEqual([6, 7, 8, 9]) + }) + + test('previous delta index exceeds length of revocation status list', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [200], + revoked: [5], + txnTime: 1, + } + + const revocationStatusList = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1] + + expect(() => indyVdrCreateLatestRevocationDelta(accum, revocationStatusList, delta)).toThrowError() + }) + }) +}) diff --git a/packages/indy-vdr/src/anoncreds/utils/identifiers.ts b/packages/indy-vdr/src/anoncreds/utils/identifiers.ts index 1e9d6a8fd3..46bebaadf7 100644 --- a/packages/indy-vdr/src/anoncreds/utils/identifiers.ts +++ b/packages/indy-vdr/src/anoncreds/utils/identifiers.ts @@ -46,18 +46,28 @@ export function getDidIndySchemaId(namespace: string, unqualifiedDid: string, na export function getDidIndyCredentialDefinitionId( namespace: string, unqualifiedDid: string, - seqNo: string | number, + schemaSeqNo: string | number, tag: string ) { - return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/CLAIM_DEF/${seqNo}/${tag}` + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/${tag}` } -export function getDidIndyRevocationRegistryId( +export function getDidIndyRevocationRegistryDefinitionId( namespace: string, unqualifiedDid: string, - seqNo: string | number, + schemaSeqNo: string | number, credentialDefinitionTag: string, revocationRegistryTag: string ) { - return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/REV_REG_DEF/${seqNo}/${credentialDefinitionTag}/${revocationRegistryTag}` + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/REV_REG_DEF/${schemaSeqNo}/${credentialDefinitionTag}/${revocationRegistryTag}` +} + +export function getDidIndyRevocationRegistryEntryId( + namespace: string, + unqualifiedDid: string, + schemaSeqNo: string | number, + credentialDefinitionTag: string, + revocationRegistryTag: string +) { + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/REV_REG_ENTRY/${schemaSeqNo}/${credentialDefinitionTag}/${revocationRegistryTag}` } diff --git a/packages/indy-vdr/src/anoncreds/utils/transform.ts b/packages/indy-vdr/src/anoncreds/utils/transform.ts index 36f4628bb4..87180e50d3 100644 --- a/packages/indy-vdr/src/anoncreds/utils/transform.ts +++ b/packages/indy-vdr/src/anoncreds/utils/transform.ts @@ -1,26 +1,51 @@ import type { AnonCredsRevocationStatusList, AnonCredsRevocationRegistryDefinition } from '@aries-framework/anoncreds' +import { AriesFrameworkError } from '@aries-framework/core' + +export type RevocationRegistryDelta = { + accum: string + issued: number[] + revoked: number[] + txnTime: number +} + +enum RevocationState { + Active, + Revoked, +} + export function anonCredsRevocationStatusListFromIndyVdr( revocationRegistryDefinitionId: string, revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition, - delta: RevocRegDelta, - timestamp: number, + delta: RevocationRegistryDelta, isIssuanceByDefault: boolean ): AnonCredsRevocationStatusList { + // Check whether the highest delta index is supported in the `maxCredNum` field of the + // revocation registry definition. This will likely also be checked on other levels as well + // by the ledger or the indy-vdr library itself + if (Math.max(...delta.issued, ...delta.revoked) >= revocationRegistryDefinition.value.maxCredNum) { + throw new AriesFrameworkError( + `Highest delta index '${Math.max( + ...delta.issued, + ...delta.revoked + )}' is too large for the Revocation registry maxCredNum '${revocationRegistryDefinition.value.maxCredNum}' ` + ) + } + // 0 means unrevoked, 1 means revoked - const defaultState = isIssuanceByDefault ? 0 : 1 + const defaultState = isIssuanceByDefault ? RevocationState.Active : RevocationState.Revoked // Fill with default value const revocationList = new Array(revocationRegistryDefinition.value.maxCredNum).fill(defaultState) // Set all `issuer` indexes to 0 (not revoked) for (const issued of delta.issued ?? []) { - revocationList[issued] = 0 + revocationList[issued] = RevocationState.Active } // Set all `revoked` indexes to 1 (revoked) for (const revoked of delta.revoked ?? []) { - revocationList[revoked] = 1 + revocationList[revoked] = RevocationState.Revoked } return { @@ -28,12 +53,83 @@ export function anonCredsRevocationStatusListFromIndyVdr( currentAccumulator: delta.accum, revRegDefId: revocationRegistryDefinitionId, revocationList, - timestamp, + timestamp: delta.txnTime, } } -interface RevocRegDelta { - accum: string - issued: number[] - revoked: number[] +/** + * + * Transforms the previous deltas and the full revocation status list into the latest delta + * + * ## Example + * + * input: + * + * revocationStatusList: [0, 1, 1, 1, 0, 0, 0, 1, 1, 0] + * previousDelta: + * - issued: [1, 2, 5, 8, 9] + * - revoked: [0, 3, 4, 6, 7] + * + * output: + * - issued: [5, 9] + * - revoked: [3, 7] + * + */ +export function indyVdrCreateLatestRevocationDelta( + currentAccumulator: string, + revocationStatusList: Array, + previousDelta?: RevocationRegistryDelta +) { + if (previousDelta && Math.max(...previousDelta.issued, ...previousDelta.revoked) > revocationStatusList.length - 1) { + throw new AriesFrameworkError( + `Indy Vdr delta contains an index '${Math.max( + ...previousDelta.revoked, + ...previousDelta.issued + )}' that exceeds the length of the revocation status list '${revocationStatusList.length}'` + ) + } + + const issued: Array = [] + const revoked: Array = [] + + if (previousDelta) { + for (const issuedIdx of previousDelta.issued) { + // Check whether the revocationStatusList has a different state compared to the delta + if (revocationStatusList[issuedIdx] !== RevocationState.Active) { + issued.push(issuedIdx) + } + } + + for (const revokedIdx of previousDelta.revoked) { + // Check whether the revocationStatusList has a different state compared to the delta + if (revocationStatusList[revokedIdx] !== RevocationState.Revoked) { + revoked.push(revokedIdx) + } + } + + revocationStatusList.forEach((revocationStatus, idx) => { + // Check whether the revocationStatusList entry is not included in the previous delta issued indices + if (revocationStatus === RevocationState.Active && !previousDelta.issued.includes(idx)) { + issued.push(idx) + } + + // Check whether the revocationStatusList entry is not included in the previous delta revoked indices + if (revocationStatus === RevocationState.Revoked && !previousDelta.revoked.includes(idx)) { + revoked.push(idx) + } + }) + } else { + // No delta is provided, initial state, so the entire revocation status list is converted to two list of indices + revocationStatusList.forEach((revocationStatus, idx) => { + if (revocationStatus === RevocationState.Active) issued.push(idx) + if (revocationStatus === RevocationState.Revoked) revoked.push(idx) + }) + } + + return { + issued, + revoked, + accum: currentAccumulator, + prevAccum: previousDelta?.accum, + } } diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts index 89271f5fdd..7b2dfd768c 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts @@ -9,6 +9,8 @@ import { buildDidDocument } from './didIndyUtil' export class IndyVdrIndyDidResolver implements DidResolver { public readonly supportedMethods = ['indy'] + public readonly allowsCaching = true + public async resolve(agentContext: AgentContext, did: string): Promise { const didDocumentMetadata = {} try { diff --git a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts index 4484586078..2563bbbd18 100644 --- a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts @@ -12,6 +12,8 @@ import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovU export class IndyVdrSovDidResolver implements DidResolver { public readonly supportedMethods = ['sov'] + public readonly allowsCaching = true + public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise { const didDocumentMetadata = {} diff --git a/packages/indy-vdr/tests/__fixtures__/anoncreds.ts b/packages/indy-vdr/tests/__fixtures__/anoncreds.ts index fea36d5fcb..f4b57c21f0 100644 --- a/packages/indy-vdr/tests/__fixtures__/anoncreds.ts +++ b/packages/indy-vdr/tests/__fixtures__/anoncreds.ts @@ -28,3 +28,14 @@ export const credentialDefinitionValue = { y: '1 1BF97F07270EC21A89E43BCA645D86A755F846B547238F1DA379E088CDD9B40D 1 146BB00F56FFC0DEF6541CEB484C718559B398DB1547B52850E46B23144161F1 1 079A1BEF8DFFA4E6352F701D476664340E7FBE5D3F46B897412BD2B5F10E33D7 1 02FDC508AEF90FB11961AF332BE4037973C76B954FFA48848F7E0588E93FCA8C 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', }, } + +export const revocationRegistryDefinitionValue = { + maxCredNum: 11, + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', +} diff --git a/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts index c98a807b91..f55b43fd8d 100644 --- a/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts @@ -1,14 +1,12 @@ import type { IndyVdrDidCreateOptions, IndyVdrDidCreateResult } from '../src/dids/IndyVdrIndyDidRegistrar' -import type { RevocationRegistryEntryResponse } from '@hyperledger/indy-vdr-shared' -import { parseIndyDid } from '@aries-framework/anoncreds' +import { + getUnqualifiedRevocationRegistryDefinitionId, + parseIndyDid, + parseIndyRevocationRegistryId, +} from '@aries-framework/anoncreds' import { Agent, DidsModule, TypedArrayEncoder } from '@aries-framework/core' import { indyVdr } from '@hyperledger/indy-vdr-nodejs' -import { - CustomRequest, - RevocationRegistryDefinitionRequest, - RevocationRegistryEntryRequest, -} from '@hyperledger/indy-vdr-shared' import { agentDependencies, getAgentConfig, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' import { IndySdkModule } from '../../indy-sdk/src' @@ -16,10 +14,9 @@ import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' import { IndyVdrIndyDidResolver, IndyVdrModule, IndyVdrSovDidResolver } from '../src' import { IndyVdrAnonCredsRegistry } from '../src/anoncreds/IndyVdrAnonCredsRegistry' import { IndyVdrIndyDidRegistrar } from '../src/dids/IndyVdrIndyDidRegistrar' -import { verificationKeyForIndyDid } from '../src/dids/didIndyUtil' import { IndyVdrPoolService } from '../src/pool' -import { credentialDefinitionValue } from './__fixtures__/anoncreds' +import { credentialDefinitionValue, revocationRegistryDefinitionValue } from './__fixtures__/anoncreds' import { indyVdrModuleConfig } from './helpers' const endorserConfig = getAgentConfig('IndyVdrAnonCredsRegistryEndorser') @@ -64,7 +61,6 @@ const agent = new Agent({ }) const indyVdrPoolService = endorser.dependencyManager.resolve(IndyVdrPoolService) -const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') describe('IndyVdrAnonCredsRegistry', () => { let endorserDid: string @@ -104,7 +100,6 @@ describe('IndyVdrAnonCredsRegistry', () => { const didIndyIssuerId = didCreateResult.didState.did const { namespaceIdentifier: legacyIssuerId } = parseIndyDid(didIndyIssuerId) const dynamicVersion = `1.${Math.random() * 100}` - const signingKey = await verificationKeyForIndyDid(endorser.context, didIndyIssuerId) const legacySchemaId = `${legacyIssuerId}:2:test:${dynamicVersion}` const didIndySchemaId = `did:indy:pool:localtest:${legacyIssuerId}/anoncreds/v0/SCHEMA/test/${dynamicVersion}` @@ -120,6 +115,7 @@ describe('IndyVdrAnonCredsRegistry', () => { version: dynamicVersion, }, }) + const schemaSeqNo = schemaResult.schemaMetadata.indyLedgerSeqNo as number expect(schemaResult).toMatchObject({ schemaState: { @@ -174,8 +170,8 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }) - const legacyCredentialDefinitionId = `${legacyIssuerId}:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG` - const didIndyCredentialDefinitionId = `did:indy:pool:localtest:${legacyIssuerId}/anoncreds/v0/CLAIM_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG` + const legacyCredentialDefinitionId = `${legacyIssuerId}:3:CL:${schemaSeqNo}:TAG` + const didIndyCredentialDefinitionId = `did:indy:pool:localtest:${legacyIssuerId}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/TAG` const credentialDefinitionResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(endorser.context, { credentialDefinition: { issuerId: didIndyIssuerId, @@ -250,18 +246,15 @@ describe('IndyVdrAnonCredsRegistry', () => { resolutionMetadata: {}, }) - // We don't support creating a revocation registry using AFJ yet, so we directly use indy-vdr to create the revocation registry - const legacyRevocationRegistryId = `${legacyIssuerId}:4:${legacyIssuerId}:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG:CL_ACCUM:tag` - const didIndyRevocationRegistryId = `did:indy:pool:localtest:${legacyIssuerId}/anoncreds/v0/REV_REG_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG/tag` - const revocationRegistryRequest = new RevocationRegistryDefinitionRequest({ - submitterDid: legacyIssuerId, - revocationRegistryDefinitionV1: { - credDefId: legacyCredentialDefinitionId, - id: legacyRevocationRegistryId, + const { + revocationRegistryDefinitionState: { revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId }, + } = await indyVdrAnonCredsRegistry.registerRevocationRegistryDefinition(endorser.context, { + revocationRegistryDefinition: { + tag: 'REV_TAG', + issuerId: didIndyIssuerId, + credDefId: didIndyCredentialDefinitionId, revocDefType: 'CL_ACCUM', - tag: 'tag', value: { - issuanceType: 'ISSUANCE_BY_DEFAULT', maxCredNum: 100, publicKeys: { accumKey: { @@ -272,55 +265,37 @@ describe('IndyVdrAnonCredsRegistry', () => { tailsLocation: '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', }, - ver: '1.0', }, - }) - - // After this call, the revocation registry should now be resolvable - const writeRequest = await pool.prepareWriteRequest( - endorser.context, - revocationRegistryRequest, - signingKey, - endorserDid - ) - const endorsedRequest = await endorser.modules.indyVdr.endorseTransaction(writeRequest.body, endorserDid) - await pool.submitRequest(new CustomRequest({ customRequest: endorsedRequest })) - - // Also create a revocation registry entry - const revocationEntryRequest = new RevocationRegistryEntryRequest({ - revocationRegistryDefinitionId: legacyRevocationRegistryId, - revocationRegistryDefinitionType: 'CL_ACCUM', - revocationRegistryEntry: { - ver: '1.0', - value: { - accum: '1', - }, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, }, - submitterDid: legacyIssuerId, }) - // After this call we can query the revocation registry entries (using timestamp now) + if (!didIndyRevocationRegistryDefinitionId) { + throw Error('revocation registry definition was not created correctly') + } - const revocationEntryWriteRequest = await pool.prepareWriteRequest( - endorser.context, - revocationEntryRequest, - signingKey, - endorserDid + const { credentialDefinitionTag, revocationRegistryTag } = parseIndyRevocationRegistryId( + didIndyRevocationRegistryDefinitionId ) - const endorsedRevocationEntryWriteRequest = await endorser.modules.indyVdr.endorseTransaction( - revocationEntryWriteRequest.body, - endorserDid + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + legacyIssuerId, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag ) - const entryResponse = (await pool.submitRequest( - new CustomRequest({ customRequest: endorsedRevocationEntryWriteRequest }) - )) as RevocationRegistryEntryResponse + + // Wait some time before resolving revocation registry definition object + await new Promise((res) => setTimeout(res, 1000)) const legacyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( endorser.context, - legacyRevocationRegistryId + legacyRevocationRegistryDefinitionId ) + expect(legacyRevocationRegistryDefinition).toMatchObject({ - revocationRegistryDefinitionId: legacyRevocationRegistryId, + revocationRegistryDefinitionId: legacyRevocationRegistryDefinitionId, revocationRegistryDefinition: { issuerId: legacyIssuerId, revocDefType: 'CL_ACCUM', @@ -335,7 +310,7 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }, }, - tag: 'tag', + tag: 'REV_TAG', credDefId: legacyCredentialDefinitionId, }, revocationRegistryDefinitionMetadata: { @@ -347,10 +322,11 @@ describe('IndyVdrAnonCredsRegistry', () => { const didIndyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( endorser.context, - didIndyRevocationRegistryId + didIndyRevocationRegistryDefinitionId ) + expect(didIndyRevocationRegistryDefinition).toMatchObject({ - revocationRegistryDefinitionId: didIndyRevocationRegistryId, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, revocationRegistryDefinition: { issuerId: didIndyIssuerId, revocDefType: 'CL_ACCUM', @@ -365,7 +341,7 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }, }, - tag: 'tag', + tag: 'REV_TAG', credDefId: didIndyCredentialDefinitionId, }, revocationRegistryDefinitionMetadata: { @@ -375,10 +351,30 @@ describe('IndyVdrAnonCredsRegistry', () => { resolutionMetadata: {}, }) + const registerStatusListResult = await indyVdrAnonCredsRegistry.registerRevocationStatusList(endorser.context, { + revocationStatusList: { + issuerId: didIndyIssuerId, + revRegDefId: didIndyRevocationRegistryDefinitionId, + revocationList: new Array(100).fill(0), + currentAccumulator: '1', + }, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, + }, + }) + + if (registerStatusListResult.revocationStatusListState.state !== 'finished') { + throw new Error(`Unable to register status list: ${JSON.stringify(registerStatusListResult)}`) + } + + // Wait some time before resolving revocation status list object + await new Promise((res) => setTimeout(res, 1000)) + const legacyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( endorser.context, - legacyRevocationRegistryId, - entryResponse.result.txnMetadata.txnTime + legacyRevocationRegistryDefinitionId, + registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp ) expect(legacyRevocationStatusList).toMatchObject({ @@ -386,13 +382,13 @@ describe('IndyVdrAnonCredsRegistry', () => { revocationStatusList: { issuerId: legacyIssuerId, currentAccumulator: '1', - revRegDefId: legacyRevocationRegistryId, + revRegDefId: legacyRevocationRegistryDefinitionId, revocationList: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], - timestamp: entryResponse.result.txnMetadata.txnTime, + timestamp: registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp, }, revocationStatusListMetadata: { didIndyNamespace: 'pool:localtest', @@ -401,8 +397,8 @@ describe('IndyVdrAnonCredsRegistry', () => { const didIndyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( endorser.context, - didIndyRevocationRegistryId, - entryResponse.result.txnMetadata.txnTime + didIndyRevocationRegistryDefinitionId, + registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp ) expect(didIndyRevocationStatusList).toMatchObject({ @@ -410,13 +406,13 @@ describe('IndyVdrAnonCredsRegistry', () => { revocationStatusList: { issuerId: didIndyIssuerId, currentAccumulator: '1', - revRegDefId: didIndyRevocationRegistryId, + revRegDefId: didIndyRevocationRegistryDefinitionId, revocationList: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], - timestamp: entryResponse.result.txnMetadata.txnTime, + timestamp: registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp, }, revocationStatusListMetadata: { didIndyNamespace: 'pool:localtest', @@ -429,8 +425,6 @@ describe('IndyVdrAnonCredsRegistry', () => { const legacyIssuerId = 'DJKobikPAaYWAu9vfhEEo5' const didIndyIssuerId = 'did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5' - const signingKey = await verificationKeyForIndyDid(agent.context, didIndyIssuerId) - const legacySchemaId = `DJKobikPAaYWAu9vfhEEo5:2:test:${dynamicVersion}` const didIndySchemaId = `did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5/anoncreds/v0/SCHEMA/test/${dynamicVersion}` @@ -447,6 +441,8 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }) + const schemaSeqNo = schemaResult.schemaMetadata.indyLedgerSeqNo as number + expect(schemaResult).toMatchObject({ schemaState: { state: 'finished', @@ -500,8 +496,8 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }) - const legacyCredentialDefinitionId = `DJKobikPAaYWAu9vfhEEo5:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG` - const didIndyCredentialDefinitionId = `did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5/anoncreds/v0/CLAIM_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG` + const legacyCredentialDefinitionId = `DJKobikPAaYWAu9vfhEEo5:3:CL:${schemaSeqNo}:TAG` + const didIndyCredentialDefinitionId = `did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/TAG` const credentialDefinitionResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(endorser.context, { credentialDefinition: { issuerId: didIndyIssuerId, @@ -576,18 +572,15 @@ describe('IndyVdrAnonCredsRegistry', () => { resolutionMetadata: {}, }) - // We don't support creating a revocation registry using AFJ yet, so we directly use indy-vdr to create the revocation registry - const legacyRevocationRegistryId = `DJKobikPAaYWAu9vfhEEo5:4:DJKobikPAaYWAu9vfhEEo5:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG:CL_ACCUM:tag` - const didIndyRevocationRegistryId = `did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5/anoncreds/v0/REV_REG_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG/tag` - const revocationRegistryRequest = new RevocationRegistryDefinitionRequest({ - submitterDid: 'DJKobikPAaYWAu9vfhEEo5', - revocationRegistryDefinitionV1: { - credDefId: legacyCredentialDefinitionId, - id: legacyRevocationRegistryId, + const { + revocationRegistryDefinitionState: { revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId }, + } = await indyVdrAnonCredsRegistry.registerRevocationRegistryDefinition(endorser.context, { + revocationRegistryDefinition: { + tag: 'REV_TAG', + issuerId: didIndyIssuerId, + credDefId: didIndyCredentialDefinitionId, revocDefType: 'CL_ACCUM', - tag: 'tag', value: { - issuanceType: 'ISSUANCE_BY_DEFAULT', maxCredNum: 100, publicKeys: { accumKey: { @@ -598,42 +591,37 @@ describe('IndyVdrAnonCredsRegistry', () => { tailsLocation: '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', }, - ver: '1.0', }, - }) - - // After this call, the revocation registry should now be resolvable - const writeRequest = await pool.prepareWriteRequest(endorser.context, revocationRegistryRequest, signingKey) - await pool.submitRequest(writeRequest) - - // Also create a revocation registry entry - const revocationEntryRequest = new RevocationRegistryEntryRequest({ - revocationRegistryDefinitionId: legacyRevocationRegistryId, - revocationRegistryDefinitionType: 'CL_ACCUM', - revocationRegistryEntry: { - ver: '1.0', - value: { - accum: '1', - }, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, }, - submitterDid: legacyIssuerId, }) - // After this call we can query the revocation registry entries (using timestamp now) + if (!didIndyRevocationRegistryDefinitionId) { + throw Error('revocation registry definition was not created correctly') + } - const revocationEntryWriteRequest = await pool.prepareWriteRequest( - endorser.context, - revocationEntryRequest, - signingKey + const { credentialDefinitionTag, revocationRegistryTag } = parseIndyRevocationRegistryId( + didIndyRevocationRegistryDefinitionId + ) + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + legacyIssuerId, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag ) - const entryResponse = await pool.submitRequest(revocationEntryWriteRequest) + + // Wait some time before resolving revocation registry definition object + await new Promise((res) => setTimeout(res, 1000)) const legacyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( endorser.context, - legacyRevocationRegistryId + legacyRevocationRegistryDefinitionId ) + expect(legacyRevocationRegistryDefinition).toMatchObject({ - revocationRegistryDefinitionId: legacyRevocationRegistryId, + revocationRegistryDefinitionId: legacyRevocationRegistryDefinitionId, revocationRegistryDefinition: { issuerId: legacyIssuerId, revocDefType: 'CL_ACCUM', @@ -648,7 +636,7 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }, }, - tag: 'tag', + tag: 'REV_TAG', credDefId: legacyCredentialDefinitionId, }, revocationRegistryDefinitionMetadata: { @@ -660,10 +648,11 @@ describe('IndyVdrAnonCredsRegistry', () => { const didIndyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( endorser.context, - didIndyRevocationRegistryId + didIndyRevocationRegistryDefinitionId ) + expect(didIndyRevocationRegistryDefinition).toMatchObject({ - revocationRegistryDefinitionId: didIndyRevocationRegistryId, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, revocationRegistryDefinition: { issuerId: didIndyIssuerId, revocDefType: 'CL_ACCUM', @@ -678,7 +667,7 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }, }, - tag: 'tag', + tag: 'REV_TAG', credDefId: didIndyCredentialDefinitionId, }, revocationRegistryDefinitionMetadata: { @@ -688,10 +677,30 @@ describe('IndyVdrAnonCredsRegistry', () => { resolutionMetadata: {}, }) + const registerStatusListResult = await indyVdrAnonCredsRegistry.registerRevocationStatusList(endorser.context, { + revocationStatusList: { + issuerId: didIndyIssuerId, + revRegDefId: didIndyRevocationRegistryDefinitionId, + revocationList: new Array(100).fill(0), + currentAccumulator: '1', + }, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, + }, + }) + + if (registerStatusListResult.revocationStatusListState.state !== 'finished') { + throw new Error(`Unable to register status list: ${JSON.stringify(registerStatusListResult)}`) + } + + // Wait some time before resolving revocation status list object + await new Promise((res) => setTimeout(res, 1000)) + const legacyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( endorser.context, - legacyRevocationRegistryId, - entryResponse.result.txnMetadata.txnTime + legacyRevocationRegistryDefinitionId, + registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp ) expect(legacyRevocationStatusList).toMatchObject({ @@ -699,13 +708,13 @@ describe('IndyVdrAnonCredsRegistry', () => { revocationStatusList: { issuerId: legacyIssuerId, currentAccumulator: '1', - revRegDefId: legacyRevocationRegistryId, + revRegDefId: legacyRevocationRegistryDefinitionId, revocationList: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], - timestamp: entryResponse.result.txnMetadata.txnTime, + timestamp: registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp, }, revocationStatusListMetadata: { didIndyNamespace: 'pool:localtest', @@ -714,8 +723,8 @@ describe('IndyVdrAnonCredsRegistry', () => { const didIndyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( endorser.context, - didIndyRevocationRegistryId, - entryResponse.result.txnMetadata.txnTime + didIndyRevocationRegistryDefinitionId, + registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp ) expect(didIndyRevocationStatusList).toMatchObject({ @@ -723,13 +732,13 @@ describe('IndyVdrAnonCredsRegistry', () => { revocationStatusList: { issuerId: didIndyIssuerId, currentAccumulator: '1', - revRegDefId: didIndyRevocationRegistryId, + revRegDefId: didIndyRevocationRegistryDefinitionId, revocationList: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], - timestamp: entryResponse.result.txnMetadata.txnTime, + timestamp: registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp, }, revocationStatusListMetadata: { didIndyNamespace: 'pool:localtest', @@ -737,7 +746,10 @@ describe('IndyVdrAnonCredsRegistry', () => { }) }) - test('register and resolve a schema and credential definition (external)', async () => { + test('register and resolve a schema, credential definition, revocation registry and status list (external)', async () => { + // ---- + // CREATE DID + // ---- const didCreateTxResult = (await agent.dids.create({ method: 'indy', options: { @@ -773,8 +785,10 @@ describe('IndyVdrAnonCredsRegistry', () => { const legacyIssuerId = namespaceIdentifier const didIndyIssuerId = agentDid - const signingKey = await verificationKeyForIndyDid(agent.context, didIndyIssuerId) + // ---- + // CREATE SCHEMA + // ---- const legacySchemaId = `${namespaceIdentifier}:2:test:${dynamicVersion}` const didIndySchemaId = `did:indy:pool:localtest:${namespaceIdentifier}/anoncreds/v0/SCHEMA/test/${dynamicVersion}` @@ -859,6 +873,9 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }) + // ---- + // CREATE CREDENTIAL DEFINITION + // ---- const legacyCredentialDefinitionId = `${namespaceIdentifier}:3:CL:${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}:TAG` const didIndyCredentialDefinitionId = `did:indy:pool:localtest:${namespaceIdentifier}/anoncreds/v0/CLAIM_DEF/${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}/TAG` @@ -879,13 +896,13 @@ describe('IndyVdrAnonCredsRegistry', () => { const { credentialDefinitionState } = createCredDefTxResult if (credentialDefinitionState.state !== 'action' || credentialDefinitionState.action !== 'endorseIndyTransaction') - throw Error('unexpected schema state') + throw Error('unexpected credential definition state') const endorsedCredDefTx = await endorser.modules.indyVdr.endorseTransaction( credentialDefinitionState.credentialDefinitionRequest, endorserDid ) - const SubmitCredDefTxResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(agent.context, { + const submitCredDefTxResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(agent.context, { credentialDefinition: credentialDefinitionState.credentialDefinition, options: { endorserMode: 'external', @@ -893,7 +910,7 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }) - expect(SubmitCredDefTxResult).toMatchObject({ + expect(submitCredDefTxResult).toMatchObject({ credentialDefinitionMetadata: {}, credentialDefinitionState: { credentialDefinition: { @@ -953,92 +970,76 @@ describe('IndyVdrAnonCredsRegistry', () => { resolutionMetadata: {}, }) - // We don't support creating a revocation registry using AFJ yet, so we directly use indy-vdr to create the revocation registry - const legacyRevocationRegistryId = `${namespaceIdentifier}:4:${namespaceIdentifier}:3:CL:${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}:TAG:CL_ACCUM:tag` - const didIndyRevocationRegistryId = `did:indy:pool:localtest:${namespaceIdentifier}/anoncreds/v0/REV_REG_DEF/${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}/TAG/tag` - const revocationRegistryRequest = new RevocationRegistryDefinitionRequest({ - submitterDid: namespaceIdentifier, - revocationRegistryDefinitionV1: { - credDefId: legacyCredentialDefinitionId, - id: legacyRevocationRegistryId, + // ---- + // CREATE REVOCATION REGISTRY + // ---- + const legacyRevocationRegistryDefinitionId = `${namespaceIdentifier}:4:${namespaceIdentifier}:3:CL:${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}:TAG:CL_ACCUM:REV_TAG` + const didIndyRevocationRegistryDefinitionId = `did:indy:pool:localtest:${namespaceIdentifier}/anoncreds/v0/REV_REG_DEF/${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}/TAG/REV_TAG` + + const createRevRegDefTxResult = await indyVdrAnonCredsRegistry.registerRevocationRegistryDefinition(agent.context, { + revocationRegistryDefinition: { + tag: 'REV_TAG', revocDefType: 'CL_ACCUM', - tag: 'tag', - value: { - issuanceType: 'ISSUANCE_BY_DEFAULT', - maxCredNum: 100, - publicKeys: { - accumKey: { - z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', - }, - }, - tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - tailsLocation: - '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - }, - ver: '1.0', + credDefId: didIndyCredentialDefinitionId, + issuerId: didIndyIssuerId, + value: revocationRegistryDefinitionValue, + }, + options: { + endorserMode: 'external', + endorserDid, }, }) - // After this call, the revocation registry should now be resolvable - const writeRequest = await pool.prepareWriteRequest( - agent.context, - revocationRegistryRequest, - signingKey, + const { revocationRegistryDefinitionState } = createRevRegDefTxResult + + if ( + revocationRegistryDefinitionState.state !== 'action' || + revocationRegistryDefinitionState.action !== 'endorseIndyTransaction' + ) { + throw Error('unexpected revocation registry definition state') + } + + const endorsedRevRegDefTx = await endorser.modules.indyVdr.endorseTransaction( + revocationRegistryDefinitionState.revocationRegistryDefinitionRequest, endorserDid ) - const endorsedRequest = await endorser.modules.indyVdr.endorseTransaction(writeRequest.body, endorserDid) - await pool.submitRequest(new CustomRequest({ customRequest: endorsedRequest })) - - // Also create a revocation registry entry - const revocationEntryRequest = new RevocationRegistryEntryRequest({ - revocationRegistryDefinitionId: legacyRevocationRegistryId, - revocationRegistryDefinitionType: 'CL_ACCUM', - revocationRegistryEntry: { - ver: '1.0', - value: { - accum: '1', - }, + const submitRevRegDefTxResult = await indyVdrAnonCredsRegistry.registerRevocationRegistryDefinition(agent.context, { + revocationRegistryDefinition: revocationRegistryDefinitionState.revocationRegistryDefinition, + options: { + endorserMode: 'external', + endorsedTransaction: endorsedRevRegDefTx, }, - submitterDid: legacyIssuerId, }) - // After this call we can query the revocation registry entries (using timestamp now) + expect(submitRevRegDefTxResult).toMatchObject({ + revocationRegistryDefinitionMetadata: {}, + revocationRegistryDefinitionState: { + revocationRegistryDefinition: { + credDefId: didIndyCredentialDefinitionId, + issuerId: didIndyIssuerId, + tag: 'REV_TAG', + value: revocationRegistryDefinitionValue, + }, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + }) - const revocationEntryWriteRequest = await pool.prepareWriteRequest( - agent.context, - revocationEntryRequest, - signingKey, - endorserDid - ) - const endorsedRevEntryWriteRequest = await endorser.modules.indyVdr.endorseTransaction( - revocationEntryWriteRequest.body, - endorserDid - ) - const entryResponse = (await pool.submitRequest( - new CustomRequest({ customRequest: endorsedRevEntryWriteRequest }) - )) as RevocationRegistryEntryResponse + await new Promise((res) => setTimeout(res, 1000)) const legacyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( agent.context, - legacyRevocationRegistryId + legacyRevocationRegistryDefinitionId ) + expect(legacyRevocationRegistryDefinition).toMatchObject({ - revocationRegistryDefinitionId: legacyRevocationRegistryId, + revocationRegistryDefinitionId: legacyRevocationRegistryDefinitionId, revocationRegistryDefinition: { issuerId: legacyIssuerId, revocDefType: 'CL_ACCUM', - value: { - maxCredNum: 100, - tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - tailsLocation: - '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - publicKeys: { - accumKey: { - z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', - }, - }, - }, - tag: 'tag', + value: revocationRegistryDefinitionValue, + tag: 'REV_TAG', credDefId: legacyCredentialDefinitionId, }, revocationRegistryDefinitionMetadata: { @@ -1050,25 +1051,16 @@ describe('IndyVdrAnonCredsRegistry', () => { const didIndyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( agent.context, - didIndyRevocationRegistryId + didIndyRevocationRegistryDefinitionId ) + expect(didIndyRevocationRegistryDefinition).toMatchObject({ - revocationRegistryDefinitionId: didIndyRevocationRegistryId, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, revocationRegistryDefinition: { issuerId: didIndyIssuerId, revocDefType: 'CL_ACCUM', - value: { - maxCredNum: 100, - tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - tailsLocation: - '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - publicKeys: { - accumKey: { - z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', - }, - }, - }, - tag: 'tag', + value: revocationRegistryDefinitionValue, + tag: 'REV_TAG', credDefId: didIndyCredentialDefinitionId, }, revocationRegistryDefinitionMetadata: { @@ -1078,10 +1070,59 @@ describe('IndyVdrAnonCredsRegistry', () => { resolutionMetadata: {}, }) + // ---- + // CREATE REVOCATION STATUS LIST + // ---- + const revocationStatusListValue = { + issuerId: didIndyIssuerId, + revRegDefId: didIndyRevocationRegistryDefinitionId, + revocationList: [1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0], + currentAccumulator: '1', + } + + const createRevStatusListTxResult = await indyVdrAnonCredsRegistry.registerRevocationStatusList(agent.context, { + options: { + endorserMode: 'external', + endorserDid, + }, + revocationStatusList: revocationStatusListValue, + }) + + const { revocationStatusListState } = createRevStatusListTxResult + + if (revocationStatusListState.state !== 'action' || revocationStatusListState.action !== 'endorseIndyTransaction') { + throw Error('unexpected revocation status list state') + } + + const endorsedRevStatusListTx = await endorser.modules.indyVdr.endorseTransaction( + revocationStatusListState.revocationStatusListRequest, + endorserDid + ) + + const submitRevStatusListTxResult = await indyVdrAnonCredsRegistry.registerRevocationStatusList(agent.context, { + revocationStatusList: revocationStatusListState.revocationStatusList, + options: { + endorserMode: 'external', + endorsedTransaction: endorsedRevStatusListTx, + }, + }) + + expect(submitRevStatusListTxResult).toMatchObject({ + revocationStatusListMetadata: {}, + revocationStatusListState: { + revocationStatusList: { ...revocationStatusListValue, timestamp: expect.any(Number) }, + state: 'finished', + }, + registrationMetadata: {}, + }) + + // Wait some time before resolving status list + await new Promise((res) => setTimeout(res, 1000)) + const legacyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( agent.context, - legacyRevocationRegistryId, - entryResponse.result.txnMetadata.txnTime + legacyRevocationRegistryDefinitionId, + submitRevStatusListTxResult.revocationStatusListState.revocationStatusList?.timestamp as number ) expect(legacyRevocationStatusList).toMatchObject({ @@ -1089,13 +1130,9 @@ describe('IndyVdrAnonCredsRegistry', () => { revocationStatusList: { issuerId: legacyIssuerId, currentAccumulator: '1', - revRegDefId: legacyRevocationRegistryId, - revocationList: [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - timestamp: entryResponse.result.txnMetadata.txnTime, + revRegDefId: legacyRevocationRegistryDefinitionId, + revocationList: revocationStatusListValue.revocationList, + timestamp: submitRevStatusListTxResult.revocationStatusListState.revocationStatusList?.timestamp, }, revocationStatusListMetadata: { didIndyNamespace: 'pool:localtest', @@ -1104,8 +1141,8 @@ describe('IndyVdrAnonCredsRegistry', () => { const didIndyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( agent.context, - didIndyRevocationRegistryId, - entryResponse.result.txnMetadata.txnTime + didIndyRevocationRegistryDefinitionId, + submitRevStatusListTxResult.revocationStatusListState.revocationStatusList?.timestamp as number ) expect(didIndyRevocationStatusList).toMatchObject({ @@ -1113,13 +1150,9 @@ describe('IndyVdrAnonCredsRegistry', () => { revocationStatusList: { issuerId: didIndyIssuerId, currentAccumulator: '1', - revRegDefId: didIndyRevocationRegistryId, - revocationList: [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - timestamp: entryResponse.result.txnMetadata.txnTime, + revRegDefId: didIndyRevocationRegistryDefinitionId, + revocationList: revocationStatusListValue.revocationList, + timestamp: submitRevStatusListTxResult.revocationStatusListState.revocationStatusList?.timestamp, }, revocationStatusListMetadata: { didIndyNamespace: 'pool:localtest', diff --git a/packages/sd-jwt-vc/package.json b/packages/sd-jwt-vc/package.json index df62927318..1f973c648e 100644 --- a/packages/sd-jwt-vc/package.json +++ b/packages/sd-jwt-vc/package.json @@ -31,7 +31,7 @@ "jwt-sd": "^0.1.2" }, "devDependencies": { - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", + "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", "reflect-metadata": "^0.1.13", "rimraf": "^4.4.0", "typescript": "~4.9.5" diff --git a/packages/tenants/tests/tenants.e2e.test.ts b/packages/tenants/tests/tenants.e2e.test.ts index f37e962449..ffd2518f5b 100644 --- a/packages/tenants/tests/tenants.e2e.test.ts +++ b/packages/tenants/tests/tenants.e2e.test.ts @@ -1,6 +1,6 @@ import type { InitConfig } from '@aries-framework/core' -import { ConnectionsModule, OutOfBandRecord, Agent } from '@aries-framework/core' +import { ConnectionsModule, OutOfBandRecord, Agent, CacheModule, InMemoryLruCache } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' @@ -39,6 +39,9 @@ const agent1 = new Agent({ connections: new ConnectionsModule({ autoAcceptConnections: true, }), + cache: new CacheModule({ + cache: new InMemoryLruCache({ limit: 500 }), + }), }, dependencies: agentDependencies, }) @@ -51,6 +54,9 @@ const agent2 = new Agent({ connections: new ConnectionsModule({ autoAcceptConnections: true, }), + cache: new CacheModule({ + cache: new InMemoryLruCache({ limit: 500 }), + }), }, dependencies: agentDependencies, }) diff --git a/tests/transport/SubjectInboundTransport.ts b/tests/transport/SubjectInboundTransport.ts index 6e3b5468a2..7cf6932dec 100644 --- a/tests/transport/SubjectInboundTransport.ts +++ b/tests/transport/SubjectInboundTransport.ts @@ -48,7 +48,16 @@ export class SubjectInboundTransport implements InboundTransport { }) } - await messageReceiver.receiveMessage(message, { session }) + // This emits a custom error in order to easily catch in E2E tests when a message + // reception throws an error. TODO: Remove as soon as agent throws errors automatically + try { + await messageReceiver.receiveMessage(message, { session }) + } catch (error) { + agent.events.emit(agent.context, { + type: 'AgentReceiveMessageError', + payload: error, + }) + } }, }) } diff --git a/yarn.lock b/yarn.lock index abae25902c..59c30b61a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32,6 +32,13 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@astronautlabs/jsonpath@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@astronautlabs/jsonpath/-/jsonpath-1.1.2.tgz#af19bb4a7d13dcfbc60c3c998ee1e73d7c2ddc38" + integrity sha512-FqL/muoreH7iltYC1EB5Tvox5E8NSOOPGkgns4G+qxRKl6k5dxEVljUjB5NcKESzkqwnUqWjSZkL61XGYOuV+A== + dependencies: + static-eval "2.0.2" + "@azure/core-asynciterator-polyfill@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.2.tgz#0dd3849fb8d97f062a39db0e5cadc9ffaf861fec" @@ -52,11 +59,24 @@ "@babel/highlight" "^7.22.13" chalk "^2.4.2" +"@babel/code-frame@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== + dependencies: + "@babel/highlight" "^7.23.4" + chalk "^2.4.2" + "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.4": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.4.tgz#457ffe647c480dff59c2be092fc3acf71195c87f" integrity sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g== +"@babel/compat-data@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" + integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== + "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.20.0": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.4.tgz#c6dc73242507b8e2a27fd13a9c1814f9fa34a659" @@ -78,6 +98,27 @@ json5 "^2.2.2" semver "^6.3.0" +"@babel/core@^7.14.6": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.9.tgz#b028820718000f267870822fec434820e9b1e4d1" + integrity sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helpers" "^7.23.9" + "@babel/parser" "^7.23.9" + "@babel/template" "^7.23.9" + "@babel/traverse" "^7.23.9" + "@babel/types" "^7.23.9" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/generator@^7.20.0", "@babel/generator@^7.21.4", "@babel/generator@^7.7.2": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.4.tgz#64a94b7448989f421f919d5239ef553b37bb26bc" @@ -98,6 +139,16 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" +"@babel/generator@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" + integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== + dependencies: + "@babel/types" "^7.23.6" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" @@ -116,6 +167,17 @@ lru-cache "^5.1.1" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" + integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== + dependencies: + "@babel/compat-data" "^7.23.5" + "@babel/helper-validator-option" "^7.23.5" + browserslist "^4.22.2" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz#3a017163dc3c2ba7deb9a7950849a9586ea24c18" @@ -197,6 +259,13 @@ dependencies: "@babel/types" "^7.21.4" +"@babel/helper-module-imports@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== + dependencies: + "@babel/types" "^7.22.15" + "@babel/helper-module-transforms@^7.21.2": version "7.21.2" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz#160caafa4978ac8c00ac66636cb0fa37b024e2d2" @@ -211,6 +280,17 @@ "@babel/traverse" "^7.21.2" "@babel/types" "^7.21.2" +"@babel/helper-module-transforms@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" + integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" @@ -223,6 +303,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== +"@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.3": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + "@babel/helper-remap-async-to-generator@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" @@ -252,6 +337,13 @@ dependencies: "@babel/types" "^7.20.2" +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers@^7.20.0": version "7.20.0" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" @@ -283,6 +375,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== +"@babel/helper-string-parser@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" + integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" @@ -298,6 +395,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180" integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ== +"@babel/helper-validator-option@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" + integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== + "@babel/helper-wrap-function@^7.18.9": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz#75e2d84d499a0ab3b31c33bcfe59d6b8a45f62e3" @@ -317,6 +419,15 @@ "@babel/traverse" "^7.21.0" "@babel/types" "^7.21.0" +"@babel/helpers@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.9.tgz#c3e20bbe7f7a7e10cb9b178384b4affdf5995c7d" + integrity sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ== + dependencies: + "@babel/template" "^7.23.9" + "@babel/traverse" "^7.23.9" + "@babel/types" "^7.23.9" + "@babel/highlight@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" @@ -335,6 +446,15 @@ chalk "^2.4.2" js-tokens "^4.0.0" +"@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.0", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17" @@ -345,6 +465,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== +"@babel/parser@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" + integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== + "@babel/plugin-proposal-async-generator-functions@^7.0.0": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326" @@ -371,6 +496,14 @@ "@babel/helper-plugin-utils" "^7.18.9" "@babel/plugin-syntax-export-default-from" "^7.18.6" +"@babel/plugin-proposal-export-namespace-from@^7.14.5": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz#5f7313ab348cdb19d590145f9247540e94761203" + integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-proposal-nullish-coalescing-operator@^7.0.0", "@babel/plugin-proposal-nullish-coalescing-operator@^7.13.8": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" @@ -442,6 +575,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.18.0", "@babel/plugin-syntax-flow@^7.18.6": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.21.4.tgz#3e37fca4f06d93567c1cd9b75156422e90a67107" @@ -633,6 +773,15 @@ "@babel/helper-plugin-utils" "^7.20.2" "@babel/helper-simple-access" "^7.20.2" +"@babel/plugin-transform-modules-commonjs@^7.14.5": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz#661ae831b9577e52be57dd8356b734f9700b53b4" + integrity sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA== + dependencies: + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + "@babel/plugin-transform-named-capturing-groups-regex@^7.0.0": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz#626298dd62ea51d452c3be58b285d23195ba69a8" @@ -815,6 +964,15 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" +"@babel/template@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a" + integrity sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" + "@babel/traverse@^7.20.0", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" @@ -831,6 +989,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" + integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4", "@babel/types@^7.3.0", "@babel/types@^7.3.3": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.4.tgz#2d5d6bb7908699b3b416409ffd3b5daa25b030d4" @@ -849,6 +1023,15 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@babel/types@^7.23.6", "@babel/types@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002" + integrity sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1071,11 +1254,93 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@digitalbazaar/bitstring@^3.0.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@digitalbazaar/bitstring/-/bitstring-3.1.0.tgz#bbbacb80eaaa53594723a801879b3a95a0401b11" + integrity sha512-Cii+Sl++qaexOvv3vchhgZFfSmtHPNIPzGegaq4ffPnflVXFu+V2qrJ17aL2+gfLxrlC/zazZFuAltyKTPq7eg== + dependencies: + base64url-universal "^2.0.0" + pako "^2.0.4" + +"@digitalbazaar/http-client@^3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@digitalbazaar/http-client/-/http-client-3.4.1.tgz#5116fc44290d647cfe4b615d1f3fad9d6005e44d" + integrity sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g== + dependencies: + ky "^0.33.3" + ky-universal "^0.11.0" + undici "^5.21.2" + "@digitalbazaar/security-context@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@digitalbazaar/security-context/-/security-context-1.0.0.tgz#23624692cfadc6d97e1eb787ad38a19635d89297" integrity sha512-mlj+UmodxTAdMCHGxnGVTRLHcSLyiEOVRiz3J6yiRliJWyrgeXs34wlWjBorDIEMDIjK2JwZrDuFEKO9bS5nKQ== +"@digitalbazaar/vc-status-list-context@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@digitalbazaar/vc-status-list-context/-/vc-status-list-context-3.0.1.tgz#0507b7b6f6ee8b5e7e4d402e7a2905efdc70a316" + integrity sha512-vQsqQXpmSXKNy/C0xxFUOBzz60dHh6oupQam1xRC8IspVC11hYJiX9SAhmbI0ulHvX1R2JfqZaJHZjmAyMZ/aA== + +"@digitalbazaar/vc-status-list@^7.0.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@digitalbazaar/vc-status-list/-/vc-status-list-7.1.0.tgz#1d585a1766106e1586e1e2f87092dd0381b3f036" + integrity sha512-p5uxKJlX13N8TcTuv9qFDeej+6bndU+Rh1Cez2MT+bXQE6Jpn5t336FBSHmcECB4yUfZQpkmV/LOcYU4lW8Ojw== + dependencies: + "@digitalbazaar/bitstring" "^3.0.0" + "@digitalbazaar/vc" "^5.0.0" + "@digitalbazaar/vc-status-list-context" "^3.0.1" + credentials-context "^2.0.0" + +"@digitalbazaar/vc@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@digitalbazaar/vc/-/vc-5.0.0.tgz#20180fb492cb755eb2c6b6df9a17f7407d5e4b5a" + integrity sha512-XmLM7Ag5W+XidGnFuxFIyUFSMnHnWEMJlHei602GG94+WzFJ6Ik8txzPQL8T18egSoiTsd1VekymbIlSimhuaQ== + dependencies: + credentials-context "^2.0.0" + jsonld "^8.0.0" + jsonld-signatures "^11.0.0" + +"@digitalcredentials/base58-universal@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@digitalcredentials/base58-universal/-/base58-universal-1.0.1.tgz#41b5a16cdeaac9cf01b23f1e564c560c2599b607" + integrity sha512-1xKdJnfITMvrF/sCgwBx2C4p7qcNAARyIvrAOZGqIHmBaT/hAenpC8bf44qVY+UIMuCYP23kqpIfJQebQDThDQ== + +"@digitalcredentials/base64url-universal@^2.0.2": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@digitalcredentials/base64url-universal/-/base64url-universal-2.0.6.tgz#43c59c62a33b024e7adc3c56403d18dbcb61ec61" + integrity sha512-QJyK6xS8BYNnkKLhEAgQc6Tb9DMe+GkHnBAWJKITCxVRXJAFLhJnr+FsJnCThS3x2Y0UiiDAXoWjwMqtUrp4Kg== + dependencies: + base64url "^3.0.1" + +"@digitalcredentials/bitstring@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@digitalcredentials/bitstring/-/bitstring-2.0.1.tgz#bb887f1d0999980598754e426d831c96a26a3863" + integrity sha512-9priXvsEJGI4LYHPwLqf5jv9HtQGlG0MgeuY8Q4NHN+xWz5rYMylh1TYTVThKa3XI6xF2pR2oEfKZD21eWXveQ== + dependencies: + "@digitalcredentials/base64url-universal" "^2.0.2" + pako "^2.0.4" + +"@digitalcredentials/ed25519-signature-2020@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@digitalcredentials/ed25519-signature-2020/-/ed25519-signature-2020-3.0.2.tgz#2df8fb6f814a1964b40ebb3402d41630c30120da" + integrity sha512-R8IrR21Dh+75CYriQov3nVHKaOVusbxfk9gyi6eCAwLHKn6fllUt+2LQfuUrL7Ts/sGIJqQcev7YvkX9GvyYRA== + dependencies: + "@digitalcredentials/base58-universal" "^1.0.1" + "@digitalcredentials/ed25519-verification-key-2020" "^3.1.1" + "@digitalcredentials/jsonld-signatures" "^9.3.1" + ed25519-signature-2018-context "^1.1.0" + ed25519-signature-2020-context "^1.0.1" + +"@digitalcredentials/ed25519-verification-key-2020@^3.1.1": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@digitalcredentials/ed25519-verification-key-2020/-/ed25519-verification-key-2020-3.2.2.tgz#cdf271bf4bb44dd2c417dcde6d7a0436e31d84ca" + integrity sha512-ZfxNFZlA379MZpf+gV2tUYyiZ15eGVgjtCQLWlyu3frWxsumUgv++o0OJlMnrDsWGwzFMRrsXcosd5+752rLOA== + dependencies: + "@digitalcredentials/base58-universal" "^1.0.1" + "@stablelib/ed25519" "^1.0.1" + base64url-universal "^1.1.0" + crypto-ld "^6.0.0" + "@digitalcredentials/http-client@^1.0.0": version "1.2.2" resolved "https://registry.yarnpkg.com/@digitalcredentials/http-client/-/http-client-1.2.2.tgz#8b09ab6f1e3aa8878d91d3ca51946ca8265cc92e" @@ -1095,6 +1360,17 @@ isomorphic-webcrypto "^2.3.8" serialize-error "^8.0.1" +"@digitalcredentials/jsonld-signatures@^9.3.2", "@digitalcredentials/jsonld-signatures@^9.4.0": + version "9.4.0" + resolved "https://registry.yarnpkg.com/@digitalcredentials/jsonld-signatures/-/jsonld-signatures-9.4.0.tgz#d5881122c4202449b88a7e2384f8e615ae55582c" + integrity sha512-DnR+HDTm7qpcDd0wcD1w6GdlAwfHjQSgu+ahion8REkCkkMRywF+CLunU7t8AZpFB2Gr/+N8naUtiEBNje1Oew== + dependencies: + "@digitalbazaar/security-context" "^1.0.0" + "@digitalcredentials/jsonld" "^6.0.0" + fast-text-encoding "^1.0.3" + isomorphic-webcrypto "^2.3.8" + serialize-error "^8.0.1" + "@digitalcredentials/jsonld@^5.2.1": version "5.2.1" resolved "https://registry.yarnpkg.com/@digitalcredentials/jsonld/-/jsonld-5.2.1.tgz#60acf587bec8331e86324819fd19692939118775" @@ -1115,6 +1391,11 @@ canonicalize "^1.0.1" lru-cache "^6.0.0" +"@digitalcredentials/open-badges-context@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@digitalcredentials/open-badges-context/-/open-badges-context-2.1.0.tgz#cefd29af4642adf8feeed5bb7ede663b14913c2f" + integrity sha512-VK7X5u6OoBFxkyIFplNqUPVbo+8vFSAEoam8tSozpj05KPfcGw41Tp5p9fqMnY38oPfwtZR2yDNSctj/slrE0A== + "@digitalcredentials/rdf-canonize@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@digitalcredentials/rdf-canonize/-/rdf-canonize-1.0.0.tgz#6297d512072004c2be7f280246383a9c4b0877ff" @@ -1123,15 +1404,39 @@ fast-text-encoding "^1.0.3" isomorphic-webcrypto "^2.3.8" -"@digitalcredentials/vc@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@digitalcredentials/vc/-/vc-1.1.2.tgz#868a56962f5137c29eb51eea1ba60251ebf69ad1" - integrity sha512-TSgny9XUh+W7uFjdcpvZzN7I35F9YMTv6jVINXr7UaLNgrinIjy6A5RMGQH9ecpcaoLMemKB5XjtLOOOQ3vknQ== +"@digitalcredentials/vc-status-list@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@digitalcredentials/vc-status-list/-/vc-status-list-5.0.2.tgz#9de8b23b6d533668a354ff464a689ecc42f24445" + integrity sha512-PI0N7SM0tXpaNLelbCNsMAi34AjOeuhUzMSYTkHdeqRPX7oT2F3ukyOssgr4koEqDxw9shHtxHu3fSJzrzcPMQ== + dependencies: + "@digitalbazaar/vc-status-list-context" "^3.0.1" + "@digitalcredentials/bitstring" "^2.0.1" + "@digitalcredentials/vc" "^4.1.1" + credentials-context "^2.0.0" + +"@digitalcredentials/vc@^4.1.1": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@digitalcredentials/vc/-/vc-4.2.0.tgz#d2197b26547d670965d5969a9e49437f244b5944" + integrity sha512-8Rxpn77JghJN7noBQdcMuzm/tB8vhDwPoFepr3oGd5w+CyJxOk2RnBlgIGlAAGA+mALFWECPv1rANfXno+hdjA== dependencies: "@digitalcredentials/jsonld" "^5.2.1" "@digitalcredentials/jsonld-signatures" "^9.3.1" credentials-context "^2.0.0" +"@digitalcredentials/vc@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@digitalcredentials/vc/-/vc-6.0.1.tgz#e4bdbac37d677c5288f2ad8d9ea59c3b41e0fd78" + integrity sha512-TZgLoi00Jc9uv3b6jStH+G8+bCqpHIqFw9DYODz+fVjNh197ksvcYqSndUDHa2oi0HCcK+soI8j4ba3Sa4Pl4w== + dependencies: + "@digitalbazaar/vc-status-list" "^7.0.0" + "@digitalcredentials/ed25519-signature-2020" "^3.0.2" + "@digitalcredentials/jsonld" "^6.0.0" + "@digitalcredentials/jsonld-signatures" "^9.3.2" + "@digitalcredentials/open-badges-context" "^2.1.0" + "@digitalcredentials/vc-status-list" "^5.0.2" + credentials-context "^2.0.0" + fix-esm "^1.0.1" + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -1164,6 +1469,11 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b" integrity sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng== +"@fastify/busboy@^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff" + integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA== + "@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" @@ -1205,59 +1515,59 @@ resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== -"@hyperledger/anoncreds-nodejs@^0.2.0-dev.4": - version "0.2.0-dev.4" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.0-dev.4.tgz#ac125817beb631dedbe27cb8d4c21d2123104d5e" - integrity sha512-EH/jAH+aATH9KByWF1lk1p76BN6VIsRZhG7jyRT1LAaaUNnmpQnjX6d/Mfkofvk4xFIRbp0cDl/UjaKaKfLsww== +"@hyperledger/anoncreds-nodejs@^0.2.0-dev.5": + version "0.2.0-dev.5" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.0-dev.5.tgz#6464de1220d22b3a6db68286ba9c970f6f441adb" + integrity sha512-8Comk3hx1xqcsbmS3xRtm5XS8XKymAsNM7dQ3UQeirtBkiAl1AzexraTLq/tAer6Cnmo/UpnhbEjbnJCyp8V3g== dependencies: "@2060.io/ffi-napi" "4.0.8" "@2060.io/ref-napi" "3.0.6" - "@hyperledger/anoncreds-shared" "0.2.0-dev.4" + "@hyperledger/anoncreds-shared" "0.2.0-dev.5" "@mapbox/node-pre-gyp" "^1.0.11" ref-array-di "1.2.2" ref-struct-di "1.1.1" -"@hyperledger/anoncreds-shared@0.2.0-dev.4", "@hyperledger/anoncreds-shared@^0.2.0-dev.4": - version "0.2.0-dev.4" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.0-dev.4.tgz#8050647fcb153b594671270d4c51b3b423e575be" - integrity sha512-8hwXc9zab8MgXgwo0OL5bShxMAfDEiBAB1/r+8mbwgANefDZwHwNOkq0yQLwT2KfSsvH9la7N2ehrtUf5E2FKg== +"@hyperledger/anoncreds-shared@0.2.0-dev.5", "@hyperledger/anoncreds-shared@^0.2.0-dev.5": + version "0.2.0-dev.5" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.0-dev.5.tgz#1d6da9db5cc16ba8766fb4db1166dbe5af63e96e" + integrity sha512-YtVju8WBKj3tdZbPWGjwdx7jkE5ePPfspPCvbcjIia00CWPES7UUkfjn8NVk82rq/Gi7IoWR3Jdpfv8rPe0fEA== -"@hyperledger/aries-askar-nodejs@^0.2.0-dev.1": - version "0.2.0-dev.1" - resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-nodejs/-/aries-askar-nodejs-0.2.0-dev.1.tgz#5b0fffe88438108e7ae34ac2f1f59fd31b9f1bc0" - integrity sha512-Ie1lw/4GNI1sGwNZ5ak6yV2dnhRLs7tPf1Q3CLPTsq1NtjPofsAAcwTfwDE2pYIdxCTXalC2ecy3VvXgtpllFA== +"@hyperledger/aries-askar-nodejs@^0.2.0-dev.5": + version "0.2.0-dev.5" + resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-nodejs/-/aries-askar-nodejs-0.2.0-dev.5.tgz#e831648d75ebde8e3f583e531710a21b08252f8d" + integrity sha512-C/17MpOP5jZdIHEAUnkQ0DymiQAPFACiw1tmBFOVhHTF7PZDtSXzzp+orewaKsXcFL5Qc1FoEyves5ougftAbw== dependencies: "@2060.io/ffi-napi" "4.0.8" "@2060.io/ref-napi" "3.0.6" - "@hyperledger/aries-askar-shared" "0.2.0-dev.1" + "@hyperledger/aries-askar-shared" "0.2.0-dev.5" "@mapbox/node-pre-gyp" "^1.0.10" node-cache "^5.1.2" ref-array-di "^1.2.2" ref-struct-di "^1.1.1" -"@hyperledger/aries-askar-shared@0.2.0-dev.1", "@hyperledger/aries-askar-shared@^0.2.0-dev.1": - version "0.2.0-dev.1" - resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-shared/-/aries-askar-shared-0.2.0-dev.1.tgz#08c36f39355cc780cc3198e1cf5fc16d871ece91" - integrity sha512-I92Aflknb2HjDuT6UOcLqJjbZLQ6nP5E2Y4F9wreTIrk+nsN++UTvbuhuIz7PFddWfrT+mFtVG3E4cHaNU10pw== +"@hyperledger/aries-askar-shared@0.2.0-dev.5", "@hyperledger/aries-askar-shared@^0.2.0-dev.5": + version "0.2.0-dev.5" + resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-shared/-/aries-askar-shared-0.2.0-dev.5.tgz#81881eee427ee3f4ae2f56d248f83a6425ea79b8" + integrity sha512-H5yQEWDUL+G4rN85CyJe30dSeW7cSFHnFXaC1g9xkTXCom7eT4XxT8TpY5D/QBr3KWf26KECc/I1roZOTJQQJQ== dependencies: buffer "^6.0.3" -"@hyperledger/indy-vdr-nodejs@^0.2.0-dev.5": - version "0.2.0-dev.5" - resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-nodejs/-/indy-vdr-nodejs-0.2.0-dev.5.tgz#ea40095116e0abdd4c28214122f01669367f1db5" - integrity sha512-TeeapuZBRS7+Tbn8QQ3RoMpGaI/QHjeU7TlDU5UHNoEFuZcBdDcdH6V9QAoJ1RNxc6k7tiUYKFir8LMQ+hXrXQ== +"@hyperledger/indy-vdr-nodejs@^0.2.0-dev.6": + version "0.2.0-dev.6" + resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-nodejs/-/indy-vdr-nodejs-0.2.0-dev.6.tgz#c21916600e17cf6ee46fc78a054cb904f9156594" + integrity sha512-yOmfOqJJJapJRWdKSJQG7q/frKGUrntoae4fiYnwdQEWy4rdRiyZPo0ht9R6uuZ/AQwxtNMMRylvQZBfHA+vKA== dependencies: "@2060.io/ffi-napi" "4.0.8" "@2060.io/ref-napi" "3.0.6" - "@hyperledger/indy-vdr-shared" "0.2.0-dev.5" + "@hyperledger/indy-vdr-shared" "0.2.0-dev.6" "@mapbox/node-pre-gyp" "^1.0.10" ref-array-di "^1.2.2" ref-struct-di "^1.1.1" -"@hyperledger/indy-vdr-shared@0.2.0-dev.5", "@hyperledger/indy-vdr-shared@^0.2.0-dev.5": - version "0.2.0-dev.5" - resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-shared/-/indy-vdr-shared-0.2.0-dev.5.tgz#ab33feda90dcbf457f3ff59da266ab32968ab48c" - integrity sha512-oPvNG5ePvtuz3H+KxWdCdxWXeo3Jxs8AFAAuG8qLPSlicEHpWchbT1amun8ccp1lk7pIBx9J0aLf08yrM5H8iw== +"@hyperledger/indy-vdr-shared@0.2.0-dev.6", "@hyperledger/indy-vdr-shared@^0.2.0-dev.6": + version "0.2.0-dev.6" + resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-shared/-/indy-vdr-shared-0.2.0-dev.6.tgz#4954ee06fa8a2e4545b35cd525b7b86e0f10b6fe" + integrity sha512-pNLq0zkqv5rFCpU9tzyJ5DPvED5YE+UFP8iKwVD7fe+mAD6/VpweOunYNKgIBT4K1DYI21q7bs3SzxQZ0hLlKw== "@isaacs/string-locale-compare@^1.1.0": version "1.1.0" @@ -2572,6 +2882,32 @@ debug "^4.3.4" uint8arrays "^3.1.1" +"@sphereon/pex-models@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.1.2.tgz#e1a0ce16ccc6b32128fc8c2da79d65fc35f6d10f" + integrity sha512-Ec1qZl8tuPd+s6E+ZM7v+HkGkSOjGDMLNN1kqaxAfWpITBYtTLb+d5YvwjvBZ1P2upZ7zwNER97FfW5n/30y2w== + +"@sphereon/pex@^2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.2.2.tgz#3df9ed75281b46f0899256774060ed2ff982fade" + integrity sha512-NkR8iDTC2PSnYsOHlG2M2iOpFTTbzszs2/pL3iK3Dlv9QYLqX7NtPAlmeSwaoVP1NB1ewcs6U1DtemQAD+90yQ== + dependencies: + "@astronautlabs/jsonpath" "^1.1.2" + "@sphereon/pex-models" "^2.1.2" + "@sphereon/ssi-types" "^0.17.5" + ajv "^8.12.0" + ajv-formats "^2.1.1" + jwt-decode "^3.1.2" + nanoid "^3.3.6" + string.prototype.matchall "^4.0.8" + +"@sphereon/ssi-types@^0.17.5": + version "0.17.5" + resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.17.5.tgz#7b4de0326e7c2993ab816caeef6deaea41a5f65f" + integrity sha512-hoQOkeOtshvIzNAG+HTqcKxeGssLVfwX7oILHJgs6VMb1GhR6QlqjMAxflDxZ/8Aq2R0I6fEPWmf73zAXY2X2Q== + dependencies: + jwt-decode "^3.1.2" + "@sphereon/ssi-types@^0.9.0": version "0.9.0" resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.9.0.tgz#d140eb6abd77381926d0da7ac51b3c4b96a31b4b" @@ -2621,7 +2957,7 @@ resolved "https://registry.yarnpkg.com/@stablelib/constant-time/-/constant-time-1.0.1.tgz#bde361465e1cf7b9753061b77e376b0ca4c77e35" integrity sha512-tNOs3uD0vSJcK6z1fvef4Y+buN7DXhzHDPqRLSXUel1UfqMB1PWNsnnAezrKfEwTLpN0cGH2p9NNjs6IqeD0eg== -"@stablelib/ed25519@^1.0.2", "@stablelib/ed25519@^1.0.3": +"@stablelib/ed25519@^1.0.1", "@stablelib/ed25519@^1.0.2", "@stablelib/ed25519@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@stablelib/ed25519/-/ed25519-1.0.3.tgz#f8fdeb6f77114897c887bb6a3138d659d3f35996" integrity sha512-puIMWaX9QlRsbhxfDc5i+mNPMY+0TmQEskunY1rZEBPi1acBCVQAhnsk/1Hk50DGPtVsZtAWQg4NHGlVaO9Hqg== @@ -2929,6 +3265,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/jsonpath@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@types/jsonpath/-/jsonpath-0.2.4.tgz#065be59981c1420832835af656377622271154be" + integrity sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA== + "@types/long@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" @@ -3057,9 +3398,9 @@ "@types/node" "*" "@types/ws@^8.5.4": - version "8.5.5" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" - integrity sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg== + version "8.5.10" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" + integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== dependencies: "@types/node" "*" @@ -3290,6 +3631,13 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -3300,6 +3648,16 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^8.0.0, ajv@^8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + anser@^1.4.9: version "1.4.10" resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b" @@ -3534,6 +3892,19 @@ array.prototype.flatmap@^1.3.1: es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" +arraybuffer.prototype.slice@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz#98bd561953e3e74bb34938e77647179dfe6e9f12" + integrity sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + is-array-buffer "^3.0.2" + is-shared-array-buffer "^1.0.2" + arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -3796,6 +4167,25 @@ base64-js@*, base64-js@^1.1.2, base64-js@^1.3.0, base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +base64url-universal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/base64url-universal/-/base64url-universal-1.1.0.tgz#94da6356c1d43ead55b1d91c045c0a5b09ec8181" + integrity sha512-WyftvZqye29YQ10ZnuiBeEj0lk8SN8xHU9hOznkLc85wS1cLTp6RpzlMrHxMPD9nH7S55gsBqMqgGyz93rqmkA== + dependencies: + base64url "^3.0.0" + +base64url-universal@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64url-universal/-/base64url-universal-2.0.0.tgz#6023785c0e349a90de1cf396e8a4519750a4e67b" + integrity sha512-6Hpg7EBf3t148C3+fMzjf+CHnADVDafWzlJUXAqqqbm4MKNXbsoPdOkWeRTjNlkYG7TpyjIpRO1Gk0SnsFD1rw== + dependencies: + base64url "^3.0.1" + +base64url@^3.0.0, base64url@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -3954,6 +4344,16 @@ browserslist@^4.21.3, browserslist@^4.21.5: node-releases "^2.0.8" update-browserslist-db "^1.0.10" +browserslist@^4.22.2: + version "4.22.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.3.tgz#299d11b7e947a6b843981392721169e27d60c5a6" + integrity sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A== + dependencies: + caniuse-lite "^1.0.30001580" + electron-to-chromium "^1.4.648" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -4120,6 +4520,15 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.4, call-bind@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" + integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== + dependencies: + function-bind "^1.1.2" + get-intrinsic "^1.2.1" + set-function-length "^1.1.1" + caller-callsite@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" @@ -4168,6 +4577,11 @@ caniuse-lite@^1.0.30001449: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz#f58a717afe92f9e69d0e35ff64df596bfad93912" integrity sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ== +caniuse-lite@^1.0.30001580: + version "1.0.30001581" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz#0dfd4db9e94edbdca67d57348ebc070dece279f4" + integrity sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ== + canonicalize@^1.0.1: version "1.0.8" resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.8.tgz#24d1f1a00ed202faafd9bf8e63352cd4450c6df1" @@ -4814,6 +5228,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypto-ld@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/crypto-ld/-/crypto-ld-6.0.0.tgz#cf8dcf566cb3020bdb27f0279e6cc9b46d031cd7" + integrity sha512-XWL1LslqggNoaCI/m3I7HcvaSt9b2tYzdrXO+jHLUj9G1BvRfvV7ZTFDVY5nifYuIGAPdAGu7unPxLRustw3VA== + crypto-random-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" @@ -4842,6 +5261,11 @@ data-uri-to-buffer@^3.0.1: resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + dateformat@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" @@ -4859,7 +5283,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -4906,7 +5330,7 @@ deep-extend@^0.6.0, deep-extend@~0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deep-is@^0.1.3: +deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== @@ -4928,6 +5352,15 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +define-data-property@^1.0.1, define-data-property@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -5119,6 +5552,16 @@ duplexer@^0.1.1: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== +ed25519-signature-2018-context@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ed25519-signature-2018-context/-/ed25519-signature-2018-context-1.1.0.tgz#68002ea7497c32e8170667cfd67468dedf7d220e" + integrity sha512-ppDWYMNwwp9bploq0fS4l048vHIq41nWsAbPq6H4mNVx9G/GxW3fwg4Ln0mqctP13MoEpREK7Biz8TbVVdYXqA== + +ed25519-signature-2020-context@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ed25519-signature-2020-context/-/ed25519-signature-2020-context-1.1.0.tgz#b2f724f07db154ddf0fd6605410d88736e56fd07" + integrity sha512-dBGSmoUIK6h2vadDctrDnhhTO01PR2hJk0mRNEfrRDPCjaIwrfy4J+eziEQ9Q1m8By4f/CSRgKM1h53ydKfdNg== + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -5136,6 +5579,11 @@ electron-to-chromium@^1.4.284: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.371.tgz#393983ef087268a20c926a89be30e9f0bfc803b0" integrity sha512-jlBzY4tFcJaiUjzhRTCWAqRvTO/fWzjA3Bls0mykzGZ7zvcMP7h05W6UcgzfT9Ca1SW2xyKDOFRyI0pQeRNZGw== +electron-to-chromium@^1.4.648: + version "1.4.648" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.648.tgz#c7b46c9010752c37bb4322739d6d2dd82354fbe4" + integrity sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg== + elliptic@^6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" @@ -5270,6 +5718,51 @@ es-abstract@^1.19.0, es-abstract@^1.20.4: unbox-primitive "^1.0.2" which-typed-array "^1.1.9" +es-abstract@^1.22.1: + version "1.22.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.3.tgz#48e79f5573198de6dee3589195727f4f74bc4f32" + integrity sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA== + dependencies: + array-buffer-byte-length "^1.0.0" + arraybuffer.prototype.slice "^1.0.2" + available-typed-arrays "^1.0.5" + call-bind "^1.0.5" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.2" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + internal-slot "^1.0.5" + is-array-buffer "^3.0.2" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.12" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + safe-array-concat "^1.0.1" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.8" + string.prototype.trimend "^1.0.7" + string.prototype.trimstart "^1.0.7" + typed-array-buffer "^1.0.0" + typed-array-byte-length "^1.0.0" + typed-array-byte-offset "^1.0.0" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.13" + es-set-tostringtag@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" @@ -5346,6 +5839,18 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escodegen@^1.8.1: + version "1.14.3" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + eslint-config-prettier@^8.3.0: version "8.8.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" @@ -5492,7 +5997,12 @@ espree@^9.5.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.0" -esprima@^4.0.0, esprima@~4.0.0: +esprima@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.2.2.tgz#76a0fd66fcfe154fd292667dc264019750b1657b" + integrity sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A== + +esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -5511,7 +6021,7 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1: +estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -5771,7 +6281,7 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-sta resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@^2.0.6: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== @@ -5807,6 +6317,14 @@ fetch-blob@^2.1.1: resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-2.1.2.tgz#a7805db1361bd44c1ef62bb57fb5fe8ea173ef3c" integrity sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow== +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + figlet@^1.5.2: version "1.6.0" resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.6.0.tgz#812050fa9f01043b4d44ddeb11f20fb268fa4b93" @@ -5946,6 +6464,15 @@ find-workspaces@^0.1.0: type-fest "^3.2.0" yaml "^2.1.3" +fix-esm@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fix-esm/-/fix-esm-1.0.1.tgz#e0e2199d841e43ff7db9b5f5ba7496bc45130ebb" + integrity sha512-EZtb7wPXZS54GaGxaWxMlhd1DUDCnAg5srlYdu/1ZVeW+7wwR3Tp59nu52dXByFs3MBRq+SByx1wDOJpRvLEXw== + dependencies: + "@babel/core" "^7.14.6" + "@babel/plugin-proposal-export-namespace-from" "^7.14.5" + "@babel/plugin-transform-modules-commonjs" "^7.14.5" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -5975,9 +6502,9 @@ flow-parser@^0.185.0: integrity sha512-2hJ5ACYeJCzNtiVULov6pljKOLygy0zddoqSI1fFetM+XRPpRshFdGEijtqlamA1XwyZ+7rhryI6FQFzvtLWUQ== follow-redirects@^1.14.0, follow-redirects@^1.15.0: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + version "1.15.4" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf" + integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw== for-each@^0.3.3: version "0.3.3" @@ -6000,6 +6527,13 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -6086,6 +6620,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" @@ -6096,6 +6635,16 @@ function.prototype.name@^1.1.5: es-abstract "^1.19.0" functions-have-names "^1.2.2" +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + functions-have-names@^1.2.2, functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" @@ -6177,6 +6726,16 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@ has "^1.0.3" has-symbols "^1.0.3" +get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== + dependencies: + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -6540,6 +7099,13 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + hermes-estree@0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.8.0.tgz#530be27243ca49f008381c1f3e8b18fb26bf9ec0" @@ -7183,6 +7749,13 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.9: gopd "^1.0.1" has-tostringtag "^1.0.0" +is-typed-array@^1.1.12: + version "1.1.12" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" + integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== + dependencies: + which-typed-array "^1.1.11" + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" @@ -7217,6 +7790,11 @@ isarray@1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -7908,6 +8486,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -7963,11 +8546,39 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonld-signatures@^11.0.0: + version "11.2.1" + resolved "https://registry.yarnpkg.com/jsonld-signatures/-/jsonld-signatures-11.2.1.tgz#e2ff23ac7476fcdb92e5fecd9a1734ceaf904bb0" + integrity sha512-RNaHTEeRrX0jWeidPCwxMq/E/Ze94zFyEZz/v267ObbCHQlXhPO7GtkY6N5PSHQfQhZPXa8NlMBg5LiDF4dNbA== + dependencies: + "@digitalbazaar/security-context" "^1.0.0" + jsonld "^8.0.0" + serialize-error "^8.1.0" + +jsonld@^8.0.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-8.3.2.tgz#7033f8994aed346b536e9046025f7f1fe9669934" + integrity sha512-MwBbq95szLwt8eVQ1Bcfwmgju/Y5P2GdtlHE2ncyfuYjIdEhluUVyj1eudacf1mOkWIoS9GpDBTECqhmq7EOaA== + dependencies: + "@digitalbazaar/http-client" "^3.4.1" + canonicalize "^1.0.1" + lru-cache "^6.0.0" + rdf-canonize "^3.4.0" + jsonparse@^1.2.0, jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== +jsonpath@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/jsonpath/-/jsonpath-1.1.1.tgz#0ca1ed8fb65bb3309248cc9d5466d12d5b0b9901" + integrity sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w== + dependencies: + esprima "1.2.2" + static-eval "2.0.2" + underscore "1.12.1" + just-diff-apply@^5.2.0: version "5.5.0" resolved "https://registry.yarnpkg.com/just-diff-apply/-/just-diff-apply-5.5.0.tgz#771c2ca9fa69f3d2b54e7c3f5c1dfcbcc47f9f0f" @@ -8019,6 +8630,14 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +ky-universal@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/ky-universal/-/ky-universal-0.11.0.tgz#f5edf857865aaaea416a1968222148ad7d9e4017" + integrity sha512-65KyweaWvk+uKKkCrfAf+xqN2/epw1IJDtlyCPxYffFCMR8u1sp2U65NtWpnozYfZxQ6IUzIlvUcw+hQ82U2Xw== + dependencies: + abort-controller "^3.0.0" + node-fetch "^3.2.10" + ky-universal@^0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/ky-universal/-/ky-universal-0.8.2.tgz#edc398d54cf495d7d6830aa1ab69559a3cc7f824" @@ -8032,6 +8651,11 @@ ky@^0.25.1: resolved "https://registry.yarnpkg.com/ky/-/ky-0.25.1.tgz#0df0bd872a9cc57e31acd5dbc1443547c881bfbc" integrity sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA== +ky@^0.33.3: + version "0.33.3" + resolved "https://registry.yarnpkg.com/ky/-/ky-0.33.3.tgz#bf1ad322a3f2c3428c13cfa4b3af95e6c4a2f543" + integrity sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw== + lerna@^6.5.1: version "6.6.1" resolved "https://registry.yarnpkg.com/lerna/-/lerna-6.6.1.tgz#4897171aed64e244a2d0f9000eef5c5b228f9332" @@ -8127,6 +8751,14 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + libnpmaccess@6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-6.0.3.tgz#473cc3e4aadb2bc713419d92e45d23b070d8cded" @@ -9110,6 +9742,11 @@ nan@^2.11.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== +nanoid@^3.3.6: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -9240,6 +9877,11 @@ node-dir@^0.1.17: dependencies: minimatch "^3.0.2" +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-fetch@2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -9269,6 +9911,15 @@ node-fetch@^2.6.12: dependencies: whatwg-url "^5.0.0" +node-fetch@^3.2.10: + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + node-gyp-build@^4.2.1, node-gyp-build@^4.3.0: version "4.6.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" @@ -9327,6 +9978,11 @@ node-pre-gyp@0.17.0: semver "^5.7.1" tar "^4.4.13" +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + node-releases@^2.0.8: version "2.0.10" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" @@ -9724,6 +10380,11 @@ object-inspect@^1.10.3, object-inspect@^1.12.3, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -9811,6 +10472,18 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -10015,6 +10688,11 @@ pacote@^15.0.0, pacote@^15.0.8: ssri "^10.0.0" tar "^6.1.11" +pako@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -10195,6 +10873,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + prettier-linter-helpers@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" @@ -10470,6 +11153,13 @@ rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +rdf-canonize@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/rdf-canonize/-/rdf-canonize-3.4.0.tgz#87f88342b173cc371d812a07de350f0c1aa9f058" + integrity sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA== + dependencies: + setimmediate "^1.0.5" + react-devtools-core@^4.26.1: version "4.28.5" resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.28.5.tgz#c8442b91f068cdf0c899c543907f7f27d79c2508" @@ -10801,6 +11491,15 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.2.0" functions-have-names "^1.2.3" +regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" + integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + set-function-name "^2.0.0" + regexpu-core@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" @@ -10835,6 +11534,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -10968,6 +11672,16 @@ rxjs@^7.2.0, rxjs@^7.5.5, rxjs@^7.8.0: dependencies: tslib "^2.1.0" +safe-array-concat@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" + integrity sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + isarray "^2.0.5" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -11037,7 +11751,7 @@ semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^ dependencies: lru-cache "^6.0.0" -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -11066,7 +11780,7 @@ serialize-error@^2.1.0: resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a" integrity sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw== -serialize-error@^8.0.1: +serialize-error@^8.0.1, serialize-error@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67" integrity sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ== @@ -11088,6 +11802,25 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-function-length@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed" + integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ== + dependencies: + define-data-property "^1.1.1" + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + +set-function-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" + integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== + dependencies: + define-data-property "^1.0.1" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.0" + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -11098,6 +11831,11 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -11398,6 +12136,13 @@ stacktrace-parser@^0.1.3: dependencies: type-fest "^0.7.1" +static-eval@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.0.2.tgz#2d1759306b1befa688938454c546b7871f806a42" + integrity sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg== + dependencies: + escodegen "^1.8.1" + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -11457,6 +12202,21 @@ string-width@^1.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string.prototype.matchall@^4.0.8: + version "4.0.10" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" + integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + regexp.prototype.flags "^1.5.0" + set-function-name "^2.0.0" + side-channel "^1.0.4" + string.prototype.trim@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" @@ -11466,6 +12226,15 @@ string.prototype.trim@^1.2.7: define-properties "^1.1.4" es-abstract "^1.20.4" +string.prototype.trim@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz#f9ac6f8af4bd55ddfa8895e6aea92a96395393bd" + integrity sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + string.prototype.trimend@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" @@ -11475,6 +12244,15 @@ string.prototype.trimend@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" +string.prototype.trimend@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz#1bb3afc5008661d73e2dc015cd4853732d6c471e" + integrity sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + string.prototype.trimstart@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" @@ -11484,6 +12262,15 @@ string.prototype.trimstart@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" +string.prototype.trimstart@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz#d4cdb44b83a4737ffbac2d406e405d43d0184298" + integrity sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -11957,6 +12744,13 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== + dependencies: + prelude-ls "~1.1.2" + type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -12025,6 +12819,36 @@ type@^2.7.2: resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== +typed-array-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" + integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-typed-array "^1.1.10" + +typed-array-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" + integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" + integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + typed-array-length@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" @@ -12084,11 +12908,23 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +underscore@1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" + integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici@^5.21.2: + version "5.28.2" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.2.tgz#fea200eac65fc7ecaff80a023d1a0543423b4c91" + integrity sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w== + dependencies: + "@fastify/busboy" "^2.0.0" + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -12212,6 +13048,14 @@ update-browserslist-db@^1.0.10: escalade "^3.1.1" picocolors "^1.0.0" +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -12354,6 +13198,11 @@ web-did-resolver@^2.0.21: cross-fetch "^4.0.0" did-resolver "^4.0.0" +web-streams-polyfill@^3.0.3: + version "3.3.2" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz#32e26522e05128203a7de59519be3c648004343b" + integrity sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ== + webcrypto-core@^1.7.7: version "1.7.7" resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.7.7.tgz#06f24b3498463e570fed64d7cab149e5437b162c" @@ -12404,6 +13253,17 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== +which-typed-array@^1.1.11, which-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.13.tgz#870cd5be06ddb616f504e7b039c4c24898184d36" + integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.4" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + which-typed-array@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" @@ -12449,6 +13309,11 @@ word-wrap@^1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== +word-wrap@~1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" @@ -12552,9 +13417,9 @@ ws@^7, ws@^7.5.1: integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== ws@^8.13.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== xstream@^11.14.0: version "11.14.0" From dfbb1b92bb4e5164072fb9656fa88738aca5f94f Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 29 Jan 2024 17:13:52 -0300 Subject: [PATCH 10/15] fix: merge issues Signed-off-by: Ariel Gentile --- packages/core/src/agent/MessageSender.ts | 2 +- packages/core/src/agent/__tests__/MessageSender.test.ts | 4 ++-- .../routing/services/__tests__/MediatorService.test.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/agent/MessageSender.ts b/packages/core/src/agent/MessageSender.ts index f89934fbd4..6ecb98a424 100644 --- a/packages/core/src/agent/MessageSender.ts +++ b/packages/core/src/agent/MessageSender.ts @@ -18,7 +18,7 @@ import { DidCommDocumentService } from '../modules/didcomm' import { getKeyFromVerificationMethod } from '../modules/dids/domain/key-type' import { didKeyToInstanceOfKey, verkeyToDidKey } from '../modules/dids/helpers' import { DidResolverService } from '../modules/dids/services/DidResolverService' -import { MessagePickupRepository } from '../modules/message-pìckup/storage' +import { MessagePickupRepository } from '../modules/message-pickup/storage' import { inject, injectable } from '../plugins' import { MessageValidator } from '../utils/MessageValidator' import { getProtocolScheme } from '../utils/uri' diff --git a/packages/core/src/agent/__tests__/MessageSender.test.ts b/packages/core/src/agent/__tests__/MessageSender.test.ts index 04ff6450a2..2c596a37e6 100644 --- a/packages/core/src/agent/__tests__/MessageSender.test.ts +++ b/packages/core/src/agent/__tests__/MessageSender.test.ts @@ -2,7 +2,7 @@ import type { ConnectionRecord } from '../../modules/connections' import type { ResolvedDidCommService } from '../../modules/didcomm' import type { DidDocumentService } from '../../modules/dids' -import type { MessagePickupRepository } from '../../modules/message-pìckup/storage' +import type { MessagePickupRepository } from '../../modules/message-pickup/storage' import type { OutboundTransport } from '../../transport' import type { EncryptedMessage } from '../../types' import type { AgentMessageSentEvent } from '../Events' @@ -24,7 +24,7 @@ import { DidCommDocumentService } from '../../modules/didcomm' import { DidResolverService, DidDocument, VerificationMethod } from '../../modules/dids' import { DidCommV1Service } from '../../modules/dids/domain/service/DidCommV1Service' import { verkeyToInstanceOfKey } from '../../modules/dids/helpers' -import { InMemoryMessagePickupRepository } from '../../modules/message-pìckup/storage' +import { InMemoryMessagePickupRepository } from '../../modules/message-pickup/storage' import { EnvelopeService as EnvelopeServiceImpl } from '../EnvelopeService' import { EventEmitter } from '../EventEmitter' import { AgentEventTypes } from '../Events' diff --git a/packages/core/src/modules/routing/services/__tests__/MediatorService.test.ts b/packages/core/src/modules/routing/services/__tests__/MediatorService.test.ts index baff063ec2..a2741fc29a 100644 --- a/packages/core/src/modules/routing/services/__tests__/MediatorService.test.ts +++ b/packages/core/src/modules/routing/services/__tests__/MediatorService.test.ts @@ -5,7 +5,7 @@ import { EventEmitter } from '../../../../agent/EventEmitter' import { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' import { ConnectionService, DidExchangeState } from '../../../connections' import { isDidKey } from '../../../dids/helpers' -import { MessagePickupApi } from '../../../message-pìckup' +import { MessagePickupApi } from '../../../message-pickup' import { KeylistUpdateAction, KeylistUpdateMessage, KeylistUpdateResult } from '../../messages' import { MediationRole, MediationState } from '../../models' import { MediationRecord, MediatorRoutingRecord } from '../../repository' From 1816dbb823e9a14cf02b516d3a8e50826a90c1d0 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 29 Jan 2024 17:52:19 -0300 Subject: [PATCH 11/15] fix: default to `DeliverOnly` as it is current behaviour Signed-off-by: Ariel Gentile --- .../core/src/modules/message-pickup/__tests__/pickup.test.ts | 3 --- packages/core/src/modules/routing/MediatorModuleConfig.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts b/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts index 6285f2828d..fad3e3d2d7 100644 --- a/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts +++ b/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts @@ -143,7 +143,6 @@ describe('E2E Pick Up protocol', () => { const basicMessagePromise = waitForBasicMessage(recipientAgent, { content: message, }) - const trustPingPromise = waitForTrustPingReceivedEvent(mediatorAgent, {}) await recipientAgent.messagePickup.pickupMessages({ connectionId: recipientMediatorConnection.id, protocolVersion: 'v2', @@ -152,8 +151,6 @@ describe('E2E Pick Up protocol', () => { const basicMessage = await basicMessagePromise expect(basicMessage.content).toBe(message) - // Wait for trust ping to be received and stop message pickup - await trustPingPromise await recipientAgent.mediationRecipient.stopMessagePickup() }) }) diff --git a/packages/core/src/modules/routing/MediatorModuleConfig.ts b/packages/core/src/modules/routing/MediatorModuleConfig.ts index 0f72359782..d0b576dd4a 100644 --- a/packages/core/src/modules/routing/MediatorModuleConfig.ts +++ b/packages/core/src/modules/routing/MediatorModuleConfig.ts @@ -39,6 +39,6 @@ export class MediatorModuleConfig { /** See {@link MediatorModuleConfigOptions.messageForwardingStrategy} */ public get messageForwardingStrategy() { - return this.options.messageForwardingStrategy ?? MessageForwardingStrategy.QueueAndDeliver + return this.options.messageForwardingStrategy ?? MessageForwardingStrategy.DeliverOnly } } From b6f952d478533ec295171953cc322cbe61984eb3 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 30 Jan 2024 19:24:28 -0300 Subject: [PATCH 12/15] feat: add flag to avoid message-pickup messages to be queued Signed-off-by: Ariel Gentile --- packages/core/src/agent/AgentMessage.ts | 9 +++++++++ packages/core/src/agent/MessageSender.ts | 2 +- .../src/modules/message-pickup/__tests__/pickup.test.ts | 8 ++++---- .../protocol/v1/messages/V1BatchMessage.ts | 2 ++ .../protocol/v1/messages/V1BatchPickupMessage.ts | 2 ++ .../protocol/v2/messages/V2DeliveryRequestMessage.ts | 2 ++ .../protocol/v2/messages/V2LiveDeliveryChangeMessage.ts | 2 ++ .../protocol/v2/messages/V2MessageDeliveryMessage.ts | 2 ++ .../protocol/v2/messages/V2MessagesReceivedMessage.ts | 2 ++ .../protocol/v2/messages/V2StatusMessage.ts | 2 ++ .../protocol/v2/messages/V2StatusRequestMessage.ts | 2 ++ 11 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/core/src/agent/AgentMessage.ts b/packages/core/src/agent/AgentMessage.ts index 320be216c4..fdca23daa9 100644 --- a/packages/core/src/agent/AgentMessage.ts +++ b/packages/core/src/agent/AgentMessage.ts @@ -32,6 +32,15 @@ export class AgentMessage extends Decorated { @Exclude() public readonly allowDidSovPrefix: boolean = false + /** + * Whether to use Queue Transport in case the recipient of this message does not have a reliable + * endpoint available + * + * @see https://github.com/decentralized-identity/didcomm-messaging/blob/main/extensions/return_route/main.md#queue-transport + */ + @Exclude() + public readonly allowQueueTransport: boolean = true + public toJSON({ useDidSovPrefixWhereAllowed }: { useDidSovPrefixWhereAllowed?: boolean } = {}): PlaintextMessage { const json = JsonTransformer.toJSON(this) diff --git a/packages/core/src/agent/MessageSender.ts b/packages/core/src/agent/MessageSender.ts index 57f994db2f..a09ed505a8 100644 --- a/packages/core/src/agent/MessageSender.ts +++ b/packages/core/src/agent/MessageSender.ts @@ -323,7 +323,7 @@ export class MessageSender { // We didn't succeed to send the message over open session, or directly to serviceEndpoint // If the other party shared a queue service endpoint in their did doc we queue the message - if (queueService) { + if (queueService && message.allowQueueTransport) { this.logger.debug(`Queue message for connection ${connection.id} (${connection.theirLabel})`) const keys = { diff --git a/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts b/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts index fad3e3d2d7..f2d21c03f7 100644 --- a/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts +++ b/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts @@ -6,13 +6,13 @@ import { Subject } from 'rxjs' import { SubjectInboundTransport } from '../../../../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' -import { getAgentOptions, waitForBasicMessage, waitForTrustPingReceivedEvent } from '../../../../tests/helpers' +import { getAgentOptions, waitForBasicMessage } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { HandshakeProtocol } from '../../connections' const recipientOptions = getAgentOptions('Mediation: Recipient Pickup', {}, getIndySdkModules()) const mediatorOptions = getAgentOptions( - 'Mediation: Mediator Pickup', + 'Mediation: Message Pickup', { endpoints: ['wss://mediator'], }, @@ -30,7 +30,7 @@ describe('E2E Pick Up protocol', () => { await mediatorAgent.wallet.delete() }) - test('E2E Pick Up V1 protocol', async () => { + test('E2E manual Pick Up V1 loop', async () => { const mediatorMessages = new Subject() const subjectMap = { @@ -92,7 +92,7 @@ describe('E2E Pick Up protocol', () => { expect(basicMessage.content).toBe(message) }) - test('E2E Pick Up V2 protocol', async () => { + test('E2E manual Pick Up V2 loop', async () => { const mediatorMessages = new Subject() // FIXME: we harcoded that pickup of messages MUST be using ws(s) scheme when doing implicit pickup diff --git a/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchMessage.ts b/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchMessage.ts index 91e0b5debc..bf01bea731 100644 --- a/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchMessage.ts +++ b/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchMessage.ts @@ -33,6 +33,8 @@ export interface BatchMessageOptions { * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0212-pickup/README.md#batch */ export class V1BatchMessage extends AgentMessage { + public readonly allowQueueTransport = false + public constructor(options: BatchMessageOptions) { super() diff --git a/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchPickupMessage.ts b/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchPickupMessage.ts index aa5e7ff646..950c700b3d 100644 --- a/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchPickupMessage.ts +++ b/packages/core/src/modules/message-pickup/protocol/v1/messages/V1BatchPickupMessage.ts @@ -15,6 +15,8 @@ export interface BatchPickupMessageOptions { * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0212-pickup/README.md#batch-pickup */ export class V1BatchPickupMessage extends AgentMessage { + public readonly allowQueueTransport = false + /** * Create new BatchPickupMessage instance. * diff --git a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2DeliveryRequestMessage.ts b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2DeliveryRequestMessage.ts index b7c37bf426..2a1e73f867 100644 --- a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2DeliveryRequestMessage.ts +++ b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2DeliveryRequestMessage.ts @@ -12,6 +12,8 @@ export interface V2DeliveryRequestMessageOptions { } export class V2DeliveryRequestMessage extends AgentMessage { + public readonly allowQueueTransport = false + public constructor(options: V2DeliveryRequestMessageOptions) { super() diff --git a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts index 4d38fc2409..3b14501f6b 100644 --- a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts +++ b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2LiveDeliveryChangeMessage.ts @@ -11,6 +11,8 @@ export interface V2LiveDeliveryChangeMessageOptions { } export class V2LiveDeliveryChangeMessage extends AgentMessage { + public readonly allowQueueTransport = false + public constructor(options: V2LiveDeliveryChangeMessageOptions) { super() diff --git a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessageDeliveryMessage.ts b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessageDeliveryMessage.ts index 073efa1819..4523c5d54b 100644 --- a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessageDeliveryMessage.ts +++ b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessageDeliveryMessage.ts @@ -15,6 +15,8 @@ export interface V2MessageDeliveryMessageOptions { } export class V2MessageDeliveryMessage extends AgentMessage { + public readonly allowQueueTransport = false + public constructor(options: V2MessageDeliveryMessageOptions) { super() diff --git a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessagesReceivedMessage.ts b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessagesReceivedMessage.ts index f6e9d49d41..889e08853c 100644 --- a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessagesReceivedMessage.ts +++ b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2MessagesReceivedMessage.ts @@ -11,6 +11,8 @@ export interface V2MessagesReceivedMessageOptions { } export class V2MessagesReceivedMessage extends AgentMessage { + public readonly allowQueueTransport = false + public constructor(options: V2MessagesReceivedMessageOptions) { super() diff --git a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusMessage.ts b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusMessage.ts index a28296742e..46d3a8c226 100644 --- a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusMessage.ts +++ b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusMessage.ts @@ -19,6 +19,8 @@ export interface V2StatusMessageOptions { } export class V2StatusMessage extends AgentMessage { + public readonly allowQueueTransport = false + public constructor(options: V2StatusMessageOptions) { super() if (options) { diff --git a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusRequestMessage.ts b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusRequestMessage.ts index eb6908bae2..c10acf8b75 100644 --- a/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusRequestMessage.ts +++ b/packages/core/src/modules/message-pickup/protocol/v2/messages/V2StatusRequestMessage.ts @@ -10,6 +10,8 @@ export interface V2StatusRequestMessageOptions { } export class V2StatusRequestMessage extends AgentMessage { + public readonly allowQueueTransport = false + public constructor(options: V2StatusRequestMessageOptions) { super() From f39fe5c567b4a69b309e93f1ece49b165206bb75 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Wed, 31 Jan 2024 10:00:56 -0300 Subject: [PATCH 13/15] refactor: some setting naming and params to be more clear Signed-off-by: Ariel Gentile --- .../message-pickup/MessagePickupApi.ts | 8 +- .../message-pickup/__tests__/pickup.test.ts | 41 +++++++++- .../protocol/v1/V1MessagePickupProtocol.ts | 6 +- .../protocol/v2/V2MessagePickupProtocol.ts | 2 - .../__tests__/V2MessagePickupProtocol.test.ts | 4 - .../InMemoryMessagePickupRepository.ts | 8 +- .../storage/MessagePickupRepositoryOptions.ts | 2 +- .../routing/MediationRecipientModuleConfig.ts | 16 ++-- .../modules/routing/MediatorModuleConfig.ts | 9 ++- .../routing/MessageForwardingStrategy.ts | 12 +-- .../routing/services/MediatorService.ts | 4 +- tests/e2e-ws-pickup-v2.test.ts | 75 +++++++++++++++---- 12 files changed, 135 insertions(+), 52 deletions(-) diff --git a/packages/core/src/modules/message-pickup/MessagePickupApi.ts b/packages/core/src/modules/message-pickup/MessagePickupApi.ts index b2e1950090..0169897423 100644 --- a/packages/core/src/modules/message-pickup/MessagePickupApi.ts +++ b/packages/core/src/modules/message-pickup/MessagePickupApi.ts @@ -8,7 +8,7 @@ import type { SetLiveDeliveryModeOptions, SetLiveDeliveryModeReturnType, } from './MessagePickupApiOptions' -import type { MessagePickupSessionRole } from './MessagePickupSession' +import type { MessagePickupSession, MessagePickupSessionRole } from './MessagePickupSession' import type { V1MessagePickupProtocol, V2MessagePickupProtocol } from './protocol' import type { MessagePickupProtocol } from './protocol/MessagePickupProtocol' import type { MessagePickupRepository } from './storage/MessagePickupRepository' @@ -28,6 +28,12 @@ import { MessagePickupSessionService } from './services/MessagePickupSessionServ export interface MessagePickupApi { queueMessage(options: QueueMessageOptions): Promise pickupMessages(options: PickupMessagesOptions): Promise + getLiveModeSession(options: { + connectionId: string + role?: MessagePickupSessionRole + }): Promise + deliverMessages(options: DeliverMessagesOptions): Promise + deliverMessagesFromQueue(options: DeliverMessagesFromQueueOptions): Promise } @injectable() diff --git a/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts b/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts index f2d21c03f7..cbdfd62be1 100644 --- a/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts +++ b/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts @@ -6,17 +6,26 @@ import { Subject } from 'rxjs' import { SubjectInboundTransport } from '../../../../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' -import { getAgentOptions, waitForBasicMessage } from '../../../../tests/helpers' +import { getAgentOptions, waitForAgentMessageProcessedEvent, waitForBasicMessage } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { HandshakeProtocol } from '../../connections' +import { MediatorModule } from '../../routing' +import { MessageForwardingStrategy } from '../../routing/MessageForwardingStrategy' +import { V2MessagesReceivedMessage, V2StatusMessage } from '../protocol' -const recipientOptions = getAgentOptions('Mediation: Recipient Pickup', {}, getIndySdkModules()) +const recipientOptions = getAgentOptions('Mediation Pickup Loop Recipient', {}, getIndySdkModules()) const mediatorOptions = getAgentOptions( - 'Mediation: Message Pickup', + 'Mediation Pickup Loop Mediator', { endpoints: ['wss://mediator'], }, - getIndySdkModules() + { + ...getIndySdkModules(), + mediator: new MediatorModule({ + autoAcceptMediationRequests: true, + messageForwardingStrategy: MessageForwardingStrategy.QueueAndLiveModeDelivery, + }), + } ) describe('E2E Pick Up protocol', () => { @@ -136,6 +145,13 @@ describe('E2E Pick Up protocol', () => { mediatorRecipientConnection = await mediatorAgent.connections.returnWhenIsConnected(mediatorRecipientConnection!.id) + // Now they are connected, reinitialize recipient agent in order to lose the session (as with SubjectTransport it remains open) + await recipientAgent.shutdown() + + recipientAgent = new Agent(recipientOptions) + recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await recipientAgent.initialize() + const message = 'hello pickup V2' await mediatorAgent.basicMessages.sendMessage(mediatorRecipientConnection.id, message) @@ -147,10 +163,27 @@ describe('E2E Pick Up protocol', () => { connectionId: recipientMediatorConnection.id, protocolVersion: 'v2', }) + const firstStatusMessage = await waitForAgentMessageProcessedEvent(recipientAgent, { + messageType: V2StatusMessage.type.messageTypeUri, + }) + + expect((firstStatusMessage as V2StatusMessage).messageCount).toBe(1) const basicMessage = await basicMessagePromise expect(basicMessage.content).toBe(message) + const messagesReceived = await waitForAgentMessageProcessedEvent(mediatorAgent, { + messageType: V2MessagesReceivedMessage.type.messageTypeUri, + }) + + expect((messagesReceived as V2MessagesReceivedMessage).messageIdList.length).toBe(1) + + const secondStatusMessage = await waitForAgentMessageProcessedEvent(recipientAgent, { + messageType: V2StatusMessage.type.messageTypeUri, + }) + + expect((secondStatusMessage as V2StatusMessage).messageCount).toBe(0) + await recipientAgent.mediationRecipient.stopMessagePickup() }) }) diff --git a/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts index 0a9b5711ba..48f86e393c 100644 --- a/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts +++ b/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts @@ -111,7 +111,11 @@ export class V1MessagePickupProtocol extends BaseMessagePickupProtocol { InjectionSymbols.MessagePickupRepository ) - const messages = await pickupMessageQueue.takeFromQueue({ connectionId: connection.id, limit: message.batchSize }) + const messages = await pickupMessageQueue.takeFromQueue({ + connectionId: connection.id, + limit: message.batchSize, + deleteMessages: true, + }) const batchMessages = messages.map( (msg) => diff --git a/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts index df2fcfd766..96e9722c31 100644 --- a/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts +++ b/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts @@ -106,7 +106,6 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { connectionId: connectionRecord.id, recipientKey, limit: 10, // TODO: Define as config parameter - keepMessages: true, })) if (messagesToDeliver.length === 0) { @@ -183,7 +182,6 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { connectionId: connection.id, recipientKey: recipientKey ? verkeyToDidKey(recipientKey) : undefined, limit: message.limit, - keepMessages: true, }) const attachments = messages.map( diff --git a/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts b/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts index ec8fd4b62d..7707b56db7 100644 --- a/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts +++ b/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts @@ -153,7 +153,6 @@ describe('V2MessagePickupProtocol', () => { expect(messagePickupRepository.takeFromQueue).toHaveBeenCalledWith({ connectionId: mockConnection.id, limit: 10, - keepMessages: true, }) }) @@ -185,7 +184,6 @@ describe('V2MessagePickupProtocol', () => { expect(messagePickupRepository.takeFromQueue).toHaveBeenCalledWith({ connectionId: mockConnection.id, limit: 10, - keepMessages: true, }) }) @@ -217,7 +215,6 @@ describe('V2MessagePickupProtocol', () => { expect(messagePickupRepository.takeFromQueue).toHaveBeenCalledWith({ connectionId: mockConnection.id, limit: 2, - keepMessages: true, }) }) @@ -237,7 +234,6 @@ describe('V2MessagePickupProtocol', () => { connectionId: mockConnection.id, limit: 10, recipientKey: verkeyToDidKey('recipientKey'), - keepMessages: true, }) }) }) diff --git a/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts b/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts index 84a3557f85..65f5e7da46 100644 --- a/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts +++ b/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts @@ -33,13 +33,15 @@ export class InMemoryMessagePickupRepository implements MessagePickupRepository const messages = this.messages.filter( (msg) => - msg.connectionId === connectionId && (recipientKey === undefined || msg.recipientKeys.includes(recipientKey)) + msg.connectionId === connectionId && + (recipientKey === undefined || msg.recipientKeys.includes(recipientKey)) && + msg.state === 'pending' ) return messages.length } public takeFromQueue(options: TakeFromQueueOptions): QueuedMessage[] { - const { connectionId, recipientKey, limit, keepMessages } = options + const { connectionId, recipientKey, limit, deleteMessages } = options let messages = this.messages.filter( (msg) => @@ -60,7 +62,7 @@ export class InMemoryMessagePickupRepository implements MessagePickupRepository if (index !== -1) this.messages[index].state = 'sending' }) - if (!keepMessages) { + if (deleteMessages) { this.removeMessages({ connectionId, messageIds: messages.map((msg) => msg.id) }) } diff --git a/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts b/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts index e580949c32..85b26e0c91 100644 --- a/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts +++ b/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts @@ -9,7 +9,7 @@ export interface TakeFromQueueOptions { connectionId: string recipientKey?: string limit?: number - keepMessages?: boolean + deleteMessages?: boolean } export interface AddMessageOptions { diff --git a/packages/core/src/modules/routing/MediationRecipientModuleConfig.ts b/packages/core/src/modules/routing/MediationRecipientModuleConfig.ts index 6f94234fc5..cab467c18e 100644 --- a/packages/core/src/modules/routing/MediationRecipientModuleConfig.ts +++ b/packages/core/src/modules/routing/MediationRecipientModuleConfig.ts @@ -10,28 +10,28 @@ export interface MediationRecipientModuleConfigOptions { * features protocol to determine the best strategy. * * - * - `MediatorPickupStrategy.PickUpV1` - explicitly pick up messages from the mediator according to [RFC 0212 Pickup Protocol](https://github.com/hyperledger/aries-rfcs/blob/main/features/0212-pickup/README.md) - * - `MediatorPickupStrategy.PickUpV2` - pick up messages from the mediator according to [RFC 0685 Pickup V2 Protocol](https://github.com/hyperledger/aries-rfcs/tree/main/features/0685-pickup-v2/README.md). - * - `MediatorPickupStrategy.Implicit` - Open a WebSocket with the mediator to implicitly receive messages. (currently used by Aries Cloud Agent Python) - * - `MediatorPickupStrategy.None` - Do not retrieve messages from the mediator. + * - `MediatorPickupStrategy.PickUpV1` - explicitly pick up messages from the mediator in periodic loops according to [RFC 0212 Pickup Protocol](https://github.com/hyperledger/aries-rfcs/blob/main/features/0212-pickup/README.md) + * - `MediatorPickupStrategy.PickUpV2` - pick up messages from the mediator in periodic loops according to [RFC 0685 Pickup V2 Protocol](https://github.com/hyperledger/aries-rfcs/tree/main/features/0685-pickup-v2/README.md). + * - `MediatorPickupStrategy.PickUpV2LiveMode` - pick up messages from the mediator using Live Mode as specified in [RFC 0685 Pickup V2 Protocol](https://github.com/hyperledger/aries-rfcs/tree/main/features/0685-pickup-v2/README.md). + * - `MediatorPickupStrategy.Implicit` - Open a WebSocket with the mediator to implicitly receive messages. (currently used by Aries Cloud Agent Python) + * - `MediatorPickupStrategy.None` - Do not retrieve messages from the mediator automatically. You can launch manual pickup flows afterwards. * * @default undefined */ mediatorPickupStrategy?: MediatorPickupStrategy /** - * Interval in milliseconds between picking up message from the mediator. This is only applicable when the pickup protocol v1 - * is used. + * Interval in milliseconds between picking up message from the mediator. This is only applicable when the pickup protocol v1 or v2 in polling mode + * are used. * * @default 5000 */ mediatorPollingInterval?: number /** - * Maximum number of messages to retrieve from the mediator in a single batch. This is only applicable when the pickup protocol v2 + * Maximum number of messages to retrieve from the mediator in a single batch. This is applicable for both pickup protocol v1 and v2 * is used. * - * @todo integrate with pickup protocol v1 * @default 10 */ maximumMessagePickup?: number diff --git a/packages/core/src/modules/routing/MediatorModuleConfig.ts b/packages/core/src/modules/routing/MediatorModuleConfig.ts index d0b576dd4a..e20fc8422c 100644 --- a/packages/core/src/modules/routing/MediatorModuleConfig.ts +++ b/packages/core/src/modules/routing/MediatorModuleConfig.ts @@ -17,10 +17,11 @@ export interface MediatorModuleConfigOptions { * * * - `MessageForwardingStrategy.QueueOnly` - simply queue encrypted message into MessagePickupRepository. It will be in charge of manually trigering MessagePickupApi.deliver() afterwards. - * - `MessageForwardingStrategy.QueueAndDeliver` - Queue message into MessagePickupRepository and deliver it (along any other queued message). - * - `MessageForwardingStrategy.DeliverOnly` - Deliver message directly. Do not add into queue (it might be manually added after, e.g. in case of failure) + * - `MessageForwardingStrategy.QueueAndLiveModeDelivery` - Queue message into MessagePickupRepository and deliver it (along any other queued message). + * - `MessageForwardingStrategy.DirectDelivery` - Deliver message directly. Do not add into queue (it might be manually added after, e.g. in case of failure) * - * @default MessageForwardingStrategy.QueueAndDeliver + * @default MessageForwardingStrategy.DirectDelivery + * @todo Update default to QueueAndLiveModeDelivery */ messageForwardingStrategy?: MessageForwardingStrategy } @@ -39,6 +40,6 @@ export class MediatorModuleConfig { /** See {@link MediatorModuleConfigOptions.messageForwardingStrategy} */ public get messageForwardingStrategy() { - return this.options.messageForwardingStrategy ?? MessageForwardingStrategy.DeliverOnly + return this.options.messageForwardingStrategy ?? MessageForwardingStrategy.DirectDelivery } } diff --git a/packages/core/src/modules/routing/MessageForwardingStrategy.ts b/packages/core/src/modules/routing/MessageForwardingStrategy.ts index c3e253f038..06ce1e05c9 100644 --- a/packages/core/src/modules/routing/MessageForwardingStrategy.ts +++ b/packages/core/src/modules/routing/MessageForwardingStrategy.ts @@ -1,13 +1,13 @@ export enum MessageForwardingStrategy { // When a forward is received, simply queue encrypted message. MessagePickupRepository - // will be in charge of manually triggering MessagePickupApi.deliver() + // will be in charge of manually triggering MessagePickupApi.deliverMessages() QueueOnly = 'QueueOnly', - // Queue message into MessagePickupRepository and, if a live mode session si active, + // Queue message into MessagePickupRepository and, if a Message Pickup Live mode session is active, // deliver it along any other queued message - QueueAndDeliver = 'QueueAndDeliver', + QueueAndLiveModeDelivery = 'QueueAndLiveModeDelivery', - // Attempt to deliver message directly. Do not add into pickup queue (it might be manually - // added after, e.g. in case of failure) - DeliverOnly = 'DeliverOnly', + // Attempt to deliver message directly if a transport session is available. It will eventually added + // into pickup queue in case of failure on the delivery + DirectDelivery = 'DirectDelivery', } diff --git a/packages/core/src/modules/routing/services/MediatorService.ts b/packages/core/src/modules/routing/services/MediatorService.ts index 5645df7ba2..448a0b22a6 100644 --- a/packages/core/src/modules/routing/services/MediatorService.ts +++ b/packages/core/src/modules/routing/services/MediatorService.ts @@ -100,7 +100,7 @@ export class MediatorService { message: message.message, }) break - case MessageForwardingStrategy.QueueAndDeliver: { + case MessageForwardingStrategy.QueueAndLiveModeDelivery: { await this.messagePickupApi.queueMessage({ connectionId: mediationRecord.connectionId, recipientKeys: [verkeyToDidKey(message.to)], @@ -118,7 +118,7 @@ export class MediatorService { } break } - case MessageForwardingStrategy.DeliverOnly: + case MessageForwardingStrategy.DirectDelivery: // The message inside the forward message is packed so we just send the packed // message to the connection associated with it await messageSender.sendPackage(agentContext, { diff --git a/tests/e2e-ws-pickup-v2.test.ts b/tests/e2e-ws-pickup-v2.test.ts index 68c64cda7f..57eb52afd8 100644 --- a/tests/e2e-ws-pickup-v2.test.ts +++ b/tests/e2e-ws-pickup-v2.test.ts @@ -1,6 +1,7 @@ import type { AnonCredsTestsAgent } from '../packages/anoncreds/tests/legacyAnonCredsSetup' import { getLegacyAnonCredsModules } from '../packages/anoncreds/tests/legacyAnonCredsSetup' +import { MessageForwardingStrategy } from '../packages/core/src/modules/routing/MessageForwardingStrategy' import { getAgentOptions } from '../packages/core/tests/helpers' import { e2eTest } from './e2e-test' @@ -15,19 +16,6 @@ import { } from '@credo-ts/core' import { WsInboundTransport } from '@credo-ts/node' -const recipientOptions = getAgentOptions( - 'E2E WS Pickup V2 Recipient ', - {}, - { - ...getLegacyAnonCredsModules({ - autoAcceptCredentials: AutoAcceptCredential.ContentApproved, - }), - mediationRecipient: new MediationRecipientModule({ - mediatorPickupStrategy: MediatorPickupStrategy.PickUpV2, - }), - } -) - // FIXME: port numbers should not depend on availability from other test suites that use web sockets const mediatorPort = 4100 const mediatorOptions = getAgentOptions( @@ -39,7 +27,10 @@ const mediatorOptions = getAgentOptions( ...getLegacyAnonCredsModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), - mediator: new MediatorModule({ autoAcceptMediationRequests: true }), + mediator: new MediatorModule({ + autoAcceptMediationRequests: true, + messageForwardingStrategy: MessageForwardingStrategy.QueueAndLiveModeDelivery, + }), } ) @@ -66,7 +57,6 @@ describe('E2E WS Pickup V2 tests', () => { let senderAgent: AnonCredsTestsAgent beforeEach(async () => { - recipientAgent = new Agent(recipientOptions) as AnonCredsTestsAgent mediatorAgent = new Agent(mediatorOptions) as AnonCredsTestsAgent senderAgent = new Agent(senderOptions) as AnonCredsTestsAgent }) @@ -80,7 +70,60 @@ describe('E2E WS Pickup V2 tests', () => { await senderAgent.wallet.delete() }) - test('Full WS flow (connect, request mediation, issue, verify) using Message Pickup V2', async () => { + test('Full WS flow (connect, request mediation, issue, verify) using Message Pickup V2 polling mode', async () => { + const recipientOptions = getAgentOptions( + 'E2E WS Pickup V2 Recipient polling mode', + {}, + { + ...getLegacyAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediationRecipient: new MediationRecipientModule({ + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV2, + mediatorPollingInterval: 1000, + }), + } + ) + + recipientAgent = new Agent(recipientOptions) as AnonCredsTestsAgent + + // Recipient Setup + recipientAgent.registerOutboundTransport(new WsOutboundTransport()) + await recipientAgent.initialize() + + // Mediator Setup + mediatorAgent.registerInboundTransport(new WsInboundTransport({ port: mediatorPort })) + mediatorAgent.registerOutboundTransport(new WsOutboundTransport()) + await mediatorAgent.initialize() + + // Sender Setup + senderAgent.registerInboundTransport(new WsInboundTransport({ port: senderPort })) + senderAgent.registerOutboundTransport(new WsOutboundTransport()) + await senderAgent.initialize() + + await e2eTest({ + mediatorAgent, + senderAgent, + recipientAgent, + }) + }) + + test('Full WS flow (connect, request mediation, issue, verify) using Message Pickup V2 live mode', async () => { + const recipientOptions = getAgentOptions( + 'E2E WS Pickup V2 Recipient live mode', + {}, + { + ...getLegacyAnonCredsModules({ + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + }), + mediationRecipient: new MediationRecipientModule({ + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV2LiveMode, + }), + } + ) + + recipientAgent = new Agent(recipientOptions) as AnonCredsTestsAgent + // Recipient Setup recipientAgent.registerOutboundTransport(new WsOutboundTransport()) await recipientAgent.initialize() From b41e818097291c9c9b88128c8fd4d0c189166a33 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Wed, 31 Jan 2024 13:03:34 -0300 Subject: [PATCH 14/15] fix: use recipientDid instead of recipientKey Signed-off-by: Ariel Gentile --- packages/core/src/agent/MessageSender.ts | 6 +++--- .../modules/message-pickup/MessagePickupApi.ts | 18 ++++++++++-------- .../message-pickup/MessagePickupApiOptions.ts | 8 +++++--- .../protocol/MessagePickupProtocolOptions.ts | 2 +- .../protocol/v1/V1MessagePickupProtocol.ts | 5 +---- .../protocol/v2/V2MessagePickupProtocol.ts | 8 ++++---- .../__tests__/V2MessagePickupProtocol.test.ts | 4 ++-- .../storage/InMemoryMessagePickupRepository.ts | 14 +++++++------- .../storage/MessagePickupRepositoryOptions.ts | 6 +++--- .../routing/services/MediatorService.ts | 6 +++--- 10 files changed, 39 insertions(+), 38 deletions(-) diff --git a/packages/core/src/agent/MessageSender.ts b/packages/core/src/agent/MessageSender.ts index a09ed505a8..31fc4fac70 100644 --- a/packages/core/src/agent/MessageSender.ts +++ b/packages/core/src/agent/MessageSender.ts @@ -5,7 +5,6 @@ import type { TransportSession } from './TransportService' import type { AgentContext } from './context' import type { ConnectionRecord } from '../modules/connections' import type { ResolvedDidCommService } from '../modules/didcomm' -import type { DidDocument } from '../modules/dids' import type { OutOfBandRecord } from '../modules/oob/repository' import type { OutboundTransport } from '../transport/OutboundTransport' import type { EncryptedMessage, OutboundPackage } from '../types' @@ -15,6 +14,7 @@ import { ReturnRouteTypes } from '../decorators/transport/TransportDecorator' import { AriesFrameworkError, MessageSendingError } from '../error' import { Logger } from '../logger' import { DidCommDocumentService } from '../modules/didcomm' +import { DidKey, type DidDocument } from '../modules/dids' import { getKeyFromVerificationMethod } from '../modules/dids/domain/key-type' import { didKeyToInstanceOfKey, verkeyToDidKey } from '../modules/dids/helpers' import { DidResolverService } from '../modules/dids/services/DidResolverService' @@ -180,7 +180,7 @@ export class MessageSender { this.logger.debug(`Queue packed message for connection ${connection.id} (${connection.theirLabel})`) await this.messagePickupRepository.addMessage({ connectionId: connection.id, - recipientKeys: [recipientKey], + recipientDids: [verkeyToDidKey(recipientKey)], payload: encryptedMessage, }) return @@ -335,7 +335,7 @@ export class MessageSender { const encryptedMessage = await this.envelopeService.packMessage(agentContext, message, keys) await this.messagePickupRepository.addMessage({ connectionId: connection.id, - recipientKeys: keys.recipientKeys.map((item) => verkeyToDidKey(item.publicKeyBase58)), + recipientDids: keys.recipientKeys.map((item) => new DidKey(item).did), payload: encryptedMessage, }) diff --git a/packages/core/src/modules/message-pickup/MessagePickupApi.ts b/packages/core/src/modules/message-pickup/MessagePickupApi.ts index 0169897423..9878cc2684 100644 --- a/packages/core/src/modules/message-pickup/MessagePickupApi.ts +++ b/packages/core/src/modules/message-pickup/MessagePickupApi.ts @@ -7,6 +7,8 @@ import type { QueueMessageReturnType, SetLiveDeliveryModeOptions, SetLiveDeliveryModeReturnType, + DeliverMessagesReturnType, + DeliverMessagesFromQueueReturnType, } from './MessagePickupApiOptions' import type { MessagePickupSession, MessagePickupSessionRole } from './MessagePickupSession' import type { V1MessagePickupProtocol, V2MessagePickupProtocol } from './protocol' @@ -32,8 +34,9 @@ export interface MessagePickupApi { connectionId: string role?: MessagePickupSessionRole }): Promise - deliverMessages(options: DeliverMessagesOptions): Promise - deliverMessagesFromQueue(options: DeliverMessagesFromQueueOptions): Promise + deliverMessages(options: DeliverMessagesOptions): Promise + deliverMessagesFromQueue(options: DeliverMessagesFromQueueOptions): Promise + setLiveDeliveryMode(options: SetLiveDeliveryModeOptions): Promise } @injectable() @@ -85,14 +88,14 @@ export class MessagePickupApi { this.logger.debug('Queuing message...') - const { connectionId, message, recipientKeys } = options + const { connectionId, message, recipientDids } = options const connectionRecord = await this.connectionService.getById(this.agentContext, connectionId) const messagePickupRepository = this.agentContext.dependencyManager.resolve( InjectionSymbols.MessagePickupRepository ) - await messagePickupRepository.addMessage({ connectionId: connectionRecord.id, recipientKeys, payload: message }) + await messagePickupRepository.addMessage({ connectionId: connectionRecord.id, recipientDids, payload: message }) } /** @@ -153,13 +156,12 @@ export class MessagePickupApi { connectionId: string protocolVersion: MessagePickupProtocolVersionType - recipientKey?: string + recipientDid?: string batchSize?: number } @@ -43,4 +43,6 @@ export type PickupMessagesReturnType = void export type DeliverMessagesReturnType = void +export type DeliverMessagesFromQueueReturnType = void + export type SetLiveDeliveryModeReturnType = void diff --git a/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts index 41391f3286..4f4409c501 100644 --- a/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts +++ b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts @@ -4,7 +4,7 @@ import type { QueuedMessage } from '../storage' export interface PickupMessagesProtocolOptions { connectionRecord: ConnectionRecord - recipientKey?: string + recipientDid?: string batchSize?: number } diff --git a/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts index 48f86e393c..b0994478a9 100644 --- a/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts +++ b/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts @@ -94,10 +94,7 @@ export class V1MessagePickupProtocol extends BaseMessagePickupProtocol { } } - public async setLiveDeliveryMode( - agentContext: AgentContext, - options: SetLiveDeliveryModeProtocolOptions - ): Promise> { + public async setLiveDeliveryMode(): Promise> { throw new AriesFrameworkError('Live Delivery mode not supported in Message Pickup V1 protocol') } diff --git a/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts index 96e9722c31..c0b927b039 100644 --- a/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts +++ b/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts @@ -78,7 +78,7 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { agentContext: AgentContext, options: PickupMessagesProtocolOptions ): Promise> { - const { connectionRecord, recipientKey } = options + const { connectionRecord, recipientDid: recipientKey } = options connectionRecord.assertReady() const message = new V2StatusRequestMessage({ @@ -104,7 +104,7 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { messages ?? (await messagePickupRepository.takeFromQueue({ connectionId: connectionRecord.id, - recipientKey, + recipientDid: recipientKey, limit: 10, // TODO: Define as config parameter })) @@ -156,7 +156,7 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { recipientKey, messageCount: await messagePickupRepository.getAvailableMessageCount({ connectionId: connection.id, - recipientKey: recipientKey ? verkeyToDidKey(recipientKey) : undefined, + recipientDid: recipientKey ? verkeyToDidKey(recipientKey) : undefined, }), }) @@ -180,7 +180,7 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { // Get available messages from queue, but don't delete them const messages = await messagePickupRepository.takeFromQueue({ connectionId: connection.id, - recipientKey: recipientKey ? verkeyToDidKey(recipientKey) : undefined, + recipientDid: recipientKey ? verkeyToDidKey(recipientKey) : undefined, limit: message.limit, }) diff --git a/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts b/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts index 7707b56db7..bf0d6b2f0a 100644 --- a/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts +++ b/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts @@ -233,7 +233,7 @@ describe('V2MessagePickupProtocol', () => { expect(messagePickupRepository.takeFromQueue).toHaveBeenCalledWith({ connectionId: mockConnection.id, limit: 10, - recipientKey: verkeyToDidKey('recipientKey'), + recipientDid: verkeyToDidKey('recipientKey'), }) }) }) @@ -299,7 +299,7 @@ describe('V2MessagePickupProtocol', () => { it('creates a status request message', async () => { const { message: statusRequestMessage } = await pickupProtocol.createPickupMessage(agentContext, { connectionRecord: mockConnection, - recipientKey: 'a-key', + recipientDid: 'a-key', }) expect(statusRequestMessage).toMatchObject({ diff --git a/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts b/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts index 65f5e7da46..f066899369 100644 --- a/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts +++ b/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts @@ -14,7 +14,7 @@ import { uuid } from '../../../utils/uuid' interface InMemoryQueuedMessage extends QueuedMessage { connectionId: string - recipientKeys: string[] + recipientDids: string[] state: 'pending' | 'sending' } @@ -29,25 +29,25 @@ export class InMemoryMessagePickupRepository implements MessagePickupRepository } public getAvailableMessageCount(options: GetAvailableMessageCountOptions): number | Promise { - const { connectionId, recipientKey } = options + const { connectionId, recipientDid } = options const messages = this.messages.filter( (msg) => msg.connectionId === connectionId && - (recipientKey === undefined || msg.recipientKeys.includes(recipientKey)) && + (recipientDid === undefined || msg.recipientDids.includes(recipientDid)) && msg.state === 'pending' ) return messages.length } public takeFromQueue(options: TakeFromQueueOptions): QueuedMessage[] { - const { connectionId, recipientKey, limit, deleteMessages } = options + const { connectionId, recipientDid, limit, deleteMessages } = options let messages = this.messages.filter( (msg) => msg.connectionId === connectionId && msg.state === 'pending' && - (recipientKey === undefined || msg.recipientKeys.includes(recipientKey)) + (recipientDid === undefined || msg.recipientDids.includes(recipientDid)) ) const messagesToTake = limit ?? messages.length @@ -70,14 +70,14 @@ export class InMemoryMessagePickupRepository implements MessagePickupRepository } public addMessage(options: AddMessageOptions) { - const { connectionId, recipientKeys, payload } = options + const { connectionId, recipientDids, payload } = options const id = uuid() this.messages.push({ id, connectionId, encryptedMessage: payload, - recipientKeys, + recipientDids, state: 'pending', }) diff --git a/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts b/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts index 85b26e0c91..e586d5756a 100644 --- a/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts +++ b/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts @@ -2,19 +2,19 @@ import type { EncryptedMessage } from '../../../types' export interface GetAvailableMessageCountOptions { connectionId: string - recipientKey?: string + recipientDid?: string } export interface TakeFromQueueOptions { connectionId: string - recipientKey?: string + recipientDid?: string limit?: number deleteMessages?: boolean } export interface AddMessageOptions { connectionId: string - recipientKeys: string[] + recipientDids: string[] payload: EncryptedMessage } diff --git a/packages/core/src/modules/routing/services/MediatorService.ts b/packages/core/src/modules/routing/services/MediatorService.ts index 448a0b22a6..d7cdf433f1 100644 --- a/packages/core/src/modules/routing/services/MediatorService.ts +++ b/packages/core/src/modules/routing/services/MediatorService.ts @@ -96,14 +96,14 @@ export class MediatorService { case MessageForwardingStrategy.QueueOnly: await this.messagePickupApi.queueMessage({ connectionId: mediationRecord.connectionId, - recipientKeys: [verkeyToDidKey(message.to)], + recipientDids: [verkeyToDidKey(message.to)], message: message.message, }) break case MessageForwardingStrategy.QueueAndLiveModeDelivery: { await this.messagePickupApi.queueMessage({ connectionId: mediationRecord.connectionId, - recipientKeys: [verkeyToDidKey(message.to)], + recipientDids: [verkeyToDidKey(message.to)], message: message.message, }) const session = await this.messagePickupApi.getLiveModeSession({ @@ -113,7 +113,7 @@ export class MediatorService { if (session) { await this.messagePickupApi.deliverMessagesFromQueue({ pickupSessionId: session.id, - recipientKey: verkeyToDidKey(message.to), + recipientDid: verkeyToDidKey(message.to), }) } break From 180695c1700a82e37d7fcfdc2ee386ae36144210 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Wed, 31 Jan 2024 13:57:50 -0300 Subject: [PATCH 15/15] Merge branch 'main' into feat/pickup-live-mode --- .devcontainer/Dockerfile | 18 - .devcontainer/devcontainer.json | 4 +- .github/actions/setup-cheqd/action.yml | 14 - .github/actions/setup-indy-pool/action.yml | 36 - .github/actions/setup-libindy/action.yml | 18 - .../setup-postgres-wallet-plugin/action.yml | 21 - .github/actions/setup-postgres/action.yml | 12 - .github/workflows/continuous-deployment.yml | 8 - .github/workflows/continuous-integration.yml | 34 +- DEVREADME.md | 107 +-- Dockerfile | 66 +- README.md | 18 +- TROUBLESHOOTING.md | 85 --- demo/package.json | 2 - demo/src/Alice.ts | 2 +- demo/src/BaseAgent.ts | 64 +- demo/src/Faber.ts | 2 +- docker-compose.arm.yml | 32 + docker-compose.yml | 31 + docker/docker-compose-mediators-ngrok.yml | 19 - docker/docker-compose-mediators.yml | 16 - package.json | 7 +- .../action-menu/tests/action-menu.e2e.test.ts | 10 +- .../anoncreds-rs/src/AnonCredsRsModule.ts | 2 +- packages/anoncreds-rs/src/index.ts | 1 + .../src/services/AnonCredsRsHolderService.ts | 12 +- .../__tests__/AnonCredsRsServices.test.ts | 7 +- .../src/services/__tests__/helpers.ts | 2 + .../anoncreds-rs/tests/anoncreds-flow.test.ts | 4 +- packages/anoncreds-rs/tests/anoncredsSetup.ts | 22 +- packages/anoncreds-rs/tests/indy-flow.test.ts | 2 +- .../v2-credential-revocation.e2e.test.ts | 4 +- .../tests/v2-credentials.e2e.test.ts | 1 + .../anoncreds-rs/tests/v2-proofs.e2e.test.ts | 2 +- packages/anoncreds/package.json | 1 - packages/anoncreds/src/AnonCredsApi.ts | 2 + .../AnonCredsCredentialFormatService.ts | 4 +- .../LegacyIndyCredentialFormatService.ts | 4 +- .../legacy-indy-format-services.test.ts | 120 +++- packages/anoncreds/src/models/exchange.ts | 1 + packages/anoncreds/src/models/internal.ts | 4 +- .../v1-connectionless-proofs.e2e.test.ts | 17 +- .../src/updates/__tests__/0.3.test.ts | 47 +- .../__tests__/__snapshots__/0.3.test.ts.snap | 8 +- .../tests/InMemoryAnonCredsRegistry.ts | 27 +- packages/anoncreds/tests/anoncreds.test.ts | 54 +- .../anoncreds/tests/legacyAnonCredsSetup.ts | 84 +-- packages/askar/src/AskarModule.ts | 2 +- .../askar/src/storage/AskarStorageService.ts | 2 +- packages/askar/src/utils/askarKeyTypes.ts | 43 +- packages/askar/src/utils/askarWalletConfig.ts | 44 +- packages/askar/src/wallet/AskarBaseWallet.ts | 315 +++------ packages/askar/src/wallet/AskarWallet.ts | 11 +- .../AskarWalletPostgresStorageConfig.ts | 22 - .../src/wallet/AskarWalletStorageConfig.ts | 47 ++ packages/askar/src/wallet/didcommV1.ts | 177 +++++ packages/askar/src/wallet/index.ts | 2 +- .../askar/tests/askar-inmemory.e2e.test.ts | 6 +- .../askar/tests/askar-postgres.e2e.test.ts | 18 +- packages/askar/tests/askar-sqlite.e2e.test.ts | 6 +- packages/askar/tests/helpers.ts | 17 +- packages/bbs-signatures/src/BbsModule.ts | 2 +- .../tests/bbs-signatures.e2e.test.ts | 25 +- .../tests/bbs-signing-provider.e2e.test.ts | 34 +- ...proof.credentials.propose-offerBbs.test.ts | 5 +- packages/cheqd/package.json | 2 - packages/cheqd/src/CheqdModule.ts | 2 +- .../tests/cheqd-did-registrar.e2e.test.ts | 4 +- .../tests/cheqd-did-resolver.e2e.test.ts | 4 +- .../cheqd-sdk-anoncreds-registry.e2e.test.ts | 20 +- packages/cheqd/tests/setupCheqdModule.ts | 8 - packages/core/src/agent/Agent.ts | 4 +- packages/core/src/agent/Dispatcher.ts | 6 +- packages/core/src/agent/MessageSender.ts | 6 +- .../core/src/agent/__tests__/Agent.test.ts | 10 +- .../src/agent/__tests__/MessageSender.test.ts | 9 +- .../src/crypto/__tests__/JwsService.test.ts | 11 +- .../signature/SignatureDecoratorUtils.test.ts | 6 +- .../__tests__/basic-messages.e2e.test.ts | 25 +- .../__tests__/ConnectionService.test.ts | 6 +- .../__tests__/connection-manual.e2e.test.ts | 12 +- .../__tests__/did-rotate.e2e.test.ts | 9 +- .../__tests__/didexchange-numalgo.e2e.test.ts | 9 +- .../v2-connectionless-credentials.e2e.test.ts | 12 +- .../v2-credentials-auto-accept.e2e.test.ts | 14 +- ...f.credentials.propose-offerED25519.test.ts | 44 +- .../modules/dids/__tests__/DidsApi.test.ts | 18 +- .../dids/__tests__/dids-registrar.e2e.test.ts | 14 +- .../dids/__tests__/dids-resolver.e2e.test.ts | 16 +- .../modules/dids/__tests__/peer-did.test.ts | 6 +- .../DifPresentationExchangeModule.ts | 2 +- .../v1-discover-features.e2e.test.ts | 25 +- .../v2-discover-features.e2e.test.ts | 25 +- .../message-pickup/__tests__/pickup.test.ts | 21 +- .../oob/__tests__/connect-to-self.e2e.test.ts | 13 +- .../oob/__tests__/implicit.e2e.test.ts | 46 +- ...entationExchangeProofFormatService.test.ts | 26 +- .../v2-indy-connectionless-proofs.e2e.test.ts | 16 +- .../v2/__tests__/v2-indy-proofs.e2e.test.ts | 5 + ...entation-exchange-presentation.e2e.test.ts | 2 +- .../routing/__tests__/mediation.test.ts | 34 +- .../vc/__tests__/W3cCredentialService.test.ts | 10 +- .../vc/__tests__/W3cCredentialsApi.test.ts | 22 +- .../W3cJsonLdCredentialService.test.ts | 7 +- .../modules/vc/data-integrity/deriveProof.ts | 1 + .../proof-purposes/ProofPurpose.ts | 1 + .../vc/jwt-vc/W3cJwtCredentialService.ts | 2 +- .../__tests__/W3cJwtCredentialService.test.ts | 12 +- .../storage/migration/__tests__/0.1.test.ts | 70 +- .../storage/migration/__tests__/0.2.test.ts | 61 +- .../storage/migration/__tests__/0.3.test.ts | 20 +- .../__tests__/UpdateAssistant.test.ts | 28 +- .../__tests__/__snapshots__/0.2.test.ts.snap | 22 +- .../migration/__tests__/backup-askar.test.ts | 11 +- .../migration/__tests__/backup.test.ts | 16 +- packages/core/tests/agents.test.ts | 23 +- packages/core/tests/connections.test.ts | 36 +- packages/core/tests/generic-records.test.ts | 13 +- packages/core/tests/helpers.ts | 103 ++- packages/core/tests/index.ts | 1 - packages/core/tests/indySdk.ts | 3 - packages/core/tests/jsonld.ts | 23 +- packages/core/tests/migration.test.ts | 4 +- .../core/tests/multi-protocol-version.test.ts | 23 +- .../tests/oob-mediation-provision.test.ts | 22 +- packages/core/tests/oob-mediation.test.ts | 22 +- packages/core/tests/oob.test.ts | 17 +- packages/core/tests/setup.ts | 3 - packages/core/tests/wallet.test.ts | 186 ----- .../indy-sdk-to-askar-migration/package.json | 2 - .../src/IndySdkToAskarMigrationUpdater.ts | 2 +- .../tests/indy-sdk-040-wallet.db | Bin 0 -> 57344 bytes .../tests/migrate.test.ts | 99 +-- packages/indy-sdk/CHANGELOG.md | 59 -- packages/indy-sdk/README.md | 31 - packages/indy-sdk/jest.config.ts | 13 - packages/indy-sdk/package.json | 41 -- packages/indy-sdk/src/IndySdkModule.ts | 60 -- packages/indy-sdk/src/IndySdkModuleConfig.ts | 81 --- packages/indy-sdk/src/anoncreds/index.ts | 4 - .../services/IndySdkAnonCredsRegistry.ts | 634 ----------------- .../services/IndySdkHolderService.ts | 464 ------------- .../services/IndySdkIssuerService.ts | 173 ----- .../services/IndySdkIssuerServiceMetadata.ts | 3 - .../services/IndySdkRevocationService.ts | 155 ----- .../services/IndySdkVerifierService.ts | 96 --- .../utils/__tests__/assertUnqualified.test.ts | 152 ----- .../utils/__tests__/identifiers.test.ts | 79 --- .../utils/__tests__/transform.test.ts | 114 ---- .../src/anoncreds/utils/assertUnqualified.ts | 129 ---- .../src/anoncreds/utils/identifiers.ts | 63 -- .../indy-sdk/src/anoncreds/utils/tails.ts | 45 -- .../indy-sdk/src/anoncreds/utils/transform.ts | 161 ----- .../src/dids/IndySdkIndyDidRegistrar.ts | 328 --------- .../src/dids/IndySdkIndyDidResolver.ts | 126 ---- .../src/dids/IndySdkSovDidResolver.ts | 101 --- .../__tests__/IndySdkIndyDidRegistrar.test.ts | 501 -------------- .../__tests__/IndySdkIndyDidResolver.test.ts | 127 ---- .../__tests__/IndySdkSovDidResolver.test.ts | 132 ---- .../didIndyPool1R1xKJw17sUoXhejEpugMYJ.json | 50 -- .../didIndyPool1WJz9mHyW9BZksioQnRsrAo.json | 48 -- .../didSovR1xKJw17sUoXhejEpugMYJ.json | 51 -- .../didSovWJz9mHyW9BZksioQnRsrAo.json | 49 -- packages/indy-sdk/src/dids/didIndyUtil.ts | 55 -- packages/indy-sdk/src/dids/didSovUtil.ts | 138 ---- packages/indy-sdk/src/dids/index.ts | 3 - packages/indy-sdk/src/error/IndySdkError.ts | 11 - packages/indy-sdk/src/error/index.ts | 2 - packages/indy-sdk/src/error/indyError.ts | 100 --- packages/indy-sdk/src/index.ts | 23 - packages/indy-sdk/src/ledger/IndySdkPool.ts | 218 ------ .../indy-sdk/src/ledger/IndySdkPoolService.ts | 357 ---------- .../__tests__/IndySdkPoolService.test.ts | 422 ------------ .../src/ledger/__tests__/didResponses.ts | 58 -- .../serializeRequestForSignature.test.ts | 87 --- .../src/ledger/__tests__/util.test.ts | 45 -- .../src/ledger/error/IndySdkPoolError.ts | 7 - .../error/IndySdkPoolNotConfiguredError.ts | 7 - .../ledger/error/IndySdkPoolNotFoundError.ts | 7 - packages/indy-sdk/src/ledger/error/index.ts | 3 - packages/indy-sdk/src/ledger/index.ts | 2 - .../ledger/serializeRequestForSignature.ts | 61 -- packages/indy-sdk/src/ledger/util.ts | 9 - .../src/storage/IndySdkStorageService.ts | 321 --------- .../__tests__/IndySdkStorageService.test.ts | 314 --------- packages/indy-sdk/src/storage/index.ts | 1 - packages/indy-sdk/src/types.ts | 6 - .../indy-sdk/src/utils/__tests__/did.test.ts | 77 --- .../indy-sdk/src/utils/assertIndySdkWallet.ts | 13 - packages/indy-sdk/src/utils/did.ts | 89 --- packages/indy-sdk/src/utils/promises.ts | 44 -- packages/indy-sdk/src/wallet/IndySdkWallet.ts | 645 ------------------ .../wallet/__tests__/IndySdkWallet.test.ts | 114 ---- packages/indy-sdk/src/wallet/index.ts | 1 - .../indy-sdk/tests/__fixtures__/anoncreds.ts | 30 - .../tests/indy-did-registrar.e2e.test.ts | 125 ---- .../tests/indy-did-resolver.e2e.test.ts | 99 --- .../indy-sdk-anoncreds-registry.e2e.test.ts | 345 ---------- packages/indy-sdk/tests/postgres.e2e.test.ts | 112 --- packages/indy-sdk/tests/setup.ts | 1 - packages/indy-sdk/tests/setupIndySdkModule.ts | 35 - .../tests/sov-did-resolver.e2e.test.ts | 102 --- packages/indy-sdk/tsconfig.build.json | 7 - packages/indy-sdk/tsconfig.json | 6 - packages/indy-vdr/src/IndyVdrModule.ts | 2 +- .../src/anoncreds/IndyVdrAnonCredsRegistry.ts | 4 +- .../src/anoncreds/utils/identifiers.ts | 5 - .../__tests__/IndyVdrIndyDidRegistrar.test.ts | 8 +- .../indy-vdr-anoncreds-registry.e2e.test.ts | 76 +-- .../tests/indy-vdr-did-registrar.e2e.test.ts | 217 +++--- .../indy-vdr-indy-did-resolver.e2e.test.ts | 9 +- .../indy-vdr/tests/indy-vdr-pool.e2e.test.ts | 7 +- .../indy-vdr-sov-did-resolver.e2e.test.ts | 9 +- packages/node/src/PostgresPlugin.ts | 108 --- packages/node/src/index.ts | 10 +- .../src/OpenId4VcClientModule.ts | 2 +- .../tests/openid4vc-client.e2e.test.ts | 13 +- .../__tests__/QuestionAnswerService.test.ts | 7 +- .../tests/question-answer.e2e.test.ts | 10 +- packages/react-native/jest.config.ts | 4 - packages/sd-jwt-vc/package.json | 1 - packages/sd-jwt-vc/src/SdJwtVcModule.ts | 2 +- .../src/__tests__/SdJwtVcService.test.ts | 28 +- packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts | 42 +- packages/tenants/src/TenantsModule.ts | 2 +- .../tenants/src/__tests__/TenantAgent.test.ts | 18 +- .../tenants/src/__tests__/TenantsApi.test.ts | 5 +- .../TenantSessionCoordinator.test.ts | 20 +- .../tenants/tests/tenant-sessions.e2e.test.ts | 16 +- .../tests/tenants-askar-profiles.e2e.test.ts | 12 +- packages/tenants/tests/tenants.e2e.test.ts | 17 +- samples/extension-module/package.json | 16 +- .../extension-module/tests/dummy.e2e.test.ts | 8 +- tests/InMemoryStorageService.ts | 68 +- tests/InMemoryWallet.ts | 343 ++++++++++ tests/InMemoryWalletModule.ts | 22 + ...> e2e-askar-indy-vdr-anoncreds-rs.test.ts} | 23 +- tests/e2e-http.test.ts | 16 +- tests/e2e-subject.test.ts | 16 +- tests/e2e-ws-pickup-v2.test.ts | 26 +- tests/e2e-ws.test.ts | 20 +- yarn.lock | 221 +----- 242 files changed, 2071 insertions(+), 10727 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .github/actions/setup-cheqd/action.yml delete mode 100644 .github/actions/setup-indy-pool/action.yml delete mode 100644 .github/actions/setup-libindy/action.yml delete mode 100644 .github/actions/setup-postgres-wallet-plugin/action.yml delete mode 100644 .github/actions/setup-postgres/action.yml delete mode 100644 TROUBLESHOOTING.md create mode 100644 docker-compose.arm.yml create mode 100644 docker-compose.yml delete mode 100644 docker/docker-compose-mediators-ngrok.yml delete mode 100644 docker/docker-compose-mediators.yml delete mode 100644 packages/askar/src/wallet/AskarWalletPostgresStorageConfig.ts create mode 100644 packages/askar/src/wallet/AskarWalletStorageConfig.ts create mode 100644 packages/askar/src/wallet/didcommV1.ts delete mode 100644 packages/core/tests/indySdk.ts delete mode 100644 packages/core/tests/wallet.test.ts create mode 100644 packages/indy-sdk-to-askar-migration/tests/indy-sdk-040-wallet.db delete mode 100644 packages/indy-sdk/CHANGELOG.md delete mode 100644 packages/indy-sdk/README.md delete mode 100644 packages/indy-sdk/jest.config.ts delete mode 100644 packages/indy-sdk/package.json delete mode 100644 packages/indy-sdk/src/IndySdkModule.ts delete mode 100644 packages/indy-sdk/src/IndySdkModuleConfig.ts delete mode 100644 packages/indy-sdk/src/anoncreds/index.ts delete mode 100644 packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts delete mode 100644 packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts delete mode 100644 packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts delete mode 100644 packages/indy-sdk/src/anoncreds/services/IndySdkIssuerServiceMetadata.ts delete mode 100644 packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts delete mode 100644 packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts delete mode 100644 packages/indy-sdk/src/anoncreds/utils/__tests__/assertUnqualified.test.ts delete mode 100644 packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts delete mode 100644 packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts delete mode 100644 packages/indy-sdk/src/anoncreds/utils/assertUnqualified.ts delete mode 100644 packages/indy-sdk/src/anoncreds/utils/identifiers.ts delete mode 100644 packages/indy-sdk/src/anoncreds/utils/tails.ts delete mode 100644 packages/indy-sdk/src/anoncreds/utils/transform.ts delete mode 100644 packages/indy-sdk/src/dids/IndySdkIndyDidRegistrar.ts delete mode 100644 packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts delete mode 100644 packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts delete mode 100644 packages/indy-sdk/src/dids/__tests__/IndySdkIndyDidRegistrar.test.ts delete mode 100644 packages/indy-sdk/src/dids/__tests__/IndySdkIndyDidResolver.test.ts delete mode 100644 packages/indy-sdk/src/dids/__tests__/IndySdkSovDidResolver.test.ts delete mode 100644 packages/indy-sdk/src/dids/__tests__/__fixtures__/didIndyPool1R1xKJw17sUoXhejEpugMYJ.json delete mode 100644 packages/indy-sdk/src/dids/__tests__/__fixtures__/didIndyPool1WJz9mHyW9BZksioQnRsrAo.json delete mode 100644 packages/indy-sdk/src/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json delete mode 100644 packages/indy-sdk/src/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json delete mode 100644 packages/indy-sdk/src/dids/didIndyUtil.ts delete mode 100644 packages/indy-sdk/src/dids/didSovUtil.ts delete mode 100644 packages/indy-sdk/src/dids/index.ts delete mode 100644 packages/indy-sdk/src/error/IndySdkError.ts delete mode 100644 packages/indy-sdk/src/error/index.ts delete mode 100644 packages/indy-sdk/src/error/indyError.ts delete mode 100644 packages/indy-sdk/src/index.ts delete mode 100644 packages/indy-sdk/src/ledger/IndySdkPool.ts delete mode 100644 packages/indy-sdk/src/ledger/IndySdkPoolService.ts delete mode 100644 packages/indy-sdk/src/ledger/__tests__/IndySdkPoolService.test.ts delete mode 100644 packages/indy-sdk/src/ledger/__tests__/didResponses.ts delete mode 100644 packages/indy-sdk/src/ledger/__tests__/serializeRequestForSignature.test.ts delete mode 100644 packages/indy-sdk/src/ledger/__tests__/util.test.ts delete mode 100644 packages/indy-sdk/src/ledger/error/IndySdkPoolError.ts delete mode 100644 packages/indy-sdk/src/ledger/error/IndySdkPoolNotConfiguredError.ts delete mode 100644 packages/indy-sdk/src/ledger/error/IndySdkPoolNotFoundError.ts delete mode 100644 packages/indy-sdk/src/ledger/error/index.ts delete mode 100644 packages/indy-sdk/src/ledger/index.ts delete mode 100644 packages/indy-sdk/src/ledger/serializeRequestForSignature.ts delete mode 100644 packages/indy-sdk/src/ledger/util.ts delete mode 100644 packages/indy-sdk/src/storage/IndySdkStorageService.ts delete mode 100644 packages/indy-sdk/src/storage/__tests__/IndySdkStorageService.test.ts delete mode 100644 packages/indy-sdk/src/storage/index.ts delete mode 100644 packages/indy-sdk/src/types.ts delete mode 100644 packages/indy-sdk/src/utils/__tests__/did.test.ts delete mode 100644 packages/indy-sdk/src/utils/assertIndySdkWallet.ts delete mode 100644 packages/indy-sdk/src/utils/did.ts delete mode 100644 packages/indy-sdk/src/utils/promises.ts delete mode 100644 packages/indy-sdk/src/wallet/IndySdkWallet.ts delete mode 100644 packages/indy-sdk/src/wallet/__tests__/IndySdkWallet.test.ts delete mode 100644 packages/indy-sdk/src/wallet/index.ts delete mode 100644 packages/indy-sdk/tests/__fixtures__/anoncreds.ts delete mode 100644 packages/indy-sdk/tests/indy-did-registrar.e2e.test.ts delete mode 100644 packages/indy-sdk/tests/indy-did-resolver.e2e.test.ts delete mode 100644 packages/indy-sdk/tests/indy-sdk-anoncreds-registry.e2e.test.ts delete mode 100644 packages/indy-sdk/tests/postgres.e2e.test.ts delete mode 100644 packages/indy-sdk/tests/setup.ts delete mode 100644 packages/indy-sdk/tests/setupIndySdkModule.ts delete mode 100644 packages/indy-sdk/tests/sov-did-resolver.e2e.test.ts delete mode 100644 packages/indy-sdk/tsconfig.build.json delete mode 100644 packages/indy-sdk/tsconfig.json delete mode 100644 packages/node/src/PostgresPlugin.ts create mode 100644 tests/InMemoryWallet.ts create mode 100644 tests/InMemoryWalletModule.ts rename tests/{e2e-askar-indy-sdk-wallet-subject.test.ts => e2e-askar-indy-vdr-anoncreds-rs.test.ts} (83%) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 3d51f0a5a0..0000000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -# arm + amd compatible Dockerfile -FROM ghcr.io/findy-network/findy-base:indy-1.16.ubuntu-18.04 AS indy-base - -FROM ubuntu:18.04 - -# install indy deps and files from base -RUN apt-get update && apt-get install -y libsodium23 libssl1.1 libzmq5 git zsh - -COPY --from=indy-base /usr/include/indy /usr/include/indy -COPY --from=indy-base /usr/lib/libindy.a /usr/lib/libindy.a -COPY --from=indy-base /usr/lib/libindy.so /usr/lib/libindy.so - -RUN apt-get install -y curl python3 build-essential ca-certificates && \ - curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash && \ - export NVM_DIR="$HOME/.nvm" && \ - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ - nvm install v16 && \ - npm install yarn -g diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1728c1a7cc..61745b0bc6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,5 @@ { - "build": { - "dockerfile": "Dockerfile" - }, + "image": "node:18", "runArgs": ["--env-file", ".devcontainer/devcontainer.env"], "workspaceMount": "source=${localWorkspaceFolder},target=/work,type=bind", "workspaceFolder": "/work" diff --git a/.github/actions/setup-cheqd/action.yml b/.github/actions/setup-cheqd/action.yml deleted file mode 100644 index e8a64207e7..0000000000 --- a/.github/actions/setup-cheqd/action.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Setup cheqd -description: Setup a cheqd network to perform tests -author: 'daev@cheqd.io' - -runs: - using: composite - steps: - - name: Start cheqd localnet - run: docker run --rm -d -p 26657:26657 ghcr.io/cheqd/cheqd-testnet:latest - shell: bash - -branding: - icon: scissors - color: purple diff --git a/.github/actions/setup-indy-pool/action.yml b/.github/actions/setup-indy-pool/action.yml deleted file mode 100644 index 23e1ebd8cf..0000000000 --- a/.github/actions/setup-indy-pool/action.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Setup Indy Pool -description: Setup an Indy ledger pool and register test did on the ledger -author: 'timo@animo.id' - -inputs: - seed: - description: Seed to register on the ledger - required: true - endorserSeed: - description: Endorser seed to register on the ledger - required: true - -runs: - using: composite - steps: - - name: Start indy pool - run: | - docker build -f network/indy-pool.dockerfile -t indy-pool . - docker run -d --name indy-pool -p 9701-9708:9701-9708 indy-pool - shell: bash - - - name: Setup Indy CLI - run: docker exec indy-pool indy-cli-setup - shell: bash - - - name: Register Trustee DID on ledger - run: docker exec indy-pool add-did-from-seed ${{ inputs.seed }} TRUSTEE - shell: bash - - - name: Register Endorser DID on ledger - run: docker exec indy-pool add-did-from-seed ${{ inputs.endorserSeed }} ENDORSER - shell: bash - -branding: - icon: scissors - color: purple diff --git a/.github/actions/setup-libindy/action.yml b/.github/actions/setup-libindy/action.yml deleted file mode 100644 index 086635ee6c..0000000000 --- a/.github/actions/setup-libindy/action.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Setup Libindy -description: Download and install the libindy binary from the sovrin repository -author: 'timo@animo.id' - -runs: - using: composite - steps: - - name: Setup Indy - run: | - sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 - sudo add-apt-repository "deb https://repo.sovrin.org/sdk/deb bionic stable" - sudo apt-get update -y - sudo apt-get install -y --allow-unauthenticated libindy - shell: bash - -branding: - icon: scissors - color: purple diff --git a/.github/actions/setup-postgres-wallet-plugin/action.yml b/.github/actions/setup-postgres-wallet-plugin/action.yml deleted file mode 100644 index a03b2f3fde..0000000000 --- a/.github/actions/setup-postgres-wallet-plugin/action.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Setup Postgres wallet plugin -description: Setup Postgres wallet plugin -author: 'sairanjit.tummalapalli@ayanworks.com' - -runs: - using: composite - steps: - # cargo build failing on latest release of rust due to - # socket2 dependency in the plugin https://users.rust-lang.org/t/build-broken-with-parse-quote-spanned-is-ambiguous/80280/2 - # so pointing rust version to 1.63.0 - - name: Setup Postgres wallet plugin - run: | - sudo apt-get install -y libzmq3-dev libsodium-dev pkg-config libssl-dev - curl https://sh.rustup.rs -sSf | bash -s -- -y - export PATH="/root/.cargo/bin:${PATH}" - rustup default 1.63.0 - cd ../ - git clone https://github.com/hyperledger/indy-sdk.git - cd indy-sdk/experimental/plugins/postgres_storage/ - cargo build --release - shell: bash diff --git a/.github/actions/setup-postgres/action.yml b/.github/actions/setup-postgres/action.yml deleted file mode 100644 index 6e69e6574f..0000000000 --- a/.github/actions/setup-postgres/action.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: Setup Postgres -description: Setup Postgres -author: 'sairanjit.tummalapalli@ayanworks.com' - -runs: - using: composite - steps: - - name: Setup Postgres - run: | - docker pull postgres - docker run --name postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres - shell: bash diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml index 2916ed1ec8..b9e47524ab 100644 --- a/.github/workflows/continuous-deployment.yml +++ b/.github/workflows/continuous-deployment.yml @@ -20,10 +20,6 @@ jobs: # pulls all commits (needed for lerna to correctly version) fetch-depth: 0 - # setup dependencies - - name: Setup Libindy - uses: ./.github/actions/setup-libindy - - name: Setup NodeJS uses: actions/setup-node@v4 with: @@ -69,10 +65,6 @@ jobs: - name: Checkout credo uses: actions/checkout@v4 - # setup dependencies - - name: Setup Libindy - uses: ./.github/actions/setup-libindy - - name: Setup NodeJS uses: actions/setup-node@v4 with: diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 3aa49df90e..77b45515c3 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -9,10 +9,6 @@ on: workflow_dispatch: env: - TEST_AGENT_PUBLIC_DID_SEED: 000000000000000000000000Trustee9 - ENDORSER_AGENT_PUBLIC_DID_SEED: 00000000000000000000000Endorser9 - GENESIS_TXN_PATH: network/genesis/local-genesis.txn - LIB_INDY_STRG_POSTGRES: /home/runner/work/credo-ts/indy-sdk/experimental/plugins/postgres_storage/target/release # for Linux NODE_OPTIONS: --max_old_space_size=6144 # Make sure we're not running multiple release steps at the same time as this can give issues with determining the next npm version to release. @@ -53,10 +49,6 @@ jobs: - name: Checkout credo-ts uses: actions/checkout@v4 - # setup dependencies - - name: Setup Libindy - uses: ./.github/actions/setup-libindy - - name: Setup NodeJS uses: actions/setup-node@v4 with: @@ -91,24 +83,8 @@ jobs: uses: actions/checkout@v4 # setup dependencies - - - name: Setup Libindy - uses: ./.github/actions/setup-libindy - - - name: Setup Indy Pool - uses: ./.github/actions/setup-indy-pool - with: - seed: ${TEST_AGENT_PUBLIC_DID_SEED} - endorserSeed: ${ENDORSER_AGENT_PUBLIC_DID_SEED} - - - name: Setup Cheqd - uses: ./.github/actions/setup-cheqd - - - name: Setup Postgres - uses: ./.github/actions/setup-postgres - - - name: Setup Postgres wallet plugin - uses: ./.github/actions/setup-postgres-wallet-plugin + - name: Setup services + run: docker compose up -d - name: Setup NodeJS uses: actions/setup-node@v4 @@ -120,7 +96,7 @@ jobs: run: yarn install --frozen-lockfile - name: Run tests - run: TEST_AGENT_PUBLIC_DID_SEED=${TEST_AGENT_PUBLIC_DID_SEED} ENDORSER_AGENT_PUBLIC_DID_SEED=${ENDORSER_AGENT_PUBLIC_DID_SEED} GENESIS_TXN_PATH=${GENESIS_TXN_PATH} yarn test --coverage --forceExit --bail + run: yarn test --coverage --forceExit --bail - uses: codecov/codecov-action@v3 if: always() @@ -138,10 +114,6 @@ jobs: fetch-depth: 0 persist-credentials: false - # setup dependencies - - name: Setup Libindy - uses: ./.github/actions/setup-libindy - - name: Setup NodeJS uses: actions/setup-node@v4 with: diff --git a/DEVREADME.md b/DEVREADME.md index 7fa9a7a06a..bfd3e87e9e 100644 --- a/DEVREADME.md +++ b/DEVREADME.md @@ -18,74 +18,22 @@ GENESIS_TXN_PATH=/work/network/genesis/local-genesis.txn ## Running tests -Test are executed using jest. Some test require either the **mediator agents** or the **ledger** to be running. When running tests that require a connection to the ledger pool, you need to set the `TEST_AGENT_PUBLIC_DID_SEED`, `ENDORSER_AGENT_PUBLIC_DID_SEED` and `GENESIS_TXN_PATH` environment variables. +Test are executed using jest. Some test require the **indy ledger**, **cheqd ledger** or **postgres database** to be running. -### Setting environment variables - -If you're using the setup as described in this document, you don't need to provide any environment variables as the default will be sufficient. - -- `GENESIS_TXN_PATH`: The path to the genesis transaction that allows us to connect to the indy pool. - - `GENESIS_TXN_PATH=network/genesis/local-genesis.txn` - default. Works with the [ledger setup](#setup-indy-ledger) from the previous step. - - `GENESIS_TXN_PATH=network/genesis/builder-net-genesis.txn` - Sovrin BuilderNet genesis. - - `GENESIS_TXN_PATH=/path/to/any/ledger/you/like` -- `TEST_AGENT_PUBLIC_DID_SEED`: The seed to use for the public DID. This will be used to do public write operations to the ledger. You should use a seed for a DID that is already registered on the ledger. - - If using the local or default genesis, use the same seed you used for the `add-did-from-seed` command from the [ledger setup](#setup-indy-ledger) in the previous step. (default is `000000000000000000000000Trustee9`) - - If using the BuilderNet genesis, make sure your seed is registered on the BuilderNet using [selfserve.sovrin.org](https://selfserve.sovrin.org/) and you have read and accepted the associated [Transaction Author Agreement](https://github.com/sovrin-foundation/sovrin/blob/master/TAA/TAA.md). We are not responsible for any unwanted consequences of using the BuilderNet. -- `ENDORSER_AGENT_PUBLIC_DID_SEED`: The seed to use for the public Endorser DID. This will be used to endorse transactions. You should use a seed for a DID that is already registered on the ledger. - - If using the local or default genesis, use the same seed you used for the `add-did-from-seed` command from the [ledger setup](#setup-indy-ledger) in the previous step. (default is `00000000000000000000000Endorser9`) - - If using the BuilderNet genesis, make sure your seed is registered on the BuilderNet using [selfserve.sovrin.org](https://selfserve.sovrin.org/) and you have read and accepted the associated [Transaction Author Agreement](https://github.com/sovrin-foundation/sovrin/blob/master/TAA/TAA.md). We are not responsible for any unwanted consequences of using the BuilderNet. - -### Setup Postgres +When running tests that require a connection to the indy ledger pool, you can set the `TEST_AGENT_PUBLIC_DID_SEED`, `ENDORSER_AGENT_PUBLIC_DID_SEED` and `GENESIS_TXN_PATH` environment variables. -> Note: Setup the postgres plugin first by following the [docs](https://https://credo.js.org/) +### Quick Setup -```sh -# Get postgres docker image -docker pull postgres - -# Run postgres in docker -docker run --name postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres -``` - -### Setup Indy Ledger - -For testing we've added a setup to this repo that allows you to quickly setup an indy ledger. +To quickly set up all services needed to run tests (Postgres, Hyperledger Indy Ledger, and Cheqd Ledger), run the following command: ```sh -# Build indy pool -docker build -f network/indy-pool.dockerfile -t indy-pool . --platform linux/amd64 - -# NOTE: If you are on an ARM (M1) mac use the `network/indy-pool-arm.dockerfile` instead -# docker build -f network/indy-pool-arm.dockerfile -t indy-pool . --platform linux/arm64/v8 - -# Start indy pool -docker run -d --rm --name indy-pool -p 9701-9708:9701-9708 indy-pool - -# Setup CLI. This creates a wallet, connects to the ledger and sets the Transaction Author Agreement -docker exec indy-pool indy-cli-setup - -# DID and Verkey from seed. Set 'ENDORSER' role in order to be able to register public DIDs -docker exec indy-pool add-did-from-seed 00000000000000000000000Endorser9 ENDORSER - -# DID and Verkey from seed. Set 'Trustee' -docker exec indy-pool add-did-from-seed 000000000000000000000000Trustee9 TRUSTEE - -# If you want to register using the DID/Verkey you can use -# docker exec indy-pool add-did "NkGXDEPgpFGjQKMYmz6SyF" "CrSA1WbYYWLJoHm16Xw1VEeWxFvXtWjtsfEzMsjB5vDT" +docker compose up -d ``` -### Setup Cheqd Ledger - -In addition, there's also a docker command to run a cheqd test network. +If you're running on an ARM based machine (such as Apple Silicon), you can use the `docker-compose.arm.yml` file instead: ```sh -docker run --rm -d -p 26657:26657 ghcr.io/cheqd/cheqd-testnet:latest -``` - -If you want to run tests without the cheqd ledger, you can use the following ignore pattern: - -```sh -yarn test --testPathIgnorePatterns packages/cheqd +docker compose -f docker-compose.arm.yml up -d ``` ### Run all tests @@ -96,34 +44,17 @@ You can run the tests using the following command. yarn test ``` -If you're not using the ledger setup from above, make sure you pass the correct environment variables from [Setting environment variables](#setting-environment-variables) for connecting to the indy **ledger** pool. - -```sh -GENESIS_TXN_PATH=network/genesis/local-genesis.txn TEST_AGENT_PUBLIC_DID_SEED=000000000000000000000000Trustee9 ENDORSER_AGENT_PUBLIC_DID_SEED=00000000000000000000000Endorser9 yarn test -``` - -Locally, you might want to run the tests without postgres tests. You can do that by ignoring the tests: - -```sh -yarn test --testPathIgnorePatterns postgres.e2e.test.ts -``` - -In case you run into trouble running the tests, e.g. complaining about snapshots not being up-to-date, you can try and remove the data stored for the indy-client or Credo. Note this removes all wallets and data, so make sure you're okay with all data being removed. On a Unix system with default setup you achieve this by running: - -```sh -rm -rf ~/.indy-client ~/.afj -``` - -## Usage with Docker - -If you don't want to install the libindy dependencies yourself, or want a clean environment when running the framework or tests you can use docker. - -Make sure you followed the [local ledger setup](#setup-indy-ledger) to setup a local indy pool inside docker. +### Setting environment variables -```sh -# Builds the framework docker image with all dependencies installed -docker build -t credo . +If you're using the setup as described in this document, you don't need to provide any environment variables as the default will be sufficient. -# Run test with ledger pool -docker run -it --rm --network host credo yarn test -``` +- `GENESIS_TXN_PATH`: The path to the genesis transaction that allows us to connect to the indy pool. + - `GENESIS_TXN_PATH=network/genesis/local-genesis.txn` - default. Works with the [ledger setup](#setup-indy-ledger) from the previous step. + - `GENESIS_TXN_PATH=network/genesis/builder-net-genesis.txn` - Sovrin BuilderNet genesis. + - `GENESIS_TXN_PATH=/path/to/any/ledger/you/like` +- `TEST_AGENT_PUBLIC_DID_SEED`: The seed to use for the public DID. This will be used to do public write operations to the ledger. You should use a seed for a DID that is already registered on the ledger. + - If using the local or default genesis, use the same seed you used for the `add-did-from-seed` command from the [ledger setup](#setup-indy-ledger) in the previous step. (default is `000000000000000000000000Trustee9`) + - If using the BuilderNet genesis, make sure your seed is registered on the BuilderNet using [selfserve.sovrin.org](https://selfserve.sovrin.org/) and you have read and accepted the associated [Transaction Author Agreement](https://github.com/sovrin-foundation/sovrin/blob/master/TAA/TAA.md). We are not responsible for any unwanted consequences of using the BuilderNet. +- `ENDORSER_AGENT_PUBLIC_DID_SEED`: The seed to use for the public Endorser DID. This will be used to endorse transactions. You should use a seed for a DID that is already registered on the ledger. + - If using the local or default genesis, use the same seed you used for the `add-did-from-seed` command from the [ledger setup](#setup-indy-ledger) in the previous step. (default is `00000000000000000000000Endorser9`) + - If using the BuilderNet genesis, make sure your seed is registered on the BuilderNet using [selfserve.sovrin.org](https://selfserve.sovrin.org/) and you have read and accepted the associated [Transaction Author Agreement](https://github.com/sovrin-foundation/sovrin/blob/master/TAA/TAA.md). We are not responsible for any unwanted consequences of using the BuilderNet. diff --git a/Dockerfile b/Dockerfile index 5babd3d3da..83236c6e63 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,66 +1,4 @@ -## Stage 1: Build indy-sdk and postgres plugin - -FROM ubuntu:22.04 as base - -# Set this value only during build -ARG DEBIAN_FRONTEND noninteractive - -# Define packages to install -ENV PACKAGES software-properties-common ca-certificates \ - curl build-essential git \ - libzmq3-dev libsodium-dev pkg-config gnupg - -# Combined update and install to ensure Docker caching works correctly -RUN apt-get update -y \ - && apt-get install -y $PACKAGES - -RUN curl http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1-1ubuntu2.1~18.04.23_amd64.deb -o libssl1.1.deb \ - # libssl1.1 (required by libindy) - && dpkg -i libssl1.1.deb \ - && curl http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl-dev_1.1.1-1ubuntu2.1~18.04.23_amd64.deb -o libssl-dev1.1.deb \ - # libssl-dev1.1 (required to compile libindy with posgres plugin) - && dpkg -i libssl-dev1.1.deb - -# Add APT sources -RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 \ - && add-apt-repository "deb https://repo.sovrin.org/sdk/deb bionic stable" \ - && mkdir -p /etc/apt/keyrings \ - && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ - && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \ - && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ - && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list - -# Install libindy, NodeJS and yarn -RUN apt-get update -y \ - # Install libindy - && apt-get install -y --allow-unauthenticated libindy \ - && apt-get install -y nodejs \ - && apt-get install -y --no-install-recommends yarn \ - && rm -rf /var/lib/apt/lists/* \ - && apt-get clean -y - -# postgres plugin setup -# install rust and set up rustup -RUN curl https://sh.rustup.rs -sSf | bash -s -- -y -ENV PATH="/root/.cargo/bin:${PATH}" - -# cargo build failing on latest release of rust due to socket2 dependency in the plugin https://users.rust-lang.org/t/build-broken-with-parse-quote-spanned-is-ambiguous/80280/2 so pointing rust version to 1.63.0 -RUN rustup default 1.63.0 - -# clone indy-sdk and build postgres plugin -RUN git clone https://github.com/hyperledger/indy-sdk.git -WORKDIR /indy-sdk/experimental/plugins/postgres_storage/ -RUN cargo build --release - -# set up library path for postgres plugin -ENV LIB_INDY_STRG_POSTGRES="/indy-sdk/experimental/plugins/postgres_storage/target/release" - -## Stage 2: Build Credo - -FROM base as final - -# Set environment variables -ENV RUN_MODE="docker" +FROM node:18 # Set working directory WORKDIR /www @@ -71,3 +9,5 @@ COPY . . # Run yarn install and build RUN yarn install --frozen-lockfile \ && yarn build + +entrypoint ["yarn", "run-mediator"] \ No newline at end of file diff --git a/README.md b/README.md index 9a8c903932..0630435a30 100644 --- a/README.md +++ b/README.md @@ -90,15 +90,7 @@ Credo is a framework written in TypeScript for building **SSI Agents and DIDComm - @credo-ts/indy-sdk - - - @credo-ts/indy-sdk version - - - - - @credo-ts/indy-vdr + @aries-framework/indy-vdr @credo-ts/indy-vdr version @@ -168,6 +160,14 @@ Credo is a framework written in TypeScript for building **SSI Agents and DIDComm + + ~~@aries-framework/indy-sdk~~ (deprecated, unmaintained after 0.4.x) + + + @aries-framework/indy-sdk version + + + ## Getting Started diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md deleted file mode 100644 index b34c8a40f9..0000000000 --- a/TROUBLESHOOTING.md +++ /dev/null @@ -1,85 +0,0 @@ -# Troubleshooting - -This document contains the most common errors that arise when first installing libindy and Credo. If you encounter a problem that is not listed here and manage to fix it, please open a PR describing the steps taken to resolve the issue. - -- [macOS](#macos) - - [Unable to find `libindy.dylib`](#unable-to-find-libindydylib) - - [Unable to find `libssl.1.0.0.dylib`](#unable-to-find-libssl100dylib) - - [Library not loaded: `libsodium.18.dylib`](#library-not-loaded-libsodium18dylib) - -## macOS - -### Unable to find `libindy.dylib` - -Installing Libindy on macOS can be tricky. If the the troubleshooting section of the NodeJS Wrapper documentation doesn't provide an answer and you're getting the following error: - -``` -dlopen(//credo/node_modules/indy-sdk/build/Release/indynodejs.node, 1): Library not loaded: /Users/jenkins/workspace/indy-sdk_indy-sdk-cd_master/libindy/target/release/deps/libindy.dylib - Referenced from: //credo/node_modules/indy-sdk/build/Release/indynodejs.node - Reason: image not found -``` - -See this StackOverflow answer: https://stackoverflow.com/questions/19776571/error-dlopen-library-not-loaded-reason-image-not-found - -The NodeJS Wrapper tries to find the library at the hardcoded CI built path `/Users/jenkins/workspace/indy-sdk_indy-sdk-cd_master/libindy/target/release/deps/libindy.dylib`. However the library will probably be located at `/usr/local/lib/libindy.dylib` (depending on how you installed libindy). - -To check where the NodeJS wrapper points to the static CI build path you can run: - -```bash -$ otool -L node_modules/indy-sdk/build/Release/indynodejs.node -node_modules/indy-sdk/build/Release/indynodejs.node: - /Users/jenkins/workspace/indy-sdk_indy-sdk-cd_master/libindy/target/release/deps/libindy.dylib (compatibility version 0.0.0, current version 0.0.0) - /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 902.1.0) - /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1) -``` - -You can manually change the path using the `install_name_tool`. Be sure change the path if you're not using the default. - -```bash -install_name_tool -change /Users/jenkins/workspace/indy-sdk_indy-sdk-cd_master/libindy/target/release/deps/libindy.dylib /usr/local/lib/libindy.dylib node_modules/indy-sdk/build/Release/indynodejs.node -``` - -### Unable to find `libssl.1.0.0.dylib` - -Libindy makes use of OpenSSL 1.0, however macOS by default has OpenSSL version 1.1. The standard brew repo also doesn't contain version 1.0 anymore. So if you're getting something that looks like the following error: - -``` -dlopen(//credo/node_modules/indy-sdk/build/Release/indynodejs.node, 1): Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib - Referenced from: //libindy_1.15.0/lib/libindy.dylib - Reason: image not found -``` - -You can manually install OpenSSL 1.0 with the following Brew command: - -```sh -brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/64555220bfbf4a25598523c2e4d3a232560eaad7/Formula/openssl.rb -f -``` - -In newer versions of HomeBrew installing packages is disabled, which will give an error that looks something like this: - -``` -Error: Calling Installation of openssl from a GitHub commit URL is disabled! Use 'brew extract openssl' to stable tap on GitHub instead. -``` - -They advise to use `brew extract` which also gives errors. The easiest way is to download the file and then extract it: - -```sh -curl https://raw.githubusercontent.com/Homebrew/homebrew-core/64555220bfbf4a25598523c2e4d3a232560eaad7/Formula/openssl.rb -o openssl.rb -brew install openssl.rb -``` - -### Library not loaded: `libsodium.18.dylib` - -When you install `libsodium` it automatically installs version 23. However libindy needs version 18. So if you're getting something that looks like the following error: - -``` -dyld: Library not loaded: /usr/local/opt/libsodium/lib/libsodium.18.dylib -``` - -You can manually link the path for version 18 to the path of version 23 with the following command: - -```sh -ln -s /usr/local/opt/libsodium/lib/libsodium.23.dylib /usr/local/opt/libsodium/lib/libsodium.18.dylib -``` - -Inspired by [this answer](https://github.com/Homebrew/homebrew-php/issues/4589) to the same error using php71-libsodium. diff --git a/demo/package.json b/demo/package.json index 0b3188e5b8..36028b4eef 100644 --- a/demo/package.json +++ b/demo/package.json @@ -24,12 +24,10 @@ "@credo-ts/anoncreds-rs": "*", "@credo-ts/askar": "*", "@credo-ts/core": "*", - "@credo-ts/indy-sdk": "*", "@credo-ts/indy-vdr": "*", "@credo-ts/cheqd": "*", "@credo-ts/node": "*", "@types/figlet": "^1.5.4", - "@types/indy-sdk": "^1.16.26", "@types/inquirer": "^8.2.6", "clear": "^0.1.0", "figlet": "^1.5.2", diff --git a/demo/src/Alice.ts b/demo/src/Alice.ts index f5fa2e48ce..ca50a0f50a 100644 --- a/demo/src/Alice.ts +++ b/demo/src/Alice.ts @@ -8,7 +8,7 @@ export class Alice extends BaseAgent { public connectionRecordFaberId?: string public constructor(port: number, name: string) { - super({ port, name, useLegacyIndySdk: true }) + super({ port, name }) this.connected = false } diff --git a/demo/src/BaseAgent.ts b/demo/src/BaseAgent.ts index 3be75da659..abd249fb6c 100644 --- a/demo/src/BaseAgent.ts +++ b/demo/src/BaseAgent.ts @@ -1,5 +1,4 @@ import type { InitConfig } from '@credo-ts/core' -import type { IndySdkPoolConfig } from '@credo-ts/indy-sdk' import type { IndyVdrPoolConfig } from '@credo-ts/indy-vdr' import { @@ -32,14 +31,11 @@ import { Agent, HttpOutboundTransport, } from '@credo-ts/core' -import { IndySdkAnonCredsRegistry, IndySdkModule, IndySdkSovDidResolver } from '@credo-ts/indy-sdk' import { IndyVdrIndyDidResolver, IndyVdrAnonCredsRegistry, IndyVdrModule } from '@credo-ts/indy-vdr' import { agentDependencies, HttpInboundTransport } from '@credo-ts/node' import { anoncreds } from '@hyperledger/anoncreds-nodejs' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { indyVdr } from '@hyperledger/indy-vdr-nodejs' -import { randomUUID } from 'crypto' -import indySdk from 'indy-sdk' import { greenText } from './OutputClass' @@ -49,13 +45,11 @@ const bcovrin = `{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node1","blsk {"reqSignature":{},"txn":{"data":{"data":{"alias":"Node4","blskey":"2zN3bHM1m4rLz54MJHYSwvqzPchYp8jkHswveCLAEJVcX6Mm1wHQD1SkPYMzUDTZvWvhuE6VNAkK3KxVeEmsanSmvjVkReDeBEMxeDaayjcZjFGPydyey1qxBHmTvAnBKoPydvuTAqx5f7YNNRAdeLmUi99gERUU7TD8KfAa6MpQ9bw","blskey_pop":"RPLagxaR5xdimFzwmzYnz4ZhWtYQEj8iR5ZU53T2gitPCyCHQneUn2Huc4oeLd2B2HzkGnjAff4hWTJT6C7qHYB1Mv2wU5iHHGFWkhnTX9WsEAbunJCV2qcaXScKj4tTfvdDKfLiVuU2av6hbsMztirRze7LvYBkRHV3tGwyCptsrP","client_ip":"138.197.138.255","client_port":9708,"node_ip":"138.197.138.255","node_port":9707,"services":["VALIDATOR"]},"dest":"4PS3EDQ3dW1tci1Bp6543CfuuebjFrg36kLAUcskGfaA"},"metadata":{"from":"TWwCRQRZ2ZHMJFn9TzLp7W"},"type":"0"},"txnMetadata":{"seqNo":4,"txnId":"aa5e817d7cc626170eca175822029339a444eb0ee8f0bd20d3b0b76e566fb008"},"ver":"1"}` export const indyNetworkConfig = { - // Need unique network id as we will have multiple agent processes in the agent - id: randomUUID(), genesisTransactions: bcovrin, indyNamespace: 'bcovrin:test', isProduction: false, connectOnStartup: true, -} satisfies IndySdkPoolConfig | IndyVdrPoolConfig +} satisfies IndyVdrPoolConfig type DemoAgent = Agent> @@ -64,17 +58,8 @@ export class BaseAgent { public name: string public config: InitConfig public agent: DemoAgent - public useLegacyIndySdk: boolean - public constructor({ - port, - name, - useLegacyIndySdk = false, - }: { - port: number - name: string - useLegacyIndySdk?: boolean - }) { + public constructor({ port, name }: { port: number; name: string }) { this.name = name this.port = port @@ -89,8 +74,6 @@ export class BaseAgent { this.config = config - this.useLegacyIndySdk = useLegacyIndySdk - this.agent = new Agent({ config, dependencies: agentDependencies, @@ -167,46 +150,3 @@ function getAskarAnonCredsIndyModules() { }), } as const } - -function getLegacyIndySdkModules() { - const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService() - const legacyIndyProofFormatService = new LegacyIndyProofFormatService() - - return { - connections: new ConnectionsModule({ - autoAcceptConnections: true, - }), - credentials: new CredentialsModule({ - autoAcceptCredentials: AutoAcceptCredential.ContentApproved, - credentialProtocols: [ - new V1CredentialProtocol({ - indyCredentialFormat: legacyIndyCredentialFormatService, - }), - new V2CredentialProtocol({ - credentialFormats: [legacyIndyCredentialFormatService], - }), - ], - }), - proofs: new ProofsModule({ - autoAcceptProofs: AutoAcceptProof.ContentApproved, - proofProtocols: [ - new V1ProofProtocol({ - indyProofFormat: legacyIndyProofFormatService, - }), - new V2ProofProtocol({ - proofFormats: [legacyIndyProofFormatService], - }), - ], - }), - anoncreds: new AnonCredsModule({ - registries: [new IndySdkAnonCredsRegistry()], - }), - indySdk: new IndySdkModule({ - indySdk, - networks: [indyNetworkConfig], - }), - dids: new DidsModule({ - resolvers: [new IndySdkSovDidResolver()], - }), - } as const -} diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts index d9ee218209..5f3542b803 100644 --- a/demo/src/Faber.ts +++ b/demo/src/Faber.ts @@ -21,7 +21,7 @@ export class Faber extends BaseAgent { public ui: BottomBar public constructor(port: number, name: string) { - super({ port, name, useLegacyIndySdk: true }) + super({ port, name }) this.ui = new ui.BottomBar() } diff --git a/docker-compose.arm.yml b/docker-compose.arm.yml new file mode 100644 index 0000000000..263f77baf2 --- /dev/null +++ b/docker-compose.arm.yml @@ -0,0 +1,32 @@ +version: '3' +services: + postgres: + image: postgres:15-alpine + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - '5432:5432' + + indy-pool: + build: + context: . + dockerfile: network/indy-pool-arm.dockerfile + platform: linux/arm64/v8 + ports: + - '9701-9708:9701-9708' + # Start supervisord in bg, run commands, bring supervisor to fg + command: > + /bin/bash -c " + /usr/bin/supervisord & + indy-cli-setup && + add-did-from-seed 00000000000000000000000Endorser9 ENDORSER && + add-did-from-seed 000000000000000000000000Trustee9 TRUSTEE && + /usr/bin/supervisord -n + " + + cheqd-ledger: + image: ghcr.io/cheqd/cheqd-testnet:latest + platform: linux/amd64 + ports: + - '26657:26657' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..f7dea20d37 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +version: '3' +services: + postgres: + image: postgres:15-alpine + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - '5432:5432' + + indy-pool: + build: + context: . + dockerfile: network/indy-pool.dockerfile + ports: + - '9701-9708:9701-9708' + # Start supervisord in bg, run commands, bring supervisor to fg + command: > + /bin/bash -c " + /usr/bin/supervisord & + indy-cli-setup && + add-did-from-seed 00000000000000000000000Endorser9 ENDORSER && + add-did-from-seed 000000000000000000000000Trustee9 TRUSTEE && + /usr/bin/supervisord -n + " + + cheqd-ledger: + image: ghcr.io/cheqd/cheqd-testnet:latest + platform: linux/amd64 + ports: + - '26657:26657' diff --git a/docker/docker-compose-mediators-ngrok.yml b/docker/docker-compose-mediators-ngrok.yml deleted file mode 100644 index 36c5899b36..0000000000 --- a/docker/docker-compose-mediators-ngrok.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: '3' - -# This file extends docker-compose-mediators.yml - -services: - mediator: - environment: - NGROK_NAME: mediator-ngrok - entrypoint: ./scripts/ngrok-wait.sh - depends_on: [mediator-ngrok] - - mediator-ngrok: - image: wernight/ngrok - command: ngrok http -bind-tls=true --log stdout mediator:3001 - networks: - - hyperledger - -networks: - hyperledger: diff --git a/docker/docker-compose-mediators.yml b/docker/docker-compose-mediators.yml deleted file mode 100644 index 372d682563..0000000000 --- a/docker/docker-compose-mediators.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: '3' - -services: - mediator: - build: .. - image: credo - container_name: credo-mediator - command: yarn run-mediator - platform: linux/amd64 - networks: - - hyperledger - ports: - - 3001:3001 - -networks: - hyperledger: diff --git a/package.json b/package.json index 1568d37c90..30e910f3ba 100644 --- a/package.json +++ b/package.json @@ -28,16 +28,18 @@ }, "devDependencies": { "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", + "@types/bn.js": "^5.1.5", "@types/cors": "^2.8.10", "@types/eslint": "^8.21.2", "@types/express": "^4.17.13", - "@types/jest": "^29.5.5", + "@types/jest": "^29.5.11", "@types/node": "^18.18.8", "@types/uuid": "^9.0.1", "@types/varint": "^6.0.0", "@types/ws": "^8.5.4", "@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/parser": "^5.48.1", + "bn.js": "^5.2.1", "conventional-changelog-conventionalcommits": "^5.0.0", "conventional-recommended-bump": "^6.1.0", "cors": "^2.8.5", @@ -48,12 +50,11 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-workspaces": "^0.8.0", "express": "^4.17.1", - "indy-sdk": "^1.16.0-dev-1655", "jest": "^29.7.0", "lerna": "^6.5.1", "prettier": "^2.3.1", "rxjs": "^7.8.0", - "ts-jest": "^29.0.5", + "ts-jest": "^29.1.2", "ts-node": "^10.0.0", "tsconfig-paths": "^4.1.2", "tsyringe": "^4.8.0", diff --git a/packages/action-menu/tests/action-menu.e2e.test.ts b/packages/action-menu/tests/action-menu.e2e.test.ts index 425a3f15ee..134887830c 100644 --- a/packages/action-menu/tests/action-menu.e2e.test.ts +++ b/packages/action-menu/tests/action-menu.e2e.test.ts @@ -2,8 +2,7 @@ import type { ConnectionRecord } from '@credo-ts/core' import { Agent } from '@credo-ts/core' -import { getAgentOptions, makeConnection, testLogger, setupSubjectTransports, indySdk } from '../../core/tests' -import { IndySdkModule } from '../../indy-sdk/src' +import { makeConnection, testLogger, setupSubjectTransports, getInMemoryAgentOptions } from '../../core/tests' import { waitForActionMenuRecord } from './helpers' @@ -11,12 +10,9 @@ import { ActionMenu, ActionMenuModule, ActionMenuRecord, ActionMenuRole, ActionM const modules = { actionMenu: new ActionMenuModule(), - indySdk: new IndySdkModule({ - indySdk, - }), } -const faberAgentOptions = getAgentOptions( +const faberAgentOptions = getInMemoryAgentOptions( 'Faber Action Menu', { endpoints: ['rxjs:faber'], @@ -24,7 +20,7 @@ const faberAgentOptions = getAgentOptions( modules ) -const aliceAgentOptions = getAgentOptions( +const aliceAgentOptions = getInMemoryAgentOptions( 'Alice Action Menu', { endpoints: ['rxjs:alice'], diff --git a/packages/anoncreds-rs/src/AnonCredsRsModule.ts b/packages/anoncreds-rs/src/AnonCredsRsModule.ts index d2abdbcb73..5e78a2238f 100644 --- a/packages/anoncreds-rs/src/AnonCredsRsModule.ts +++ b/packages/anoncreds-rs/src/AnonCredsRsModule.ts @@ -23,7 +23,7 @@ export class AnonCredsRsModule implements Module { dependencyManager .resolve(AgentConfig) .logger.warn( - "The '@credo-ts/anoncreds-rs' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + "The '@credo-ts/anoncreds-rs' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." ) dependencyManager.registerInstance(AnonCredsRsModuleConfig, this.config) diff --git a/packages/anoncreds-rs/src/index.ts b/packages/anoncreds-rs/src/index.ts index 5fdd9486c7..952884c8c5 100644 --- a/packages/anoncreds-rs/src/index.ts +++ b/packages/anoncreds-rs/src/index.ts @@ -3,3 +3,4 @@ export * from './services' // Module export { AnonCredsRsModule } from './AnonCredsRsModule' +export { AnonCredsRsModuleConfig } from './AnonCredsRsModuleConfig' diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts index 1408aa9ae8..bd5643f7c3 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts @@ -317,8 +317,8 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { credentialDefinitionId: credentialRecord.credential.cred_def_id, credentialId: credentialRecord.credentialId, schemaId: credentialRecord.credential.schema_id, - credentialRevocationId: credentialRecord.credentialRevocationId, - revocationRegistryId: credentialRecord.credential.rev_reg_id, + credentialRevocationId: credentialRecord.credentialRevocationId ?? null, + revocationRegistryId: credentialRecord.credential.rev_reg_id ?? null, methodName: credentialRecord.methodName, } } @@ -346,8 +346,8 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { credentialDefinitionId: credentialRecord.credential.cred_def_id, credentialId: credentialRecord.credentialId, schemaId: credentialRecord.credential.schema_id, - credentialRevocationId: credentialRecord.credentialRevocationId, - revocationRegistryId: credentialRecord.credential.rev_reg_id, + credentialRevocationId: credentialRecord.credentialRevocationId ?? null, + revocationRegistryId: credentialRecord.credential.rev_reg_id ?? null, methodName: credentialRecord.methodName, })) } @@ -412,8 +412,8 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { credentialDefinitionId: credentialRecord.credential.cred_def_id, credentialId: credentialRecord.credentialId, schemaId: credentialRecord.credential.schema_id, - credentialRevocationId: credentialRecord.credentialRevocationId, - revocationRegistryId: credentialRecord.credential.rev_reg_id, + credentialRevocationId: credentialRecord.credentialRevocationId ?? null, + revocationRegistryId: credentialRecord.credential.rev_reg_id ?? null, methodName: credentialRecord.methodName, }, interval: proofRequest.non_revoked, diff --git a/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts index 5f86d08b46..f23ff1f230 100644 --- a/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts +++ b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts @@ -25,6 +25,7 @@ import { anoncreds } from '@hyperledger/anoncreds-nodejs' import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' +import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' import { encodeCredentialValue } from '../../../../anoncreds/src/utils/credential' import { InMemoryAnonCredsRegistry } from '../../../../anoncreds/tests/InMemoryAnonCredsRegistry' import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' @@ -37,9 +38,11 @@ const anonCredsVerifierService = new AnonCredsRsVerifierService() const anonCredsHolderService = new AnonCredsRsHolderService() const anonCredsIssuerService = new AnonCredsRsIssuerService() const storageService = new InMemoryStorageService() +const wallet = new InMemoryWallet() const registry = new InMemoryAnonCredsRegistry() const agentContext = getAgentContext({ + wallet, registerInstances: [ [InjectionSymbols.Stop$, new Subject()], [InjectionSymbols.AgentDependencies, agentDependencies], @@ -190,7 +193,7 @@ describe('AnonCredsRsServices', () => { schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, revocationRegistryId: null, - credentialRevocationId: undefined, // Should it be null in this case? + credentialRevocationId: null, methodName: 'inMemory', }) @@ -398,7 +401,7 @@ describe('AnonCredsRsServices', () => { schemaId: unqualifiedSchemaId, credentialDefinitionId: unqualifiedCredentialDefinitionId, revocationRegistryId: null, - credentialRevocationId: undefined, // Should it be null in this case? + credentialRevocationId: null, methodName: 'inMemory', }) diff --git a/packages/anoncreds-rs/src/services/__tests__/helpers.ts b/packages/anoncreds-rs/src/services/__tests__/helpers.ts index 16b8fb1a79..e1eaffb115 100644 --- a/packages/anoncreds-rs/src/services/__tests__/helpers.ts +++ b/packages/anoncreds-rs/src/services/__tests__/helpers.ts @@ -160,6 +160,8 @@ export function createCredentialForHolder(options: { credentialId, schemaId, methodName: 'inMemory', + credentialRevocationId: null, + revocationRegistryId: null, } const returnObj = { credential: credentialObj.toJson() as unknown as AnonCredsCredential, diff --git a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts index 288c610110..4f4dece3c0 100644 --- a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts +++ b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts @@ -83,7 +83,7 @@ const indyDid = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' describe('AnonCreds format services using anoncreds-rs', () => { afterEach(() => { - inMemoryStorageService.records = {} + inMemoryStorageService.contextCorrelationIdToRecords = {} }) test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => { @@ -355,7 +355,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean schemaId: schemaState.schemaId, credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, revocationRegistryId: revocable ? revocationRegistryDefinitionId : null, - credentialRevocationId: revocable ? '1' : undefined, + credentialRevocationId: revocable ? '1' : null, methodName: 'inMemory', }) diff --git a/packages/anoncreds-rs/tests/anoncredsSetup.ts b/packages/anoncreds-rs/tests/anoncredsSetup.ts index e82b043351..aff1d1ccca 100644 --- a/packages/anoncreds-rs/tests/anoncredsSetup.ts +++ b/packages/anoncreds-rs/tests/anoncredsSetup.ts @@ -37,12 +37,10 @@ import { randomUUID } from 'crypto' import { AnonCredsCredentialFormatService, AnonCredsProofFormatService, AnonCredsModule } from '../../anoncreds/src' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' -import { AskarModule } from '../../askar/src' -import { askarModuleConfig } from '../../askar/tests/helpers' import { sleep } from '../../core/src/utils/sleep' import { setupSubjectTransports, setupEventReplaySubjects } from '../../core/tests' import { - getAgentOptions, + getInMemoryAgentOptions, makeConnection, waitForCredentialRecordSubject, waitForProofExchangeRecordSubject, @@ -55,6 +53,7 @@ import { LocalDidResolver } from './LocalDidResolver' // Helper type to get the type of the agents (with the custom modules) for the credential tests export type AnonCredsTestsAgent = Agent< + // eslint-disable-next-line @typescript-eslint/no-explicit-any ReturnType & { mediationRecipient?: any; mediator?: any } > @@ -97,7 +96,6 @@ export const getAnonCredsModules = ({ dids: new DidsModule({ resolvers: [new LocalDidResolver()], }), - askar: new AskarModule(askarModuleConfig), cache: new CacheModule({ cache: new InMemoryLruCache({ limit: 100 }), }), @@ -201,7 +199,7 @@ export async function issueAnonCredsCredential({ holderReplay: EventReplaySubject issuerHolderConnectionId: string - revocationRegistryDefinitionId?: string + revocationRegistryDefinitionId: string | null offer: AnonCredsOfferCredentialFormat }) { let issuerCredentialExchangeRecord = await issuerAgent.credentials.offerCredential({ @@ -209,7 +207,11 @@ export async function issueAnonCredsCredential({ connectionId: issuerHolderConnectionId, protocolVersion: 'v2', credentialFormats: { - anoncreds: { ...offer, revocationRegistryDefinitionId, revocationRegistryIndex: 1 }, + anoncreds: { + ...offer, + revocationRegistryDefinitionId: revocationRegistryDefinitionId ?? undefined, + revocationRegistryIndex: 1, + }, }, autoAcceptCredential: AutoAcceptCredential.ContentApproved, }) @@ -267,7 +269,7 @@ interface SetupAnonCredsTestsReturn> { const issuerAgent = new Agent( - getAgentOptions( + getInMemoryAgentOptions( issuerName, { endpoints: ['rxjs:issuer'], @@ -312,7 +314,7 @@ export async function setupAnonCredsTests< ) const holderAgent = new Agent( - getAgentOptions( + getInMemoryAgentOptions( holderName, { endpoints: ['rxjs:holder'], @@ -327,7 +329,7 @@ export async function setupAnonCredsTests< const verifierAgent = verifierName ? new Agent( - getAgentOptions( + getInMemoryAgentOptions( verifierName, { endpoints: ['rxjs:verifier'], diff --git a/packages/anoncreds-rs/tests/indy-flow.test.ts b/packages/anoncreds-rs/tests/indy-flow.test.ts index a8f0c65966..d080620255 100644 --- a/packages/anoncreds-rs/tests/indy-flow.test.ts +++ b/packages/anoncreds-rs/tests/indy-flow.test.ts @@ -285,7 +285,7 @@ describe('Legacy indy format services using anoncreds-rs', () => { schemaId: unqualifiedSchemaId, credentialDefinitionId: unqualifiedCredentialDefinitionId, revocationRegistryId: null, - credentialRevocationId: undefined, // FIXME: should be null? + credentialRevocationId: null, methodName: 'inMemory', }) diff --git a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts index ff9381c0b1..ee335955ea 100644 --- a/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts +++ b/packages/anoncreds-rs/tests/v2-credential-revocation.e2e.test.ts @@ -28,7 +28,7 @@ describe('IC v2 credential revocation', () => { let faberAgent: AnonCredsTestsAgent let aliceAgent: AnonCredsTestsAgent let credentialDefinitionId: string - let revocationRegistryDefinitionId: string | undefined + let revocationRegistryDefinitionId: string | null let aliceConnectionId: string let faberReplay: EventReplaySubject @@ -105,7 +105,7 @@ describe('IC v2 credential revocation', () => { anoncreds: { credentialDefinitionId: credentialDefinitionId, attributes: credentialPreview.attributes, - revocationRegistryDefinitionId, + revocationRegistryDefinitionId: revocationRegistryDefinitionId ?? undefined, revocationRegistryIndex: 1, }, }, diff --git a/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts b/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts index 6739004d50..ba0e70a692 100644 --- a/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts +++ b/packages/anoncreds-rs/tests/v2-credentials.e2e.test.ts @@ -239,6 +239,7 @@ describe('IC V2 AnonCreds credentials', () => { credentialDefinitionId: credentialDefinitionId, attributes: credentialPreview.attributes, }, + revocationRegistryDefinitionId: null, }) // test that delete credential removes from both repository and wallet diff --git a/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts b/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts index 97e5338765..aff906251b 100644 --- a/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts +++ b/packages/anoncreds-rs/tests/v2-proofs.e2e.test.ts @@ -28,7 +28,7 @@ describe('PP V2 AnonCreds Proofs', () => { let aliceAgent: AnonCredsTestsAgent let aliceReplay: EventReplaySubject let credentialDefinitionId: string - let revocationRegistryDefinitionId: string | undefined + let revocationRegistryDefinitionId: string | null let aliceConnectionId: string let faberConnectionId: string let faberProofExchangeRecord: ProofExchangeRecord diff --git a/packages/anoncreds/package.json b/packages/anoncreds/package.json index be387c9823..3112fa7f1a 100644 --- a/packages/anoncreds/package.json +++ b/packages/anoncreds/package.json @@ -32,7 +32,6 @@ }, "devDependencies": { "@credo-ts/node": "0.4.2", - "indy-sdk": "^1.16.0-dev-1636", "rimraf": "^4.4.0", "rxjs": "^7.8.0", "typescript": "~4.9.5" diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index d0b0cc1d6b..6f729de76f 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -278,6 +278,8 @@ export class AnonCredsApi { supportRevocation: options.options.supportRevocation, schema: schemaResult.schema, }, + // NOTE: indy-sdk support has been removed from main repo, but keeping + // this in place to allow the indy-sdk to still be used as a custom package for some time // FIXME: Indy SDK requires the schema seq no to be passed in here. This is not ideal. { indyLedgerSchemaSeqNo: schemaResult.schemaMetadata.indyLedgerSeqNo, diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index 534c1956cb..a63a6bd0a2 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -468,8 +468,8 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService const credential = await anonCredsHolderService.getCredential(agentContext, { credentialId }) credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, { - credentialRevocationId: credential.credentialRevocationId, - revocationRegistryId: credential.revocationRegistryId, + credentialRevocationId: credential.credentialRevocationId ?? undefined, + revocationRegistryId: credential.revocationRegistryId ?? undefined, }) credentialRecord.setTags({ anonCredsRevocationRegistryId: credential.revocationRegistryId, diff --git a/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts index 5ed540cfb8..dd53ce16c3 100644 --- a/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts @@ -434,8 +434,8 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic const credential = await anonCredsHolderService.getCredential(agentContext, { credentialId }) credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, { - credentialRevocationId: credential.credentialRevocationId, - revocationRegistryId: credential.revocationRegistryId, + credentialRevocationId: credential.credentialRevocationId ?? undefined, + revocationRegistryId: credential.revocationRegistryId ?? undefined, }) credentialRecord.setTags({ anonCredsRevocationRegistryId: credential.revocationRegistryId, diff --git a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts index d3047c7403..ecda92dcb8 100644 --- a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts +++ b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts @@ -3,30 +3,39 @@ import type { AnonCredsCredentialRequest } from '../../models' import { CredentialState, CredentialExchangeRecord, - SigningProviderRegistry, KeyType, CredentialPreviewAttribute, ProofExchangeRecord, ProofState, EventEmitter, + InjectionSymbols, } from '@credo-ts/core' -import * as indySdk from 'indy-sdk' import { Subject } from 'rxjs' -import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' +import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' +import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' import { - IndySdkHolderService, - IndySdkIssuerService, - IndySdkModuleConfig, - IndySdkStorageService, - IndySdkVerifierService, - IndySdkWallet, -} from '../../../../indy-sdk/src' -import { IndySdkRevocationService } from '../../../../indy-sdk/src/anoncreds/services/IndySdkRevocationService' -import { legacyIndyDidFromPublicKeyBase58 } from '../../../../indy-sdk/src/utils/did' + AnonCredsRsHolderService, + AnonCredsRsIssuerService, + AnonCredsRsModuleConfig, + AnonCredsRsVerifierService, +} from '../../../../anoncreds-rs/src' +import { anoncreds } from '../../../../anoncreds-rs/tests/helpers' +import { indyDidFromPublicKeyBase58 } from '../../../../core/src/utils/did' +import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' import { InMemoryAnonCredsRegistry } from '../../../tests/InMemoryAnonCredsRegistry' import { AnonCredsModuleConfig } from '../../AnonCredsModuleConfig' -import { AnonCredsLinkSecretRecord, AnonCredsLinkSecretRepository } from '../../repository' +import { + AnonCredsCredentialDefinitionPrivateRecord, + AnonCredsCredentialDefinitionPrivateRepository, + AnonCredsCredentialDefinitionRecord, + AnonCredsCredentialDefinitionRepository, + AnonCredsCredentialRepository, + AnonCredsKeyCorrectnessProofRecord, + AnonCredsKeyCorrectnessProofRepository, + AnonCredsLinkSecretRecord, + AnonCredsLinkSecretRepository, +} from '../../repository' import { AnonCredsHolderServiceSymbol, AnonCredsIssuerServiceSymbol, @@ -48,14 +57,24 @@ const anonCredsModuleConfig = new AnonCredsModuleConfig({ }) const agentConfig = getAgentConfig('LegacyIndyFormatServicesTest') -const anonCredsRevocationService = new IndySdkRevocationService(indySdk) -const anonCredsVerifierService = new IndySdkVerifierService(indySdk) -const anonCredsHolderService = new IndySdkHolderService(anonCredsRevocationService, indySdk) -const anonCredsIssuerService = new IndySdkIssuerService(indySdk) -const wallet = new IndySdkWallet(indySdk, agentConfig.logger, new SigningProviderRegistry([])) -const storageService = new IndySdkStorageService(indySdk) +const anonCredsVerifierService = new AnonCredsRsVerifierService() +const anonCredsHolderService = new AnonCredsRsHolderService() +const anonCredsIssuerService = new AnonCredsRsIssuerService() +const wallet = new InMemoryWallet() +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const storageService = new InMemoryStorageService() const eventEmitter = new EventEmitter(agentDependencies, new Subject()) const anonCredsLinkSecretRepository = new AnonCredsLinkSecretRepository(storageService, eventEmitter) +const anonCredsCredentialDefinitionRepository = new AnonCredsCredentialDefinitionRepository( + storageService, + eventEmitter +) +const anonCredsCredentialDefinitionPrivateRepository = new AnonCredsCredentialDefinitionPrivateRepository( + storageService, + eventEmitter +) +const anonCredsCredentialRepository = new AnonCredsCredentialRepository(storageService, eventEmitter) +const anonCredsKeyCorrectnessProofRepository = new AnonCredsKeyCorrectnessProofRepository(storageService, eventEmitter) const agentContext = getAgentContext({ registerInstances: [ [AnonCredsIssuerServiceSymbol, anonCredsIssuerService], @@ -64,7 +83,18 @@ const agentContext = getAgentContext({ [AnonCredsRegistryService, new AnonCredsRegistryService()], [AnonCredsModuleConfig, anonCredsModuleConfig], [AnonCredsLinkSecretRepository, anonCredsLinkSecretRepository], - [IndySdkModuleConfig, new IndySdkModuleConfig({ indySdk, autoCreateLinkSecret: false })], + [AnonCredsCredentialDefinitionRepository, anonCredsCredentialDefinitionRepository], + [AnonCredsCredentialDefinitionPrivateRepository, anonCredsCredentialDefinitionPrivateRepository], + [AnonCredsCredentialRepository, anonCredsCredentialRepository], + [AnonCredsKeyCorrectnessProofRepository, anonCredsKeyCorrectnessProofRepository], + [InjectionSymbols.StorageService, storageService], + [ + AnonCredsRsModuleConfig, + new AnonCredsRsModuleConfig({ + anoncreds, + autoCreateLinkSecret: false, + }), + ], ], agentConfig, wallet, @@ -87,15 +117,16 @@ describe('Legacy indy format services', () => { test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => { // This is just so we don't have to register an actual indy did (as we don't have the indy did registrar configured) const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) - const unqualifiedIndyDid = legacyIndyDidFromPublicKeyBase58(key.publicKeyBase58) + const unqualifiedIndyDid = indyDidFromPublicKeyBase58(key.publicKeyBase58) const indyDid = `did:indy:pool1:${unqualifiedIndyDid}` // Create link secret - await anonCredsHolderService.createLinkSecret(agentContext, { + const { linkSecretValue } = await anonCredsHolderService.createLinkSecret(agentContext, { linkSecretId: 'link-secret-id', }) const anonCredsLinkSecret = new AnonCredsLinkSecretRecord({ linkSecretId: 'link-secret-id', + value: linkSecretValue, }) anonCredsLinkSecret.setTag('isDefault', true) await anonCredsLinkSecretRepository.save(agentContext, anonCredsLinkSecret) @@ -107,25 +138,19 @@ describe('Legacy indy format services', () => { version: '1.0.0', }) - const { schemaState, schemaMetadata } = await registry.registerSchema(agentContext, { + const { schemaState } = await registry.registerSchema(agentContext, { schema, options: {}, }) - const { credentialDefinition } = await anonCredsIssuerService.createCredentialDefinition( - agentContext, - { + const { credentialDefinition, credentialDefinitionPrivate, keyCorrectnessProof } = + await anonCredsIssuerService.createCredentialDefinition(agentContext, { issuerId: indyDid, schemaId: schemaState.schemaId as string, schema, tag: 'Employee Credential', supportRevocation: false, - }, - { - // Need to pass this as the indy-sdk MUST have the seqNo - indyLedgerSchemaSeqNo: schemaMetadata.indyLedgerSeqNo as number, - } - ) + }) const { credentialDefinitionState } = await registry.registerCredentialDefinition(agentContext, { credentialDefinition, @@ -141,6 +166,35 @@ describe('Legacy indy format services', () => { throw new Error('Failed to create schema or credential definition') } + await anonCredsCredentialDefinitionRepository.save( + agentContext, + new AnonCredsCredentialDefinitionRecord({ + credentialDefinition, + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + methodName: 'indy', + }) + ) + + if (!keyCorrectnessProof || !credentialDefinitionPrivate) { + throw new Error('Failed to create credential definition private or key correctness proof') + } + + await anonCredsKeyCorrectnessProofRepository.save( + agentContext, + new AnonCredsKeyCorrectnessProofRecord({ + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + value: keyCorrectnessProof, + }) + ) + + await anonCredsCredentialDefinitionPrivateRepository.save( + agentContext, + new AnonCredsCredentialDefinitionPrivateRecord({ + credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + value: credentialDefinitionPrivate, + }) + ) + const holderCredentialRecord = new CredentialExchangeRecord({ protocolVersion: 'v1', state: CredentialState.ProposalSent, @@ -248,7 +302,7 @@ describe('Legacy indy format services', () => { credentialDefinitionId: legacyCredentialDefinitionId, revocationRegistryId: null, credentialRevocationId: null, - methodName: 'indy', + methodName: 'inMemory', }) expect(holderCredentialRecord.metadata.data).toEqual({ diff --git a/packages/anoncreds/src/models/exchange.ts b/packages/anoncreds/src/models/exchange.ts index 5213153ff9..7d483a602f 100644 --- a/packages/anoncreds/src/models/exchange.ts +++ b/packages/anoncreds/src/models/exchange.ts @@ -90,6 +90,7 @@ export interface AnonCredsProof { predicates: Record } // TODO: extend types for proof property + // eslint-disable-next-line @typescript-eslint/no-explicit-any proof: any identifiers: Array<{ schema_id: string diff --git a/packages/anoncreds/src/models/internal.ts b/packages/anoncreds/src/models/internal.ts index 8aacc72a52..112a0fb128 100644 --- a/packages/anoncreds/src/models/internal.ts +++ b/packages/anoncreds/src/models/internal.ts @@ -5,8 +5,8 @@ export interface AnonCredsCredentialInfo { } schemaId: string credentialDefinitionId: string - revocationRegistryId?: string | undefined - credentialRevocationId?: string | undefined + revocationRegistryId: string | null + credentialRevocationId: string | null methodName: string } diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts index 57127b9269..9b7c86c770 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts @@ -23,13 +23,12 @@ import { uuid } from '../../../../../../core/src/utils/uuid' import { testLogger, waitForProofExchangeRecordSubject, - getAgentOptions, makeConnection, setupEventReplaySubjects, + getInMemoryAgentOptions, } from '../../../../../../core/tests' -import { getIndySdkModules } from '../../../../../../indy-sdk/tests/setupIndySdkModule' import { - getLegacyAnonCredsModules, + getAnonCredsIndyModules, issueLegacyAnonCredsCredential, prepareForAnonCredsIssuance, setupAnonCredsTests, @@ -62,6 +61,7 @@ describe('V1 Proofs - Connectionless - Indy', () => { attributeNames: ['name', 'age'], }) + // FIXME: We should reuse anoncreds crypto object as it will speed up tests significantly await issueLegacyAnonCredsCredential({ issuerAgent: faberAgent, holderAgent: aliceAgent, @@ -359,13 +359,12 @@ describe('V1 Proofs - Connectionless - Indy', () => { const unique = uuid().substring(0, 4) - const mediatorAgentOptions = getAgentOptions( + const mediatorAgentOptions = getInMemoryAgentOptions( `Connectionless proofs with mediator Mediator-${unique}`, { endpoints: ['rxjs:mediator'], }, { - ...getIndySdkModules(), mediator: new MediatorModule({ autoAcceptMediationRequests: true, }), @@ -391,11 +390,11 @@ describe('V1 Proofs - Connectionless - Indy', () => { handshakeProtocols: [HandshakeProtocol.Connections], }) - const faberAgentOptions = getAgentOptions( + const faberAgentOptions = getInMemoryAgentOptions( `Connectionless proofs with mediator Faber-${unique}`, {}, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptProofs: AutoAcceptProof.Always, }), mediationRecipient: new MediationRecipientModule({ @@ -407,11 +406,11 @@ describe('V1 Proofs - Connectionless - Indy', () => { } ) - const aliceAgentOptions = getAgentOptions( + const aliceAgentOptions = getInMemoryAgentOptions( `Connectionless proofs with mediator Alice-${unique}`, {}, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptProofs: AutoAcceptProof.Always, }), mediationRecipient: new MediationRecipientModule({ diff --git a/packages/anoncreds/src/updates/__tests__/0.3.test.ts b/packages/anoncreds/src/updates/__tests__/0.3.test.ts index 449724e874..69972503c6 100644 --- a/packages/anoncreds/src/updates/__tests__/0.3.test.ts +++ b/packages/anoncreds/src/updates/__tests__/0.3.test.ts @@ -3,9 +3,8 @@ import { readFileSync } from 'fs' import path from 'path' import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' -import { indySdk, agentDependencies } from '../../../../core/tests' -import { IndySdkWallet } from '../../../../indy-sdk/src' -import { IndySdkSymbol } from '../../../../indy-sdk/src/types' +import { RegisteredAskarTestWallet } from '../../../../askar/tests/helpers' +import { agentDependencies, getAskarWalletConfig } from '../../../../core/tests' import { InMemoryAnonCredsRegistry } from '../../../tests/InMemoryAnonCredsRegistry' import { AnonCredsModule } from '../../AnonCredsModule' import { @@ -32,9 +31,8 @@ describe('UpdateAssistant | AnonCreds | v0.3.1 - v0.4', () => { const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the IndySdkModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) - dependencyManager.registerInstance(IndySdkSymbol, indySdk) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) dependencyManager.registerInstance(AnonCredsIssuerServiceSymbol, {}) dependencyManager.registerInstance(AnonCredsHolderServiceSymbol, {}) dependencyManager.registerInstance(AnonCredsVerifierServiceSymbol, {}) @@ -43,10 +41,7 @@ describe('UpdateAssistant | AnonCreds | v0.3.1 - v0.4', () => { { config: { label: 'Test Agent', - walletConfig: { - id: `Wallet: 0.3 Update AnonCreds - Holder`, - key: `Key: 0.3 Update AnonCreds - Holder`, - }, + walletConfig: getAskarWalletConfig('0.3 Update AnonCreds - Holder', { inMemory: false, random: 'static' }), }, dependencies: agentDependencies, modules: { @@ -69,7 +64,12 @@ describe('UpdateAssistant | AnonCreds | v0.3.1 - v0.4', () => { // Set storage after initialization. This mimics as if this wallet // is opened as an existing wallet instead of a new wallet - storageService.records = JSON.parse(holderRecordsString) + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(holderRecordsString), + creationDate: new Date(), + }, + } expect(await updateAssistant.isUpToDate()).toBe(false) expect(await updateAssistant.getNeededUpdates('0.4')).toEqual([ @@ -85,9 +85,7 @@ describe('UpdateAssistant | AnonCreds | v0.3.1 - v0.4', () => { expect(await updateAssistant.isUpToDate()).toBe(true) expect(await updateAssistant.getNeededUpdates()).toEqual([]) - // MEDIATOR_ROUTING_RECORD recipientKeys will be different every time, and is not what we're testing here - delete storageService.records.MEDIATOR_ROUTING_RECORD - expect(storageService.records).toMatchSnapshot() + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() await agent.wallet.delete() @@ -108,9 +106,8 @@ describe('UpdateAssistant | AnonCreds | v0.3.1 - v0.4', () => { const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the IndySdkModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) - dependencyManager.registerInstance(IndySdkSymbol, indySdk) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) dependencyManager.registerInstance(AnonCredsIssuerServiceSymbol, {}) dependencyManager.registerInstance(AnonCredsHolderServiceSymbol, {}) dependencyManager.registerInstance(AnonCredsVerifierServiceSymbol, {}) @@ -119,10 +116,7 @@ describe('UpdateAssistant | AnonCreds | v0.3.1 - v0.4', () => { { config: { label: 'Test Agent', - walletConfig: { - id: `Wallet: 0.3 Update AnonCreds - Issuer`, - key: `Key: 0.3 Update AnonCreds - Issuer`, - }, + walletConfig: getAskarWalletConfig('0.3 Update AnonCreds - Issuer', { inMemory: false, random: 'static' }), }, dependencies: agentDependencies, modules: { @@ -211,7 +205,12 @@ describe('UpdateAssistant | AnonCreds | v0.3.1 - v0.4', () => { // Set storage after initialization. This mimics as if this wallet // is opened as an existing wallet instead of a new wallet - storageService.records = JSON.parse(issuerRecordsString) + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(issuerRecordsString), + creationDate: new Date(), + }, + } expect(await updateAssistant.isUpToDate()).toBe(false) expect(await updateAssistant.getNeededUpdates()).toEqual([ @@ -227,9 +226,7 @@ describe('UpdateAssistant | AnonCreds | v0.3.1 - v0.4', () => { expect(await updateAssistant.isUpToDate()).toBe(true) expect(await updateAssistant.getNeededUpdates()).toEqual([]) - // MEDIATOR_ROUTING_RECORD recipientKeys will be different every time, and is not what we're testing here - delete storageService.records.MEDIATOR_ROUTING_RECORD - expect(storageService.records).toMatchSnapshot() + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() await agent.wallet.delete() diff --git a/packages/anoncreds/src/updates/__tests__/__snapshots__/0.3.test.ts.snap b/packages/anoncreds/src/updates/__tests__/__snapshots__/0.3.test.ts.snap index d6061e5889..5deeb55c0d 100644 --- a/packages/anoncreds/src/updates/__tests__/__snapshots__/0.3.test.ts.snap +++ b/packages/anoncreds/src/updates/__tests__/__snapshots__/0.3.test.ts.snap @@ -6,7 +6,7 @@ exports[`UpdateAssistant | AnonCreds | v0.3.1 - v0.4 should correctly update the "id": "1-4e4f-41d9-94c4-f49351b811f1", "tags": { "isDefault": true, - "linkSecretId": "Wallet: 0.3 Update AnonCreds - Holder", + "linkSecretId": "Wallet: 0.3 Update AnonCreds - Holder - static", }, "type": "AnonCredsLinkSecretRecord", "value": { @@ -14,7 +14,7 @@ exports[`UpdateAssistant | AnonCreds | v0.3.1 - v0.4 should correctly update the "isDefault": true, }, "id": "1-4e4f-41d9-94c4-f49351b811f1", - "linkSecretId": "Wallet: 0.3 Update AnonCreds - Holder", + "linkSecretId": "Wallet: 0.3 Update AnonCreds - Holder - static", "metadata": {}, "updatedAt": "2023-03-19T22:50:20.522Z", "value": undefined, @@ -345,7 +345,7 @@ exports[`UpdateAssistant | AnonCreds | v0.3.1 - v0.4 should correctly update the "id": "1-4e4f-41d9-94c4-f49351b811f1", "tags": { "isDefault": true, - "linkSecretId": "Wallet: 0.3 Update AnonCreds - Issuer", + "linkSecretId": "Wallet: 0.3 Update AnonCreds - Issuer - static", }, "type": "AnonCredsLinkSecretRecord", "value": { @@ -353,7 +353,7 @@ exports[`UpdateAssistant | AnonCreds | v0.3.1 - v0.4 should correctly update the "isDefault": true, }, "id": "1-4e4f-41d9-94c4-f49351b811f1", - "linkSecretId": "Wallet: 0.3 Update AnonCreds - Issuer", + "linkSecretId": "Wallet: 0.3 Update AnonCreds - Issuer - static", "metadata": {}, "updatedAt": "2023-03-19T22:50:20.522Z", "value": undefined, diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index f1194047f5..382b1f68b0 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -1,21 +1,21 @@ import type { + AnonCredsCredentialDefinition, AnonCredsRegistry, - GetSchemaReturn, - RegisterSchemaOptions, - RegisterSchemaReturn, + AnonCredsRevocationRegistryDefinition, + AnonCredsRevocationStatusList, + AnonCredsSchema, GetCredentialDefinitionReturn, - RegisterCredentialDefinitionOptions, - RegisterCredentialDefinitionReturn, GetRevocationRegistryDefinitionReturn, GetRevocationStatusListReturn, - AnonCredsRevocationStatusList, - AnonCredsRevocationRegistryDefinition, - AnonCredsSchema, - AnonCredsCredentialDefinition, + GetSchemaReturn, + RegisterCredentialDefinitionOptions, + RegisterCredentialDefinitionReturn, RegisterRevocationRegistryDefinitionOptions, RegisterRevocationRegistryDefinitionReturn, - RegisterRevocationStatusListReturn, RegisterRevocationStatusListOptions, + RegisterRevocationStatusListReturn, + RegisterSchemaOptions, + RegisterSchemaReturn, } from '../src' import type { AgentContext } from '@credo-ts/core' @@ -26,12 +26,12 @@ import { getDidIndyCredentialDefinitionId, getDidIndyRevocationRegistryDefinitionId, getDidIndySchemaId, -} from '../../indy-sdk/src/anoncreds/utils/identifiers' +} from '../../indy-vdr/src/anoncreds/utils/identifiers' import { - parseIndyCredentialDefinitionId, getUnqualifiedRevocationRegistryDefinitionId, getUnqualifiedCredentialDefinitionId, getUnqualifiedSchemaId, + parseIndyCredentialDefinitionId, parseIndyDid, parseIndySchemaId, } from '../src' @@ -43,9 +43,6 @@ import { dateToTimestamp } from '../src/utils/timestamp' export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { public readonly methodName = 'inMemory' - // Roughly match that the identifier starts with an unqualified indy did. Once the - // anoncreds tests are not based on the indy-sdk anymore, we can use any identifier - // we want, but the indy-sdk is picky about the identifier format. public readonly supportedIdentifier = /.+/ private schemas: Record diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts index 56427e47fa..dc0ce53967 100644 --- a/packages/anoncreds/tests/anoncreds.test.ts +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -1,8 +1,8 @@ -import { Agent, KeyDerivationMethod, KeyType, TypedArrayEncoder } from '@credo-ts/core' -import { agentDependencies } from '@credo-ts/node' -import * as indySdk from 'indy-sdk' +import { Agent, KeyType, TypedArrayEncoder } from '@credo-ts/core' -import { IndySdkModule } from '../../indy-sdk/src/IndySdkModule' +import { AnonCredsRsModule } from '../../anoncreds-rs/src' +import { anoncreds } from '../../anoncreds-rs/tests/helpers' +import { getInMemoryAgentOptions } from '../../core/tests' import { AnonCredsModule } from '../src' import { InMemoryAnonCredsRegistry } from './InMemoryAnonCredsRegistry' @@ -71,33 +71,25 @@ const existingRevocationStatusLists = { }, } -const agent = new Agent({ - config: { - label: '@credo-ts/anoncreds', - walletConfig: { - id: '@credo-ts/anoncreds', - key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', - keyDerivationMethod: KeyDerivationMethod.Raw, - }, - }, - modules: { - indySdk: new IndySdkModule({ - indySdk, - autoCreateLinkSecret: false, - }), - anoncreds: new AnonCredsModule({ - registries: [ - new InMemoryAnonCredsRegistry({ - existingSchemas, - existingCredentialDefinitions, - existingRevocationRegistryDefinitions, - existingRevocationStatusLists, - }), - ], - }), - }, - dependencies: agentDependencies, -}) +const agent = new Agent( + getInMemoryAgentOptions( + 'credo-anoncreds-package', + {}, + { + anoncredsRs: new AnonCredsRsModule({ anoncreds, autoCreateLinkSecret: false }), + anoncreds: new AnonCredsModule({ + registries: [ + new InMemoryAnonCredsRegistry({ + existingSchemas, + existingCredentialDefinitions, + existingRevocationRegistryDefinitions, + existingRevocationStatusLists, + }), + ], + }), + } + ) +) describe('AnonCreds API', () => { beforeEach(async () => { diff --git a/packages/anoncreds/tests/legacyAnonCredsSetup.ts b/packages/anoncreds/tests/legacyAnonCredsSetup.ts index 0d8b3f9148..f9752c4a7c 100644 --- a/packages/anoncreds/tests/legacyAnonCredsSetup.ts +++ b/packages/anoncreds/tests/legacyAnonCredsSetup.ts @@ -11,6 +11,7 @@ import type { import type { AutoAcceptProof, ConnectionRecord } from '@credo-ts/core' import { + AgentEventTypes, TypedArrayEncoder, CacheModule, InMemoryLruCache, @@ -31,12 +32,10 @@ import { randomUUID } from 'crypto' import { AnonCredsRsModule } from '../../anoncreds-rs/src' import { anoncreds } from '../../anoncreds-rs/tests/helpers' -import { AskarModule } from '../../askar/src' -import { askarModuleConfig } from '../../askar/tests/helpers' import { sleep } from '../../core/src/utils/sleep' import { setupSubjectTransports, setupEventReplaySubjects } from '../../core/tests' import { - getAgentOptions, + getInMemoryAgentOptions, importExistingIndyDidFromPrivateKey, makeConnection, publicDidSeed, @@ -44,14 +43,6 @@ import { waitForProofExchangeRecordSubject, } from '../../core/tests/helpers' import testLogger from '../../core/tests/logger' -import { - IndySdkAnonCredsRegistry, - IndySdkIndyDidRegistrar, - IndySdkIndyDidResolver, - IndySdkModule, - IndySdkSovDidResolver, -} from '../../indy-sdk/src' -import { getIndySdkModuleConfig } from '../../indy-sdk/tests/setupIndySdkModule' import { IndyVdrAnonCredsRegistry, IndyVdrSovDidResolver, @@ -73,54 +64,12 @@ import { } from '../src' // Helper type to get the type of the agents (with the custom modules) for the credential tests -export type AnonCredsTestsAgent = - | Agent & { mediationRecipient?: any; mediator?: any }> - | Agent & { mediationRecipient?: any; mediator?: any }> - -export const getLegacyAnonCredsModules = ({ - autoAcceptCredentials, - autoAcceptProofs, -}: { autoAcceptCredentials?: AutoAcceptCredential; autoAcceptProofs?: AutoAcceptProof } = {}) => { - const indyCredentialFormat = new LegacyIndyCredentialFormatService() - const indyProofFormat = new LegacyIndyProofFormatService() - - // Register the credential and proof protocols - const modules = { - credentials: new CredentialsModule({ - autoAcceptCredentials, - credentialProtocols: [ - new V1CredentialProtocol({ indyCredentialFormat }), - new V2CredentialProtocol({ - credentialFormats: [indyCredentialFormat], - }), - ], - }), - proofs: new ProofsModule({ - autoAcceptProofs, - proofProtocols: [ - new V1ProofProtocol({ indyProofFormat }), - new V2ProofProtocol({ - proofFormats: [indyProofFormat], - }), - ], - }), - anoncreds: new AnonCredsModule({ - registries: [new IndySdkAnonCredsRegistry()], - }), - dids: new DidsModule({ - resolvers: [new IndySdkSovDidResolver(), new IndySdkIndyDidResolver()], - registrars: [new IndySdkIndyDidRegistrar()], - }), - indySdk: new IndySdkModule(getIndySdkModuleConfig()), - cache: new CacheModule({ - cache: new InMemoryLruCache({ limit: 100 }), - }), - } as const - - return modules -} +export type AnonCredsTestsAgent = Agent< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ReturnType & { mediationRecipient?: any; mediator?: any } +> -export const getAskarAnonCredsIndyModules = ({ +export const getAnonCredsIndyModules = ({ autoAcceptCredentials, autoAcceptProofs, }: { autoAcceptCredentials?: AutoAcceptCredential; autoAcceptProofs?: AutoAcceptProof } = {}) => { @@ -161,7 +110,6 @@ export const getAskarAnonCredsIndyModules = ({ resolvers: [new IndyVdrSovDidResolver(), new IndyVdrIndyDidResolver()], registrars: [new IndyVdrIndyDidRegistrar()], }), - askar: new AskarModule(askarModuleConfig), cache: new CacheModule({ cache: new InMemoryLruCache({ limit: 100 }), }), @@ -352,12 +300,12 @@ export async function setupAnonCredsTests< createConnections?: CreateConnections }): Promise> { const issuerAgent = new Agent( - getAgentOptions( + getInMemoryAgentOptions( issuerName, { endpoints: ['rxjs:issuer'], }, - getLegacyAnonCredsModules({ + getAnonCredsIndyModules({ autoAcceptCredentials, autoAcceptProofs, }) @@ -365,12 +313,12 @@ export async function setupAnonCredsTests< ) const holderAgent = new Agent( - getAgentOptions( + getInMemoryAgentOptions( holderName, { endpoints: ['rxjs:holder'], }, - getLegacyAnonCredsModules({ + getAnonCredsIndyModules({ autoAcceptCredentials, autoAcceptProofs, }) @@ -379,12 +327,12 @@ export async function setupAnonCredsTests< const verifierAgent = verifierName ? new Agent( - getAgentOptions( + getInMemoryAgentOptions( verifierName, { endpoints: ['rxjs:verifier'], }, - getLegacyAnonCredsModules({ + getAnonCredsIndyModules({ autoAcceptCredentials, autoAcceptProofs, }) @@ -395,7 +343,11 @@ export async function setupAnonCredsTests< setupSubjectTransports(verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent]) const [issuerReplay, holderReplay, verifierReplay] = setupEventReplaySubjects( verifierAgent ? [issuerAgent, holderAgent, verifierAgent] : [issuerAgent, holderAgent], - [CredentialEventTypes.CredentialStateChanged, ProofEventTypes.ProofStateChanged] + [ + CredentialEventTypes.CredentialStateChanged, + ProofEventTypes.ProofStateChanged, + AgentEventTypes.AgentMessageProcessed, + ] ) await issuerAgent.initialize() diff --git a/packages/askar/src/AskarModule.ts b/packages/askar/src/AskarModule.ts index fe86f89755..561e0c2f85 100644 --- a/packages/askar/src/AskarModule.ts +++ b/packages/askar/src/AskarModule.ts @@ -21,7 +21,7 @@ export class AskarModule implements Module { dependencyManager .resolve(AgentConfig) .logger.warn( - "The '@credo-ts/askar' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + "The '@credo-ts/askar' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." ) dependencyManager.registerInstance(AskarModuleConfig, this.config) diff --git a/packages/askar/src/storage/AskarStorageService.ts b/packages/askar/src/storage/AskarStorageService.ts index b34531871e..8edfb5c2f1 100644 --- a/packages/askar/src/storage/AskarStorageService.ts +++ b/packages/askar/src/storage/AskarStorageService.ts @@ -110,7 +110,7 @@ export class AskarStorageService implements StorageService return recordToInstance(record, recordClass) } catch (error) { if (error instanceof RecordNotFoundError) throw error - throw new WalletError(`Error getting record`, { cause: error }) + throw new WalletError(`Error getting record ${recordClass.name}`, { cause: error }) } } diff --git a/packages/askar/src/utils/askarKeyTypes.ts b/packages/askar/src/utils/askarKeyTypes.ts index 9731618b21..cbbf3713d2 100644 --- a/packages/askar/src/utils/askarKeyTypes.ts +++ b/packages/askar/src/utils/askarKeyTypes.ts @@ -1,15 +1,40 @@ import { KeyType } from '@credo-ts/core' import { KeyAlgs } from '@hyperledger/aries-askar-shared' +export enum AskarKeyTypePurpose { + KeyManagement = 'KeyManagement', + Signing = 'Signing', +} + const keyTypeToAskarAlg = { - [KeyType.Ed25519]: KeyAlgs.Ed25519, - [KeyType.X25519]: KeyAlgs.X25519, - [KeyType.Bls12381g1]: KeyAlgs.Bls12381G1, - [KeyType.Bls12381g2]: KeyAlgs.Bls12381G2, - [KeyType.Bls12381g1g2]: KeyAlgs.Bls12381G1G2, - [KeyType.P256]: KeyAlgs.EcSecp256r1, -} as const - -export const isKeyTypeSupportedByAskar = (keyType: KeyType) => keyType in keyTypeToAskarAlg + [KeyType.Ed25519]: { + keyAlg: KeyAlgs.Ed25519, + purposes: [AskarKeyTypePurpose.KeyManagement, AskarKeyTypePurpose.Signing], + }, + [KeyType.X25519]: { + keyAlg: KeyAlgs.X25519, + purposes: [AskarKeyTypePurpose.KeyManagement, AskarKeyTypePurpose.Signing], + }, + [KeyType.Bls12381g1]: { + keyAlg: KeyAlgs.Bls12381G1, + purposes: [AskarKeyTypePurpose.KeyManagement], + }, + [KeyType.Bls12381g2]: { + keyAlg: KeyAlgs.Bls12381G2, + purposes: [AskarKeyTypePurpose.KeyManagement], + }, + [KeyType.Bls12381g1g2]: { + keyAlg: KeyAlgs.Bls12381G1, + purposes: [AskarKeyTypePurpose.KeyManagement], + }, + [KeyType.P256]: { + keyAlg: KeyAlgs.EcSecp256r1, + purposes: [AskarKeyTypePurpose.KeyManagement], + }, +} + +export const isKeyTypeSupportedByAskarForPurpose = (keyType: KeyType, purpose: AskarKeyTypePurpose) => + keyType in keyTypeToAskarAlg && + keyTypeToAskarAlg[keyType as keyof typeof keyTypeToAskarAlg].purposes.includes(purpose) export const keyTypesSupportedByAskar = Object.keys(keyTypeToAskarAlg) as KeyType[] diff --git a/packages/askar/src/utils/askarWalletConfig.ts b/packages/askar/src/utils/askarWalletConfig.ts index 2ddfbb00a0..1819222e2a 100644 --- a/packages/askar/src/utils/askarWalletConfig.ts +++ b/packages/askar/src/utils/askarWalletConfig.ts @@ -1,9 +1,13 @@ -import type { AskarWalletPostgresStorageConfig } from '../wallet/AskarWalletPostgresStorageConfig' import type { WalletConfig } from '@credo-ts/core' import { KeyDerivationMethod, WalletError } from '@credo-ts/core' import { KdfMethod, StoreKeyMethod } from '@hyperledger/aries-askar-shared' +import { + isAskarWalletPostgresStorageConfig, + isAskarWalletSqliteStorageConfig, +} from '../wallet/AskarWalletStorageConfig' + export const keyDerivationMethodToStoreKeyMethod = (keyDerivationMethod: KeyDerivationMethod) => { const correspondenceTable = { [KeyDerivationMethod.Raw]: KdfMethod.Raw, @@ -32,33 +36,27 @@ export const uriFromWalletConfig = ( walletConfig.storage = { type: 'sqlite' } } - if (walletConfig.storage.type === 'sqlite') { - if (walletConfig.storage.inMemory) { + const urlParams = [] + + const storageConfig = walletConfig.storage + if (isAskarWalletSqliteStorageConfig(storageConfig)) { + if (storageConfig.config?.inMemory) { uri = 'sqlite://:memory:' } else { - path = (walletConfig.storage.path as string) ?? `${credoDataPath}/wallet/${walletConfig.id}/sqlite.db` + path = storageConfig.config?.path ?? `${credoDataPath}/wallet/${walletConfig.id}/sqlite.db` uri = `sqlite://${path}` } - } else if (walletConfig.storage.type === 'postgres') { - const storageConfig = walletConfig.storage as unknown as AskarWalletPostgresStorageConfig - + } else if (isAskarWalletPostgresStorageConfig(storageConfig)) { if (!storageConfig.config || !storageConfig.credentials) { throw new WalletError('Invalid storage configuration for postgres wallet') } - const urlParams = [] if (storageConfig.config.connectTimeout !== undefined) { urlParams.push(`connect_timeout=${encodeURIComponent(storageConfig.config.connectTimeout)}`) } if (storageConfig.config.idleTimeout !== undefined) { urlParams.push(`idle_timeout=${encodeURIComponent(storageConfig.config.idleTimeout)}`) } - if (storageConfig.config.maxConnections !== undefined) { - urlParams.push(`max_connections=${encodeURIComponent(storageConfig.config.maxConnections)}`) - } - if (storageConfig.config.minConnections !== undefined) { - urlParams.push(`min_connections=${encodeURIComponent(storageConfig.config.minConnections)}`) - } if (storageConfig.credentials.adminAccount !== undefined) { urlParams.push(`admin_account=${encodeURIComponent(storageConfig.credentials.adminAccount)}`) } @@ -69,12 +67,20 @@ export const uriFromWalletConfig = ( uri = `postgres://${encodeURIComponent(storageConfig.credentials.account)}:${encodeURIComponent( storageConfig.credentials.password )}@${storageConfig.config.host}/${encodeURIComponent(walletConfig.id)}` - - if (urlParams.length > 0) { - uri = `${uri}?${urlParams.join('&')}` - } } else { - throw new WalletError(`Storage type not supported: ${walletConfig.storage.type}`) + throw new WalletError(`Storage type not supported: ${storageConfig.type}`) + } + + // Common config options + if (storageConfig.config?.maxConnections !== undefined) { + urlParams.push(`max_connections=${encodeURIComponent(storageConfig.config.maxConnections)}`) + } + if (storageConfig.config?.minConnections !== undefined) { + urlParams.push(`min_connections=${encodeURIComponent(storageConfig.config.minConnections)}`) + } + + if (urlParams.length > 0) { + uri = `${uri}?${urlParams.join('&')}` } return { uri, path } diff --git a/packages/askar/src/wallet/AskarBaseWallet.ts b/packages/askar/src/wallet/AskarBaseWallet.ts index 90aae49f79..70796751e6 100644 --- a/packages/askar/src/wallet/AskarBaseWallet.ts +++ b/packages/askar/src/wallet/AskarBaseWallet.ts @@ -12,30 +12,33 @@ import type { Logger, SigningProviderRegistry, } from '@credo-ts/core' -import type { KeyEntryObject, Session } from '@hyperledger/aries-askar-shared' +import type { Session } from '@hyperledger/aries-askar-shared' import { WalletKeyExistsError, isValidSeed, isValidPrivateKey, - JsonTransformer, JsonEncoder, - KeyType, Buffer, AriesFrameworkError, WalletError, Key, TypedArrayEncoder, } from '@credo-ts/core' -import { KeyAlgs, CryptoBox, Store, Key as AskarKey, keyAlgFromString } from '@hyperledger/aries-askar-shared' -// eslint-disable-next-line import/order +import { CryptoBox, Store, Key as AskarKey, keyAlgFromString } from '@hyperledger/aries-askar-shared' import BigNumber from 'bn.js' -const isError = (error: unknown): error is Error => error instanceof Error +import { + AskarErrorCode, + AskarKeyTypePurpose, + isAskarError, + isKeyTypeSupportedByAskarForPurpose, + keyTypesSupportedByAskar, +} from '../utils' -import { AskarErrorCode, isAskarError, isKeyTypeSupportedByAskar, keyTypesSupportedByAskar } from '../utils' +import { didcommV1Pack, didcommV1Unpack } from './didcommV1' -import { JweEnvelope, JweRecipient } from './JweEnvelope' +const isError = (error: unknown): error is Error => error instanceof Error export abstract class AskarBaseWallet implements Wallet { protected _session?: Session @@ -96,13 +99,13 @@ export abstract class AskarBaseWallet implements Wallet { throw new WalletError('Invalid private key provided') } - if (isKeyTypeSupportedByAskar(keyType)) { + if (isKeyTypeSupportedByAskarForPurpose(keyType, AskarKeyTypePurpose.KeyManagement)) { const algorithm = keyAlgFromString(keyType) // Create key let key: AskarKey | undefined try { - const key = privateKey + key = privateKey ? AskarKey.fromSecretBytes({ secretKey: privateKey, algorithm }) : seed ? AskarKey.fromSeed({ seed, algorithm }) @@ -154,32 +157,81 @@ export abstract class AskarBaseWallet implements Wallet { * @returns A signature for the data */ public async sign({ data, key }: WalletSignOptions): Promise { - let keyEntry: KeyEntryObject | null | undefined + let askarKey: AskarKey | null | undefined + let keyPair: KeyPair | null | undefined + try { - if (isKeyTypeSupportedByAskar(key.keyType)) { + if (isKeyTypeSupportedByAskarForPurpose(key.keyType, AskarKeyTypePurpose.KeyManagement)) { + askarKey = (await this.session.fetchKey({ name: key.publicKeyBase58 }))?.key + } + + // FIXME: remove the custom KeyPair record now that we deprecate Indy SDK. + // We can do this in a migration script + + // Fallback to fetching key from the non-askar storage, this is to handle the case + // where a key wasn't supported at first by the wallet, but now is + if (!askarKey) { + keyPair = await this.retrieveKeyPair(key.publicKeyBase58) + + // If we have the key stored in a custom record, but it is now supported by Askar, + // we 'import' the key into askar storage and remove the custom key record + if (keyPair && isKeyTypeSupportedByAskarForPurpose(keyPair.keyType, AskarKeyTypePurpose.KeyManagement)) { + askarKey = AskarKey.fromSecretBytes({ + secretKey: TypedArrayEncoder.fromBase58(keyPair.privateKeyBase58), + algorithm: keyAlgFromString(keyPair.keyType), + }) + await this.session.insertKey({ + name: key.publicKeyBase58, + key: askarKey, + }) + // Now we can remove it from the custom record as we have imported it into Askar + await this.deleteKeyPair(key.publicKeyBase58) + keyPair = undefined + } + } + + if (!askarKey && !keyPair) { + throw new WalletError('Key entry not found') + } + + // Not all keys are supported for signing + if (isKeyTypeSupportedByAskarForPurpose(key.keyType, AskarKeyTypePurpose.Signing)) { if (!TypedArrayEncoder.isTypedArray(data)) { throw new WalletError(`Currently not supporting signing of multiple messages`) } - keyEntry = await this.session.fetchKey({ name: key.publicKeyBase58 }) - if (!keyEntry) { + askarKey = + askarKey ?? + (keyPair + ? AskarKey.fromSecretBytes({ + secretKey: TypedArrayEncoder.fromBase58(keyPair.privateKeyBase58), + algorithm: keyAlgFromString(keyPair.keyType), + }) + : undefined) + + if (!askarKey) { throw new WalletError('Key entry not found') } - const signed = keyEntry.key.signMessage({ message: data as Buffer }) - - keyEntry.key.handle.free() - + const signed = askarKey.signMessage({ message: data as Buffer }) return Buffer.from(signed) } else { // Check if there is a signing key provider for the specified key type. if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(key.keyType) - const keyPair = await this.retrieveKeyPair(key.publicKeyBase58) + // It could be that askar supports storing the key, but can't sign with it + // (in case of bls) + const privateKeyBase58 = + keyPair?.privateKeyBase58 ?? + (askarKey?.secretBytes ? TypedArrayEncoder.toBase58(askarKey.secretBytes) : undefined) + + if (!privateKeyBase58) { + throw new WalletError('Key entry not found') + } const signed = await signingKeyProvider.sign({ data, - privateKeyBase58: keyPair.privateKeyBase58, + privateKeyBase58: privateKeyBase58, publicKeyBase58: key.publicKeyBase58, }) @@ -188,11 +240,12 @@ export abstract class AskarBaseWallet implements Wallet { throw new WalletError(`Unsupported keyType: ${key.keyType}`) } } catch (error) { - keyEntry?.key.handle.free() if (!isError(error)) { throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) } throw new WalletError(`Error signing data with verkey ${key.publicKeyBase58}. ${error.message}`, { cause: error }) + } finally { + askarKey?.handle.free() } } @@ -211,31 +264,29 @@ export abstract class AskarBaseWallet implements Wallet { public async verify({ data, key, signature }: WalletVerifyOptions): Promise { let askarKey: AskarKey | undefined try { - if (isKeyTypeSupportedByAskar(key.keyType)) { + if (isKeyTypeSupportedByAskarForPurpose(key.keyType, AskarKeyTypePurpose.Signing)) { if (!TypedArrayEncoder.isTypedArray(data)) { throw new WalletError(`Currently not supporting verification of multiple messages`) } - const askarKey = AskarKey.fromPublicBytes({ + askarKey = AskarKey.fromPublicBytes({ algorithm: keyAlgFromString(key.keyType), publicKey: key.publicKey, }) const verified = askarKey.verifySignature({ message: data as Buffer, signature }) askarKey.handle.free() return verified - } else { + } else if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { // Check if there is a signing key provider for the specified key type. - if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { - const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(key.keyType) - - const signed = await signingKeyProvider.verify({ - data, - signature, - publicKeyBase58: key.publicKeyBase58, - }) + const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(key.keyType) + const signed = await signingKeyProvider.verify({ + data, + signature, + publicKeyBase58: key.publicKeyBase58, + }) - return signed - } + return signed + } else { throw new WalletError(`Unsupported keyType: ${key.keyType}`) } } catch (error) { @@ -262,96 +313,18 @@ export abstract class AskarBaseWallet implements Wallet { recipientKeys: string[], senderVerkey?: string // in base58 ): Promise { - let cek: AskarKey | undefined - let senderKey: KeyEntryObject | null | undefined - let senderExchangeKey: AskarKey | undefined + const senderKey = senderVerkey ? await this.session.fetchKey({ name: senderVerkey }) : undefined try { - cek = AskarKey.generate(KeyAlgs.Chacha20C20P) - - senderKey = senderVerkey ? await this.session.fetchKey({ name: senderVerkey }) : undefined if (senderVerkey && !senderKey) { - throw new WalletError(`Unable to pack message. Sender key ${senderVerkey} not found in wallet.`) + throw new WalletError(`Sender key not found`) } - senderExchangeKey = senderKey ? senderKey.key.convertkey({ algorithm: KeyAlgs.X25519 }) : undefined - - const recipients: JweRecipient[] = [] + const envelope = didcommV1Pack(payload, recipientKeys, senderKey?.key) - for (const recipientKey of recipientKeys) { - let targetExchangeKey: AskarKey | undefined - try { - targetExchangeKey = AskarKey.fromPublicBytes({ - publicKey: Key.fromPublicKeyBase58(recipientKey, KeyType.Ed25519).publicKey, - algorithm: KeyAlgs.Ed25519, - }).convertkey({ algorithm: KeyAlgs.X25519 }) - - if (senderVerkey && senderExchangeKey) { - const encryptedSender = CryptoBox.seal({ - recipientKey: targetExchangeKey, - message: Buffer.from(senderVerkey), - }) - const nonce = CryptoBox.randomNonce() - const encryptedCek = CryptoBox.cryptoBox({ - recipientKey: targetExchangeKey, - senderKey: senderExchangeKey, - message: cek.secretBytes, - nonce, - }) - - recipients.push( - new JweRecipient({ - encryptedKey: encryptedCek, - header: { - kid: recipientKey, - sender: TypedArrayEncoder.toBase64URL(encryptedSender), - iv: TypedArrayEncoder.toBase64URL(nonce), - }, - }) - ) - } else { - const encryptedCek = CryptoBox.seal({ - recipientKey: targetExchangeKey, - message: cek.secretBytes, - }) - recipients.push( - new JweRecipient({ - encryptedKey: encryptedCek, - header: { - kid: recipientKey, - }, - }) - ) - } - } finally { - targetExchangeKey?.handle.free() - } - } - - const protectedJson = { - enc: 'xchacha20poly1305_ietf', - typ: 'JWM/1.0', - alg: senderVerkey ? 'Authcrypt' : 'Anoncrypt', - recipients: recipients.map((item) => JsonTransformer.toJSON(item)), - } - - const { ciphertext, tag, nonce } = cek.aeadEncrypt({ - message: Buffer.from(JSON.stringify(payload)), - aad: Buffer.from(JsonEncoder.toBase64URL(protectedJson)), - }).parts - - const envelope = new JweEnvelope({ - ciphertext: TypedArrayEncoder.toBase64URL(ciphertext), - iv: TypedArrayEncoder.toBase64URL(nonce), - protected: JsonEncoder.toBase64URL(protectedJson), - tag: TypedArrayEncoder.toBase64URL(tag), - }).toJson() - - return envelope as EncryptedMessage + return envelope } finally { - cek?.handle.free() senderKey?.key.handle.free() - senderExchangeKey?.handle.free() } } @@ -363,101 +336,21 @@ export abstract class AskarBaseWallet implements Wallet { */ public async unpack(messagePackage: EncryptedMessage): Promise { const protectedJson = JsonEncoder.fromBase64(messagePackage.protected) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const recipientKids: string[] = protectedJson.recipients.map((r: any) => r.header.kid) - const alg = protectedJson.alg - if (!['Anoncrypt', 'Authcrypt'].includes(alg)) { - throw new WalletError(`Unsupported pack algorithm: ${alg}`) - } - - const recipients = [] - - for (const recip of protectedJson.recipients) { - const kid = recip.header.kid - if (!kid) { - throw new WalletError('Blank recipient key') - } - const sender = recip.header.sender ? TypedArrayEncoder.fromBase64(recip.header.sender) : undefined - const iv = recip.header.iv ? TypedArrayEncoder.fromBase64(recip.header.iv) : undefined - if (sender && !iv) { - throw new WalletError('Missing IV') - } else if (!sender && iv) { - throw new WalletError('Unexpected IV') - } - recipients.push({ - kid, - sender, - iv, - encrypted_key: TypedArrayEncoder.fromBase64(recip.encrypted_key), - }) - } - - let payloadKey, senderKey, recipientKey - - for (const recipient of recipients) { - let recipientKeyEntry: KeyEntryObject | null | undefined - let sender_x: AskarKey | undefined - let recip_x: AskarKey | undefined - + for (const recipientKid of recipientKids) { + const recipientKeyEntry = await this.session.fetchKey({ name: recipientKid }) try { - recipientKeyEntry = await this.session.fetchKey({ name: recipient.kid }) if (recipientKeyEntry) { - const recip_x = recipientKeyEntry.key.convertkey({ algorithm: KeyAlgs.X25519 }) - recipientKey = recipient.kid - - if (recipient.sender && recipient.iv) { - senderKey = TypedArrayEncoder.toUtf8String( - CryptoBox.sealOpen({ - recipientKey: recip_x, - ciphertext: recipient.sender, - }) - ) - const sender_x = AskarKey.fromPublicBytes({ - algorithm: KeyAlgs.Ed25519, - publicKey: TypedArrayEncoder.fromBase58(senderKey), - }).convertkey({ algorithm: KeyAlgs.X25519 }) - - payloadKey = CryptoBox.open({ - recipientKey: recip_x, - senderKey: sender_x, - message: recipient.encrypted_key, - nonce: recipient.iv, - }) - } else { - payloadKey = CryptoBox.sealOpen({ ciphertext: recipient.encrypted_key, recipientKey: recip_x }) - } - break + return didcommV1Unpack(messagePackage, recipientKeyEntry.key) } } finally { recipientKeyEntry?.key.handle.free() - sender_x?.handle.free() - recip_x?.handle.free() } } - if (!payloadKey) { - throw new WalletError('No corresponding recipient key found') - } - - if (!senderKey && alg === 'Authcrypt') { - throw new WalletError('Sender public key not provided for Authcrypt') - } - let cek: AskarKey | undefined - try { - cek = AskarKey.fromSecretBytes({ algorithm: KeyAlgs.Chacha20C20P, secretKey: payloadKey }) - const message = cek.aeadDecrypt({ - ciphertext: TypedArrayEncoder.fromBase64(messagePackage.ciphertext), - nonce: TypedArrayEncoder.fromBase64(messagePackage.iv), - tag: TypedArrayEncoder.fromBase64(messagePackage.tag), - aad: TypedArrayEncoder.fromString(messagePackage.protected), - }) - return { - plaintextMessage: JsonEncoder.fromBuffer(message), - senderKey, - recipientKey, - } - } finally { - cek?.handle.free() - } + throw new WalletError('No corresponding recipient key found') } public async generateNonce(): Promise { @@ -481,20 +374,26 @@ export abstract class AskarBaseWallet implements Wallet { } } - private async retrieveKeyPair(publicKeyBase58: string): Promise { + private async retrieveKeyPair(publicKeyBase58: string): Promise { try { const entryObject = await this.session.fetch({ category: 'KeyPairRecord', name: `key-${publicKeyBase58}` }) - if (entryObject?.value) { - return JsonEncoder.fromString(entryObject?.value as string) as KeyPair - } else { - throw new WalletError(`No content found for record with public key: ${publicKeyBase58}`) - } + if (!entryObject) return null + + return JsonEncoder.fromString(entryObject?.value as string) as KeyPair } catch (error) { throw new WalletError('Error retrieving KeyPair record', { cause: error }) } } + private async deleteKeyPair(publicKeyBase58: string): Promise { + try { + await this.session.remove({ category: 'KeyPairRecord', name: `key-${publicKeyBase58}` }) + } catch (error) { + throw new WalletError('Error removing KeyPair record', { cause: error }) + } + } + private async storeKeyPair(keyPair: KeyPair): Promise { try { await this.session.insert({ diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index 8f74d91ba3..078c75f647 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -23,6 +23,7 @@ import { AskarErrorCode, isAskarError, keyDerivationMethodToStoreKeyMethod, uriF import { AskarBaseWallet } from './AskarBaseWallet' import { AskarProfileWallet } from './AskarProfileWallet' +import { isAskarWalletSqliteStorageConfig } from './AskarWalletStorageConfig' /** * @todo: rename after 0.5.0, as we now have multiple types of AskarWallet @@ -218,7 +219,9 @@ export class AskarWallet extends AskarBaseWallet { if ( isAskarError(error) && (error.code === AskarErrorCode.NotFound || - (error.code === AskarErrorCode.Backend && walletConfig.storage?.inMemory)) + (error.code === AskarErrorCode.Backend && + isAskarWalletSqliteStorageConfig(walletConfig.storage) && + walletConfig.storage.config?.inMemory)) ) { const errorMessage = `Wallet '${walletConfig.id}' not found` this.logger.debug(errorMessage) @@ -281,6 +284,10 @@ export class AskarWallet extends AskarBaseWallet { const { path: destinationPath, key: exportKey } = exportConfig const { path: sourcePath } = uriFromWalletConfig(this.walletConfig, this.fileSystem.dataPath) + + if (isAskarWalletSqliteStorageConfig(this.walletConfig.storage) && this.walletConfig.storage?.inMemory) { + throw new WalletError('Export is not supported for in memory wallet') + } if (!sourcePath) { throw new WalletError('Export is only supported for SQLite backend') } @@ -295,7 +302,7 @@ export class AskarWallet extends AskarBaseWallet { const exportedWalletConfig = await this.getAskarWalletConfig({ ...this.walletConfig, key: exportKey, - storage: { type: 'sqlite', path: destinationPath }, + storage: { type: 'sqlite', config: { path: destinationPath } }, }) // Make sure destination path exists diff --git a/packages/askar/src/wallet/AskarWalletPostgresStorageConfig.ts b/packages/askar/src/wallet/AskarWalletPostgresStorageConfig.ts deleted file mode 100644 index e921792530..0000000000 --- a/packages/askar/src/wallet/AskarWalletPostgresStorageConfig.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { WalletStorageConfig } from '@credo-ts/core' - -export interface AskarWalletPostgresConfig { - host: string - connectTimeout?: number - idleTimeout?: number - maxConnections?: number - minConnections?: number -} - -export interface AskarWalletPostgresCredentials { - account: string - password: string - adminAccount?: string - adminPassword?: string -} - -export interface AskarWalletPostgresStorageConfig extends WalletStorageConfig { - type: 'postgres' - config: AskarWalletPostgresConfig - credentials: AskarWalletPostgresCredentials -} diff --git a/packages/askar/src/wallet/AskarWalletStorageConfig.ts b/packages/askar/src/wallet/AskarWalletStorageConfig.ts new file mode 100644 index 0000000000..be73af4546 --- /dev/null +++ b/packages/askar/src/wallet/AskarWalletStorageConfig.ts @@ -0,0 +1,47 @@ +import type { WalletStorageConfig } from '@credo-ts/core' + +export interface AskarWalletPostgresConfig { + host: string + connectTimeout?: number + idleTimeout?: number + maxConnections?: number + minConnections?: number +} + +export interface AskarWalletSqliteConfig { + // TODO: add other sqlite config options + maxConnections?: number + minConnections?: number + inMemory?: boolean + path?: string +} + +export interface AskarWalletPostgresCredentials { + account: string + password: string + adminAccount?: string + adminPassword?: string +} + +export interface AskarWalletPostgresStorageConfig extends WalletStorageConfig { + type: 'postgres' + config: AskarWalletPostgresConfig + credentials: AskarWalletPostgresCredentials +} + +export interface AskarWalletSqliteStorageConfig extends WalletStorageConfig { + type: 'sqlite' + config?: AskarWalletSqliteConfig +} + +export function isAskarWalletSqliteStorageConfig( + config?: WalletStorageConfig +): config is AskarWalletSqliteStorageConfig { + return config?.type === 'sqlite' +} + +export function isAskarWalletPostgresStorageConfig( + config?: WalletStorageConfig +): config is AskarWalletPostgresStorageConfig { + return config?.type === 'postgres' +} diff --git a/packages/askar/src/wallet/didcommV1.ts b/packages/askar/src/wallet/didcommV1.ts new file mode 100644 index 0000000000..6ab9a7f76d --- /dev/null +++ b/packages/askar/src/wallet/didcommV1.ts @@ -0,0 +1,177 @@ +import type { EncryptedMessage } from '@credo-ts/core' + +import { WalletError, JsonEncoder, JsonTransformer, Key, KeyType, TypedArrayEncoder, Buffer } from '@credo-ts/core' +import { CryptoBox, Key as AskarKey, KeyAlgs } from '@hyperledger/aries-askar-shared' + +import { JweEnvelope, JweRecipient } from './JweEnvelope' + +export function didcommV1Pack(payload: Record, recipientKeys: string[], senderKey?: AskarKey) { + let cek: AskarKey | undefined + let senderExchangeKey: AskarKey | undefined + + try { + cek = AskarKey.generate(KeyAlgs.Chacha20C20P) + + senderExchangeKey = senderKey ? senderKey.convertkey({ algorithm: KeyAlgs.X25519 }) : undefined + + const recipients: JweRecipient[] = [] + + for (const recipientKey of recipientKeys) { + let targetExchangeKey: AskarKey | undefined + try { + targetExchangeKey = AskarKey.fromPublicBytes({ + publicKey: Key.fromPublicKeyBase58(recipientKey, KeyType.Ed25519).publicKey, + algorithm: KeyAlgs.Ed25519, + }).convertkey({ algorithm: KeyAlgs.X25519 }) + + if (senderKey && senderExchangeKey) { + const encryptedSender = CryptoBox.seal({ + recipientKey: targetExchangeKey, + message: TypedArrayEncoder.fromString(TypedArrayEncoder.toBase58(senderKey.publicBytes)), + }) + const nonce = CryptoBox.randomNonce() + const encryptedCek = CryptoBox.cryptoBox({ + recipientKey: targetExchangeKey, + senderKey: senderExchangeKey, + message: cek.secretBytes, + nonce, + }) + + recipients.push( + new JweRecipient({ + encryptedKey: encryptedCek, + header: { + kid: recipientKey, + sender: TypedArrayEncoder.toBase64URL(encryptedSender), + iv: TypedArrayEncoder.toBase64URL(nonce), + }, + }) + ) + } else { + const encryptedCek = CryptoBox.seal({ + recipientKey: targetExchangeKey, + message: cek.secretBytes, + }) + recipients.push( + new JweRecipient({ + encryptedKey: encryptedCek, + header: { + kid: recipientKey, + }, + }) + ) + } + } finally { + targetExchangeKey?.handle.free() + } + } + + const protectedJson = { + enc: 'xchacha20poly1305_ietf', + typ: 'JWM/1.0', + alg: senderKey ? 'Authcrypt' : 'Anoncrypt', + recipients: recipients.map((item) => JsonTransformer.toJSON(item)), + } + + const { ciphertext, tag, nonce } = cek.aeadEncrypt({ + message: Buffer.from(JSON.stringify(payload)), + aad: Buffer.from(JsonEncoder.toBase64URL(protectedJson)), + }).parts + + const envelope = new JweEnvelope({ + ciphertext: TypedArrayEncoder.toBase64URL(ciphertext), + iv: TypedArrayEncoder.toBase64URL(nonce), + protected: JsonEncoder.toBase64URL(protectedJson), + tag: TypedArrayEncoder.toBase64URL(tag), + }).toJson() + + return envelope as EncryptedMessage + } finally { + cek?.handle.free() + senderExchangeKey?.handle.free() + } +} + +export function didcommV1Unpack(messagePackage: EncryptedMessage, recipientKey: AskarKey) { + const protectedJson = JsonEncoder.fromBase64(messagePackage.protected) + + const alg = protectedJson.alg + if (!['Anoncrypt', 'Authcrypt'].includes(alg)) { + throw new WalletError(`Unsupported pack algorithm: ${alg}`) + } + + const recipient = protectedJson.recipients.find( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (r: any) => r.header.kid === TypedArrayEncoder.toBase58(recipientKey.publicBytes) + ) + + if (!recipient) { + throw new WalletError('No corresponding recipient key found') + } + + const sender = recipient?.header.sender ? TypedArrayEncoder.fromBase64(recipient.header.sender) : undefined + const iv = recipient?.header.iv ? TypedArrayEncoder.fromBase64(recipient.header.iv) : undefined + const encrypted_key = TypedArrayEncoder.fromBase64(recipient.encrypted_key) + + if (sender && !iv) { + throw new WalletError('Missing IV') + } else if (!sender && iv) { + throw new WalletError('Unexpected IV') + } + + let payloadKey, senderKey + + let sender_x: AskarKey | undefined + let recip_x: AskarKey | undefined + + try { + recip_x = recipientKey.convertkey({ algorithm: KeyAlgs.X25519 }) + + if (sender && iv) { + senderKey = TypedArrayEncoder.toUtf8String( + CryptoBox.sealOpen({ + recipientKey: recip_x, + ciphertext: sender, + }) + ) + sender_x = AskarKey.fromPublicBytes({ + algorithm: KeyAlgs.Ed25519, + publicKey: TypedArrayEncoder.fromBase58(senderKey), + }).convertkey({ algorithm: KeyAlgs.X25519 }) + + payloadKey = CryptoBox.open({ + recipientKey: recip_x, + senderKey: sender_x, + message: encrypted_key, + nonce: iv, + }) + } else { + payloadKey = CryptoBox.sealOpen({ ciphertext: encrypted_key, recipientKey: recip_x }) + } + } finally { + sender_x?.handle.free() + recip_x?.handle.free() + } + + if (!senderKey && alg === 'Authcrypt') { + throw new WalletError('Sender public key not provided for Authcrypt') + } + + let cek: AskarKey | undefined + try { + cek = AskarKey.fromSecretBytes({ algorithm: KeyAlgs.Chacha20C20P, secretKey: payloadKey }) + const message = cek.aeadDecrypt({ + ciphertext: TypedArrayEncoder.fromBase64(messagePackage.ciphertext), + nonce: TypedArrayEncoder.fromBase64(messagePackage.iv), + tag: TypedArrayEncoder.fromBase64(messagePackage.tag), + aad: TypedArrayEncoder.fromString(messagePackage.protected), + }) + return { + plaintextMessage: JsonEncoder.fromBuffer(message), + senderKey, + recipientKey: TypedArrayEncoder.toBase58(recipientKey.publicBytes), + } + } finally { + cek?.handle.free() + } +} diff --git a/packages/askar/src/wallet/index.ts b/packages/askar/src/wallet/index.ts index fb71764d57..49e1da0a79 100644 --- a/packages/askar/src/wallet/index.ts +++ b/packages/askar/src/wallet/index.ts @@ -1,3 +1,3 @@ export { AskarWallet } from './AskarWallet' export { AskarProfileWallet } from './AskarProfileWallet' -export * from './AskarWalletPostgresStorageConfig' +export * from './AskarWalletStorageConfig' diff --git a/packages/askar/tests/askar-inmemory.e2e.test.ts b/packages/askar/tests/askar-inmemory.e2e.test.ts index e848602aab..d98e762fa5 100644 --- a/packages/askar/tests/askar-inmemory.e2e.test.ts +++ b/packages/askar/tests/askar-inmemory.e2e.test.ts @@ -7,16 +7,16 @@ import { Subject } from 'rxjs' import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' -import { e2eTest, getSqliteAgentOptions } from './helpers' +import { e2eTest, getAskarSqliteAgentOptions } from './helpers' -const aliceInMemoryAgentOptions = getSqliteAgentOptions( +const aliceInMemoryAgentOptions = getAskarSqliteAgentOptions( 'AgentsAlice', { endpoints: ['rxjs:alice'], }, true ) -const bobInMemoryAgentOptions = getSqliteAgentOptions( +const bobInMemoryAgentOptions = getAskarSqliteAgentOptions( 'AgentsBob', { endpoints: ['rxjs:bob'], diff --git a/packages/askar/tests/askar-postgres.e2e.test.ts b/packages/askar/tests/askar-postgres.e2e.test.ts index f21c19ec73..92a7168b5f 100644 --- a/packages/askar/tests/askar-postgres.e2e.test.ts +++ b/packages/askar/tests/askar-postgres.e2e.test.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' -import type { AskarWalletPostgresStorageConfig } from '../src/wallet' import { Agent } from '@credo-ts/core' import { Subject } from 'rxjs' @@ -8,23 +7,12 @@ import { Subject } from 'rxjs' import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' -import { e2eTest, getPostgresAgentOptions } from './helpers' +import { askarPostgresStorageConfig, e2eTest, getAskarPostgresAgentOptions } from './helpers' -const storageConfig: AskarWalletPostgresStorageConfig = { - type: 'postgres', - config: { - host: 'localhost:5432', - }, - credentials: { - account: 'postgres', - password: 'postgres', - }, -} - -const alicePostgresAgentOptions = getPostgresAgentOptions('AgentsAlice', storageConfig, { +const alicePostgresAgentOptions = getAskarPostgresAgentOptions('AgentsAlice', askarPostgresStorageConfig, { endpoints: ['rxjs:alice'], }) -const bobPostgresAgentOptions = getPostgresAgentOptions('AgentsBob', storageConfig, { +const bobPostgresAgentOptions = getAskarPostgresAgentOptions('AgentsBob', askarPostgresStorageConfig, { endpoints: ['rxjs:bob'], }) diff --git a/packages/askar/tests/askar-sqlite.e2e.test.ts b/packages/askar/tests/askar-sqlite.e2e.test.ts index 3a3797ec4b..7de1f8408b 100644 --- a/packages/askar/tests/askar-sqlite.e2e.test.ts +++ b/packages/askar/tests/askar-sqlite.e2e.test.ts @@ -15,10 +15,10 @@ import { Store } from '@hyperledger/aries-askar-shared' import { tmpdir } from 'os' import path from 'path' -import { getSqliteAgentOptions } from './helpers' +import { getAskarSqliteAgentOptions } from './helpers' -const aliceAgentOptions = getSqliteAgentOptions('AgentsAlice') -const bobAgentOptions = getSqliteAgentOptions('AgentsBob') +const aliceAgentOptions = getAskarSqliteAgentOptions('AgentsAlice') +const bobAgentOptions = getAskarSqliteAgentOptions('AgentsBob') describe('Askar SQLite agents', () => { let aliceAgent: Agent diff --git a/packages/askar/tests/helpers.ts b/packages/askar/tests/helpers.ts index 0300735c1c..af6fddea15 100644 --- a/packages/askar/tests/helpers.ts +++ b/packages/askar/tests/helpers.ts @@ -15,6 +15,8 @@ import { AskarWallet } from '../src/wallet' export const askarModuleConfig = new AskarModuleConfig({ ariesAskar }) registerAriesAskar({ askar: askarModuleConfig.ariesAskar }) +export const askarModule = new AskarModule(askarModuleConfig) +export { ariesAskar } // When using the AskarWallet directly, the native dependency won't be loaded by default. // So in tests depending on Askar, we import this wallet so we're sure the native dependency is loaded. @@ -26,7 +28,18 @@ export const genesisPath = process.env.GENESIS_TXN_PATH export const publicDidSeed = process.env.TEST_AGENT_PUBLIC_DID_SEED ?? '000000000000000000000000Trustee9' -export function getPostgresAgentOptions( +export const askarPostgresStorageConfig: AskarWalletPostgresStorageConfig = { + type: 'postgres', + config: { + host: 'localhost:5432', + }, + credentials: { + account: 'postgres', + password: 'postgres', + }, +} + +export function getAskarPostgresAgentOptions( name: string, storageConfig: AskarWalletPostgresStorageConfig, extraConfig: Partial = {} @@ -55,7 +68,7 @@ export function getPostgresAgentOptions( } as const } -export function getSqliteAgentOptions(name: string, extraConfig: Partial = {}, inMemory?: boolean) { +export function getAskarSqliteAgentOptions(name: string, extraConfig: Partial = {}, inMemory?: boolean) { const random = utils.uuid().slice(0, 4) const config: InitConfig = { label: `SQLiteAgent: ${name} - ${random}`, diff --git a/packages/bbs-signatures/src/BbsModule.ts b/packages/bbs-signatures/src/BbsModule.ts index 152e92b9ff..5cd1c4206d 100644 --- a/packages/bbs-signatures/src/BbsModule.ts +++ b/packages/bbs-signatures/src/BbsModule.ts @@ -20,7 +20,7 @@ export class BbsModule implements Module { dependencyManager .resolve(AgentConfig) .logger.warn( - "The '@credo-ts/bbs-signatures' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + "The '@credo-ts/bbs-signatures' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." ) // Signing providers. diff --git a/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts b/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts index fcdc8d49b9..0a6c098624 100644 --- a/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts +++ b/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts @@ -20,14 +20,13 @@ import { W3cJsonLdVerifiableCredential, } from '@credo-ts/core' +import { RegisteredAskarTestWallet } from '../../askar/tests/helpers' import { W3cCredentialsModuleConfig } from '../../core/src/modules/vc/W3cCredentialsModuleConfig' import { SignatureSuiteRegistry } from '../../core/src/modules/vc/data-integrity/SignatureSuiteRegistry' import { W3cJsonLdCredentialService } from '../../core/src/modules/vc/data-integrity/W3cJsonLdCredentialService' import { customDocumentLoader } from '../../core/src/modules/vc/data-integrity/__tests__/documentLoader' import { LinkedDataProof } from '../../core/src/modules/vc/data-integrity/models/LinkedDataProof' -import { getAgentConfig, getAgentContext } from '../../core/tests/helpers' -import { IndySdkWallet } from '../../indy-sdk/src' -import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' +import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers' import { BbsBlsSignature2020, BbsBlsSignatureProof2020, Bls12381g2SigningProvider } from '../src' import { BbsBlsSignature2020Fixtures } from './fixtures' @@ -66,11 +65,17 @@ describeSkipNode18('BBS W3cCredentialService', () => { let agentContext: AgentContext let w3cJsonLdCredentialService: W3cJsonLdCredentialService let w3cCredentialService: W3cCredentialService - const seed = TypedArrayEncoder.fromString('testseed000000000000000000000001') const privateKey = TypedArrayEncoder.fromString('testseed000000000000000000000001') beforeAll(async () => { - wallet = new IndySdkWallet(indySdk, agentConfig.logger, signingProviderRegistry) + // Use askar wallet so we can use the signing provider registry + // TODO: support signing provider registry in memory wallet + // so we don't have to use askar here + wallet = new RegisteredAskarTestWallet( + agentConfig.logger, + new agentDependencies.FileSystem(), + signingProviderRegistry + ) await wallet.createAndOpen(agentConfig.walletConfig) agentContext = getAgentContext({ agentConfig, @@ -124,7 +129,13 @@ describeSkipNode18('BBS W3cCredentialService', () => { let verificationMethod: string beforeAll(async () => { - const key = await wallet.createKey({ keyType: KeyType.Bls12381g2, seed }) + // FIXME: askar doesn't create the same privateKey based on the same seed as when generated used askar BBS library... + // See https://github.com/hyperledger/aries-askar/issues/219 + const key = await wallet.createKey({ + keyType: KeyType.Bls12381g2, + privateKey: TypedArrayEncoder.fromBase58('2szQ7zB4tKLJPsGK3YTp9SNQ6hoWYFG5rGhmgfQM4nb7'), + }) + issuerDidKey = new DidKey(key) verificationMethod = `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}` }) @@ -140,7 +151,7 @@ describeSkipNode18('BBS W3cCredentialService', () => { format: ClaimFormat.LdpVc, credential, proofType: 'BbsBlsSignature2020', - verificationMethod: verificationMethod, + verificationMethod, }) expect(vc).toBeInstanceOf(W3cJsonLdVerifiableCredential) diff --git a/packages/bbs-signatures/tests/bbs-signing-provider.e2e.test.ts b/packages/bbs-signatures/tests/bbs-signing-provider.e2e.test.ts index 6c1ab056a6..6b8515aed0 100644 --- a/packages/bbs-signatures/tests/bbs-signing-provider.e2e.test.ts +++ b/packages/bbs-signatures/tests/bbs-signing-provider.e2e.test.ts @@ -3,9 +3,8 @@ import type { Wallet, WalletConfig } from '@credo-ts/core' import { KeyDerivationMethod, KeyType, WalletError, TypedArrayEncoder, SigningProviderRegistry } from '@credo-ts/core' import { BBS_SIGNATURE_LENGTH } from '@mattrglobal/bbs-signatures' -import testLogger from '../../core/tests/logger' -import { IndySdkWallet } from '../../indy-sdk/src' -import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' +import { RegisteredAskarTestWallet } from '../../askar/tests/helpers' +import { testLogger, agentDependencies } from '../../core/tests' import { Bls12381g2SigningProvider } from '../src' import { describeSkipNode18 } from './util' @@ -24,7 +23,11 @@ describeSkipNode18('BBS Signing Provider', () => { const message = TypedArrayEncoder.fromString('sample-message') beforeEach(async () => { - wallet = new IndySdkWallet(indySdk, testLogger, new SigningProviderRegistry([new Bls12381g2SigningProvider()])) + wallet = new RegisteredAskarTestWallet( + testLogger, + new agentDependencies.FileSystem(), + new SigningProviderRegistry([new Bls12381g2SigningProvider()]) + ) await wallet.createAndOpen(walletConfig) }) @@ -33,15 +36,24 @@ describeSkipNode18('BBS Signing Provider', () => { }) test('Create bls12381g2 keypair', async () => { - await expect(wallet.createKey({ seed, keyType: KeyType.Bls12381g2 })).resolves.toMatchObject({ - publicKeyBase58: - '25TvGExLTWRTgn9h2wZuohrQmmLafXiacY4dhv66wcbY8pLbuNTBRMTgWVcPKh2wsEyrRPmnhLdc4C7LEcJ2seoxzBkoydJEdQD8aqg5dw8wesBTS9Twg8EjuFG1WPRAiERd', - keyType: KeyType.Bls12381g2, - }) + const key = await wallet.createKey({ seed, keyType: KeyType.Bls12381g2 }) + expect(key.keyType).toStrictEqual(KeyType.Bls12381g2) + expect(key.publicKeyBase58).toStrictEqual( + 'yVLZ92FeZ3AYco43LXtJgtM8kUD1WPUyQPw4VwxZ1iYSak85GYGSJwURhVJM4R6ASRGuM9vjjSU91pKbaqTWQgLjPJjFuK8HdDmAHi3thYun9QUGjarrK7BzC11LurcpYqD' + ) }) - test('Fail to create bls12381g1g2 keypair', async () => { - await expect(wallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 })).rejects.toThrowError(WalletError) + test('Fail to sign with bls12381g1g2 keypair', async () => { + const key = await wallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) + + await expect( + wallet.sign({ + data: message, + key, + }) + ).rejects.toThrow( + 'Error signing data with verkey AeAihfn5UFf7y9oesemKE1oLmTwKMRv7fafTepespr3qceF4RUMggAbogkoC8n6rXgtJytq4oGy59DsVHxmNj9WGWwkiRnP3Sz2r924RLVbc2NdP4T7yEPsSFZPsWmLjgnP1vXHpj4bVXNcTmkUmF6mSXinF3HehnQVip14vRFuMzYVxMUh28ofTJzbtUqxMWZQRu. Unsupported keyType: bls12381g1g2' + ) }) test('Create a signature with a bls12381g2 keypair', async () => { diff --git a/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts b/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts index feaead2f3e..f6b1983c15 100644 --- a/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts +++ b/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts @@ -63,15 +63,18 @@ describeSkipNode18('credentials, BBS+ signature', () => { } = await setupJsonLdTests({ issuerName: 'Faber Agent Credentials LD BBS+', holderName: 'Alice Agent Credentials LD BBS+', + useBbs: true, })) await faberAgent.context.wallet.createKey({ keyType: KeyType.Ed25519, privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), }) + // FIXME: askar doesn't create the same privateKey based on the same seed as when generated used askar BBS library... + // See https://github.com/hyperledger/aries-askar/issues/219 await faberAgent.context.wallet.createKey({ keyType: KeyType.Bls12381g2, - seed: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + privateKey: TypedArrayEncoder.fromBase58('2szQ7zB4tKLJPsGK3YTp9SNQ6hoWYFG5rGhmgfQM4nb7'), }) }) diff --git a/packages/cheqd/package.json b/packages/cheqd/package.json index 2ee4f995d2..f13502ab40 100644 --- a/packages/cheqd/package.json +++ b/packages/cheqd/package.json @@ -37,8 +37,6 @@ "tsyringe": "^4.8.0" }, "devDependencies": { - "@credo-ts/indy-sdk": "0.4.2", - "@types/indy-sdk": "*", "rimraf": "^4.0.7", "typescript": "~4.9.4" } diff --git a/packages/cheqd/src/CheqdModule.ts b/packages/cheqd/src/CheqdModule.ts index fa9d393f44..af739a194e 100644 --- a/packages/cheqd/src/CheqdModule.ts +++ b/packages/cheqd/src/CheqdModule.ts @@ -18,7 +18,7 @@ export class CheqdModule implements Module { dependencyManager .resolve(AgentConfig) .logger.warn( - "The '@credo-ts/cheqd' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + "The '@credo-ts/cheqd' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." ) // Register config diff --git a/packages/cheqd/tests/cheqd-did-registrar.e2e.test.ts b/packages/cheqd/tests/cheqd-did-registrar.e2e.test.ts index 69f0f524c2..d86a9c7f77 100644 --- a/packages/cheqd/tests/cheqd-did-registrar.e2e.test.ts +++ b/packages/cheqd/tests/cheqd-did-registrar.e2e.test.ts @@ -4,12 +4,12 @@ import type { DidDocument } from '@credo-ts/core' import { Agent, TypedArrayEncoder } from '@credo-ts/core' import { generateKeyPairFromSeed } from '@stablelib/ed25519' -import { getAgentOptions } from '../../core/tests/helpers' +import { getInMemoryAgentOptions } from '../../core/tests/helpers' import { validService } from './setup' import { getCheqdModules } from './setupCheqdModule' -const agentOptions = getAgentOptions('Faber Dids Registrar', {}, getCheqdModules()) +const agentOptions = getInMemoryAgentOptions('Faber Dids Registrar', {}, getCheqdModules()) describe('Cheqd DID registrar', () => { let agent: Agent> diff --git a/packages/cheqd/tests/cheqd-did-resolver.e2e.test.ts b/packages/cheqd/tests/cheqd-did-resolver.e2e.test.ts index 4a11e53ed5..498cd6e4ec 100644 --- a/packages/cheqd/tests/cheqd-did-resolver.e2e.test.ts +++ b/packages/cheqd/tests/cheqd-did-resolver.e2e.test.ts @@ -1,13 +1,13 @@ import { Agent, JsonTransformer } from '@credo-ts/core' -import { getAgentOptions } from '../../core/tests/helpers' +import { getInMemoryAgentOptions } from '../../core/tests/helpers' import { getClosestResourceVersion } from '../src/dids/didCheqdUtil' import { DefaultRPCUrl } from '../src/ledger/CheqdLedgerService' import { getCheqdModules } from './setupCheqdModule' export const resolverAgent = new Agent( - getAgentOptions('Cheqd resolver', {}, getCheqdModules(undefined, DefaultRPCUrl.Testnet)) + getInMemoryAgentOptions('Cheqd resolver', {}, getCheqdModules(undefined, DefaultRPCUrl.Testnet)) ) describe('Cheqd DID resolver', () => { diff --git a/packages/cheqd/tests/cheqd-sdk-anoncreds-registry.e2e.test.ts b/packages/cheqd/tests/cheqd-sdk-anoncreds-registry.e2e.test.ts index 77c6caaf6c..39802bb20e 100644 --- a/packages/cheqd/tests/cheqd-sdk-anoncreds-registry.e2e.test.ts +++ b/packages/cheqd/tests/cheqd-sdk-anoncreds-registry.e2e.test.ts @@ -2,21 +2,21 @@ import type { CheqdDidCreateOptions } from '../src' import { Agent, JsonTransformer, TypedArrayEncoder } from '@credo-ts/core' -import { agentDependencies, getAgentConfig } from '../../core/tests/helpers' +import { getInMemoryAgentOptions } from '../../core/tests/helpers' import { CheqdAnonCredsRegistry } from '../src/anoncreds' import { resolverAgent } from './cheqd-did-resolver.e2e.test' import { getCheqdModules } from './setupCheqdModule' -const agentConfig = getAgentConfig('cheqdAnonCredsRegistry') - -const agent = new Agent({ - config: agentConfig, - dependencies: agentDependencies, - modules: getCheqdModules( - 'ugly dirt sorry girl prepare argue door man that manual glow scout bomb pigeon matter library transfer flower clown cat miss pluck drama dizzy' - ), -}) +const agent = new Agent( + getInMemoryAgentOptions( + 'cheqdAnonCredsRegistry', + {}, + getCheqdModules( + 'ugly dirt sorry girl prepare argue door man that manual glow scout bomb pigeon matter library transfer flower clown cat miss pluck drama dizzy' + ) + ) +) const cheqdAnonCredsRegistry = new CheqdAnonCredsRegistry() diff --git a/packages/cheqd/tests/setupCheqdModule.ts b/packages/cheqd/tests/setupCheqdModule.ts index 885f7a2e5d..b4a6f39fea 100644 --- a/packages/cheqd/tests/setupCheqdModule.ts +++ b/packages/cheqd/tests/setupCheqdModule.ts @@ -1,16 +1,9 @@ import type { CheqdModuleConfigOptions } from '../src' import { DidsModule } from '@credo-ts/core' -import { IndySdkModule, IndySdkModuleConfig } from '@credo-ts/indy-sdk' -import indySdk from 'indy-sdk' import { CheqdModule, CheqdDidRegistrar, CheqdDidResolver } from '../src' -export const getIndySdkModuleConfig = () => - new IndySdkModuleConfig({ - indySdk, - }) - export const getCheqdModuleConfig = (seed?: string, rpcUrl?: string) => ({ networks: [ @@ -30,5 +23,4 @@ export const getCheqdModules = (seed?: string, rpcUrl?: string) => ({ registrars: [new CheqdDidRegistrar()], resolvers: [new CheqdDidResolver()], }), - indySdk: new IndySdkModule(getIndySdkModuleConfig()), }) diff --git a/packages/core/src/agent/Agent.ts b/packages/core/src/agent/Agent.ts index e3c0fda86d..891965b0a9 100644 --- a/packages/core/src/agent/Agent.ts +++ b/packages/core/src/agent/Agent.ts @@ -78,7 +78,7 @@ export class Agent extends BaseAge // Register possibly already defined services if (!dependencyManager.isRegistered(InjectionSymbols.Wallet)) { throw new AriesFrameworkError( - "Missing required dependency: 'Wallet'. You can register it using one of the provided modules such as the AskarModule or the IndySdkModule, or implement your own." + "Missing required dependency: 'Wallet'. You can register it using the AskarModule, or implement your own." ) } if (!dependencyManager.isRegistered(InjectionSymbols.Logger)) { @@ -86,7 +86,7 @@ export class Agent extends BaseAge } if (!dependencyManager.isRegistered(InjectionSymbols.StorageService)) { throw new AriesFrameworkError( - "Missing required dependency: 'StorageService'. You can register it using one of the provided modules such as the AskarModule or the IndySdkModule, or implement your own." + "Missing required dependency: 'StorageService'. You can register it using the AskarModule, or implement your own." ) } diff --git a/packages/core/src/agent/Dispatcher.ts b/packages/core/src/agent/Dispatcher.ts index 5301745040..4a2bf62a38 100644 --- a/packages/core/src/agent/Dispatcher.ts +++ b/packages/core/src/agent/Dispatcher.ts @@ -89,11 +89,7 @@ class Dispatcher { outboundMessage.inboundMessageContext = messageContext } - if (outboundMessage.isOutboundServiceMessage()) { - await this.messageSender.sendMessageToService(outboundMessage) - } else { - await this.messageSender.sendMessage(outboundMessage) - } + await this.messageSender.sendMessage(outboundMessage) } // Emit event that allows to hook into received messages this.eventEmitter.emit(agentContext, { diff --git a/packages/core/src/agent/MessageSender.ts b/packages/core/src/agent/MessageSender.ts index 31fc4fac70..bcf23f93b8 100644 --- a/packages/core/src/agent/MessageSender.ts +++ b/packages/core/src/agent/MessageSender.ts @@ -225,6 +225,7 @@ export class MessageSender { if (session) { this.logger.debug(`Found session with return routing for message '${message.id}' (connection '${connection.id}'`) + try { await this.sendMessageToSession(agentContext, session, message) this.emitMessageSentEvent(outboundMessageContext, OutboundMessageSendStatus.SentToSession) @@ -358,10 +359,7 @@ export class MessageSender { ) } - /** - * @deprecated Use `sendMessage` directly instead. Will be made private in 0.5.0 - */ - public async sendMessageToService(outboundMessageContext: OutboundMessageContext) { + private async sendMessageToService(outboundMessageContext: OutboundMessageContext) { const session = this.findSessionForOutboundContext(outboundMessageContext) if (session) { diff --git a/packages/core/src/agent/__tests__/Agent.test.ts b/packages/core/src/agent/__tests__/Agent.test.ts index 22268ccb93..3c2e3d4dcd 100644 --- a/packages/core/src/agent/__tests__/Agent.test.ts +++ b/packages/core/src/agent/__tests__/Agent.test.ts @@ -2,8 +2,8 @@ import type { DependencyManager, Module } from '../../plugins' import { injectable } from 'tsyringe' -import { getIndySdkModules } from '../../../../indy-sdk/tests/setupIndySdkModule' -import { getAgentOptions } from '../../../tests/helpers' +import { InMemoryWalletModule } from '../../../../../tests/InMemoryWalletModule' +import { getInMemoryAgentOptions } from '../../../tests/helpers' import { InjectionSymbols } from '../../constants' import { BasicMessageRepository, BasicMessageService } from '../../modules/basic-messages' import { BasicMessagesApi } from '../../modules/basic-messages/BasicMessagesApi' @@ -33,7 +33,7 @@ import { FeatureRegistry } from '../FeatureRegistry' import { MessageReceiver } from '../MessageReceiver' import { MessageSender } from '../MessageSender' -const agentOptions = getAgentOptions('Agent Class Test', {}, getIndySdkModules()) +const agentOptions = getInMemoryAgentOptions('Agent Class Test') const myModuleMethod = jest.fn() @injectable() @@ -61,7 +61,7 @@ describe('Agent', () => { ...agentOptions, modules: { myModule: new MyModule(), - ...getIndySdkModules(), + inMemory: new InMemoryWalletModule(), }, }) @@ -79,7 +79,7 @@ describe('Agent', () => { mediationRecipient: new MediationRecipientModule({ maximumMessagePickup: 42, }), - ...getIndySdkModules(), + inMemory: new InMemoryWalletModule(), }, }) diff --git a/packages/core/src/agent/__tests__/MessageSender.test.ts b/packages/core/src/agent/__tests__/MessageSender.test.ts index 2c596a37e6..6c12cf1e01 100644 --- a/packages/core/src/agent/__tests__/MessageSender.test.ts +++ b/packages/core/src/agent/__tests__/MessageSender.test.ts @@ -335,6 +335,7 @@ describe('MessageSender', () => { transportServiceFindSessionByIdMock.mockReturnValue(session) messageSender.registerOutboundTransport(outboundTransport) const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage') + // @ts-ignore const sendMessageToServiceSpy = jest.spyOn(messageSender, 'sendMessageToService') const contextWithSessionId = new OutboundMessageContext(outboundMessageContext.message, { @@ -521,7 +522,7 @@ describe('MessageSender', () => { service, }, }) - await expect(messageSender.sendMessageToService(outboundMessageContext)).rejects.toThrow( + await expect(messageSender.sendMessage(outboundMessageContext)).rejects.toThrow( `Agent has no outbound transport!` ) @@ -549,7 +550,7 @@ describe('MessageSender', () => { }, }) - await messageSender.sendMessageToService(outboundMessageContext) + await messageSender.sendMessage(outboundMessageContext) expect(eventListenerMock).toHaveBeenCalledWith({ type: AgentEventTypes.AgentMessageSent, @@ -585,7 +586,7 @@ describe('MessageSender', () => { }, }) - await messageSender.sendMessageToService(outboundMessageContext) + await messageSender.sendMessage(outboundMessageContext) expect(eventListenerMock).toHaveBeenCalledWith({ type: AgentEventTypes.AgentMessageSent, @@ -616,7 +617,7 @@ describe('MessageSender', () => { }, }) - await expect(messageSender.sendMessageToService(outboundMessageContext)).rejects.toThrow( + await expect(messageSender.sendMessage(outboundMessageContext)).rejects.toThrow( /Unable to send message to service/ ) expect(eventListenerMock).toHaveBeenCalledWith({ diff --git a/packages/core/src/crypto/__tests__/JwsService.test.ts b/packages/core/src/crypto/__tests__/JwsService.test.ts index f43de829a1..d6654aaa0d 100644 --- a/packages/core/src/crypto/__tests__/JwsService.test.ts +++ b/packages/core/src/crypto/__tests__/JwsService.test.ts @@ -1,15 +1,14 @@ import type { AgentContext } from '../../agent' import type { Key, Wallet } from '@credo-ts/core' -import { RegisteredAskarTestWallet } from '../../../../askar/tests/helpers' -import { agentDependencies, getAgentConfig, getAgentContext } from '../../../tests/helpers' +import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' +import { getAgentConfig, getAgentContext } from '../../../tests/helpers' import { DidKey } from '../../modules/dids' import { JsonEncoder, TypedArrayEncoder } from '../../utils' import { JwsService } from '../JwsService' import { KeyType } from '../KeyType' import { JwaSignatureAlgorithm } from '../jose/jwa' import { getJwkFromKey } from '../jose/jwk' -import { SigningProviderRegistry } from '../signing-provider' import * as didJwsz6Mkf from './__fixtures__/didJwsz6Mkf' import * as didJwsz6Mkv from './__fixtures__/didJwsz6Mkv' @@ -25,11 +24,7 @@ describe('JwsService', () => { beforeAll(async () => { const config = getAgentConfig('JwsService') - wallet = new RegisteredAskarTestWallet( - config.logger, - new agentDependencies.FileSystem(), - new SigningProviderRegistry([]) - ) + wallet = new InMemoryWallet() agentContext = getAgentContext({ wallet, }) diff --git a/packages/core/src/decorators/signature/SignatureDecoratorUtils.test.ts b/packages/core/src/decorators/signature/SignatureDecoratorUtils.test.ts index 93d3610f29..e3fbdbfe11 100644 --- a/packages/core/src/decorators/signature/SignatureDecoratorUtils.test.ts +++ b/packages/core/src/decorators/signature/SignatureDecoratorUtils.test.ts @@ -1,10 +1,8 @@ import type { Wallet } from '../../wallet' -import { IndySdkWallet } from '../../../../indy-sdk/src' -import { indySdk } from '../../../../indy-sdk/tests/setupIndySdkModule' +import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' import { getAgentConfig } from '../../../tests/helpers' import { KeyType } from '../../crypto' -import { SigningProviderRegistry } from '../../crypto/signing-provider' import { TypedArrayEncoder } from '../../utils' import { SignatureDecorator } from './SignatureDecorator' @@ -47,7 +45,7 @@ describe('Decorators | Signature | SignatureDecoratorUtils', () => { beforeAll(async () => { const config = getAgentConfig('SignatureDecoratorUtilsTest') - wallet = new IndySdkWallet(indySdk, config.logger, new SigningProviderRegistry([])) + wallet = new InMemoryWallet() // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await wallet.createAndOpen(config.walletConfig!) }) diff --git a/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts b/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts index 77f746030c..57ceae6bb5 100644 --- a/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts +++ b/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts @@ -6,29 +6,20 @@ import { Subject } from 'rxjs' import { SubjectInboundTransport } from '../../../../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' -import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' -import { getAgentOptions, makeConnection, waitForBasicMessage } from '../../../../tests/helpers' +import { getInMemoryAgentOptions, makeConnection, waitForBasicMessage } from '../../../../tests/helpers' import testLogger from '../../../../tests/logger' import { Agent } from '../../../agent/Agent' import { MessageSendingError, RecordNotFoundError } from '../../../error' import { BasicMessage } from '../messages' import { BasicMessageRecord } from '../repository' -const faberConfig = getAgentOptions( - 'Faber Basic Messages', - { - endpoints: ['rxjs:faber'], - }, - getIndySdkModules() -) - -const aliceConfig = getAgentOptions( - 'Alice Basic Messages', - { - endpoints: ['rxjs:alice'], - }, - getIndySdkModules() -) +const faberConfig = getInMemoryAgentOptions('Faber Basic Messages', { + endpoints: ['rxjs:faber'], +}) + +const aliceConfig = getInMemoryAgentOptions('Alice Basic Messages', { + endpoints: ['rxjs:alice'], +}) describe('Basic Messages E2E', () => { let faberAgent: Agent diff --git a/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts index 378596a888..6ec32e78c3 100644 --- a/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts +++ b/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts @@ -5,8 +5,7 @@ import type { Routing } from '../services/ConnectionService' import { Subject } from 'rxjs' -import { IndySdkWallet } from '../../../../../indy-sdk/src' -import { indySdk } from '../../../../../indy-sdk/tests/setupIndySdkModule' +import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' import { getAgentConfig, getAgentContext, @@ -18,7 +17,6 @@ import { AgentMessage } from '../../../agent/AgentMessage' import { EventEmitter } from '../../../agent/EventEmitter' import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' import { Key, KeyType } from '../../../crypto' -import { SigningProviderRegistry } from '../../../crypto/signing-provider' import { signData, unpackAndVerifySignatureDecorator } from '../../../decorators/signature/SignatureDecoratorUtils' import { JsonTransformer } from '../../../utils/JsonTransformer' import { indyDidFromPublicKeyBase58 } from '../../../utils/did' @@ -92,7 +90,7 @@ describe('ConnectionService', () => { let agentContext: AgentContext beforeAll(async () => { - wallet = new IndySdkWallet(indySdk, agentConfig.logger, new SigningProviderRegistry([])) + wallet = new InMemoryWallet() agentContext = getAgentContext({ wallet, agentConfig, diff --git a/packages/core/src/modules/connections/__tests__/connection-manual.e2e.test.ts b/packages/core/src/modules/connections/__tests__/connection-manual.e2e.test.ts index 9e029a27df..fe519c8950 100644 --- a/packages/core/src/modules/connections/__tests__/connection-manual.e2e.test.ts +++ b/packages/core/src/modules/connections/__tests__/connection-manual.e2e.test.ts @@ -4,9 +4,8 @@ import type { ConnectionStateChangedEvent } from '../ConnectionEvents' import { firstValueFrom } from 'rxjs' import { filter, first, map, timeout } from 'rxjs/operators' -import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' import { setupSubjectTransports } from '../../../../tests' -import { getAgentOptions } from '../../../../tests/helpers' +import { getInMemoryAgentOptions } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { ConnectionEventTypes } from '../ConnectionEvents' import { ConnectionsModule } from '../ConnectionsModule' @@ -46,39 +45,36 @@ describe('Manual Connection Flow', () => { // This test was added to reproduce a bug where all connections based on a reusable invitation would use the same keys // This was only present in the manual flow, which is almost never used. it('can connect multiple times using the same reusable invitation without manually using the connections api', async () => { - const aliceAgentOptions = getAgentOptions( + const aliceAgentOptions = getInMemoryAgentOptions( 'Manual Connection Flow Alice', { label: 'alice', endpoints: ['rxjs:alice'], }, { - ...getIndySdkModules(), connections: new ConnectionsModule({ autoAcceptConnections: false, }), } ) - const bobAgentOptions = getAgentOptions( + const bobAgentOptions = getInMemoryAgentOptions( 'Manual Connection Flow Bob', { label: 'bob', endpoints: ['rxjs:bob'], }, { - ...getIndySdkModules(), connections: new ConnectionsModule({ autoAcceptConnections: false, }), } ) - const faberAgentOptions = getAgentOptions( + const faberAgentOptions = getInMemoryAgentOptions( 'Manual Connection Flow Faber', { endpoints: ['rxjs:faber'], }, { - ...getIndySdkModules(), connections: new ConnectionsModule({ autoAcceptConnections: false, }), diff --git a/packages/core/src/modules/connections/__tests__/did-rotate.e2e.test.ts b/packages/core/src/modules/connections/__tests__/did-rotate.e2e.test.ts index 14c8496094..a6f885cb5f 100644 --- a/packages/core/src/modules/connections/__tests__/did-rotate.e2e.test.ts +++ b/packages/core/src/modules/connections/__tests__/did-rotate.e2e.test.ts @@ -5,10 +5,9 @@ import type { ConnectionRecord } from '../repository' import { ReplaySubject, first, firstValueFrom, timeout } from 'rxjs' import { MessageSender } from '../../..//agent/MessageSender' -import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' import { setupSubjectTransports, testLogger } from '../../../../tests' import { - getAgentOptions, + getInMemoryAgentOptions, makeConnection, waitForAgentMessageProcessedEvent, waitForBasicMessage, @@ -32,7 +31,7 @@ describe('Rotation E2E tests', () => { let bobAliceConnection: ConnectionRecord | undefined beforeEach(async () => { - const aliceAgentOptions = getAgentOptions( + const aliceAgentOptions = getInMemoryAgentOptions( 'DID Rotate Alice', { label: 'alice', @@ -40,13 +39,12 @@ describe('Rotation E2E tests', () => { logger: testLogger, }, { - ...getIndySdkModules(), connections: new ConnectionsModule({ autoAcceptConnections: true, }), } ) - const bobAgentOptions = getAgentOptions( + const bobAgentOptions = getInMemoryAgentOptions( 'DID Rotate Bob', { label: 'bob', @@ -54,7 +52,6 @@ describe('Rotation E2E tests', () => { logger: testLogger, }, { - ...getIndySdkModules(), connections: new ConnectionsModule({ autoAcceptConnections: true, }), diff --git a/packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts b/packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts index 50cf2742ee..3edcc48d0f 100644 --- a/packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts +++ b/packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts @@ -4,9 +4,8 @@ import type { ConnectionStateChangedEvent } from '../ConnectionEvents' import { firstValueFrom } from 'rxjs' import { filter, first, map, timeout } from 'rxjs/operators' -import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' import { setupSubjectTransports } from '../../../../tests' -import { getAgentOptions } from '../../../../tests/helpers' +import { getInMemoryAgentOptions } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { uuid } from '../../../utils/uuid' import { DidsModule, PeerDidNumAlgo, createPeerDidDocumentFromServices } from '../../dids' @@ -96,14 +95,13 @@ async function didExchangeNumAlgoBaseTest(options: { // Make a common in-memory did registry for both agents const didRegistry = new InMemoryDidRegistry() - const aliceAgentOptions = getAgentOptions( + const aliceAgentOptions = getInMemoryAgentOptions( 'DID Exchange numalgo settings Alice', { label: 'alice', endpoints: ['rxjs:alice'], }, { - ...getIndySdkModules(), connections: new ConnectionsModule({ autoAcceptConnections: false, peerNumAlgoForDidExchangeRequests: options.requesterNumAlgoSetting, @@ -111,13 +109,12 @@ async function didExchangeNumAlgoBaseTest(options: { dids: new DidsModule({ registrars: [didRegistry], resolvers: [didRegistry] }), } ) - const faberAgentOptions = getAgentOptions( + const faberAgentOptions = getInMemoryAgentOptions( 'DID Exchange numalgo settings Alice', { endpoints: ['rxjs:faber'], }, { - ...getIndySdkModules(), connections: new ConnectionsModule({ autoAcceptConnections: false, peerNumAlgoForDidExchangeRequests: options.responderNumAlgoSetting, diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.e2e.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.e2e.test.ts index 7fdb14f74a..2a90d4ec88 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.e2e.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.e2e.test.ts @@ -8,10 +8,10 @@ import { ReplaySubject, Subject } from 'rxjs' import { SubjectInboundTransport } from '../../../../../../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../../../../../../tests/transport/SubjectOutboundTransport' import { - getLegacyAnonCredsModules, + getAnonCredsIndyModules, prepareForAnonCredsIssuance, } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' -import { waitForCredentialRecordSubject, getAgentOptions } from '../../../../../../tests/helpers' +import { waitForCredentialRecordSubject, getInMemoryAgentOptions } from '../../../../../../tests/helpers' import testLogger from '../../../../../../tests/logger' import { Agent } from '../../../../../agent/Agent' import { CredentialEventTypes } from '../../../CredentialEvents' @@ -20,20 +20,20 @@ import { CredentialState } from '../../../models/CredentialState' import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' import { V2CredentialPreview } from '../messages' -const faberAgentOptions = getAgentOptions( +const faberAgentOptions = getInMemoryAgentOptions( 'Faber connection-less Credentials V2', { endpoints: ['rxjs:faber'], }, - getLegacyAnonCredsModules() + getAnonCredsIndyModules() ) -const aliceAgentOptions = getAgentOptions( +const aliceAgentOptions = getInMemoryAgentOptions( 'Alice connection-less Credentials V2', { endpoints: ['rxjs:alice'], }, - getLegacyAnonCredsModules() + getAnonCredsIndyModules() ) const credentialPreview = V2CredentialPreview.fromRecord({ diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-credentials-auto-accept.e2e.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-credentials-auto-accept.e2e.test.ts index 88422a79c4..5e909c1457 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-credentials-auto-accept.e2e.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-credentials-auto-accept.e2e.test.ts @@ -2,11 +2,16 @@ import type { AnonCredsTestsAgent } from '../../../../../../../anoncreds/tests/l import type { EventReplaySubject } from '../../../../../../tests' import { setupAnonCredsTests } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' -import { waitForCredentialRecord, waitForCredentialRecordSubject } from '../../../../../../tests/helpers' +import { + waitForCredentialRecord, + waitForCredentialRecordSubject, + waitForAgentMessageProcessedEventSubject, +} from '../../../../../../tests/helpers' import testLogger from '../../../../../../tests/logger' import { AutoAcceptCredential } from '../../../models/CredentialAutoAcceptType' import { CredentialState } from '../../../models/CredentialState' import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import { V2ProposeCredentialMessage } from '../messages' import { V2CredentialPreview } from '../messages/V2CredentialPreview' describe('V2 Credentials Auto Accept', () => { @@ -435,6 +440,13 @@ describe('V2 Credentials Auto Accept', () => { state: CredentialState.ProposalReceived, threadId: aliceCredentialRecord.threadId, }) + + // ProposalReceived is emitted before the whole message is finished processing + // So to not get errors when shutting down the agent, we wait for the message to be processed + await waitForAgentMessageProcessedEventSubject(faberReplay, { + threadId: aliceCredentialRecord.threadId, + messageType: V2ProposeCredentialMessage.type.messageTypeUri, + }) }) }) }) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts index 860b29a43e..bde3f9def5 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts @@ -1,30 +1,19 @@ import type { EventReplaySubject } from '../../../../../../tests' -import { randomUUID } from 'crypto' - import { LegacyIndyCredentialFormatService, LegacyIndyProofFormatService, V1CredentialProtocol, V1ProofProtocol, - AnonCredsModule, } from '../../../../../../../anoncreds/src' -import { prepareForAnonCredsIssuance } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' import { - IndySdkAnonCredsRegistry, - IndySdkIndyDidRegistrar, - IndySdkIndyDidResolver, - IndySdkModule, - IndySdkSovDidResolver, -} from '../../../../../../../indy-sdk/src' -import { indySdk } from '../../../../../../../indy-sdk/tests/setupIndySdkModule' + getAnonCredsIndyModules, + prepareForAnonCredsIssuance, +} from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' import { + getInMemoryAgentOptions, setupEventReplaySubjects, setupSubjectTransports, - genesisPath, - taaAcceptanceMechanism, - taaVersion, - getAgentOptions, waitForCredentialRecordSubject, testLogger, makeConnection, @@ -34,7 +23,6 @@ import { KeyType } from '../../../../../crypto' import { TypedArrayEncoder } from '../../../../../utils' import { JsonTransformer } from '../../../../../utils/JsonTransformer' import { CacheModule, InMemoryLruCache } from '../../../../cache' -import { DidsModule } from '../../../../dids' import { ProofEventTypes, ProofsModule, V2ProofProtocol } from '../../../../proofs' import { W3cCredentialsModule } from '../../../../vc' import { customDocumentLoader } from '../../../../vc/data-integrity/__tests__/documentLoader' @@ -88,6 +76,7 @@ const indyProofFormat = new LegacyIndyProofFormatService() const getIndyJsonLdModules = () => ({ + ...getAnonCredsIndyModules(), credentials: new CredentialsModule({ credentialProtocols: [ new V1CredentialProtocol({ indyCredentialFormat }), @@ -104,25 +93,6 @@ const getIndyJsonLdModules = () => }), ], }), - anoncreds: new AnonCredsModule({ - registries: [new IndySdkAnonCredsRegistry()], - }), - dids: new DidsModule({ - resolvers: [new IndySdkSovDidResolver(), new IndySdkIndyDidResolver()], - registrars: [new IndySdkIndyDidRegistrar()], - }), - indySdk: new IndySdkModule({ - indySdk, - networks: [ - { - isProduction: false, - genesisPath, - id: randomUUID(), - indyNamespace: `pool:localtest`, - transactionAuthorAgreement: { version: taaVersion, acceptanceMechanism: taaAcceptanceMechanism }, - }, - ], - }), cache: new CacheModule({ cache: new InMemoryLruCache({ limit: 100 }), }), @@ -142,7 +112,7 @@ describe('V2 Credentials - JSON-LD - Ed25519', () => { beforeAll(async () => { faberAgent = new Agent( - getAgentOptions( + getInMemoryAgentOptions( 'Faber Agent Indy/JsonLD', { endpoints: ['rxjs:faber'], @@ -151,7 +121,7 @@ describe('V2 Credentials - JSON-LD - Ed25519', () => { ) ) aliceAgent = new Agent( - getAgentOptions( + getInMemoryAgentOptions( 'Alice Agent Indy/JsonLD', { endpoints: ['rxjs:alice'], diff --git a/packages/core/src/modules/dids/__tests__/DidsApi.test.ts b/packages/core/src/modules/dids/__tests__/DidsApi.test.ts index 7760694d8d..274a5bc031 100644 --- a/packages/core/src/modules/dids/__tests__/DidsApi.test.ts +++ b/packages/core/src/modules/dids/__tests__/DidsApi.test.ts @@ -1,6 +1,4 @@ -import { IndySdkModule } from '../../../../../indy-sdk/src' -import { indySdk } from '../../../../tests' -import { getAgentOptions } from '../../../../tests/helpers' +import { getInMemoryAgentOptions } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { isLongFormDidPeer4, isShortFormDidPeer4 } from '../methods/peer/peerDidNumAlgo4' @@ -13,15 +11,7 @@ import { createPeerDidDocumentFromServices, } from '@credo-ts/core' -const agentOptions = getAgentOptions( - 'DidsApi', - {}, - { - indySdk: new IndySdkModule({ - indySdk, - }), - } -) +const agentOptions = getInMemoryAgentOptions('DidsApi') const agent = new Agent(agentOptions) @@ -70,6 +60,8 @@ describe('DidsApi', () => { method: 'key', methodSpecificIdentifier: 'z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty', role: 'created', + alternativeDids: undefined, + recipientKeyFingerprints: [], }) expect(createdDids[0].toJSON()).toMatchObject({ @@ -153,6 +145,8 @@ describe('DidsApi', () => { method: 'peer', methodSpecificIdentifier: '0z6Mkhu3G8viiebsWmCiSgWiQoCZrTeuX76oLDow81YNYvJQM', role: 'created', + alternativeDids: undefined, + recipientKeyFingerprints: [], }) expect(createdDids[0].toJSON()).toMatchObject({ diff --git a/packages/core/src/modules/dids/__tests__/dids-registrar.e2e.test.ts b/packages/core/src/modules/dids/__tests__/dids-registrar.e2e.test.ts index 1f6478d73e..457cb2d73d 100644 --- a/packages/core/src/modules/dids/__tests__/dids-registrar.e2e.test.ts +++ b/packages/core/src/modules/dids/__tests__/dids-registrar.e2e.test.ts @@ -1,24 +1,14 @@ import type { KeyDidCreateOptions } from '../methods/key/KeyDidRegistrar' import type { PeerDidNumAlgo0CreateOptions } from '../methods/peer/PeerDidRegistrar' -import { IndySdkModule } from '../../../../../indy-sdk/src' -import { indySdk } from '../../../../tests' -import { getAgentOptions } from '../../../../tests/helpers' +import { getInMemoryAgentOptions } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { KeyType } from '../../../crypto' import { PeerDidNumAlgo } from '../methods/peer/didPeer' import { JsonTransformer, TypedArrayEncoder } from '@credo-ts/core' -const agentOptions = getAgentOptions( - 'Faber Dids Registrar', - {}, - { - indySdk: new IndySdkModule({ - indySdk, - }), - } -) +const agentOptions = getInMemoryAgentOptions('Faber Dids Registrar') describe('dids', () => { let agent: Agent diff --git a/packages/core/src/modules/dids/__tests__/dids-resolver.e2e.test.ts b/packages/core/src/modules/dids/__tests__/dids-resolver.e2e.test.ts index 3e46ada4f0..feba6ee688 100644 --- a/packages/core/src/modules/dids/__tests__/dids-resolver.e2e.test.ts +++ b/packages/core/src/modules/dids/__tests__/dids-resolver.e2e.test.ts @@ -1,20 +1,8 @@ -import { IndySdkModule } from '../../../../../indy-sdk/src' -import { indySdk } from '../../../../tests' -import { getAgentOptions } from '../../../../tests/helpers' +import { getInMemoryAgentOptions } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { JsonTransformer } from '../../../utils' -const agent = new Agent( - getAgentOptions( - 'Faber Dids', - {}, - { - indySdk: new IndySdkModule({ - indySdk, - }), - } - ) -) +const agent = new Agent(getInMemoryAgentOptions('Faber Dids')) describe('dids', () => { beforeAll(async () => { diff --git a/packages/core/src/modules/dids/__tests__/peer-did.test.ts b/packages/core/src/modules/dids/__tests__/peer-did.test.ts index 566e580d17..6f5afc193b 100644 --- a/packages/core/src/modules/dids/__tests__/peer-did.test.ts +++ b/packages/core/src/modules/dids/__tests__/peer-did.test.ts @@ -4,13 +4,11 @@ import type { Wallet } from '../../../wallet' import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' -import { IndySdkWallet } from '../../../../../indy-sdk/src' -import { indySdk } from '../../../../../indy-sdk/tests/setupIndySdkModule' +import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' import { getAgentConfig, getAgentContext } from '../../../../tests/helpers' import { EventEmitter } from '../../../agent/EventEmitter' import { InjectionSymbols } from '../../../constants' import { Key, KeyType } from '../../../crypto' -import { SigningProviderRegistry } from '../../../crypto/signing-provider' import { JsonTransformer, TypedArrayEncoder } from '../../../utils' import { DidsModuleConfig } from '../DidsModuleConfig' import { @@ -41,7 +39,7 @@ describe('peer dids', () => { let eventEmitter: EventEmitter beforeEach(async () => { - wallet = new IndySdkWallet(indySdk, config.logger, new SigningProviderRegistry([])) + wallet = new InMemoryWallet() const storageService = new InMemoryStorageService() eventEmitter = new EventEmitter(config.agentDependencies, new Subject()) didRepository = new DidRepository(storageService, eventEmitter) diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts index 7cb2c86c5a..ee426de40f 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts @@ -16,7 +16,7 @@ export class DifPresentationExchangeModule implements Module { dependencyManager .resolve(AgentConfig) .logger.warn( - "The 'DifPresentationExchangeModule' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + "The 'DifPresentationExchangeModule' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." ) // service diff --git a/packages/core/src/modules/discover-features/__tests__/v1-discover-features.e2e.test.ts b/packages/core/src/modules/discover-features/__tests__/v1-discover-features.e2e.test.ts index 68fb1a0103..9ca14efd37 100644 --- a/packages/core/src/modules/discover-features/__tests__/v1-discover-features.e2e.test.ts +++ b/packages/core/src/modules/discover-features/__tests__/v1-discover-features.e2e.test.ts @@ -6,29 +6,20 @@ import type { import { ReplaySubject } from 'rxjs' -import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' import { setupSubjectTransports } from '../../../../tests' -import { getAgentOptions, makeConnection } from '../../../../tests/helpers' +import { getInMemoryAgentOptions, makeConnection } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { DiscoverFeaturesEventTypes } from '../DiscoverFeaturesEvents' import { waitForDisclosureSubject, waitForQuerySubject } from './helpers' -const faberAgentOptions = getAgentOptions( - 'Faber Discover Features V1 E2E', - { - endpoints: ['rxjs:faber'], - }, - getIndySdkModules() -) - -const aliceAgentOptions = getAgentOptions( - 'Alice Discover Features V1 E2E', - { - endpoints: ['rxjs:alice'], - }, - getIndySdkModules() -) +const faberAgentOptions = getInMemoryAgentOptions('Faber Discover Features V1 E2E', { + endpoints: ['rxjs:faber'], +}) + +const aliceAgentOptions = getInMemoryAgentOptions('Alice Discover Features V1 E2E', { + endpoints: ['rxjs:alice'], +}) describe('v1 discover features', () => { let faberAgent: Agent diff --git a/packages/core/src/modules/discover-features/__tests__/v2-discover-features.e2e.test.ts b/packages/core/src/modules/discover-features/__tests__/v2-discover-features.e2e.test.ts index f5a4b9f782..c76a574992 100644 --- a/packages/core/src/modules/discover-features/__tests__/v2-discover-features.e2e.test.ts +++ b/packages/core/src/modules/discover-features/__tests__/v2-discover-features.e2e.test.ts @@ -6,30 +6,21 @@ import type { import { ReplaySubject } from 'rxjs' -import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' import { setupSubjectTransports } from '../../../../tests' -import { getAgentOptions, makeConnection } from '../../../../tests/helpers' +import { getInMemoryAgentOptions, makeConnection } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { GoalCode, Feature } from '../../../agent/models' import { DiscoverFeaturesEventTypes } from '../DiscoverFeaturesEvents' import { waitForDisclosureSubject, waitForQuerySubject } from './helpers' -const faberAgentOptions = getAgentOptions( - 'Faber Discover Features V2 E2E', - { - endpoints: ['rxjs:faber'], - }, - getIndySdkModules() -) - -const aliceAgentOptions = getAgentOptions( - 'Alice Discover Features V2 E2E', - { - endpoints: ['rxjs:alice'], - }, - getIndySdkModules() -) +const faberAgentOptions = getInMemoryAgentOptions('Faber Discover Features V2 E2E', { + endpoints: ['rxjs:faber'], +}) + +const aliceAgentOptions = getInMemoryAgentOptions('Alice Discover Features V2 E2E', { + endpoints: ['rxjs:alice'], +}) describe('v2 discover features', () => { let faberAgent: Agent diff --git a/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts b/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts index cbdfd62be1..cbe324d324 100644 --- a/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts +++ b/packages/core/src/modules/message-pickup/__tests__/pickup.test.ts @@ -5,7 +5,7 @@ import { Subject } from 'rxjs' import { SubjectInboundTransport } from '../../../../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' -import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' +import { askarModule } from '../../../../../askar/tests/helpers' import { getAgentOptions, waitForAgentMessageProcessedEvent, waitForBasicMessage } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { HandshakeProtocol } from '../../connections' @@ -13,14 +13,23 @@ import { MediatorModule } from '../../routing' import { MessageForwardingStrategy } from '../../routing/MessageForwardingStrategy' import { V2MessagesReceivedMessage, V2StatusMessage } from '../protocol' -const recipientOptions = getAgentOptions('Mediation Pickup Loop Recipient', {}, getIndySdkModules()) +const recipientOptions = getAgentOptions( + 'Mediation Pickup Loop Recipient', + {}, + { + askar: askarModule, + }, + // Agent is shutdown during test, so we can't use in-memory wallet + false +) const mediatorOptions = getAgentOptions( 'Mediation Pickup Loop Mediator', { + // Agent is shutdown during test, so we can't use in-memory wallet endpoints: ['wss://mediator'], }, { - ...getIndySdkModules(), + askar: askarModule, mediator: new MediatorModule({ autoAcceptMediationRequests: true, messageForwardingStrategy: MessageForwardingStrategy.QueueAndLiveModeDelivery, @@ -81,9 +90,6 @@ describe('E2E Pick Up protocol', () => { // Now they are connected, reinitialize recipient agent in order to lose the session (as with SubjectTransport it remains open) await recipientAgent.shutdown() - - recipientAgent = new Agent(recipientOptions) - recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await recipientAgent.initialize() const message = 'hello pickup V1' @@ -147,9 +153,6 @@ describe('E2E Pick Up protocol', () => { // Now they are connected, reinitialize recipient agent in order to lose the session (as with SubjectTransport it remains open) await recipientAgent.shutdown() - - recipientAgent = new Agent(recipientOptions) - recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await recipientAgent.initialize() const message = 'hello pickup V2' diff --git a/packages/core/src/modules/oob/__tests__/connect-to-self.e2e.test.ts b/packages/core/src/modules/oob/__tests__/connect-to-self.e2e.test.ts index 6f80f36419..2532e9e0af 100644 --- a/packages/core/src/modules/oob/__tests__/connect-to-self.e2e.test.ts +++ b/packages/core/src/modules/oob/__tests__/connect-to-self.e2e.test.ts @@ -5,20 +5,15 @@ import { Subject } from 'rxjs' import { SubjectInboundTransport } from '../../../../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' -import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' -import { getAgentOptions } from '../../../../tests/helpers' +import { getInMemoryAgentOptions } from '../../../../tests/helpers' import { HandshakeProtocol, DidExchangeState } from '../../connections' import { OutOfBandState } from '../domain/OutOfBandState' import { Agent } from '@credo-ts/core' -const faberAgentOptions = getAgentOptions( - 'Faber Agent OOB Connect to Self', - { - endpoints: ['rxjs:faber'], - }, - getIndySdkModules() -) +const faberAgentOptions = getInMemoryAgentOptions('Faber Agent OOB Connect to Self', { + endpoints: ['rxjs:faber'], +}) describe('out of band', () => { let faberAgent: Agent diff --git a/packages/core/src/modules/oob/__tests__/implicit.e2e.test.ts b/packages/core/src/modules/oob/__tests__/implicit.e2e.test.ts index 388622da0d..41535e27ae 100644 --- a/packages/core/src/modules/oob/__tests__/implicit.e2e.test.ts +++ b/packages/core/src/modules/oob/__tests__/implicit.e2e.test.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import type { IndySdkIndyDidCreateOptions } from '@credo-ts/indy-sdk' +import type { IndyVdrDidCreateOptions } from '@credo-ts/indy-vdr' -import { getLegacyAnonCredsModules } from '../../../../../anoncreds/tests/legacyAnonCredsSetup' +import { getAnonCredsIndyModules } from '../../../../../anoncreds/tests/legacyAnonCredsSetup' import { setupSubjectTransports } from '../../../../tests' import { - getAgentOptions, + getInMemoryAgentOptions, importExistingIndyDidFromPrivateKey, publicDidSeed, waitForConnectionRecord, @@ -13,20 +13,21 @@ import { Agent } from '../../../agent/Agent' import { TypedArrayEncoder } from '../../../utils' import { sleep } from '../../../utils/sleep' import { DidExchangeState, HandshakeProtocol } from '../../connections' +import { DidCommV1Service, DidCommV2Service, DidDocumentService } from '../../dids' -const faberAgentOptions = getAgentOptions( +const faberAgentOptions = getInMemoryAgentOptions( 'Faber Agent OOB Implicit', { endpoints: ['rxjs:faber'], }, - getLegacyAnonCredsModules() + getAnonCredsIndyModules() ) -const aliceAgentOptions = getAgentOptions( +const aliceAgentOptions = getInMemoryAgentOptions( 'Alice Agent OOB Implicit', { endpoints: ['rxjs:alice'], }, - getLegacyAnonCredsModules() + getAnonCredsIndyModules() ) describe('out of band implicit', () => { @@ -230,15 +231,34 @@ describe('out of band implicit', () => { }) async function createPublicDid(agent: Agent, unqualifiedSubmitterDid: string, endpoint: string) { - const createResult = await agent.dids.create({ + const createResult = await agent.dids.create({ method: 'indy', options: { - submitterDid: `did:indy:pool:localtest:${unqualifiedSubmitterDid}`, + endorserMode: 'internal', + endorserDid: `did:indy:pool:localtest:${unqualifiedSubmitterDid}`, + useEndpointAttrib: true, + services: [ + new DidDocumentService({ + id: `#endpoint`, + serviceEndpoint: endpoint, + type: 'endpoint', + }), + new DidCommV1Service({ + id: `#did-communication`, + priority: 0, + recipientKeys: [`#key-agreement-1`], + routingKeys: [], + serviceEndpoint: endpoint, + accept: ['didcomm/aip2;env=rfc19'], + }), + new DidCommV2Service({ + accept: ['didcomm/v2'], + id: `#didcomm-1`, + routingKeys: [], + serviceEndpoint: endpoint, + }), + ], alias: 'Alias', - endpoints: { - endpoint, - types: ['DIDComm', 'did-communication', 'endpoint'], - }, }, }) diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts index 316927aaf8..f2b82f120c 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts @@ -4,8 +4,7 @@ import type { DifPresentationExchangeProofFormat } from '../DifPresentationExcha import { PresentationSubmissionLocation } from '@sphereon/pex' -import { getIndySdkModules } from '../../../../../../../indy-sdk/tests/setupIndySdkModule' -import { agentDependencies, getAgentConfig } from '../../../../../../tests' +import { getInMemoryAgentOptions } from '../../../../../../tests' import { Agent } from '../../../../../agent/Agent' import { DifPresentationExchangeModule, DifPresentationExchangeService } from '../../../../dif-presentation-exchange' import { @@ -92,17 +91,18 @@ describe('Presentation Exchange ProofFormatService', () => { let agent: Agent beforeAll(async () => { - agent = new Agent({ - config: getAgentConfig('PresentationExchangeProofFormatService'), - modules: { - someModule: new DifPresentationExchangeModule(), - proofs: new ProofsModule({ - proofProtocols: [new V2ProofProtocol({ proofFormats: [new PresentationExchangeProofFormatService()] })], - }), - ...getIndySdkModules(), - }, - dependencies: agentDependencies, - }) + agent = new Agent( + getInMemoryAgentOptions( + 'PresentationExchangeProofFormatService', + {}, + { + pex: new DifPresentationExchangeModule(), + proofs: new ProofsModule({ + proofProtocols: [new V2ProofProtocol({ proofFormats: [new PresentationExchangeProofFormatService()] })], + }), + } + ) + ) await agent.initialize() diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts index e83dc5809a..4fecad52ea 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts @@ -7,18 +7,18 @@ import { SubjectInboundTransport } from '../../../../../../../../tests/transport import { SubjectOutboundTransport } from '../../../../../../../../tests/transport/SubjectOutboundTransport' import { V1CredentialPreview } from '../../../../../../../anoncreds/src' import { - getLegacyAnonCredsModules, + getAnonCredsIndyModules, issueLegacyAnonCredsCredential, prepareForAnonCredsIssuance, setupAnonCredsTests, } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' import { waitForProofExchangeRecordSubject, - getAgentOptions, makeConnection, testLogger, setupEventReplaySubjects, waitForProofExchangeRecord, + getInMemoryAgentOptions, } from '../../../../../../tests' import { Agent } from '../../../../../agent/Agent' import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment' @@ -265,13 +265,13 @@ describe('V2 Connectionless Proofs - Indy', () => { const unique = uuid().substring(0, 4) - const mediatorOptions = getAgentOptions( + const mediatorOptions = getInMemoryAgentOptions( `Connectionless proofs with mediator Mediator-${unique}`, { endpoints: ['rxjs:mediator'], }, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptProofs: AutoAcceptProof.Always, }), mediator: new MediatorModule({ @@ -299,11 +299,11 @@ describe('V2 Connectionless Proofs - Indy', () => { handshakeProtocols: [HandshakeProtocol.Connections], }) - const faberOptions = getAgentOptions( + const faberOptions = getInMemoryAgentOptions( `Connectionless proofs with mediator Faber-${unique}`, {}, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptProofs: AutoAcceptProof.Always, }), mediationRecipient: new MediationRecipientModule({ @@ -315,11 +315,11 @@ describe('V2 Connectionless Proofs - Indy', () => { } ) - const aliceOptions = getAgentOptions( + const aliceOptions = getInMemoryAgentOptions( `Connectionless proofs with mediator Alice-${unique}`, {}, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptProofs: AutoAcceptProof.Always, }), mediationRecipient: new MediationRecipientModule({ diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts index 29a7fce4c8..a942e4744f 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts @@ -345,11 +345,16 @@ describe('Present Proof', () => { presentation: { indy: { proof: { + // FIXME: Indy SDK only had one proof: https://github.com/hyperledger/anoncreds-rs/issues/292 proofs: [ { primary_proof: expect.any(Object), non_revoc_proof: null, }, + { + primary_proof: expect.any(Object), + non_revoc_proof: null, + }, ], aggregated_proof: { c_hash: expect.any(String), diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts index a0901029a6..fdea6b6698 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts @@ -110,7 +110,7 @@ describe('Present Proof', () => { const verifierProofExchangeRecord = await verifierPresentationRecordPromise const didCommMessageRepository = - proverAgent.dependencyManager.resolve(DidCommMessageRepository) + verifierAgent.dependencyManager.resolve(DidCommMessageRepository) const proposal = await didCommMessageRepository.findAgentMessage(verifierAgent.context, { associatedRecordId: verifierProofExchangeRecord.id, diff --git a/packages/core/src/modules/routing/__tests__/mediation.test.ts b/packages/core/src/modules/routing/__tests__/mediation.test.ts index 574f999297..cd5b284cdf 100644 --- a/packages/core/src/modules/routing/__tests__/mediation.test.ts +++ b/packages/core/src/modules/routing/__tests__/mediation.test.ts @@ -8,8 +8,7 @@ import { Subject } from 'rxjs' import { SubjectInboundTransport } from '../../../../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' -import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' -import { getAgentOptions, waitForBasicMessage } from '../../../../tests/helpers' +import { getInMemoryAgentOptions, waitForBasicMessage } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { sleep } from '../../../utils/sleep' import { ConnectionRecord, HandshakeProtocol } from '../../connections' @@ -18,27 +17,22 @@ import { MediatorModule } from '../MediatorModule' import { MediatorPickupStrategy } from '../MediatorPickupStrategy' import { MediationState } from '../models/MediationState' -const recipientAgentOptions = getAgentOptions('Mediation: Recipient', {}, getIndySdkModules()) -const mediatorAgentOptions = getAgentOptions( +const recipientAgentOptions = getInMemoryAgentOptions('Mediation: Recipient') +const mediatorAgentOptions = getInMemoryAgentOptions( 'Mediation: Mediator', { endpoints: ['rxjs:mediator'], }, { - ...getIndySdkModules(), mediator: new MediatorModule({ autoAcceptMediationRequests: true, }), } ) -const senderAgentOptions = getAgentOptions( - 'Mediation: Sender', - { - endpoints: ['rxjs:sender'], - }, - getIndySdkModules() -) +const senderAgentOptions = getInMemoryAgentOptions('Mediation: Sender', { + endpoints: ['rxjs:sender'], +}) describe('mediator establishment', () => { let recipientAgent: Agent @@ -245,22 +239,10 @@ describe('mediator establishment', () => { expect(recipientMediator?.state).toBe(MediationState.Granted) + await recipientAgent.mediationRecipient.stopMessagePickup() + // Restart recipient agent await recipientAgent.shutdown() - recipientAgent = new Agent({ - ...recipientAgentOptions, - modules: { - ...recipientAgentOptions.modules, - mediationRecipient: new MediationRecipientModule({ - mediatorInvitationUrl: mediatorOutOfBandRecord.outOfBandInvitation.toUrl({ - domain: 'https://example.com/ssi', - }), - mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, - }), - }, - }) - recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) - recipientAgent.registerInboundTransport(new SubjectInboundTransport(recipientMessages)) await recipientAgent.initialize() // Initialize sender agent diff --git a/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts b/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts index 2a3961fa89..f708a56e96 100644 --- a/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts +++ b/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts @@ -1,9 +1,9 @@ import type { AgentContext } from '../../../agent' import type { Wallet } from '../../../wallet' -import { IndySdkWallet } from '../../../../../indy-sdk/src' -import { getAgentConfig, indySdk, getAgentContext, mockFunction } from '../../../../tests' -import { SigningProviderRegistry, JwsService } from '../../../crypto' +import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../tests' +import { JwsService } from '../../../crypto' import { JsonTransformer, asArray } from '../../../utils' import { W3cCredentialService } from '../W3cCredentialService' import { W3cCredentialsModuleConfig } from '../W3cCredentialsModuleConfig' @@ -17,8 +17,6 @@ import { W3cJwtCredentialService } from '../jwt-vc' import { W3cPresentation } from '../models' import { W3cCredentialRepository, W3cCredentialRecord } from '../repository' -const signingProviderRegistry = new SigningProviderRegistry([]) - jest.mock('../repository/W3cCredentialRepository') const W3cCredentialsRepositoryMock = W3cCredentialRepository as jest.Mock @@ -48,7 +46,7 @@ describe('W3cCredentialsService', () => { let w3cCredentialsRepository: W3cCredentialRepository beforeAll(async () => { - wallet = new IndySdkWallet(indySdk, agentConfig.logger, signingProviderRegistry) + wallet = new InMemoryWallet() await wallet.createAndOpen(agentConfig.walletConfig) agentContext = getAgentContext({ agentConfig, diff --git a/packages/core/src/modules/vc/__tests__/W3cCredentialsApi.test.ts b/packages/core/src/modules/vc/__tests__/W3cCredentialsApi.test.ts index 01a67407cf..c39f408452 100644 --- a/packages/core/src/modules/vc/__tests__/W3cCredentialsApi.test.ts +++ b/packages/core/src/modules/vc/__tests__/W3cCredentialsApi.test.ts @@ -1,5 +1,4 @@ -import { IndySdkModule } from '../../../../../indy-sdk/src' -import { getAgentOptions, indySdk } from '../../../../tests' +import { getInMemoryAgentOptions } from '../../../../tests' import { Agent } from '../../../agent/Agent' import { JsonTransformer } from '../../../utils' import { W3cCredentialService } from '../W3cCredentialService' @@ -9,16 +8,15 @@ import { Ed25519Signature2018Fixtures } from '../data-integrity/__tests__/fixtur import { W3cJsonLdVerifiableCredential } from '../data-integrity/models' import { W3cCredentialRepository } from '../repository' -const modules = { - indySdk: new IndySdkModule({ - indySdk, - }), - w3cCredentials: new W3cCredentialsModule({ - documentLoader: customDocumentLoader, - }), -} - -const agentOptions = getAgentOptions('W3cCredentialsApi', {}, modules) +const agentOptions = getInMemoryAgentOptions( + 'W3cCredentialsApi', + {}, + { + w3cCredentials: new W3cCredentialsModule({ + documentLoader: customDocumentLoader, + }), + } +) const agent = new Agent(agentOptions) diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts b/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts index 0e1a416dd2..b6170844f7 100644 --- a/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts +++ b/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts @@ -1,11 +1,9 @@ import type { AgentContext } from '../../../../agent' import type { Wallet } from '../../../../wallet' -import { IndySdkWallet } from '../../../../../../indy-sdk/src' -import { indySdk } from '../../../../../../indy-sdk/tests/setupIndySdkModule' +import { InMemoryWallet } from '../../../../../../../tests/InMemoryWallet' import { getAgentConfig, getAgentContext } from '../../../../../tests/helpers' import { KeyType } from '../../../../crypto' -import { SigningProviderRegistry } from '../../../../crypto/signing-provider' import { asArray, TypedArrayEncoder } from '../../../../utils' import { JsonTransformer } from '../../../../utils/JsonTransformer' import { WalletError } from '../../../../wallet/error' @@ -41,7 +39,6 @@ const signatureSuiteRegistry = new SignatureSuiteRegistry([ }, ]) -const signingProviderRegistry = new SigningProviderRegistry([]) const agentConfig = getAgentConfig('W3cJsonLdCredentialServiceTest') describe('W3cJsonLdCredentialsService', () => { @@ -51,7 +48,7 @@ describe('W3cJsonLdCredentialsService', () => { const privateKey = TypedArrayEncoder.fromString('testseed000000000000000000000001') beforeAll(async () => { - wallet = new IndySdkWallet(indySdk, agentConfig.logger, signingProviderRegistry) + wallet = new InMemoryWallet() await wallet.createAndOpen(agentConfig.walletConfig) agentContext = getAgentContext({ agentConfig, diff --git a/packages/core/src/modules/vc/data-integrity/deriveProof.ts b/packages/core/src/modules/vc/data-integrity/deriveProof.ts index fe89595115..47d0dc550b 100644 --- a/packages/core/src/modules/vc/data-integrity/deriveProof.ts +++ b/packages/core/src/modules/vc/data-integrity/deriveProof.ts @@ -38,6 +38,7 @@ export interface W3cJsonLdDeriveProofOptions { export const deriveProof = async ( proofDocument: JsonObject, revealDocument: JsonObject, + // eslint-disable-next-line @typescript-eslint/no-explicit-any { suite, skipProofCompaction, documentLoader, nonce }: any ): Promise => { if (!suite) { diff --git a/packages/core/src/modules/vc/data-integrity/proof-purposes/ProofPurpose.ts b/packages/core/src/modules/vc/data-integrity/proof-purposes/ProofPurpose.ts index 2695f3276c..af04ec9f41 100644 --- a/packages/core/src/modules/vc/data-integrity/proof-purposes/ProofPurpose.ts +++ b/packages/core/src/modules/vc/data-integrity/proof-purposes/ProofPurpose.ts @@ -1 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type ProofPurpose = any diff --git a/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts b/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts index 87bf4b476c..0abeb864cf 100644 --- a/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts +++ b/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts @@ -40,7 +40,7 @@ export class W3cJwtCredentialService { // Warn about experimental module agentContext.config.logger.warn( - "The 'W3cJwtCredentialService' is experimental and could have unexpected breaking changes. When using this service, make sure to use strict versions for all @aries-framework packages." + "The 'W3cJwtCredentialService' is experimental and could have unexpected breaking changes. When using this service, make sure to use strict versions for all @credo-ts packages." ) this.hasWarned = true diff --git a/packages/core/src/modules/vc/jwt-vc/__tests__/W3cJwtCredentialService.test.ts b/packages/core/src/modules/vc/jwt-vc/__tests__/W3cJwtCredentialService.test.ts index 42fb659cb0..51aa0706b0 100644 --- a/packages/core/src/modules/vc/jwt-vc/__tests__/W3cJwtCredentialService.test.ts +++ b/packages/core/src/modules/vc/jwt-vc/__tests__/W3cJwtCredentialService.test.ts @@ -1,7 +1,7 @@ -import { RegisteredAskarTestWallet } from '../../../../../../askar/tests/helpers' -import { agentDependencies, getAgentConfig, getAgentContext, testLogger } from '../../../../../tests' +import { InMemoryWallet } from '../../../../../../../tests/InMemoryWallet' +import { getAgentConfig, getAgentContext, testLogger } from '../../../../../tests' import { InjectionSymbols } from '../../../../constants' -import { JwsService, KeyType, SigningProviderRegistry } from '../../../../crypto' +import { JwsService, KeyType } from '../../../../crypto' import { JwaSignatureAlgorithm } from '../../../../crypto/jose/jwa' import { getJwkFromKey } from '../../../../crypto/jose/jwk' import { AriesFrameworkError, ClassValidationError } from '../../../../error' @@ -23,11 +23,7 @@ import { didIonJwtVcPresentationProfileJwtVc } from './fixtures/jwt-vc-presentat import { didKeyTransmuteJwtVc, didKeyTransmuteJwtVp } from './fixtures/transmute-verifiable-data' const config = getAgentConfig('W3cJwtCredentialService') -const wallet = new RegisteredAskarTestWallet( - config.logger, - new agentDependencies.FileSystem(), - new SigningProviderRegistry([]) -) +const wallet = new InMemoryWallet() const agentContext = getAgentContext({ wallet, registerInstances: [ diff --git a/packages/core/src/storage/migration/__tests__/0.1.test.ts b/packages/core/src/storage/migration/__tests__/0.1.test.ts index ff2033f3ba..deaf622249 100644 --- a/packages/core/src/storage/migration/__tests__/0.1.test.ts +++ b/packages/core/src/storage/migration/__tests__/0.1.test.ts @@ -4,9 +4,7 @@ import { readFileSync } from 'fs' import path from 'path' import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' -import { IndySdkWallet } from '../../../../../indy-sdk/src' -import { IndySdkSymbol } from '../../../../../indy-sdk/src/types' -import { indySdk } from '../../../../../indy-sdk/tests/setupIndySdkModule' +import { RegisteredAskarTestWallet } from '../../../../../askar/tests/helpers' import { Agent } from '../../../../src' import { agentDependencies as dependencies } from '../../../../tests/helpers' import { InjectionSymbols } from '../../../constants' @@ -40,9 +38,8 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the IndySdkModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) - dependencyManager.registerInstance(IndySdkSymbol, indySdk) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) const agent = new Agent( { @@ -62,7 +59,12 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { // Set storage after initialization. This mimics as if this wallet // is opened as an existing wallet instead of a new wallet - storageService.records = JSON.parse(aliceMediationRecordsString) + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceMediationRecordsString), + creationDate: new Date(), + }, + } expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([ { @@ -77,9 +79,9 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { expect(await updateAssistant.isUpToDate('0.2')).toBe(true) expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([]) - // MEDIATOR_ROUTING_RECORD recipientKeys will be different every time, and is not what we're testing here - delete storageService.records.MEDIATOR_ROUTING_RECORD - expect(storageService.records).toMatchSnapshot(mediationRoleUpdateStrategy) + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot( + mediationRoleUpdateStrategy + ) await agent.shutdown() await agent.wallet.delete() @@ -99,9 +101,8 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the IndySdkModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) - dependencyManager.registerInstance(IndySdkSymbol, indySdk) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) const agent = new Agent( { @@ -121,7 +122,12 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { // Set storage after initialization. This mimics as if this wallet // is opened as an existing wallet instead of a new wallet - storageService.records = JSON.parse(aliceCredentialRecordsString) + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceCredentialRecordsString), + creationDate: new Date(), + }, + } expect(await updateAssistant.isUpToDate('0.2')).toBe(false) expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([ @@ -137,9 +143,7 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { expect(await updateAssistant.isUpToDate('0.2')).toBe(true) expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([]) - // MEDIATOR_ROUTING_RECORD recipientKeys will be different every time, and is not what we're testing here - delete storageService.records.MEDIATOR_ROUTING_RECORD - expect(storageService.records).toMatchSnapshot() + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() await agent.wallet.delete() @@ -160,9 +164,8 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the IndySdkModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) - dependencyManager.registerInstance(IndySdkSymbol, indySdk) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) const agent = new Agent( { @@ -182,7 +185,12 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { // Set storage after initialization. This mimics as if this wallet // is opened as an existing wallet instead of a new wallet - storageService.records = JSON.parse(aliceCredentialRecordsString) + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceCredentialRecordsString), + creationDate: new Date(), + }, + } expect(await updateAssistant.isUpToDate('0.2')).toBe(false) expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([ @@ -198,9 +206,7 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { expect(await updateAssistant.isUpToDate('0.2')).toBe(true) expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([]) - // MEDIATOR_ROUTING_RECORD recipientKeys will be different every time, and is not what we're testing here - delete storageService.records.MEDIATOR_ROUTING_RECORD - expect(storageService.records).toMatchSnapshot() + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() await agent.wallet.delete() @@ -221,9 +227,8 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the IndySdkModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) - dependencyManager.registerInstance(IndySdkSymbol, indySdk) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) const agent = new Agent( { @@ -247,7 +252,12 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { // Set storage after initialization. This mimics as if this wallet // is opened as an existing wallet instead of a new wallet - storageService.records = JSON.parse(aliceConnectionRecordsString) + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceConnectionRecordsString), + creationDate: new Date(), + }, + } expect(await updateAssistant.isUpToDate('0.2')).toBe(false) expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([ @@ -263,9 +273,7 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { expect(await updateAssistant.isUpToDate('0.2')).toBe(true) expect(await updateAssistant.getNeededUpdates('0.2')).toEqual([]) - // MEDIATOR_ROUTING_RECORD recipientKeys will be different every time, and is not what we're testing here - delete storageService.records.MEDIATOR_ROUTING_RECORD - expect(storageService.records).toMatchSnapshot() + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() await agent.wallet.delete() diff --git a/packages/core/src/storage/migration/__tests__/0.2.test.ts b/packages/core/src/storage/migration/__tests__/0.2.test.ts index a66ef7c832..4ad72fca11 100644 --- a/packages/core/src/storage/migration/__tests__/0.2.test.ts +++ b/packages/core/src/storage/migration/__tests__/0.2.test.ts @@ -2,17 +2,15 @@ import { readFileSync } from 'fs' import path from 'path' import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' -import { IndySdkWallet } from '../../../../../indy-sdk/src' -import { IndySdkSymbol } from '../../../../../indy-sdk/src/types' -import { Agent } from '../../../../src' -import { indySdk } from '../../../../tests' +import { RegisteredAskarTestWallet } from '../../../../../askar/tests/helpers' +import { Agent, MediatorRoutingRecord } from '../../../../src' import { agentDependencies } from '../../../../tests/helpers' import { InjectionSymbols } from '../../../constants' import { DependencyManager } from '../../../plugins' import * as uuid from '../../../utils/uuid' import { UpdateAssistant } from '../UpdateAssistant' -const backupDate = new Date('2022-01-21T22:50:20.522Z') +const backupDate = new Date('2023-01-21T22:50:20.522Z') jest.useFakeTimers().setSystemTime(backupDate) const walletConfig = { @@ -34,9 +32,8 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the IndySdkModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) - dependencyManager.registerInstance(IndySdkSymbol, indySdk) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) const agent = new Agent( { @@ -59,7 +56,12 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { // Set storage after initialization. This mimics as if this wallet // is opened as an existing wallet instead of a new wallet - storageService.records = JSON.parse(aliceCredentialRecordsString) + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceCredentialRecordsString), + creationDate: new Date(), + }, + } expect(await updateAssistant.isUpToDate()).toBe(false) expect(await updateAssistant.getNeededUpdates('0.3.1')).toEqual([ @@ -79,10 +81,7 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { expect(await updateAssistant.isUpToDate()).toBe(true) expect(await updateAssistant.getNeededUpdates()).toEqual([]) - - // MEDIATOR_ROUTING_RECORD recipientKeys will be different every time, and is not what we're testing here - delete storageService.records.MEDIATOR_ROUTING_RECORD - expect(storageService.records).toMatchSnapshot() + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() await agent.wallet.delete() @@ -103,9 +102,8 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the IndySdkModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) - dependencyManager.registerInstance(IndySdkSymbol, indySdk) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) const agent = new Agent( { @@ -127,13 +125,16 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { // Set storage after initialization. This mimics as if this wallet // is opened as an existing wallet instead of a new wallet - storageService.records = JSON.parse(aliceCredentialRecordsString) + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceCredentialRecordsString), + creationDate: new Date(), + }, + } await agent.initialize() - - // MEDIATOR_ROUTING_RECORD recipientKeys will be different every time, and is not what we're testing here - delete storageService.records.MEDIATOR_ROUTING_RECORD - expect(storageService.records).toMatchSnapshot() + await storageService.deleteById(agent.context, MediatorRoutingRecord, 'MEDIATOR_ROUTING_RECORD') + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() await agent.wallet.delete() @@ -150,9 +151,8 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() - // If we register the IndySdkModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) - dependencyManager.registerInstance(IndySdkSymbol, indySdk) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) const agent = new Agent( @@ -175,14 +175,17 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { // Set storage after initialization. This mimics as if this wallet // is opened as an existing wallet instead of a new wallet - storageService.records = JSON.parse(aliceDidRecordsString) + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceDidRecordsString), + creationDate: new Date(), + }, + } await agent.initialize() - // MEDIATOR_ROUTING_RECORD recipientKeys will be different every time, and is not what we're testing here - delete storageService.records.MEDIATOR_ROUTING_RECORD - - expect(storageService.records).toMatchSnapshot() + await storageService.deleteById(agent.context, MediatorRoutingRecord, 'MEDIATOR_ROUTING_RECORD') + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() await agent.wallet.delete() diff --git a/packages/core/src/storage/migration/__tests__/0.3.test.ts b/packages/core/src/storage/migration/__tests__/0.3.test.ts index e8479803fc..7cef8aafd7 100644 --- a/packages/core/src/storage/migration/__tests__/0.3.test.ts +++ b/packages/core/src/storage/migration/__tests__/0.3.test.ts @@ -2,9 +2,7 @@ import { readFileSync } from 'fs' import path from 'path' import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' -import { IndySdkWallet } from '../../../../../indy-sdk/src' -import { IndySdkSymbol } from '../../../../../indy-sdk/src/types' -import { indySdk } from '../../../../tests' +import { RegisteredAskarTestWallet } from '../../../../../askar/tests/helpers' import { agentDependencies } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { InjectionSymbols } from '../../../constants' @@ -34,9 +32,8 @@ describe('UpdateAssistant | v0.3.1 - v0.4', () => { const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the IndySdkModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) - dependencyManager.registerInstance(IndySdkSymbol, indySdk) + // If we register the AskarModule it will register the storage service, but we use in memory storage here + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) const agent = new Agent( { @@ -59,7 +56,12 @@ describe('UpdateAssistant | v0.3.1 - v0.4', () => { // Set storage after initialization. This mimics as if this wallet // is opened as an existing wallet instead of a new wallet - storageService.records = JSON.parse(aliceDidRecordsString) + storageService.contextCorrelationIdToRecords = { + default: { + records: JSON.parse(aliceDidRecordsString), + creationDate: new Date(), + }, + } expect(await updateAssistant.isUpToDate()).toBe(false) expect(await updateAssistant.getNeededUpdates('0.4')).toEqual([ @@ -75,9 +77,7 @@ describe('UpdateAssistant | v0.3.1 - v0.4', () => { expect(await updateAssistant.isUpToDate()).toBe(true) expect(await updateAssistant.getNeededUpdates()).toEqual([]) - // MEDIATOR_ROUTING_RECORD recipientKeys will be different every time, and is not what we're testing here - delete storageService.records.MEDIATOR_ROUTING_RECORD - expect(storageService.records).toMatchSnapshot() + expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() await agent.wallet.delete() diff --git a/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts b/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts index 2f9e3e80a3..eac4d95755 100644 --- a/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts +++ b/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts @@ -1,17 +1,13 @@ +import type { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' import type { BaseRecord } from '../../BaseRecord' -import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' -import { IndySdkWallet } from '../../../../../indy-sdk/src' -import { IndySdkSymbol } from '../../../../../indy-sdk/src/types' -import { indySdk } from '../../../../../indy-sdk/tests/setupIndySdkModule' -import { getAgentOptions } from '../../../../tests/helpers' +import { getInMemoryAgentOptions } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { InjectionSymbols } from '../../../constants' -import { DependencyManager } from '../../../plugins' import { UpdateAssistant } from '../UpdateAssistant' import { CURRENT_FRAMEWORK_STORAGE_VERSION } from '../updates' -const agentOptions = getAgentOptions('UpdateAssistant', {}) +const agentOptions = getInMemoryAgentOptions('UpdateAssistant', {}) describe('UpdateAssistant', () => { let updateAssistant: UpdateAssistant @@ -19,14 +15,7 @@ describe('UpdateAssistant', () => { let storageService: InMemoryStorageService beforeEach(async () => { - const dependencyManager = new DependencyManager() - storageService = new InMemoryStorageService() - // If we register the IndySdkModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) - dependencyManager.registerInstance(IndySdkSymbol, indySdk) - dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - - agent = new Agent(agentOptions, dependencyManager) + agent = new Agent(agentOptions) updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { @@ -34,6 +23,8 @@ describe('UpdateAssistant', () => { }, }) + storageService = agent.dependencyManager.resolve(InjectionSymbols.StorageService) + await updateAssistant.initialize() }) @@ -44,10 +35,13 @@ describe('UpdateAssistant', () => { describe('upgrade()', () => { it('should not upgrade records when upgrading after a new wallet is created', async () => { - const beforeStorage = JSON.stringify(storageService.records) + const beforeStorage = JSON.stringify(storageService.contextCorrelationIdToRecords) await updateAssistant.update() - expect(JSON.parse(beforeStorage)).toEqual(storageService.records) + // We parse and stringify so the dates are equal (both string) + expect(JSON.parse(beforeStorage)).toEqual( + JSON.parse(JSON.stringify(storageService.contextCorrelationIdToRecords)) + ) }) }) diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/0.2.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/0.2.test.ts.snap index c4767da0c1..cf2fb076af 100644 --- a/packages/core/src/storage/migration/__tests__/__snapshots__/0.2.test.ts.snap +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/0.2.test.ts.snap @@ -122,7 +122,7 @@ exports[`UpdateAssistant | v0.2 - v0.3.1 should correctly update proof records a "id": "STORAGE_VERSION_RECORD_ID", "metadata": {}, "storageVersion": "0.4", - "updatedAt": "2022-01-21T22:50:20.522Z", + "updatedAt": "2023-01-21T22:50:20.522Z", }, }, "ea840186-3c77-45f4-a2e6-349811ad8994": { @@ -303,7 +303,7 @@ exports[`UpdateAssistant | v0.2 - v0.3.1 should correctly update the did records "id": "1-4e4f-41d9-94c4-f49351b811f1", "metadata": {}, "role": "created", - "updatedAt": "2022-01-21T22:50:20.522Z", + "updatedAt": "2023-01-21T22:50:20.522Z", }, }, "2-4e4f-41d9-94c4-f49351b811f1": { @@ -366,7 +366,7 @@ exports[`UpdateAssistant | v0.2 - v0.3.1 should correctly update the did records "id": "2-4e4f-41d9-94c4-f49351b811f1", "metadata": {}, "role": "received", - "updatedAt": "2022-01-21T22:50:20.522Z", + "updatedAt": "2023-01-21T22:50:20.522Z", }, }, "3-4e4f-41d9-94c4-f49351b811f1": { @@ -429,7 +429,7 @@ exports[`UpdateAssistant | v0.2 - v0.3.1 should correctly update the did records "id": "3-4e4f-41d9-94c4-f49351b811f1", "metadata": {}, "role": "created", - "updatedAt": "2022-01-21T22:50:20.522Z", + "updatedAt": "2023-01-21T22:50:20.522Z", }, }, "4-4e4f-41d9-94c4-f49351b811f1": { @@ -494,7 +494,7 @@ exports[`UpdateAssistant | v0.2 - v0.3.1 should correctly update the did records "id": "4-4e4f-41d9-94c4-f49351b811f1", "metadata": {}, "role": "created", - "updatedAt": "2022-01-21T22:50:20.522Z", + "updatedAt": "2023-01-21T22:50:20.522Z", }, }, "5-4e4f-41d9-94c4-f49351b811f1": { @@ -557,7 +557,7 @@ exports[`UpdateAssistant | v0.2 - v0.3.1 should correctly update the did records "id": "5-4e4f-41d9-94c4-f49351b811f1", "metadata": {}, "role": "received", - "updatedAt": "2022-01-21T22:50:20.522Z", + "updatedAt": "2023-01-21T22:50:20.522Z", }, }, "6-4e4f-41d9-94c4-f49351b811f1": { @@ -620,7 +620,7 @@ exports[`UpdateAssistant | v0.2 - v0.3.1 should correctly update the did records "id": "6-4e4f-41d9-94c4-f49351b811f1", "metadata": {}, "role": "received", - "updatedAt": "2022-01-21T22:50:20.522Z", + "updatedAt": "2023-01-21T22:50:20.522Z", }, }, "7-4e4f-41d9-94c4-f49351b811f1": { @@ -683,7 +683,7 @@ exports[`UpdateAssistant | v0.2 - v0.3.1 should correctly update the did records "id": "7-4e4f-41d9-94c4-f49351b811f1", "metadata": {}, "role": "received", - "updatedAt": "2022-01-21T22:50:20.522Z", + "updatedAt": "2023-01-21T22:50:20.522Z", }, }, "8-4e4f-41d9-94c4-f49351b811f1": { @@ -746,7 +746,7 @@ exports[`UpdateAssistant | v0.2 - v0.3.1 should correctly update the did records "id": "8-4e4f-41d9-94c4-f49351b811f1", "metadata": {}, "role": "created", - "updatedAt": "2022-01-21T22:50:20.522Z", + "updatedAt": "2023-01-21T22:50:20.522Z", }, }, "STORAGE_VERSION_RECORD_ID": { @@ -758,7 +758,7 @@ exports[`UpdateAssistant | v0.2 - v0.3.1 should correctly update the did records "id": "STORAGE_VERSION_RECORD_ID", "metadata": {}, "storageVersion": "0.4", - "updatedAt": "2022-01-21T22:50:20.522Z", + "updatedAt": "2023-01-21T22:50:20.522Z", }, }, } @@ -886,7 +886,7 @@ exports[`UpdateAssistant | v0.2 - v0.3.1 should correctly update the proofs reco "id": "STORAGE_VERSION_RECORD_ID", "metadata": {}, "storageVersion": "0.4", - "updatedAt": "2022-01-21T22:50:20.522Z", + "updatedAt": "2023-01-21T22:50:20.522Z", }, }, "ea840186-3c77-45f4-a2e6-349811ad8994": { diff --git a/packages/core/src/storage/migration/__tests__/backup-askar.test.ts b/packages/core/src/storage/migration/__tests__/backup-askar.test.ts index 1b83777bdd..3db486adc1 100644 --- a/packages/core/src/storage/migration/__tests__/backup-askar.test.ts +++ b/packages/core/src/storage/migration/__tests__/backup-askar.test.ts @@ -4,9 +4,8 @@ import type { StorageUpdateError } from '../error/StorageUpdateError' import { readFileSync, unlinkSync } from 'fs' import path from 'path' -import { AskarModule } from '../../../../../askar/src' -import { askarModuleConfig } from '../../../../../askar/tests/helpers' -import { getAgentOptions } from '../../../../tests/helpers' +import { askarModule } from '../../../../../askar/tests/helpers' +import { getAgentOptions, getAskarWalletConfig } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { InjectionSymbols } from '../../../constants' import { AriesFrameworkError } from '../../../error' @@ -17,9 +16,11 @@ import { UpdateAssistant } from '../UpdateAssistant' const agentOptions = getAgentOptions( 'UpdateAssistant | Backup | Aries Askar', - {}, { - askar: new AskarModule(askarModuleConfig), + walletConfig: getAskarWalletConfig('UpdateAssistant | Backup | Aries Askar', { inMemory: false }), + }, + { + askar: askarModule, } ) diff --git a/packages/core/src/storage/migration/__tests__/backup.test.ts b/packages/core/src/storage/migration/__tests__/backup.test.ts index e582263b57..6001ac2a99 100644 --- a/packages/core/src/storage/migration/__tests__/backup.test.ts +++ b/packages/core/src/storage/migration/__tests__/backup.test.ts @@ -4,8 +4,8 @@ import type { StorageUpdateError } from '../error/StorageUpdateError' import { readFileSync, unlinkSync } from 'fs' import path from 'path' -import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' -import { getAgentOptions } from '../../../../tests/helpers' +import { askarModule } from '../../../../../askar/tests/helpers' +import { getAgentOptions, getAskarWalletConfig } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { InjectionSymbols } from '../../../constants' import { AriesFrameworkError } from '../../../error' @@ -14,7 +14,17 @@ import { JsonTransformer } from '../../../utils' import { StorageUpdateService } from '../StorageUpdateService' import { UpdateAssistant } from '../UpdateAssistant' -const agentOptions = getAgentOptions('UpdateAssistant | Backup', {}, getIndySdkModules()) +const agentOptions = getAgentOptions( + 'UpdateAssistant | Backup', + { + walletConfig: getAskarWalletConfig('UpdateAssistant | Backup', { + inMemory: false, + }), + }, + { askar: askarModule } +) +// eslint-disable-next-line @typescript-eslint/no-non-null-assertion +agentOptions.config.walletConfig!.storage!.inMemory = false const aliceCredentialRecordsString = readFileSync( path.join(__dirname, '__fixtures__/alice-4-credentials-0.1.json'), diff --git a/packages/core/tests/agents.test.ts b/packages/core/tests/agents.test.ts index 9bded8ba18..6a8a7b23b4 100644 --- a/packages/core/tests/agents.test.ts +++ b/packages/core/tests/agents.test.ts @@ -1,27 +1,18 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { ConnectionRecord } from '../src/modules/connections' -import { getIndySdkModules } from '../../indy-sdk/tests/setupIndySdkModule' import { Agent } from '../src/agent/Agent' import { HandshakeProtocol } from '../src/modules/connections' -import { waitForBasicMessage, getAgentOptions } from './helpers' +import { waitForBasicMessage, getInMemoryAgentOptions } from './helpers' import { setupSubjectTransports } from './transport' -const aliceAgentOptions = getAgentOptions( - 'Agents Alice', - { - endpoints: ['rxjs:alice'], - }, - getIndySdkModules() -) -const bobAgentOptions = getAgentOptions( - 'Agents Bob', - { - endpoints: ['rxjs:bob'], - }, - getIndySdkModules() -) +const aliceAgentOptions = getInMemoryAgentOptions('Agents Alice', { + endpoints: ['rxjs:alice'], +}) +const bobAgentOptions = getInMemoryAgentOptions('Agents Bob', { + endpoints: ['rxjs:bob'], +}) describe('agents', () => { let aliceAgent: Agent diff --git a/packages/core/tests/connections.test.ts b/packages/core/tests/connections.test.ts index 0f64b31d2c..d158acc272 100644 --- a/packages/core/tests/connections.test.ts +++ b/packages/core/tests/connections.test.ts @@ -3,7 +3,6 @@ import type { AgentMessageProcessedEvent, KeylistUpdate } from '../src' import { filter, firstValueFrom, map, timeout } from 'rxjs' -import { getIndySdkModules } from '../../indy-sdk/tests/setupIndySdkModule' import { MediatorModule, Key, @@ -17,7 +16,7 @@ import { Agent } from '../src/agent/Agent' import { didKeyToVerkey } from '../src/modules/dids/helpers' import { OutOfBandState } from '../src/modules/oob/domain/OutOfBandState' -import { getAgentOptions, waitForTrustPingResponseReceivedEvent } from './helpers' +import { getInMemoryAgentOptions, waitForTrustPingResponseReceivedEvent } from './helpers' import { setupSubjectTransports } from './transport' describe('connections', () => { @@ -27,34 +26,21 @@ describe('connections', () => { let mediatorAgent: Agent beforeEach(async () => { - const faberAgentOptions = getAgentOptions( - 'Faber Agent Connections', - { - endpoints: ['rxjs:faber'], - }, - getIndySdkModules() - ) - const aliceAgentOptions = getAgentOptions( - 'Alice Agent Connections', - { - endpoints: ['rxjs:alice'], - }, - getIndySdkModules() - ) - const acmeAgentOptions = getAgentOptions( - 'Acme Agent Connections', - { - endpoints: ['rxjs:acme'], - }, - getIndySdkModules() - ) - const mediatorAgentOptions = getAgentOptions( + const faberAgentOptions = getInMemoryAgentOptions('Faber Agent Connections', { + endpoints: ['rxjs:faber'], + }) + const aliceAgentOptions = getInMemoryAgentOptions('Alice Agent Connections', { + endpoints: ['rxjs:alice'], + }) + const acmeAgentOptions = getInMemoryAgentOptions('Acme Agent Connections', { + endpoints: ['rxjs:acme'], + }) + const mediatorAgentOptions = getInMemoryAgentOptions( 'Mediator Agent Connections', { endpoints: ['rxjs:mediator'], }, { - ...getIndySdkModules(), mediator: new MediatorModule({ autoAcceptMediationRequests: true, }), diff --git a/packages/core/tests/generic-records.test.ts b/packages/core/tests/generic-records.test.ts index 3d37def0ed..bdf605d517 100644 --- a/packages/core/tests/generic-records.test.ts +++ b/packages/core/tests/generic-records.test.ts @@ -1,18 +1,13 @@ import type { GenericRecord } from '../src/modules/generic-records/repository/GenericRecord' -import { getIndySdkModules } from '../../indy-sdk/tests/setupIndySdkModule' import { Agent } from '../src/agent/Agent' import { RecordNotFoundError } from '../src/error' -import { getAgentOptions } from './helpers' +import { getInMemoryAgentOptions } from './helpers' -const aliceAgentOptions = getAgentOptions( - 'Generic Records Alice', - { - endpoints: ['rxjs:alice'], - }, - getIndySdkModules() -) +const aliceAgentOptions = getInMemoryAgentOptions('Generic Records Alice', { + endpoints: ['rxjs:alice'], +}) describe('genericRecords', () => { let aliceAgent: Agent diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index b1312050a8..260bb9fc27 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { AskarWalletSqliteStorageConfig } from '../../askar/src/wallet' import type { AgentDependencies, BaseEvent, @@ -14,9 +15,8 @@ import type { CredentialState, ConnectionStateChangedEvent, Buffer, - RevocationNotificationReceivedEvent, AgentMessageProcessedEvent, - AgentMessageReceivedEvent, + RevocationNotificationReceivedEvent, } from '../src' import type { AgentModulesInput, EmptyModuleMap } from '../src/agent/AgentModules' import type { TrustPingReceivedEvent, TrustPingResponseReceivedEvent } from '../src/modules/connections/TrustPingEvents' @@ -29,8 +29,10 @@ import path from 'path' import { lastValueFrom, firstValueFrom, ReplaySubject } from 'rxjs' import { catchError, filter, map, take, timeout } from 'rxjs/operators' -import { agentDependencies, IndySdkPostgresWalletScheme } from '../../node/src' +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' +import { agentDependencies } from '../../node/src' import { + AgentEventTypes, OutOfBandDidCommService, ConnectionsModule, ConnectionEventTypes, @@ -47,7 +49,6 @@ import { InjectionSymbols, ProofEventTypes, TrustPingEventTypes, - AgentEventTypes, } from '../src' import { Key, KeyType } from '../src/crypto' import { DidKey } from '../src/modules/dids/methods/key' @@ -56,6 +57,7 @@ import { OutOfBandState } from '../src/modules/oob/domain/OutOfBandState' import { OutOfBandInvitation } from '../src/modules/oob/messages' import { OutOfBandRecord } from '../src/modules/oob/repository' import { KeyDerivationMethod } from '../src/types' +import { sleep } from '../src/utils/sleep' import { uuid } from '../src/utils/uuid' import testLogger, { TestLogger } from './logger' @@ -71,19 +73,39 @@ export const taaVersion = (process.env.TEST_AGENT_TAA_VERSION ?? '1') as `${numb export const taaAcceptanceMechanism = process.env.TEST_AGENT_TAA_ACCEPTANCE_MECHANISM ?? 'accept' export { agentDependencies } +export function getAskarWalletConfig( + name: string, + { + inMemory = true, + random = uuid().slice(0, 4), + maxConnections, + }: { inMemory?: boolean; random?: string; maxConnections?: number } = {} +) { + return { + id: `Wallet: ${name} - ${random}`, + key: 'DZ9hPqFWTPxemcGea72C1X1nusqk5wFNLq6QPjwXGqAa', // generated using indy.generateWalletKey + keyDerivationMethod: KeyDerivationMethod.Raw, + // Use in memory by default + storage: { + type: 'sqlite', + config: { + inMemory, + maxConnections, + }, + } satisfies AskarWalletSqliteStorageConfig, + } satisfies WalletConfig +} + export function getAgentOptions( name: string, extraConfig: Partial = {}, - inputModules?: AgentModules -): { config: InitConfig; modules: AgentModules; dependencies: AgentDependencies } { + inputModules?: AgentModules, + inMemoryWallet = true +): { config: InitConfig; modules: AgentModules; dependencies: AgentDependencies; inMemory?: boolean } { const random = uuid().slice(0, 4) const config: InitConfig = { label: `Agent: ${name} - ${random}`, - walletConfig: { - id: `Wallet: ${name} - ${random}`, - key: 'DZ9hPqFWTPxemcGea72C1X1nusqk5wFNLq6QPjwXGqAa', // generated using indy.generateWalletKey - keyDerivationMethod: KeyDerivationMethod.Raw, - }, + walletConfig: getAskarWalletConfig(name, { inMemory: inMemoryWallet, random }), // TODO: determine the log level based on an environment variable. This will make it // possible to run e.g. failed github actions in debug mode for extra logs logger: TestLogger.fromLogger(testLogger, name), @@ -104,33 +126,20 @@ export function getAgentOptions( +export function getInMemoryAgentOptions( name: string, extraConfig: Partial = {}, inputModules?: AgentModules -) { +): { config: InitConfig; modules: AgentModules; dependencies: AgentDependencies } { const random = uuid().slice(0, 4) const config: InitConfig = { label: `Agent: ${name} - ${random}`, walletConfig: { - // NOTE: IndySDK Postgres database per wallet doesn't support special characters/spaces in the wallet name - id: `PostgresWallet${name}${random}`, - key: `Key${name}`, - storage: { - type: 'postgres_storage', - config: { - url: 'localhost:5432', - wallet_scheme: IndySdkPostgresWalletScheme.DatabasePerWallet, - }, - credentials: { - account: 'postgres', - password: 'postgres', - admin_account: 'postgres', - admin_password: 'postgres', - }, - }, + id: `Wallet: ${name} - ${random}`, + key: `Wallet: ${name}`, }, - autoUpdateStorageOnStartup: false, + // TODO: determine the log level based on an environment variable. This will make it + // possible to run e.g. failed github actions in debug mode for extra logs logger: TestLogger.fromLogger(testLogger, name), ...extraConfig, } @@ -138,13 +147,16 @@ export function getPostgresAgentOptions any>(fn: T): jest.Moc export function mockProperty(object: T, property: K, value: T[K]) { Object.defineProperty(object, property, { get: () => value }) } + +export async function retryUntilResult Promise>( + method: M, + { + intervalMs = 500, + delay = 1000, + maxAttempts = 5, + }: { + intervalMs?: number + delay?: number + maxAttempts?: number + } = {} +): Promise { + await sleep(delay) + + for (let i = 0; i < maxAttempts; i++) { + const result = await method() + if (result) return result + await sleep(intervalMs) + } + + throw new Error(`Unable to get result from method in ${maxAttempts} attempts`) +} diff --git a/packages/core/tests/index.ts b/packages/core/tests/index.ts index 2822fb23e1..b8ea2ca430 100644 --- a/packages/core/tests/index.ts +++ b/packages/core/tests/index.ts @@ -2,7 +2,6 @@ export * from './jsonld' export * from './transport' export * from './events' export * from './helpers' -export * from './indySdk' import testLogger from './logger' diff --git a/packages/core/tests/indySdk.ts b/packages/core/tests/indySdk.ts deleted file mode 100644 index b5e5a3075d..0000000000 --- a/packages/core/tests/indySdk.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' - -export { indySdk } diff --git a/packages/core/tests/jsonld.ts b/packages/core/tests/jsonld.ts index 1cfb263ab0..9dd4760296 100644 --- a/packages/core/tests/jsonld.ts +++ b/packages/core/tests/jsonld.ts @@ -1,9 +1,9 @@ import type { EventReplaySubject } from './events' import type { AutoAcceptCredential, AutoAcceptProof, ConnectionRecord } from '../src' +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' +import { askarModule } from '../../askar/tests/helpers' import { BbsModule } from '../../bbs-signatures/src/BbsModule' -import { IndySdkModule } from '../../indy-sdk/src' -import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' import { PresentationExchangeProofFormatService, V2ProofProtocol, @@ -29,7 +29,8 @@ export type JsonLdTestsAgent = Agent> export const getJsonLdModules = ({ autoAcceptCredentials, autoAcceptProofs, -}: { autoAcceptCredentials?: AutoAcceptCredential; autoAcceptProofs?: AutoAcceptProof } = {}) => + useBbs = false, +}: { autoAcceptCredentials?: AutoAcceptCredential; autoAcceptProofs?: AutoAcceptProof; useBbs?: boolean } = {}) => ({ credentials: new CredentialsModule({ credentialProtocols: [new V2CredentialProtocol({ credentialFormats: [new JsonLdCredentialFormatService()] })], @@ -45,10 +46,15 @@ export const getJsonLdModules = ({ cache: new CacheModule({ cache: new InMemoryLruCache({ limit: 100 }), }), - indySdk: new IndySdkModule({ - indySdk, - }), - bbs: new BbsModule(), + // We don't support signing provider in in memory wallet yet, so if BBS is used we need to use Askar + ...(useBbs + ? { + askar: askarModule, + bbs: new BbsModule(), + } + : { + inMemory: new InMemoryWalletModule(), + }), } as const) interface SetupJsonLdTestsReturn { @@ -88,6 +94,7 @@ export async function setupJsonLdTests< autoAcceptCredentials, autoAcceptProofs, createConnections, + useBbs = false, }: { issuerName: string holderName: string @@ -95,10 +102,12 @@ export async function setupJsonLdTests< autoAcceptCredentials?: AutoAcceptCredential autoAcceptProofs?: AutoAcceptProof createConnections?: CreateConnections + useBbs?: boolean }): Promise> { const modules = getJsonLdModules({ autoAcceptCredentials, autoAcceptProofs, + useBbs, }) const issuerAgent = new Agent( diff --git a/packages/core/tests/migration.test.ts b/packages/core/tests/migration.test.ts index ac898caa73..bc39a9382a 100644 --- a/packages/core/tests/migration.test.ts +++ b/packages/core/tests/migration.test.ts @@ -1,12 +1,12 @@ import type { VersionString } from '../src/utils/version' -import { getIndySdkModules } from '../../indy-sdk/tests/setupIndySdkModule' +import { askarModule } from '../../askar/tests/helpers' import { Agent } from '../src/agent/Agent' import { UpdateAssistant } from '../src/storage/migration/UpdateAssistant' import { getAgentOptions } from './helpers' -const agentOptions = getAgentOptions('Migration', {}, getIndySdkModules()) +const agentOptions = getAgentOptions('Migration', {}, { askar: askarModule }) describe('migration', () => { test('manually initiating the update assistant to perform an update', async () => { diff --git a/packages/core/tests/multi-protocol-version.test.ts b/packages/core/tests/multi-protocol-version.test.ts index 4f7596d5ff..1f9fa3c915 100644 --- a/packages/core/tests/multi-protocol-version.test.ts +++ b/packages/core/tests/multi-protocol-version.test.ts @@ -2,29 +2,20 @@ import type { AgentMessageProcessedEvent } from '../src/agent/Events' import { filter, firstValueFrom, timeout } from 'rxjs' -import { getIndySdkModules } from '../../indy-sdk/tests/setupIndySdkModule' import { parseMessageType, MessageSender, AgentMessage, IsValidMessageType } from '../src' import { Agent } from '../src/agent/Agent' import { AgentEventTypes } from '../src/agent/Events' import { OutboundMessageContext } from '../src/agent/models' -import { getAgentOptions } from './helpers' +import { getInMemoryAgentOptions } from './helpers' import { setupSubjectTransports } from './transport' -const aliceAgentOptions = getAgentOptions( - 'Multi Protocol Versions - Alice', - { - endpoints: ['rxjs:alice'], - }, - getIndySdkModules() -) -const bobAgentOptions = getAgentOptions( - 'Multi Protocol Versions - Bob', - { - endpoints: ['rxjs:bob'], - }, - getIndySdkModules() -) +const aliceAgentOptions = getInMemoryAgentOptions('Multi Protocol Versions - Alice', { + endpoints: ['rxjs:alice'], +}) +const bobAgentOptions = getInMemoryAgentOptions('Multi Protocol Versions - Bob', { + endpoints: ['rxjs:bob'], +}) describe('multi version protocols', () => { let aliceAgent: Agent diff --git a/packages/core/tests/oob-mediation-provision.test.ts b/packages/core/tests/oob-mediation-provision.test.ts index 5e18f36197..3bf27ba9eb 100644 --- a/packages/core/tests/oob-mediation-provision.test.ts +++ b/packages/core/tests/oob-mediation-provision.test.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { OutOfBandInvitation } from '../src/modules/oob/messages' -import { getIndySdkModules } from '../../indy-sdk/tests/setupIndySdkModule' import { Agent } from '../src/agent/Agent' import { DidExchangeState, HandshakeProtocol } from '../src/modules/connections' import { @@ -11,36 +10,29 @@ import { MediationRecipientModule, } from '../src/modules/routing' -import { getAgentOptions, waitForBasicMessage } from './helpers' +import { getInMemoryAgentOptions, waitForBasicMessage } from './helpers' import { setupSubjectTransports } from './transport' -const faberAgentOptions = getAgentOptions( - 'OOB mediation provision - Faber Agent', - { - endpoints: ['rxjs:faber'], - }, - getIndySdkModules() -) -const aliceAgentOptions = getAgentOptions( +const faberAgentOptions = getInMemoryAgentOptions('OOB mediation provision - Faber Agent', { + endpoints: ['rxjs:faber'], +}) +const aliceAgentOptions = getInMemoryAgentOptions( 'OOB mediation provision - Alice Recipient Agent', { endpoints: ['rxjs:alice'], }, { - ...getIndySdkModules(), mediationRecipient: new MediationRecipientModule({ - // FIXME: discover features returns that we support this protocol, but we don't support all roles - // we should return that we only support the mediator role so we don't have to explicitly declare this mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), } ) -const mediatorAgentOptions = getAgentOptions( +const mediatorAgentOptions = getInMemoryAgentOptions( 'OOB mediation provision - Mediator Agent', { endpoints: ['rxjs:mediator'], }, - { ...getIndySdkModules(), mediator: new MediatorModule({ autoAcceptMediationRequests: true }) } + { mediator: new MediatorModule({ autoAcceptMediationRequests: true }) } ) describe('out of band with mediation set up with provision method', () => { diff --git a/packages/core/tests/oob-mediation.test.ts b/packages/core/tests/oob-mediation.test.ts index 272c468d3b..3372ccc8cf 100644 --- a/packages/core/tests/oob-mediation.test.ts +++ b/packages/core/tests/oob-mediation.test.ts @@ -7,7 +7,6 @@ import { filter, firstValueFrom, map, Subject, timeout } from 'rxjs' import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' -import { getIndySdkModules } from '../../indy-sdk/tests/setupIndySdkModule' import { Agent } from '../src/agent/Agent' import { AgentEventTypes } from '../src/agent/Events' import { DidExchangeState, HandshakeProtocol } from '../src/modules/connections' @@ -22,35 +21,28 @@ import { MediatorModule, } from '../src/modules/routing' -import { getAgentOptions, waitForBasicMessage } from './helpers' +import { getInMemoryAgentOptions, waitForBasicMessage } from './helpers' -const faberAgentOptions = getAgentOptions( - 'OOB mediation - Faber Agent', - { - endpoints: ['rxjs:faber'], - }, - getIndySdkModules() -) -const aliceAgentOptions = getAgentOptions( +const faberAgentOptions = getInMemoryAgentOptions('OOB mediation - Faber Agent', { + endpoints: ['rxjs:faber'], +}) +const aliceAgentOptions = getInMemoryAgentOptions( 'OOB mediation - Alice Recipient Agent', { endpoints: ['rxjs:alice'], }, { - ...getIndySdkModules(), mediationRecipient: new MediationRecipientModule({ - // FIXME: discover features returns that we support this protocol, but we don't support all roles - // we should return that we only support the mediator role so we don't have to explicitly declare this mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), } ) -const mediatorAgentOptions = getAgentOptions( +const mediatorAgentOptions = getInMemoryAgentOptions( 'OOB mediation - Mediator Agent', { endpoints: ['rxjs:mediator'], }, - { ...getIndySdkModules(), mediator: new MediatorModule({ autoAcceptMediationRequests: true }) } + { mediator: new MediatorModule({ autoAcceptMediationRequests: true }) } ) describe('out of band with mediation', () => { diff --git a/packages/core/tests/oob.test.ts b/packages/core/tests/oob.test.ts index c573c73215..278b7850f4 100644 --- a/packages/core/tests/oob.test.ts +++ b/packages/core/tests/oob.test.ts @@ -8,7 +8,7 @@ import { Subject } from 'rxjs' import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' -import { getLegacyAnonCredsModules, prepareForAnonCredsIssuance } from '../../anoncreds/tests/legacyAnonCredsSetup' +import { getAnonCredsIndyModules, prepareForAnonCredsIssuance } from '../../anoncreds/tests/legacyAnonCredsSetup' import { Agent } from '../src/agent/Agent' import { Key } from '../src/crypto' import { DidExchangeState, HandshakeProtocol } from '../src/modules/connections' @@ -20,25 +20,26 @@ import { OutOfBandInvitation } from '../src/modules/oob/messages' import { JsonEncoder, JsonTransformer } from '../src/utils' import { TestMessage } from './TestMessage' -import { getAgentOptions, waitForCredentialRecord } from './helpers' +import { getInMemoryAgentOptions, waitForCredentialRecord } from './helpers' import { AgentEventTypes, AriesFrameworkError, AutoAcceptCredential, CredentialState } from '@credo-ts/core' -const faberAgentOptions = getAgentOptions( +// FIXME: oob.test doesn't need heavy AnonCreds / indy dependencies +const faberAgentOptions = getInMemoryAgentOptions( 'Faber Agent OOB', { endpoints: ['rxjs:faber'], }, - getLegacyAnonCredsModules({ + getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }) ) -const aliceAgentOptions = getAgentOptions( +const aliceAgentOptions = getInMemoryAgentOptions( 'Alice Agent OOB', { endpoints: ['rxjs:alice'], }, - getLegacyAnonCredsModules({ + getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }) ) @@ -63,8 +64,8 @@ describe('out of band', () => { autoAcceptConnection: false, } - let faberAgent: Agent> - let aliceAgent: Agent> + let faberAgent: Agent> + let aliceAgent: Agent> let credentialTemplate: CreateCredentialOfferOptions<[V1CredentialProtocol]> beforeAll(async () => { diff --git a/packages/core/tests/setup.ts b/packages/core/tests/setup.ts index b1f1d020b2..a2ba1429d2 100644 --- a/packages/core/tests/setup.ts +++ b/packages/core/tests/setup.ts @@ -2,9 +2,6 @@ import 'reflect-metadata' import type { ConnectionRecord } from '../src/modules/connections/repository/ConnectionRecord' -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { askarModuleConfig } from '../../askar/tests/helpers' - jest.setTimeout(120000) expect.extend({ toBeConnectedWith }) diff --git a/packages/core/tests/wallet.test.ts b/packages/core/tests/wallet.test.ts deleted file mode 100644 index 78e089482a..0000000000 --- a/packages/core/tests/wallet.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { tmpdir } from 'os' -import path from 'path' - -import { getIndySdkModules } from '../../indy-sdk/tests/setupIndySdkModule' -import { Agent } from '../src/agent/Agent' -import { BasicMessageRepository, BasicMessageRecord, BasicMessageRole } from '../src/modules/basic-messages' -import { KeyDerivationMethod } from '../src/types' -import { uuid } from '../src/utils/uuid' -import { WalletInvalidKeyError } from '../src/wallet/error' -import { WalletDuplicateError } from '../src/wallet/error/WalletDuplicateError' -import { WalletNotFoundError } from '../src/wallet/error/WalletNotFoundError' - -import { getAgentOptions } from './helpers' - -const aliceAgentOptions = getAgentOptions('wallet-tests-Alice', {}, getIndySdkModules()) -const bobAgentOptions = getAgentOptions('wallet-tests-Bob', {}, getIndySdkModules()) - -describe('wallet', () => { - let aliceAgent: Agent - let bobAgent: Agent - - beforeEach(async () => { - aliceAgent = new Agent(aliceAgentOptions) - bobAgent = new Agent(bobAgentOptions) - }) - - afterEach(async () => { - await aliceAgent.shutdown() - await bobAgent.shutdown() - - if (aliceAgent.wallet.isProvisioned) { - await aliceAgent.wallet.delete() - } - - if (bobAgent.wallet.isProvisioned) { - await bobAgent.wallet.delete() - } - }) - - test('open, create and open wallet with different wallet key that it is in agent config', async () => { - const walletConfig = { - id: 'mywallet', - key: 'mysecretwalletkey', - } - - try { - await aliceAgent.wallet.open(walletConfig) - } catch (error) { - if (error instanceof WalletNotFoundError) { - await aliceAgent.wallet.create(walletConfig) - await aliceAgent.wallet.open(walletConfig) - } - } - - await aliceAgent.initialize() - - expect(aliceAgent.isInitialized).toBe(true) - }) - - test('when creating already existing wallet throw WalletDuplicateError', async () => { - const walletConfig = { - id: 'mywallet', - key: 'mysecretwalletkey', - } - - await aliceAgent.wallet.create(walletConfig) - - await expect(aliceAgent.wallet.create(walletConfig)).rejects.toThrowError(WalletDuplicateError) - }) - - test('when opening non-existing wallet throw WalletNotFoundError', async () => { - const walletConfig = { - id: 'mywallet', - key: 'mysecretwalletkey', - } - - await expect(aliceAgent.wallet.open(walletConfig)).rejects.toThrowError(WalletNotFoundError) - }) - - test('when opening wallet with invalid key throw WalletInvalidKeyError', async () => { - const walletConfig = { - id: 'mywallet', - key: 'mysecretwalletkey', - } - - await aliceAgent.wallet.create(walletConfig) - await expect(aliceAgent.wallet.open({ ...walletConfig, key: 'abcd' })).rejects.toThrowError(WalletInvalidKeyError) - }) - - test('when create wallet and shutdown, wallet is closed', async () => { - const walletConfig = { - id: 'mywallet', - key: 'mysecretwalletkey', - } - - await aliceAgent.wallet.create(walletConfig) - - await aliceAgent.shutdown() - - await expect(aliceAgent.wallet.open(walletConfig)).resolves.toBeUndefined() - }) - - test('create wallet with custom key derivation method', async () => { - const walletConfig = { - id: 'mywallet', - key: 'mysecretwalletkey', - keyDerivationMethod: KeyDerivationMethod.Argon2IInt, - } - - await aliceAgent.wallet.createAndOpen(walletConfig) - - expect(aliceAgent.wallet.isInitialized).toBe(true) - }) - - test('when exporting and importing a wallet, content is copied', async () => { - await bobAgent.initialize() - const bobBasicMessageRepository = bobAgent.dependencyManager.resolve(BasicMessageRepository) - - const basicMessageRecord = new BasicMessageRecord({ - id: 'some-id', - connectionId: 'connId', - content: 'hello', - role: BasicMessageRole.Receiver, - sentTime: 'sentIt', - }) - - // Save in wallet - await bobBasicMessageRepository.save(bobAgent.context, basicMessageRecord) - - if (!bobAgent.config.walletConfig) { - throw new Error('No wallet config on bobAgent') - } - - const backupKey = 'someBackupKey' - const backupWalletName = `backup-${uuid()}` - const backupPath = path.join(tmpdir(), backupWalletName) - - // Create backup and delete wallet - await bobAgent.wallet.export({ path: backupPath, key: backupKey }) - await bobAgent.wallet.delete() - - // Initialize the wallet again and assert record does not exist - // This should create a new wallet - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await bobAgent.wallet.initialize(bobAgentOptions.config.walletConfig!) - expect(await bobBasicMessageRepository.findById(bobAgent.context, basicMessageRecord.id)).toBeNull() - await bobAgent.wallet.delete() - - // Import backup with different wallet id and initialize - await bobAgent.wallet.import({ id: backupWalletName, key: backupWalletName }, { path: backupPath, key: backupKey }) - await bobAgent.wallet.initialize({ id: backupWalletName, key: backupWalletName }) - - // Expect same basic message record to exist in new wallet - expect(await bobBasicMessageRepository.getById(bobAgent.context, basicMessageRecord.id)).toMatchObject({ - id: basicMessageRecord.id, - connectionId: basicMessageRecord.connectionId, - content: basicMessageRecord.content, - createdAt: basicMessageRecord.createdAt, - updatedAt: basicMessageRecord.updatedAt, - type: basicMessageRecord.type, - }) - }) - - test('changing wallet key', async () => { - const walletConfig = { - id: 'mywallet', - key: 'mysecretwalletkey', - } - - await aliceAgent.wallet.createAndOpen(walletConfig) - await aliceAgent.initialize() - - //Close agent - const walletConfigRekey = { - id: 'mywallet', - key: 'mysecretwalletkey', - rekey: '123', - } - - await aliceAgent.shutdown() - await aliceAgent.wallet.rotateKey(walletConfigRekey) - await aliceAgent.initialize() - - expect(aliceAgent.isInitialized).toBe(true) - }) -}) diff --git a/packages/indy-sdk-to-askar-migration/package.json b/packages/indy-sdk-to-askar-migration/package.json index c39397df01..53bfc2f452 100644 --- a/packages/indy-sdk-to-askar-migration/package.json +++ b/packages/indy-sdk-to-askar-migration/package.json @@ -30,10 +30,8 @@ "@credo-ts/node": "0.4.2" }, "devDependencies": { - "@credo-ts/indy-sdk": "0.4.2", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", "@hyperledger/aries-askar-shared": "^0.2.0-dev.5", - "indy-sdk": "^1.16.0-dev-1655", "rimraf": "^4.4.0", "typescript": "~4.9.5" }, diff --git a/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts b/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts index 9e1332533c..3d9f51aced 100644 --- a/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts +++ b/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts @@ -119,7 +119,7 @@ export class IndySdkToAskarMigrationUpdater { /** * Location of the new wallet */ - private get newWalletPath() { + public get newWalletPath() { return `${this.fs.dataPath}/wallet/${this.walletConfig.id}/sqlite.db` } diff --git a/packages/indy-sdk-to-askar-migration/tests/indy-sdk-040-wallet.db b/packages/indy-sdk-to-askar-migration/tests/indy-sdk-040-wallet.db new file mode 100644 index 0000000000000000000000000000000000000000..1fc1e28b4080b86ad51c7bed7e72af3cf8c2c817 GIT binary patch literal 57344 zcmeI4X;f3!7RQqqWezPxQWY@{P|{u{_a=lytcnmB1q@P^Dv=>b1VLsHQ34ckU@3^G zloq9Gg(^?<30jA$l@^>=eRhecsMJ|Ro>kvdD{r45u_)H{zW9OeySU`$oPGB2`|ljR zH%kL1`)Cr?f{57oxgm)HG3z}Rhr=2#5U^M*4xBtVyABh$;C8)%SN5N--*m}gjq+Py z#`9#E8CI~&lX*LMlTAf?wh0;b(oAXDlihj6dp(b2_OL^fCP{L5_p|Jl9>U2 zoRFH@Z}J&&l>{9gv@;< zp6OfSkrh3JBg4Y1(MAS*p^%;9kQfpg{fgu6Wf{K+GsR_skK$Ea#)21zE|{<16oGG` zj}N6E?yULs@fVmupeYJ>g;Makg}>5sl8bV>z*{lho;arjUDCYtKG9F9@O1ap*>uh7 zLzBdmiirxP!q-(1pj(|_uL&1+Gk^wgQ}`&LRa{*HTwUB0ug(VgyOBov^Q_R>xF6q7 z$Y$x3N{EYw(5jH6#8}!-UG>DAWH zyzFO#p88pDr+V4X`Z2?|1nu6>`epVn`ng|E{j9fA5lwYy^g{-3iKlC^u1~Cfm99CU zpSiE~v)-h8n7*&~v)p3~FYkPj(mh`dC-kvO{1Vjv>)`erh=kwWFA-XL~H?RBE zr<-0Km!vA8AA>Mn~dj3!Cqtt0VIQjGb)wSygwFYbh{@}rEZD$SS6A%uUU9X&O zm2`~?UUubQR`aI4w#Q7&Kr*(eoCQC4AOR$R1dsp{Kmter2_OL^fCPFbu-J{wWvwtx z9|pHQ3ByKzPCGFaLo*cZD5WIuB9l|JlbB*0B@``kgln3jr7#RHPI8K&#Bd=NQ8WXm z3?}4YBbHENCzzK}QVB&YC9qfw&O}m*5s4`oyp<4p83pd7GK!`hAt+5T5D0=WUP9<@1&LsuApl_l{6IveQxhTv$%6r%oE#~FK@KNPG6p}8JWYa90D`1M zRzcBF3Hc$0a0AD98*b;k!f>Z`+ zf+vVbz|#zrL^Ob8U_=Cyy3UYOAUqKtnv0PT0|+jLVnA~mq=#10$mTAs_Evhtw!+-tL@c_+t6$DOBqEDXy(IXg0AgsLEJfK}WO&8>BtT3zp$7j0Q( z|KTRJ%%uAKf+dFDH)kr<4OJJfY5$nxa?6v`V4YNESC(?Qb*|PWcU)vkm6gYw^7OLI z-`6=gUvF+wf3c?F;d;N)o8Px>jJTP0U0pSHU%sZK+5PH2!eaanht~8RIR5LKb|)gd zE-PzR&b#fm?8)AFoD_Ngyu7KrD^c;DyDS~#5h2e9)Eu{B4zeSoa(J#8kFD!7H;XJp z+gTf?a_pKo-=qd#s@peuQmtL$G}(?tyY?I#>*y}1U-IbK-4nx0?pJ5q9Bz@c6!lTTJA3dyP zCA&@3;lJ5D++?>g`pES!iz|I&yl#ur?tQmF`h8W7^`xx@hqSYvWPiU-wRE`u)A#Of z+VkK`b<@oGgA-N$rlx-G=h8pOwGj>c=&<2OmvgVy zb!>GwZZp~IN#0~9Md|3&M?dD61dbXJSNXGMjD75&hohC@;{wXoE{-}n^z8bm8F|Aq zHg}w}jci@NZ2GVp%WYCl)gQ4ud9J{h9kH`-+pT++S6gD%&uS_h)lxaTYVf*BzE+~K zowj84>>s@A!imrYc5y~UOA_Z|=D(U)7VXH2<`}U)(^_Y)Kh{68$hLN~(9JDqVN3e% zz#-ep1CKk;&%fyvp8BBJ?9BOxm-^iG$ZH%}kTk=cef8sIRW}YSG)%tK)>2`caPxyv zDQ6aL4&0VuQy6EuBHyCcI-}inUqQ>YoSh>Bw{KXw$Nkj3{I&tX@qs%-KfLtqREwXl z#dWl=x^cI?IB;Ub%#p|I7AxB(m_IvL7ZtY;{Th zbinM>yLOak{&!5lousk-fA?x@b@v!*rOeKqySI6G>~HnSdpOD0ciE=2{dy>Jsw6#%b)j;{wt$KCJcE9<2X@Y@sfdmc zRu3M$;>@x_A7l8_jmz_3nHQT6 { - beforeAll(() => { - registerAriesAskar({ askar: ariesAskar }) - }) - test('indy-sdk sqlite to aries-askar sqlite successful migration', async () => { const indySdkAndAskarConfig: InitConfig = { - label: `indy | indy-sdk sqlite to aries-askar sqlite successful migration | ${utils.uuid()}`, + label: `indy | indy-sdk sqlite to aries-askar sqlite successful migration`, walletConfig: { - id: `indy-sdk sqlite to aries-askar sqlite successful migration | ${utils.uuid()}`, + id: `indy-sdk sqlite to aries-askar sqlite successful migration`, key: 'GfwU1DC7gEZNs3w41tjBiZYj7BNToDoFEqKY6wZXqs1A', keyDerivationMethod: KeyDerivationMethod.Raw, }, } - const indySdkAgent = new Agent({ - config: indySdkAndAskarConfig, - modules: { indySdk: new IndySdkModule({ indySdk: indy }) }, - dependencies: agentDependencies, - }) - const indySdkAgentDbPath = `${homedir()}/.indy_client/wallet/${indySdkAndAskarConfig.walletConfig?.id}/sqlite.db` - - const genericRecordContent = { foo: 'bar' } - - await indySdkAgent.initialize() - - const record = await indySdkAgent.genericRecords.save({ content: genericRecordContent }) - - await indySdkAgent.shutdown() - + const indySdkWalletTestPath = path.join(__dirname, 'indy-sdk-040-wallet.db') const askarAgent = new Agent({ config: indySdkAndAskarConfig, - modules: { askar: new AskarModule({ ariesAskar }) }, + modules: { askar: askarModule }, dependencies: agentDependencies, }) - const updater = await IndySdkToAskarMigrationUpdater.initialize({ dbPath: indySdkAgentDbPath, agent: askarAgent }) - await updater.update() + // Remove new wallet path (if exists) + if (existsSync(updater.newWalletPath)) unlinkSync(updater.newWalletPath) + + // Create old wallet path and copy test wallet + mkdirSync(path.dirname(indySdkAgentDbPath), { recursive: true }) + copyFileSync(indySdkWalletTestPath, indySdkAgentDbPath) + + await updater.update() await askarAgent.initialize() - await expect(askarAgent.genericRecords.findById(record.id)).resolves.toMatchObject({ - content: genericRecordContent, - }) + await expect(askarAgent.genericRecords.getAll()).resolves.toMatchObject([ + { + content: { + foo: 'bar', + }, + }, + ]) await askarAgent.shutdown() }) @@ -70,38 +60,21 @@ describe('Indy SDK To Askar Migration', () => { */ test('indy-sdk sqlite to aries-askar sqlite fails and restores', async () => { const indySdkAndAskarConfig: InitConfig = { - label: `indy | indy-sdk sqlite to aries-askar sqlite fails and restores | ${utils.uuid()}`, + label: `indy | indy-sdk sqlite to aries-askar sqlite fails and restores`, walletConfig: { - id: `indy-sdk sqlite to aries-askar sqlite fails and restores | ${utils.uuid()}`, - key: 'GfwU1DC7gEZNs3w41tjBiZYj7BNToDoFEqKY6wZXqs1A', + id: `indy-sdk sqlite to aries-askar sqlite fails and restores`, + // NOTE: wrong key passed + key: 'wrong-key', keyDerivationMethod: KeyDerivationMethod.Raw, }, } - const indySdkAgent = new Agent({ - config: indySdkAndAskarConfig, - modules: { indySdk: new IndySdkModule({ indySdk: indy }) }, - dependencies: agentDependencies, - }) - const indySdkAgentDbPath = `${homedir()}/.indy_client/wallet/${indySdkAndAskarConfig.walletConfig?.id}/sqlite.db` - - const genericRecordContent = { foo: 'bar' } - - await indySdkAgent.initialize() - - const record = await indySdkAgent.genericRecords.save({ content: genericRecordContent }) - - await indySdkAgent.shutdown() + const indySdkWalletTestPath = path.join(__dirname, 'indy-sdk-040-wallet.db') const askarAgent = new Agent({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - config: { ...indySdkAndAskarConfig, walletConfig: { ...indySdkAndAskarConfig.walletConfig!, key: 'wrong-key' } }, - modules: { - askar: new AskarModule({ - ariesAskar, - }), - }, + config: indySdkAndAskarConfig, + modules: { askar: askarModule }, dependencies: agentDependencies, }) @@ -110,12 +83,14 @@ describe('Indy SDK To Askar Migration', () => { agent: askarAgent, }) - await expect(updater.update()).rejects.toThrowError(IndySdkToAskarMigrationError) + // Remove new wallet path (if exists) + if (existsSync(updater.newWalletPath)) unlinkSync(updater.newWalletPath) - await indySdkAgent.initialize() + // Create old wallet path and copy test wallet + mkdirSync(path.dirname(indySdkAgentDbPath), { recursive: true }) + copyFileSync(indySdkWalletTestPath, indySdkAgentDbPath) - await expect(indySdkAgent.genericRecords.findById(record.id)).resolves.toMatchObject({ - content: genericRecordContent, - }) + await expect(updater.update()).rejects.toThrowError(IndySdkToAskarMigrationError) + expect(existsSync(indySdkWalletTestPath)).toBe(true) }) }) diff --git a/packages/indy-sdk/CHANGELOG.md b/packages/indy-sdk/CHANGELOG.md deleted file mode 100644 index 162267c246..0000000000 --- a/packages/indy-sdk/CHANGELOG.md +++ /dev/null @@ -1,59 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) - -### Bug Fixes - -- update tsyringe for ts 5 support ([#1588](https://github.com/hyperledger/aries-framework-javascript/issues/1588)) ([296955b](https://github.com/hyperledger/aries-framework-javascript/commit/296955b3a648416ac6b502da05a10001920af222)) - -## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) - -### Bug Fixes - -- **anoncreds:** wrong key name for predicates in proof object ([#1517](https://github.com/hyperledger/aries-framework-javascript/issues/1517)) ([d895c78](https://github.com/hyperledger/aries-framework-javascript/commit/d895c78e0e02954a95ad1fd7e2251ee9a02445dc)) -- force did:key resolver/registrar presence ([#1535](https://github.com/hyperledger/aries-framework-javascript/issues/1535)) ([aaa13dc](https://github.com/hyperledger/aries-framework-javascript/commit/aaa13dc77d6d5133cd02e768e4173462fa65064a)) - -### Features - -- **anoncreds:** auto create link secret ([#1521](https://github.com/hyperledger/aries-framework-javascript/issues/1521)) ([c6f03e4](https://github.com/hyperledger/aries-framework-javascript/commit/c6f03e49d79a33b1c4b459cef11add93dee051d0)) - -# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) - -### Bug Fixes - -- **anoncreds:** include prover_did for legacy indy ([#1342](https://github.com/hyperledger/aries-framework-javascript/issues/1342)) ([d38ecb1](https://github.com/hyperledger/aries-framework-javascript/commit/d38ecb14cb58f1eb78e01c91699bb990d805dc08)) -- **anoncreds:** make revocation status list inline with the spec ([#1421](https://github.com/hyperledger/aries-framework-javascript/issues/1421)) ([644e860](https://github.com/hyperledger/aries-framework-javascript/commit/644e860a05f40166e26c497a2e8619c9a38df11d)) -- did cache key not being set correctly ([#1394](https://github.com/hyperledger/aries-framework-javascript/issues/1394)) ([1125e81](https://github.com/hyperledger/aries-framework-javascript/commit/1125e81962ffa752bf40fa8f7f4226e186f22013)) -- expose indy pool configs and action menu messages ([#1333](https://github.com/hyperledger/aries-framework-javascript/issues/1333)) ([518e5e4](https://github.com/hyperledger/aries-framework-javascript/commit/518e5e4dfb59f9c0457bfd233409e9f4b3c429ee)) -- **indy-sdk:** import from core ([#1346](https://github.com/hyperledger/aries-framework-javascript/issues/1346)) ([254f661](https://github.com/hyperledger/aries-framework-javascript/commit/254f661c2e925b62dd07c3565099f9e226bd2b41)) -- issuance with unqualified identifiers ([#1431](https://github.com/hyperledger/aries-framework-javascript/issues/1431)) ([de90caf](https://github.com/hyperledger/aries-framework-javascript/commit/de90cafb8d12b7a940f881184cd745c4b5043cbc)) -- reference to indyLedgers in IndyXXXNotConfiguredError ([#1397](https://github.com/hyperledger/aries-framework-javascript/issues/1397)) ([d6e2ea2](https://github.com/hyperledger/aries-framework-javascript/commit/d6e2ea2194a4860265fe299ef8ee4cb4799ab1a6)) -- remove named capture groups ([#1378](https://github.com/hyperledger/aries-framework-javascript/issues/1378)) ([a4204ef](https://github.com/hyperledger/aries-framework-javascript/commit/a4204ef2db769de53d12f0d881d2c4422545c390)) -- seed and private key validation and return type in registrars ([#1324](https://github.com/hyperledger/aries-framework-javascript/issues/1324)) ([c0e5339](https://github.com/hyperledger/aries-framework-javascript/commit/c0e5339edfa32df92f23fb9c920796b4b59adf52)) -- various anoncreds revocation fixes ([#1416](https://github.com/hyperledger/aries-framework-javascript/issues/1416)) ([d9cfc7d](https://github.com/hyperledger/aries-framework-javascript/commit/d9cfc7df6679d2008d66070a6c8a818440d066ab)) - -- feat!: add data, cache and temp dirs to FileSystem (#1306) ([ff5596d](https://github.com/hyperledger/aries-framework-javascript/commit/ff5596d0631e93746494c017797d0191b6bdb0b1)), closes [#1306](https://github.com/hyperledger/aries-framework-javascript/issues/1306) - -### Features - -- 0.4.0 migration script ([#1392](https://github.com/hyperledger/aries-framework-javascript/issues/1392)) ([bc5455f](https://github.com/hyperledger/aries-framework-javascript/commit/bc5455f7b42612a2b85e504bc6ddd36283a42bfa)) -- add anoncreds-rs package ([#1275](https://github.com/hyperledger/aries-framework-javascript/issues/1275)) ([efe0271](https://github.com/hyperledger/aries-framework-javascript/commit/efe0271198f21f1307df0f934c380f7a5c720b06)) -- add fetch indy schema method ([#1290](https://github.com/hyperledger/aries-framework-javascript/issues/1290)) ([1d782f5](https://github.com/hyperledger/aries-framework-javascript/commit/1d782f54bbb4abfeb6b6db6cd4f7164501b6c3d9)) -- **anoncreds:** add anoncreds API ([#1232](https://github.com/hyperledger/aries-framework-javascript/issues/1232)) ([3a4c5ec](https://github.com/hyperledger/aries-framework-javascript/commit/3a4c5ecd940e49d4d192eef1d41f2aaedb34d85a)) -- **anoncreds:** add getCredential(s) methods ([#1386](https://github.com/hyperledger/aries-framework-javascript/issues/1386)) ([2efc009](https://github.com/hyperledger/aries-framework-javascript/commit/2efc0097138585391940fbb2eb504e50df57ec87)) -- **anoncreds:** add legacy indy credential format ([#1220](https://github.com/hyperledger/aries-framework-javascript/issues/1220)) ([13f3740](https://github.com/hyperledger/aries-framework-javascript/commit/13f374079262168f90ec7de7c3393beb9651295c)) -- **anoncreds:** legacy indy proof format service ([#1283](https://github.com/hyperledger/aries-framework-javascript/issues/1283)) ([c72fd74](https://github.com/hyperledger/aries-framework-javascript/commit/c72fd7416f2c1bc0497a84036e16adfa80585e49)) -- **anoncreds:** store method name in records ([#1387](https://github.com/hyperledger/aries-framework-javascript/issues/1387)) ([47636b4](https://github.com/hyperledger/aries-framework-javascript/commit/47636b4a08ffbfa9a3f2a5a3c5aebda44f7d16c8)) -- **anoncreds:** use legacy prover did ([#1374](https://github.com/hyperledger/aries-framework-javascript/issues/1374)) ([c17013c](https://github.com/hyperledger/aries-framework-javascript/commit/c17013c808a278d624210ce9e4333860cd78fc19)) -- **cache:** add caching interface ([#1229](https://github.com/hyperledger/aries-framework-javascript/issues/1229)) ([25b2bcf](https://github.com/hyperledger/aries-framework-javascript/commit/25b2bcf81648100b572784e4489a288cc9da0557)) -- **indy-vdr:** add IndyVdrAnonCredsRegistry ([#1270](https://github.com/hyperledger/aries-framework-javascript/issues/1270)) ([d056316](https://github.com/hyperledger/aries-framework-javascript/commit/d056316712b5ee5c42a159816b5dda0b05ad84a8)) -- **openid4vc:** jwt format and more crypto ([#1472](https://github.com/hyperledger/aries-framework-javascript/issues/1472)) ([bd4932d](https://github.com/hyperledger/aries-framework-javascript/commit/bd4932d34f7314a6d49097b6460c7570e1ebc7a8)) - -### BREAKING CHANGES - -- Agent-produced files will now be divided in different system paths depending on their nature: data, temp and cache. Previously, they were located at a single location, defaulting to a temporary directory. - -If you specified a custom path in `FileSystem` object constructor, you now must provide an object containing `baseDataPath`, `baseTempPath` and `baseCachePath`. They can point to the same path, although it's recommended to specify different path to avoid future file clashes. diff --git a/packages/indy-sdk/README.md b/packages/indy-sdk/README.md deleted file mode 100644 index 28cfde1767..0000000000 --- a/packages/indy-sdk/README.md +++ /dev/null @@ -1,31 +0,0 @@ -

-
- Credo Logo -

-

Credo IndySDK Module

-

- License - typescript - @credo-ts/indy-sdk version - -

-
- -IndySDK module for [Credo](https://github.com/openwallet-foundation/credo-ts.git). diff --git a/packages/indy-sdk/jest.config.ts b/packages/indy-sdk/jest.config.ts deleted file mode 100644 index 93c0197296..0000000000 --- a/packages/indy-sdk/jest.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Config } from '@jest/types' - -import base from '../../jest.config.base' - -import packageJson from './package.json' - -const config: Config.InitialOptions = { - ...base, - displayName: packageJson.name, - setupFilesAfterEnv: ['./tests/setup.ts'], -} - -export default config diff --git a/packages/indy-sdk/package.json b/packages/indy-sdk/package.json deleted file mode 100644 index f9dfd67cf4..0000000000 --- a/packages/indy-sdk/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "@credo-ts/indy-sdk", - "main": "build/index", - "types": "build/index", - "private": true, - "version": "0.4.2", - "files": [ - "build" - ], - "license": "Apache-2.0", - "publishConfig": { - "access": "public" - }, - "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/indy-sdk", - "repository": { - "type": "git", - "url": "https://github.com/openwallet-foundation/credo-ts", - "directory": "packages/indy-sdk" - }, - "scripts": { - "build": "yarn run clean && yarn run compile", - "clean": "rimraf ./build", - "compile": "tsc -p tsconfig.build.json", - "prepublishOnly": "yarn run build", - "test": "jest" - }, - "dependencies": { - "@credo-ts/anoncreds": "0.4.2", - "@credo-ts/core": "0.4.2", - "@stablelib/ed25519": "^1.0.3", - "@types/indy-sdk": "1.16.27", - "class-transformer": "0.5.1", - "class-validator": "0.14.0", - "rxjs": "^7.2.0", - "tsyringe": "^4.8.0" - }, - "devDependencies": { - "rimraf": "^4.4.0", - "typescript": "~4.9.5" - } -} diff --git a/packages/indy-sdk/src/IndySdkModule.ts b/packages/indy-sdk/src/IndySdkModule.ts deleted file mode 100644 index d501ece519..0000000000 --- a/packages/indy-sdk/src/IndySdkModule.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { IndySdkModuleConfigOptions } from './IndySdkModuleConfig' -import type { AgentContext, DependencyManager, Module } from '@credo-ts/core' - -import { - AnonCredsHolderServiceSymbol, - AnonCredsIssuerServiceSymbol, - AnonCredsVerifierServiceSymbol, -} from '@credo-ts/anoncreds' -import { AriesFrameworkError, InjectionSymbols } from '@credo-ts/core' - -import { IndySdkModuleConfig } from './IndySdkModuleConfig' -import { IndySdkHolderService, IndySdkIssuerService, IndySdkVerifierService } from './anoncreds' -import { IndySdkPoolService } from './ledger' -import { IndySdkStorageService } from './storage' -import { IndySdkSymbol } from './types' -import { IndySdkWallet } from './wallet' - -export class IndySdkModule implements Module { - public readonly config: IndySdkModuleConfig - - public constructor(config: IndySdkModuleConfigOptions) { - this.config = new IndySdkModuleConfig(config) - } - - public register(dependencyManager: DependencyManager) { - dependencyManager.registerInstance(IndySdkSymbol, this.config.indySdk) - - // Register config - dependencyManager.registerInstance(IndySdkModuleConfig, this.config) - - if (dependencyManager.isRegistered(InjectionSymbols.Wallet)) { - throw new AriesFrameworkError('There is an instance of Wallet already registered') - } else { - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, IndySdkWallet) - } - - if (dependencyManager.isRegistered(InjectionSymbols.StorageService)) { - throw new AriesFrameworkError('There is an instance of StorageService already registered') - } else { - dependencyManager.registerSingleton(InjectionSymbols.StorageService, IndySdkStorageService) - } - - // NOTE: for now we are registering the needed indy services. We may want to make this - // more explicit and require the user to register the services they need on the specific modules. - dependencyManager.registerSingleton(IndySdkPoolService) - dependencyManager.registerSingleton(AnonCredsIssuerServiceSymbol, IndySdkIssuerService) - dependencyManager.registerSingleton(AnonCredsHolderServiceSymbol, IndySdkHolderService) - dependencyManager.registerSingleton(AnonCredsVerifierServiceSymbol, IndySdkVerifierService) - } - - public async initialize(agentContext: AgentContext): Promise { - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - - for (const pool of indySdkPoolService.pools) { - if (pool.config.connectOnStartup) { - await pool.connect() - } - } - } -} diff --git a/packages/indy-sdk/src/IndySdkModuleConfig.ts b/packages/indy-sdk/src/IndySdkModuleConfig.ts deleted file mode 100644 index 65478f507c..0000000000 --- a/packages/indy-sdk/src/IndySdkModuleConfig.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { IndySdkPoolConfig } from './ledger' -import type * as IndySdk from 'indy-sdk' - -/** - * IndySdkModuleConfigOptions defines the interface for the options of the IndySdkModuleConfig class. - */ -export interface IndySdkModuleConfigOptions { - /** - * Implementation of the IndySdk interface according to the @types/indy-sdk package. - * - * - * ## Node.JS - * - * ```ts - * import * as indySdk from 'indy-sdk' - * - * const indySdkModule = new IndySdkModule({ - * indySdk - * }) - * ``` - * - * ## React Native - * - * ```ts - * import indySdk from 'indy-sdk-react-native' - * - * const indySdkModule = new IndySdkModule({ - * indySdk - * }) - * ``` - */ - indySdk: typeof IndySdk - - /** - * Array of indy networks to connect to. Each item in the list must include either the `genesisPath` or `genesisTransactions` property. - * - * @default [] - * - * @example - * ``` - * { - * isProduction: false, - * genesisPath: '/path/to/genesis.txn', - * indyNamespace: 'localhost:test', - * transactionAuthorAgreement: { - * version: '1', - * acceptanceMechanism: 'accept' - * } - * } - * ``` - */ - networks?: IndySdkPoolConfig[] - - /** - * Create a default link secret if there are no created link secrets. - * @defaultValue true - */ - autoCreateLinkSecret?: boolean -} - -export class IndySdkModuleConfig { - private options: IndySdkModuleConfigOptions - - public constructor(options: IndySdkModuleConfigOptions) { - this.options = options - } - - /** See {@link IndySdkModuleConfigOptions.indySdk} */ - public get indySdk() { - return this.options.indySdk - } - - public get networks() { - return this.options.networks ?? [] - } - - /** See {@link AnonCredsModuleConfigOptions.autoCreateLinkSecret} */ - public get autoCreateLinkSecret() { - return this.options.autoCreateLinkSecret ?? true - } -} diff --git a/packages/indy-sdk/src/anoncreds/index.ts b/packages/indy-sdk/src/anoncreds/index.ts deleted file mode 100644 index adba521ce0..0000000000 --- a/packages/indy-sdk/src/anoncreds/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { IndySdkAnonCredsRegistry } from './services/IndySdkAnonCredsRegistry' -export { IndySdkHolderService } from './services/IndySdkHolderService' -export { IndySdkIssuerService } from './services/IndySdkIssuerService' -export { IndySdkVerifierService } from './services/IndySdkVerifierService' diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts deleted file mode 100644 index 96b398d567..0000000000 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts +++ /dev/null @@ -1,634 +0,0 @@ -import type { IndySdkPool } from '../../ledger' -import type { IndySdk } from '../../types' -import type { - AnonCredsRegistry, - GetCredentialDefinitionReturn, - GetRevocationStatusListReturn, - GetRevocationRegistryDefinitionReturn, - GetSchemaReturn, - RegisterCredentialDefinitionOptions, - RegisterCredentialDefinitionReturn, - RegisterSchemaOptions, - RegisterSchemaReturn, - RegisterRevocationRegistryDefinitionReturn, - RegisterRevocationStatusListReturn, -} from '@credo-ts/anoncreds' -import type { AgentContext } from '@credo-ts/core' -import type { Schema as IndySdkSchema } from 'indy-sdk' - -import { - getUnqualifiedCredentialDefinitionId, - getUnqualifiedRevocationRegistryDefinitionId, - getUnqualifiedSchemaId, - parseIndyCredentialDefinitionId, - parseIndyDid, - parseIndyRevocationRegistryId, - parseIndySchemaId, -} from '@credo-ts/anoncreds' -import { AriesFrameworkError } from '@credo-ts/core' - -import { verificationKeyForIndyDid } from '../../dids/didIndyUtil' -import { IndySdkError, isIndyError } from '../../error' -import { IndySdkPoolService } from '../../ledger' -import { IndySdkSymbol } from '../../types' -import { - getDidIndyCredentialDefinitionId, - getDidIndySchemaId, - indySdkAnonCredsRegistryIdentifierRegex, -} from '../utils/identifiers' -import { anonCredsRevocationStatusListFromIndySdk } from '../utils/transform' - -export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { - public readonly methodName = 'indy' - - /** - * This class supports resolving and registering objects with did:indy as well as legacy indy identifiers. - * It needs to include support for the schema, credential definition, revocation registry as well - * as the issuer id (which is needed when registering objects). - */ - public readonly supportedIdentifier = indySdkAnonCredsRegistryIdentifierRegex - - public async getSchema(agentContext: AgentContext, schemaId: string): Promise { - try { - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - - // parse schema id (supports did:indy and legacy) - const { did, namespaceIdentifier, schemaName, schemaVersion } = parseIndySchemaId(schemaId) - const { pool } = await indySdkPoolService.getPoolForDid(agentContext, did) - agentContext.config.logger.debug(`Getting schema '${schemaId}' from ledger '${pool.didIndyNamespace}'`) - - // even though we support did:indy and legacy identifiers we always need to fetch using the legacy identifier - const legacySchemaId = getUnqualifiedSchemaId(namespaceIdentifier, schemaName, schemaVersion) - const request = await indySdk.buildGetSchemaRequest(null, legacySchemaId) - - agentContext.config.logger.trace( - `Submitting get schema request for schema '${schemaId}' to ledger '${pool.didIndyNamespace}'` - ) - const response = await indySdkPoolService.submitReadRequest(pool, request) - - agentContext.config.logger.trace(`Got un-parsed schema '${schemaId}' from ledger '${pool.didIndyNamespace}'`, { - response, - }) - - const [, schema] = await indySdk.parseGetSchemaResponse(response) - agentContext.config.logger.debug(`Got schema '${schemaId}' from ledger '${pool.didIndyNamespace}'`, { - schema, - }) - - return { - schema: { - attrNames: schema.attrNames, - name: schema.name, - version: schema.version, - issuerId: did, - }, - schemaId, - resolutionMetadata: {}, - schemaMetadata: { - didIndyNamespace: pool.didIndyNamespace, - // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. - // For this reason we return it in the metadata. - indyLedgerSeqNo: schema.seqNo, - }, - } - } catch (error) { - agentContext.config.logger.error(`Error retrieving schema '${schemaId}'`, { - error, - schemaId, - }) - - return { - schemaId, - resolutionMetadata: { - error: 'notFound', - message: `unable to resolve credential definition: ${error.message}`, - }, - schemaMetadata: {}, - } - } - } - - public async registerSchema( - agentContext: AgentContext, - options: RegisterSchemaOptions - ): Promise { - try { - // This will throw an error if trying to register a schema with a legacy indy identifier. We only support did:indy identifiers - // for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. - const { namespaceIdentifier, namespace } = parseIndyDid(options.schema.issuerId) - - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - - const pool = indySdkPoolService.getPoolForNamespace(namespace) - agentContext.config.logger.debug( - `Register schema on ledger '${pool.didIndyNamespace}' with did '${options.schema.issuerId}'`, - options.schema - ) - - const didIndySchemaId = getDidIndySchemaId( - namespace, - namespaceIdentifier, - options.schema.name, - options.schema.version - ) - const legacySchemaId = getUnqualifiedSchemaId(namespaceIdentifier, options.schema.name, options.schema.version) - - const schema = { - attrNames: options.schema.attrNames, - name: options.schema.name, - version: options.schema.version, - id: legacySchemaId, - ver: '1.0', - // Casted as because the type expect a seqNo, but that's not actually required for the input of - // buildSchemaRequest (seqNo is not yet known) - } as IndySdkSchema - - const request = await indySdk.buildSchemaRequest(namespaceIdentifier, schema) - const submitterKey = await verificationKeyForIndyDid(agentContext, options.schema.issuerId) - - const response = await indySdkPoolService.submitWriteRequest(agentContext, pool, request, submitterKey) - agentContext.config.logger.debug(`Registered schema '${schema.id}' on ledger '${pool.didIndyNamespace}'`, { - response, - schema, - }) - - return { - schemaState: { - state: 'finished', - schema: { - attrNames: schema.attrNames, - issuerId: options.schema.issuerId, - name: schema.name, - version: schema.version, - }, - schemaId: didIndySchemaId, - }, - registrationMetadata: {}, - schemaMetadata: { - // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. - // For this reason we return it in the metadata. - indyLedgerSeqNo: response.result.txnMetadata.seqNo, - }, - } - } catch (error) { - agentContext.config.logger.error(`Error registering schema for did '${options.schema.issuerId}'`, { - error, - did: options.schema.issuerId, - schema: options.schema, - }) - - return { - schemaMetadata: {}, - registrationMetadata: {}, - schemaState: { - state: 'failed', - schema: options.schema, - reason: `unknownError: ${error.message}`, - }, - } - } - } - - public async getCredentialDefinition( - agentContext: AgentContext, - credentialDefinitionId: string - ): Promise { - try { - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - - // we support did:indy and legacy identifiers - const { did, namespaceIdentifier, schemaSeqNo, tag } = parseIndyCredentialDefinitionId(credentialDefinitionId) - const { pool } = await indySdkPoolService.getPoolForDid(agentContext, did) - - agentContext.config.logger.debug( - `Using ledger '${pool.didIndyNamespace}' to retrieve credential definition '${credentialDefinitionId}'` - ) - - const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId(namespaceIdentifier, schemaSeqNo, tag) - const request = await indySdk.buildGetCredDefRequest(null, legacyCredentialDefinitionId) - - agentContext.config.logger.trace( - `Submitting get credential definition request for credential definition '${credentialDefinitionId}' to ledger '${pool.didIndyNamespace}'` - ) - - const response = await indySdkPoolService.submitReadRequest(pool, request) - agentContext.config.logger.trace( - `Got un-parsed credential definition '${credentialDefinitionId}' from ledger '${pool.didIndyNamespace}'`, - { - response, - } - ) - - const [, credentialDefinition] = await indySdk.parseGetCredDefResponse(response) - const { schema } = await this.fetchIndySchemaWithSeqNo(agentContext, pool, Number(credentialDefinition.schemaId)) - - if (credentialDefinition && schema) { - agentContext.config.logger.debug( - `Got credential definition '${credentialDefinitionId}' from ledger '${pool.didIndyNamespace}'`, - { - credentialDefinition, - } - ) - - // Format the schema id based on the type of the credential definition id - const schemaId = credentialDefinitionId.startsWith('did:indy') - ? getDidIndySchemaId(pool.didIndyNamespace, namespaceIdentifier, schema.name, schema.version) - : schema.schemaId - - return { - credentialDefinitionId, - credentialDefinition: { - issuerId: did, - schemaId, - tag: credentialDefinition.tag, - type: 'CL', - value: credentialDefinition.value, - }, - credentialDefinitionMetadata: { - didIndyNamespace: pool.didIndyNamespace, - }, - resolutionMetadata: {}, - } - } - - agentContext.config.logger.error(`Error retrieving credential definition '${credentialDefinitionId}'`, { - credentialDefinitionId, - }) - - return { - credentialDefinitionId, - credentialDefinitionMetadata: {}, - resolutionMetadata: { - error: 'notFound', - message: `unable to resolve credential definition`, - }, - } - } catch (error) { - agentContext.config.logger.error(`Error retrieving credential definition '${credentialDefinitionId}'`, { - error, - credentialDefinitionId, - }) - - return { - credentialDefinitionId, - credentialDefinitionMetadata: {}, - resolutionMetadata: { - error: 'notFound', - message: `unable to resolve credential definition: ${error.message}`, - }, - } - } - } - - public async registerCredentialDefinition( - agentContext: AgentContext, - options: RegisterCredentialDefinitionOptions - ): Promise { - try { - // This will throw an error if trying to register a credential defintion with a legacy indy identifier. We only support did:indy - // identifiers for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. - const { namespaceIdentifier, namespace } = parseIndyDid(options.credentialDefinition.issuerId) - - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - - const pool = indySdkPoolService.getPoolForNamespace(namespace) - agentContext.config.logger.debug( - `Registering credential definition on ledger '${pool.didIndyNamespace}' with did '${options.credentialDefinition.issuerId}'`, - options.credentialDefinition - ) - - // TODO: check structure of the schemaId - // TODO: this will bypass caching if done on a higher level. - const { schema, schemaMetadata, resolutionMetadata } = await this.getSchema( - agentContext, - options.credentialDefinition.schemaId - ) - - if (!schema || !schemaMetadata.indyLedgerSeqNo || typeof schemaMetadata.indyLedgerSeqNo !== 'number') { - return { - registrationMetadata: {}, - credentialDefinitionMetadata: { - didIndyNamespace: pool.didIndyNamespace, - }, - credentialDefinitionState: { - credentialDefinition: options.credentialDefinition, - state: 'failed', - reason: `error resolving schema with id ${options.credentialDefinition.schemaId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, - }, - } - } - - const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId( - namespaceIdentifier, - schemaMetadata.indyLedgerSeqNo, - options.credentialDefinition.tag - ) - const didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId( - namespace, - namespaceIdentifier, - schemaMetadata.indyLedgerSeqNo, - options.credentialDefinition.tag - ) - - const request = await indySdk.buildCredDefRequest(namespaceIdentifier, { - id: legacyCredentialDefinitionId, - // Indy ledger requires the credential schemaId to be a string of the schema seqNo. - schemaId: schemaMetadata.indyLedgerSeqNo.toString(), - tag: options.credentialDefinition.tag, - type: options.credentialDefinition.type, - value: options.credentialDefinition.value, - ver: '1.0', - }) - - const submitterKey = await verificationKeyForIndyDid(agentContext, options.credentialDefinition.issuerId) - const response = await indySdkPoolService.submitWriteRequest(agentContext, pool, request, submitterKey) - - agentContext.config.logger.debug( - `Registered credential definition '${didIndyCredentialDefinitionId}' on ledger '${pool.didIndyNamespace}'`, - { - response, - credentialDefinition: options.credentialDefinition, - } - ) - - return { - credentialDefinitionMetadata: {}, - credentialDefinitionState: { - credentialDefinition: options.credentialDefinition, - credentialDefinitionId: didIndyCredentialDefinitionId, - state: 'finished', - }, - registrationMetadata: {}, - } - } catch (error) { - agentContext.config.logger.error( - `Error registering credential definition for schema '${options.credentialDefinition.schemaId}'`, - { - error, - did: options.credentialDefinition.issuerId, - credentialDefinition: options.credentialDefinition, - } - ) - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - public async getRevocationRegistryDefinition( - agentContext: AgentContext, - revocationRegistryDefinitionId: string - ): Promise { - try { - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - - const { did, namespaceIdentifier, credentialDefinitionTag, revocationRegistryTag, schemaSeqNo } = - parseIndyRevocationRegistryId(revocationRegistryDefinitionId) - const { pool } = await indySdkPoolService.getPoolForDid(agentContext, did) - - agentContext.config.logger.debug( - `Using ledger '${pool.didIndyNamespace}' to retrieve revocation registry definition '${revocationRegistryDefinitionId}'` - ) - - const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryDefinitionId( - namespaceIdentifier, - schemaSeqNo, - credentialDefinitionTag, - revocationRegistryTag - ) - const request = await indySdk.buildGetRevocRegDefRequest(null, legacyRevocationRegistryId) - - agentContext.config.logger.trace( - `Submitting get revocation registry definition request for revocation registry definition '${revocationRegistryDefinitionId}' to ledger` - ) - const response = await indySdkPoolService.submitReadRequest(pool, request) - agentContext.config.logger.trace( - `Got un-parsed revocation registry definition '${revocationRegistryDefinitionId}' from ledger '${pool.didIndyNamespace}'`, - { - response, - } - ) - - const [, revocationRegistryDefinition] = await indySdk.parseGetRevocRegDefResponse(response) - - agentContext.config.logger.debug( - `Got revocation registry definition '${revocationRegistryDefinitionId}' from ledger`, - { - revocationRegistryDefinition, - } - ) - - const credentialDefinitionId = revocationRegistryDefinitionId.startsWith('did:indy:') - ? getDidIndyCredentialDefinitionId( - pool.didIndyNamespace, - namespaceIdentifier, - schemaSeqNo, - credentialDefinitionTag - ) - : getUnqualifiedCredentialDefinitionId(namespaceIdentifier, schemaSeqNo, credentialDefinitionTag) - - return { - resolutionMetadata: {}, - revocationRegistryDefinition: { - issuerId: did, - credDefId: credentialDefinitionId, - value: { - maxCredNum: revocationRegistryDefinition.value.maxCredNum, - publicKeys: revocationRegistryDefinition.value.publicKeys, - tailsHash: revocationRegistryDefinition.value.tailsHash, - tailsLocation: revocationRegistryDefinition.value.tailsLocation, - }, - tag: revocationRegistryDefinition.tag, - revocDefType: 'CL_ACCUM', - }, - revocationRegistryDefinitionId, - revocationRegistryDefinitionMetadata: { - issuanceType: revocationRegistryDefinition.value.issuanceType, - didIndyNamespace: pool.didIndyNamespace, - }, - } - } catch (error) { - agentContext.config.logger.error( - `Error retrieving revocation registry definition '${revocationRegistryDefinitionId}' from ledger`, - { - error, - revocationRegistryDefinitionId: revocationRegistryDefinitionId, - } - ) - - return { - resolutionMetadata: { - error: 'notFound', - message: `unable to resolve revocation registry definition: ${error.message}`, - }, - revocationRegistryDefinitionId, - revocationRegistryDefinitionMetadata: {}, - } - } - } - - public async registerRevocationRegistryDefinition(): Promise { - throw new AriesFrameworkError('Not implemented!') - } - - public async getRevocationStatusList( - agentContext: AgentContext, - revocationRegistryId: string, - timestamp: number - ): Promise { - try { - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - - const { did, namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, revocationRegistryTag } = - parseIndyRevocationRegistryId(revocationRegistryId) - const { pool } = await indySdkPoolService.getPoolForDid(agentContext, did) - - agentContext.config.logger.debug( - `Using ledger '${pool.didIndyNamespace}' to retrieve revocation registry deltas with revocation registry definition id '${revocationRegistryId}' until ${timestamp}` - ) - - const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryDefinitionId( - namespaceIdentifier, - schemaSeqNo, - credentialDefinitionTag, - revocationRegistryTag - ) - - // TODO: implement caching for returned deltas - const request = await indySdk.buildGetRevocRegDeltaRequest(null, legacyRevocationRegistryId, 0, timestamp) - - agentContext.config.logger.trace( - `Submitting get revocation registry delta request for revocation registry '${revocationRegistryId}' to ledger` - ) - - const response = await indySdkPoolService.submitReadRequest(pool, request) - agentContext.config.logger.trace( - `Got revocation registry delta unparsed-response '${revocationRegistryId}' from ledger`, - { - response, - } - ) - - const [, revocationRegistryDelta, deltaTimestamp] = await indySdk.parseGetRevocRegDeltaResponse(response) - - agentContext.config.logger.debug( - `Got revocation registry deltas '${revocationRegistryId}' until timestamp ${timestamp} from ledger`, - { - revocationRegistryDelta, - deltaTimestamp, - } - ) - - const { resolutionMetadata, revocationRegistryDefinition, revocationRegistryDefinitionMetadata } = - await this.getRevocationRegistryDefinition(agentContext, revocationRegistryId) - - if ( - !revocationRegistryDefinition || - !revocationRegistryDefinitionMetadata.issuanceType || - typeof revocationRegistryDefinitionMetadata.issuanceType !== 'string' - ) { - return { - resolutionMetadata: { - error: `error resolving revocation registry definition with id ${revocationRegistryId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, - }, - revocationStatusListMetadata: { - didIndyNamespace: pool.didIndyNamespace, - }, - } - } - - const isIssuanceByDefault = revocationRegistryDefinitionMetadata.issuanceType === 'ISSUANCE_BY_DEFAULT' - - return { - resolutionMetadata: {}, - revocationStatusList: anonCredsRevocationStatusListFromIndySdk( - revocationRegistryId, - revocationRegistryDefinition, - revocationRegistryDelta, - deltaTimestamp, - isIssuanceByDefault - ), - revocationStatusListMetadata: { - didIndyNamespace: pool.didIndyNamespace, - }, - } - } catch (error) { - agentContext.config.logger.error( - `Error retrieving revocation registry delta '${revocationRegistryId}' from ledger, potentially revocation interval ends before revocation registry creation?"`, - { - error, - revocationRegistryId: revocationRegistryId, - } - ) - - return { - resolutionMetadata: { - error: 'notFound', - message: `Error retrieving revocation registry delta '${revocationRegistryId}' from ledger, potentially revocation interval ends before revocation registry creation: ${error.message}`, - }, - revocationStatusListMetadata: {}, - } - } - } - - public async registerRevocationStatusList(): Promise { - throw new AriesFrameworkError('Not implemented!') - } - - private async fetchIndySchemaWithSeqNo(agentContext: AgentContext, pool: IndySdkPool, seqNo: number) { - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - - agentContext.config.logger.debug(`Getting transaction with seqNo '${seqNo}' from ledger '${pool.didIndyNamespace}'`) - - const request = await indySdk.buildGetTxnRequest(null, 'DOMAIN', seqNo) - - agentContext.config.logger.trace(`Submitting get transaction request to ledger '${pool.didIndyNamespace}'`) - const response = await indySdkPoolService.submitReadRequest(pool, request) - - const schema = response.result.data as SchemaType - - if (schema.txn.type !== '101') { - agentContext.config.logger.error(`Could not get schema from ledger for seq no ${seqNo}'`) - return {} - } - - return { - schema: { - // txnId is the schema id - schemaId: schema.txnMetadata.txnId, - attr_name: schema.txn.data.data.attr_names, - name: schema.txn.data.data.name, - version: schema.txn.data.data.version, - issuerId: schema.txn.metadata.from, - seqNo, - }, - indyNamespace: pool.didIndyNamespace, - } - } -} - -interface SchemaType { - txnMetadata: { - txnId: string - } - txn: { - metadata: { - from: string - } - data: { - data: { - attr_names: string[] - version: string - name: string - } - } - - type: string - } -} diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts deleted file mode 100644 index c4daf4266b..0000000000 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts +++ /dev/null @@ -1,464 +0,0 @@ -import type { - AnonCredsHolderService, - AnonCredsProof, - CreateCredentialRequestOptions, - CreateCredentialRequestReturn, - CreateProofOptions, - AnonCredsCredentialInfo, - GetCredentialOptions, - StoreCredentialOptions, - GetCredentialsForProofRequestOptions, - GetCredentialsForProofRequestReturn, - AnonCredsSelectedCredentials, - CreateLinkSecretOptions, - CreateLinkSecretReturn, - GetCredentialsOptions, -} from '@credo-ts/anoncreds' -import type { AgentContext } from '@credo-ts/core' -import type { - CredentialDefs, - IndyRequestedCredentials, - RevStates, - Schemas, - IndyCredential as IndySdkCredential, - IndyProofRequest, -} from 'indy-sdk' - -import { - parseIndyCredentialDefinitionId, - AnonCredsLinkSecretRepository, - generateLegacyProverDidLikeString, - storeLinkSecret, -} from '@credo-ts/anoncreds' -import { AriesFrameworkError, injectable, inject, utils } from '@credo-ts/core' - -import { IndySdkModuleConfig } from '../../IndySdkModuleConfig' -import { IndySdkError, isIndyError } from '../../error' -import { IndySdk, IndySdkSymbol } from '../../types' -import { assertIndySdkWallet } from '../../utils/assertIndySdkWallet' -import { - assertAllUnqualified, - assertUnqualifiedCredentialOffer, - assertUnqualifiedProofRequest, -} from '../utils/assertUnqualified' -import { - anonCredsCredentialRequestMetadataFromIndySdk, - indySdkCredentialDefinitionFromAnonCreds, - indySdkCredentialRequestMetadataFromAnonCreds, - indySdkRevocationRegistryDefinitionFromAnonCreds, - indySdkSchemaFromAnonCreds, -} from '../utils/transform' - -import { IndySdkRevocationService } from './IndySdkRevocationService' - -@injectable() -export class IndySdkHolderService implements AnonCredsHolderService { - private indySdk: IndySdk - private indyRevocationService: IndySdkRevocationService - - public constructor(indyRevocationService: IndySdkRevocationService, @inject(IndySdkSymbol) indySdk: IndySdk) { - this.indySdk = indySdk - this.indyRevocationService = indyRevocationService - } - - public async createLinkSecret( - agentContext: AgentContext, - options: CreateLinkSecretOptions - ): Promise { - assertIndySdkWallet(agentContext.wallet) - - const linkSecretId = options.linkSecretId ?? utils.uuid() - - try { - await this.indySdk.proverCreateMasterSecret(agentContext.wallet.handle, linkSecretId) - - // We don't have the value for the link secret when using the indy-sdk so we can't return it. - return { - linkSecretId, - } - } catch (error) { - agentContext.config.logger.error(`Error creating link secret`, { - error, - linkSecretId, - }) - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - public async createProof(agentContext: AgentContext, options: CreateProofOptions): Promise { - const { credentialDefinitions, proofRequest, selectedCredentials, schemas } = options - - assertIndySdkWallet(agentContext.wallet) - - // Make sure all identifiers are unqualified - assertAllUnqualified({ - schemaIds: Object.keys(options.schemas), - credentialDefinitionIds: Object.keys(options.credentialDefinitions), - revocationRegistryIds: Object.keys(options.revocationRegistries), - }) - - const linkSecretRepository = agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository) - - try { - agentContext.config.logger.debug('Creating Indy Proof') - const indyRevocationStates: RevStates = await this.indyRevocationService.createRevocationState( - agentContext, - proofRequest, - selectedCredentials, - options.revocationRegistries - ) - - // The AnonCredsSchema doesn't contain the seqNo anymore. However, the indy credential definition id - // does contain the seqNo, so we can extract it from the credential definition id. - const seqNoMap: { [schemaId: string]: number } = {} - - // Convert AnonCreds credential definitions to Indy credential definitions - const indyCredentialDefinitions: CredentialDefs = {} - for (const credentialDefinitionId in credentialDefinitions) { - const credentialDefinition = credentialDefinitions[credentialDefinitionId] - indyCredentialDefinitions[credentialDefinitionId] = indySdkCredentialDefinitionFromAnonCreds( - credentialDefinitionId, - credentialDefinition - ) - - // Get the seqNo for the schemas so we can use it when transforming the schemas - const { schemaSeqNo } = parseIndyCredentialDefinitionId(credentialDefinitionId) - seqNoMap[credentialDefinition.schemaId] = Number(schemaSeqNo) - } - - // Convert AnonCreds schemas to Indy schemas - const indySchemas: Schemas = {} - for (const schemaId in schemas) { - const schema = schemas[schemaId] - indySchemas[schemaId] = indySdkSchemaFromAnonCreds(schemaId, schema, seqNoMap[schemaId]) - } - - const linkSecretRecord = await linkSecretRepository.findDefault(agentContext) - if (!linkSecretRecord) { - // No default link secret - throw new AriesFrameworkError( - 'No default link secret found. Indy SDK requires a default link secret to be created before creating a proof.' - ) - } - - const indyProof = await this.indySdk.proverCreateProof( - agentContext.wallet.handle, - proofRequest as IndyProofRequest, - this.parseSelectedCredentials(selectedCredentials), - linkSecretRecord.linkSecretId, - indySchemas, - indyCredentialDefinitions, - indyRevocationStates - ) - - agentContext.config.logger.trace('Created Indy Proof', { - indyProof, - }) - - // FIXME IndyProof if badly typed in indy-sdk. It contains a `requested_predicates` property, which should be `predicates`. - return indyProof as unknown as AnonCredsProof - } catch (error) { - agentContext.config.logger.error(`Error creating Indy Proof`, { - error, - proofRequest, - selectedCredentials, - }) - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - public async storeCredential(agentContext: AgentContext, options: StoreCredentialOptions): Promise { - assertIndySdkWallet(agentContext.wallet) - assertAllUnqualified({ - schemaIds: [options.credentialDefinition.schemaId, options.credential.schema_id], - credentialDefinitionIds: [options.credentialDefinitionId, options.credential.cred_def_id], - revocationRegistryIds: [options.revocationRegistry?.id, options.credential.rev_reg_id], - }) - - const indyRevocationRegistryDefinition = options.revocationRegistry - ? indySdkRevocationRegistryDefinitionFromAnonCreds( - options.revocationRegistry.id, - options.revocationRegistry.definition - ) - : null - - try { - return await this.indySdk.proverStoreCredential( - agentContext.wallet.handle, - options.credentialId ?? null, - indySdkCredentialRequestMetadataFromAnonCreds(options.credentialRequestMetadata), - options.credential, - indySdkCredentialDefinitionFromAnonCreds(options.credentialDefinitionId, options.credentialDefinition), - indyRevocationRegistryDefinition - ) - } catch (error) { - agentContext.config.logger.error(`Error storing Indy Credential '${options.credentialId}'`, { - error, - }) - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - public async getCredential( - agentContext: AgentContext, - options: GetCredentialOptions - ): Promise { - assertIndySdkWallet(agentContext.wallet) - - try { - const result = await this.indySdk.proverGetCredential(agentContext.wallet.handle, options.credentialId) - - return { - credentialDefinitionId: result.cred_def_id, - attributes: result.attrs, - credentialId: result.referent, - schemaId: result.schema_id, - credentialRevocationId: result.cred_rev_id, - revocationRegistryId: result.rev_reg_id, - methodName: 'indy', - } - } catch (error) { - agentContext.config.logger.error(`Error getting Indy Credential '${options.credentialId}'`, { - error, - }) - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - public async getCredentials(agentContext: AgentContext, options: GetCredentialsOptions) { - assertIndySdkWallet(agentContext.wallet) - - // Indy SDK only supports indy credentials - if (options.methodName && options.methodName !== 'indy') { - return [] - } - - assertAllUnqualified({ - credentialDefinitionIds: [options.credentialDefinitionId], - schemaIds: [options.schemaId], - issuerIds: [options.issuerId, options.schemaIssuerId], - }) - - const credentials = await this.indySdk.proverGetCredentials(agentContext.wallet.handle, { - cred_def_id: options.credentialDefinitionId, - schema_id: options.schemaId, - schema_issuer_did: options.schemaIssuerId, - schema_name: options.schemaName, - schema_version: options.schemaVersion, - issuer_did: options.issuerId, - }) - - return credentials.map((credential) => ({ - credentialDefinitionId: credential.cred_def_id, - attributes: credential.attrs, - credentialId: credential.referent, - schemaId: credential.schema_id, - credentialRevocationId: credential.cred_rev_id, - revocationRegistryId: credential.rev_reg_id, - methodName: 'indy', - })) - } - - public async createCredentialRequest( - agentContext: AgentContext, - options: CreateCredentialRequestOptions - ): Promise { - assertIndySdkWallet(agentContext.wallet) - - assertUnqualifiedCredentialOffer(options.credentialOffer) - assertAllUnqualified({ - schemaIds: [options.credentialDefinition.schemaId], - issuerIds: [options.credentialDefinition.issuerId], - }) - - if (!options.useLegacyProverDid) { - throw new AriesFrameworkError('Indy SDK only supports legacy prover did for credential requests') - } - - const linkSecretRepository = agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository) - - // We just generate a prover did like string, as it's not used for anything and we don't need - // to prove ownership of the did. It's deprecated in AnonCreds v1, but kept for backwards compatibility - const proverDid = generateLegacyProverDidLikeString() - - // If a link secret is specified, use it. Otherwise, attempt to use default link secret - let linkSecretRecord = options.linkSecretId - ? await linkSecretRepository.getByLinkSecretId(agentContext, options.linkSecretId) - : await linkSecretRepository.findDefault(agentContext) - - // No default link secret. Automatically create one if set on module config - if (!linkSecretRecord) { - const moduleConfig = agentContext.dependencyManager.resolve(IndySdkModuleConfig) - if (!moduleConfig.autoCreateLinkSecret) { - throw new AriesFrameworkError( - 'No link secret provided to createCredentialRequest and no default link secret has been found' - ) - } - const { linkSecretId } = await this.createLinkSecret(agentContext, {}) - linkSecretRecord = await storeLinkSecret(agentContext, { linkSecretId, setAsDefault: true }) - } - - try { - const result = await this.indySdk.proverCreateCredentialReq( - agentContext.wallet.handle, - proverDid, - options.credentialOffer, - // NOTE: Is it safe to use the cred_def_id from the offer? I think so. You can't create a request - // for a cred def that is not in the offer - indySdkCredentialDefinitionFromAnonCreds(options.credentialOffer.cred_def_id, options.credentialDefinition), - linkSecretRecord.linkSecretId - ) - - return { - credentialRequest: result[0], - // The type is typed as a Record in the indy-sdk, but the anoncreds package contains the correct type - credentialRequestMetadata: anonCredsCredentialRequestMetadataFromIndySdk(result[1]), - } - } catch (error) { - agentContext.config.logger.error(`Error creating Indy Credential Request`, { - error, - credentialOffer: options.credentialOffer, - }) - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - public async deleteCredential(agentContext: AgentContext, credentialId: string): Promise { - assertIndySdkWallet(agentContext.wallet) - - try { - return await this.indySdk.proverDeleteCredential(agentContext.wallet.handle, credentialId) - } catch (error) { - agentContext.config.logger.error(`Error deleting Indy Credential from Wallet`, { - error, - }) - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - public async getCredentialsForProofRequest( - agentContext: AgentContext, - options: GetCredentialsForProofRequestOptions - ): Promise { - assertIndySdkWallet(agentContext.wallet) - assertUnqualifiedProofRequest(options.proofRequest) - - try { - // Open indy credential search - const searchHandle = await this.indySdk.proverSearchCredentialsForProofReq( - agentContext.wallet.handle, - options.proofRequest as IndyProofRequest, - options.extraQuery ?? null - ) - - const start = options.start ?? 0 - - try { - // Make sure database cursors start at 'start' (bit ugly, but no way around in indy) - if (start > 0) { - await this.fetchCredentialsForReferent(agentContext, searchHandle, options.attributeReferent, start) - } - - // Fetch the credentials - const credentials = await this.fetchCredentialsForReferent( - agentContext, - searchHandle, - options.attributeReferent, - options.limit - ) - - // TODO: sort the credentials (irrevocable first) - return credentials.map((credential) => ({ - credentialInfo: { - credentialDefinitionId: credential.cred_info.cred_def_id, - credentialId: credential.cred_info.referent, - attributes: credential.cred_info.attrs, - schemaId: credential.cred_info.schema_id, - revocationRegistryId: credential.cred_info.rev_reg_id, - credentialRevocationId: credential.cred_info.cred_rev_id, - methodName: 'indy', - }, - interval: credential.interval, - })) - } finally { - // Always close search - await this.indySdk.proverCloseCredentialsSearchForProofReq(searchHandle) - } - } catch (error) { - if (isIndyError(error)) { - throw new IndySdkError(error) - } - - throw error - } - } - - private async fetchCredentialsForReferent( - agentContext: AgentContext, - searchHandle: number, - referent: string, - limit?: number - ) { - try { - let credentials: IndySdkCredential[] = [] - - // Allow max of 256 per fetch operation - const chunk = limit ? Math.min(256, limit) : 256 - - // Loop while limit not reached (or no limit specified) - while (!limit || credentials.length < limit) { - // Retrieve credentials - const credentialsJson = await this.indySdk.proverFetchCredentialsForProofReq(searchHandle, referent, chunk) - credentials = [...credentials, ...credentialsJson] - - // If the number of credentials returned is less than chunk - // It means we reached the end of the iterator (no more credentials) - if (credentialsJson.length < chunk) { - return credentials - } - } - - return credentials - } catch (error) { - agentContext.config.logger.error(`Error Fetching Indy Credentials For Referent`, { - error, - }) - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - /** - * Converts a public api form of {@link AnonCredsSelectedCredentials} interface into a format {@link Indy.IndyRequestedCredentials} that Indy SDK expects. - **/ - private parseSelectedCredentials(selectedCredentials: AnonCredsSelectedCredentials): IndyRequestedCredentials { - const indyRequestedCredentials: IndyRequestedCredentials = { - requested_attributes: {}, - requested_predicates: {}, - self_attested_attributes: {}, - } - - for (const groupName in selectedCredentials.attributes) { - indyRequestedCredentials.requested_attributes[groupName] = { - cred_id: selectedCredentials.attributes[groupName].credentialId, - revealed: selectedCredentials.attributes[groupName].revealed, - timestamp: selectedCredentials.attributes[groupName].timestamp, - } - } - - for (const groupName in selectedCredentials.predicates) { - indyRequestedCredentials.requested_predicates[groupName] = { - cred_id: selectedCredentials.predicates[groupName].credentialId, - timestamp: selectedCredentials.predicates[groupName].timestamp, - } - } - - return indyRequestedCredentials - } -} diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts deleted file mode 100644 index 075f89598a..0000000000 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { CreateCredentialDefinitionMetadata } from './IndySdkIssuerServiceMetadata' -import type { - AnonCredsIssuerService, - CreateCredentialDefinitionOptions, - CreateCredentialOfferOptions, - CreateCredentialOptions, - CreateCredentialReturn, - CreateSchemaOptions, - AnonCredsCredentialOffer, - AnonCredsSchema, - CreateCredentialDefinitionReturn, - CreateRevocationRegistryDefinitionReturn, - AnonCredsRevocationStatusList, -} from '@credo-ts/anoncreds' -import type { AgentContext } from '@credo-ts/core' - -import { parseIndyDid, getUnqualifiedSchemaId, generateLegacyProverDidLikeString } from '@credo-ts/anoncreds' -import { injectable, AriesFrameworkError, inject } from '@credo-ts/core' - -import { IndySdkError, isIndyError } from '../../error' -import { IndySdk, IndySdkSymbol } from '../../types' -import { assertIndySdkWallet } from '../../utils/assertIndySdkWallet' -import { - assertUnqualifiedCredentialDefinitionId, - assertUnqualifiedCredentialOffer, - assertUnqualifiedCredentialRequest, - assertUnqualifiedRevocationRegistryId, -} from '../utils/assertUnqualified' -import { indySdkSchemaFromAnonCreds } from '../utils/transform' - -@injectable() -export class IndySdkIssuerService implements AnonCredsIssuerService { - private indySdk: IndySdk - - public constructor(@inject(IndySdkSymbol) indySdk: IndySdk) { - this.indySdk = indySdk - } - - public async createRevocationStatusList(): Promise { - throw new AriesFrameworkError('Method not implemented.') - } - - public async updateRevocationStatusList(): Promise { - throw new AriesFrameworkError('Method not implemented.') - } - - public async createRevocationRegistryDefinition(): Promise { - throw new AriesFrameworkError('Method not implemented.') - } - - public async createSchema(agentContext: AgentContext, options: CreateSchemaOptions): Promise { - // We only support passing qualified did:indy issuer ids in the indy issuer service when creating objects - const { namespaceIdentifier } = parseIndyDid(options.issuerId) - - const { name, version, attrNames, issuerId } = options - assertIndySdkWallet(agentContext.wallet) - - try { - const [, schema] = await this.indySdk.issuerCreateSchema(namespaceIdentifier, name, version, attrNames) - - return { - issuerId, - attrNames: schema.attrNames, - name: schema.name, - version: schema.version, - } - } catch (error) { - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - public async createCredentialDefinition( - agentContext: AgentContext, - options: CreateCredentialDefinitionOptions, - metadata?: CreateCredentialDefinitionMetadata - ): Promise { - const { tag, supportRevocation, schema, issuerId, schemaId } = options - - // We only support passing qualified did:indy issuer ids in the indy issuer service when creating objects - const { namespaceIdentifier } = parseIndyDid(options.issuerId) - - // parse schema in a way that supports both unqualified and qualified identifiers - const legacySchemaId = getUnqualifiedSchemaId(namespaceIdentifier, schema.name, schema.version) - - if (!metadata) - throw new AriesFrameworkError('The metadata parameter is required when using Indy, but received undefined.') - - try { - assertIndySdkWallet(agentContext.wallet) - const [, credentialDefinition] = await this.indySdk.issuerCreateAndStoreCredentialDef( - agentContext.wallet.handle, - namespaceIdentifier, - indySdkSchemaFromAnonCreds(legacySchemaId, schema, metadata.indyLedgerSchemaSeqNo), - tag, - 'CL', - { - support_revocation: supportRevocation, - } - ) - - return { - credentialDefinition: { - issuerId, - tag: credentialDefinition.tag, - schemaId, - type: 'CL', - value: credentialDefinition.value, - }, - } - } catch (error) { - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - public async createCredentialOffer( - agentContext: AgentContext, - options: CreateCredentialOfferOptions - ): Promise { - assertIndySdkWallet(agentContext.wallet) - assertUnqualifiedCredentialDefinitionId(options.credentialDefinitionId) - - try { - return await this.indySdk.issuerCreateCredentialOffer(agentContext.wallet.handle, options.credentialDefinitionId) - } catch (error) { - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - public async createCredential( - agentContext: AgentContext, - options: CreateCredentialOptions - ): Promise { - const { - revocationStatusList, - credentialOffer, - credentialRequest, - credentialValues, - revocationRegistryDefinitionId, - } = options - - assertIndySdkWallet(agentContext.wallet) - assertUnqualifiedCredentialOffer(options.credentialOffer) - assertUnqualifiedCredentialRequest(options.credentialRequest) - if (options.revocationRegistryDefinitionId) { - assertUnqualifiedRevocationRegistryId(options.revocationRegistryDefinitionId) - } - - try { - if (revocationRegistryDefinitionId || revocationStatusList) { - throw new AriesFrameworkError('Revocation not supported yet') - } - - // prover_did is deprecated and thus if not provided we generate something on our side, as it's still required by the indy sdk - const proverDid = credentialRequest.prover_did ?? generateLegacyProverDidLikeString() - - const [credential, credentialRevocationId] = await this.indySdk.issuerCreateCredential( - agentContext.wallet.handle, - credentialOffer, - { ...credentialRequest, prover_did: proverDid }, - credentialValues, - revocationRegistryDefinitionId ?? null, - 0 - ) - - return { - credential, - credentialRevocationId, - } - } catch (error) { - throw isIndyError(error) ? new IndySdkError(error) : error - } - } -} diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerServiceMetadata.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerServiceMetadata.ts deleted file mode 100644 index bb02f17967..0000000000 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerServiceMetadata.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type CreateCredentialDefinitionMetadata = { - indyLedgerSchemaSeqNo: number -} diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts deleted file mode 100644 index 78c851d41f..0000000000 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { - AnonCredsRevocationRegistryDefinition, - AnonCredsRevocationStatusList, - AnonCredsProofRequest, - AnonCredsSelectedCredentials, - AnonCredsCredentialInfo, - AnonCredsNonRevokedInterval, -} from '@credo-ts/anoncreds' -import type { AgentContext } from '@credo-ts/core' -import type { RevStates } from 'indy-sdk' - -import { assertBestPracticeRevocationInterval } from '@credo-ts/anoncreds' -import { AriesFrameworkError, inject, injectable } from '@credo-ts/core' - -import { IndySdkError, isIndyError } from '../../error' -import { IndySdk, IndySdkSymbol } from '../../types' -import { createTailsReader } from '../utils/tails' -import { - indySdkRevocationDeltaFromAnonCreds, - indySdkRevocationRegistryDefinitionFromAnonCreds, -} from '../utils/transform' - -enum RequestReferentType { - Attribute = 'attribute', - Predicate = 'predicate', - SelfAttestedAttribute = 'self-attested-attribute', -} - -/** - * Internal class that handles revocation related logic for the Indy SDK - * - * @internal - */ -@injectable() -export class IndySdkRevocationService { - private indySdk: IndySdk - - public constructor(@inject(IndySdkSymbol) indySdk: IndySdk) { - this.indySdk = indySdk - } - - /** - * Creates the revocation state for the requested credentials in a format that the Indy SDK expects. - */ - public async createRevocationState( - agentContext: AgentContext, - proofRequest: AnonCredsProofRequest, - selectedCredentials: AnonCredsSelectedCredentials, - revocationRegistries: { - [revocationRegistryDefinitionId: string]: { - // Tails is already downloaded - tailsFilePath: string - definition: AnonCredsRevocationRegistryDefinition - revocationStatusLists: { - [timestamp: string]: AnonCredsRevocationStatusList - } - } - } - ): Promise { - try { - agentContext.config.logger.debug(`Creating Revocation State(s) for proof request`, { - proofRequest, - selectedCredentials, - }) - const indyRevocationStates: RevStates = {} - const referentCredentials: Array<{ - type: RequestReferentType - referent: string - credentialInfo: AnonCredsCredentialInfo - referentRevocationInterval: AnonCredsNonRevokedInterval | undefined - timestamp: number | undefined - }> = [] - - //Retrieve information for referents and push to single array - for (const [referent, selectedCredential] of Object.entries(selectedCredentials.attributes ?? {})) { - referentCredentials.push({ - referent, - credentialInfo: selectedCredential.credentialInfo, - type: RequestReferentType.Attribute, - referentRevocationInterval: proofRequest.requested_attributes[referent].non_revoked, - timestamp: selectedCredential.timestamp, - }) - } - for (const [referent, selectedCredential] of Object.entries(selectedCredentials.predicates ?? {})) { - referentCredentials.push({ - referent, - credentialInfo: selectedCredential.credentialInfo, - type: RequestReferentType.Predicate, - referentRevocationInterval: proofRequest.requested_predicates[referent].non_revoked, - timestamp: selectedCredential.timestamp, - }) - } - - for (const { referent, credentialInfo, type, referentRevocationInterval, timestamp } of referentCredentials) { - // Prefer referent-specific revocation interval over global revocation interval - const requestRevocationInterval = referentRevocationInterval ?? proofRequest.non_revoked - const credentialRevocationId = credentialInfo.credentialRevocationId - const revocationRegistryId = credentialInfo.revocationRegistryId - - // If revocation interval is present and the credential is revocable then create revocation state - if (requestRevocationInterval && timestamp && credentialRevocationId && revocationRegistryId) { - agentContext.config.logger.trace( - `Presentation is requesting proof of non revocation for ${type} referent '${referent}', creating revocation state for credential`, - { - requestRevocationInterval, - credentialRevocationId, - revocationRegistryId, - } - ) - - assertBestPracticeRevocationInterval(requestRevocationInterval) - - const { definition, revocationStatusLists, tailsFilePath } = revocationRegistries[revocationRegistryId] - - // Extract revocation status list for the given timestamp - const revocationStatusList = revocationStatusLists[timestamp] - if (!revocationStatusList) { - throw new AriesFrameworkError( - `Revocation status list for revocation registry ${revocationRegistryId} and timestamp ${timestamp} not found in revocation status lists. All revocation status lists must be present.` - ) - } - - const tails = await createTailsReader(agentContext, tailsFilePath) - - const revocationState = await this.indySdk.createRevocationState( - tails, - indySdkRevocationRegistryDefinitionFromAnonCreds(revocationRegistryId, definition), - indySdkRevocationDeltaFromAnonCreds(revocationStatusList), - revocationStatusList.timestamp, - credentialRevocationId - ) - - if (!indyRevocationStates[revocationRegistryId]) { - indyRevocationStates[revocationRegistryId] = {} - } - indyRevocationStates[revocationRegistryId][timestamp] = revocationState - } - } - - agentContext.config.logger.debug(`Created Revocation States for Proof Request`, { - indyRevocationStates, - }) - - return indyRevocationStates - } catch (error) { - agentContext.config.logger.error(`Error creating Indy Revocation State for Proof Request`, { - error, - proofRequest, - selectedCredentials, - }) - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } -} diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts deleted file mode 100644 index d30fab4393..0000000000 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { AnonCredsProof, AnonCredsVerifierService, VerifyProofOptions } from '@credo-ts/anoncreds' -import type { AgentContext } from '@credo-ts/core' -import type { CredentialDefs, Schemas, RevocRegDefs, RevRegs, IndyProofRequest, IndyProof } from 'indy-sdk' - -import { parseIndyCredentialDefinitionId } from '@credo-ts/anoncreds' -import { inject, injectable } from '@credo-ts/core' - -import { IndySdkError, isIndyError } from '../../error' -import { IndySdk, IndySdkSymbol } from '../../types' -import { assertAllUnqualified } from '../utils/assertUnqualified' -import { - indySdkCredentialDefinitionFromAnonCreds, - indySdkRevocationRegistryDefinitionFromAnonCreds, - indySdkRevocationRegistryFromAnonCreds, - indySdkSchemaFromAnonCreds, -} from '../utils/transform' - -@injectable() -export class IndySdkVerifierService implements AnonCredsVerifierService { - private indySdk: IndySdk - - public constructor(@inject(IndySdkSymbol) indySdk: IndySdk) { - this.indySdk = indySdk - } - - public async verifyProof(agentContext: AgentContext, options: VerifyProofOptions): Promise { - assertAllUnqualified({ - credentialDefinitionIds: Object.keys(options.credentialDefinitions), - schemaIds: Object.keys(options.schemas), - revocationRegistryIds: Object.keys(options.revocationRegistries), - }) - - try { - // The AnonCredsSchema doesn't contain the seqNo anymore. However, the indy credential definition id - // does contain the seqNo, so we can extract it from the credential definition id. - const seqNoMap: { [schemaId: string]: number } = {} - - // Convert AnonCreds credential definitions to Indy credential definitions - const indyCredentialDefinitions: CredentialDefs = {} - for (const credentialDefinitionId in options.credentialDefinitions) { - const credentialDefinition = options.credentialDefinitions[credentialDefinitionId] - - indyCredentialDefinitions[credentialDefinitionId] = indySdkCredentialDefinitionFromAnonCreds( - credentialDefinitionId, - credentialDefinition - ) - - // Get the seqNo for the schemas so we can use it when transforming the schemas - const { schemaSeqNo } = parseIndyCredentialDefinitionId(credentialDefinitionId) - seqNoMap[credentialDefinition.schemaId] = Number(schemaSeqNo) - } - - // Convert AnonCreds schemas to Indy schemas - const indySchemas: Schemas = {} - for (const schemaId in options.schemas) { - const schema = options.schemas[schemaId] - indySchemas[schemaId] = indySdkSchemaFromAnonCreds(schemaId, schema, seqNoMap[schemaId]) - } - - // Convert AnonCreds revocation definitions to Indy revocation definitions - const indyRevocationDefinitions: RevocRegDefs = {} - const indyRevocationRegistries: RevRegs = {} - - for (const revocationRegistryDefinitionId in options.revocationRegistries) { - const { definition, revocationStatusLists } = options.revocationRegistries[revocationRegistryDefinitionId] - indyRevocationDefinitions[revocationRegistryDefinitionId] = indySdkRevocationRegistryDefinitionFromAnonCreds( - revocationRegistryDefinitionId, - definition - ) - - // Initialize empty object for this revocation registry - indyRevocationRegistries[revocationRegistryDefinitionId] = {} - - // Also transform the revocation lists for the specified timestamps into the revocation registry - // format Indy expects - for (const timestamp in revocationStatusLists) { - const revocationStatusList = revocationStatusLists[timestamp] - indyRevocationRegistries[revocationRegistryDefinitionId][timestamp] = - indySdkRevocationRegistryFromAnonCreds(revocationStatusList) - } - } - - return await this.indySdk.verifierVerifyProof( - options.proofRequest as IndyProofRequest, - // FIXME IndyProof if badly typed in indy-sdk. It contains a `requested_predicates` property, which should be `predicates`. - options.proof as unknown as IndyProof, - indySchemas, - indyCredentialDefinitions, - indyRevocationDefinitions, - indyRevocationRegistries - ) - } catch (error) { - throw isIndyError(error) ? new IndySdkError(error) : error - } - } -} diff --git a/packages/indy-sdk/src/anoncreds/utils/__tests__/assertUnqualified.test.ts b/packages/indy-sdk/src/anoncreds/utils/__tests__/assertUnqualified.test.ts deleted file mode 100644 index ba15e97c24..0000000000 --- a/packages/indy-sdk/src/anoncreds/utils/__tests__/assertUnqualified.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import type { AnonCredsCredentialOffer, AnonCredsCredentialRequest } from '@credo-ts/anoncreds' - -import { - assertUnqualifiedCredentialDefinitionId, - assertUnqualifiedCredentialOffer, - assertUnqualifiedCredentialRequest, - assertUnqualifiedIssuerId, - assertUnqualifiedProofRequest, - assertUnqualifiedRevocationRegistryId, - assertUnqualifiedSchemaId, -} from '../assertUnqualified' - -describe('assertUnqualified', () => { - describe('assertUnqualifiedCredentialDefinitionId', () => { - test('throws when a non-unqualified credential definition id is passed', () => { - expect(() => - assertUnqualifiedCredentialDefinitionId( - 'did:indy:local:N7baRMcyvPwWc8v85CtZ6e/anoncreds/v0/CLAIM_DEF/100669/SCH Employee ID' - ) - ).toThrow() - }) - - test('does not throw when an unqualified credential definition id is passed', () => { - expect(() => - assertUnqualifiedCredentialDefinitionId('N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID') - ).not.toThrow() - }) - }) - - describe('assertUnqualifiedSchemaId', () => { - test('throws when a non-unqualified schema id is passed', () => { - expect(() => - assertUnqualifiedSchemaId('did:indy:local:BQ42WeE24jFHeyGg8x9XAz/anoncreds/v0/SCHEMA/Medical Bill/1.0') - ).toThrowError('Schema id') - }) - - test('does not throw when an unqualified schema id is passed', () => { - expect(() => assertUnqualifiedSchemaId('BQ42WeE24jFHeyGg8x9XAz:2:Medical Bill:1.0')).not.toThrow() - }) - }) - - describe('assertUnqualifiedRevocationRegistryId', () => { - test('throws when a non-unqualified revocation registry id is passed', () => { - expect(() => - assertUnqualifiedRevocationRegistryId( - 'did:indy:local:N7baRMcyvPwWc8v85CtZ6e/anoncreds/v0/REV_REG_DEF/100669/SCH Employee ID/1-1024' - ) - ).toThrowError('Revocation registry id') - }) - - test('does not throw when an unqualified revocation registry id is passed', () => { - expect(() => - assertUnqualifiedRevocationRegistryId( - 'N7baRMcyvPwWc8v85CtZ6e:4:N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID:CL_ACCUM:1-1024' - ) - ).not.toThrow() - }) - }) - - describe('assertUnqualifiedIssuerId', () => { - test('throws when a non-unqualified issuer id is passed', () => { - expect(() => assertUnqualifiedIssuerId('did:indy:sovrin:N7baRMcyvPwWc8v85CtZ6e')).toThrowError('Issuer id') - }) - - test('does not throw when an unqualified issuer id is passed', () => { - expect(() => assertUnqualifiedIssuerId('N7baRMcyvPwWc8v85CtZ6e')).not.toThrow() - }) - }) - - describe('assertUnqualifiedCredentialOffer', () => { - test('throws when non-unqualified identifiers are passed', () => { - expect(() => - assertUnqualifiedCredentialOffer({ - cred_def_id: 'did:indy:local:N7baRMcyvPwWc8v85CtZ6e/anoncreds/v0/CLAIM_DEF/100669/SCH Employee ID', - schema_id: 'BQ42WeE24jFHeyGg8x9XAz:2:Medical Bill:1.0', - } as AnonCredsCredentialOffer) - ).toThrowError('Credential definition id') - - expect(() => - assertUnqualifiedCredentialOffer({ - cred_def_id: 'N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID', - schema_id: 'did:indy:local:BQ42WeE24jFHeyGg8x9XAz/anoncreds/v0/SCHEMA/Medical Bill/1.0', - } as AnonCredsCredentialOffer) - ).toThrowError('Schema id') - }) - - test('does not throw when only unqualified identifiers are passed', () => { - expect(() => - assertUnqualifiedCredentialOffer({ - cred_def_id: 'N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID', - schema_id: 'BQ42WeE24jFHeyGg8x9XAz:2:Medical Bill:1.0', - } as AnonCredsCredentialOffer) - ).not.toThrow() - }) - }) - - describe('assertUnqualifiedCredentialRequest', () => { - test('throws when non-unqualified identifiers are passed', () => { - expect(() => - assertUnqualifiedCredentialRequest({ - cred_def_id: 'did:indy:local:N7baRMcyvPwWc8v85CtZ6e/anoncreds/v0/CLAIM_DEF/100669/SCH Employee ID', - } as AnonCredsCredentialRequest) - ).toThrowError('Credential definition id') - }) - - test('does not throw when only unqualified identifiers are passed', () => { - expect(() => - assertUnqualifiedCredentialRequest({ - cred_def_id: 'N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID', - } as AnonCredsCredentialRequest) - ).not.toThrow() - }) - }) - - describe('assertUnqualifiedProofRequest', () => { - test('throws when non-unqualified identifiers are passed', () => { - expect(() => - assertUnqualifiedProofRequest({ - requested_attributes: { - a: { - restrictions: [ - { - cred_def_id: 'did:indy:local:N7baRMcyvPwWc8v85CtZ6e/anoncreds/v0/CLAIM_DEF/100669/SCH Employee ID', - }, - ], - }, - }, - requested_predicates: {}, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any) - ).toThrowError('Credential definition id') - }) - - test('does not throw when only unqualified identifiers are passed', () => { - expect(() => - assertUnqualifiedProofRequest({ - requested_attributes: { - a: { - restrictions: [ - { - schema_id: 'BQ42WeE24jFHeyGg8x9XAz:2:Medical Bill:1.0', - }, - ], - }, - }, - requested_predicates: {}, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any) - ).not.toThrow() - }) - }) -}) diff --git a/packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts b/packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts deleted file mode 100644 index 546579330b..0000000000 --- a/packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - getDidIndyCredentialDefinitionId, - getDidIndyRevocationRegistryDefinitionId, - getDidIndySchemaId, - indySdkAnonCredsRegistryIdentifierRegex, -} from '../identifiers' - -describe('identifiers', () => { - describe('indySdkAnonCredsRegistryIdentifierRegex', () => { - test('matches against a legacy schema id, credential definition id and revocation registry id', () => { - const did = '7Tqg6BwSSWapxgUDm9KKgg' - const schemaId = 'BQ42WeE24jFHeyGg8x9XAz:2:Medical Bill:1.0' - const credentialDefinitionId = 'N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID' - const revocationRegistryId = - 'N7baRMcyvPwWc8v85CtZ6e:4:N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID:CL_ACCUM:1-1024' - - const anotherId = 'some:id' - - // unqualified issuerId not in regex on purpose. See note in implementation. - expect(indySdkAnonCredsRegistryIdentifierRegex.test(did)).toEqual(false) - - expect(indySdkAnonCredsRegistryIdentifierRegex.test(schemaId)).toEqual(true) - expect(indySdkAnonCredsRegistryIdentifierRegex.test(credentialDefinitionId)).toEqual(true) - expect(indySdkAnonCredsRegistryIdentifierRegex.test(revocationRegistryId)).toEqual(true) - expect(indySdkAnonCredsRegistryIdentifierRegex.test(anotherId)).toEqual(false) - }) - - test('matches against a did indy did, schema id, credential definition id and revocation registry id', () => { - const did = 'did:indy:local:7Tqg6BwSSWapxgUDm9KKgg' - const schemaId = 'did:indy:local:BQ42WeE24jFHeyGg8x9XAz/anoncreds/v0/SCHEMA/Medical Bill/1.0' - const credentialDefinitionId = - 'did:indy:local:N7baRMcyvPwWc8v85CtZ6e/anoncreds/v0/CLAIM_DEF/100669/SCH Employee ID' - const revocationRegistryId = - 'did:indy:local:N7baRMcyvPwWc8v85CtZ6e/anoncreds/v0/REV_REG_DEF/100669/SCH Employee ID/1-1024' - - const anotherId = 'did:indy:local:N7baRMcyvPwWc8v85CtZ6e/anoncreds/v0/SOME_DEF' - - expect(indySdkAnonCredsRegistryIdentifierRegex.test(did)).toEqual(true) - expect(indySdkAnonCredsRegistryIdentifierRegex.test(schemaId)).toEqual(true) - expect(indySdkAnonCredsRegistryIdentifierRegex.test(credentialDefinitionId)).toEqual(true) - expect(indySdkAnonCredsRegistryIdentifierRegex.test(revocationRegistryId)).toEqual(true) - expect(indySdkAnonCredsRegistryIdentifierRegex.test(anotherId)).toEqual(false) - }) - }) - - test('getDidIndySchemaId returns a valid schema id given a did, name, and version', () => { - const namespace = 'sovrin:test' - const did = '12345' - const name = 'backbench' - const version = '420' - - expect(getDidIndySchemaId(namespace, did, name, version)).toEqual( - 'did:indy:sovrin:test:12345/anoncreds/v0/SCHEMA/backbench/420' - ) - }) - - test('getDidIndyCredentialDefinitionId returns a valid credential definition id given a did, seqNo, and tag', () => { - const namespace = 'sovrin:test' - const did = '12345' - const seqNo = 420 - const tag = 'someTag' - - expect(getDidIndyCredentialDefinitionId(namespace, did, seqNo, tag)).toEqual( - 'did:indy:sovrin:test:12345/anoncreds/v0/CLAIM_DEF/420/someTag' - ) - }) - - test('getDidIndyRevocationRegistryId returns a valid credential definition id given a did, seqNo, and tag', () => { - const namespace = 'sovrin:test' - const did = '12345' - const seqNo = 420 - const credentialDefinitionTag = 'someTag' - const tag = 'anotherTag' - - expect(getDidIndyRevocationRegistryDefinitionId(namespace, did, seqNo, credentialDefinitionTag, tag)).toEqual( - 'did:indy:sovrin:test:12345/anoncreds/v0/REV_REG_DEF/420/someTag/anotherTag' - ) - }) -}) diff --git a/packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts b/packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts deleted file mode 100644 index f94060d8fd..0000000000 --- a/packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { AnonCredsCredentialDefinition, AnonCredsSchema } from '../../../../../anoncreds/src' -import type { CredDef, Schema } from 'indy-sdk' - -import { - anonCredsCredentialDefinitionFromIndySdk, - anonCredsSchemaFromIndySdk, - indySdkCredentialDefinitionFromAnonCreds, - indySdkSchemaFromAnonCreds, -} from '../transform' - -describe('transform', () => { - it('anonCredsSchemaFromIndySdk should return a valid anoncreds schema', () => { - const schema: Schema = { - attrNames: ['hello'], - id: 'TL1EaPFCZ8Si5aUrqScBDt:2:Example Schema:1.0.0', - name: 'Example Schema', - seqNo: 150, - ver: '1.0', - version: '1.0.0', - } - - expect(anonCredsSchemaFromIndySdk(schema)).toEqual({ - attrNames: ['hello'], - issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', - name: 'Example Schema', - version: '1.0.0', - }) - }) - - it('indySdkSchemaFromAnonCreds should return a valid indy sdk schema', () => { - const schemaId = 'TL1EaPFCZ8Si5aUrqScBDt:2:Example Schema:1.0.0' - const schema: AnonCredsSchema = { - attrNames: ['hello'], - issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', - name: 'Example Schema', - version: '1.0.0', - } - - expect(indySdkSchemaFromAnonCreds(schemaId, schema, 150)).toEqual({ - attrNames: ['hello'], - id: 'TL1EaPFCZ8Si5aUrqScBDt:2:Example Schema:1.0.0', - name: 'Example Schema', - seqNo: 150, - ver: '1.0', - version: '1.0.0', - }) - }) - - it('anonCredsCredentialDefinitionFromIndySdk should return a valid anoncreds credential definition', () => { - const credDef: CredDef = { - id: 'TL1EaPFCZ8Si5aUrqScBDt:3:CL:420:someTag', - schemaId: '8910:2:Example Schema:1.0.0', - tag: 'someTag', - type: 'CL', - value: { - primary: { - something: 'string', - }, - }, - ver: '1.0', - } - - expect(anonCredsCredentialDefinitionFromIndySdk(credDef)).toEqual({ - issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', - schemaId: '8910:2:Example Schema:1.0.0', - tag: 'someTag', - type: 'CL', - value: { - primary: { - something: 'string', - }, - }, - }) - }) - - it('indySdkCredentialDefinitionFromAnonCreds should return a valid indy sdk credential definition', () => { - const credentialDefinitionId = 'TL1EaPFCZ8Si5aUrqScBDt:3:CL:420:someTag' - const credentialDefinition: AnonCredsCredentialDefinition = { - issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', - schemaId: '8910:2:Example Schema:1.0.0', - tag: 'someTag', - type: 'CL', - value: { - primary: { - something: 'string', - }, - }, - } - - expect(indySdkCredentialDefinitionFromAnonCreds(credentialDefinitionId, credentialDefinition)).toEqual({ - id: 'TL1EaPFCZ8Si5aUrqScBDt:3:CL:420:someTag', - schemaId: '8910:2:Example Schema:1.0.0', - tag: 'someTag', - type: 'CL', - value: { - primary: { - something: 'string', - }, - }, - ver: '1.0', - }) - }) - - // TODO: add tests for these models once finalized in the anoncreds spec - test.todo( - 'anonCredsRevocationRegistryDefinitionFromIndySdk should return a valid anoncreds revocation registry definition' - ) - test.todo( - 'indySdkRevocationRegistryDefinitionFromAnonCreds should return a valid indy sdk revocation registry definition' - ) - test.todo('anonCredsRevocationStatusListFromIndySdk should return a valid anoncreds revocation list') - test.todo('indySdkRevocationRegistryFromAnonCreds should return a valid indy sdk revocation registry') - test.todo('indySdkRevocationDeltaFromAnonCreds should return a valid indy sdk revocation delta') -}) diff --git a/packages/indy-sdk/src/anoncreds/utils/assertUnqualified.ts b/packages/indy-sdk/src/anoncreds/utils/assertUnqualified.ts deleted file mode 100644 index 42463ca455..0000000000 --- a/packages/indy-sdk/src/anoncreds/utils/assertUnqualified.ts +++ /dev/null @@ -1,129 +0,0 @@ -import type { AnonCredsCredentialOffer, AnonCredsCredentialRequest, AnonCredsProofRequest } from '@credo-ts/anoncreds' - -import { - unqualifiedRevocationRegistryIdRegex, - unqualifiedCredentialDefinitionIdRegex, - unqualifiedIndyDidRegex, - unqualifiedSchemaIdRegex, -} from '@credo-ts/anoncreds' -import { AriesFrameworkError } from '@credo-ts/core' - -/** - * Assert that a credential definition id is unqualified. - */ -export function assertUnqualifiedCredentialDefinitionId(credentialDefinitionId: string) { - if (!unqualifiedCredentialDefinitionIdRegex.test(credentialDefinitionId)) { - throw new AriesFrameworkError( - `Credential definition id '${credentialDefinitionId}' is not an unqualified credential definition id. Indy SDK only supports unqualified identifiers.` - ) - } -} - -/** - * Assert that a schema id is unqualified. - */ -export function assertUnqualifiedSchemaId(schemaId: string) { - if (!unqualifiedSchemaIdRegex.test(schemaId)) { - throw new AriesFrameworkError( - `Schema id '${schemaId}' is not an unqualified schema id. Indy SDK only supports unqualified identifiers.` - ) - } -} - -/** - * Assert that a revocation registry id is unqualified. - */ -export function assertUnqualifiedRevocationRegistryId(revocationRegistryId: string) { - if (!unqualifiedRevocationRegistryIdRegex.test(revocationRegistryId)) { - throw new AriesFrameworkError( - `Revocation registry id '${revocationRegistryId}' is not an unqualified revocation registry id. Indy SDK only supports unqualified identifiers.` - ) - } -} - -/** - * Assert that an issuer id is unqualified. - */ -export function assertUnqualifiedIssuerId(issuerId: string) { - if (!unqualifiedIndyDidRegex.test(issuerId)) { - throw new AriesFrameworkError( - `Issuer id '${issuerId}' is not an unqualified issuer id. Indy SDK only supports unqualified identifiers.` - ) - } -} - -/** - * Assert that a credential offer only contains unqualified identifiers. - */ -export function assertUnqualifiedCredentialOffer(credentialOffer: AnonCredsCredentialOffer) { - assertUnqualifiedCredentialDefinitionId(credentialOffer.cred_def_id) - assertUnqualifiedSchemaId(credentialOffer.schema_id) -} - -/** - * Assert that a credential request only contains unqualified identifiers. - */ -export function assertUnqualifiedCredentialRequest(credentialRequest: AnonCredsCredentialRequest) { - assertUnqualifiedCredentialDefinitionId(credentialRequest.cred_def_id) -} - -/** - * Assert that a proof request only contains unqualified identifiers. - */ -export function assertUnqualifiedProofRequest(proofRequest: AnonCredsProofRequest) { - const allRequested = [ - ...Object.values(proofRequest.requested_attributes), - ...Object.values(proofRequest.requested_predicates), - ] - - for (const requested of allRequested) { - for (const restriction of requested.restrictions ?? []) { - assertAllUnqualified({ - credentialDefinitionIds: [restriction.cred_def_id], - schemaIds: [restriction.schema_id], - revocationRegistryIds: [restriction.rev_reg_id], - issuerIds: [restriction.issuer_did, restriction.schema_issuer_did], - }) - } - } -} - -export function assertAllUnqualified({ - schemaIds = [], - credentialDefinitionIds = [], - revocationRegistryIds = [], - issuerIds = [], -}: { - schemaIds?: Array - credentialDefinitionIds?: Array - revocationRegistryIds?: Array - issuerIds?: Array -}) { - for (const schemaId of schemaIds) { - // We don't validate undefined values - if (!schemaId) continue - - assertUnqualifiedSchemaId(schemaId) - } - - for (const credentialDefinitionId of credentialDefinitionIds) { - // We don't validate undefined values - if (!credentialDefinitionId) continue - - assertUnqualifiedCredentialDefinitionId(credentialDefinitionId) - } - - for (const revocationRegistryId of revocationRegistryIds) { - // We don't validate undefined values - if (!revocationRegistryId) continue - - assertUnqualifiedRevocationRegistryId(revocationRegistryId) - } - - for (const issuerId of issuerIds) { - // We don't validate undefined values - if (!issuerId) continue - - assertUnqualifiedIssuerId(issuerId) - } -} diff --git a/packages/indy-sdk/src/anoncreds/utils/identifiers.ts b/packages/indy-sdk/src/anoncreds/utils/identifiers.ts deleted file mode 100644 index 0946ac67f0..0000000000 --- a/packages/indy-sdk/src/anoncreds/utils/identifiers.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * NOTE: this file is available in both the indy-sdk and indy-vdr packages. If making changes to - * this file, make sure to update both files if applicable. - */ - -import { - unqualifiedSchemaIdRegex, - unqualifiedCredentialDefinitionIdRegex, - unqualifiedRevocationRegistryIdRegex, - didIndyCredentialDefinitionIdRegex, - didIndyRevocationRegistryIdRegex, - didIndySchemaIdRegex, - didIndyRegex, -} from '@credo-ts/anoncreds' - -// combines both legacy and did:indy anoncreds identifiers and also the issuer id -const indySdkAnonCredsRegexes = [ - // NOTE: we only include the qualified issuer id here, as we don't support registering objects based on legacy issuer ids. - // you can still resolve using legacy issuer ids, but you need to use the full did:indy identifier when registering. - // As we find a matching anoncreds registry based on the issuerId only when creating an object, this will make sure - // it will throw an no registry found for identifier error. - // issuer id - didIndyRegex, - - // schema - didIndySchemaIdRegex, - unqualifiedSchemaIdRegex, - - // credential definition - didIndyCredentialDefinitionIdRegex, - unqualifiedCredentialDefinitionIdRegex, - - // revocation registry - unqualifiedRevocationRegistryIdRegex, - didIndyRevocationRegistryIdRegex, -] - -export const indySdkAnonCredsRegistryIdentifierRegex = new RegExp( - indySdkAnonCredsRegexes.map((r) => r.source).join('|') -) - -export function getDidIndySchemaId(namespace: string, unqualifiedDid: string, name: string, version: string) { - return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/SCHEMA/${name}/${version}` -} - -export function getDidIndyCredentialDefinitionId( - namespace: string, - unqualifiedDid: string, - schemaSeqNo: string | number, - tag: string -) { - return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/${tag}` -} - -export function getDidIndyRevocationRegistryDefinitionId( - namespace: string, - unqualifiedDid: string, - schemaSeqNo: string | number, - credentialDefinitionTag: string, - revocationRegistryTag: string -) { - return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/REV_REG_DEF/${schemaSeqNo}/${credentialDefinitionTag}/${revocationRegistryTag}` -} diff --git a/packages/indy-sdk/src/anoncreds/utils/tails.ts b/packages/indy-sdk/src/anoncreds/utils/tails.ts deleted file mode 100644 index db1e4f8505..0000000000 --- a/packages/indy-sdk/src/anoncreds/utils/tails.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { IndySdk } from '../../types' -import type { AgentContext, FileSystem } from '@credo-ts/core' - -import { AriesFrameworkError, getDirFromFilePath, InjectionSymbols } from '@credo-ts/core' - -import { IndySdkError, isIndyError } from '../../error' -import { IndySdkSymbol } from '../../types' - -/** - * Get a handler for the blob storage tails file reader. - * - * @param agentContext The agent context - * @param tailsFilePath The path of the tails file - * @returns The blob storage reader handle - */ -export async function createTailsReader(agentContext: AgentContext, tailsFilePath: string) { - const fileSystem = agentContext.dependencyManager.resolve(InjectionSymbols.FileSystem) - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - - try { - agentContext.config.logger.debug(`Opening tails reader at path ${tailsFilePath}`) - const tailsFileExists = await fileSystem.exists(tailsFilePath) - - // Extract directory from path (should also work with windows paths) - const dirname = getDirFromFilePath(tailsFilePath) - - if (!tailsFileExists) { - throw new AriesFrameworkError(`Tails file does not exist at path ${tailsFilePath}`) - } - - const tailsReaderConfig = { - base_dir: dirname, - } - - const tailsReader = await indySdk.openBlobStorageReader('default', tailsReaderConfig) - agentContext.config.logger.debug(`Opened tails reader at path ${tailsFilePath}`) - return tailsReader - } catch (error) { - if (isIndyError(error)) { - throw new IndySdkError(error) - } - - throw error - } -} diff --git a/packages/indy-sdk/src/anoncreds/utils/transform.ts b/packages/indy-sdk/src/anoncreds/utils/transform.ts deleted file mode 100644 index 531730d14f..0000000000 --- a/packages/indy-sdk/src/anoncreds/utils/transform.ts +++ /dev/null @@ -1,161 +0,0 @@ -import type { - AnonCredsCredentialDefinition, - AnonCredsRevocationStatusList, - AnonCredsRevocationRegistryDefinition, - AnonCredsSchema, - AnonCredsCredentialRequestMetadata, - AnonCredsLinkSecretBlindingData, -} from '@credo-ts/anoncreds' -import type { CredDef, CredReqMetadata, RevocReg, RevocRegDef, RevocRegDelta, Schema } from 'indy-sdk' - -import { parseIndyCredentialDefinitionId, parseIndySchemaId } from '@credo-ts/anoncreds' - -export function anonCredsSchemaFromIndySdk(schema: Schema): AnonCredsSchema { - const { did } = parseIndySchemaId(schema.id) - return { - issuerId: did, - name: schema.name, - version: schema.version, - attrNames: schema.attrNames, - } -} - -export function indySdkSchemaFromAnonCreds(schemaId: string, schema: AnonCredsSchema, indyLedgerSeqNo: number): Schema { - return { - id: schemaId, - attrNames: schema.attrNames, - name: schema.name, - version: schema.version, - ver: '1.0', - seqNo: indyLedgerSeqNo, - } -} - -export function anonCredsCredentialDefinitionFromIndySdk(credentialDefinition: CredDef): AnonCredsCredentialDefinition { - const { did } = parseIndyCredentialDefinitionId(credentialDefinition.id) - - return { - issuerId: did, - schemaId: credentialDefinition.schemaId, - tag: credentialDefinition.tag, - type: 'CL', - value: credentialDefinition.value, - } -} - -export function indySdkCredentialDefinitionFromAnonCreds( - credentialDefinitionId: string, - credentialDefinition: AnonCredsCredentialDefinition -): CredDef { - return { - id: credentialDefinitionId, - schemaId: credentialDefinition.schemaId, - tag: credentialDefinition.tag, - type: credentialDefinition.type, - value: credentialDefinition.value, - ver: '1.0', - } -} - -export function indySdkRevocationRegistryDefinitionFromAnonCreds( - revocationRegistryDefinitionId: string, - revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition -): RevocRegDef { - return { - id: revocationRegistryDefinitionId, - credDefId: revocationRegistryDefinition.credDefId, - revocDefType: revocationRegistryDefinition.revocDefType, - tag: revocationRegistryDefinition.tag, - value: { - issuanceType: 'ISSUANCE_BY_DEFAULT', // NOTE: we always use ISSUANCE_BY_DEFAULT when passing to the indy-sdk. It doesn't matter, as we have the revocation List with the full state - maxCredNum: revocationRegistryDefinition.value.maxCredNum, - publicKeys: revocationRegistryDefinition.value.publicKeys, - tailsHash: revocationRegistryDefinition.value.tailsHash, - tailsLocation: revocationRegistryDefinition.value.tailsLocation, - }, - ver: '1.0', - } -} - -export function anonCredsRevocationStatusListFromIndySdk( - revocationRegistryDefinitionId: string, - revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition, - delta: RevocRegDelta, - timestamp: number, - isIssuanceByDefault: boolean -): AnonCredsRevocationStatusList { - // 0 means unrevoked, 1 means revoked - const defaultState = isIssuanceByDefault ? 0 : 1 - - // Fill with default value - const revocationList = new Array(revocationRegistryDefinition.value.maxCredNum).fill(defaultState) - - // Set all `issuer` indexes to 0 (not revoked) - for (const issued of delta.value.issued ?? []) { - revocationList[issued] = 0 - } - - // Set all `revoked` indexes to 1 (revoked) - for (const revoked of delta.value.revoked ?? []) { - revocationList[revoked] = 1 - } - - return { - issuerId: revocationRegistryDefinition.issuerId, - currentAccumulator: delta.value.accum, - revRegDefId: revocationRegistryDefinitionId, - revocationList, - timestamp, - } -} - -export function indySdkRevocationRegistryFromAnonCreds(revocationStatusList: AnonCredsRevocationStatusList): RevocReg { - return { - ver: '1.0', - value: { - accum: revocationStatusList.currentAccumulator, - }, - } -} - -export function indySdkRevocationDeltaFromAnonCreds( - revocationStatusList: AnonCredsRevocationStatusList -): RevocRegDelta { - // Get all indices from the revocationStatusList that are revoked (so have value '1') - const revokedIndices = revocationStatusList.revocationList.reduce( - (revoked, current, index) => (current === 1 ? [...revoked, index] : revoked), - [] - ) - - return { - value: { - accum: revocationStatusList.currentAccumulator, - issued: [], - revoked: revokedIndices, - // NOTE: this must be a valid accumulator but it's not actually used. So we set it to the - // currentAccumulator as that should always be a valid accumulator. - prevAccum: revocationStatusList.currentAccumulator, - }, - ver: '1.0', - } -} - -export function anonCredsCredentialRequestMetadataFromIndySdk( - credentialRequestMetadata: CredReqMetadata -): AnonCredsCredentialRequestMetadata { - return { - link_secret_blinding_data: credentialRequestMetadata.master_secret_blinding_data as AnonCredsLinkSecretBlindingData, - link_secret_name: credentialRequestMetadata.master_secret_name as string, - nonce: credentialRequestMetadata.nonce as string, - } -} - -export function indySdkCredentialRequestMetadataFromAnonCreds( - credentialRequestMetadata: AnonCredsCredentialRequestMetadata -): CredReqMetadata { - return { - master_secret_blinding_data: credentialRequestMetadata.link_secret_blinding_data, - master_secret_name: credentialRequestMetadata.link_secret_name, - nonce: credentialRequestMetadata.nonce, - } -} diff --git a/packages/indy-sdk/src/dids/IndySdkIndyDidRegistrar.ts b/packages/indy-sdk/src/dids/IndySdkIndyDidRegistrar.ts deleted file mode 100644 index dc4d66552a..0000000000 --- a/packages/indy-sdk/src/dids/IndySdkIndyDidRegistrar.ts +++ /dev/null @@ -1,328 +0,0 @@ -import type { IndyEndpointAttrib } from './didSovUtil' -import type { IndySdkPool } from '../ledger' -import type { IndySdk } from '../types' -import type { - AgentContext, - Buffer, - DidCreateOptions, - DidCreateResult, - DidDeactivateResult, - DidRegistrar, - DidUpdateResult, -} from '@credo-ts/core' -import type { NymRole } from 'indy-sdk' - -import { parseIndyDid } from '@credo-ts/anoncreds' -import { DidDocumentRole, DidRecord, DidRepository, KeyType, Key } from '@credo-ts/core' - -import { IndySdkError } from '../error' -import { isIndyError } from '../error/indyError' -import { IndySdkPoolService } from '../ledger' -import { IndySdkSymbol } from '../types' -import { assertIndySdkWallet } from '../utils/assertIndySdkWallet' -import { isLegacySelfCertifiedDid, legacyIndyDidFromPublicKeyBase58 } from '../utils/did' - -import { createKeyAgreementKey, indyDidDocumentFromDid, verificationKeyForIndyDid } from './didIndyUtil' -import { addServicesFromEndpointsAttrib } from './didSovUtil' - -export class IndySdkIndyDidRegistrar implements DidRegistrar { - public readonly supportedMethods = ['indy'] - - public async create(agentContext: AgentContext, options: IndySdkIndyDidCreateOptions): Promise { - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - const didRepository = agentContext.dependencyManager.resolve(DidRepository) - - const { alias, role, submitterDid, endpoints } = options.options - let did = options.did - let namespaceIdentifier: string - let verificationKey: Key - const privateKey = options.secret?.privateKey - - if (did && privateKey) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `Only one of 'privateKey' or 'did' must be provided`, - }, - } - } - - try { - assertIndySdkWallet(agentContext.wallet) - - // Parse submitterDid and extract namespace based on the submitter did - const { namespace: submitterNamespace, namespaceIdentifier: submitterNamespaceIdentifier } = - parseIndyDid(submitterDid) - const submitterSigningKey = await verificationKeyForIndyDid(agentContext, submitterDid) - - // Only supports version 1 did identifier (which is same as did:sov) - if (did) { - if (!options.options.verkey) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'If a did is defined, a matching verkey must be provided', - }, - } - } - - const { namespace, namespaceIdentifier: _namespaceIdentifier } = parseIndyDid(did) - namespaceIdentifier = _namespaceIdentifier - - verificationKey = Key.fromPublicKeyBase58(options.options.verkey, KeyType.Ed25519) - - if (!isLegacySelfCertifiedDid(namespaceIdentifier, options.options.verkey)) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `Did must be first 16 bytes of the the verkey base58 encoded.`, - }, - } - } - - if (submitterNamespace !== namespace) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `The submitter did uses namespace ${submitterNamespace} and the did to register uses namespace ${namespace}. Namespaces must match.`, - }, - } - } - } else { - // Create a new key and calculate did according to the rules for indy did method - verificationKey = await agentContext.wallet.createKey({ privateKey, keyType: KeyType.Ed25519 }) - namespaceIdentifier = legacyIndyDidFromPublicKeyBase58(verificationKey.publicKeyBase58) - did = `did:indy:${submitterNamespace}:${namespaceIdentifier}` - } - - const pool = indySdkPoolService.getPoolForNamespace(submitterNamespace) - await this.registerPublicDid( - agentContext, - pool, - submitterNamespaceIdentifier, - submitterSigningKey, - namespaceIdentifier, - verificationKey, - alias, - role - ) - - // Create did document - const didDocumentBuilder = indyDidDocumentFromDid(did, verificationKey.publicKeyBase58) - - // Add services if endpoints object was passed. - if (endpoints) { - const keyAgreementId = `${did}#key-agreement-1` - - await this.setEndpointsForDid(agentContext, pool, namespaceIdentifier, verificationKey, endpoints) - - didDocumentBuilder - .addContext('https://w3id.org/security/suites/x25519-2019/v1') - .addVerificationMethod({ - controller: did, - id: keyAgreementId, - publicKeyBase58: createKeyAgreementKey(verificationKey.publicKeyBase58), - type: 'X25519KeyAgreementKey2019', - }) - .addKeyAgreement(keyAgreementId) - - // Process endpoint attrib following the same rules as for did:sov - addServicesFromEndpointsAttrib(didDocumentBuilder, did, endpoints, keyAgreementId) - } - - // Build did document. - const didDocument = didDocumentBuilder.build() - - // Save the did so we know we created it and can issue with it - const didRecord = new DidRecord({ - did, - role: DidDocumentRole.Created, - tags: { - recipientKeyFingerprints: didDocument.recipientKeys.map((key: Key) => key.fingerprint), - }, - }) - await didRepository.save(agentContext, didRecord) - - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'finished', - did, - didDocument, - secret: { - // FIXME: the uni-registrar creates the seed in the registrar method - // if it doesn't exist so the seed can always be returned. Currently - // we can only return it if the seed was passed in by the user. Once - // we have a secure method for generating seeds we should use the same - // approach - privateKey: options.secret?.privateKey, - }, - }, - } - } catch (error) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `unknownError: ${error.message}`, - }, - } - } - } - - public async update(): Promise { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `notImplemented: updating did:indy not implemented yet`, - }, - } - } - - public async deactivate(): Promise { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `notImplemented: deactivating did:indy not implemented yet`, - }, - } - } - - private async registerPublicDid( - agentContext: AgentContext, - pool: IndySdkPool, - unqualifiedSubmitterDid: string, - submitterSigningKey: Key, - unqualifiedDid: string, - signingKey: Key, - alias: string, - role?: NymRole - ) { - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - - try { - agentContext.config.logger.debug(`Register public did '${unqualifiedDid}' on ledger '${pool.didIndyNamespace}'`) - - const request = await indySdk.buildNymRequest( - unqualifiedSubmitterDid, - unqualifiedDid, - signingKey.publicKeyBase58, - alias, - role || null - ) - - const response = await indySdkPoolService.submitWriteRequest(agentContext, pool, request, submitterSigningKey) - - agentContext.config.logger.debug( - `Registered public did '${unqualifiedDid}' on ledger '${pool.didIndyNamespace}'`, - { - response, - } - ) - } catch (error) { - agentContext.config.logger.error( - `Error registering public did '${unqualifiedDid}' on ledger '${pool.didIndyNamespace}'`, - { - error, - unqualifiedSubmitterDid, - unqualifiedDid, - verkey: signingKey.publicKeyBase58, - alias, - role, - pool: pool.didIndyNamespace, - } - ) - - throw error - } - } - - private async setEndpointsForDid( - agentContext: AgentContext, - pool: IndySdkPool, - unqualifiedDid: string, - signingKey: Key, - endpoints: IndyEndpointAttrib - ): Promise { - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - - try { - agentContext.config.logger.debug( - `Set endpoints for did '${unqualifiedDid}' on ledger '${pool.didIndyNamespace}'`, - endpoints - ) - - const request = await indySdk.buildAttribRequest( - unqualifiedDid, - unqualifiedDid, - null, - { endpoint: endpoints }, - null - ) - - const response = await indySdkPoolService.submitWriteRequest(agentContext, pool, request, signingKey) - agentContext.config.logger.debug( - `Successfully set endpoints for did '${unqualifiedDid}' on ledger '${pool.didIndyNamespace}'`, - { - response, - endpoints, - } - ) - } catch (error) { - agentContext.config.logger.error( - `Error setting endpoints for did '${unqualifiedDid}' on ledger '${pool.didIndyNamespace}'`, - { - error, - unqualifiedDid, - endpoints, - } - ) - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } -} - -interface IndySdkIndyDidCreateOptionsBase extends DidCreateOptions { - // The indy sdk can only publish a very limited did document (what is mostly known as a legacy did:sov did) and thus we require everything - // needed to construct the did document to be passed through the options object. - didDocument?: never - options: { - alias: string - role?: NymRole - verkey?: string - endpoints?: IndyEndpointAttrib - submitterDid: string - } - secret?: { - privateKey?: Buffer - } -} - -interface IndySdkIndyDidCreateOptionsWithDid extends IndySdkIndyDidCreateOptionsBase { - method?: never - did: string -} - -interface IndySdkIndyDidCreateOptionsWithoutDid extends IndySdkIndyDidCreateOptionsBase { - method: 'indy' - did?: never -} - -export type IndySdkIndyDidCreateOptions = IndySdkIndyDidCreateOptionsWithDid | IndySdkIndyDidCreateOptionsWithoutDid diff --git a/packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts b/packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts deleted file mode 100644 index 540ba3e0fb..0000000000 --- a/packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts +++ /dev/null @@ -1,126 +0,0 @@ -import type { IndyEndpointAttrib } from './didSovUtil' -import type { IndySdkPool } from '../ledger' -import type { IndySdk } from '../types' -import type { DidResolutionResult, DidResolver, AgentContext } from '@credo-ts/core' - -import { parseIndyDid } from '@credo-ts/anoncreds' - -import { isIndyError, IndySdkError } from '../error' -import { IndySdkPoolService } from '../ledger/IndySdkPoolService' -import { IndySdkSymbol } from '../types' -import { getFullVerkey } from '../utils/did' - -import { createKeyAgreementKey, indyDidDocumentFromDid } from './didIndyUtil' -import { addServicesFromEndpointsAttrib } from './didSovUtil' - -export class IndySdkIndyDidResolver implements DidResolver { - public readonly supportedMethods = ['indy'] - - public readonly allowsCaching = true - - public async resolve(agentContext: AgentContext, did: string): Promise { - const didDocumentMetadata = {} - - try { - const { namespaceIdentifier, namespace } = parseIndyDid(did) - - const poolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - const pool = poolService.getPoolForNamespace(namespace) - - const nym = await this.getPublicDid(agentContext, pool, namespaceIdentifier) - const endpoints = await this.getEndpointsForDid(agentContext, pool, namespaceIdentifier) - - // For modern did:indy DIDs, we assume that GET_NYM is always a full verkey in base58. - // For backwards compatibility, we accept a shortened verkey and convert it using previous convention - const verkey = getFullVerkey(did, nym.verkey) - const builder = indyDidDocumentFromDid(did, verkey) - - // NOTE: we don't support the `diddocContent` field in the GET_NYM response using the indy-sdk. So if the did would have the `diddocContent` field - // we will ignore it without knowing if it is present. We may be able to extract the diddocContent from the GET_NYM response in the future, but need - // some dids registered with diddocContent to test with. - if (endpoints) { - const keyAgreementId = `${did}#key-agreement-1` - - builder - .addContext('https://w3id.org/security/suites/x25519-2019/v1') - .addVerificationMethod({ - controller: did, - id: keyAgreementId, - publicKeyBase58: createKeyAgreementKey(verkey), - type: 'X25519KeyAgreementKey2019', - }) - .addKeyAgreement(keyAgreementId) - addServicesFromEndpointsAttrib(builder, did, endpoints, keyAgreementId) - } - - return { - didDocument: builder.build(), - didDocumentMetadata, - didResolutionMetadata: { contentType: 'application/did+ld+json' }, - } - } catch (error) { - return { - didDocument: null, - didDocumentMetadata, - didResolutionMetadata: { - error: 'notFound', - message: `resolver_error: Unable to resolve did '${did}': ${error}`, - }, - } - } - } - - private async getPublicDid(agentContext: AgentContext, pool: IndySdkPool, unqualifiedDid: string) { - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - - const request = await indySdk.buildGetNymRequest(null, unqualifiedDid) - const response = await indySdkPoolService.submitReadRequest(pool, request) - - return await indySdk.parseGetNymResponse(response) - } - - private async getEndpointsForDid(agentContext: AgentContext, pool: IndySdkPool, unqualifiedDid: string) { - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - - try { - agentContext.config.logger.debug( - `Get endpoints for did '${unqualifiedDid}' from ledger '${pool.didIndyNamespace}'` - ) - - const request = await indySdk.buildGetAttribRequest(null, unqualifiedDid, 'endpoint', null, null) - - agentContext.config.logger.debug( - `Submitting get endpoint ATTRIB request for did '${unqualifiedDid}' to ledger '${pool.didIndyNamespace}'` - ) - const response = await indySdkPoolService.submitReadRequest(pool, request) - - if (!response.result.data) { - return null - } - - const endpoints = JSON.parse(response.result.data as string)?.endpoint as IndyEndpointAttrib - agentContext.config.logger.debug( - `Got endpoints '${JSON.stringify(endpoints)}' for did '${unqualifiedDid}' from ledger '${ - pool.didIndyNamespace - }'`, - { - response, - endpoints, - } - ) - - return endpoints - } catch (error) { - agentContext.config.logger.error( - `Error retrieving endpoints for did '${unqualifiedDid}' from ledger '${pool.didIndyNamespace}'`, - { - error, - } - ) - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } -} diff --git a/packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts b/packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts deleted file mode 100644 index 46c7454ad3..0000000000 --- a/packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { IndyEndpointAttrib } from './didSovUtil' -import type { IndySdkPool } from '../ledger' -import type { IndySdk } from '../types' -import type { DidResolutionResult, ParsedDid, DidResolver, AgentContext } from '@credo-ts/core' - -import { isIndyError, IndySdkError } from '../error' -import { IndySdkPoolService } from '../ledger/IndySdkPoolService' -import { IndySdkSymbol } from '../types' - -import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovUtil' - -export class IndySdkSovDidResolver implements DidResolver { - public readonly supportedMethods = ['sov'] - - public readonly allowsCaching = true - - public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise { - const didDocumentMetadata = {} - - try { - const poolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - const { pool, nymResponse } = await poolService.getPoolForDid(agentContext, parsed.id) - const nym = nymResponse ?? (await this.getPublicDid(agentContext, pool, parsed.id)) - const endpoints = await this.getEndpointsForDid(agentContext, pool, parsed.id) - - const keyAgreementId = `${parsed.did}#key-agreement-1` - const builder = sovDidDocumentFromDid(parsed.did, nym.verkey) - - if (endpoints) { - addServicesFromEndpointsAttrib(builder, parsed.did, endpoints, keyAgreementId) - } - - return { - didDocument: builder.build(), - didDocumentMetadata, - didResolutionMetadata: { contentType: 'application/did+ld+json' }, - } - } catch (error) { - return { - didDocument: null, - didDocumentMetadata, - didResolutionMetadata: { - error: 'notFound', - message: `resolver_error: Unable to resolve did '${did}': ${error}`, - }, - } - } - } - - private async getPublicDid(agentContext: AgentContext, pool: IndySdkPool, did: string) { - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - - const request = await indySdk.buildGetNymRequest(null, did) - const response = await indySdkPoolService.submitReadRequest(pool, request) - - return await indySdk.parseGetNymResponse(response) - } - - private async getEndpointsForDid(agentContext: AgentContext, pool: IndySdkPool, unqualifiedDid: string) { - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - - try { - agentContext.config.logger.debug( - `Get endpoints for did '${unqualifiedDid}' from ledger '${pool.didIndyNamespace}'` - ) - - const request = await indySdk.buildGetAttribRequest(null, unqualifiedDid, 'endpoint', null, null) - - agentContext.config.logger.debug( - `Submitting get endpoint ATTRIB request for did '${unqualifiedDid}' to ledger '${pool.didIndyNamespace}'` - ) - const response = await indySdkPoolService.submitReadRequest(pool, request) - - if (!response.result.data) return null - - const endpoints = JSON.parse(response.result.data as string)?.endpoint as IndyEndpointAttrib - agentContext.config.logger.debug( - `Got endpoints '${JSON.stringify(endpoints)}' for did '${unqualifiedDid}' from ledger '${ - pool.didIndyNamespace - }'`, - { - response, - endpoints, - } - ) - - return endpoints ?? null - } catch (error) { - agentContext.config.logger.error( - `Error retrieving endpoints for did '${unqualifiedDid}' from ledger '${pool.didIndyNamespace}'`, - { - error, - } - ) - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } -} diff --git a/packages/indy-sdk/src/dids/__tests__/IndySdkIndyDidRegistrar.test.ts b/packages/indy-sdk/src/dids/__tests__/IndySdkIndyDidRegistrar.test.ts deleted file mode 100644 index 3852629be4..0000000000 --- a/packages/indy-sdk/src/dids/__tests__/IndySdkIndyDidRegistrar.test.ts +++ /dev/null @@ -1,501 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import type { IndySdkPool } from '../../ledger/IndySdkPool' -import type { DidRecord, RecordSavedEvent } from '@credo-ts/core' - -import { - SigningProviderRegistry, - DidsApi, - DidDocument, - VerificationMethod, - KeyType, - Key, - TypedArrayEncoder, - DidRepository, - JsonTransformer, - DidDocumentRole, - EventEmitter, - RepositoryEventTypes, -} from '@credo-ts/core' -import { Subject } from 'rxjs' - -import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' -import { mockFunction, getAgentConfig, getAgentContext, agentDependencies, indySdk } from '../../../../core/tests' -import { IndySdkPoolService } from '../../ledger/IndySdkPoolService' -import { IndySdkWallet } from '../../wallet' -import { IndySdkIndyDidRegistrar } from '../IndySdkIndyDidRegistrar' - -jest.mock('../../ledger/IndySdkPoolService') -const IndySdkPoolServiceMock = IndySdkPoolService as jest.Mock -const indySdkPoolServiceMock = new IndySdkPoolServiceMock() - -const pool = { - config: { indyNamespace: 'pool1' }, -} as IndySdkPool -mockFunction(indySdkPoolServiceMock.getPoolForNamespace).mockReturnValue(pool) - -const agentConfig = getAgentConfig('IndySdkIndyDidRegistrar') -const wallet = new IndySdkWallet(indySdk, agentConfig.logger, new SigningProviderRegistry([])) - -jest - .spyOn(wallet, 'createKey') - .mockResolvedValue(Key.fromPublicKeyBase58('E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', KeyType.Ed25519)) -const storageService = new InMemoryStorageService() -const eventEmitter = new EventEmitter(agentDependencies, new Subject()) -const didRepository = new DidRepository(storageService, eventEmitter) - -const agentContext = getAgentContext({ - wallet, - registerInstances: [ - [DidRepository, didRepository], - [IndySdkPoolService, indySdkPoolServiceMock], - [ - DidsApi, - { - resolve: jest.fn().mockResolvedValue({ - didDocument: new DidDocument({ - id: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - authentication: [ - new VerificationMethod({ - id: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg#verkey', - type: 'Ed25519VerificationKey2018', - controller: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', - }), - ], - }), - }), - }, - ], - ], - agentConfig, -}) - -const indySdkIndyDidRegistrar = new IndySdkIndyDidRegistrar() - -describe('IndySdkIndyDidRegistrar', () => { - afterEach(() => { - jest.clearAllMocks() - }) - - test('returns an error state if both did and privateKey are provided', async () => { - const result = await indySdkIndyDidRegistrar.create(agentContext, { - did: 'did:indy:pool1:did-value', - options: { - submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - alias: 'Hello', - }, - secret: { - privateKey: TypedArrayEncoder.fromString('key'), - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `Only one of 'privateKey' or 'did' must be provided`, - }, - }) - }) - - test('returns an error state if the submitter did is not a valid did:indy did', async () => { - const result = await indySdkIndyDidRegistrar.create(agentContext, { - method: 'indy', - options: { - submitterDid: 'BzCbsNYhMrjHiqZDTUASHg', - alias: 'Hello', - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'unknownError: BzCbsNYhMrjHiqZDTUASHg is not a valid did:indy did', - }, - }) - }) - - test('returns an error state if did is provided, but it is not a valid did:indy did', async () => { - const result = await indySdkIndyDidRegistrar.create(agentContext, { - did: 'BzCbsNYhMrjHiqZDTUASHg', - options: { - submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - verkey: 'verkey', - alias: 'Hello', - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'unknownError: BzCbsNYhMrjHiqZDTUASHg is not a valid did:indy did', - }, - }) - }) - - test('returns an error state if did is provided, but no verkey', async () => { - const result = await indySdkIndyDidRegistrar.create(agentContext, { - did: 'BzCbsNYhMrjHiqZDTUASHg', - options: { - submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - alias: 'Hello', - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'If a did is defined, a matching verkey must be provided', - }, - }) - }) - - test('returns an error state if did and verkey are provided, but the did is not self certifying', async () => { - const result = await indySdkIndyDidRegistrar.create(agentContext, { - did: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - options: { - submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - verkey: 'verkey', - alias: 'Hello', - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'Did must be first 16 bytes of the the verkey base58 encoded.', - }, - }) - }) - - test('returns an error state if did is provided, but does not match with the namespace from the submitterDid', async () => { - const result = await indySdkIndyDidRegistrar.create(agentContext, { - did: 'did:indy:pool2:R1xKJw17sUoXhejEpugMYJ', - options: { - submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', - alias: 'Hello', - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: - 'The submitter did uses namespace pool1 and the did to register uses namespace pool2. Namespaces must match.', - }, - }) - }) - - test('creates a did:indy document without services', async () => { - const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') - - // @ts-ignore method is private - const registerPublicDidSpy = jest.spyOn(indySdkIndyDidRegistrar, 'registerPublicDid') - // @ts-ignore type check fails because method is private - registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve()) - - const result = await indySdkIndyDidRegistrar.create(agentContext, { - method: 'indy', - options: { - alias: 'Hello', - submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - role: 'STEWARD', - }, - secret: { - privateKey, - }, - }) - expect(registerPublicDidSpy).toHaveBeenCalledWith( - agentContext, - pool, - // Unqualified submitter did - 'BzCbsNYhMrjHiqZDTUASHg', - // submitter signing key, - expect.any(Key), - // Unqualified created indy did - 'R1xKJw17sUoXhejEpugMYJ', - // Verkey - expect.any(Key), - // Alias - 'Hello', - // Role - 'STEWARD' - ) - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'finished', - did: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', - didDocument: { - '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], - id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', - verificationMethod: [ - { - id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey', - type: 'Ed25519VerificationKey2018', - controller: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', - publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', - }, - ], - authentication: ['did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey'], - assertionMethod: undefined, - keyAgreement: undefined, - }, - secret: { - privateKey, - }, - }, - }) - }) - - test('creates a did:indy document by passing did', async () => { - // @ts-ignore method is private - const registerPublicDidSpy = jest.spyOn(indySdkIndyDidRegistrar, 'registerPublicDid') - // @ts-ignore type check fails because method is private - registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve()) - - const result = await indySdkIndyDidRegistrar.create(agentContext, { - did: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', - options: { - verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', - alias: 'Hello', - submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - role: 'STEWARD', - }, - secret: {}, - }) - expect(registerPublicDidSpy).toHaveBeenCalledWith( - agentContext, - pool, - // Unqualified submitter did - 'BzCbsNYhMrjHiqZDTUASHg', - // submitter signing key, - expect.any(Key), - // Unqualified created indy did - 'R1xKJw17sUoXhejEpugMYJ', - // Verkey - expect.any(Key), - // Alias - 'Hello', - // Role - 'STEWARD' - ) - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'finished', - did: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', - didDocument: { - '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], - id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', - verificationMethod: [ - { - id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey', - type: 'Ed25519VerificationKey2018', - controller: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', - publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', - }, - ], - authentication: ['did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey'], - assertionMethod: undefined, - keyAgreement: undefined, - }, - secret: {}, - }, - }) - }) - - test('creates a did:indy document with services', async () => { - const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') - - // @ts-ignore method is private - const registerPublicDidSpy = jest.spyOn(indySdkIndyDidRegistrar, 'registerPublicDid') - // @ts-ignore type check fails because method is private - registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve()) - - // @ts-ignore method is private - const setEndpointsForDidSpy = jest.spyOn(indySdkIndyDidRegistrar, 'setEndpointsForDid') - // @ts-ignore type check fails because method is private - setEndpointsForDidSpy.mockImplementationOnce(() => Promise.resolve(undefined)) - - const result = await indySdkIndyDidRegistrar.create(agentContext, { - method: 'indy', - options: { - alias: 'Hello', - submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - role: 'STEWARD', - endpoints: { - endpoint: 'https://example.com/endpoint', - routingKeys: ['key-1'], - types: ['DIDComm', 'did-communication', 'endpoint'], - }, - }, - secret: { - privateKey, - }, - }) - - expect(registerPublicDidSpy).toHaveBeenCalledWith( - agentContext, - pool, - // Unqualified submitter did - 'BzCbsNYhMrjHiqZDTUASHg', - // submitter signing key, - expect.any(Key), - // Unqualified created indy did - 'R1xKJw17sUoXhejEpugMYJ', - // Verkey - expect.any(Key), - // Alias - 'Hello', - // Role - 'STEWARD' - ) - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'finished', - did: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', - didDocument: { - '@context': [ - 'https://w3id.org/did/v1', - 'https://w3id.org/security/suites/ed25519-2018/v1', - 'https://w3id.org/security/suites/x25519-2019/v1', - 'https://didcomm.org/messaging/contexts/v2', - ], - id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', - verificationMethod: [ - { - id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey', - type: 'Ed25519VerificationKey2018', - controller: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', - publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', - }, - { - id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#key-agreement-1', - type: 'X25519KeyAgreementKey2019', - controller: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', - publicKeyBase58: 'Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', - }, - ], - service: [ - { - id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#endpoint', - serviceEndpoint: 'https://example.com/endpoint', - type: 'endpoint', - }, - { - id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#did-communication', - serviceEndpoint: 'https://example.com/endpoint', - type: 'did-communication', - priority: 0, - recipientKeys: ['did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#key-agreement-1'], - routingKeys: ['key-1'], - accept: ['didcomm/aip2;env=rfc19'], - }, - { - id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#didcomm-1', - serviceEndpoint: 'https://example.com/endpoint', - type: 'DIDComm', - routingKeys: ['key-1'], - accept: ['didcomm/v2'], - }, - ], - authentication: ['did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey'], - assertionMethod: undefined, - keyAgreement: ['did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#key-agreement-1'], - }, - secret: { - privateKey, - }, - }, - }) - }) - - test('stores the did document', async () => { - const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') - - // @ts-ignore method is private - const registerPublicDidSpy = jest.spyOn(indySdkIndyDidRegistrar, 'registerPublicDid') - // @ts-ignore type check fails because method is private - registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve()) - - // @ts-ignore method is private - const setEndpointsForDidSpy = jest.spyOn(indySdkIndyDidRegistrar, 'setEndpointsForDid') - // @ts-ignore type check fails because method is private - setEndpointsForDidSpy.mockImplementationOnce(() => Promise.resolve(undefined)) - - const saveCalled = jest.fn() - eventEmitter.on>(RepositoryEventTypes.RecordSaved, saveCalled) - - await indySdkIndyDidRegistrar.create(agentContext, { - method: 'indy', - options: { - alias: 'Hello', - submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - role: 'STEWARD', - endpoints: { - endpoint: 'https://example.com/endpoint', - routingKeys: ['key-1'], - types: ['DIDComm', 'did-communication', 'endpoint'], - }, - }, - secret: { - privateKey, - }, - }) - - expect(saveCalled).toHaveBeenCalledTimes(1) - const [saveEvent] = saveCalled.mock.calls[0] - - expect(saveEvent.payload.record).toMatchObject({ - did: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', - role: DidDocumentRole.Created, - _tags: { - recipientKeyFingerprints: ['z6LSrH6AdsQeZuKKmG6Ehx7abEQZsVg2psR2VU536gigUoAe'], - }, - didDocument: undefined, - }) - }) - - test('returns an error state when calling update', async () => { - const result = await indySdkIndyDidRegistrar.update() - - expect(result).toEqual({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `notImplemented: updating did:indy not implemented yet`, - }, - }) - }) - - test('returns an error state when calling deactivate', async () => { - const result = await indySdkIndyDidRegistrar.deactivate() - - expect(result).toEqual({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `notImplemented: deactivating did:indy not implemented yet`, - }, - }) - }) -}) diff --git a/packages/indy-sdk/src/dids/__tests__/IndySdkIndyDidResolver.test.ts b/packages/indy-sdk/src/dids/__tests__/IndySdkIndyDidResolver.test.ts deleted file mode 100644 index 3b431bc964..0000000000 --- a/packages/indy-sdk/src/dids/__tests__/IndySdkIndyDidResolver.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type { IndySdkPool } from '../../ledger' -import type { IndyEndpointAttrib } from '../didSovUtil' -import type { GetNymResponse } from 'indy-sdk' - -import { SigningProviderRegistry, JsonTransformer } from '@credo-ts/core' -import indySdk from 'indy-sdk' - -import { mockFunction, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' -import { IndySdkPoolService } from '../../ledger/IndySdkPoolService' -import { IndySdkSymbol } from '../../types' -import { IndySdkWallet } from '../../wallet' -import { IndySdkIndyDidResolver } from '../IndySdkIndyDidResolver' - -import didIndyPool1R1xKJw17sUoXhejEpugMYJFixture from './__fixtures__/didIndyPool1R1xKJw17sUoXhejEpugMYJ.json' -import didIndyPool1WJz9mHyW9BZksioQnRsrAoFixture from './__fixtures__/didIndyPool1WJz9mHyW9BZksioQnRsrAo.json' - -jest.mock('../../ledger/IndySdkPoolService') -const IndySdkPoolServiceMock = IndySdkPoolService as jest.Mock -const indySdkPoolServiceMock = new IndySdkPoolServiceMock() - -mockFunction(indySdkPoolServiceMock.getPoolForNamespace).mockReturnValue({ - config: { indyNamespace: 'pool1' }, -} as IndySdkPool) - -const agentConfig = getAgentConfig('IndySdkIndyDidResolver') - -const wallet = new IndySdkWallet(indySdk, agentConfig.logger, new SigningProviderRegistry([])) - -const agentContext = getAgentContext({ - wallet, - agentConfig, - registerInstances: [ - [IndySdkPoolService, indySdkPoolServiceMock], - [IndySdkSymbol, indySdk], - ], -}) - -const indySdkSovDidResolver = new IndySdkIndyDidResolver() - -describe('IndySdkIndyDidResolver', () => { - it('should correctly resolve a did:indy document', async () => { - const did = 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ' - - const nymResponse: GetNymResponse = { - did: 'R1xKJw17sUoXhejEpugMYJ', - verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', - role: 'ENDORSER', - } - - const endpoints: IndyEndpointAttrib = { - endpoint: 'https://ssi.com', - profile: 'https://profile.com', - hub: 'https://hub.com', - } - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(indySdkSovDidResolver, 'getPublicDid').mockResolvedValue(nymResponse) - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(indySdkSovDidResolver, 'getEndpointsForDid').mockResolvedValue(endpoints) - - const result = await indySdkSovDidResolver.resolve(agentContext, did) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocument: didIndyPool1R1xKJw17sUoXhejEpugMYJFixture, - didDocumentMetadata: {}, - didResolutionMetadata: { - contentType: 'application/did+ld+json', - }, - }) - }) - - it('should resolve a did:indy document with routingKeys and types entries in the attrib', async () => { - const did = 'did:indy:pool1:WJz9mHyW9BZksioQnRsrAo' - - const nymResponse: GetNymResponse = { - did: 'WJz9mHyW9BZksioQnRsrAo', - verkey: 'GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8', - role: 'ENDORSER', - } - - const endpoints: IndyEndpointAttrib = { - endpoint: 'https://agent.com', - types: ['endpoint', 'did-communication', 'DIDComm'], - routingKeys: ['routingKey1', 'routingKey2'], - } - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(indySdkSovDidResolver, 'getPublicDid').mockResolvedValue(nymResponse) - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(indySdkSovDidResolver, 'getEndpointsForDid').mockResolvedValue(endpoints) - - const result = await indySdkSovDidResolver.resolve(agentContext, did) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocument: didIndyPool1WJz9mHyW9BZksioQnRsrAoFixture, - didDocumentMetadata: {}, - didResolutionMetadata: { - contentType: 'application/did+ld+json', - }, - }) - }) - - it('should return did resolution metadata with error if the indy ledger service throws an error', async () => { - const did = 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ' - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(indySdkSovDidResolver, 'getPublicDid').mockRejectedValue(new Error('Error retrieving did')) - - const result = await indySdkSovDidResolver.resolve(agentContext, did) - - expect(result).toMatchObject({ - didDocument: null, - didDocumentMetadata: {}, - didResolutionMetadata: { - error: 'notFound', - message: `resolver_error: Unable to resolve did 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ': Error: Error retrieving did`, - }, - }) - }) -}) diff --git a/packages/indy-sdk/src/dids/__tests__/IndySdkSovDidResolver.test.ts b/packages/indy-sdk/src/dids/__tests__/IndySdkSovDidResolver.test.ts deleted file mode 100644 index b79ca7c507..0000000000 --- a/packages/indy-sdk/src/dids/__tests__/IndySdkSovDidResolver.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import type { IndySdkPool } from '../../ledger' -import type { IndyEndpointAttrib } from '../didSovUtil' -import type { GetNymResponse } from 'indy-sdk' - -import { SigningProviderRegistry, JsonTransformer } from '@credo-ts/core' -import indySdk from 'indy-sdk' - -import { parseDid } from '../../../../core/src/modules/dids/domain/parse' -import { mockFunction, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' -import { IndySdkPoolService } from '../../ledger/IndySdkPoolService' -import { IndySdkSymbol } from '../../types' -import { IndySdkWallet } from '../../wallet' -import { IndySdkSovDidResolver } from '../IndySdkSovDidResolver' - -import didSovR1xKJw17sUoXhejEpugMYJFixture from './__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json' -import didSovWJz9mHyW9BZksioQnRsrAoFixture from './__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json' - -jest.mock('../../ledger/IndySdkPoolService') -const IndySdkPoolServiceMock = IndySdkPoolService as jest.Mock -const indySdkPoolServiceMock = new IndySdkPoolServiceMock() - -mockFunction(indySdkPoolServiceMock.getPoolForNamespace).mockReturnValue({ - config: { indyNamespace: 'pool1' }, -} as IndySdkPool) - -mockFunction(indySdkPoolServiceMock.getPoolForDid).mockResolvedValue({ - pool: { config: { indyNamespace: 'pool1' } } as IndySdkPool, -}) - -const agentConfig = getAgentConfig('IndySdkSovDidResolver') - -const wallet = new IndySdkWallet(indySdk, agentConfig.logger, new SigningProviderRegistry([])) - -const agentContext = getAgentContext({ - wallet, - agentConfig, - registerInstances: [ - [IndySdkPoolService, indySdkPoolServiceMock], - [IndySdkSymbol, indySdk], - ], -}) - -const indySdkSovDidResolver = new IndySdkSovDidResolver() - -describe('IndySdkSovDidResolver', () => { - it('should correctly resolve a did:sov document', async () => { - const did = 'did:sov:R1xKJw17sUoXhejEpugMYJ' - - const nymResponse: GetNymResponse = { - did: 'R1xKJw17sUoXhejEpugMYJ', - verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', - role: 'ENDORSER', - } - - const endpoints: IndyEndpointAttrib = { - endpoint: 'https://ssi.com', - profile: 'https://profile.com', - hub: 'https://hub.com', - } - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(indySdkSovDidResolver, 'getPublicDid').mockResolvedValue(nymResponse) - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(indySdkSovDidResolver, 'getEndpointsForDid').mockResolvedValue(endpoints) - - const result = await indySdkSovDidResolver.resolve(agentContext, did, parseDid(did)) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocument: didSovR1xKJw17sUoXhejEpugMYJFixture, - didDocumentMetadata: {}, - didResolutionMetadata: { - contentType: 'application/did+ld+json', - }, - }) - }) - - it('should resolve a did:sov document with routingKeys and types entries in the attrib', async () => { - const did = 'did:sov:WJz9mHyW9BZksioQnRsrAo' - - const nymResponse: GetNymResponse = { - did: 'WJz9mHyW9BZksioQnRsrAo', - verkey: 'GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8', - role: 'ENDORSER', - } - - const endpoints: IndyEndpointAttrib = { - endpoint: 'https://agent.com', - types: ['endpoint', 'did-communication', 'DIDComm'], - routingKeys: ['routingKey1', 'routingKey2'], - } - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(indySdkSovDidResolver, 'getPublicDid').mockResolvedValue(nymResponse) - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(indySdkSovDidResolver, 'getEndpointsForDid').mockResolvedValue(endpoints) - - const result = await indySdkSovDidResolver.resolve(agentContext, did, parseDid(did)) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocument: didSovWJz9mHyW9BZksioQnRsrAoFixture, - didDocumentMetadata: {}, - didResolutionMetadata: { - contentType: 'application/did+ld+json', - }, - }) - }) - - it('should return did resolution metadata with error if the indy ledger service throws an error', async () => { - const did = 'did:sov:R1xKJw17sUoXhejEpugMYJ' - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(indySdkSovDidResolver, 'getPublicDid').mockRejectedValue(new Error('Error retrieving did')) - - const result = await indySdkSovDidResolver.resolve(agentContext, did, parseDid(did)) - - expect(result).toMatchObject({ - didDocument: null, - didDocumentMetadata: {}, - didResolutionMetadata: { - error: 'notFound', - message: `resolver_error: Unable to resolve did 'did:sov:R1xKJw17sUoXhejEpugMYJ': Error: Error retrieving did`, - }, - }) - }) -}) diff --git a/packages/indy-sdk/src/dids/__tests__/__fixtures__/didIndyPool1R1xKJw17sUoXhejEpugMYJ.json b/packages/indy-sdk/src/dids/__tests__/__fixtures__/didIndyPool1R1xKJw17sUoXhejEpugMYJ.json deleted file mode 100644 index c0bd51aa30..0000000000 --- a/packages/indy-sdk/src/dids/__tests__/__fixtures__/didIndyPool1R1xKJw17sUoXhejEpugMYJ.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "@context": [ - "https://w3id.org/did/v1", - "https://w3id.org/security/suites/ed25519-2018/v1", - "https://w3id.org/security/suites/x25519-2019/v1" - ], - "id": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ", - "verificationMethod": [ - { - "type": "Ed25519VerificationKey2018", - "controller": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ", - "id": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey", - "publicKeyBase58": "E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu" - }, - { - "type": "X25519KeyAgreementKey2019", - "controller": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ", - "id": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#key-agreement-1", - "publicKeyBase58": "Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt" - } - ], - "authentication": ["did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey"], - "keyAgreement": ["did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#key-agreement-1"], - "service": [ - { - "id": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#endpoint", - "type": "endpoint", - "serviceEndpoint": "https://ssi.com" - }, - { - "accept": ["didcomm/aip2;env=rfc19"], - "id": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#did-communication", - "priority": 0, - "recipientKeys": ["did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#key-agreement-1"], - "routingKeys": [], - "serviceEndpoint": "https://ssi.com", - "type": "did-communication" - }, - { - "id": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#profile", - "serviceEndpoint": "https://profile.com", - "type": "profile" - }, - { - "id": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#hub", - "serviceEndpoint": "https://hub.com", - "type": "hub" - } - ] -} diff --git a/packages/indy-sdk/src/dids/__tests__/__fixtures__/didIndyPool1WJz9mHyW9BZksioQnRsrAo.json b/packages/indy-sdk/src/dids/__tests__/__fixtures__/didIndyPool1WJz9mHyW9BZksioQnRsrAo.json deleted file mode 100644 index a943f3bf9e..0000000000 --- a/packages/indy-sdk/src/dids/__tests__/__fixtures__/didIndyPool1WJz9mHyW9BZksioQnRsrAo.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "@context": [ - "https://w3id.org/did/v1", - "https://w3id.org/security/suites/ed25519-2018/v1", - "https://w3id.org/security/suites/x25519-2019/v1", - "https://didcomm.org/messaging/contexts/v2" - ], - "id": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo", - "verificationMethod": [ - { - "type": "Ed25519VerificationKey2018", - "controller": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo", - "id": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#verkey", - "publicKeyBase58": "GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8" - }, - { - "type": "X25519KeyAgreementKey2019", - "controller": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo", - "id": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#key-agreement-1", - "publicKeyBase58": "S3AQEEKkGYrrszT9D55ozVVX2XixYp8uynqVm4okbud" - } - ], - "authentication": ["did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#verkey"], - "keyAgreement": ["did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], - "service": [ - { - "id": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#endpoint", - "type": "endpoint", - "serviceEndpoint": "https://agent.com" - }, - { - "id": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#did-communication", - "type": "did-communication", - "priority": 0, - "recipientKeys": ["did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], - "routingKeys": ["routingKey1", "routingKey2"], - "accept": ["didcomm/aip2;env=rfc19"], - "serviceEndpoint": "https://agent.com" - }, - { - "id": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#didcomm-1", - "type": "DIDComm", - "serviceEndpoint": "https://agent.com", - "accept": ["didcomm/v2"], - "routingKeys": ["routingKey1", "routingKey2"] - } - ] -} diff --git a/packages/indy-sdk/src/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json b/packages/indy-sdk/src/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json deleted file mode 100644 index 6a6e4ed706..0000000000 --- a/packages/indy-sdk/src/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "@context": [ - "https://w3id.org/did/v1", - "https://w3id.org/security/suites/ed25519-2018/v1", - "https://w3id.org/security/suites/x25519-2019/v1" - ], - "id": "did:sov:R1xKJw17sUoXhejEpugMYJ", - "verificationMethod": [ - { - "type": "Ed25519VerificationKey2018", - "controller": "did:sov:R1xKJw17sUoXhejEpugMYJ", - "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#key-1", - "publicKeyBase58": "E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu" - }, - { - "type": "X25519KeyAgreementKey2019", - "controller": "did:sov:R1xKJw17sUoXhejEpugMYJ", - "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1", - "publicKeyBase58": "Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt" - } - ], - "authentication": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-1"], - "assertionMethod": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-1"], - "keyAgreement": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1"], - "service": [ - { - "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#endpoint", - "type": "endpoint", - "serviceEndpoint": "https://ssi.com" - }, - { - "accept": ["didcomm/aip2;env=rfc19"], - "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#did-communication", - "priority": 0, - "recipientKeys": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1"], - "routingKeys": [], - "serviceEndpoint": "https://ssi.com", - "type": "did-communication" - }, - { - "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#profile", - "serviceEndpoint": "https://profile.com", - "type": "profile" - }, - { - "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#hub", - "serviceEndpoint": "https://hub.com", - "type": "hub" - } - ] -} diff --git a/packages/indy-sdk/src/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json b/packages/indy-sdk/src/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json deleted file mode 100644 index 7b74e0587f..0000000000 --- a/packages/indy-sdk/src/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "@context": [ - "https://w3id.org/did/v1", - "https://w3id.org/security/suites/ed25519-2018/v1", - "https://w3id.org/security/suites/x25519-2019/v1", - "https://didcomm.org/messaging/contexts/v2" - ], - "id": "did:sov:WJz9mHyW9BZksioQnRsrAo", - "verificationMethod": [ - { - "type": "Ed25519VerificationKey2018", - "controller": "did:sov:WJz9mHyW9BZksioQnRsrAo", - "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#key-1", - "publicKeyBase58": "GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8" - }, - { - "type": "X25519KeyAgreementKey2019", - "controller": "did:sov:WJz9mHyW9BZksioQnRsrAo", - "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1", - "publicKeyBase58": "S3AQEEKkGYrrszT9D55ozVVX2XixYp8uynqVm4okbud" - } - ], - "authentication": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-1"], - "assertionMethod": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-1"], - "keyAgreement": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], - "service": [ - { - "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#endpoint", - "type": "endpoint", - "serviceEndpoint": "https://agent.com" - }, - { - "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#did-communication", - "type": "did-communication", - "priority": 0, - "recipientKeys": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], - "routingKeys": ["routingKey1", "routingKey2"], - "accept": ["didcomm/aip2;env=rfc19"], - "serviceEndpoint": "https://agent.com" - }, - { - "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#didcomm-1", - "type": "DIDComm", - "serviceEndpoint": "https://agent.com", - "accept": ["didcomm/v2"], - "routingKeys": ["routingKey1", "routingKey2"] - } - ] -} diff --git a/packages/indy-sdk/src/dids/didIndyUtil.ts b/packages/indy-sdk/src/dids/didIndyUtil.ts deleted file mode 100644 index 6131be042d..0000000000 --- a/packages/indy-sdk/src/dids/didIndyUtil.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { AgentContext } from '@credo-ts/core' - -import { - getKeyFromVerificationMethod, - AriesFrameworkError, - convertPublicKeyToX25519, - DidDocumentBuilder, - DidsApi, - TypedArrayEncoder, -} from '@credo-ts/core' - -// Create a base DIDDoc template according to https://hyperledger.github.io/indy-did-method/#base-diddoc-template -export function indyDidDocumentFromDid(did: string, publicKeyBase58: string) { - const verificationMethodId = `${did}#verkey` - - const builder = new DidDocumentBuilder(did) - .addContext('https://w3id.org/security/suites/ed25519-2018/v1') - .addVerificationMethod({ - controller: did, - id: verificationMethodId, - publicKeyBase58, - type: 'Ed25519VerificationKey2018', - }) - .addAuthentication(verificationMethodId) - - return builder -} - -export function createKeyAgreementKey(verkey: string) { - return TypedArrayEncoder.toBase58(convertPublicKeyToX25519(TypedArrayEncoder.fromBase58(verkey))) -} - -/** - * Fetches the verification key for a given did:indy did and returns the key as a {@link Key} object. - * - * @throws {@link AriesFrameworkError} if the did could not be resolved or the key could not be extracted - */ -export async function verificationKeyForIndyDid(agentContext: AgentContext, did: string) { - // FIXME: we should store the didDocument in the DidRecord so we don't have to fetch our own did - // from the ledger to know which key is associated with the did - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const didResult = await didsApi.resolve(did) - - if (!didResult.didDocument) { - throw new AriesFrameworkError( - `Could not resolve did ${did}. ${didResult.didResolutionMetadata.error} ${didResult.didResolutionMetadata.message}` - ) - } - - // did:indy dids MUST have a verificationMethod with #verkey - const verificationMethod = didResult.didDocument.dereferenceKey(`${did}#verkey`) - const key = getKeyFromVerificationMethod(verificationMethod) - - return key -} diff --git a/packages/indy-sdk/src/dids/didSovUtil.ts b/packages/indy-sdk/src/dids/didSovUtil.ts deleted file mode 100644 index e98e02a3f6..0000000000 --- a/packages/indy-sdk/src/dids/didSovUtil.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { - TypedArrayEncoder, - DidDocumentService, - DidDocumentBuilder, - DidCommV1Service, - DidCommV2Service, - convertPublicKeyToX25519, -} from '@credo-ts/core' - -import { getFullVerkey } from '../utils/did' - -export type DidCommServicesEndpointType = 'endpoint' | 'did-communication' | 'DIDComm' - -export interface IndyEndpointAttrib { - endpoint?: string - types?: Array<'endpoint' | 'did-communication' | 'DIDComm'> - routingKeys?: string[] - [key: string]: unknown -} - -/** - * Get a base did:sov did document based on the provided did and verkey - * https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html#crud-operation-definitions - */ -export function sovDidDocumentFromDid(fullDid: string, verkey: string) { - const verificationMethodId = `${fullDid}#key-1` - const keyAgreementId = `${fullDid}#key-agreement-1` - - const publicKeyBase58 = getFullVerkey(fullDid, verkey) - const publicKeyX25519 = TypedArrayEncoder.toBase58( - convertPublicKeyToX25519(TypedArrayEncoder.fromBase58(publicKeyBase58)) - ) - - const builder = new DidDocumentBuilder(fullDid) - .addContext('https://w3id.org/security/suites/ed25519-2018/v1') - .addContext('https://w3id.org/security/suites/x25519-2019/v1') - .addVerificationMethod({ - controller: fullDid, - id: verificationMethodId, - publicKeyBase58: publicKeyBase58, - type: 'Ed25519VerificationKey2018', - }) - .addVerificationMethod({ - controller: fullDid, - id: keyAgreementId, - publicKeyBase58: publicKeyX25519, - type: 'X25519KeyAgreementKey2019', - }) - .addAuthentication(verificationMethodId) - .addAssertionMethod(verificationMethodId) - .addKeyAgreement(keyAgreementId) - - return builder -} - -// Process Indy Attrib Endpoint Types according to: https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html > Read (Resolve) > DID Service Endpoint -function processEndpointTypes(types?: string[]) { - const expectedTypes = ['endpoint', 'did-communication', 'DIDComm'] - const defaultTypes = ['endpoint', 'did-communication'] - - // Return default types if types "is NOT present [or] empty" - if (!types || types.length <= 0) { - return defaultTypes - } - - // Return default types if types "contain any other values" - for (const type of types) { - if (!expectedTypes.includes(type)) { - return defaultTypes - } - } - - // Return provided types - return types -} - -export function addServicesFromEndpointsAttrib( - builder: DidDocumentBuilder, - did: string, - endpoints: IndyEndpointAttrib, - keyAgreementId: string -) { - const { endpoint, routingKeys, types, ...otherEndpoints } = endpoints - - if (endpoint) { - const processedTypes = processEndpointTypes(types) - - // If 'endpoint' included in types, add id to the services array - if (processedTypes.includes('endpoint')) { - builder.addService( - new DidDocumentService({ - id: `${did}#endpoint`, - serviceEndpoint: endpoint, - type: 'endpoint', - }) - ) - } - - // If 'did-communication' included in types, add DIDComm v1 entry - if (processedTypes.includes('did-communication')) { - builder.addService( - new DidCommV1Service({ - id: `${did}#did-communication`, - serviceEndpoint: endpoint, - priority: 0, - routingKeys: routingKeys ?? [], - recipientKeys: [keyAgreementId], - accept: ['didcomm/aip2;env=rfc19'], - }) - ) - - // If 'DIDComm' included in types, add DIDComm v2 entry - if (processedTypes.includes('DIDComm')) { - builder - .addService( - new DidCommV2Service({ - id: `${did}#didcomm-1`, - serviceEndpoint: endpoint, - routingKeys: routingKeys ?? [], - accept: ['didcomm/v2'], - }) - ) - .addContext('https://didcomm.org/messaging/contexts/v2') - } - } - } - - // Add other endpoint types - for (const [type, endpoint] of Object.entries(otherEndpoints)) { - builder.addService( - new DidDocumentService({ - id: `${did}#${type}`, - serviceEndpoint: endpoint as string, - type, - }) - ) - } -} diff --git a/packages/indy-sdk/src/dids/index.ts b/packages/indy-sdk/src/dids/index.ts deleted file mode 100644 index 8017bf8749..0000000000 --- a/packages/indy-sdk/src/dids/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { IndySdkIndyDidRegistrar, IndySdkIndyDidCreateOptions } from './IndySdkIndyDidRegistrar' -export { IndySdkSovDidResolver } from './IndySdkSovDidResolver' -export { IndySdkIndyDidResolver } from './IndySdkIndyDidResolver' diff --git a/packages/indy-sdk/src/error/IndySdkError.ts b/packages/indy-sdk/src/error/IndySdkError.ts deleted file mode 100644 index 9720b5347a..0000000000 --- a/packages/indy-sdk/src/error/IndySdkError.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { IndyError } from './indyError' - -import { AriesFrameworkError } from '@credo-ts/core' - -export class IndySdkError extends AriesFrameworkError { - public constructor(indyError: IndyError, message?: string) { - const base = `${indyError.name}(${indyError.indyName}): ${indyError.message}` - - super(message ? `${message}: ${base}` : base, { cause: indyError }) - } -} diff --git a/packages/indy-sdk/src/error/index.ts b/packages/indy-sdk/src/error/index.ts deleted file mode 100644 index 5829a46d0a..0000000000 --- a/packages/indy-sdk/src/error/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './IndySdkError' -export * from './indyError' diff --git a/packages/indy-sdk/src/error/indyError.ts b/packages/indy-sdk/src/error/indyError.ts deleted file mode 100644 index b253cf4e05..0000000000 --- a/packages/indy-sdk/src/error/indyError.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { AriesFrameworkError } from '@credo-ts/core' - -export const indyErrors = { - 100: 'CommonInvalidParam1', - 101: 'CommonInvalidParam2', - 102: 'CommonInvalidParam3', - 103: 'CommonInvalidParam4', - 104: 'CommonInvalidParam5', - 105: 'CommonInvalidParam6', - 106: 'CommonInvalidParam7', - 107: 'CommonInvalidParam8', - 108: 'CommonInvalidParam9', - 109: 'CommonInvalidParam10', - 110: 'CommonInvalidParam11', - 111: 'CommonInvalidParam12', - 112: 'CommonInvalidState', - 113: 'CommonInvalidStructure', - 114: 'CommonIOError', - 115: 'CommonInvalidParam13', - 116: 'CommonInvalidParam14', - 200: 'WalletInvalidHandle', - 201: 'WalletUnknownTypeError', - 202: 'WalletTypeAlreadyRegisteredError', - 203: 'WalletAlreadyExistsError', - 204: 'WalletNotFoundError', - 205: 'WalletIncompatiblePoolError', - 206: 'WalletAlreadyOpenedError', - 207: 'WalletAccessFailed', - 208: 'WalletInputError', - 209: 'WalletDecodingError', - 210: 'WalletStorageError', - 211: 'WalletEncryptionError', - 212: 'WalletItemNotFound', - 213: 'WalletItemAlreadyExists', - 214: 'WalletQueryError', - 300: 'PoolLedgerNotCreatedError', - 301: 'PoolLedgerInvalidPoolHandle', - 302: 'PoolLedgerTerminated', - 303: 'LedgerNoConsensusError', - 304: 'LedgerInvalidTransaction', - 305: 'LedgerSecurityError', - 306: 'PoolLedgerConfigAlreadyExistsError', - 307: 'PoolLedgerTimeout', - 308: 'PoolIncompatibleProtocolVersion', - 309: 'LedgerNotFound', - 400: 'AnoncredsRevocationRegistryFullError', - 401: 'AnoncredsInvalidUserRevocId', - 404: 'AnoncredsMasterSecretDuplicateNameError', - 405: 'AnoncredsProofRejected', - 406: 'AnoncredsCredentialRevoked', - 407: 'AnoncredsCredDefAlreadyExistsError', - 500: 'UnknownCryptoTypeError', - 600: 'DidAlreadyExistsError', - 700: 'PaymentUnknownMethodError', - 701: 'PaymentIncompatibleMethodsError', - 702: 'PaymentInsufficientFundsError', - 703: 'PaymentSourceDoesNotExistError', - 704: 'PaymentOperationNotSupportedError', - 705: 'PaymentExtraFundsError', - 706: 'TransactionNotAllowedError', -} as const - -type IndyErrorValues = (typeof indyErrors)[keyof typeof indyErrors] - -export interface IndyError { - name: 'IndyError' - message: string - indyName?: string -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isIndyError(error: any, errorName?: IndyErrorValues): error is IndyError { - if (typeof error !== 'object' || error === null) return false - - const indyError = error.name === 'IndyError' - - // if no specific indy error name is passed - // or the error is no indy error - // we can already return - if (!indyError || !errorName) return indyError - - // NodeJS Wrapper is missing some type names. When a type is missing it will - // only have the error code as string in the message field - // Until that is fixed we take that into account to make Credo work with rn-indy-sdk - // See: https://github.com/AbsaOSS/rn-indy-sdk/pull/24 - // See: https://github.com/hyperledger/indy-sdk/pull/2283 - if (!error.indyName) { - const errorCode = Number(error.message) - if (!isNaN(errorCode) && Object.prototype.hasOwnProperty.call(indyErrors, errorCode)) { - // We already check if the property is set. We can safely ignore this typescript error - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return errorName === indyErrors[errorCode] - } - - throw new AriesFrameworkError(`Could not determine errorName of indyError ${error.message}`) - } - - return error.indyName === errorName -} diff --git a/packages/indy-sdk/src/index.ts b/packages/indy-sdk/src/index.ts deleted file mode 100644 index 5857f00da2..0000000000 --- a/packages/indy-sdk/src/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Dids -export * from './dids' - -// Ledger -export { IndySdkPoolConfig } from './ledger' - -// Wallet -export { IndySdkWallet } from './wallet' - -// Storage -export { IndySdkStorageService } from './storage' - -// AnonCreds -export { - IndySdkAnonCredsRegistry, - IndySdkHolderService, - IndySdkIssuerService, - IndySdkVerifierService, -} from './anoncreds' - -// Module -export { IndySdkModule } from './IndySdkModule' -export { IndySdkModuleConfig } from './IndySdkModuleConfig' diff --git a/packages/indy-sdk/src/ledger/IndySdkPool.ts b/packages/indy-sdk/src/ledger/IndySdkPool.ts deleted file mode 100644 index abd8a138aa..0000000000 --- a/packages/indy-sdk/src/ledger/IndySdkPool.ts +++ /dev/null @@ -1,218 +0,0 @@ -import type { IndySdk } from '../types' -import type { FileSystem, Logger } from '@credo-ts/core' -import type { LedgerReadReplyResponse, LedgerRequest, LedgerWriteReplyResponse } from 'indy-sdk' -import type { Subject } from 'rxjs' - -import { AriesFrameworkError } from '@credo-ts/core' - -import { isIndyError, IndySdkError } from '../error' - -import { IndySdkPoolError } from './error' -import { isLedgerRejectResponse, isLedgerReqnackResponse } from './util' - -export interface TransactionAuthorAgreement { - version: `${number}.${number}` | `${number}` - acceptanceMechanism: string -} - -export interface IndySdkPoolConfig { - /** - * Optional id that influences the pool config that is created by the indy-sdk. - * Uses the indyNamespace as the pool identifier if not provided. - */ - id?: string - - genesisPath?: string - genesisTransactions?: string - - isProduction: boolean - indyNamespace: string - transactionAuthorAgreement?: TransactionAuthorAgreement - connectOnStartup?: boolean -} - -export class IndySdkPool { - private indySdk: IndySdk - private logger: Logger - private fileSystem: FileSystem - private poolConfig: IndySdkPoolConfig - private _poolHandle?: number - private poolConnected?: Promise - public authorAgreement?: AuthorAgreement | null - - public constructor( - poolConfig: IndySdkPoolConfig, - indySdk: IndySdk, - logger: Logger, - stop$: Subject, - fileSystem: FileSystem - ) { - this.indySdk = indySdk - this.fileSystem = fileSystem - this.poolConfig = poolConfig - this.logger = logger - - // Listen to stop$ (shutdown) and close pool - stop$.subscribe(async () => { - if (this._poolHandle) { - await this.close() - } - }) - } - - public get didIndyNamespace(): string { - return this.config.indyNamespace - } - - public get id() { - return this.poolConfig.id - } - - public get config() { - return this.poolConfig - } - - public async close() { - const poolHandle = this._poolHandle - - if (!poolHandle) { - return - } - - this._poolHandle = undefined - this.poolConnected = undefined - - await this.indySdk.closePoolLedger(poolHandle) - } - - public async delete() { - // Close the pool if currently open - if (this._poolHandle) { - await this.close() - } - - await this.indySdk.deletePoolLedgerConfig(this.poolConfig.indyNamespace) - } - - public async connect() { - if (!this.poolConnected) { - // Save the promise of connectToLedger to determine if we are done connecting - this.poolConnected = this.connectToLedger() - this.poolConnected.catch((error) => { - // Set poolConnected to undefined so we can retry connection upon failure - this.poolConnected = undefined - this.logger.error('Connection to pool: ' + this.poolConfig.genesisPath + ' failed.', { error }) - }) - return this.poolConnected - } else { - throw new AriesFrameworkError('Cannot attempt connection to ledger, already connecting.') - } - } - - private async connectToLedger() { - const poolName = this.poolConfig.id ?? this.poolConfig.indyNamespace - const genesisPath = await this.getGenesisPath() - - if (!genesisPath) { - throw new AriesFrameworkError('Cannot connect to ledger without genesis file') - } - - this.logger.debug(`Connecting to ledger pool '${poolName}'`, { genesisPath }) - await this.indySdk.setProtocolVersion(2) - - try { - this._poolHandle = await this.indySdk.openPoolLedger(poolName) - return - } catch (error) { - if (!isIndyError(error, 'PoolLedgerNotCreatedError')) { - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - this.logger.debug(`Pool '${poolName}' does not exist yet, creating.`, { - indyError: 'PoolLedgerNotCreatedError', - }) - try { - await this.indySdk.createPoolLedgerConfig(poolName, { genesis_txn: genesisPath }) - this._poolHandle = await this.indySdk.openPoolLedger(poolName) - return - } catch (error) { - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - private async submitRequest(request: LedgerRequest) { - return this.indySdk.submitRequest(await this.getPoolHandle(), request) - } - - public async submitReadRequest(request: LedgerRequest) { - const response = await this.submitRequest(request) - - if (isLedgerRejectResponse(response) || isLedgerReqnackResponse(response)) { - throw new IndySdkPoolError( - `Ledger '${this.didIndyNamespace}' rejected read transaction request: ${response.reason}` - ) - } - - return response as LedgerReadReplyResponse - } - - public async submitWriteRequest(request: LedgerRequest) { - const response = await this.submitRequest(request) - - if (isLedgerRejectResponse(response) || isLedgerReqnackResponse(response)) { - throw new IndySdkPoolError( - `Ledger '${this.didIndyNamespace}' rejected write transaction request: ${response.reason}` - ) - } - - return response as LedgerWriteReplyResponse - } - - private async getPoolHandle() { - if (this.poolConnected) { - // If we have tried to already connect to pool wait for it - try { - await this.poolConnected - } catch (error) { - this.logger.error('Connection to pool: ' + this.poolConfig.genesisPath + ' failed.', { error }) - } - } - - if (!this._poolHandle) await this.connect() - if (!this._poolHandle) throw new IndySdkPoolError('Pool handle not set after connection') - - return this._poolHandle - } - - private async getGenesisPath() { - // If the path is already provided return it - if (this.poolConfig.genesisPath) return this.poolConfig.genesisPath - - // Determine the genesisPath - const genesisPath = this.fileSystem.tempPath + `/genesis-${this.poolConfig.id ?? this.poolConfig.indyNamespace}.txn` - // Store genesis data if provided - if (this.poolConfig.genesisTransactions) { - await this.fileSystem.write(genesisPath, this.poolConfig.genesisTransactions) - this.poolConfig.genesisPath = genesisPath - return genesisPath - } - - // No genesisPath - return null - } -} - -export interface AuthorAgreement { - digest: string - version: string - text: string - ratification_ts: number - acceptanceMechanisms: AcceptanceMechanisms -} - -export interface AcceptanceMechanisms { - aml: Record - amlContext: string - version: string -} diff --git a/packages/indy-sdk/src/ledger/IndySdkPoolService.ts b/packages/indy-sdk/src/ledger/IndySdkPoolService.ts deleted file mode 100644 index dc2ee2b292..0000000000 --- a/packages/indy-sdk/src/ledger/IndySdkPoolService.ts +++ /dev/null @@ -1,357 +0,0 @@ -import type { AcceptanceMechanisms, AuthorAgreement } from './IndySdkPool' -import type { IndySdk } from '../types' -import type { AgentContext, Key } from '@credo-ts/core' -import type { GetNymResponse, LedgerReadReplyResponse, LedgerRequest, LedgerWriteReplyResponse } from 'indy-sdk' - -import { didIndyRegex } from '@credo-ts/anoncreds' -import { - TypedArrayEncoder, - CacheModuleConfig, - InjectionSymbols, - Logger, - injectable, - inject, - FileSystem, -} from '@credo-ts/core' -import { Subject } from 'rxjs' - -import { IndySdkModuleConfig } from '../IndySdkModuleConfig' -import { IndySdkError, isIndyError } from '../error' -import { assertIndySdkWallet } from '../utils/assertIndySdkWallet' -import { isLegacySelfCertifiedDid } from '../utils/did' -import { allSettled, onlyFulfilled, onlyRejected } from '../utils/promises' - -import { IndySdkPool } from './IndySdkPool' -import { IndySdkPoolError, IndySdkPoolNotConfiguredError, IndySdkPoolNotFoundError } from './error' -import { serializeRequestForSignature } from './serializeRequestForSignature' - -export interface CachedDidResponse { - nymResponse: GetNymResponse - indyNamespace: string -} - -@injectable() -export class IndySdkPoolService { - public pools: IndySdkPool[] = [] - private logger: Logger - private indySdk: IndySdk - private stop$: Subject - private fileSystem: FileSystem - private indySdkModuleConfig: IndySdkModuleConfig - - public constructor( - @inject(InjectionSymbols.Logger) logger: Logger, - @inject(InjectionSymbols.Stop$) stop$: Subject, - @inject(InjectionSymbols.FileSystem) fileSystem: FileSystem, - indySdkModuleConfig: IndySdkModuleConfig - ) { - this.logger = logger - this.indySdk = indySdkModuleConfig.indySdk - this.fileSystem = fileSystem - this.stop$ = stop$ - this.indySdkModuleConfig = indySdkModuleConfig - - this.pools = this.indySdkModuleConfig.networks.map( - (network) => new IndySdkPool(network, this.indySdk, this.logger, this.stop$, this.fileSystem) - ) - } - - /** - * Get the most appropriate pool for the given did. - * If the did is a qualified indy did, the pool will be determined based on the namespace. - * If it is a legacy unqualified indy did, the pool will be determined based on the algorithm as described in this document: - * https://docs.google.com/document/d/109C_eMsuZnTnYe2OAd02jAts1vC4axwEKIq7_4dnNVA/edit - * - * This method will optionally return a nym response when the did has been resolved to determine the ledger - * either now or in the past. The nymResponse can be used to prevent multiple ledger queries fetching the same - * did - */ - public async getPoolForDid( - agentContext: AgentContext, - did: string - ): Promise<{ pool: IndySdkPool; nymResponse?: GetNymResponse }> { - // Check if the did starts with did:indy - const match = did.match(didIndyRegex) - - if (match) { - const [, namespace] = match - - const pool = this.getPoolForNamespace(namespace) - - if (pool) return { pool } - - throw new IndySdkPoolError(`Pool for indy namespace '${namespace}' not found`) - } else { - return await this.getPoolForLegacyDid(agentContext, did) - } - } - - private async getPoolForLegacyDid( - agentContext: AgentContext, - did: string - ): Promise<{ pool: IndySdkPool; nymResponse: GetNymResponse }> { - const pools = this.pools - - if (pools.length === 0) { - throw new IndySdkPoolNotConfiguredError( - 'No indy ledgers configured. Provide at least one pool configuration in IndySdkModuleConfigOptions.networks' - ) - } - - const cache = agentContext.dependencyManager.resolve(CacheModuleConfig).cache - const cacheKey = `IndySdkPoolService:${did}` - - const cachedNymResponse = await cache.get(agentContext, cacheKey) - const pool = this.pools.find((pool) => pool.didIndyNamespace === cachedNymResponse?.indyNamespace) - - // If we have the nym response with associated pool in the cache, we'll use that - if (cachedNymResponse && pool) { - this.logger.trace(`Found ledger '${pool.didIndyNamespace}' for did '${did}' in cache`) - return { nymResponse: cachedNymResponse.nymResponse, pool } - } - - const { successful, rejected } = await this.getSettledDidResponsesFromPools(did, pools) - - if (successful.length === 0) { - const allNotFound = rejected.every((e) => e.reason instanceof IndySdkPoolNotFoundError) - const rejectedOtherThanNotFound = rejected.filter((e) => !(e.reason instanceof IndySdkPoolNotFoundError)) - - // All ledgers returned response that the did was not found - if (allNotFound) { - throw new IndySdkPoolNotFoundError(`Did '${did}' not found on any of the ledgers (total ${this.pools.length}).`) - } - - // one or more of the ledgers returned an unknown error - throw new IndySdkPoolError( - `Unknown error retrieving did '${did}' from '${rejectedOtherThanNotFound.length}' of '${pools.length}' ledgers. ${rejectedOtherThanNotFound[0].reason}`, - { cause: rejectedOtherThanNotFound[0].reason } - ) - } - - // If there are self certified DIDs we always prefer it over non self certified DIDs - // We take the first self certifying DID as we take the order in the - // IndySdkModuleConfigOptions.networks config as the order of preference of ledgers - let value = successful.find((response) => - isLegacySelfCertifiedDid(response.value.did.did, response.value.did.verkey) - )?.value - - if (!value) { - // Split between production and nonProduction ledgers. If there is at least one - // successful response from a production ledger, only keep production ledgers - // otherwise we only keep the non production ledgers. - const production = successful.filter((s) => s.value.pool.config.isProduction) - const nonProduction = successful.filter((s) => !s.value.pool.config.isProduction) - const productionOrNonProduction = production.length >= 1 ? production : nonProduction - - // We take the first value as we take the order in the IndySdkModuleConfigOptions.networks - // config as the order of preference of ledgers - value = productionOrNonProduction[0].value - } - - await cache.set(agentContext, cacheKey, { - nymResponse: value.did, - indyNamespace: value.pool.didIndyNamespace, - } satisfies CachedDidResponse) - - return { pool: value.pool, nymResponse: value.did } - } - - private async getSettledDidResponsesFromPools(did: string, pools: IndySdkPool[]) { - this.logger.trace(`Retrieving did '${did}' from ${pools.length} ledgers`) - const didResponses = await allSettled(pools.map((pool) => this.getDidFromPool(did, pool))) - - const successful = onlyFulfilled(didResponses) - this.logger.trace(`Retrieved ${successful.length} responses from ledgers for did '${did}'`) - - const rejected = onlyRejected(didResponses) - - return { - rejected, - successful, - } - } - - /** - * Get the most appropriate pool for the given indyNamespace - */ - public getPoolForNamespace(indyNamespace?: string) { - if (this.pools.length === 0) { - throw new IndySdkPoolNotConfiguredError( - 'No indy ledgers configured. Provide at least one pool configuration in IndySdkModuleConfigOptions.networks' - ) - } - - if (!indyNamespace) { - this.logger.warn('Not passing the indyNamespace is deprecated and will be removed in the future version.') - return this.pools[0] - } - - const pool = this.pools.find((pool) => pool.didIndyNamespace === indyNamespace) - - if (!pool) { - throw new IndySdkPoolNotFoundError(`No ledgers found for indy namespace '${indyNamespace}'.`) - } - - return pool - } - - public async submitWriteRequest( - agentContext: AgentContext, - pool: IndySdkPool, - request: LedgerRequest, - signingKey: Key - ): Promise { - try { - const requestWithTaa = await this.appendTaa(pool, request) - const signedRequestWithTaa = await this.signRequest(agentContext, signingKey, requestWithTaa) - - const response = await pool.submitWriteRequest(signedRequestWithTaa) - - return response - } catch (error) { - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - public async submitReadRequest(pool: IndySdkPool, request: LedgerRequest): Promise { - try { - const response = await pool.submitReadRequest(request) - - return response - } catch (error) { - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - private async signRequest(agentContext: AgentContext, key: Key, request: LedgerRequest): Promise { - assertIndySdkWallet(agentContext.wallet) - - try { - const signedPayload = await this.indySdk.cryptoSign( - agentContext.wallet.handle, - key.publicKeyBase58, - TypedArrayEncoder.fromString(serializeRequestForSignature(request)) - ) - - const signedRequest = { ...request, signature: TypedArrayEncoder.toBase58(signedPayload) } - return signedRequest - } catch (error) { - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - private async appendTaa(pool: IndySdkPool, request: LedgerRequest) { - try { - const authorAgreement = await this.getTransactionAuthorAgreement(pool) - const taa = pool.config.transactionAuthorAgreement - - // If ledger does not have TAA, we can just send request - if (authorAgreement == null) { - return request - } - // Ledger has taa but user has not specified which one to use - if (!taa) { - throw new IndySdkPoolError( - `Please, specify a transaction author agreement with version and acceptance mechanism. ${JSON.stringify( - authorAgreement - )}` - ) - } - - // Throw an error if the pool doesn't have the specified version and acceptance mechanism - if ( - authorAgreement.version !== taa.version || - !(taa.acceptanceMechanism in authorAgreement.acceptanceMechanisms.aml) - ) { - // Throw an error with a helpful message - const errMessage = `Unable to satisfy matching TAA with mechanism ${JSON.stringify( - taa.acceptanceMechanism - )} and version ${JSON.stringify(taa.version)} in pool.\n Found ${JSON.stringify( - Object.keys(authorAgreement.acceptanceMechanisms.aml) - )} and version ${authorAgreement.version} in pool.` - throw new IndySdkPoolError(errMessage) - } - - const requestWithTaa = await this.indySdk.appendTxnAuthorAgreementAcceptanceToRequest( - request, - authorAgreement.text, - taa.version, - authorAgreement.digest, - taa.acceptanceMechanism, - // Current time since epoch - // We can't use ratification_ts, as it must be greater than 1499906902 - Math.floor(new Date().getTime() / 1000) - ) - - return requestWithTaa - } catch (error) { - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - private async getTransactionAuthorAgreement(pool: IndySdkPool): Promise { - try { - // TODO Replace this condition with memoization - if (pool.authorAgreement !== undefined) { - return pool.authorAgreement - } - - const taaRequest = await this.indySdk.buildGetTxnAuthorAgreementRequest(null) - const taaResponse = await this.submitReadRequest(pool, taaRequest) - const acceptanceMechanismRequest = await this.indySdk.buildGetAcceptanceMechanismsRequest(null) - const acceptanceMechanismResponse = await this.submitReadRequest(pool, acceptanceMechanismRequest) - - // TAA can be null - if (taaResponse.result.data == null) { - pool.authorAgreement = null - return null - } - - // If TAA is not null, we can be sure AcceptanceMechanisms is also not null - const authorAgreement = taaResponse.result.data as AuthorAgreement - const acceptanceMechanisms = acceptanceMechanismResponse.result.data as AcceptanceMechanisms - pool.authorAgreement = { - ...authorAgreement, - acceptanceMechanisms, - } - return pool.authorAgreement - } catch (error) { - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - private async getDidFromPool(did: string, pool: IndySdkPool): Promise { - try { - this.logger.trace(`Get public did '${did}' from ledger '${pool.didIndyNamespace}'`) - const request = await this.indySdk.buildGetNymRequest(null, did) - - this.logger.trace(`Submitting get did request for did '${did}' to ledger '${pool.didIndyNamespace}'`) - const response = await pool.submitReadRequest(request) - - const result = await this.indySdk.parseGetNymResponse(response) - this.logger.trace(`Retrieved did '${did}' from ledger '${pool.didIndyNamespace}'`, result) - - return { - did: result, - pool, - response, - } - } catch (error) { - this.logger.trace(`Error retrieving did '${did}' from ledger '${pool.didIndyNamespace}'`, { - error, - did, - }) - if (isIndyError(error, 'LedgerNotFound')) { - throw new IndySdkPoolNotFoundError(`Did '${did}' not found on ledger ${pool.didIndyNamespace}`) - } else { - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - } -} - -export interface PublicDidRequest { - did: GetNymResponse - pool: IndySdkPool - response: LedgerReadReplyResponse -} diff --git a/packages/indy-sdk/src/ledger/__tests__/IndySdkPoolService.test.ts b/packages/indy-sdk/src/ledger/__tests__/IndySdkPoolService.test.ts deleted file mode 100644 index f7b489c21d..0000000000 --- a/packages/indy-sdk/src/ledger/__tests__/IndySdkPoolService.test.ts +++ /dev/null @@ -1,422 +0,0 @@ -import type { IndySdkPoolConfig } from '../IndySdkPool' -import type { CachedDidResponse } from '../IndySdkPoolService' - -import { - CacheModuleConfig, - InMemoryLruCache, - SigningProviderRegistry, - AriesFrameworkError, - Key, - KeyType, -} from '@credo-ts/core' -import indySdk from 'indy-sdk' -import { Subject } from 'rxjs' - -import { getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' -import { NodeFileSystem } from '../../../../node/src/NodeFileSystem' -import { IndySdkModuleConfig } from '../../IndySdkModuleConfig' -import { IndySdkWallet } from '../../wallet/IndySdkWallet' -import { IndySdkPoolService } from '../IndySdkPoolService' -import { IndySdkPoolError, IndySdkPoolNotConfiguredError, IndySdkPoolNotFoundError } from '../error' - -import { getDidResponsesForDid } from './didResponses' - -const pools: IndySdkPoolConfig[] = [ - { - indyNamespace: 'sovrin', - isProduction: true, - genesisTransactions: 'xxx', - transactionAuthorAgreement: { version: '1', acceptanceMechanism: 'accept' }, - }, - { - indyNamespace: 'sovrin:builder', - isProduction: false, - genesisTransactions: 'xxx', - transactionAuthorAgreement: { version: '1', acceptanceMechanism: 'accept' }, - }, - { - indyNamespace: 'sovrin:staging', - isProduction: false, - genesisTransactions: 'xxx', - transactionAuthorAgreement: { version: '1', acceptanceMechanism: 'accept' }, - }, - { - indyNamespace: 'indicio', - isProduction: true, - genesisTransactions: 'xxx', - transactionAuthorAgreement: { version: '1', acceptanceMechanism: 'accept' }, - }, - { - indyNamespace: 'bcovrin:test', - isProduction: false, - genesisTransactions: 'xxx', - transactionAuthorAgreement: { version: '1', acceptanceMechanism: 'accept' }, - }, -] - -const config = getAgentConfig('IndySdkPoolServiceTest') -const cache = new InMemoryLruCache({ limit: 1 }) - -const indySdkModule = new IndySdkModuleConfig({ indySdk, networks: pools }) -const wallet = new IndySdkWallet(indySdk, config.logger, new SigningProviderRegistry([])) - -const agentContext = getAgentContext({ - wallet, - registerInstances: [[CacheModuleConfig, new CacheModuleConfig({ cache })]], -}) - -const poolService = new IndySdkPoolService(config.logger, new Subject(), new NodeFileSystem(), indySdkModule) - -describe('IndySdkPoolService', () => { - beforeAll(async () => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await wallet.createAndOpen(config.walletConfig!) - }) - - afterAll(async () => { - await wallet.delete() - }) - - afterEach(() => { - cache.clear() - }) - - describe('getPoolForDid', () => { - it('should throw a IndySdkPoolNotConfiguredError error if no pools are configured on the pool service', async () => { - const oldPools = poolService.pools - poolService.pools = [] - - expect(poolService.getPoolForDid(agentContext, 'some-did')).rejects.toThrow(IndySdkPoolNotConfiguredError) - - poolService.pools = oldPools - }) - - it('should throw a IndySdkPoolError if all ledger requests throw an error other than NotFoundError', async () => { - const did = 'Y5bj4SjCiTM9PgeheKAiXx' - - poolService.pools.forEach((pool) => { - const spy = jest.spyOn(pool, 'submitReadRequest') - spy.mockImplementationOnce(() => Promise.reject(new AriesFrameworkError('Something went wrong'))) - }) - - expect(poolService.getPoolForDid(agentContext, did)).rejects.toThrowError(IndySdkPoolError) - }) - - it('should throw a IndySdkPoolNotFoundError if all pools did not find the did on the ledger', async () => { - const did = 'Y5bj4SjCiTM9PgeheKAiXx' - // Not found on any of the ledgers - const responses = getDidResponsesForDid(did, pools, {}) - - poolService.pools.forEach((pool, index) => { - const spy = jest.spyOn(pool, 'submitReadRequest') - spy.mockImplementationOnce(responses[index]) - }) - - expect(poolService.getPoolForDid(agentContext, did)).rejects.toThrowError(IndySdkPoolNotFoundError) - }) - - it('should return the pool based on namespace if did is a valid did:indy did', async () => { - const { pool } = await poolService.getPoolForDid(agentContext, 'did:indy:sovrin:Y5bj4SjCiTM9PgeheKAiXx') - - expect(pool.didIndyNamespace).toBe('sovrin') - }) - - it('should return the pool if the did was only found on one ledger', async () => { - const did = 'TL1EaPFCZ8Si5aUrqScBDt' - // Only found on one ledger - const responses = getDidResponsesForDid(did, pools, { - sovrin: '~43X4NhAFqREffK7eWdKgFH', - }) - - poolService.pools.forEach((pool, index) => { - const spy = jest.spyOn(pool, 'submitReadRequest') - spy.mockImplementationOnce(responses[index]) - }) - - const { pool } = await poolService.getPoolForDid(agentContext, did) - - expect(pool.config.indyNamespace).toBe('sovrin') - }) - - it('should return the first pool with a self certifying DID if at least one did is self certifying ', async () => { - const did = 'did:sov:q7ATwTYbQDgiigVijUAej' - // Found on one production and one non production ledger - const responses = getDidResponsesForDid(did, pools, { - indicio: '~43X4NhAFqREffK7eWdKgFH', - 'bcovrin:test': '43X4NhAFqREffK7eWdKgFH43X4NhAFqREffK7eWdKgFH', - 'sovrin:builder': '~43X4NhAFqREffK7eWdKgFH', - }) - - poolService.pools.forEach((pool, index) => { - const spy = jest.spyOn(pool, 'submitReadRequest') - spy.mockImplementationOnce(responses[index]) - }) - - const { pool } = await poolService.getPoolForDid(agentContext, did) - - expect(pool.config.indyNamespace).toBe('sovrin:builder') - }) - - it('should return the production pool if the did was found on one production and one non production ledger and both DIDs are not self certifying', async () => { - const did = 'V6ty6ttM3EjuCtosH6sGtW' - // Found on one production and one non production ledger - const responses = getDidResponsesForDid(did, pools, { - indicio: '43X4NhAFqREffK7eWdKgFH43X4NhAFqREffK7eWdKgFH', - 'sovrin:builder': '43X4NhAFqREffK7eWdKgFH43X4NhAFqREffK7eWdKgFH', - }) - - poolService.pools.forEach((pool, index) => { - const spy = jest.spyOn(pool, 'submitReadRequest') - spy.mockImplementationOnce(responses[index]) - }) - - const { pool } = await poolService.getPoolForDid(agentContext, did) - - expect(pool.config.indyNamespace).toBe('indicio') - }) - - it('should return the pool with the self certified did if the did was found on two production ledgers where one did is self certified', async () => { - const did = 'VsKV7grR1BUE29mG2Fm2kX' - // Found on two production ledgers. Sovrin is self certified - const responses = getDidResponsesForDid(did, pools, { - sovrin: '~43X4NhAFqREffK7eWdKgFH', - indicio: 'kqa2HyagzfMAq42H5f9u3UMwnSBPQx2QfrSyXbUPxMn', - }) - - poolService.pools.forEach((pool, index) => { - const spy = jest.spyOn(pool, 'submitReadRequest') - spy.mockImplementationOnce(responses[index]) - }) - - const { pool } = await poolService.getPoolForDid(agentContext, did) - - expect(pool.config.indyNamespace).toBe('sovrin') - }) - - it('should return the first pool with a self certified did if the did was found on three non production ledgers where two DIDs are self certified', async () => { - const did = 'HEi9QViXNThGQaDsQ3ptcw' - // Found on two non production ledgers. Sovrin is self certified - const responses = getDidResponsesForDid(did, pools, { - 'sovrin:builder': '~M9kv2Ez61cur7X39DXWh8W', - 'sovrin:staging': '~M9kv2Ez61cur7X39DXWh8W', - 'bcovrin:test': '3SeuRm3uYuQDYmHeuMLu1xNHozNTtzS3kbZRFMMCWrX4', - }) - - poolService.pools.forEach((pool, index) => { - const spy = jest.spyOn(pool, 'submitReadRequest') - spy.mockImplementationOnce(responses[index]) - }) - - const { pool } = await poolService.getPoolForDid(agentContext, did) - - expect(pool.config.indyNamespace).toBe('sovrin:builder') - }) - - it('should return the pool from the cache if the did was found in the cache', async () => { - const did = 'HEi9QViXNThGQaDsQ3ptcw' - - const expectedPool = pools[3] - - const didResponse: CachedDidResponse = { - nymResponse: { - did, - role: 'ENDORSER', - verkey: '~M9kv2Ez61cur7X39DXWh8W', - }, - indyNamespace: expectedPool.indyNamespace, - } - - await cache.set(agentContext, `IndySdkPoolService:${did}`, didResponse) - const { pool } = await poolService.getPoolForDid(agentContext, did) - - expect(pool.config.indyNamespace).toBe(pool.didIndyNamespace) - }) - - it('should set the indyNamespace in the cache if the did was not found in the cache, but resolved later on', async () => { - const did = 'HEi9QViXNThGQaDsQ3ptcw' - // Found on one ledger - const responses = getDidResponsesForDid(did, pools, { - 'sovrin:builder': '~M9kv2Ez61cur7X39DXWh8W', - }) - - poolService.pools.forEach((pool, index) => { - const spy = jest.spyOn(pool, 'submitReadRequest') - spy.mockImplementationOnce(responses[index]) - }) - - const { pool } = await poolService.getPoolForDid(agentContext, did) - - expect(pool.config.indyNamespace).toBe('sovrin:builder') - expect(pool.config.indyNamespace).toBe('sovrin:builder') - - expect(await cache.get(agentContext, `IndySdkPoolService:${did}`)).toEqual({ - nymResponse: { - did, - verkey: '~M9kv2Ez61cur7X39DXWh8W', - role: '0', - }, - indyNamespace: 'sovrin:builder', - }) - }) - }) - - describe('getPoolForNamespace', () => { - it('should throw a IndySdkPoolNotConfiguredError error if no pools are configured on the pool service', async () => { - const oldPools = poolService.pools - poolService.pools = [] - - expect(() => poolService.getPoolForNamespace()).toThrow(IndySdkPoolNotConfiguredError) - - poolService.pools = oldPools - }) - - it('should return the first pool if indyNamespace is not provided', async () => { - const expectedPool = pools[0] - - expect(poolService.getPoolForNamespace().didIndyNamespace).toEqual(expectedPool.indyNamespace) - }) - - it('should throw a IndySdkPoolNotFoundError error if any of the pools did not have the provided indyNamespace', async () => { - const indyNameSpace = 'test' - const responses = pools.map((pool) => pool.indyNamespace) - - poolService.pools.forEach((pool, index) => { - const spy = jest.spyOn(pool, 'didIndyNamespace', 'get') - spy.mockReturnValueOnce(responses[index]) - }) - - expect(() => poolService.getPoolForNamespace(indyNameSpace)).toThrow(IndySdkPoolNotFoundError) - }) - - it('should return the first pool that indyNamespace matches', async () => { - const expectedPool = pools[3] - const indyNameSpace = 'indicio' - const responses = pools.map((pool) => pool.indyNamespace) - - poolService.pools.forEach((pool, index) => { - const spy = jest.spyOn(pool, 'didIndyNamespace', 'get') - spy.mockReturnValueOnce(responses[index]) - }) - - const pool = poolService.getPoolForNamespace(indyNameSpace) - - expect(pool.didIndyNamespace).toEqual(expectedPool.indyNamespace) - }) - }) - - describe('submitWriteRequest', () => { - it('should throw an error if the config version does not match', async () => { - const pool = poolService.getPoolForNamespace() - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(poolService, 'getTransactionAuthorAgreement').mockResolvedValue({ - digest: 'abcde', - version: '2.0', - text: 'jhsdhbv', - ratification_ts: 12345678, - acceptanceMechanisms: { - aml: { accept: 'accept' }, - amlContext: 'accept', - version: '3', - }, - } as never) - await expect( - poolService.submitWriteRequest( - agentContext, - pool, - { - reqId: 1668174449192969000, - identifier: 'BBPoJqRKatdcfLEAFL7exC', - operation: { - type: '1', - dest: 'N8NQHLtCKfPmWMgCSdfa7h', - verkey: 'GAb4NUvpBcHVCvtP45vTVa5Bp74vFg3iXzdp1Gbd68Wf', - alias: 'Heinz57', - }, - protocolVersion: 2, - }, - Key.fromPublicKeyBase58('GAb4NUvpBcHVCvtP45vTVa5Bp74vFg3iXzdp1Gbd68Wf', KeyType.Ed25519) - ) - ).rejects.toThrowError( - 'Unable to satisfy matching TAA with mechanism "accept" and version "1" in pool.\n Found ["accept"] and version 2.0 in pool.' - ) - }) - - it('should throw an error if the config acceptance mechanism does not match', async () => { - const pool = poolService.getPoolForNamespace() - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(poolService, 'getTransactionAuthorAgreement').mockResolvedValue({ - digest: 'abcde', - version: '1.0', - text: 'jhsdhbv', - ratification_ts: 12345678, - acceptanceMechanisms: { - aml: { decline: 'accept' }, - amlContext: 'accept', - version: '1', - }, - } as never) - await expect( - poolService.submitWriteRequest( - agentContext, - pool, - { - reqId: 1668174449192969000, - identifier: 'BBPoJqRKatdcfLEAFL7exC', - operation: { - type: '1', - dest: 'N8NQHLtCKfPmWMgCSdfa7h', - verkey: 'GAb4NUvpBcHVCvtP45vTVa5Bp74vFg3iXzdp1Gbd68Wf', - alias: 'Heinz57', - }, - protocolVersion: 2, - }, - Key.fromPublicKeyBase58('GAb4NUvpBcHVCvtP45vTVa5Bp74vFg3iXzdp1Gbd68Wf', KeyType.Ed25519) - ) - ).rejects.toThrowError( - 'Unable to satisfy matching TAA with mechanism "accept" and version "1" in pool.\n Found ["decline"] and version 1.0 in pool.' - ) - }) - - it('should throw an error if no config is present', async () => { - const pool = poolService.getPoolForNamespace() - pool.authorAgreement = undefined - pool.config.transactionAuthorAgreement = undefined - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - jest.spyOn(poolService, 'getTransactionAuthorAgreement').mockResolvedValue({ - digest: 'abcde', - version: '1.0', - text: 'jhsdhbv', - ratification_ts: 12345678, - acceptanceMechanisms: { - aml: { accept: 'accept' }, - amlContext: 'accept', - version: '3', - }, - } as never) - await expect( - poolService.submitWriteRequest( - agentContext, - pool, - { - reqId: 1668174449192969000, - identifier: 'BBPoJqRKatdcfLEAFL7exC', - operation: { - type: '1', - dest: 'N8NQHLtCKfPmWMgCSdfa7h', - verkey: 'GAb4NUvpBcHVCvtP45vTVa5Bp74vFg3iXzdp1Gbd68Wf', - alias: 'Heinz57', - }, - protocolVersion: 2, - }, - Key.fromPublicKeyBase58('GAb4NUvpBcHVCvtP45vTVa5Bp74vFg3iXzdp1Gbd68Wf', KeyType.Ed25519) - ) - ).rejects.toThrowError(/Please, specify a transaction author agreement with version and acceptance mechanism/) - }) - }) -}) diff --git a/packages/indy-sdk/src/ledger/__tests__/didResponses.ts b/packages/indy-sdk/src/ledger/__tests__/didResponses.ts deleted file mode 100644 index 4d3dac6596..0000000000 --- a/packages/indy-sdk/src/ledger/__tests__/didResponses.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { IndySdkPoolConfig } from '../IndySdkPool' -import type * as Indy from 'indy-sdk' - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -// eslint-disable-next-line import/no-extraneous-dependencies -import IndyError from 'indy-sdk/src/IndyError' - -export function getDidResponse({ did, verkey }: { did: string; verkey: string }) { - const response: Indy.LedgerReadReplyResponse = { - op: 'REPLY', - result: { - txnTime: 1632680963, - reqId: 1632681194706196000, - state_proof: { - multi_signature: { - participants: ['Node3', 'Node4', 'Node2'], - value: { - state_root_hash: 'AqMNuzJHeHduhggd8URobGyc1W89oGEjmohRXkB66JZo', - ledger_id: 1, - pool_state_root_hash: 'NCGqbfRWDWtLB2bDuL6TC5BhrRdQMc5MyKdXQqXii44', - timestamp: 1632680963, - txn_root_hash: 'AxqfyyDFuY74kiXcfvkYCWCVrHsrQutKaoi3ao4Vp8K7', - }, - signature: - 'QwkoPr9pwXyBdtMMUtJ841QjX3pTEQP6bumBpHCWiBCn4AduEW55SQXHjfQZd7EXEjArMfjNyDjgC3Qsvh51WAFGK74C3Tq7k5zYbm7kbVZdUse2i27XiDkMuB6sriroi7XHfnV3Bo55ig3APAFXD7mQrKTGE2ov17CF6yn1ns81vf', - }, - proof_nodes: - '+QHS+JygNttWkmVHYjZyCCk0TNJr5l7AJOnuLNU99qWyNhfBuWq4efh3uHV7ImlkZW50aWZpZXIiOiJWNFNHUlU4Nlo1OGQ2VFY3UEJVZTZmIiwicm9sZSI6IjAiLCJzZXFObyI6MTEsInR4blRpbWUiOjE2MzI2ODA5NjMsInZlcmtleSI6In40M1g0TmhBRnFSRWZmSzdlV2RLZ0ZIIn35ATGg09I/bgmxWmztC58rrZwebgwutUGli7VUyVOFwmuLFqOAoNrtARUl8FhzgOfGsZGlm8IVqgH1wB5KaoajR9sA53e2oJqauj70Qf++s0g43b1zvnQEyQJh2lfNqxFRtmaADvkwgKACG8f0w2NsuDibWYibc1TYySAgUKSeIevHF6wVZdMBL6BEAIIJs0un9jVqVEABbCWTkc0rybTVrFgaKU6LD6ciGYCAgICgJHIm3oUOYlDrQlw95UDkRdOc2tGIsE9g2r12AjpJiUKAoH0lXE47VtUlFvwnCC5rgY878m6TpeEZTJIKd4SUxXtqoBvSoTludXD0XkhTPm4YxfCcAdCaiDvkzM8w6O4v5/e1oDs6GXxRL8inD2b3RY1v/ufksDHNqfFKaK2MEIjNIZwagA==', - root_hash: 'AqMNuzJHeHduhggd8URobGyc1W89oGEjmohRXkB66JZo', - }, - seqNo: 11, - identifier: 'LibindyDid111111111111', - dest: did, - data: `{"dest":"${did}","identifier":"V4SGRU86Z58d6TV7PBUe6f","role":"0","seqNo":11,"txnTime":1632680963,"verkey":"${verkey}"}`, - type: '105', - }, - } - - return response -} - -export function getDidResponsesForDid( - did: string, - pools: IndySdkPoolConfig[], - responses: { [key: string]: string | undefined } -) { - return pools.map((pool) => { - const verkey = responses[pool.indyNamespace] - - if (verkey) { - return () => Promise.resolve(getDidResponse({ did, verkey })) - } - - // LedgerNotFound - return () => Promise.reject(new IndyError(309)) - }) -} diff --git a/packages/indy-sdk/src/ledger/__tests__/serializeRequestForSignature.test.ts b/packages/indy-sdk/src/ledger/__tests__/serializeRequestForSignature.test.ts deleted file mode 100644 index 7bf0c64a67..0000000000 --- a/packages/indy-sdk/src/ledger/__tests__/serializeRequestForSignature.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { serializeRequestForSignature } from '../serializeRequestForSignature' - -describe('serializeRequestForSignature', () => { - it('Should correctly serialize the json for signature input', () => { - const request = { - name: 'John Doe', - age: 43, - operation: { - dest: 54, - }, - phones: ['1234567', '2345678', { rust: 5, age: 1 }, 3], - } - - const expectedResult = 'age:43|name:John Doe|operation:dest:54|phones:1234567,2345678,age:1|rust:5,3' - - expect(serializeRequestForSignature(request)).toEqual(expectedResult) - }) - - it('Should correctly serialize the json for signature with skipped fields', () => { - const request = { - name: 'John Doe', - age: 43, - operation: { - type: '100', - hash: 'cool hash', - dest: 54, - }, - fees: 'fees1', - signature: 'sign1', - signatures: 'sign-m', - phones: ['1234567', '2345678', { rust: 5, age: 1 }, 3], - } - - const expectedResult = - 'age:43|name:John Doe|operation:dest:54|hash:46aa0c92129b33ee72ee1478d2ae62fa6e756869dedc6c858af3214a6fcf1904|type:100|phones:1234567,2345678,age:1|rust:5,3' - - expect(serializeRequestForSignature(request)).toEqual(expectedResult) - }) - - it('Should correctly serialize the json for signature with raw hash for attrib related types', () => { - const request = { - name: 'John Doe', - age: 43, - operation: { - type: '100', - hash: 'cool hash', - dest: 54, - raw: 'string for hash', - }, - phones: ['1234567', '2345678', { rust: 5, age: 1 }, 3], - } - - const expectedResult = - 'age:43|name:John Doe|operation:dest:54|hash:46aa0c92129b33ee72ee1478d2ae62fa6e756869dedc6c858af3214a6fcf1904|raw:1dcd0759ce38f57049344a6b3c5fc18144fca1724713090c2ceeffa788c02711|type:100|phones:1234567,2345678,age:1|rust:5,3' - - expect(serializeRequestForSignature(request)).toEqual(expectedResult) - }) - - it('Should correctly serialize the json for signature with raw hash for non-attrib related types', () => { - const request = { - name: 'John Doe', - age: 43, - operation: { - type: '101', - hash: 'cool hash', - dest: 54, - raw: 'string for hash', - }, - phones: ['1234567', '2345678', { rust: 5, age: 1 }, 3], - } - - const expectedResult = - 'age:43|name:John Doe|operation:dest:54|hash:cool hash|raw:string for hash|type:101|phones:1234567,2345678,age:1|rust:5,3' - - expect(serializeRequestForSignature(request)).toEqual(expectedResult) - }) - - it('Should correctly serialize the json for signature with null signature', () => { - const request = { - signature: null, - } - - const expectedResult = '' - - expect(serializeRequestForSignature(request)).toEqual(expectedResult) - }) -}) diff --git a/packages/indy-sdk/src/ledger/__tests__/util.test.ts b/packages/indy-sdk/src/ledger/__tests__/util.test.ts deleted file mode 100644 index 38976758ae..0000000000 --- a/packages/indy-sdk/src/ledger/__tests__/util.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { LedgerRejectResponse, LedgerReqnackResponse } from 'indy-sdk' - -import * as LedgerUtil from '../util' - -describe('LedgerUtils', () => { - // IsLedgerRejectResponse - it('Should return true if the response op is: REJECT', () => { - const ledgerResponse: LedgerRejectResponse = { - op: 'REJECT', - reqId: 1, - reason: 'Why not', - identifier: '123456', - } - expect(LedgerUtil.isLedgerRejectResponse(ledgerResponse)).toEqual(true) - }) - it('Should return false if the response op is not: REJECT', () => { - const ledgerResponse: LedgerReqnackResponse = { - op: 'REQNACK', - reqId: 1, - reason: 'Why not', - identifier: '123456', - } - expect(LedgerUtil.isLedgerRejectResponse(ledgerResponse)).toEqual(false) - }) - - // isLedgerReqnackResponse - it('Should return true if the response op is: REQNACK', () => { - const ledgerResponse: LedgerReqnackResponse = { - op: 'REQNACK', - reqId: 1, - reason: 'Why not', - identifier: '123456', - } - expect(LedgerUtil.isLedgerReqnackResponse(ledgerResponse)).toEqual(true) - }) - it('Should return false if the response op is NOT: REQNACK', () => { - const ledgerResponse: LedgerRejectResponse = { - op: 'REJECT', - reqId: 1, - reason: 'Why not', - identifier: '123456', - } - expect(LedgerUtil.isLedgerReqnackResponse(ledgerResponse)).toEqual(false) - }) -}) diff --git a/packages/indy-sdk/src/ledger/error/IndySdkPoolError.ts b/packages/indy-sdk/src/ledger/error/IndySdkPoolError.ts deleted file mode 100644 index dd124d0e65..0000000000 --- a/packages/indy-sdk/src/ledger/error/IndySdkPoolError.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { AriesFrameworkError } from '@credo-ts/core' - -export class IndySdkPoolError extends AriesFrameworkError { - public constructor(message: string, { cause }: { cause?: Error } = {}) { - super(message, { cause }) - } -} diff --git a/packages/indy-sdk/src/ledger/error/IndySdkPoolNotConfiguredError.ts b/packages/indy-sdk/src/ledger/error/IndySdkPoolNotConfiguredError.ts deleted file mode 100644 index 91cd3c7199..0000000000 --- a/packages/indy-sdk/src/ledger/error/IndySdkPoolNotConfiguredError.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IndySdkPoolError } from './IndySdkPoolError' - -export class IndySdkPoolNotConfiguredError extends IndySdkPoolError { - public constructor(message: string, { cause }: { cause?: Error } = {}) { - super(message, { cause }) - } -} diff --git a/packages/indy-sdk/src/ledger/error/IndySdkPoolNotFoundError.ts b/packages/indy-sdk/src/ledger/error/IndySdkPoolNotFoundError.ts deleted file mode 100644 index 4977428cba..0000000000 --- a/packages/indy-sdk/src/ledger/error/IndySdkPoolNotFoundError.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IndySdkPoolError } from './IndySdkPoolError' - -export class IndySdkPoolNotFoundError extends IndySdkPoolError { - public constructor(message: string, { cause }: { cause?: Error } = {}) { - super(message, { cause }) - } -} diff --git a/packages/indy-sdk/src/ledger/error/index.ts b/packages/indy-sdk/src/ledger/error/index.ts deleted file mode 100644 index e2554abbdf..0000000000 --- a/packages/indy-sdk/src/ledger/error/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './IndySdkPoolError' -export * from './IndySdkPoolNotConfiguredError' -export * from './IndySdkPoolNotFoundError' diff --git a/packages/indy-sdk/src/ledger/index.ts b/packages/indy-sdk/src/ledger/index.ts deleted file mode 100644 index fe016abcec..0000000000 --- a/packages/indy-sdk/src/ledger/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './IndySdkPool' -export * from './IndySdkPoolService' diff --git a/packages/indy-sdk/src/ledger/serializeRequestForSignature.ts b/packages/indy-sdk/src/ledger/serializeRequestForSignature.ts deleted file mode 100644 index 7338e21892..0000000000 --- a/packages/indy-sdk/src/ledger/serializeRequestForSignature.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Hasher, TypedArrayEncoder } from '@credo-ts/core' - -const ATTRIB_TYPE = '100' -const GET_ATTR_TYPE = '104' - -/// Generate the normalized form of a ledger transaction request for signing -export function serializeRequestForSignature(v: any): string { - const type = v?.operation?.type - - return _serializeRequestForSignature(v, true, type != undefined ? `${type}` : undefined) -} - -/** - * Serialize an indy ledger request object for signing input. Based on the rust code. Indy SDK requires ledger requests to be signed using - * a did, however in Credo's the wallet only creates keys, and we create custom did records. This allows us to remove the legacy createDid and - * publicDidSeed properties from the wallet, as we create the request payload ourselves. - * - * @see https://github.com/hyperledger/indy-shared-rs/blob/6af1e939586d1f16341dc03b62970cf28b32d118/indy-utils/src/txn_signature.rs#L10 - */ -function _serializeRequestForSignature(v: any, isTopLevel: boolean, _type?: string): string { - const vType = typeof v - - if (vType === 'boolean') return v ? 'True' : 'False' - if (vType === 'number') return v.toString() - if (vType === 'string') return v - - if (vType === 'object') { - if (Array.isArray(v)) { - return v.map((element) => _serializeRequestForSignature(element, false, _type)).join(',') - } - - let result = '' - let inMiddle = false - - for (const vKey of Object.keys(v).sort()) { - // Skip signature field at top level as in python code - if (isTopLevel && (vKey == 'signature' || vKey == 'fees' || vKey == 'signatures')) { - continue - } - - if (inMiddle) { - result += '|' - } - - let value = v[vKey] - if ((_type == ATTRIB_TYPE || _type == GET_ATTR_TYPE) && (vKey == 'raw' || vKey == 'hash' || vKey == 'enc')) { - // do it only for attribute related request - if (typeof value !== 'string') throw new Error('Value must be a string for hash') - const hash = Hasher.hash(TypedArrayEncoder.fromString(value), 'sha2-256') - value = Buffer.from(hash).toString('hex') - } - - result = `${result}${vKey}:${_serializeRequestForSignature(value, false, _type)}` - inMiddle = true - } - - return result - } - - return '' -} diff --git a/packages/indy-sdk/src/ledger/util.ts b/packages/indy-sdk/src/ledger/util.ts deleted file mode 100644 index d7b5fc2076..0000000000 --- a/packages/indy-sdk/src/ledger/util.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { LedgerResponse, LedgerRejectResponse, LedgerReqnackResponse } from 'indy-sdk' - -export function isLedgerRejectResponse(response: LedgerResponse): response is LedgerRejectResponse { - return response.op === 'REJECT' -} - -export function isLedgerReqnackResponse(response: LedgerResponse): response is LedgerReqnackResponse { - return response.op === 'REQNACK' -} diff --git a/packages/indy-sdk/src/storage/IndySdkStorageService.ts b/packages/indy-sdk/src/storage/IndySdkStorageService.ts deleted file mode 100644 index c2dc7e19cd..0000000000 --- a/packages/indy-sdk/src/storage/IndySdkStorageService.ts +++ /dev/null @@ -1,321 +0,0 @@ -import type { IndySdkWallet } from '../wallet/IndySdkWallet' -import type { BaseRecordConstructor, AgentContext, BaseRecord, TagsBase, Query, StorageService } from '@credo-ts/core' -import type { WalletQuery, WalletRecord, WalletSearchOptions } from 'indy-sdk' - -import { RecordDuplicateError, RecordNotFoundError, injectable, inject, JsonTransformer } from '@credo-ts/core' - -import { isIndyError, IndySdkError } from '../error' -import { IndySdk, IndySdkSymbol } from '../types' -import { assertIndySdkWallet } from '../utils/assertIndySdkWallet' - -@injectable() -export class IndySdkStorageService implements StorageService { - private indySdk: IndySdk - - private static DEFAULT_QUERY_OPTIONS = { - retrieveType: true, - retrieveTags: true, - } - - public constructor(@inject(IndySdkSymbol) indySdk: IndySdk) { - this.indySdk = indySdk - } - - private transformToRecordTagValues(tags: { [key: number]: string | undefined }): TagsBase { - const transformedTags: TagsBase = {} - - for (const [key, value] of Object.entries(tags)) { - // If the value is a boolean string ('1' or '0') - // use the boolean val - if (value === '1' && key?.includes(':')) { - const [tagName, tagValue] = key.split(':') - - const transformedValue = transformedTags[tagName] - - if (Array.isArray(transformedValue)) { - transformedTags[tagName] = [...transformedValue, tagValue] - } else { - transformedTags[tagName] = [tagValue] - } - } - // Transform '1' and '0' to boolean - else if (value === '1' || value === '0') { - transformedTags[key] = value === '1' - } - // If 1 or 0 is prefixed with 'n__' we need to remove it. This is to prevent - // casting the value to a boolean - else if (value === 'n__1' || value === 'n__0') { - transformedTags[key] = value === 'n__1' ? '1' : '0' - } - // Otherwise just use the value - else { - transformedTags[key] = value - } - } - - return transformedTags - } - - private transformFromRecordTagValues(tags: TagsBase): { [key: string]: string | undefined } { - const transformedTags: { [key: string]: string | undefined } = {} - - for (const [key, value] of Object.entries(tags)) { - // If the value is of type null we use the value undefined - // Indy doesn't support null as a value - if (value === null) { - transformedTags[key] = undefined - } - // If the value is a boolean use the indy - // '1' or '0' syntax - else if (typeof value === 'boolean') { - transformedTags[key] = value ? '1' : '0' - } - // If the value is 1 or 0, we need to add something to the value, otherwise - // the next time we deserialize the tag values it will be converted to boolean - else if (value === '1' || value === '0') { - transformedTags[key] = `n__${value}` - } - // If the value is an array we create a tag for each array - // item ("tagName:arrayItem" = "1") - else if (Array.isArray(value)) { - value.forEach((item) => { - const tagName = `${key}:${item}` - transformedTags[tagName] = '1' - }) - } - // Otherwise just use the value - else { - transformedTags[key] = value - } - } - - return transformedTags - } - - /** - * Transforms the search query into a wallet query compatible with indy WQL. - * - * The format used by Credo is almost the same as the indy query, with the exception of - * the encoding of values, however this is handled by the {@link IndyStorageService.transformToRecordTagValues} - * method. - */ - private indyQueryFromSearchQuery(query: Query): Record { - // eslint-disable-next-line prefer-const - let { $and, $or, $not, ...tags } = query - - $and = ($and as Query[] | undefined)?.map((q) => this.indyQueryFromSearchQuery(q)) - $or = ($or as Query[] | undefined)?.map((q) => this.indyQueryFromSearchQuery(q)) - $not = $not ? this.indyQueryFromSearchQuery($not as Query) : undefined - - const indyQuery = { - ...this.transformFromRecordTagValues(tags as unknown as TagsBase), - $and, - $or, - $not, - } - - return indyQuery - } - - private recordToInstance(record: WalletRecord, recordClass: BaseRecordConstructor): T { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const instance = JsonTransformer.deserialize(record.value!, recordClass) - instance.id = record.id - - const tags = record.tags ? this.transformToRecordTagValues(record.tags) : {} - instance.replaceTags(tags) - - return instance - } - - /** @inheritDoc */ - public async save(agentContext: AgentContext, record: T) { - assertIndySdkWallet(agentContext.wallet) - - record.updatedAt = new Date() - - const value = JsonTransformer.serialize(record) - const tags = this.transformFromRecordTagValues(record.getTags()) as Record - - try { - await this.indySdk.addWalletRecord(agentContext.wallet.handle, record.type, record.id, value, tags) - } catch (error) { - // Record already exists - if (isIndyError(error, 'WalletItemAlreadyExists')) { - throw new RecordDuplicateError(`Record with id ${record.id} already exists`, { recordType: record.type }) - } - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - /** @inheritDoc */ - public async update(agentContext: AgentContext, record: T): Promise { - assertIndySdkWallet(agentContext.wallet) - - record.updatedAt = new Date() - - const value = JsonTransformer.serialize(record) - const tags = this.transformFromRecordTagValues(record.getTags()) as Record - - try { - await this.indySdk.updateWalletRecordValue(agentContext.wallet.handle, record.type, record.id, value) - await this.indySdk.updateWalletRecordTags(agentContext.wallet.handle, record.type, record.id, tags) - } catch (error) { - // Record does not exist - if (isIndyError(error, 'WalletItemNotFound')) { - throw new RecordNotFoundError(`record with id ${record.id} not found.`, { - recordType: record.type, - cause: error, - }) - } - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - /** @inheritDoc */ - public async delete(agentContext: AgentContext, record: T) { - assertIndySdkWallet(agentContext.wallet) - - try { - await this.indySdk.deleteWalletRecord(agentContext.wallet.handle, record.type, record.id) - } catch (error) { - // Record does not exist - if (isIndyError(error, 'WalletItemNotFound')) { - throw new RecordNotFoundError(`record with id ${record.id} not found.`, { - recordType: record.type, - cause: error, - }) - } - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - /** @inheritDoc */ - public async deleteById( - agentContext: AgentContext, - recordClass: BaseRecordConstructor, - id: string - ): Promise { - assertIndySdkWallet(agentContext.wallet) - - try { - await this.indySdk.deleteWalletRecord(agentContext.wallet.handle, recordClass.type, id) - } catch (error) { - if (isIndyError(error, 'WalletItemNotFound')) { - throw new RecordNotFoundError(`record with id ${id} not found.`, { - recordType: recordClass.type, - cause: error, - }) - } - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - /** @inheritDoc */ - public async getById(agentContext: AgentContext, recordClass: BaseRecordConstructor, id: string): Promise { - assertIndySdkWallet(agentContext.wallet) - - try { - const record = await this.indySdk.getWalletRecord( - agentContext.wallet.handle, - recordClass.type, - id, - IndySdkStorageService.DEFAULT_QUERY_OPTIONS - ) - return this.recordToInstance(record, recordClass) - } catch (error) { - if (isIndyError(error, 'WalletItemNotFound')) { - throw new RecordNotFoundError(`record with id ${id} not found.`, { - recordType: recordClass.type, - cause: error, - }) - } - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - /** @inheritDoc */ - public async getAll(agentContext: AgentContext, recordClass: BaseRecordConstructor): Promise { - assertIndySdkWallet(agentContext.wallet) - - const recordIterator = this.search( - agentContext.wallet, - recordClass.type, - {}, - IndySdkStorageService.DEFAULT_QUERY_OPTIONS - ) - const records = [] - for await (const record of recordIterator) { - records.push(this.recordToInstance(record, recordClass)) - } - return records - } - - /** @inheritDoc */ - public async findByQuery( - agentContext: AgentContext, - recordClass: BaseRecordConstructor, - query: Query - ): Promise { - assertIndySdkWallet(agentContext.wallet) - - const indyQuery = this.indyQueryFromSearchQuery(query) - - const recordIterator = this.search( - agentContext.wallet, - recordClass.type, - indyQuery, - IndySdkStorageService.DEFAULT_QUERY_OPTIONS - ) - const records = [] - for await (const record of recordIterator) { - records.push(this.recordToInstance(record, recordClass)) - } - return records - } - - private async *search( - wallet: IndySdkWallet, - type: string, - query: WalletQuery, - { limit = Infinity, ...options }: WalletSearchOptions & { limit?: number } - ) { - try { - const searchHandle = await this.indySdk.openWalletSearch(wallet.handle, type, query, options) - - let records: WalletRecord[] = [] - - // Allow max of 256 per fetch operation - const chunk = limit ? Math.min(256, limit) : 256 - - // Loop while limit not reached (or no limit specified) - while (!limit || records.length < limit) { - // Retrieve records - const recordsJson = await this.indySdk.fetchWalletSearchNextRecords(wallet.handle, searchHandle, chunk) - - if (recordsJson.records) { - records = [...records, ...recordsJson.records] - - for (const record of recordsJson.records) { - yield record - } - } - - // If the number of records returned is less than chunk - // It means we reached the end of the iterator (no more records) - if (!records.length || !recordsJson.records || recordsJson.records.length < chunk) { - await this.indySdk.closeWalletSearch(searchHandle) - - return - } - } - } catch (error) { - throw new IndySdkError(error, `Searching '${type}' records for query '${JSON.stringify(query)}' failed`) - } - } -} diff --git a/packages/indy-sdk/src/storage/__tests__/IndySdkStorageService.test.ts b/packages/indy-sdk/src/storage/__tests__/IndySdkStorageService.test.ts deleted file mode 100644 index a6bac1e8de..0000000000 --- a/packages/indy-sdk/src/storage/__tests__/IndySdkStorageService.test.ts +++ /dev/null @@ -1,314 +0,0 @@ -import type { IndySdk } from '../../types' -import type { TagsBase } from '@credo-ts/core' - -import { RecordDuplicateError, RecordNotFoundError, SigningProviderRegistry } from '@credo-ts/core' -import * as indySdk from 'indy-sdk' - -import { TestRecord } from '../../../../core/src/storage/__tests__/TestRecord' -import { getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' -import { IndySdkWallet } from '../../wallet/IndySdkWallet' -import { IndySdkStorageService } from '../IndySdkStorageService' - -const agentConfig = getAgentConfig('IndySdkStorageServiceTest') -const wallet = new IndySdkWallet(indySdk, agentConfig.logger, new SigningProviderRegistry([])) - -const agentContext = getAgentContext({ - wallet, - agentConfig, -}) - -const storageService = new IndySdkStorageService(indySdk) -const startDate = Date.now() - -describe('IndySdkStorageService', () => { - beforeEach(async () => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await wallet.createAndOpen(agentConfig.walletConfig!) - }) - - afterEach(async () => { - await wallet.delete() - }) - - const insertRecord = async ({ id, tags }: { id?: string; tags?: TagsBase }) => { - const props = { - id, - foo: 'bar', - tags: tags ?? { myTag: 'foobar' }, - } - const record = new TestRecord(props) - await storageService.save(agentContext, record) - return record - } - - describe('tag transformation', () => { - it('should correctly transform tag values to string before storing', async () => { - const record = await insertRecord({ - id: 'test-id', - tags: { - someBoolean: true, - someOtherBoolean: false, - someStringValue: 'string', - anArrayValue: ['foo', 'bar'], - // booleans are stored as '1' and '0' so we store the string values '1' and '0' as 'n__1' and 'n__0' - someStringNumberValue: '1', - anotherStringNumberValue: '0', - }, - }) - - const retrieveRecord = await indySdk.getWalletRecord(wallet.handle, record.type, record.id, { - retrieveType: true, - retrieveTags: true, - }) - - expect(retrieveRecord.tags).toEqual({ - someBoolean: '1', - someOtherBoolean: '0', - someStringValue: 'string', - 'anArrayValue:foo': '1', - 'anArrayValue:bar': '1', - someStringNumberValue: 'n__1', - anotherStringNumberValue: 'n__0', - }) - }) - - it('should correctly transform tag values from string after retrieving', async () => { - await indySdk.addWalletRecord(wallet.handle, TestRecord.type, 'some-id', '{}', { - someBoolean: '1', - someOtherBoolean: '0', - someStringValue: 'string', - 'anArrayValue:foo': '1', - 'anArrayValue:bar': '1', - // booleans are stored as '1' and '0' so we store the string values '1' and '0' as 'n__1' and 'n__0' - someStringNumberValue: 'n__1', - anotherStringNumberValue: 'n__0', - }) - - const record = await storageService.getById(agentContext, TestRecord, 'some-id') - - expect(record.getTags()).toEqual({ - someBoolean: true, - someOtherBoolean: false, - someStringValue: 'string', - anArrayValue: expect.arrayContaining(['bar', 'foo']), - someStringNumberValue: '1', - anotherStringNumberValue: '0', - }) - }) - }) - - describe('save()', () => { - it('should throw RecordDuplicateError if a record with the id already exists', async () => { - const record = await insertRecord({ id: 'test-id' }) - - return expect(() => storageService.save(agentContext, record)).rejects.toThrowError(RecordDuplicateError) - }) - - it('should save the record', async () => { - const record = await insertRecord({ id: 'test-id' }) - const found = await storageService.getById(agentContext, TestRecord, 'test-id') - - expect(record).toEqual(found) - }) - - it('After a save the record should have update the updatedAt property', async () => { - const time = startDate - const record = await insertRecord({ id: 'test-updatedAt' }) - expect(record.updatedAt?.getTime()).toBeGreaterThan(time) - }) - }) - - describe('getById()', () => { - it('should throw RecordNotFoundError if the record does not exist', async () => { - return expect(() => storageService.getById(agentContext, TestRecord, 'does-not-exist')).rejects.toThrowError( - RecordNotFoundError - ) - }) - - it('should return the record by id', async () => { - const record = await insertRecord({ id: 'test-id' }) - const found = await storageService.getById(agentContext, TestRecord, 'test-id') - - expect(found).toEqual(record) - }) - }) - - describe('update()', () => { - it('should throw RecordNotFoundError if the record does not exist', async () => { - const record = new TestRecord({ - id: 'test-id', - foo: 'test', - tags: { some: 'tag' }, - }) - - return expect(() => storageService.update(agentContext, record)).rejects.toThrowError(RecordNotFoundError) - }) - - it('should update the record', async () => { - const record = await insertRecord({ id: 'test-id' }) - - record.replaceTags({ ...record.getTags(), foo: 'bar' }) - record.foo = 'foobaz' - await storageService.update(agentContext, record) - - const retrievedRecord = await storageService.getById(agentContext, TestRecord, record.id) - expect(retrievedRecord).toEqual(record) - }) - - it('After a record has been updated it should have updated the updatedAT property', async () => { - const time = startDate - const record = await insertRecord({ id: 'test-id' }) - - record.replaceTags({ ...record.getTags(), foo: 'bar' }) - record.foo = 'foobaz' - await storageService.update(agentContext, record) - - const retrievedRecord = await storageService.getById(agentContext, TestRecord, record.id) - expect(retrievedRecord.createdAt.getTime()).toBeGreaterThan(time) - }) - }) - - describe('delete()', () => { - it('should throw RecordNotFoundError if the record does not exist', async () => { - const record = new TestRecord({ - id: 'test-id', - foo: 'test', - tags: { some: 'tag' }, - }) - - return expect(() => storageService.delete(agentContext, record)).rejects.toThrowError(RecordNotFoundError) - }) - - it('should delete the record', async () => { - const record = await insertRecord({ id: 'test-id' }) - await storageService.delete(agentContext, record) - - return expect(() => storageService.getById(agentContext, TestRecord, record.id)).rejects.toThrowError( - RecordNotFoundError - ) - }) - }) - - describe('getAll()', () => { - it('should retrieve all records', async () => { - const createdRecords = await Promise.all( - Array(5) - .fill(undefined) - .map((_, index) => insertRecord({ id: `record-${index}` })) - ) - - const records = await storageService.getAll(agentContext, TestRecord) - - expect(records).toEqual(expect.arrayContaining(createdRecords)) - }) - }) - - describe('findByQuery()', () => { - it('should retrieve all records that match the query', async () => { - const expectedRecord = await insertRecord({ tags: { myTag: 'foobar' } }) - await insertRecord({ tags: { myTag: 'notfoobar' } }) - - const records = await storageService.findByQuery(agentContext, TestRecord, { myTag: 'foobar' }) - - expect(records.length).toBe(1) - expect(records[0]).toEqual(expectedRecord) - }) - - it('finds records using $and statements', async () => { - const expectedRecord = await insertRecord({ tags: { myTag: 'foo', anotherTag: 'bar' } }) - await insertRecord({ tags: { myTag: 'notfoobar' } }) - - const records = await storageService.findByQuery(agentContext, TestRecord, { - $and: [{ myTag: 'foo' }, { anotherTag: 'bar' }], - }) - - expect(records.length).toBe(1) - expect(records[0]).toEqual(expectedRecord) - }) - - it('finds records using $or statements', async () => { - const expectedRecord = await insertRecord({ tags: { myTag: 'foo' } }) - const expectedRecord2 = await insertRecord({ tags: { anotherTag: 'bar' } }) - await insertRecord({ tags: { myTag: 'notfoobar' } }) - - const records = await storageService.findByQuery(agentContext, TestRecord, { - $or: [{ myTag: 'foo' }, { anotherTag: 'bar' }], - }) - - expect(records.length).toBe(2) - expect(records).toEqual(expect.arrayContaining([expectedRecord, expectedRecord2])) - }) - - it('finds records using $not statements', async () => { - const expectedRecord = await insertRecord({ tags: { myTag: 'foo' } }) - const expectedRecord2 = await insertRecord({ tags: { anotherTag: 'bar' } }) - await insertRecord({ tags: { myTag: 'notfoobar' } }) - - const records = await storageService.findByQuery(agentContext, TestRecord, { - $not: { myTag: 'notfoobar' }, - }) - - expect(records.length).toBe(2) - expect(records).toEqual(expect.arrayContaining([expectedRecord, expectedRecord2])) - }) - - it('correctly transforms an advanced query into a valid WQL query', async () => { - const indySpy = jest.fn() - const storageServiceWithoutIndySdk = new IndySdkStorageService({ - openWalletSearch: indySpy, - fetchWalletSearchNextRecords: jest.fn(() => ({ records: undefined })), - closeWalletSearch: jest.fn(), - } as unknown as IndySdk) - - await storageServiceWithoutIndySdk.findByQuery(agentContext, TestRecord, { - $and: [ - { - $or: [{ myTag: true }, { myTag: false }], - }, - { - $and: [{ theNumber: '0' }, { theNumber: '1' }], - }, - ], - $or: [ - { - aValue: ['foo', 'bar'], - }, - ], - $not: { myTag: 'notfoobar' }, - }) - - const expectedQuery = { - $and: [ - { - $and: undefined, - $not: undefined, - $or: [ - { myTag: '1', $and: undefined, $or: undefined, $not: undefined }, - { myTag: '0', $and: undefined, $or: undefined, $not: undefined }, - ], - }, - { - $or: undefined, - $not: undefined, - $and: [ - { theNumber: 'n__0', $and: undefined, $or: undefined, $not: undefined }, - { theNumber: 'n__1', $and: undefined, $or: undefined, $not: undefined }, - ], - }, - ], - $or: [ - { - 'aValue:foo': '1', - 'aValue:bar': '1', - $and: undefined, - $or: undefined, - $not: undefined, - }, - ], - $not: { myTag: 'notfoobar', $and: undefined, $or: undefined, $not: undefined }, - } - - expect(indySpy).toBeCalledWith(expect.anything(), expect.anything(), expectedQuery, expect.anything()) - }) - }) -}) diff --git a/packages/indy-sdk/src/storage/index.ts b/packages/indy-sdk/src/storage/index.ts deleted file mode 100644 index ff59756cfa..0000000000 --- a/packages/indy-sdk/src/storage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './IndySdkStorageService' diff --git a/packages/indy-sdk/src/types.ts b/packages/indy-sdk/src/types.ts deleted file mode 100644 index f6ac41c161..0000000000 --- a/packages/indy-sdk/src/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { default as _IndySdk } from 'indy-sdk' - -type IndySdk = typeof _IndySdk - -export const IndySdkSymbol = Symbol('IndySdk') -export type { IndySdk } diff --git a/packages/indy-sdk/src/utils/__tests__/did.test.ts b/packages/indy-sdk/src/utils/__tests__/did.test.ts deleted file mode 100644 index 222f9898fd..0000000000 --- a/packages/indy-sdk/src/utils/__tests__/did.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { isAbbreviatedVerkey, isFullVerkey, isLegacySelfCertifiedDid } from '../did' - -const validAbbreviatedVerkeys = [ - '~PKAYz8Ev4yoQgr2LaMAWFx', - '~Soy1augaQrQYtNZRRHsikB', - '~BUF7uxYTxZ6qYdZ4G9e1Gi', - '~DbZ4gkBqhFRVsT5P7BJqyZ', - '~4zmNTdG78iYyMAQdEQLrf8', -] - -const invalidAbbreviatedVerkeys = [ - '6YnVN5Qdb6mqimTRQcQmSXrHXKdTEdRn5YHZReezUTvt', - '8jG2Bim1HNSybCTdKBRppP4PCQSSijx1pBnreqsdo8JG', - 'ABUF7uxYTxZ6qYdZ4G9e1Gi', - '~Db3IgkBqhFRVsT5P7BJqyZ', - '~4zmNTlG78iYyMAQdEQLrf8', -] - -const validFullVerkeys = [ - '6YnVN5Qdb6mqimTRQcQmSXrHXKdTEdRn5YHZReezUTvt', - '8jG2Bim1HNSybCTdKBRppP4PCQSSijx1pBnreqsdo8JG', - '9wMLhw9SSxtTUyosrndMbvWY4TtDbVvRnMtzG2NysniP', - '6m2XT39vivJ7tLSxNPM8siMnhYCZcdMxbkTcJDSzAQTu', - 'CAgL85iEecPNQMmxQ1hgbqczwq7SAerQ8RbWTRtC7SoK', - 'MqXmB7cTsTXqyxDPBbrgu5EPqw61kouK1qjMvnoPa96', -] - -const invalidFullVerkeys = [ - '~PKAYz8Ev4yoQgr2LaMAWFx', - '~Soy1augaQrQYtNZRRHsikB', - '6YnVN5Qdb6mqimTRQcQmSXrHXKdTEdRn5YHZReezUTvta', - '6m2XT39vIvJ7tLSxNPM8siMnhYCZcdMxbkTcJDSzAQTu', - 'CAgL85iEecPNQMlxQ1hgbqczwq7SAerQ8RbWTRtC7SoK', -] - -describe('Utils | Did', () => { - describe('isSelfCertifiedDid()', () => { - test('returns true if the verkey is abbreviated', () => { - expect(isLegacySelfCertifiedDid('PW8ZHpNupeWXbmpPWog6Ki', '~QQ5jiH1dgXPAnvHdJvazn9')).toBe(true) - }) - - test('returns true if the verkey is not abbreviated and the did is generated from the verkey', () => { - expect(isLegacySelfCertifiedDid('Y8q4Aq6gRAcmB6jjKk3Z7t', 'HyEoPRNvC7q4jj5joUo8AWYtxbNccbEnTAeuMYkpmNS2')).toBe( - true - ) - }) - - test('returns false if the verkey is not abbreviated and the did is not generated from the verkey', () => { - expect(isLegacySelfCertifiedDid('Y8q4Aq6gRAcmB6jjKk3Z7t', 'AcU7DnRqoXGYATD6VqsRq4eHuz55gdM3uzFBEhFd6rGh')).toBe( - false - ) - }) - }) - - describe('isAbbreviatedVerkey()', () => { - test.each(validAbbreviatedVerkeys)('returns true when valid abbreviated verkey "%s" is passed in', (verkey) => { - expect(isAbbreviatedVerkey(verkey)).toBe(true) - }) - - test.each(invalidAbbreviatedVerkeys)( - 'returns false when invalid abbreviated verkey "%s" is passed in', - (verkey) => { - expect(isAbbreviatedVerkey(verkey)).toBe(false) - } - ) - }) - - describe('isFullVerkey()', () => { - test.each(validFullVerkeys)('returns true when valid full verkey "%s" is passed in', (verkey) => { - expect(isFullVerkey(verkey)).toBe(true) - }) - - test.each(invalidFullVerkeys)('returns false when invalid full verkey "%s" is passed in', (verkey) => { - expect(isFullVerkey(verkey)).toBe(false) - }) - }) -}) diff --git a/packages/indy-sdk/src/utils/assertIndySdkWallet.ts b/packages/indy-sdk/src/utils/assertIndySdkWallet.ts deleted file mode 100644 index 4308926919..0000000000 --- a/packages/indy-sdk/src/utils/assertIndySdkWallet.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Wallet } from '@credo-ts/core' - -import { AriesFrameworkError } from '@credo-ts/core' - -import { IndySdkWallet } from '../wallet/IndySdkWallet' - -export function assertIndySdkWallet(wallet: Wallet): asserts wallet is IndySdkWallet { - if (!(wallet instanceof IndySdkWallet)) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const walletClassName = (wallet as any).constructor?.name ?? 'unknown' - throw new AriesFrameworkError(`Expected wallet to be instance of IndySdkWallet, found ${walletClassName}`) - } -} diff --git a/packages/indy-sdk/src/utils/did.ts b/packages/indy-sdk/src/utils/did.ts deleted file mode 100644 index d7dabf0023..0000000000 --- a/packages/indy-sdk/src/utils/did.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Based on DidUtils implementation in Aries Framework .NET - * @see: https://github.com/hyperledger/aries-framework-dotnet/blob/f90eaf9db8548f6fc831abea917e906201755763/src/Hyperledger.Aries/Utils/DidUtils.cs - * - * Some context about full verkeys versus abbreviated verkeys: - * A standard verkey is 32 bytes, and by default in Indy the DID is chosen as the first 16 bytes of that key, before base58 encoding. - * An abbreviated verkey replaces the first 16 bytes of the verkey with ~ when it matches the DID. - * - * When a full verkey is used to register on the ledger, this is stored as a full verkey on the ledger and also returned from the ledger as a full verkey. - * The same applies to an abbreviated verkey. If an abbreviated verkey is used to register on the ledger, this is stored as an abbreviated verkey on the ledger and also returned from the ledger as an abbreviated verkey. - * - * For this reason we need some methods to check whether verkeys are full or abbreviated, so we can align this with `indy.abbreviateVerkey` - * - * Aries Framework .NET also abbreviates verkey before sending to ledger: - * https://github.com/hyperledger/aries-framework-dotnet/blob/f90eaf9db8548f6fc831abea917e906201755763/src/Hyperledger.Aries/Ledger/DefaultLedgerService.cs#L139-L147 - */ - -import { Buffer, TypedArrayEncoder } from '@credo-ts/core' - -export const FULL_VERKEY_REGEX = /^[1-9A-HJ-NP-Za-km-z]{43,44}$/ -export const ABBREVIATED_VERKEY_REGEX = /^~[1-9A-HJ-NP-Za-km-z]{21,22}$/ - -/** - * Check whether the did is a self certifying did. If the verkey is abbreviated this method - * will always return true. Make sure that the verkey you pass in this method belongs to the - * did passed in - * - * @return Boolean indicating whether the did is self certifying - */ -export function isLegacySelfCertifiedDid(did: string, verkey: string): boolean { - // If the verkey is Abbreviated, it means the full verkey - // is the did + the verkey - if (isAbbreviatedVerkey(verkey)) { - return true - } - - const didFromVerkey = legacyIndyDidFromPublicKeyBase58(verkey) - - if (didFromVerkey === did) { - return true - } - - return false -} - -export function legacyIndyDidFromPublicKeyBase58(publicKeyBase58: string): string { - const buffer = TypedArrayEncoder.fromBase58(publicKeyBase58) - - const did = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) - - return did -} - -export function getFullVerkey(did: string, verkey: string) { - if (isFullVerkey(verkey)) return verkey - - // Did could have did:xxx prefix, only take the last item after : - const id = did.split(':').pop() ?? did - // Verkey is prefixed with ~ if abbreviated - const verkeyWithoutTilde = verkey.slice(1) - - // Create base58 encoded public key (32 bytes) - return TypedArrayEncoder.toBase58( - Buffer.concat([ - // Take did identifier (16 bytes) - TypedArrayEncoder.fromBase58(id), - // Concat the abbreviated verkey (16 bytes) - TypedArrayEncoder.fromBase58(verkeyWithoutTilde), - ]) - ) -} - -/** - * Check a base58 encoded string against a regex expression to determine if it is a full valid verkey - * @param verkey Base58 encoded string representation of a verkey - * @return Boolean indicating if the string is a valid verkey - */ -export function isFullVerkey(verkey: string): boolean { - return FULL_VERKEY_REGEX.test(verkey) -} - -/** - * Check a base58 encoded string against a regex expression to determine if it is a valid abbreviated verkey - * @param verkey Base58 encoded string representation of an abbreviated verkey - * @returns Boolean indicating if the string is a valid abbreviated verkey - */ -export function isAbbreviatedVerkey(verkey: string): boolean { - return ABBREVIATED_VERKEY_REGEX.test(verkey) -} diff --git a/packages/indy-sdk/src/utils/promises.ts b/packages/indy-sdk/src/utils/promises.ts deleted file mode 100644 index 0e843d73b5..0000000000 --- a/packages/indy-sdk/src/utils/promises.ts +++ /dev/null @@ -1,44 +0,0 @@ -// This file polyfills the allSettled method introduced in ESNext - -export type AllSettledFulfilled = { - status: 'fulfilled' - value: T -} - -export type AllSettledRejected = { - status: 'rejected' - // eslint-disable-next-line @typescript-eslint/no-explicit-any - reason: any -} - -export function allSettled(promises: Promise[]) { - return Promise.all( - promises.map((p) => - p - .then( - (value) => - ({ - status: 'fulfilled', - value, - } as AllSettledFulfilled) - ) - .catch( - (reason) => - ({ - status: 'rejected', - reason, - } as AllSettledRejected) - ) - ) - ) -} - -export function onlyFulfilled(entries: Array | AllSettledRejected>) { - // We filter for only the rejected values, so we can safely cast the type - return entries.filter((e) => e.status === 'fulfilled') as AllSettledFulfilled[] -} - -export function onlyRejected(entries: Array | AllSettledRejected>) { - // We filter for only the rejected values, so we can safely cast the type - return entries.filter((e) => e.status === 'rejected') as AllSettledRejected[] -} diff --git a/packages/indy-sdk/src/wallet/IndySdkWallet.ts b/packages/indy-sdk/src/wallet/IndySdkWallet.ts deleted file mode 100644 index 791b130dbf..0000000000 --- a/packages/indy-sdk/src/wallet/IndySdkWallet.ts +++ /dev/null @@ -1,645 +0,0 @@ -import type { - Buffer, - EncryptedMessage, - KeyDerivationMethod, - KeyPair, - UnpackedMessageContext, - Wallet, - WalletConfig, - WalletConfigRekey, - WalletCreateKeyOptions, - WalletExportImportConfig, - WalletSignOptions, - WalletVerifyOptions, -} from '@credo-ts/core' -import type { OpenWalletCredentials, WalletConfig as IndySdkWalletConfig, WalletStorageConfig } from 'indy-sdk' - -// eslint-disable-next-line import/order -import { - AriesFrameworkError, - InjectionSymbols, - isValidPrivateKey, - isValidSeed, - JsonEncoder, - Key, - KeyType, - Logger, - RecordNotFoundError, - SigningProviderRegistry, - TypedArrayEncoder, - WalletDuplicateError, - WalletError, - WalletExportPathExistsError, - WalletInvalidKeyError, - WalletKeyExistsError, - WalletNotFoundError, -} from '@credo-ts/core' - -const isError = (error: unknown): error is Error => error instanceof Error - -import { inject, injectable } from 'tsyringe' - -import { IndySdkError, isIndyError } from '../error' -import { IndySdk, IndySdkSymbol } from '../types' - -@injectable() -export class IndySdkWallet implements Wallet { - private walletConfig?: WalletConfig - private walletHandle?: number - - private logger: Logger - private signingKeyProviderRegistry: SigningProviderRegistry - private indySdk: IndySdk - - public constructor( - @inject(IndySdkSymbol) indySdk: IndySdk, - @inject(InjectionSymbols.Logger) logger: Logger, - signingKeyProviderRegistry: SigningProviderRegistry - ) { - this.logger = logger - this.signingKeyProviderRegistry = signingKeyProviderRegistry - this.indySdk = indySdk - } - - public get isProvisioned() { - return this.walletConfig !== undefined - } - - public get isInitialized() { - return this.walletHandle !== undefined - } - - public get handle() { - if (!this.walletHandle) { - throw new AriesFrameworkError( - 'Wallet has not been initialized yet. Make sure to await agent.initialize() before using the agent.' - ) - } - - return this.walletHandle - } - - public get supportedKeyTypes() { - const walletSupportedKeyTypes = [KeyType.Ed25519] - const signingKeyProviderSupportedKeyTypes = this.signingKeyProviderRegistry.supportedKeyTypes - - return Array.from(new Set([...walletSupportedKeyTypes, ...signingKeyProviderSupportedKeyTypes])) - } - - /** - * Dispose method is called when an agent context is disposed. - */ - public async dispose() { - if (this.isInitialized) { - await this.close() - } - } - - private walletStorageConfig(walletConfig: WalletConfig): IndySdkWalletConfig { - const walletStorageConfig: IndySdkWalletConfig = { - id: walletConfig.id, - storage_type: walletConfig.storage?.type, - } - - if (walletConfig.storage?.config) { - walletStorageConfig.storage_config = walletConfig.storage?.config as WalletStorageConfig - } - - return walletStorageConfig - } - - private walletCredentials( - walletConfig: WalletConfig, - rekey?: string, - rekeyDerivation?: KeyDerivationMethod - ): OpenWalletCredentials { - const walletCredentials: OpenWalletCredentials = { - key: walletConfig.key, - key_derivation_method: walletConfig.keyDerivationMethod, - } - if (rekey) { - walletCredentials.rekey = rekey - } - if (rekeyDerivation) { - walletCredentials.rekey_derivation_method = rekeyDerivation - } - if (walletConfig.storage?.credentials) { - walletCredentials.storage_credentials = walletConfig.storage?.credentials as Record - } - - return walletCredentials - } - - /** - * @throws {WalletDuplicateError} if the wallet already exists - * @throws {WalletError} if another error occurs - */ - public async create(walletConfig: WalletConfig): Promise { - await this.createAndOpen(walletConfig) - await this.close() - } - - /** - * @throws {WalletDuplicateError} if the wallet already exists - * @throws {WalletError} if another error occurs - */ - public async createAndOpen(walletConfig: WalletConfig): Promise { - this.logger.debug(`Creating wallet '${walletConfig.id}' using SQLite storage`) - - try { - await this.indySdk.createWallet(this.walletStorageConfig(walletConfig), this.walletCredentials(walletConfig)) - this.walletConfig = walletConfig - - await this.open(walletConfig) - } catch (error) { - if (isIndyError(error, 'WalletAlreadyExistsError')) { - const errorMessage = `Wallet '${walletConfig.id}' already exists` - this.logger.debug(errorMessage) - - throw new WalletDuplicateError(errorMessage, { - walletType: 'IndySdkWallet', - cause: error, - }) - } else { - if (!isError(error)) { - throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - const errorMessage = `Error creating wallet '${walletConfig.id}'` - this.logger.error(errorMessage, { - error, - errorMessage: error.message, - }) - - throw new WalletError(errorMessage, { cause: error }) - } - } - - this.logger.debug(`Successfully created wallet '${walletConfig.id}'`) - } - - /** - * @throws {WalletNotFoundError} if the wallet does not exist - * @throws {WalletError} if another error occurs - */ - public async open(walletConfig: WalletConfig): Promise { - await this._open(walletConfig) - } - - /** - * @throws {WalletNotFoundError} if the wallet does not exist - * @throws {WalletError} if another error occurs - */ - public async rotateKey(walletConfig: WalletConfigRekey): Promise { - if (!walletConfig.rekey) { - throw new WalletError('Wallet rekey undefined!. Please specify the new wallet key') - } - await this._open( - { - id: walletConfig.id, - key: walletConfig.key, - keyDerivationMethod: walletConfig.keyDerivationMethod, - }, - walletConfig.rekey, - walletConfig.rekeyDerivationMethod - ) - } - - /** - * @throws {WalletNotFoundError} if the wallet does not exist - * @throws {WalletError} if another error occurs - */ - private async _open( - walletConfig: WalletConfig, - rekey?: string, - rekeyDerivation?: KeyDerivationMethod - ): Promise { - if (this.walletHandle) { - throw new WalletError( - 'Wallet instance already opened. Close the currently opened wallet before re-opening the wallet' - ) - } - - try { - this.walletHandle = await this.indySdk.openWallet( - this.walletStorageConfig(walletConfig), - this.walletCredentials(walletConfig, rekey, rekeyDerivation) - ) - if (rekey) { - this.walletConfig = { ...walletConfig, key: rekey, keyDerivationMethod: rekeyDerivation } - } else { - this.walletConfig = walletConfig - } - } catch (error) { - if (isIndyError(error, 'WalletNotFoundError')) { - const errorMessage = `Wallet '${walletConfig.id}' not found` - this.logger.debug(errorMessage) - - throw new WalletNotFoundError(errorMessage, { - walletType: 'IndySdkWallet', - cause: error, - }) - } else if (isIndyError(error, 'WalletAccessFailed')) { - const errorMessage = `Incorrect key for wallet '${walletConfig.id}'` - this.logger.debug(errorMessage) - throw new WalletInvalidKeyError(errorMessage, { - walletType: 'IndySdkWallet', - cause: error, - }) - } else { - if (!isError(error)) { - throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - const errorMessage = `Error opening wallet '${walletConfig.id}': ${error.message}` - this.logger.error(errorMessage, { - error, - errorMessage: error.message, - }) - - throw new WalletError(errorMessage, { cause: error }) - } - } - - this.logger.debug(`Wallet '${walletConfig.id}' opened with handle '${this.handle}'`) - } - - /** - * @throws {WalletNotFoundError} if the wallet does not exist - * @throws {WalletError} if another error occurs - */ - public async delete(): Promise { - if (!this.walletConfig) { - throw new WalletError( - 'Can not delete wallet that does not have wallet config set. Make sure to call create wallet before deleting the wallet' - ) - } - - this.logger.info(`Deleting wallet '${this.walletConfig.id}'`) - - if (this.walletHandle) { - await this.close() - } - - try { - await this.indySdk.deleteWallet( - this.walletStorageConfig(this.walletConfig), - this.walletCredentials(this.walletConfig) - ) - } catch (error) { - if (isIndyError(error, 'WalletNotFoundError')) { - const errorMessage = `Error deleting wallet: wallet '${this.walletConfig.id}' not found` - this.logger.debug(errorMessage) - - throw new WalletNotFoundError(errorMessage, { - walletType: 'IndySdkWallet', - cause: error, - }) - } else { - if (!isError(error)) { - throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - const errorMessage = `Error deleting wallet '${this.walletConfig.id}': ${error.message}` - this.logger.error(errorMessage, { - error, - errorMessage: error.message, - }) - - throw new WalletError(errorMessage, { cause: error }) - } - } - } - - public async export(exportConfig: WalletExportImportConfig) { - try { - this.logger.debug(`Exporting wallet ${this.walletConfig?.id} to path ${exportConfig.path}`) - await this.indySdk.exportWallet(this.handle, exportConfig) - } catch (error) { - if (!isError(error)) { - throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - - // Export path already exists - if (isIndyError(error, 'CommonIOError')) { - throw new WalletExportPathExistsError( - `Unable to create export, wallet export at path '${exportConfig.path}' already exists`, - { cause: error } - ) - } - - const errorMessage = `Error exporting wallet: ${error.message}` - this.logger.error(errorMessage, { - error, - }) - - throw new WalletError(errorMessage, { cause: error }) - } - } - - public async import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig) { - try { - this.logger.debug(`Importing wallet ${walletConfig.id} from path ${importConfig.path}`) - await this.indySdk.importWallet( - { id: walletConfig.id }, - { key: walletConfig.key, key_derivation_method: walletConfig.keyDerivationMethod }, - importConfig - ) - } catch (error) { - if (!isError(error)) { - throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - const errorMessage = `Error importing wallet': ${error.message}` - this.logger.error(errorMessage, { - error, - }) - - throw new WalletError(errorMessage, { cause: error }) - } - } - - /** - * @throws {WalletError} if the wallet is already closed or another error occurs - */ - public async close(): Promise { - this.logger.debug(`Closing wallet ${this.walletConfig?.id}`) - if (!this.walletHandle) { - throw new WalletError('Wallet is in invalid state, you are trying to close wallet that has no `walletHandle`.') - } - - try { - await this.indySdk.closeWallet(this.walletHandle) - this.walletHandle = undefined - } catch (error) { - if (isIndyError(error, 'WalletInvalidHandle')) { - const errorMessage = `Error closing wallet: wallet already closed` - this.logger.debug(errorMessage) - - throw new WalletError(errorMessage, { - cause: error, - }) - } else { - if (!isError(error)) { - throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - const errorMessage = `Error closing wallet': ${error.message}` - this.logger.error(errorMessage, { - error, - errorMessage: error.message, - }) - - throw new WalletError(errorMessage, { cause: error }) - } - } - } - - /** - * Create a key with an optional private key and keyType. - * The keypair is also automatically stored in the wallet afterwards - * - * Bls12381g1g2 and X25519 are not supported. - */ - public async createKey({ seed, privateKey, keyType }: WalletCreateKeyOptions): Promise { - try { - if (seed && privateKey) { - throw new WalletError('Only one of seed and privateKey can be set') - } - - if (seed && !isValidSeed(seed, keyType)) { - throw new WalletError('Invalid seed provided') - } - - if (privateKey && !isValidPrivateKey(privateKey, keyType)) { - throw new WalletError('Invalid private key provided') - } - - // Ed25519 is supported natively in Indy wallet - if (keyType === KeyType.Ed25519) { - if (seed) { - throw new WalletError( - 'IndySdkWallet does not support seed. You may rather want to specify a private key for deterministic ed25519 key generation' - ) - } - try { - const verkey = await this.indySdk.createKey(this.handle, { - seed: privateKey?.toString(), - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - crypto_type: 'ed25519', - }) - - return Key.fromPublicKeyBase58(verkey, keyType) - } catch (error) { - // Handle case where key already exists - if (isIndyError(error, 'WalletItemAlreadyExists')) { - throw new WalletKeyExistsError('Key already exists') - } - - // Otherwise re-throw error - throw error - } - } - - // Check if there is a signing key provider for the specified key type. - if (this.signingKeyProviderRegistry.hasProviderForKeyType(keyType)) { - const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(keyType) - - const keyPair = await signingKeyProvider.createKeyPair({ seed, privateKey }) - await this.storeKeyPair(keyPair) - return Key.fromPublicKeyBase58(keyPair.publicKeyBase58, keyType) - } - } catch (error) { - // If already instance of `WalletError`, re-throw - if (error instanceof WalletError) throw error - - if (!isError(error)) { - throw new AriesFrameworkError(`Attempted to throw error, but it was not of type Error: ${error}`, { - cause: error, - }) - } - - throw new WalletError(`Error creating key with key type '${keyType}': ${error.message}`, { cause: error }) - } - - throw new WalletError(`Unsupported key type: '${keyType}' for wallet IndySdkWallet`) - } - - /** - * sign a Buffer with an instance of a Key class - * - * Bls12381g1g2, Bls12381g1 and X25519 are not supported. - * - * @param data Buffer The data that needs to be signed - * @param key Key The key that is used to sign the data - * - * @returns A signature for the data - */ - public async sign({ data, key }: WalletSignOptions): Promise { - try { - // Ed25519 is supported natively in Indy wallet - if (key.keyType === KeyType.Ed25519) { - // Checks to see if it is an not an Array of messages, but just a single one - if (!TypedArrayEncoder.isTypedArray(data)) { - throw new WalletError(`${KeyType.Ed25519} does not support multiple singing of multiple messages`) - } - return await this.indySdk.cryptoSign(this.handle, key.publicKeyBase58, data as Buffer) - } - - // Check if there is a signing key provider for the specified key type. - if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { - const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(key.keyType) - - const keyPair = await this.retrieveKeyPair(key.publicKeyBase58) - const signed = await signingKeyProvider.sign({ - data, - privateKeyBase58: keyPair.privateKeyBase58, - publicKeyBase58: key.publicKeyBase58, - }) - - return signed - } - } catch (error) { - if (!isError(error)) { - throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError(`Error signing data with verkey ${key.publicKeyBase58}`, { cause: error }) - } - throw new WalletError(`Unsupported keyType: ${key.keyType}`) - } - - /** - * Verify the signature with the data and the used key - * - * Bls12381g1g2, Bls12381g1 and X25519 are not supported. - * - * @param data Buffer The data that has to be confirmed to be signed - * @param key Key The key that was used in the signing process - * @param signature Buffer The signature that was created by the signing process - * - * @returns A boolean whether the signature was created with the supplied data and key - * - * @throws {WalletError} When it could not do the verification - * @throws {WalletError} When an unsupported keytype is used - */ - public async verify({ data, key, signature }: WalletVerifyOptions): Promise { - try { - // Ed25519 is supported natively in Indy wallet - if (key.keyType === KeyType.Ed25519) { - // Checks to see if it is an not an Array of messages, but just a single one - if (!TypedArrayEncoder.isTypedArray(data)) { - throw new WalletError(`${KeyType.Ed25519} does not support multiple singing of multiple messages`) - } - return await this.indySdk.cryptoVerify(key.publicKeyBase58, data as Buffer, signature) - } - - // Check if there is a signing key provider for the specified key type. - if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { - const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(key.keyType) - - const signed = await signingKeyProvider.verify({ - data, - signature, - publicKeyBase58: key.publicKeyBase58, - }) - - return signed - } - } catch (error) { - if (!isError(error)) { - throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError(`Error verifying signature of data signed with verkey ${key.publicKeyBase58}`, { - cause: error, - }) - } - throw new WalletError(`Unsupported keyType: ${key.keyType}`) - } - - public async pack( - payload: Record, - recipientKeys: string[], - senderVerkey?: string - ): Promise { - try { - const messageRaw = JsonEncoder.toBuffer(payload) - const packedMessage = await this.indySdk.packMessage(this.handle, messageRaw, recipientKeys, senderVerkey ?? null) - return JsonEncoder.fromBuffer(packedMessage) - } catch (error) { - if (!isError(error)) { - throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError('Error packing message', { cause: error }) - } - } - - public async unpack(messagePackage: EncryptedMessage): Promise { - try { - const unpackedMessageBuffer = await this.indySdk.unpackMessage(this.handle, JsonEncoder.toBuffer(messagePackage)) - const unpackedMessage = JsonEncoder.fromBuffer(unpackedMessageBuffer) - return { - senderKey: unpackedMessage.sender_verkey, - recipientKey: unpackedMessage.recipient_verkey, - plaintextMessage: JsonEncoder.fromString(unpackedMessage.message), - } - } catch (error) { - if (!isError(error)) { - throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError('Error unpacking message', { cause: error }) - } - } - - public async generateNonce(): Promise { - try { - return await this.indySdk.generateNonce() - } catch (error) { - if (!isError(error)) { - throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError('Error generating nonce', { cause: error }) - } - } - - private async retrieveKeyPair(publicKeyBase58: string): Promise { - try { - const { value } = await this.indySdk.getWalletRecord(this.handle, 'KeyPairRecord', `key-${publicKeyBase58}`, {}) - if (value) { - return JsonEncoder.fromString(value) as KeyPair - } else { - throw new WalletError(`No content found for record with public key: ${publicKeyBase58}`) - } - } catch (error) { - if (isIndyError(error, 'WalletItemNotFound')) { - throw new RecordNotFoundError(`KeyPairRecord not found for public key: ${publicKeyBase58}.`, { - recordType: 'KeyPairRecord', - cause: error, - }) - } - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - private async storeKeyPair(keyPair: KeyPair): Promise { - try { - await this.indySdk.addWalletRecord( - this.handle, - 'KeyPairRecord', - `key-${keyPair.publicKeyBase58}`, - JSON.stringify(keyPair), - { - keyType: keyPair.keyType, - } - ) - } catch (error) { - if (isIndyError(error, 'WalletItemAlreadyExists')) { - throw new WalletKeyExistsError('Key already exists') - } - throw isIndyError(error) ? new IndySdkError(error) : error - } - } - - public async generateWalletKey() { - try { - return await this.indySdk.generateWalletKey() - } catch (error) { - throw new WalletError('Error generating wallet key', { cause: error }) - } - } -} diff --git a/packages/indy-sdk/src/wallet/__tests__/IndySdkWallet.test.ts b/packages/indy-sdk/src/wallet/__tests__/IndySdkWallet.test.ts deleted file mode 100644 index b6b799fa2e..0000000000 --- a/packages/indy-sdk/src/wallet/__tests__/IndySdkWallet.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { SigningProvider, WalletConfig } from '@credo-ts/core' - -import { - Key, - WalletKeyExistsError, - KeyType, - SigningProviderRegistry, - TypedArrayEncoder, - KeyDerivationMethod, -} from '@credo-ts/core' -import indySdk from 'indy-sdk' - -import testLogger from '../../../../core/tests/logger' -import { IndySdkWallet } from '../IndySdkWallet' - -// use raw key derivation method to speed up wallet creating / opening / closing between tests -const walletConfig: WalletConfig = { - id: 'Wallet: IndySdkWalletTest', - // generated using indy.generateWalletKey - key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', - keyDerivationMethod: KeyDerivationMethod.Raw, -} - -const signingProvider = { - keyType: KeyType.X25519, - createKeyPair: () => Promise.resolve({ keyType: KeyType.X25519, privateKeyBase58: 'b', publicKeyBase58: 'a' }), -} satisfies Partial - -describe('IndySdkWallet', () => { - let indySdkWallet: IndySdkWallet - - const privateKey = TypedArrayEncoder.fromString('sample-seed') - const message = TypedArrayEncoder.fromString('sample-message') - - beforeEach(async () => { - indySdkWallet = new IndySdkWallet( - indySdk, - testLogger, - new SigningProviderRegistry([signingProvider as unknown as SigningProvider]) - ) - await indySdkWallet.createAndOpen(walletConfig) - }) - - afterEach(async () => { - await indySdkWallet.delete() - }) - - test('Get the wallet handle', () => { - expect(indySdkWallet.handle).toEqual(expect.any(Number)) - }) - - test('supportedKeyTypes', () => { - // indy supports ed25519, signing provider supports x25519 - expect(indySdkWallet.supportedKeyTypes).toEqual([KeyType.Ed25519, KeyType.X25519]) - }) - - test('Generate Nonce', async () => { - await expect(indySdkWallet.generateNonce()).resolves.toEqual(expect.any(String)) - }) - - test('Create ed25519 keypair from private key', async () => { - await expect( - indySdkWallet.createKey({ - privateKey: TypedArrayEncoder.fromString('2103de41b4ae37e8e28586d84a342b67'), - keyType: KeyType.Ed25519, - }) - ).resolves.toMatchObject({ - keyType: KeyType.Ed25519, - }) - }) - - test('throws WalletKeyExistsError when a key already exists', async () => { - const privateKey = TypedArrayEncoder.fromString('2103de41b4ae37e8e28586d84a342b68') - await expect(indySdkWallet.createKey({ privateKey, keyType: KeyType.Ed25519 })).resolves.toEqual(expect.any(Key)) - await expect(indySdkWallet.createKey({ privateKey, keyType: KeyType.Ed25519 })).rejects.toThrowError( - WalletKeyExistsError - ) - - // This should result in the signign provider being called twice, resulting in the record - // being stored twice - await expect(indySdkWallet.createKey({ privateKey, keyType: KeyType.X25519 })).resolves.toEqual(expect.any(Key)) - await expect(indySdkWallet.createKey({ privateKey, keyType: KeyType.X25519 })).rejects.toThrowError( - WalletKeyExistsError - ) - }) - - test('Fail to create ed25519 keypair from invalid private key', async () => { - await expect(indySdkWallet.createKey({ privateKey, keyType: KeyType.Ed25519 })).rejects.toThrowError( - /Invalid private key provided/ - ) - }) - - test('Fail to create x25519 keypair', async () => { - await expect(indySdkWallet.createKey({ keyType: KeyType.Bls12381g1 })).rejects.toThrowError(/Unsupported key type/) - }) - - test('Create a signature with a ed25519 keypair', async () => { - const ed25519Key = await indySdkWallet.createKey({ keyType: KeyType.Ed25519 }) - const signature = await indySdkWallet.sign({ - data: message, - key: ed25519Key, - }) - expect(signature.length).toStrictEqual(64) - }) - - test('Verify a signed message with a ed25519 publicKey', async () => { - const ed25519Key = await indySdkWallet.createKey({ keyType: KeyType.Ed25519 }) - const signature = await indySdkWallet.sign({ - data: message, - key: ed25519Key, - }) - await expect(indySdkWallet.verify({ key: ed25519Key, data: message, signature })).resolves.toStrictEqual(true) - }) -}) diff --git a/packages/indy-sdk/src/wallet/index.ts b/packages/indy-sdk/src/wallet/index.ts deleted file mode 100644 index b327ed63bf..0000000000 --- a/packages/indy-sdk/src/wallet/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { IndySdkWallet } from './IndySdkWallet' diff --git a/packages/indy-sdk/tests/__fixtures__/anoncreds.ts b/packages/indy-sdk/tests/__fixtures__/anoncreds.ts deleted file mode 100644 index eb978ec748..0000000000 --- a/packages/indy-sdk/tests/__fixtures__/anoncreds.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const credentialDefinitionValue = { - primary: { - n: '96517142458750088826087901549537285521906361834839650465292394026155791790248920518228426560592477800345470631128393537910767968076647428853737338120375137978526133371095345886547568849980095910835456337942570110635942227498396677781945046904040000347997661394155645138402989185582727368743644878567330299129483548946710969360956979880962101169330048328620192831242584775824654760726417810662811409929761424969870024291961980782988854217354212087291593903213167261779548063894662259300608395552269380441482047725811646638173390809967510159302372018819245039226007682154490256871635806558216146474297742733244470144481', - s: '20992997088800769394205042281221010730843336204635587269131066142238627416871294692123680065003125450990475247419429111144686875080339959479648984195457400282722471552678361441816569115316390063503704185107464429408708889920969284364549487320740759452356010336698287092961864738455949515401889999320804333605635972368885179914619910494573144273759358510644118555354521660927445864167887629319425342133470781407706668100509422240127902573158722086763638357241708157836231326104213948080124231104027985997092193458353052131052627451830345602820935886233072722689872803371231173593216542422645374438328309647440653637339', - r: { - master_secret: - '96243300745227716230048295249700256382424379142767068560156597061550615821183969840133023439359733351013932957841392861447122785423145599004240865527901625751619237368187131360686977600247815596986496835118582544022443932674638843143227258367859921648385998241629365673854479167826898057354386557912400420925145402535066400276579674049751639901555837852972622061540154688641944145082381483273814616102862399655638465723909813901943343059991047747289931252070264205125933226649905593045675877143065756794349492159868513288280364195700788501708587588090219665708038121636837649207584981238653023213330207384929738192210', - name: '73301750658973501389860306433954162777688414647250690792688553201037736559940890441467927863421690990807820789906540409252803697381653459639864945429958798104818241892796218340966964349674689564019059435289373607451125919476002261041343187491848656595845611576458601110066647002078334660251906541846222115184239401618625285703919125402959929850028352261117167621349930047514115676870868726855651130262227714591240534532398809967792128535084773798290351459391475237061458901325844643172504167457543287673202618731404966555015061917662865397763636445953946274068384614117513804834235388565249331682010365807270858083546', - }, - rctxt: - '37788128721284563440858950515231840450431543928224096081933216180465915572829884228780081835462293611329848268384962871736884632087015070623933628853658097637604059748079512999518737243304794110313829761155878287344472916564970806851294430356498883927870926898737394894892797927804721407643833828162246495645836390303263072281761384240973982733122383052566872688887552226083782030670443318152427129452272570595367287061688769394567289624972332234661767648489253220495098949161964171486245324730862072203259801377135500275012560207100571502032523912388082460843991502336467718632746396226650194750972544436894286230063', - z: '43785356695890052462955676926428400928903479009358861113206349419200366390858322895540291303484939601128045362682307382393826375825484851021601464391509750565285197155653613669680662395620338416776539485377195826876505126073018100680273457526216247879013350460071029101583221000647494610122617904515744711339846577920055655093367012508192004131719432915903924789974568341538556528133188398290594619318653419602058489178526243446782729272985727332736198326183868783570550373552407121582843992983431205917273352230155794805507408743590383242904107596623095433284330566906935063373759426916339149701872288610119965287995', - }, - revocation: { - g: '1 0A84C28144BC8B677839038FFFA824AB5ADE517F8DD4A89F092FAF9A3560C62D 1 00FD708E112EEA5D89AF9D0559795E6DBCF56D3B8CDF79EFF34A72EB741F896F 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', - g_dash: - '1 201F3E23CC7E9284F3EFCF9500F1E2537C398EAB2E94D2EB801AECC7FBFBDC01 1 08132C7723CF9861D4CC24B56555EF1CBD9AE746C97B3ADFA36C669F2DCE09B6 1 1B2397FB2A1ADE704E2A1E4C242612F4677F9F1BD09E6B14C2E77E25EDA4C62E 1 00CDC2CF5F278D699D52223577AB032C150A3CB4C8E8AB07AB9D592772910E95 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', - h: '1 072E0A505004F2F32B4210E72FA18A2ADF17F31479BD2059B7A8C0BA58F2ACB3 1 05C70F039E60317003C41C319753ECACC629791FDB06D6ADC5B06DD94501B973 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', - h0: '1 03CBE26D18118E9770D4A0B3E8607B3B3A8D3D3CA81FF8D41862430CC583156E 1 004A2A57E0A826AEFF007EDDAF89B02F054050843689167B10127FE9EDEEEDA9 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', - h1: '1 10C9F9DE537994E4FEF2625AFA78342C8A096238A875F6899DD500230E6022E5 1 0C0A88F53D020557377B4ED9C3826E9B8F918DD03E23B0F8ECD922F8333359D3 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', - h2: '1 017F748AEEC1DDE4E4C3FBAE771C041F0A6FAEAF34FD02AF773AC4B75025147B 1 1298DBD9A4BEE6AD54E060A57BCE932735B7738C30A9ADAEFE2F38E1858A0183 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', - htilde: - '1 0C471F0451D6AC352E28B6ECDE8D7233B75530AE59276DF0F4B9A8B0C5C7E5DB 1 24CE4461910AA5D60C09C24EE0FE51E1B1600D8BA6E483E9050EF897CA3E3C8A 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', - h_cap: - '1 225B2106DEBD353AABDFC4C7F7E8660D308FB514EA9DAE0533DDEB65CF796159 1 1F6093622F439FC22C64F157F4F35F7C592EC0169C6F0026BC44CD3E375974A7 1 142126FAC3657AD846D394E1F72FD01ECC15E84416713CD133980E324B24F4BC 1 0357995DBDCD4385E59E607761AB30AE8D9DDE005A777EE846EF51AE2816CD33 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', - u: '1 00D8DDC2EB6536CA320EE035D099937E59B11678162C1BFEB30C58FCA9F84650 1 1557A5B05A1A30D63322E187D323C9CA431BC5E811E68D4703933D9DDA26D299 1 10E8AB93AA87839B757521742EBA23C3B257C91F61A93D37AEC4C0A011B5F073 1 1DA65E40406A7875DA8CFCE9FD7F283145C166382A937B72819BDC335FE9A734 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', - pk: '1 1A7EBBE3E7F8ED50959851364B20997944FA8AE5E3FC0A2BB531BAA17179D320 1 02C55FE6F64A2A4FF49B37C513C39E56ECD565CFAD6CA46DC6D8095179351863 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', - y: '1 1BF97F07270EC21A89E43BCA645D86A755F846B547238F1DA379E088CDD9B40D 1 146BB00F56FFC0DEF6541CEB484C718559B398DB1547B52850E46B23144161F1 1 079A1BEF8DFFA4E6352F701D476664340E7FBE5D3F46B897412BD2B5F10E33D7 1 02FDC508AEF90FB11961AF332BE4037973C76B954FFA48848F7E0588E93FCA8C 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', - }, -} diff --git a/packages/indy-sdk/tests/indy-did-registrar.e2e.test.ts b/packages/indy-sdk/tests/indy-did-registrar.e2e.test.ts deleted file mode 100644 index 657f051675..0000000000 --- a/packages/indy-sdk/tests/indy-did-registrar.e2e.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { IndySdkIndyDidCreateOptions } from '../src' - -import { Agent, TypedArrayEncoder, convertPublicKeyToX25519, JsonTransformer } from '@credo-ts/core' -import { generateKeyPairFromSeed } from '@stablelib/ed25519' - -import { getAgentOptions, importExistingIndyDidFromPrivateKey, publicDidSeed } from '../../core/tests' -import { legacyIndyDidFromPublicKeyBase58 } from '../src/utils/did' - -import { getIndySdkModules } from './setupIndySdkModule' - -const agentOptions = getAgentOptions('Indy Sdk Indy Did Registrar', {}, getIndySdkModules()) -const agent = new Agent(agentOptions) - -describe('Indy SDK Indy Did Registrar', () => { - beforeAll(async () => { - await agent.initialize() - }) - - afterAll(async () => { - await agent.shutdown() - await agent.wallet.delete() - }) - - it('should create a did:indy did', async () => { - // Add existing endorser did to the wallet - const unqualifiedSubmitterDid = await importExistingIndyDidFromPrivateKey( - agent, - TypedArrayEncoder.fromString(publicDidSeed) - ) - - // Generate a seed and the indy did. This allows us to create a new did every time - // but still check if the created output document is as expected. - const privateKey = TypedArrayEncoder.fromString( - Array(32 + 1) - .join((Math.random().toString(36) + '00000000000000000').slice(2, 18)) - .slice(0, 32) - ) - - const publicKeyEd25519 = generateKeyPairFromSeed(privateKey).publicKey - const x25519PublicKeyBase58 = TypedArrayEncoder.toBase58(convertPublicKeyToX25519(publicKeyEd25519)) - const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(publicKeyEd25519) - const unqualifiedDid = legacyIndyDidFromPublicKeyBase58(ed25519PublicKeyBase58) - - const did = await agent.dids.create({ - method: 'indy', - options: { - submitterDid: `did:indy:pool:localtest:${unqualifiedSubmitterDid}`, - alias: 'Alias', - endpoints: { - endpoint: 'https://example.com/endpoint', - types: ['DIDComm', 'did-communication', 'endpoint'], - routingKeys: ['a-routing-key'], - }, - }, - secret: { - privateKey, - }, - }) - - expect(JsonTransformer.toJSON(did)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'finished', - did: `did:indy:pool:localtest:${unqualifiedDid}`, - didDocument: { - '@context': [ - 'https://w3id.org/did/v1', - 'https://w3id.org/security/suites/ed25519-2018/v1', - 'https://w3id.org/security/suites/x25519-2019/v1', - 'https://didcomm.org/messaging/contexts/v2', - ], - alsoKnownAs: undefined, - controller: undefined, - verificationMethod: [ - { - id: `did:indy:pool:localtest:${unqualifiedDid}#verkey`, - type: 'Ed25519VerificationKey2018', - controller: `did:indy:pool:localtest:${unqualifiedDid}`, - publicKeyBase58: ed25519PublicKeyBase58, - }, - { - id: `did:indy:pool:localtest:${unqualifiedDid}#key-agreement-1`, - type: 'X25519KeyAgreementKey2019', - controller: `did:indy:pool:localtest:${unqualifiedDid}`, - publicKeyBase58: x25519PublicKeyBase58, - }, - ], - service: [ - { - id: `did:indy:pool:localtest:${unqualifiedDid}#endpoint`, - serviceEndpoint: 'https://example.com/endpoint', - type: 'endpoint', - }, - { - accept: ['didcomm/aip2;env=rfc19'], - id: `did:indy:pool:localtest:${unqualifiedDid}#did-communication`, - priority: 0, - recipientKeys: [`did:indy:pool:localtest:${unqualifiedDid}#key-agreement-1`], - routingKeys: ['a-routing-key'], - serviceEndpoint: 'https://example.com/endpoint', - type: 'did-communication', - }, - { - accept: ['didcomm/v2'], - id: `did:indy:pool:localtest:${unqualifiedDid}#didcomm-1`, - routingKeys: ['a-routing-key'], - serviceEndpoint: 'https://example.com/endpoint', - type: 'DIDComm', - }, - ], - authentication: [`did:indy:pool:localtest:${unqualifiedDid}#verkey`], - assertionMethod: undefined, - keyAgreement: [`did:indy:pool:localtest:${unqualifiedDid}#key-agreement-1`], - capabilityInvocation: undefined, - capabilityDelegation: undefined, - id: `did:indy:pool:localtest:${unqualifiedDid}`, - }, - secret: { - privateKey, - }, - }, - }) - }) -}) diff --git a/packages/indy-sdk/tests/indy-did-resolver.e2e.test.ts b/packages/indy-sdk/tests/indy-did-resolver.e2e.test.ts deleted file mode 100644 index 2afd057288..0000000000 --- a/packages/indy-sdk/tests/indy-did-resolver.e2e.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { IndySdkIndyDidCreateOptions } from '../src' - -import { Agent, AriesFrameworkError, JsonTransformer, TypedArrayEncoder } from '@credo-ts/core' - -import { getAgentOptions, importExistingIndyDidFromPrivateKey, publicDidSeed } from '../../core/tests/helpers' - -import { getIndySdkModules } from './setupIndySdkModule' - -const agent = new Agent(getAgentOptions('Indy SDK Indy DID resolver', {}, getIndySdkModules())) - -describe('Indy SDK Indy DID resolver', () => { - beforeAll(async () => { - await agent.initialize() - }) - - afterAll(async () => { - await agent.shutdown() - await agent.wallet.delete() - }) - - it('should resolve a did:indy did', async () => { - // Add existing endorser did to the wallet - const unqualifiedSubmitterDid = await importExistingIndyDidFromPrivateKey( - agent, - TypedArrayEncoder.fromString(publicDidSeed) - ) - - const createResult = await agent.dids.create({ - method: 'indy', - options: { - submitterDid: `did:indy:pool:localtest:${unqualifiedSubmitterDid}`, - alias: 'Alias', - role: 'TRUSTEE', - endpoints: { - endpoint: 'http://localhost:3000', - }, - }, - }) - - // Terrible, but the did can't be immediately resolved, so we need to wait a bit - await new Promise((res) => setTimeout(res, 1000)) - - if (!createResult.didState.did) throw new AriesFrameworkError('Unable to register did') - - const didResult = await agent.dids.resolve(createResult.didState.did) - - expect(JsonTransformer.toJSON(didResult)).toMatchObject({ - didDocument: { - '@context': [ - 'https://w3id.org/did/v1', - 'https://w3id.org/security/suites/ed25519-2018/v1', - 'https://w3id.org/security/suites/x25519-2019/v1', - ], - id: createResult.didState.did, - alsoKnownAs: undefined, - controller: undefined, - verificationMethod: [ - { - type: 'Ed25519VerificationKey2018', - controller: createResult.didState.did, - id: `${createResult.didState.did}#verkey`, - publicKeyBase58: expect.any(String), - }, - { - controller: createResult.didState.did, - type: 'X25519KeyAgreementKey2019', - id: `${createResult.didState.did}#key-agreement-1`, - publicKeyBase58: expect.any(String), - }, - ], - capabilityDelegation: undefined, - capabilityInvocation: undefined, - authentication: [`${createResult.didState.did}#verkey`], - assertionMethod: undefined, - keyAgreement: [`${createResult.didState.did}#key-agreement-1`], - service: [ - { - id: `${createResult.didState.did}#endpoint`, - serviceEndpoint: 'http://localhost:3000', - type: 'endpoint', - }, - { - id: `${createResult.didState.did}#did-communication`, - accept: ['didcomm/aip2;env=rfc19'], - priority: 0, - recipientKeys: [`${createResult.didState.did}#key-agreement-1`], - routingKeys: [], - serviceEndpoint: 'http://localhost:3000', - type: 'did-communication', - }, - ], - }, - didDocumentMetadata: {}, - didResolutionMetadata: { - contentType: 'application/did+ld+json', - }, - }) - }) -}) diff --git a/packages/indy-sdk/tests/indy-sdk-anoncreds-registry.e2e.test.ts b/packages/indy-sdk/tests/indy-sdk-anoncreds-registry.e2e.test.ts deleted file mode 100644 index 256168ff94..0000000000 --- a/packages/indy-sdk/tests/indy-sdk-anoncreds-registry.e2e.test.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { Agent, Key, KeyType, TypedArrayEncoder } from '@credo-ts/core' - -import { - agentDependencies, - getAgentConfig, - importExistingIndyDidFromPrivateKey, - publicDidSeed, -} from '../../core/tests/helpers' -import { IndySdkAnonCredsRegistry } from '../src/anoncreds/services/IndySdkAnonCredsRegistry' -import { IndySdkPoolService } from '../src/ledger' - -import { credentialDefinitionValue } from './__fixtures__/anoncreds' -import { getIndySdkModules, indySdk } from './setupIndySdkModule' - -const agentConfig = getAgentConfig('IndySdkAnonCredsRegistry') -const indySdkModules = getIndySdkModules() - -const agent = new Agent({ - config: agentConfig, - dependencies: agentDependencies, - modules: indySdkModules, -}) - -const indySdkAnonCredsRegistry = new IndySdkAnonCredsRegistry() -const indySdkPoolService = agent.dependencyManager.resolve(IndySdkPoolService) -const pool = indySdkPoolService.getPoolForNamespace('pool:localtest') - -describe('IndySdkAnonCredsRegistry', () => { - beforeAll(async () => { - await agent.initialize() - - // We need to import the endorser did/key into the wallet - await importExistingIndyDidFromPrivateKey(agent, TypedArrayEncoder.fromString(publicDidSeed)) - }) - - afterAll(async () => { - await agent.shutdown() - await agent.wallet.delete() - }) - - // TODO: use different issuer for schema and credential definition to catch possible bugs - // One test as the credential definition depends on the schema - test('register and resolve a schema and credential definition', async () => { - const dynamicVersion = `1.${Math.random() * 100}` - - const legacyIssuerId = 'TL1EaPFCZ8Si5aUrqScBDt' - const signingKey = Key.fromPublicKeyBase58('FMGcFuU3QwAQLywxvmEnSorQT3NwU9wgDMMTaDFtvswm', KeyType.Ed25519) - const didIndyIssuerId = 'did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt' - - const legacySchemaId = `TL1EaPFCZ8Si5aUrqScBDt:2:test:${dynamicVersion}` - const didIndySchemaId = `did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt/anoncreds/v0/SCHEMA/test/${dynamicVersion}` - - const schemaResult = await indySdkAnonCredsRegistry.registerSchema(agent.context, { - schema: { - attrNames: ['name'], - issuerId: didIndyIssuerId, - name: 'test', - version: dynamicVersion, - }, - options: {}, - }) - - expect(schemaResult).toMatchObject({ - schemaState: { - state: 'finished', - schema: { - attrNames: ['name'], - issuerId: didIndyIssuerId, - name: 'test', - version: dynamicVersion, - }, - schemaId: didIndySchemaId, - }, - registrationMetadata: {}, - schemaMetadata: { - indyLedgerSeqNo: expect.any(Number), - }, - }) - - // Wait some time before resolving credential definition object - await new Promise((res) => setTimeout(res, 1000)) - - // Resolve using legacy schema id - const legacySchema = await indySdkAnonCredsRegistry.getSchema(agent.context, legacySchemaId) - expect(legacySchema).toMatchObject({ - schema: { - attrNames: ['name'], - name: 'test', - version: dynamicVersion, - issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', - }, - schemaId: `TL1EaPFCZ8Si5aUrqScBDt:2:test:${dynamicVersion}`, - resolutionMetadata: {}, - schemaMetadata: { - didIndyNamespace: 'pool:localtest', - indyLedgerSeqNo: expect.any(Number), - }, - }) - - // Resolve using did indy schema id - const didIndySchema = await indySdkAnonCredsRegistry.getSchema(agent.context, didIndySchemaId) - expect(didIndySchema).toMatchObject({ - schema: { - attrNames: ['name'], - name: 'test', - version: dynamicVersion, - issuerId: didIndyIssuerId, - }, - schemaId: didIndySchemaId, - resolutionMetadata: {}, - schemaMetadata: { - didIndyNamespace: 'pool:localtest', - indyLedgerSeqNo: expect.any(Number), - }, - }) - - const legacyCredentialDefinitionId = `TL1EaPFCZ8Si5aUrqScBDt:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG` - const didIndyCredentialDefinitionId = `did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt/anoncreds/v0/CLAIM_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG` - const credentialDefinitionResult = await indySdkAnonCredsRegistry.registerCredentialDefinition(agent.context, { - credentialDefinition: { - issuerId: didIndyIssuerId, - tag: 'TAG', - schemaId: didIndySchemaId, - type: 'CL', - value: credentialDefinitionValue, - }, - options: {}, - }) - - expect(credentialDefinitionResult).toMatchObject({ - credentialDefinitionMetadata: {}, - credentialDefinitionState: { - credentialDefinition: { - issuerId: didIndyIssuerId, - tag: 'TAG', - schemaId: didIndySchemaId, - type: 'CL', - value: credentialDefinitionValue, - }, - credentialDefinitionId: didIndyCredentialDefinitionId, - state: 'finished', - }, - registrationMetadata: {}, - }) - - // Wait some time before resolving credential definition object - await new Promise((res) => setTimeout(res, 1000)) - - // Resolve using legacy credential definition id - const legacyCredentialDefinition = await indySdkAnonCredsRegistry.getCredentialDefinition( - agent.context, - legacyCredentialDefinitionId - ) - expect(legacyCredentialDefinition).toMatchObject({ - credentialDefinitionId: legacyCredentialDefinitionId, - credentialDefinition: { - issuerId: legacyIssuerId, - schemaId: legacySchemaId, - tag: 'TAG', - type: 'CL', - value: credentialDefinitionValue, - }, - credentialDefinitionMetadata: { - didIndyNamespace: 'pool:localtest', - }, - resolutionMetadata: {}, - }) - - // resolve using did indy credential definition id - const didIndyCredentialDefinition = await indySdkAnonCredsRegistry.getCredentialDefinition( - agent.context, - didIndyCredentialDefinitionId - ) - - expect(didIndyCredentialDefinition).toMatchObject({ - credentialDefinitionId: didIndyCredentialDefinitionId, - credentialDefinition: { - issuerId: didIndyIssuerId, - schemaId: didIndySchemaId, - tag: 'TAG', - type: 'CL', - value: credentialDefinitionValue, - }, - credentialDefinitionMetadata: { - didIndyNamespace: 'pool:localtest', - }, - resolutionMetadata: {}, - }) - - // We don't support creating a revocation registry using Credo yet, so we directly use indy-sdk to register the revocation registry - const legacyRevocationRegistryId = `TL1EaPFCZ8Si5aUrqScBDt:4:TL1EaPFCZ8Si5aUrqScBDt:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG:CL_ACCUM:tag` - const didIndyRevocationRegistryId = `did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt/anoncreds/v0/REV_REG_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG/tag` - const revocationRegistryRequest = await indySdk.buildRevocRegDefRequest('TL1EaPFCZ8Si5aUrqScBDt', { - id: legacyRevocationRegistryId, - credDefId: legacyCredentialDefinitionId, - revocDefType: 'CL_ACCUM', - tag: 'tag', - value: { - issuanceType: 'ISSUANCE_BY_DEFAULT', - maxCredNum: 100, - publicKeys: { - accumKey: { - z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', - }, - }, - tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - tailsLocation: '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - }, - ver: '1.0', - }) - - await indySdkPoolService.submitWriteRequest(agent.context, pool, revocationRegistryRequest, signingKey) - - // indySdk.buildRevRegEntry panics, so we just pass a custom request directly - const entryResponse = await indySdkPoolService.submitWriteRequest( - agent.context, - pool, - { - identifier: legacyIssuerId, - operation: { - revocDefType: 'CL_ACCUM', - revocRegDefId: legacyRevocationRegistryId, - type: '114', - value: { - accum: - '1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000', - }, - }, - protocolVersion: 2, - reqId: Math.floor(Math.random() * 1000000), - }, - signingKey - ) - - const legacyRevocationRegistryDefinition = await indySdkAnonCredsRegistry.getRevocationRegistryDefinition( - agent.context, - legacyRevocationRegistryId - ) - expect(legacyRevocationRegistryDefinition).toMatchObject({ - revocationRegistryDefinitionId: legacyRevocationRegistryId, - revocationRegistryDefinition: { - issuerId: legacyIssuerId, - revocDefType: 'CL_ACCUM', - value: { - maxCredNum: 100, - tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - tailsLocation: - '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - publicKeys: { - accumKey: { - z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', - }, - }, - }, - tag: 'tag', - credDefId: legacyCredentialDefinitionId, - }, - revocationRegistryDefinitionMetadata: { - issuanceType: 'ISSUANCE_BY_DEFAULT', - didIndyNamespace: 'pool:localtest', - }, - resolutionMetadata: {}, - }) - - const didIndyRevocationRegistryDefinition = await indySdkAnonCredsRegistry.getRevocationRegistryDefinition( - agent.context, - didIndyRevocationRegistryId - ) - expect(didIndyRevocationRegistryDefinition).toMatchObject({ - revocationRegistryDefinitionId: didIndyRevocationRegistryId, - revocationRegistryDefinition: { - issuerId: didIndyIssuerId, - revocDefType: 'CL_ACCUM', - value: { - maxCredNum: 100, - tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - tailsLocation: - '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - publicKeys: { - accumKey: { - z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', - }, - }, - }, - tag: 'tag', - credDefId: didIndyCredentialDefinitionId, - }, - revocationRegistryDefinitionMetadata: { - issuanceType: 'ISSUANCE_BY_DEFAULT', - didIndyNamespace: 'pool:localtest', - }, - resolutionMetadata: {}, - }) - - const legacyRevocationStatusList = await indySdkAnonCredsRegistry.getRevocationStatusList( - agent.context, - legacyRevocationRegistryId, - entryResponse.result.txnMetadata.txnTime - ) - - expect(legacyRevocationStatusList).toMatchObject({ - resolutionMetadata: {}, - revocationStatusList: { - issuerId: legacyIssuerId, - currentAccumulator: - '1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000', - revRegDefId: legacyRevocationRegistryId, - revocationList: [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - timestamp: entryResponse.result.txnMetadata.txnTime, - }, - revocationStatusListMetadata: { - didIndyNamespace: 'pool:localtest', - }, - }) - - const didIndyRevocationStatusList = await indySdkAnonCredsRegistry.getRevocationStatusList( - agent.context, - didIndyRevocationRegistryId, - entryResponse.result.txnMetadata.txnTime - ) - - expect(didIndyRevocationStatusList).toMatchObject({ - resolutionMetadata: {}, - revocationStatusList: { - issuerId: didIndyIssuerId, - currentAccumulator: - '1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000', - revRegDefId: didIndyRevocationRegistryId, - revocationList: [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - timestamp: entryResponse.result.txnMetadata.txnTime, - }, - revocationStatusListMetadata: { - didIndyNamespace: 'pool:localtest', - }, - }) - }) -}) diff --git a/packages/indy-sdk/tests/postgres.e2e.test.ts b/packages/indy-sdk/tests/postgres.e2e.test.ts deleted file mode 100644 index a59359f9d8..0000000000 --- a/packages/indy-sdk/tests/postgres.e2e.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' -import type { ConnectionRecord } from '../../core/src/modules/connections' -import type { IndySdkPostgresStorageConfig } from '../../node/src' - -import { Subject } from 'rxjs' - -import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' -import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' -import { Agent } from '../../core/src/agent/Agent' -import { HandshakeProtocol } from '../../core/src/modules/connections' -import { waitForBasicMessage, getPostgresAgentOptions } from '../../core/tests/helpers' -import { loadIndySdkPostgresPlugin, IndySdkPostgresWalletScheme } from '../../node/src' - -import { getIndySdkModules } from './setupIndySdkModule' - -const alicePostgresAgentOptions = getPostgresAgentOptions( - 'AgentsAlice', - { - endpoints: ['rxjs:alice'], - }, - getIndySdkModules() -) - -const bobPostgresAgentOptions = getPostgresAgentOptions( - 'AgentsBob', - { - endpoints: ['rxjs:bob'], - }, - getIndySdkModules() -) - -describe('postgres agents', () => { - let aliceAgent: Agent - let bobAgent: Agent - let aliceConnection: ConnectionRecord - - afterAll(async () => { - await bobAgent.shutdown() - await bobAgent.wallet.delete() - await aliceAgent.shutdown() - await aliceAgent.wallet.delete() - }) - - test('make a connection between postgres agents', async () => { - const aliceMessages = new Subject() - const bobMessages = new Subject() - - const subjectMap = { - 'rxjs:alice': aliceMessages, - 'rxjs:bob': bobMessages, - } - - const storageConfig: IndySdkPostgresStorageConfig = { - type: 'postgres_storage', - config: { - url: 'localhost:5432', - wallet_scheme: IndySdkPostgresWalletScheme.DatabasePerWallet, - }, - credentials: { - account: 'postgres', - password: 'postgres', - admin_account: 'postgres', - admin_password: 'postgres', - }, - } - - // loading the postgres wallet plugin - loadIndySdkPostgresPlugin(storageConfig.config, storageConfig.credentials) - - aliceAgent = new Agent(alicePostgresAgentOptions) - aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) - aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) - await aliceAgent.initialize() - - bobAgent = new Agent(bobPostgresAgentOptions) - bobAgent.registerInboundTransport(new SubjectInboundTransport(bobMessages)) - bobAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) - await bobAgent.initialize() - - const aliceBobOutOfBandRecord = await aliceAgent.oob.createInvitation({ - handshakeProtocols: [HandshakeProtocol.Connections], - }) - - const { connectionRecord: bobConnectionAtBobAlice } = await bobAgent.oob.receiveInvitation( - aliceBobOutOfBandRecord.outOfBandInvitation - ) - await bobAgent.connections.returnWhenIsConnected(bobConnectionAtBobAlice!.id) - - const [aliceConnectionAtAliceBob] = await aliceAgent.connections.findAllByOutOfBandId(aliceBobOutOfBandRecord.id) - aliceConnection = await aliceAgent.connections.returnWhenIsConnected(aliceConnectionAtAliceBob!.id) - }) - - test('send a message to connection', async () => { - const message = 'hello, world' - await aliceAgent.basicMessages.sendMessage(aliceConnection.id, message) - - const basicMessage = await waitForBasicMessage(bobAgent, { - content: message, - }) - - expect(basicMessage.content).toBe(message) - }) - - test('can shutdown and re-initialize the same postgres agent', async () => { - expect(aliceAgent.isInitialized).toBe(true) - await aliceAgent.shutdown() - expect(aliceAgent.isInitialized).toBe(false) - await aliceAgent.initialize() - expect(aliceAgent.isInitialized).toBe(true) - }) -}) diff --git a/packages/indy-sdk/tests/setup.ts b/packages/indy-sdk/tests/setup.ts deleted file mode 100644 index 34e38c9705..0000000000 --- a/packages/indy-sdk/tests/setup.ts +++ /dev/null @@ -1 +0,0 @@ -jest.setTimeout(120000) diff --git a/packages/indy-sdk/tests/setupIndySdkModule.ts b/packages/indy-sdk/tests/setupIndySdkModule.ts deleted file mode 100644 index 2cc5a4f988..0000000000 --- a/packages/indy-sdk/tests/setupIndySdkModule.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { DidsModule, utils } from '@credo-ts/core' -import indySdk from 'indy-sdk' - -import { genesisPath, taaVersion, taaAcceptanceMechanism } from '../../core/tests/helpers' -import { - IndySdkModule, - IndySdkModuleConfig, - IndySdkIndyDidRegistrar, - IndySdkSovDidResolver, - IndySdkIndyDidResolver, -} from '../src' - -export { indySdk } - -export const getIndySdkModuleConfig = () => - new IndySdkModuleConfig({ - indySdk, - networks: [ - { - id: `localhost-${utils.uuid()}`, - isProduction: false, - genesisPath, - indyNamespace: 'pool:localtest', - transactionAuthorAgreement: { version: taaVersion, acceptanceMechanism: taaAcceptanceMechanism }, - }, - ], - }) - -export const getIndySdkModules = () => ({ - indySdk: new IndySdkModule(getIndySdkModuleConfig()), - dids: new DidsModule({ - registrars: [new IndySdkIndyDidRegistrar()], - resolvers: [new IndySdkSovDidResolver(), new IndySdkIndyDidResolver()], - }), -}) diff --git a/packages/indy-sdk/tests/sov-did-resolver.e2e.test.ts b/packages/indy-sdk/tests/sov-did-resolver.e2e.test.ts deleted file mode 100644 index 7cceab928a..0000000000 --- a/packages/indy-sdk/tests/sov-did-resolver.e2e.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { IndySdkIndyDidCreateOptions } from '../src' - -import { parseIndyDid } from '@credo-ts/anoncreds' -import { Agent, AriesFrameworkError, JsonTransformer, TypedArrayEncoder } from '@credo-ts/core' - -import { getAgentOptions, importExistingIndyDidFromPrivateKey, publicDidSeed } from '../../core/tests/helpers' - -import { getIndySdkModules } from './setupIndySdkModule' - -const agent = new Agent(getAgentOptions('Indy SDK Sov DID resolver', {}, getIndySdkModules())) - -describe('Indy SDK Sov DID resolver', () => { - beforeAll(async () => { - await agent.initialize() - }) - - afterAll(async () => { - await agent.shutdown() - await agent.wallet.delete() - }) - - test('resolve a did:sov did', async () => { - // Add existing endorser did to the wallet - const unqualifiedSubmitterDid = await importExistingIndyDidFromPrivateKey( - agent, - TypedArrayEncoder.fromString(publicDidSeed) - ) - - const createResult = await agent.dids.create({ - method: 'indy', - options: { - submitterDid: `did:indy:pool:localtest:${unqualifiedSubmitterDid}`, - alias: 'Alias', - role: 'TRUSTEE', - endpoints: { - endpoint: 'http://localhost:3000', - }, - }, - }) - - // Terrible, but the did can't be immediately resolved, so we need to wait a bit - await new Promise((res) => setTimeout(res, 1000)) - - if (!createResult.didState.did) throw new AriesFrameworkError('Unable to register did') - - const { namespaceIdentifier } = parseIndyDid(createResult.didState.did) - const sovDid = `did:sov:${namespaceIdentifier}` - const didResult = await agent.dids.resolve(sovDid) - - expect(JsonTransformer.toJSON(didResult)).toMatchObject({ - didDocument: { - '@context': [ - 'https://w3id.org/did/v1', - 'https://w3id.org/security/suites/ed25519-2018/v1', - 'https://w3id.org/security/suites/x25519-2019/v1', - ], - id: sovDid, - alsoKnownAs: undefined, - controller: undefined, - verificationMethod: [ - { - type: 'Ed25519VerificationKey2018', - controller: sovDid, - id: `${sovDid}#key-1`, - publicKeyBase58: expect.any(String), - }, - { - controller: sovDid, - type: 'X25519KeyAgreementKey2019', - id: `${sovDid}#key-agreement-1`, - publicKeyBase58: expect.any(String), - }, - ], - capabilityDelegation: undefined, - capabilityInvocation: undefined, - authentication: [`${sovDid}#key-1`], - assertionMethod: [`${sovDid}#key-1`], - keyAgreement: [`${sovDid}#key-agreement-1`], - service: [ - { - id: `${sovDid}#endpoint`, - serviceEndpoint: 'http://localhost:3000', - type: 'endpoint', - }, - { - id: `${sovDid}#did-communication`, - accept: ['didcomm/aip2;env=rfc19'], - priority: 0, - recipientKeys: [`${sovDid}#key-agreement-1`], - routingKeys: [], - serviceEndpoint: 'http://localhost:3000', - type: 'did-communication', - }, - ], - }, - didDocumentMetadata: {}, - didResolutionMetadata: { - contentType: 'application/did+ld+json', - }, - }) - }) -}) diff --git a/packages/indy-sdk/tsconfig.build.json b/packages/indy-sdk/tsconfig.build.json deleted file mode 100644 index 2b75d0adab..0000000000 --- a/packages/indy-sdk/tsconfig.build.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfig.build.json", - "compilerOptions": { - "outDir": "./build" - }, - "include": ["src/**/*"] -} diff --git a/packages/indy-sdk/tsconfig.json b/packages/indy-sdk/tsconfig.json deleted file mode 100644 index 46efe6f721..0000000000 --- a/packages/indy-sdk/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "types": ["jest"] - } -} diff --git a/packages/indy-vdr/src/IndyVdrModule.ts b/packages/indy-vdr/src/IndyVdrModule.ts index c9593c1be8..85e48bc1af 100644 --- a/packages/indy-vdr/src/IndyVdrModule.ts +++ b/packages/indy-vdr/src/IndyVdrModule.ts @@ -23,7 +23,7 @@ export class IndyVdrModule implements Module { dependencyManager .resolve(AgentConfig) .logger.warn( - "The '@credo-ts/indy-vdr' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + "The '@credo-ts/indy-vdr' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." ) // Config diff --git a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts index 7be534ad28..3d93942bd4 100644 --- a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts +++ b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts @@ -478,11 +478,11 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { revocationRegistryDefinitionId: string ): Promise { try { - const indySdkPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) const { did, namespaceIdentifier, credentialDefinitionTag, revocationRegistryTag, schemaSeqNo } = parseIndyRevocationRegistryId(revocationRegistryDefinitionId) - const { pool } = await indySdkPoolService.getPoolForDid(agentContext, did) + const { pool } = await indyVdrPoolService.getPoolForDid(agentContext, did) agentContext.config.logger.debug( `Using ledger '${pool.indyNamespace}' to retrieve revocation registry definition '${revocationRegistryDefinitionId}'` diff --git a/packages/indy-vdr/src/anoncreds/utils/identifiers.ts b/packages/indy-vdr/src/anoncreds/utils/identifiers.ts index caac46d966..7681a51327 100644 --- a/packages/indy-vdr/src/anoncreds/utils/identifiers.ts +++ b/packages/indy-vdr/src/anoncreds/utils/identifiers.ts @@ -1,8 +1,3 @@ -/** - * NOTE: this file is availalbe in both the indy-sdk and indy-vdr packages. If making changes to - * this file, make sure to update both files if applicable. - */ - import { unqualifiedSchemaIdRegex, unqualifiedCredentialDefinitionIdRegex, diff --git a/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidRegistrar.test.ts b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidRegistrar.test.ts index 271e568e25..866f8e7880 100644 --- a/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidRegistrar.test.ts +++ b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidRegistrar.test.ts @@ -14,15 +14,14 @@ import { Key, KeyType, RepositoryEventTypes, - SigningProviderRegistry, TypedArrayEncoder, VerificationMethod, } from '@credo-ts/core' import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' -import { agentDependencies, getAgentConfig, getAgentContext, indySdk, mockProperty } from '../../../../core/tests' -import { IndySdkWallet } from '../../../../indy-sdk/src' +import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' +import { agentDependencies, getAgentConfig, getAgentContext, mockProperty } from '../../../../core/tests' import { IndyVdrPool, IndyVdrPoolService } from '../../pool' import { IndyVdrIndyDidRegistrar } from '../IndyVdrIndyDidRegistrar' @@ -32,8 +31,7 @@ const poolMock = new IndyVdrPoolMock() mockProperty(poolMock, 'indyNamespace', 'ns1') const agentConfig = getAgentConfig('IndyVdrIndyDidRegistrar') - -const wallet = new IndySdkWallet(indySdk, agentConfig.logger, new SigningProviderRegistry([])) +const wallet = new InMemoryWallet() jest .spyOn(wallet, 'createKey') diff --git a/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts index dd0b3e1e6a..211066e7f6 100644 --- a/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts @@ -8,9 +8,7 @@ import { import { Agent, DidsModule, TypedArrayEncoder } from '@credo-ts/core' import { indyVdr } from '@hyperledger/indy-vdr-nodejs' -import { agentDependencies, getAgentConfig, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' -import { IndySdkModule } from '../../indy-sdk/src' -import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' +import { getInMemoryAgentOptions, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' import { IndyVdrIndyDidResolver, IndyVdrModule, IndyVdrSovDidResolver } from '../src' import { IndyVdrAnonCredsRegistry } from '../src/anoncreds/IndyVdrAnonCredsRegistry' import { IndyVdrIndyDidRegistrar } from '../src/dids/IndyVdrIndyDidRegistrar' @@ -19,49 +17,45 @@ import { IndyVdrPoolService } from '../src/pool' import { credentialDefinitionValue, revocationRegistryDefinitionValue } from './__fixtures__/anoncreds' import { indyVdrModuleConfig } from './helpers' -const endorserConfig = getAgentConfig('IndyVdrAnonCredsRegistryEndorser') -const agentConfig = getAgentConfig('IndyVdrAnonCredsRegistryAgent') - const indyVdrAnonCredsRegistry = new IndyVdrAnonCredsRegistry() -const endorser = new Agent({ - config: endorserConfig, - dependencies: agentDependencies, - modules: { - indyVdr: new IndyVdrModule({ - indyVdr, - networks: indyVdrModuleConfig.networks, - }), - indySdk: new IndySdkModule({ - indySdk, - }), - dids: new DidsModule({ - registrars: [new IndyVdrIndyDidRegistrar()], - resolvers: [new IndyVdrSovDidResolver(), new IndyVdrIndyDidResolver()], - }), - }, -}) - -const agent = new Agent({ - config: agentConfig, - dependencies: agentDependencies, - modules: { - indyVdr: new IndyVdrModule({ - indyVdr, - networks: indyVdrModuleConfig.networks, - }), - indySdk: new IndySdkModule({ - indySdk, - }), - dids: new DidsModule({ - registrars: [new IndyVdrIndyDidRegistrar()], - resolvers: [new IndyVdrSovDidResolver(), new IndyVdrIndyDidResolver()], - }), - }, -}) +const endorser = new Agent( + getInMemoryAgentOptions( + 'IndyVdrAnonCredsRegistryEndorser', + {}, + { + indyVdr: new IndyVdrModule({ + indyVdr, + networks: indyVdrModuleConfig.networks, + }), + dids: new DidsModule({ + registrars: [new IndyVdrIndyDidRegistrar()], + resolvers: [new IndyVdrSovDidResolver(), new IndyVdrIndyDidResolver()], + }), + } + ) +) + +const agent = new Agent( + getInMemoryAgentOptions( + 'IndyVdrAnonCredsRegistryAgent', + {}, + { + indyVdr: new IndyVdrModule({ + indyVdr, + networks: indyVdrModuleConfig.networks, + }), + dids: new DidsModule({ + registrars: [new IndyVdrIndyDidRegistrar()], + resolvers: [new IndyVdrSovDidResolver(), new IndyVdrIndyDidResolver()], + }), + } + ) +) const indyVdrPoolService = endorser.dependencyManager.resolve(IndyVdrPoolService) +// FIXME: this test is very slow, probably due to the sleeps. Can we speed it up? describe('IndyVdrAnonCredsRegistry', () => { let endorserDid: string beforeAll(async () => { diff --git a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts index c8ea5738a9..9cd8063c7e 100644 --- a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts @@ -15,10 +15,11 @@ import { import { indyVdr } from '@hyperledger/indy-vdr-nodejs' import { convertPublicKeyToX25519, generateKeyPairFromSeed } from '@stablelib/ed25519' -import { sleep } from '../../core/src/utils/sleep' -import { getAgentOptions, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' -import { IndySdkModule } from '../../indy-sdk/src' -import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' +import { + getInMemoryAgentOptions, + importExistingIndyDidFromPrivateKey, + retryUntilResult, +} from '../../core/tests/helpers' import { IndyVdrModule, IndyVdrSovDidResolver } from '../src' import { IndyVdrIndyDidRegistrar } from '../src/dids/IndyVdrIndyDidRegistrar' import { IndyVdrIndyDidResolver } from '../src/dids/IndyVdrIndyDidResolver' @@ -27,7 +28,7 @@ import { indyDidFromNamespaceAndInitialKey } from '../src/dids/didIndyUtil' import { indyVdrModuleConfig } from './helpers' const endorser = new Agent( - getAgentOptions( + getInMemoryAgentOptions( 'Indy VDR Indy DID Registrar', {}, { @@ -35,9 +36,6 @@ const endorser = new Agent( networks: indyVdrModuleConfig.networks, indyVdr, }), - indySdk: new IndySdkModule({ - indySdk, - }), dids: new DidsModule({ registrars: [new IndyVdrIndyDidRegistrar()], resolvers: [new IndyVdrIndyDidResolver(), new IndyVdrSovDidResolver()], @@ -46,7 +44,7 @@ const endorser = new Agent( ) ) const agent = new Agent( - getAgentOptions( + getInMemoryAgentOptions( 'Indy VDR Indy DID Registrar', {}, { @@ -54,9 +52,6 @@ const agent = new Agent( indyVdr, networks: indyVdrModuleConfig.networks, }), - indySdk: new IndySdkModule({ - indySdk, - }), dids: new DidsModule({ registrars: [new IndyVdrIndyDidRegistrar()], resolvers: [new IndyVdrIndyDidResolver(), new IndyVdrSovDidResolver()], @@ -125,33 +120,31 @@ describe('Indy VDR Indy Did Registrar', () => { const did = didRegistrationResult.didState.did if (!did) throw Error('did not defined') - // Wait some time pass to let ledger settle the object - await sleep(1000) - - const didResolutionResult = await endorser.dids.resolve(did) - expect(JsonTransformer.toJSON(didResolutionResult)).toMatchObject({ - didDocument: { - '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], - id: did, - alsoKnownAs: undefined, - controller: undefined, - verificationMethod: [ - { - type: 'Ed25519VerificationKey2018', - controller: did, - id: `${did}#verkey`, - publicKeyBase58: expect.any(String), - }, - ], - capabilityDelegation: undefined, - capabilityInvocation: undefined, - authentication: [`${did}#verkey`], - service: undefined, - }, - didDocumentMetadata: {}, - didResolutionMetadata: { - contentType: 'application/did+ld+json', - }, + // Tries to call it in an interval until it succeeds (with maxAttempts) + // As the ledger write is not always consistent in how long it takes + // to write the data, we need to retry until we get a result. + const didDocument = await retryUntilResult(async () => { + const result = await endorser.dids.resolve(did) + return result.didDocument + }) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject({ + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: expect.any(String), + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + service: undefined, }) }) @@ -210,6 +203,9 @@ describe('Indy VDR Indy Did Registrar', () => { secret: didState.secret, }) + if (didCreateSubmitResult.didState.state !== 'finished') { + throw new Error(`Unexpected did state, ${JSON.stringify(didCreateSubmitResult.didState, null, 2)}`) + } expect(JsonTransformer.toJSON(didCreateSubmitResult)).toMatchObject({ didDocumentMetadata: {}, didRegistrationMetadata: {}, @@ -237,33 +233,31 @@ describe('Indy VDR Indy Did Registrar', () => { }, }) - // Wait some time pass to let ledger settle the object - await sleep(1000) - - const didResult = await endorser.dids.resolve(did) - expect(JsonTransformer.toJSON(didResult)).toMatchObject({ - didDocument: { - '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], - id: did, - alsoKnownAs: undefined, - controller: undefined, - verificationMethod: [ - { - type: 'Ed25519VerificationKey2018', - controller: did, - id: `${did}#verkey`, - publicKeyBase58: ed25519PublicKeyBase58, - }, - ], - capabilityDelegation: undefined, - capabilityInvocation: undefined, - authentication: [`${did}#verkey`], - service: undefined, - }, - didDocumentMetadata: {}, - didResolutionMetadata: { - contentType: 'application/did+ld+json', - }, + // Tries to call it in an interval until it succeeds (with maxAttempts) + // As the ledger write is not always consistent in how long it takes + // to write the data, we need to retry until we get a result. + const didDocument = await retryUntilResult(async () => { + const result = await endorser.dids.resolve(did) + return result.didDocument + }) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject({ + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: ed25519PublicKeyBase58, + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + service: undefined, }) }) @@ -317,33 +311,31 @@ describe('Indy VDR Indy Did Registrar', () => { }, }) - // Wait some time pass to let ledger settle the object - await sleep(1000) - - const didResult = await endorser.dids.resolve(did) - expect(JsonTransformer.toJSON(didResult)).toMatchObject({ - didDocument: { - '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], - id: did, - alsoKnownAs: undefined, - controller: undefined, - verificationMethod: [ - { - type: 'Ed25519VerificationKey2018', - controller: did, - id: `${did}#verkey`, - publicKeyBase58: ed25519PublicKeyBase58, - }, - ], - capabilityDelegation: undefined, - capabilityInvocation: undefined, - authentication: [`${did}#verkey`], - service: undefined, - }, - didDocumentMetadata: {}, - didResolutionMetadata: { - contentType: 'application/did+ld+json', - }, + // Tries to call it in an interval until it succeeds (with maxAttempts) + // As the ledger write is not always consistent in how long it takes + // to write the data, we need to retry until we get a result. + const didDocument = await retryUntilResult(async () => { + const result = await endorser.dids.resolve(did) + return result.didDocument + }) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject({ + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + id: did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: did, + id: `${did}#verkey`, + publicKeyBase58: ed25519PublicKeyBase58, + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${did}#verkey`], + service: undefined, }) }) @@ -458,17 +450,15 @@ describe('Indy VDR Indy Did Registrar', () => { }, }) - // Wait some time pass to let ledger settle the object - await sleep(1000) - - const didResult = await endorser.dids.resolve(did) - expect(JsonTransformer.toJSON(didResult)).toMatchObject({ - didDocument: expectedDidDocument, - didDocumentMetadata: {}, - didResolutionMetadata: { - contentType: 'application/did+ld+json', - }, + // Tries to call it in an interval until it succeeds (with maxAttempts) + // As the ledger write is not always consistent in how long it takes + // to write the data, we need to retry until we get a result. + const didDocument = await retryUntilResult(async () => { + const result = await endorser.dids.resolve(did) + return result.didDocument }) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject(expectedDidDocument) }) test('can register an endorsed did:indy with services - did and verkey specified - using attrib endpoint', async () => { @@ -598,6 +588,10 @@ describe('Indy VDR Indy Did Registrar', () => { ], } + if (didCreateSubmitResult.didState.state !== 'finished') { + throw new Error(`Unexpected did state, ${JSON.stringify(didCreateSubmitResult.didState, null, 2)}`) + } + expect(JsonTransformer.toJSON(didCreateSubmitResult)).toMatchObject({ didDocumentMetadata: {}, didRegistrationMetadata: {}, @@ -607,16 +601,15 @@ describe('Indy VDR Indy Did Registrar', () => { didDocument: expectedDidDocument, }, }) - // Wait some time pass to let ledger settle the object - await sleep(1000) - const didResult = await endorser.dids.resolve(did) - expect(JsonTransformer.toJSON(didResult)).toMatchObject({ - didDocument: expectedDidDocument, - didDocumentMetadata: {}, - didResolutionMetadata: { - contentType: 'application/did+ld+json', - }, + // Tries to call it in an interval until it succeeds (with maxAttempts) + // As the ledger write is not always consistent in how long it takes + // to write the data, we need to retry until we get a result. + const didDocument = await retryUntilResult(async () => { + const result = await endorser.dids.resolve(did) + return result.didDocument }) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject(expectedDidDocument) }) }) diff --git a/packages/indy-vdr/tests/indy-vdr-indy-did-resolver.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-indy-did-resolver.e2e.test.ts index b344de08b5..1d7a8f9373 100644 --- a/packages/indy-vdr/tests/indy-vdr-indy-did-resolver.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-indy-did-resolver.e2e.test.ts @@ -1,16 +1,14 @@ import { DidsModule, Agent, TypedArrayEncoder, JsonTransformer } from '@credo-ts/core' import { indyVdr } from '@hyperledger/indy-vdr-nodejs' -import { getAgentOptions, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' -import { IndySdkModule } from '../../indy-sdk/src' -import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' +import { getInMemoryAgentOptions, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' import { IndyVdrModule } from '../src' import { IndyVdrIndyDidRegistrar, IndyVdrIndyDidResolver, IndyVdrSovDidResolver } from '../src/dids' import { createDidOnLedger, indyVdrModuleConfig } from './helpers' const agent = new Agent( - getAgentOptions( + getInMemoryAgentOptions( 'Indy VDR Indy DID resolver', {}, { @@ -18,9 +16,6 @@ const agent = new Agent( indyVdr, networks: indyVdrModuleConfig.networks, }), - indySdk: new IndySdkModule({ - indySdk, - }), dids: new DidsModule({ registrars: [new IndyVdrIndyDidRegistrar()], resolvers: [new IndyVdrIndyDidResolver(), new IndyVdrSovDidResolver()], diff --git a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts index adff66cdd8..ba23c7b384 100644 --- a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts @@ -1,12 +1,11 @@ import type { Key } from '@credo-ts/core' -import { TypedArrayEncoder, KeyType, SigningProviderRegistry } from '@credo-ts/core' +import { TypedArrayEncoder, KeyType } from '@credo-ts/core' import { GetNymRequest, NymRequest, SchemaRequest, CredentialDefinitionRequest } from '@hyperledger/indy-vdr-shared' +import { InMemoryWallet } from '../../../tests/InMemoryWallet' import { genesisTransactions, getAgentConfig, getAgentContext } from '../../core/tests/helpers' import testLogger from '../../core/tests/logger' -import { IndySdkWallet } from '../../indy-sdk/src' -import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' import { IndyVdrPool } from '../src/pool' import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' import { indyDidFromPublicKeyBase58 } from '../src/utils/did' @@ -14,7 +13,7 @@ import { indyDidFromPublicKeyBase58 } from '../src/utils/did' import { indyVdrModuleConfig } from './helpers' const indyVdrPoolService = new IndyVdrPoolService(testLogger, indyVdrModuleConfig) -const wallet = new IndySdkWallet(indySdk, testLogger, new SigningProviderRegistry([])) +const wallet = new InMemoryWallet() const agentConfig = getAgentConfig('IndyVdrPoolService') const agentContext = getAgentContext({ wallet, agentConfig }) diff --git a/packages/indy-vdr/tests/indy-vdr-sov-did-resolver.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-sov-did-resolver.e2e.test.ts index af73ef9d60..2088c50d51 100644 --- a/packages/indy-vdr/tests/indy-vdr-sov-did-resolver.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-sov-did-resolver.e2e.test.ts @@ -2,16 +2,14 @@ import { parseIndyDid } from '@credo-ts/anoncreds' import { DidsModule, Agent, TypedArrayEncoder, JsonTransformer } from '@credo-ts/core' import { indyVdr } from '@hyperledger/indy-vdr-nodejs' -import { getAgentOptions, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' -import { IndySdkModule } from '../../indy-sdk/src' -import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' +import { getInMemoryAgentOptions, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' import { IndyVdrModule } from '../src' import { IndyVdrIndyDidRegistrar, IndyVdrIndyDidResolver, IndyVdrSovDidResolver } from '../src/dids' import { createDidOnLedger, indyVdrModuleConfig } from './helpers' const agent = new Agent( - getAgentOptions( + getInMemoryAgentOptions( 'Indy VDR Sov DID resolver', {}, { @@ -19,9 +17,6 @@ const agent = new Agent( indyVdr, networks: indyVdrModuleConfig.networks, }), - indySdk: new IndySdkModule({ - indySdk, - }), dids: new DidsModule({ registrars: [new IndyVdrIndyDidRegistrar()], resolvers: [new IndyVdrIndyDidResolver(), new IndyVdrSovDidResolver()], diff --git a/packages/node/src/PostgresPlugin.ts b/packages/node/src/PostgresPlugin.ts deleted file mode 100644 index 22478d2b0a..0000000000 --- a/packages/node/src/PostgresPlugin.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Library } from '@2060.io/ffi-napi' -import { types } from '@2060.io/ref-napi' -import fs from 'fs' -import os from 'os' -import path from 'path' - -const LIBNAME = 'indystrgpostgres' -const ENV_VAR = 'LIB_INDY_STRG_POSTGRES' - -type Platform = 'darwin' | 'linux' | 'win32' - -type ExtensionMap = Record - -const extensions: ExtensionMap = { - darwin: { prefix: 'lib', extension: '.dylib' }, - linux: { prefix: 'lib', extension: '.so' }, - win32: { extension: '.dll' }, -} - -const libPaths: Record> = { - darwin: ['/usr/local/lib/', '/usr/lib/', '/opt/homebrew/opt/'], - linux: ['/usr/lib/', '/usr/local/lib/'], - win32: ['c:\\windows\\system32\\'], -} - -// Alias for a simple function to check if the path exists -const doesPathExist = fs.existsSync - -const getLibrary = () => { - // Detect OS; darwin, linux and windows are only supported - const platform = os.platform() - - if (platform !== 'linux' && platform !== 'win32' && platform !== 'darwin') - throw new Error(`Unsupported platform: ${platform}. linux, win32 and darwin are supported.`) - - // Get a potential path from the environment variable - const pathFromEnvironment = process.env[ENV_VAR] - - // Get the paths specific to the users operating system - const platformPaths = libPaths[platform] - - // Check if the path from the environment variable is supplied and add it - // We use unshift here so that when we want to get a valid library path this will be the first to resolve - if (pathFromEnvironment) platformPaths.unshift(pathFromEnvironment) - - // Create the path + file - const libraries = platformPaths.map((p) => - path.join(p, `${extensions[platform].prefix ?? ''}${LIBNAME}${extensions[platform].extension}`) - ) - - // Gaurd so we quit if there is no valid path for the library - if (!libraries.some(doesPathExist)) - throw new Error(`Could not find ${LIBNAME} with these paths: ${libraries.join(' ')}`) - - // Get the first valid library - // Casting here as a string because there is a guard of none of the paths - // would be valid - const validLibraryPath = libraries.find((l) => doesPathExist(l)) as string - - return Library(validLibraryPath, { - postgresstorage_init: [types.int, []], - init_storagetype: [types.int, ['string', 'string']], - }) -} - -type NativeIndyPostgres = { - postgresstorage_init: () => number - init_storagetype: (arg0: string, arg1: string) => number -} - -let indyPostgresStorage: NativeIndyPostgres | undefined - -export interface IndySdkPostgresWalletStorageConfig { - url: string - wallet_scheme: IndySdkPostgresWalletScheme - path?: string -} - -export interface IndySdkPostgresWalletStorageCredentials { - account: string - password: string - admin_account: string - admin_password: string -} - -export enum IndySdkPostgresWalletScheme { - DatabasePerWallet = 'DatabasePerWallet', - MultiWalletSingleTable = 'MultiWalletSingleTable', - MultiWalletSingleTableSharedPool = 'MultiWalletSingleTableSharedPool', -} - -export interface IndySdkPostgresStorageConfig { - type: 'postgres_storage' - config: IndySdkPostgresWalletStorageConfig - credentials: IndySdkPostgresWalletStorageCredentials -} - -export function loadIndySdkPostgresPlugin( - config: IndySdkPostgresWalletStorageConfig, - credentials: IndySdkPostgresWalletStorageCredentials -) { - if (!indyPostgresStorage) { - indyPostgresStorage = getLibrary() - } - - indyPostgresStorage.postgresstorage_init() - indyPostgresStorage.init_storagetype(JSON.stringify(config), JSON.stringify(credentials)) -} diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index a4f231c5da..18d6dfd6a5 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -4,7 +4,6 @@ import { EventEmitter } from 'events' import WebSocket from 'ws' import { NodeFileSystem } from './NodeFileSystem' -import { IndySdkPostgresStorageConfig, loadIndySdkPostgresPlugin, IndySdkPostgresWalletScheme } from './PostgresPlugin' import { HttpInboundTransport } from './transport/HttpInboundTransport' import { WsInboundTransport } from './transport/WsInboundTransport' @@ -15,11 +14,4 @@ const agentDependencies: AgentDependencies = { WebSocketClass: WebSocket, } -export { - agentDependencies, - HttpInboundTransport, - WsInboundTransport, - loadIndySdkPostgresPlugin, - IndySdkPostgresStorageConfig, - IndySdkPostgresWalletScheme, -} +export { agentDependencies, HttpInboundTransport, WsInboundTransport } diff --git a/packages/openid4vc-client/src/OpenId4VcClientModule.ts b/packages/openid4vc-client/src/OpenId4VcClientModule.ts index a6385c76f6..932f90c8b8 100644 --- a/packages/openid4vc-client/src/OpenId4VcClientModule.ts +++ b/packages/openid4vc-client/src/OpenId4VcClientModule.ts @@ -19,7 +19,7 @@ export class OpenId4VcClientModule implements Module { dependencyManager .resolve(AgentConfig) .logger.warn( - "The '@credo-ts/openid4vc-client' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + "The '@credo-ts/openid4vc-client' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." ) // Api diff --git a/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts b/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts index c4089e2a37..b0a19263af 100644 --- a/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts +++ b/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts @@ -12,10 +12,9 @@ import { } from '@credo-ts/core' import nock, { cleanAll, enableNetConnect } from 'nock' -import { AskarModule } from '../../askar/src' -import { askarModuleConfig } from '../../askar/tests/helpers' +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' import { customDocumentLoader } from '../../core/src/modules/vc/data-integrity/__tests__/documentLoader' -import { getAgentOptions } from '../../core/tests' +import { getInMemoryAgentOptions } from '../../core/tests' import { mattrLaunchpadJsonLd, waltIdJffJwt } from './fixtures' @@ -26,23 +25,21 @@ const modules = { w3cCredentials: new W3cCredentialsModule({ documentLoader: customDocumentLoader, }), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule(), } describe('OpenId4VcClient', () => { let agent: Agent beforeEach(async () => { - const agentOptions = getAgentOptions('OpenId4VcClient Agent', {}, modules) - + const agentOptions = getInMemoryAgentOptions('OpenId4VcClient Agent', {}, modules) agent = new Agent(agentOptions) - await agent.initialize() }) afterEach(async () => { - await agent.shutdown() await agent.wallet.delete() + await agent.shutdown() }) describe('Pre-authorized flow', () => { diff --git a/packages/question-answer/src/__tests__/QuestionAnswerService.test.ts b/packages/question-answer/src/__tests__/QuestionAnswerService.test.ts index a323bf6e8d..1a45b7c173 100644 --- a/packages/question-answer/src/__tests__/QuestionAnswerService.test.ts +++ b/packages/question-answer/src/__tests__/QuestionAnswerService.test.ts @@ -1,13 +1,12 @@ import type { AgentConfig, AgentContext, Repository, Wallet } from '@credo-ts/core' import type { QuestionAnswerStateChangedEvent, ValidResponse } from '@credo-ts/question-answer' -import { EventEmitter, SigningProviderRegistry, InboundMessageContext, DidExchangeState } from '@credo-ts/core' +import { EventEmitter, InboundMessageContext, DidExchangeState } from '@credo-ts/core' import { agentDependencies } from '@credo-ts/node' import { Subject } from 'rxjs' +import { InMemoryWallet } from '../../../../tests/InMemoryWallet' import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../core/tests/helpers' -import { IndySdkWallet } from '../../../indy-sdk/src' -import { indySdk } from '../../../indy-sdk/tests/setupIndySdkModule' import { QuestionAnswerRecord, @@ -61,7 +60,7 @@ describe('QuestionAnswerService', () => { beforeAll(async () => { agentConfig = getAgentConfig('QuestionAnswerServiceTest') - wallet = new IndySdkWallet(indySdk, agentConfig.logger, new SigningProviderRegistry([])) + wallet = new InMemoryWallet() agentContext = getAgentContext() // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await wallet.createAndOpen(agentConfig.walletConfig!) diff --git a/packages/question-answer/tests/question-answer.e2e.test.ts b/packages/question-answer/tests/question-answer.e2e.test.ts index b3576bad3a..dafbc55cdd 100644 --- a/packages/question-answer/tests/question-answer.e2e.test.ts +++ b/packages/question-answer/tests/question-answer.e2e.test.ts @@ -2,8 +2,7 @@ import type { ConnectionRecord } from '@credo-ts/core' import { Agent } from '@credo-ts/core' -import { indySdk, setupSubjectTransports, testLogger, getAgentOptions, makeConnection } from '../../core/tests' -import { IndySdkModule } from '../../indy-sdk/src' +import { setupSubjectTransports, testLogger, makeConnection, getInMemoryAgentOptions } from '../../core/tests' import { waitForQuestionAnswerRecord } from './helpers' @@ -11,12 +10,9 @@ import { QuestionAnswerModule, QuestionAnswerRole, QuestionAnswerState } from '@ const modules = { questionAnswer: new QuestionAnswerModule(), - indySdk: new IndySdkModule({ - indySdk, - }), } -const bobAgentOptions = getAgentOptions( +const bobAgentOptions = getInMemoryAgentOptions( 'Bob Question Answer', { endpoints: ['rxjs:bob'], @@ -24,7 +20,7 @@ const bobAgentOptions = getAgentOptions( modules ) -const aliceAgentOptions = getAgentOptions( +const aliceAgentOptions = getInMemoryAgentOptions( 'Alice Question Answer', { endpoints: ['rxjs:alice'], diff --git a/packages/react-native/jest.config.ts b/packages/react-native/jest.config.ts index 6426c5d8b8..2556d19c61 100644 --- a/packages/react-native/jest.config.ts +++ b/packages/react-native/jest.config.ts @@ -7,10 +7,6 @@ import packageJson from './package.json' const config: Config.InitialOptions = { ...base, displayName: packageJson.name, - moduleNameMapper: { - ...base.moduleNameMapper, - 'indy-sdk-react-native': 'indy-sdk', - }, } export default config diff --git a/packages/sd-jwt-vc/package.json b/packages/sd-jwt-vc/package.json index 447aa65b1b..d2bede53cd 100644 --- a/packages/sd-jwt-vc/package.json +++ b/packages/sd-jwt-vc/package.json @@ -32,7 +32,6 @@ "jwt-sd": "^0.1.2" }, "devDependencies": { - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", "reflect-metadata": "^0.1.13", "rimraf": "^4.4.0", "typescript": "~4.9.5" diff --git a/packages/sd-jwt-vc/src/SdJwtVcModule.ts b/packages/sd-jwt-vc/src/SdJwtVcModule.ts index f2c9508967..19f92b3230 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcModule.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcModule.ts @@ -20,7 +20,7 @@ export class SdJwtVcModule implements Module { dependencyManager .resolve(AgentConfig) .logger.warn( - "The '@credo-ts/sd-jwt-vc' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + "The '@credo-ts/sd-jwt-vc' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." ) // Api diff --git a/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts b/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts index 020aea72fb..f21a5510c6 100644 --- a/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts +++ b/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts @@ -1,20 +1,17 @@ import type { Key, Logger } from '@credo-ts/core' -import { AskarModule } from '@credo-ts/askar' import { getJwkFromKey, DidKey, DidsModule, KeyDidRegistrar, KeyDidResolver, - utils, KeyType, Agent, TypedArrayEncoder, } from '@credo-ts/core' -import { ariesAskar } from '@hyperledger/aries-askar-nodejs' -import { agentDependencies } from '../../../core/tests' +import { getInMemoryAgentOptions } from '../../../core/tests' import { SdJwtVcService } from '../SdJwtVcService' import { SdJwtVcRepository } from '../repository' @@ -27,17 +24,18 @@ import { simpleJwtVcPresentation, } from './sdjwtvc.fixtures' -const agent = new Agent({ - config: { label: 'sdjwtvcserviceagent', walletConfig: { id: utils.uuid(), key: utils.uuid() } }, - modules: { - askar: new AskarModule({ ariesAskar }), - dids: new DidsModule({ - resolvers: [new KeyDidResolver()], - registrars: [new KeyDidRegistrar()], - }), - }, - dependencies: agentDependencies, -}) +const agent = new Agent( + getInMemoryAgentOptions( + 'sdjwtvcserviceagent', + {}, + { + dids: new DidsModule({ + resolvers: [new KeyDidResolver()], + registrars: [new KeyDidRegistrar()], + }), + } + ) +) const logger = jest.fn() as unknown as Logger agent.context.wallet.generateNonce = jest.fn(() => Promise.resolve('salt')) diff --git a/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts b/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts index bdc7122104..89aae8ad85 100644 --- a/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts +++ b/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts @@ -1,34 +1,24 @@ import type { Key } from '@credo-ts/core' -import { AskarModule } from '@credo-ts/askar' -import { - Agent, - DidKey, - DidsModule, - KeyDidRegistrar, - KeyDidResolver, - KeyType, - TypedArrayEncoder, - utils, -} from '@credo-ts/core' -import { ariesAskar } from '@hyperledger/aries-askar-nodejs' - -import { agentDependencies } from '../../core/tests' +import { Agent, DidKey, DidsModule, KeyDidRegistrar, KeyDidResolver, KeyType, TypedArrayEncoder } from '@credo-ts/core' + +import { getInMemoryAgentOptions } from '../../core/tests' import { SdJwtVcModule } from '../src' const getAgent = (label: string) => - new Agent({ - config: { label, walletConfig: { id: utils.uuid(), key: utils.uuid() } }, - modules: { - sdJwt: new SdJwtVcModule(), - askar: new AskarModule({ ariesAskar }), - dids: new DidsModule({ - resolvers: [new KeyDidResolver()], - registrars: [new KeyDidRegistrar()], - }), - }, - dependencies: agentDependencies, - }) + new Agent( + getInMemoryAgentOptions( + label, + {}, + { + sdJwt: new SdJwtVcModule(), + dids: new DidsModule({ + resolvers: [new KeyDidResolver()], + registrars: [new KeyDidRegistrar()], + }), + } + ) + ) describe('sd-jwt-vc end to end test', () => { const issuer = getAgent('sdjwtvcissueragent') diff --git a/packages/tenants/src/TenantsModule.ts b/packages/tenants/src/TenantsModule.ts index 58660e70a0..0489967637 100644 --- a/packages/tenants/src/TenantsModule.ts +++ b/packages/tenants/src/TenantsModule.ts @@ -27,7 +27,7 @@ export class TenantsModule imp dependencyManager .resolve(AgentConfig) .logger.warn( - "The '@credo-ts/tenants' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + "The '@credo-ts/tenants' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." ) // Api diff --git a/packages/tenants/src/__tests__/TenantAgent.test.ts b/packages/tenants/src/__tests__/TenantAgent.test.ts index c4c3589492..6989bfc47c 100644 --- a/packages/tenants/src/__tests__/TenantAgent.test.ts +++ b/packages/tenants/src/__tests__/TenantAgent.test.ts @@ -1,25 +1,11 @@ import { Agent, AgentContext } from '@credo-ts/core' -import { indySdk } from '../../../core/tests' -import { agentDependencies, getAgentConfig, getAgentContext } from '../../../core/tests/helpers' -import { IndySdkModule } from '../../../indy-sdk/src' +import { getAgentConfig, getAgentContext, getInMemoryAgentOptions } from '../../../core/tests/helpers' import { TenantAgent } from '../TenantAgent' describe('TenantAgent', () => { test('possible to construct a TenantAgent instance', () => { - const agent = new Agent({ - config: { - label: 'test', - walletConfig: { - id: 'Wallet: TenantAgentRoot', - key: 'Wallet: TenantAgentRoot', - }, - }, - dependencies: agentDependencies, - modules: { - indySdk: new IndySdkModule({ indySdk }), - }, - }) + const agent = new Agent(getInMemoryAgentOptions('TenantAgentRoot')) const tenantDependencyManager = agent.dependencyManager.createChild() diff --git a/packages/tenants/src/__tests__/TenantsApi.test.ts b/packages/tenants/src/__tests__/TenantsApi.test.ts index 82b84bfb06..baf4f2fc69 100644 --- a/packages/tenants/src/__tests__/TenantsApi.test.ts +++ b/packages/tenants/src/__tests__/TenantsApi.test.ts @@ -1,7 +1,6 @@ import { Agent, AgentContext, InjectionSymbols } from '@credo-ts/core' -import { indySdk, getAgentContext, getAgentOptions, mockFunction } from '../../../core/tests' -import { IndySdkModule } from '../../../indy-sdk/src' +import { getAgentContext, getInMemoryAgentOptions, mockFunction } from '../../../core/tests' import { TenantAgent } from '../TenantAgent' import { TenantsApi } from '../TenantsApi' import { TenantAgentContextProvider } from '../context/TenantAgentContextProvider' @@ -16,7 +15,7 @@ const AgentContextProviderMock = TenantAgentContextProvider as jest.Mock { const tenantAgentContext = await tenantSessionCoordinator.getContextForSession(tenantRecord) - expect(wallet.initialize).toHaveBeenCalledWith(tenantRecord.config.walletConfig) + expect(wallet.initialize).toHaveBeenCalledWith({ + ...tenantRecord.config.walletConfig, + storage: { config: { inMemory: true }, type: 'sqlite' }, + }) expect(tenantSessionMutexMock.acquireSession).toHaveBeenCalledTimes(1) - expect(extendSpy).toHaveBeenCalledWith(tenantRecord.config) + expect(extendSpy).toHaveBeenCalledWith({ + ...tenantRecord.config, + walletConfig: { ...tenantRecord.config.walletConfig, storage: { config: { inMemory: true }, type: 'sqlite' } }, + }) expect(createChildSpy).toHaveBeenCalledWith() expect(tenantDependencyManager.registerInstance).toHaveBeenCalledWith(AgentContext, expect.any(AgentContext)) expect(tenantDependencyManager.registerInstance).toHaveBeenCalledWith(AgentConfig, expect.any(AgentConfig)) @@ -136,7 +142,10 @@ describe('TenantSessionCoordinator', () => { await expect(tenantSessionCoordinator.getContextForSession(tenantRecord)).rejects.toThrowError('Test error') - expect(wallet.initialize).toHaveBeenCalledWith(tenantRecord.config.walletConfig) + expect(wallet.initialize).toHaveBeenCalledWith({ + ...tenantRecord.config.walletConfig, + storage: { config: { inMemory: true }, type: 'sqlite' }, + }) expect(tenantSessionMutexMock.acquireSession).toHaveBeenCalledTimes(1) expect(tenantSessionMutexMock.releaseSession).toHaveBeenCalledTimes(1) }) @@ -192,7 +201,10 @@ describe('TenantSessionCoordinator', () => { }) // Initialize should only be called once - expect(wallet.initialize).toHaveBeenCalledWith(tenantRecord.config.walletConfig) + expect(wallet.initialize).toHaveBeenCalledWith({ + ...tenantRecord.config.walletConfig, + storage: { config: { inMemory: true }, type: 'sqlite' }, + }) expect(wallet.initialize).toHaveBeenCalledTimes(1) expect(tenantAgentContext1).toBe(tenantAgentContext2) diff --git a/packages/tenants/tests/tenant-sessions.e2e.test.ts b/packages/tenants/tests/tenant-sessions.e2e.test.ts index d348240b34..a434141bea 100644 --- a/packages/tenants/tests/tenant-sessions.e2e.test.ts +++ b/packages/tenants/tests/tenant-sessions.e2e.test.ts @@ -3,16 +3,17 @@ import type { InitConfig } from '@credo-ts/core' import { ConnectionsModule, Agent } from '@credo-ts/core' import { agentDependencies } from '@credo-ts/node' -import { testLogger, indySdk } from '../../core/tests' -import { IndySdkModule } from '../../indy-sdk/src' +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' +import { uuid } from '../../core/src/utils/uuid' +import { testLogger } from '../../core/tests' import { TenantsModule } from '@credo-ts/tenants' const agentConfig: InitConfig = { label: 'Tenant Agent 1', walletConfig: { - id: 'Wallet: tenant sessions e2e agent 1', - key: 'Wallet: tenant sessions e2e agent 1', + id: `tenant sessions e2e agent 1 - ${uuid().slice(0, 4)}`, + key: `tenant sessions e2e agent 1`, }, logger: testLogger, endpoints: ['rxjs:tenant-agent1'], @@ -24,7 +25,7 @@ const agent = new Agent({ dependencies: agentDependencies, modules: { tenants: new TenantsModule({ sessionAcquireTimeout: 10000 }), - indySdk: new IndySdkModule({ indySdk }), + inMemory: new InMemoryWalletModule(), connections: new ConnectionsModule({ autoAcceptConnections: true, }), @@ -67,17 +68,16 @@ describe('Tenants Sessions E2E', () => { const tenantRecordPromises = [] for (let tenantNo = 0; tenantNo < numberOfTenants; tenantNo++) { - const tenantRecord = agent.modules.tenants.createTenant({ + const tenantRecordPromise = agent.modules.tenants.createTenant({ config: { label: 'Agent 1 Tenant 1', }, }) - tenantRecordPromises.push(tenantRecord) + tenantRecordPromises.push(tenantRecordPromise) } const tenantRecords = await Promise.all(tenantRecordPromises) - const tenantAgentPromises = [] for (const tenantRecord of tenantRecords) { for (let session = 0; session < numberOfSessions; session++) { diff --git a/packages/tenants/tests/tenants-askar-profiles.e2e.test.ts b/packages/tenants/tests/tenants-askar-profiles.e2e.test.ts index a32a79394e..93761bc218 100644 --- a/packages/tenants/tests/tenants-askar-profiles.e2e.test.ts +++ b/packages/tenants/tests/tenants-askar-profiles.e2e.test.ts @@ -5,7 +5,7 @@ import { agentDependencies } from '@credo-ts/node' import { AskarModule, AskarMultiWalletDatabaseScheme, AskarProfileWallet, AskarWallet } from '../../askar/src' import { askarModuleConfig } from '../../askar/tests/helpers' -import { testLogger } from '../../core/tests' +import { getAskarWalletConfig, testLogger } from '../../core/tests' import { TenantsModule } from '@credo-ts/tenants' @@ -13,10 +13,7 @@ describe('Tenants Askar database schemes E2E', () => { test('uses AskarWallet for all wallets and tenants when database schema is DatabasePerWallet', async () => { const agentConfig: InitConfig = { label: 'Tenant Agent 1', - walletConfig: { - id: 'Wallet: askar tenants without profiles e2e agent 1', - key: 'Wallet: askar tenants without profiles e2e agent 1', - }, + walletConfig: getAskarWalletConfig('askar tenants without profiles e2e agent 1', { inMemory: false }), logger: testLogger, } @@ -74,10 +71,7 @@ describe('Tenants Askar database schemes E2E', () => { test('uses AskarWallet for main agent, and ProfileAskarWallet for tenants', async () => { const agentConfig: InitConfig = { label: 'Tenant Agent 1', - walletConfig: { - id: 'Wallet: askar tenants with profiles e2e agent 1', - key: 'Wallet: askar tenants with profiles e2e agent 1', - }, + walletConfig: getAskarWalletConfig('askar tenants with profiles e2e agent 1'), logger: testLogger, } diff --git a/packages/tenants/tests/tenants.e2e.test.ts b/packages/tenants/tests/tenants.e2e.test.ts index 3e9ad92480..c55c5bb95c 100644 --- a/packages/tenants/tests/tenants.e2e.test.ts +++ b/packages/tenants/tests/tenants.e2e.test.ts @@ -3,18 +3,19 @@ import type { InitConfig } from '@credo-ts/core' import { ConnectionsModule, OutOfBandRecord, Agent, CacheModule, InMemoryLruCache } from '@credo-ts/core' import { agentDependencies } from '@credo-ts/node' +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' -import { testLogger, indySdk } from '../../core/tests' -import { IndySdkModule } from '../../indy-sdk/src' +import { uuid } from '../../core/src/utils/uuid' +import { testLogger } from '../../core/tests' import { TenantsModule } from '@credo-ts/tenants' const agent1Config: InitConfig = { label: 'Tenant Agent 1', walletConfig: { - id: 'Wallet: tenants e2e agent 1', - key: 'Wallet: tenants e2e agent 1', + id: `tenants e2e agent 1 - ${uuid().slice(0, 4)}`, + key: `tenants e2e agent 1`, }, logger: testLogger, endpoints: ['rxjs:tenant-agent1'], @@ -23,8 +24,8 @@ const agent1Config: InitConfig = { const agent2Config: InitConfig = { label: 'Tenant Agent 2', walletConfig: { - id: 'Wallet: tenants e2e agent 2', - key: 'Wallet: tenants e2e agent 2', + id: `tenants e2e agent 2 - ${uuid().slice(0, 4)}`, + key: `tenants e2e agent 2`, }, logger: testLogger, endpoints: ['rxjs:tenant-agent2'], @@ -35,7 +36,7 @@ const agent1 = new Agent({ config: agent1Config, modules: { tenants: new TenantsModule(), - indySdk: new IndySdkModule({ indySdk }), + inMemory: new InMemoryWalletModule(), connections: new ConnectionsModule({ autoAcceptConnections: true, }), @@ -50,7 +51,7 @@ const agent2 = new Agent({ config: agent2Config, modules: { tenants: new TenantsModule(), - indySdk: new IndySdkModule({ indySdk }), + inMemory: new InMemoryWalletModule(), connections: new ConnectionsModule({ autoAcceptConnections: true, }), diff --git a/samples/extension-module/package.json b/samples/extension-module/package.json index f685881ee5..76016ac980 100644 --- a/samples/extension-module/package.json +++ b/samples/extension-module/package.json @@ -13,15 +13,17 @@ "responder": "ts-node responder.ts" }, "devDependencies": { - "@credo-ts/core": "*", - "@credo-ts/node": "*", - "ts-node": "^10.4.0" - }, - "dependencies": { + "ts-node": "^10.4.0", "@types/express": "^4.17.13", "@types/uuid": "^9.0.1", - "@types/ws": "^8.5.4", + "@types/ws": "^8.5.4" + }, + "dependencies": { + "@credo-ts/core": "*", + "@credo-ts/node": "*", + "@credo-ts/askar": "*", "class-validator": "0.14.0", - "rxjs": "^7.2.0" + "rxjs": "^7.2.0", + "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5" } } diff --git a/samples/extension-module/tests/dummy.e2e.test.ts b/samples/extension-module/tests/dummy.e2e.test.ts index c042a7d546..50458e9044 100644 --- a/samples/extension-module/tests/dummy.e2e.test.ts +++ b/samples/extension-module/tests/dummy.e2e.test.ts @@ -1,13 +1,13 @@ import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' import type { ConnectionRecord } from '@credo-ts/core' +import { AskarModule } from '@credo-ts/askar' import { Agent } from '@credo-ts/core' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { Subject } from 'rxjs' -import { indySdk } from '../../../packages/core/tests' import { getAgentOptions, makeConnection } from '../../../packages/core/tests/helpers' import testLogger from '../../../packages/core/tests/logger' -import { IndySdkModule } from '../../../packages/indy-sdk/src' import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' import { DummyModule } from '../dummy/DummyModule' @@ -17,7 +17,9 @@ import { waitForDummyRecord } from './helpers' const modules = { dummy: new DummyModule(), - indySdk: new IndySdkModule({ indySdk }), + askar: new AskarModule({ + ariesAskar, + }), } const bobAgentOptions = getAgentOptions( diff --git a/tests/InMemoryStorageService.ts b/tests/InMemoryStorageService.ts index 40f48c8406..2fb57419c3 100644 --- a/tests/InMemoryStorageService.ts +++ b/tests/InMemoryStorageService.ts @@ -2,6 +2,8 @@ import type { AgentContext } from '../packages/core/src/agent' import type { BaseRecord, TagsBase } from '../packages/core/src/storage/BaseRecord' import type { StorageService, BaseRecordConstructor, Query } from '../packages/core/src/storage/StorageService' +import { InMemoryWallet } from './InMemoryWallet' + import { RecordNotFoundError, RecordDuplicateError, JsonTransformer, injectable } from '@credo-ts/core' interface StorageRecord { @@ -15,16 +17,19 @@ interface InMemoryRecords { [id: string]: StorageRecord } +interface ContextCorrelationIdToRecords { + [contextCorrelationId: string]: { + records: InMemoryRecords + creationDate: Date + } +} + @injectable() // eslint-disable-next-line @typescript-eslint/no-explicit-any export class InMemoryStorageService = BaseRecord> implements StorageService { - public records: InMemoryRecords - - public constructor(records: InMemoryRecords = {}) { - this.records = records - } + public contextCorrelationIdToRecords: ContextCorrelationIdToRecords = {} private recordToInstance(record: StorageRecord, recordClass: BaseRecordConstructor): T { const instance = JsonTransformer.fromJSON(record.value, recordClass) @@ -34,16 +39,43 @@ export class InMemoryStorageService = BaseRe return instance } + private getRecordsForContext(agentContext: AgentContext): InMemoryRecords { + const contextCorrelationId = agentContext.contextCorrelationId + + if (!this.contextCorrelationIdToRecords[contextCorrelationId]) { + this.contextCorrelationIdToRecords[contextCorrelationId] = { + records: {}, + creationDate: new Date(), + } + } else if (agentContext.wallet instanceof InMemoryWallet && agentContext.wallet.activeWalletId) { + const walletCreationDate = agentContext.wallet.inMemoryWallets[agentContext.wallet.activeWalletId].creationDate + const storageCreationDate = this.contextCorrelationIdToRecords[contextCorrelationId].creationDate + + // If the storage was created before the wallet, it means the wallet has been deleted in the meantime + // and thus we need to recreate the storage as we don't want to serve records from the previous wallet + // FIXME: this is a flaw in our wallet/storage model. I think wallet should be for keys, and storage + // for records and you can create them separately. But that's a bigger change. + if (storageCreationDate < walletCreationDate) { + this.contextCorrelationIdToRecords[contextCorrelationId] = { + records: {}, + creationDate: new Date(), + } + } + } + + return this.contextCorrelationIdToRecords[contextCorrelationId].records + } + /** @inheritDoc */ public async save(agentContext: AgentContext, record: T) { record.updatedAt = new Date() const value = JsonTransformer.toJSON(record) - if (this.records[record.id]) { + if (this.getRecordsForContext(agentContext)[record.id]) { throw new RecordDuplicateError(`Record with id ${record.id} already exists`, { recordType: record.type }) } - this.records[record.id] = { + this.getRecordsForContext(agentContext)[record.id] = { value, id: record.id, type: record.type, @@ -57,13 +89,13 @@ export class InMemoryStorageService = BaseRe const value = JsonTransformer.toJSON(record) delete value._tags - if (!this.records[record.id]) { + if (!this.getRecordsForContext(agentContext)[record.id]) { throw new RecordNotFoundError(`record with id ${record.id} not found.`, { recordType: record.type, }) } - this.records[record.id] = { + this.getRecordsForContext(agentContext)[record.id] = { value, id: record.id, type: record.type, @@ -73,13 +105,13 @@ export class InMemoryStorageService = BaseRe /** @inheritDoc */ public async delete(agentContext: AgentContext, record: T) { - if (!this.records[record.id]) { + if (!this.getRecordsForContext(agentContext)[record.id]) { throw new RecordNotFoundError(`record with id ${record.id} not found.`, { recordType: record.type, }) } - delete this.records[record.id] + delete this.getRecordsForContext(agentContext)[record.id] } /** @inheritDoc */ @@ -88,18 +120,18 @@ export class InMemoryStorageService = BaseRe recordClass: BaseRecordConstructor, id: string ): Promise { - if (!this.records[id]) { + if (!this.getRecordsForContext(agentContext)[id]) { throw new RecordNotFoundError(`record with id ${id} not found.`, { recordType: recordClass.type, }) } - delete this.records[id] + delete this.getRecordsForContext(agentContext)[id] } /** @inheritDoc */ public async getById(agentContext: AgentContext, recordClass: BaseRecordConstructor, id: string): Promise { - const record = this.records[id] + const record = this.getRecordsForContext(agentContext)[id] if (!record) { throw new RecordNotFoundError(`record with id ${id} not found.`, { @@ -112,7 +144,7 @@ export class InMemoryStorageService = BaseRe /** @inheritDoc */ public async getAll(agentContext: AgentContext, recordClass: BaseRecordConstructor): Promise { - const records = Object.values(this.records) + const records = Object.values(this.getRecordsForContext(agentContext)) .filter((record) => record.type === recordClass.type) .map((record) => this.recordToInstance(record, recordClass)) @@ -125,7 +157,7 @@ export class InMemoryStorageService = BaseRe recordClass: BaseRecordConstructor, query: Query ): Promise { - const records = Object.values(this.records) + const records = Object.values(this.getRecordsForContext(agentContext)) .filter((record) => record.type === recordClass.type) .filter((record) => filterByQuery(record, query)) .map((record) => this.recordToInstance(record, recordClass)) @@ -165,6 +197,10 @@ function matchSimpleQuery>(record: StorageRe const tags = record.tags as TagsBase for (const [key, value] of Object.entries(query)) { + // We don't query for value undefined, the value should be null in that case + if (value === undefined) continue + + // TODO: support null if (Array.isArray(value)) { const tagValue = tags[key] if (!Array.isArray(tagValue) || !value.every((v) => tagValue.includes(v))) { diff --git a/tests/InMemoryWallet.ts b/tests/InMemoryWallet.ts new file mode 100644 index 0000000000..fd46f2359e --- /dev/null +++ b/tests/InMemoryWallet.ts @@ -0,0 +1,343 @@ +import type { + EncryptedMessage, + WalletConfig, + WalletCreateKeyOptions, + WalletSignOptions, + UnpackedMessageContext, + WalletVerifyOptions, + Wallet, +} from '@credo-ts/core' + +import { CryptoBox, Store, Key as AskarKey, keyAlgFromString } from '@hyperledger/aries-askar-nodejs' +import BigNumber from 'bn.js' + +import { didcommV1Pack, didcommV1Unpack } from '../packages/askar/src/wallet/didcommV1' + +import { + JsonEncoder, + WalletNotFoundError, + injectable, + isValidSeed, + isValidPrivateKey, + KeyType, + Buffer, + AriesFrameworkError, + WalletError, + Key, + TypedArrayEncoder, +} from '@credo-ts/core' + +const isError = (error: unknown): error is Error => error instanceof Error + +interface InMemoryKey { + publicKeyBytes: Uint8Array + secretKeyBytes: Uint8Array + keyType: KeyType +} + +interface InMemoryKeys { + [id: string]: InMemoryKey +} + +interface InMemoryWallets { + [id: string]: { + keys: InMemoryKeys + creationDate: Date + } +} + +@injectable() +export class InMemoryWallet implements Wallet { + // activeWalletId can be set even if wallet is closed. So make sure to also look at + // isInitialized to see if the wallet is actually open + public activeWalletId?: string + + public inMemoryWallets: InMemoryWallets = {} + /** + * Abstract methods that need to be implemented by subclasses + */ + public isInitialized = false + public isProvisioned = false + + public get supportedKeyTypes() { + return [KeyType.Ed25519, KeyType.P256] + } + + private get inMemoryKeys(): InMemoryKeys { + if (!this.activeWalletId || !this.isInitialized) { + throw new WalletError('No active wallet') + } + + if (!this.inMemoryWallets[this.activeWalletId]) { + throw new WalletError('wallet does not exist') + } + + return this.inMemoryWallets[this.activeWalletId].keys + } + + public async create(walletConfig: WalletConfig) { + if (this.inMemoryWallets[walletConfig.id]) { + throw new WalletError('Wallet already exists') + } + + this.inMemoryWallets[walletConfig.id] = { + keys: {}, + creationDate: new Date(), + } + } + + public async createAndOpen(walletConfig: WalletConfig) { + await this.create(walletConfig) + await this.open(walletConfig) + } + + public async open(walletConfig: WalletConfig) { + if (this.isInitialized) { + throw new WalletError('A wallet is already open') + } + + if (!this.inMemoryWallets[walletConfig.id]) { + throw new WalletNotFoundError('Wallet does not exist', { walletType: 'InMemoryWallet' }) + } + + this.activeWalletId = walletConfig.id + this.isProvisioned = true + this.isInitialized = true + } + + public rotateKey(): Promise { + throw new Error('Method not implemented.') + } + + public async close() { + this.isInitialized = false + } + + public async delete() { + if (!this.activeWalletId) { + throw new WalletError('wallet is not provisioned') + } + + delete this.inMemoryWallets[this.activeWalletId] + this.activeWalletId = undefined + this.isProvisioned = false + } + + public async export() { + throw new Error('Method not implemented.') + } + + public async import() { + throw new Error('Method not implemented.') + } + + public async dispose() { + this.isInitialized = false + } + + /** + * Create a key with an optional seed and keyType. + * The keypair is also automatically stored in the wallet afterwards + */ + public async createKey({ seed, privateKey, keyType }: WalletCreateKeyOptions): Promise { + try { + if (seed && privateKey) { + throw new WalletError('Only one of seed and privateKey can be set') + } + + if (seed && !isValidSeed(seed, keyType)) { + throw new WalletError('Invalid seed provided') + } + + if (privateKey && !isValidPrivateKey(privateKey, keyType)) { + throw new WalletError('Invalid private key provided') + } + + if (!this.supportedKeyTypes.includes(keyType)) { + throw new WalletError(`Unsupported key type: '${keyType}'`) + } + + const algorithm = keyAlgFromString(keyType) + + // Create key + let key: AskarKey | undefined + try { + key = privateKey + ? AskarKey.fromSecretBytes({ secretKey: privateKey, algorithm }) + : seed + ? AskarKey.fromSeed({ seed, algorithm }) + : AskarKey.generate(algorithm) + + const keyPublicBytes = key.publicBytes + // Store key + this.inMemoryKeys[TypedArrayEncoder.toBase58(keyPublicBytes)] = { + publicKeyBytes: keyPublicBytes, + secretKeyBytes: key.secretBytes, + keyType, + } + + return Key.fromPublicKey(keyPublicBytes, keyType) + } finally { + key?.handle.free() + } + } catch (error) { + // If already instance of `WalletError`, re-throw + if (error instanceof WalletError) throw error + + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError(`Error creating key with key type '${keyType}': ${error.message}`, { cause: error }) + } + } + + /** + * sign a Buffer with an instance of a Key class + * + * @param data Buffer The data that needs to be signed + * @param key Key The key that is used to sign the data + * + * @returns A signature for the data + */ + public async sign({ data, key }: WalletSignOptions): Promise { + const inMemoryKey = this.inMemoryKeys[key.publicKeyBase58] + if (!inMemoryKey) { + throw new WalletError(`Key not found in wallet`) + } + + if (!TypedArrayEncoder.isTypedArray(data)) { + throw new WalletError(`Currently not supporting signing of multiple messages`) + } + + let askarKey: AskarKey | undefined + try { + const inMemoryKey = this.inMemoryKeys[key.publicKeyBase58] + askarKey = AskarKey.fromSecretBytes({ + algorithm: keyAlgFromString(inMemoryKey.keyType), + secretKey: inMemoryKey.secretKeyBytes, + }) + + const signed = askarKey.signMessage({ message: data as Buffer }) + + return Buffer.from(signed) + } finally { + askarKey?.handle.free() + } + } + + /** + * Verify the signature with the data and the used key + * + * @param data Buffer The data that has to be confirmed to be signed + * @param key Key The key that was used in the signing process + * @param signature Buffer The signature that was created by the signing process + * + * @returns A boolean whether the signature was created with the supplied data and key + * + * @throws {WalletError} When it could not do the verification + * @throws {WalletError} When an unsupported keytype is used + */ + public async verify({ data, key, signature }: WalletVerifyOptions): Promise { + if (!TypedArrayEncoder.isTypedArray(data)) { + throw new WalletError(`Currently not supporting signing of multiple messages`) + } + + let askarKey: AskarKey | undefined + try { + askarKey = AskarKey.fromPublicBytes({ + algorithm: keyAlgFromString(key.keyType), + publicKey: key.publicKey, + }) + return askarKey.verifySignature({ message: data as Buffer, signature }) + } finally { + askarKey?.handle.free() + } + } + + /** + * Pack a message using DIDComm V1 algorithm + * + * @param payload message to send + * @param recipientKeys array containing recipient keys in base58 + * @param senderVerkey sender key in base58 + * @returns JWE Envelope to send + */ + public async pack( + payload: Record, + recipientKeys: string[], + senderVerkey?: string // in base58 + ): Promise { + const senderKey = senderVerkey ? this.inMemoryKeys[senderVerkey] : undefined + + if (senderVerkey && !senderKey) { + throw new WalletError(`Sender key not found`) + } + + const askarSenderKey = senderKey + ? AskarKey.fromSecretBytes({ + algorithm: keyAlgFromString(senderKey.keyType), + secretKey: senderKey.secretKeyBytes, + }) + : undefined + + try { + const envelope = didcommV1Pack(payload, recipientKeys, askarSenderKey) + return envelope + } finally { + askarSenderKey?.handle.free() + } + } + + /** + * Unpacks a JWE Envelope coded using DIDComm V1 algorithm + * + * @param messagePackage JWE Envelope + * @returns UnpackedMessageContext with plain text message, sender key and recipient key + */ + public async unpack(messagePackage: EncryptedMessage): Promise { + const protectedJson = JsonEncoder.fromBase64(messagePackage.protected) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const recipientKids: string[] = protectedJson.recipients.map((r: any) => r.header.kid) + + for (const recipientKid of recipientKids) { + const recipientKey = this.inMemoryKeys[recipientKid] + const recipientAskarKey = recipientKey + ? AskarKey.fromSecretBytes({ + algorithm: keyAlgFromString(recipientKey.keyType), + secretKey: recipientKey.secretKeyBytes, + }) + : undefined + try { + if (recipientAskarKey) { + const unpacked = didcommV1Unpack(messagePackage, recipientAskarKey) + return unpacked + } + } finally { + recipientAskarKey?.handle.free() + } + } + + throw new WalletError('No corresponding recipient key found') + } + + public async generateNonce(): Promise { + try { + // generate an 80-bit nonce suitable for AnonCreds proofs + const nonce = CryptoBox.randomNonce().slice(0, 10) + return new BigNumber(nonce).toString() + } catch (error) { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) + } + throw new WalletError('Error generating nonce', { cause: error }) + } + } + + public async generateWalletKey() { + try { + return Store.generateRawKey() + } catch (error) { + throw new WalletError('Error generating wallet key', { cause: error }) + } + } +} diff --git a/tests/InMemoryWalletModule.ts b/tests/InMemoryWalletModule.ts new file mode 100644 index 0000000000..c33326b79f --- /dev/null +++ b/tests/InMemoryWalletModule.ts @@ -0,0 +1,22 @@ +import type { DependencyManager, Module } from '@credo-ts/core' + +import { InMemoryStorageService } from './InMemoryStorageService' +import { InMemoryWallet } from './InMemoryWallet' + +import { AriesFrameworkError, InjectionSymbols } from '@credo-ts/core' + +export class InMemoryWalletModule implements Module { + public register(dependencyManager: DependencyManager) { + if (dependencyManager.isRegistered(InjectionSymbols.Wallet)) { + throw new AriesFrameworkError('There is an instance of Wallet already registered') + } else { + dependencyManager.registerContextScoped(InjectionSymbols.Wallet, InMemoryWallet) + } + + if (dependencyManager.isRegistered(InjectionSymbols.StorageService)) { + throw new AriesFrameworkError('There is an instance of StorageService already registered') + } else { + dependencyManager.registerSingleton(InjectionSymbols.StorageService, InMemoryStorageService) + } + } +} diff --git a/tests/e2e-askar-indy-sdk-wallet-subject.test.ts b/tests/e2e-askar-indy-vdr-anoncreds-rs.test.ts similarity index 83% rename from tests/e2e-askar-indy-sdk-wallet-subject.test.ts rename to tests/e2e-askar-indy-vdr-anoncreds-rs.test.ts index 560f031dc4..4d948dd281 100644 --- a/tests/e2e-askar-indy-sdk-wallet-subject.test.ts +++ b/tests/e2e-askar-indy-vdr-anoncreds-rs.test.ts @@ -3,10 +3,8 @@ import type { AnonCredsTestsAgent } from '../packages/anoncreds/tests/legacyAnon import { Subject } from 'rxjs' -import { - getAskarAnonCredsIndyModules, - getLegacyAnonCredsModules, -} from '../packages/anoncreds/tests/legacyAnonCredsSetup' +import { getAnonCredsIndyModules } from '../packages/anoncreds/tests/legacyAnonCredsSetup' +import { askarModule } from '../packages/askar/tests/helpers' import { getAgentOptions } from '../packages/core/tests/helpers' import { e2eTest } from './e2e-test' @@ -25,12 +23,13 @@ const recipientAgentOptions = getAgentOptions( 'E2E Askar Subject Recipient', {}, { - ...getAskarAnonCredsIndyModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediationRecipient: new MediationRecipientModule({ mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), + askar: askarModule, } ) const mediatorAgentOptions = getAgentOptions( @@ -39,25 +38,27 @@ const mediatorAgentOptions = getAgentOptions( endpoints: ['rxjs:mediator'], }, { - ...getAskarAnonCredsIndyModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediator: new MediatorModule({ autoAcceptMediationRequests: true }), + askar: askarModule, } ) const senderAgentOptions = getAgentOptions( - 'E2E Indy SDK Subject Sender', + 'E2E Askar Subject Sender', { endpoints: ['rxjs:sender'], }, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediationRecipient: new MediationRecipientModule({ mediatorPollingInterval: 1000, mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), + askar: askarModule, } ) @@ -67,9 +68,9 @@ describe('E2E Askar-AnonCredsRS-IndyVDR Subject tests', () => { let senderAgent: AnonCredsTestsAgent beforeEach(async () => { - recipientAgent = new Agent(recipientAgentOptions) as AnonCredsTestsAgent - mediatorAgent = new Agent(mediatorAgentOptions) as AnonCredsTestsAgent - senderAgent = new Agent(senderAgentOptions) as AnonCredsTestsAgent + recipientAgent = new Agent(recipientAgentOptions) as unknown as AnonCredsTestsAgent + mediatorAgent = new Agent(mediatorAgentOptions) as unknown as AnonCredsTestsAgent + senderAgent = new Agent(senderAgentOptions) as unknown as AnonCredsTestsAgent }) afterEach(async () => { diff --git a/tests/e2e-http.test.ts b/tests/e2e-http.test.ts index 45cc0bfae4..d4cc8eb163 100644 --- a/tests/e2e-http.test.ts +++ b/tests/e2e-http.test.ts @@ -1,7 +1,7 @@ import type { AnonCredsTestsAgent } from '../packages/anoncreds/tests/legacyAnonCredsSetup' -import { getLegacyAnonCredsModules } from '../packages/anoncreds/tests/legacyAnonCredsSetup' -import { getAgentOptions } from '../packages/core/tests/helpers' +import { getAnonCredsIndyModules } from '../packages/anoncreds/tests/legacyAnonCredsSetup' +import { getInMemoryAgentOptions } from '../packages/core/tests/helpers' import { e2eTest } from './e2e-test' @@ -15,11 +15,11 @@ import { } from '@credo-ts/core' import { HttpInboundTransport } from '@credo-ts/node' -const recipientAgentOptions = getAgentOptions( +const recipientAgentOptions = getInMemoryAgentOptions( 'E2E HTTP Recipient', {}, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediationRecipient: new MediationRecipientModule({ @@ -29,13 +29,13 @@ const recipientAgentOptions = getAgentOptions( ) const mediatorPort = 3000 -const mediatorAgentOptions = getAgentOptions( +const mediatorAgentOptions = getInMemoryAgentOptions( 'E2E HTTP Mediator', { endpoints: [`http://localhost:${mediatorPort}`], }, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediator: new MediatorModule({ @@ -45,13 +45,13 @@ const mediatorAgentOptions = getAgentOptions( ) const senderPort = 3001 -const senderAgentOptions = getAgentOptions( +const senderAgentOptions = getInMemoryAgentOptions( 'E2E HTTP Sender', { endpoints: [`http://localhost:${senderPort}`], }, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediationRecipient: new MediationRecipientModule({ diff --git a/tests/e2e-subject.test.ts b/tests/e2e-subject.test.ts index 35656e6835..cc9670abbf 100644 --- a/tests/e2e-subject.test.ts +++ b/tests/e2e-subject.test.ts @@ -3,8 +3,8 @@ import type { AnonCredsTestsAgent } from '../packages/anoncreds/tests/legacyAnon import { Subject } from 'rxjs' -import { getLegacyAnonCredsModules } from '../packages/anoncreds/tests/legacyAnonCredsSetup' -import { getAgentOptions } from '../packages/core/tests/helpers' +import { getAnonCredsIndyModules } from '../packages/anoncreds/tests/legacyAnonCredsSetup' +import { getInMemoryAgentOptions } from '../packages/core/tests/helpers' import { e2eTest } from './e2e-test' import { SubjectInboundTransport } from './transport/SubjectInboundTransport' @@ -18,11 +18,11 @@ import { MediationRecipientModule, } from '@credo-ts/core' -const recipientAgentOptions = getAgentOptions( +const recipientAgentOptions = getInMemoryAgentOptions( 'E2E Subject Recipient', {}, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediationRecipient: new MediationRecipientModule({ @@ -30,25 +30,25 @@ const recipientAgentOptions = getAgentOptions( }), } ) -const mediatorAgentOptions = getAgentOptions( +const mediatorAgentOptions = getInMemoryAgentOptions( 'E2E Subject Mediator', { endpoints: ['rxjs:mediator'], }, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediator: new MediatorModule({ autoAcceptMediationRequests: true }), } ) -const senderAgentOptions = getAgentOptions( +const senderAgentOptions = getInMemoryAgentOptions( 'E2E Subject Sender', { endpoints: ['rxjs:sender'], }, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediationRecipient: new MediationRecipientModule({ diff --git a/tests/e2e-ws-pickup-v2.test.ts b/tests/e2e-ws-pickup-v2.test.ts index 57eb52afd8..281fae0d67 100644 --- a/tests/e2e-ws-pickup-v2.test.ts +++ b/tests/e2e-ws-pickup-v2.test.ts @@ -1,6 +1,7 @@ import type { AnonCredsTestsAgent } from '../packages/anoncreds/tests/legacyAnonCredsSetup' -import { getLegacyAnonCredsModules } from '../packages/anoncreds/tests/legacyAnonCredsSetup' +import { getAnonCredsIndyModules } from '../packages/anoncreds/tests/legacyAnonCredsSetup' +import { askarModule } from '../packages/askar/tests/helpers' import { MessageForwardingStrategy } from '../packages/core/src/modules/routing/MessageForwardingStrategy' import { getAgentOptions } from '../packages/core/tests/helpers' @@ -16,6 +17,9 @@ import { } from '@credo-ts/core' import { WsInboundTransport } from '@credo-ts/node' +// FIXME: somehow if we use the in memory wallet and storage service in the WS test it will fail, +// but it succeeds with Askar. We should look into this at some point + // FIXME: port numbers should not depend on availability from other test suites that use web sockets const mediatorPort = 4100 const mediatorOptions = getAgentOptions( @@ -24,13 +28,14 @@ const mediatorOptions = getAgentOptions( endpoints: [`ws://localhost:${mediatorPort}`], }, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediator: new MediatorModule({ autoAcceptMediationRequests: true, messageForwardingStrategy: MessageForwardingStrategy.QueueAndLiveModeDelivery, }), + askar: askarModule, } ) @@ -41,13 +46,14 @@ const senderOptions = getAgentOptions( endpoints: [`ws://localhost:${senderPort}`], }, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediationRecipient: new MediationRecipientModule({ mediatorPollingInterval: 1000, mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), + askar: askarModule, } ) @@ -57,8 +63,8 @@ describe('E2E WS Pickup V2 tests', () => { let senderAgent: AnonCredsTestsAgent beforeEach(async () => { - mediatorAgent = new Agent(mediatorOptions) as AnonCredsTestsAgent - senderAgent = new Agent(senderOptions) as AnonCredsTestsAgent + mediatorAgent = new Agent(mediatorOptions) as unknown as AnonCredsTestsAgent + senderAgent = new Agent(senderOptions) as unknown as AnonCredsTestsAgent }) afterEach(async () => { @@ -75,17 +81,18 @@ describe('E2E WS Pickup V2 tests', () => { 'E2E WS Pickup V2 Recipient polling mode', {}, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediationRecipient: new MediationRecipientModule({ mediatorPickupStrategy: MediatorPickupStrategy.PickUpV2, mediatorPollingInterval: 1000, }), + askar: askarModule, } ) - recipientAgent = new Agent(recipientOptions) as AnonCredsTestsAgent + recipientAgent = new Agent(recipientOptions) as unknown as AnonCredsTestsAgent // Recipient Setup recipientAgent.registerOutboundTransport(new WsOutboundTransport()) @@ -113,16 +120,17 @@ describe('E2E WS Pickup V2 tests', () => { 'E2E WS Pickup V2 Recipient live mode', {}, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediationRecipient: new MediationRecipientModule({ mediatorPickupStrategy: MediatorPickupStrategy.PickUpV2LiveMode, }), + askar: askarModule, } ) - recipientAgent = new Agent(recipientOptions) as AnonCredsTestsAgent + recipientAgent = new Agent(recipientOptions) as unknown as AnonCredsTestsAgent // Recipient Setup recipientAgent.registerOutboundTransport(new WsOutboundTransport()) diff --git a/tests/e2e-ws.test.ts b/tests/e2e-ws.test.ts index 76aa6c9a9f..f3563ab409 100644 --- a/tests/e2e-ws.test.ts +++ b/tests/e2e-ws.test.ts @@ -1,6 +1,7 @@ import type { AnonCredsTestsAgent } from '../packages/anoncreds/tests/legacyAnonCredsSetup' -import { getLegacyAnonCredsModules } from '../packages/anoncreds/tests/legacyAnonCredsSetup' +import { getAnonCredsIndyModules } from '../packages/anoncreds/tests/legacyAnonCredsSetup' +import { askarModule } from '../packages/askar/tests/helpers' import { getAgentOptions } from '../packages/core/tests/helpers' import { e2eTest } from './e2e-test' @@ -15,16 +16,19 @@ import { } from '@credo-ts/core' import { WsInboundTransport } from '@credo-ts/node' +// FIXME: somehow if we use the in memory wallet and storage service in the WS test it will fail, +// but it succeeds with Askar. We should look into this at some point const recipientAgentOptions = getAgentOptions( 'E2E WS Recipient ', {}, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediationRecipient: new MediationRecipientModule({ mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), + askar: askarModule, } ) @@ -35,10 +39,11 @@ const mediatorAgentOptions = getAgentOptions( endpoints: [`ws://localhost:${mediatorPort}`], }, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediator: new MediatorModule({ autoAcceptMediationRequests: true }), + askar: askarModule, } ) @@ -49,13 +54,14 @@ const senderAgentOptions = getAgentOptions( endpoints: [`ws://localhost:${senderPort}`], }, { - ...getLegacyAnonCredsModules({ + ...getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediationRecipient: new MediationRecipientModule({ mediatorPollingInterval: 1000, mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), + askar: askarModule, } ) @@ -65,9 +71,9 @@ describe('E2E WS tests', () => { let senderAgent: AnonCredsTestsAgent beforeEach(async () => { - recipientAgent = new Agent(recipientAgentOptions) as AnonCredsTestsAgent - mediatorAgent = new Agent(mediatorAgentOptions) as AnonCredsTestsAgent - senderAgent = new Agent(senderAgentOptions) as AnonCredsTestsAgent + recipientAgent = new Agent(recipientAgentOptions) as unknown as AnonCredsTestsAgent + mediatorAgent = new Agent(mediatorAgentOptions) as unknown as AnonCredsTestsAgent + senderAgent = new Agent(senderAgentOptions) as unknown as AnonCredsTestsAgent }) afterEach(async () => { diff --git a/yarn.lock b/yarn.lock index 59c30b61a1..6c138fa5ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1474,7 +1474,7 @@ resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff" integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA== -"@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3": +"@gar/promisify@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== @@ -2164,14 +2164,6 @@ treeverse "^3.0.0" walk-up-path "^1.0.0" -"@npmcli/fs@^1.0.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" - integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== - dependencies: - "@gar/promisify" "^1.0.1" - semver "^7.3.5" - "@npmcli/fs@^2.1.0": version "2.1.2" resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-2.1.2.tgz#a9e2541a4a2fec2e69c29b35e6060973da79b865" @@ -2252,14 +2244,6 @@ pacote "^15.0.0" semver "^7.3.5" -"@npmcli/move-file@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" - integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== - dependencies: - mkdirp "^1.0.4" - rimraf "^3.0.2" - "@npmcli/move-file@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-2.0.1.tgz#26f6bdc379d87f75e55739bab89db525b06100e4" @@ -3059,11 +3043,6 @@ "@stablelib/wipe" "^1.0.1" "@stablelib/xchacha20" "^1.0.1" -"@tootallnate/once@1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" - integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== - "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" @@ -3142,6 +3121,13 @@ dependencies: "@types/node" "*" +"@types/bn.js@^5.1.5": + version "5.1.5" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.5.tgz#2e0dacdcce2c0f16b905d20ff87aedbc6f7b4bf0" + integrity sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A== + dependencies: + "@types/node" "*" + "@types/body-parser@*": version "1.19.2" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" @@ -3213,13 +3199,6 @@ dependencies: "@types/node" "*" -"@types/indy-sdk@*", "@types/indy-sdk@1.16.27", "@types/indy-sdk@^1.16.26": - version "1.16.27" - resolved "https://registry.yarnpkg.com/@types/indy-sdk/-/indy-sdk-1.16.27.tgz#f5f01fe2cd39b74cacf91ea84d46a2e351cefa3b" - integrity sha512-ASEGYOuz8Acbybz4W2CYTG/fF7H9UQmJIG5wz8PSAvme07QU04Yzj4RJ5Nzzjej0X/AApEHS/5Jpk3iXTOs9HQ== - dependencies: - buffer "^6.0.0" - "@types/inquirer@^8.2.6": version "8.2.6" resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-8.2.6.tgz#abd41a5fb689c7f1acb12933d787d4262a02a0ab" @@ -3247,10 +3226,10 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^29.5.5": - version "29.5.5" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.5.tgz#727204e06228fe24373df9bae76b90f3e8236a2a" - integrity sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg== +"@types/jest@^29.5.11": + version "29.5.11" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.11.tgz#0c13aa0da7d0929f078ab080ae5d4ced80fa2f2c" + integrity sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -3614,7 +3593,7 @@ agent-base@6, agent-base@^6.0.2: dependencies: debug "4" -agentkeepalive@^4.1.3, agentkeepalive@^4.2.1: +agentkeepalive@^4.2.1: version "4.3.0" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.3.0.tgz#bb999ff07412653c1803b3ced35e50729830a255" integrity sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg== @@ -4234,13 +4213,6 @@ bin-links@^4.0.1: read-cmd-shim "^4.0.0" write-file-atomic "^5.0.0" -bindings@^1.3.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -4388,7 +4360,7 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -buffer@^6.0.0, buffer@^6.0.3: +buffer@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== @@ -4430,30 +4402,6 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -cacache@^15.2.0: - version "15.3.0" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" - integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== - dependencies: - "@npmcli/fs" "^1.0.0" - "@npmcli/move-file" "^1.0.1" - chownr "^2.0.0" - fs-minipass "^2.0.0" - glob "^7.1.4" - infer-owner "^1.0.4" - lru-cache "^6.0.0" - minipass "^3.1.1" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.2" - mkdirp "^1.0.3" - p-map "^4.0.0" - promise-inflight "^1.0.1" - rimraf "^3.0.2" - ssri "^8.0.1" - tar "^6.0.2" - unique-filename "^1.1.1" - cacache@^16.0.0, cacache@^16.1.0: version "16.1.3" resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e" @@ -5612,7 +5560,7 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -encoding@^0.1.12, encoding@^0.1.13: +encoding@^0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== @@ -6344,11 +6292,6 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - file-url@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/file-url/-/file-url-3.0.0.tgz#247a586a746ce9f7a8ed05560290968afc262a77" @@ -7188,15 +7131,6 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" -http-proxy-agent@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== - dependencies: - "@tootallnate/once" "1" - agent-base "6" - debug "4" - http-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" @@ -7310,15 +7244,6 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -indy-sdk@^1.16.0-dev-1636, indy-sdk@^1.16.0-dev-1655: - version "1.16.0-dev-1655" - resolved "https://registry.yarnpkg.com/indy-sdk/-/indy-sdk-1.16.0-dev-1655.tgz#098c38df4a6eb4e13f89c0b86ebe9636944b71e0" - integrity sha512-MSWRY8rdnGAegs4v4AnzE6CT9O/3JBMUiE45I0Ihj2DMuH+XS1EJZUQEJsyis6aOQzRavv/xVtaBC8o+6azKuw== - dependencies: - bindings "^1.3.1" - nan "^2.11.1" - node-gyp "^8.0.0" - infer-owner@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" @@ -9035,28 +8960,6 @@ make-fetch-happen@^11.0.0, make-fetch-happen@^11.0.1: socks-proxy-agent "^7.0.0" ssri "^10.0.0" -make-fetch-happen@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" - integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== - dependencies: - agentkeepalive "^4.1.3" - cacache "^15.2.0" - http-cache-semantics "^4.1.0" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^6.0.0" - minipass "^3.1.3" - minipass-collect "^1.0.2" - minipass-fetch "^1.3.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^0.6.2" - promise-retry "^2.0.1" - socks-proxy-agent "^6.0.0" - ssri "^8.0.0" - make-promises-safe@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/make-promises-safe/-/make-promises-safe-5.1.0.tgz#dd9d311f555bcaa144f12e225b3d37785f0aa8f2" @@ -9547,17 +9450,6 @@ minipass-collect@^1.0.2: dependencies: minipass "^3.0.0" -minipass-fetch@^1.3.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" - integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== - dependencies: - minipass "^3.1.0" - minipass-sized "^1.0.3" - minizlib "^2.0.0" - optionalDependencies: - encoding "^0.1.12" - minipass-fetch@^2.0.3: version "2.1.2" resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-2.1.2.tgz#95560b50c472d81a3bc76f20ede80eaed76d8add" @@ -9595,7 +9487,7 @@ minipass-json-stream@^1.0.1: jsonparse "^1.3.1" minipass "^3.0.0" -minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: +minipass-pipeline@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== @@ -9617,7 +9509,7 @@ minipass@^2.6.0, minipass@^2.9.0: safe-buffer "^5.1.2" yallist "^3.0.0" -minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3, minipass@^3.1.6: +minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6: version "3.3.6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== @@ -9641,7 +9533,7 @@ minizlib@^1.3.3: dependencies: minipass "^2.9.0" -minizlib@^2.0.0, minizlib@^2.1.1, minizlib@^2.1.2: +minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== @@ -9737,11 +9629,6 @@ mute-stream@0.0.8, mute-stream@~0.0.4: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.11.1: - version "2.17.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" - integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== - nanoid@^3.3.6: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -9783,7 +9670,7 @@ needle@^2.5.2: iconv-lite "^0.4.4" sax "^1.2.4" -negotiator@0.6.3, negotiator@^0.6.2, negotiator@^0.6.3: +negotiator@0.6.3, negotiator@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== @@ -9925,22 +9812,6 @@ node-gyp-build@^4.2.1, node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== -node-gyp@^8.0.0: - version "8.4.1" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" - integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== - dependencies: - env-paths "^2.2.0" - glob "^7.1.4" - graceful-fs "^4.2.6" - make-fetch-happen "^9.1.0" - nopt "^5.0.0" - npmlog "^6.0.0" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.2" - which "^2.0.2" - node-gyp@^9.0.0: version "9.3.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.3.1.tgz#1e19f5f290afcc9c46973d68700cbd21a96192e4" @@ -11744,18 +11615,18 @@ semver@7.3.8: dependencies: lru-cache "^6.0.0" -semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -11959,15 +11830,6 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -socks-proxy-agent@^6.0.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz#2687a31f9d7185e38d530bef1944fe1f1496d6ce" - integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== - dependencies: - agent-base "^6.0.2" - debug "^4.3.3" - socks "^2.6.2" - socks-proxy-agent@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" @@ -12110,13 +11972,6 @@ ssri@^10.0.0, ssri@^10.0.1: dependencies: minipass "^4.0.0" -ssri@^8.0.0, ssri@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" - integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== - dependencies: - minipass "^3.1.1" - stack-utils@^2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" @@ -12452,7 +12307,7 @@ tar@^4.4.13: safe-buffer "^5.2.1" yallist "^3.1.1" -tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: +tar@^6.1.11, tar@^6.1.2: version "6.1.13" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw== @@ -12643,10 +12498,10 @@ trim-newlines@^3.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== -ts-jest@^29.0.5: - version "29.1.0" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.0.tgz#4a9db4104a49b76d2b368ea775b6c9535c603891" - integrity sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA== +ts-jest@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.2.tgz#7613d8c81c43c8cb312c6904027257e814c40e09" + integrity sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g== dependencies: bs-logger "0.x" fast-json-stable-stringify "2.x" @@ -12654,7 +12509,7 @@ ts-jest@^29.0.5: json5 "^2.2.3" lodash.memoize "4.x" make-error "1.x" - semver "7.x" + semver "^7.5.3" yargs-parser "^21.0.1" ts-node@^10.0.0, ts-node@^10.4.0: @@ -12958,13 +12813,6 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" -unique-filename@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" - integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== - dependencies: - unique-slug "^2.0.0" - unique-filename@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2" @@ -12979,13 +12827,6 @@ unique-filename@^3.0.0: dependencies: unique-slug "^4.0.0" -unique-slug@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" - integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== - dependencies: - imurmurhash "^0.1.4" - unique-slug@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-3.0.0.tgz#6d347cf57c8a7a7a6044aabd0e2d74e4d76dc7c9"