diff --git a/packages/core/src/modules/credentials/CredentialsModule.ts b/packages/core/src/modules/credentials/CredentialsModule.ts index fa9c383cfc..c755a84184 100644 --- a/packages/core/src/modules/credentials/CredentialsModule.ts +++ b/packages/core/src/modules/credentials/CredentialsModule.ts @@ -44,7 +44,7 @@ export interface CredentialsModule { declineOffer(credentialRecordId: string): Promise negotiateOffer(options: NegotiateOfferOptions): Promise // out of band - createOutOfBandOffer(options: OfferCredentialOptions): Promise<{ + createOffer(options: OfferCredentialOptions): Promise<{ message: AgentMessage credentialRecord: CredentialExchangeRecord }> @@ -522,18 +522,14 @@ export class CredentialsModule implements CredentialsModule { * @param options The credential options to use for the offer * @returns The credential record and credential offer message */ - public async createOutOfBandOffer(options: OfferCredentialOptions): Promise<{ + public async createOffer(options: OfferCredentialOptions): Promise<{ message: AgentMessage credentialRecord: CredentialExchangeRecord }> { - // with version we can get the Service - if (!options.protocolVersion) { - throw new AriesFrameworkError('Missing protocol version in createOutOfBandOffer') - } const service = this.getService(options.protocolVersion) this.logger.debug(`Got a CredentialService object for version ${options.protocolVersion}`) - const { message, credentialRecord } = await service.createOutOfBandOffer(options) + const { message, credentialRecord } = await service.createOffer(options) this.logger.debug('Offer Message successfully created; message= ', message) diff --git a/packages/core/src/modules/credentials/protocol/v1/V1CredentialService.ts b/packages/core/src/modules/credentials/protocol/v1/V1CredentialService.ts index d19d498046..90fc6c6565 100644 --- a/packages/core/src/modules/credentials/protocol/v1/V1CredentialService.ts +++ b/packages/core/src/modules/credentials/protocol/v1/V1CredentialService.ts @@ -30,7 +30,6 @@ import { Lifecycle, scoped } from 'tsyringe' import { AgentConfig } from '../../../../agent/AgentConfig' import { Dispatcher } from '../../../../agent/Dispatcher' import { EventEmitter } from '../../../../agent/EventEmitter' -import { ServiceDecorator } from '../../../../decorators/service/ServiceDecorator' import { AriesFrameworkError } from '../../../../error' import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage' import { isLinkedAttachment } from '../../../../utils/attachment' @@ -504,32 +503,30 @@ export class V1CredentialService extends CredentialService { public async createOffer( credentialOptions: OfferCredentialOptions ): Promise> { - if (!credentialOptions.connectionId) { - throw new AriesFrameworkError('Connection id missing from offer credential options') - } - if (!credentialOptions.credentialFormats.indy || Object.keys(credentialOptions.credentialFormats).length !== 1) { + // connection id can be undefined in connection-less scenario + const connection = credentialOptions.connectionId + ? await this.connectionService.getById(credentialOptions.connectionId) + : undefined + + const indy = credentialOptions.credentialFormats.indy + + if (!indy || Object.keys(credentialOptions.credentialFormats).length !== 1) { throw new AriesFrameworkError('Only indy proof format is supported for present proof protocol v1') } - const connection = await this.connectionService.getById(credentialOptions.connectionId) - - if ( - !credentialOptions?.credentialFormats.indy?.attributes || - !credentialOptions?.credentialFormats.indy?.credentialDefinitionId - ) { + if (!indy.attributes || !indy.credentialDefinitionId) { throw new AriesFrameworkError('Missing properties from OfferCredentialOptions object: cannot create Offer!') } + const preview: V1CredentialPreview = new V1CredentialPreview({ - attributes: credentialOptions.credentialFormats.indy?.attributes, + attributes: indy.attributes, }) - const linkedAttachments = credentialOptions.credentialFormats.indy?.linkedAttachments - const template: CredentialOfferTemplate = { ...credentialOptions, preview: preview, - credentialDefinitionId: credentialOptions?.credentialFormats.indy?.credentialDefinitionId, - linkedAttachments, + credentialDefinitionId: indy.credentialDefinitionId, + linkedAttachments: indy.linkedAttachments, } const { credentialRecord, message } = await this.createOfferProcessing(template, connection) @@ -549,6 +546,7 @@ export class V1CredentialService extends CredentialService { }) return { credentialRecord, message } } + /** * Process a received {@link OfferCredentialMessage}. This will not accept the credential offer * or send a credential request. It will only create a new credential record with @@ -714,50 +712,6 @@ export class V1CredentialService extends CredentialService { return { message: offerMessage, credentialRecord } } - public async createOutOfBandOffer( - credentialOptions: OfferCredentialOptions - ): Promise> { - if (!credentialOptions.credentialFormats.indy || Object.keys(credentialOptions.credentialFormats).length !== 1) { - throw new AriesFrameworkError('Only indy proof format is supported for present proof protocol v1') - } - - if (!credentialOptions.credentialFormats.indy?.credentialDefinitionId) { - throw new AriesFrameworkError('Missing credential definition id for out of band credential') - } - const v1Preview = new V1CredentialPreview({ - attributes: credentialOptions.credentialFormats.indy?.attributes, - }) - const template: CredentialOfferTemplate = { - credentialDefinitionId: credentialOptions.credentialFormats.indy?.credentialDefinitionId, - comment: credentialOptions.comment, - preview: v1Preview, - autoAcceptCredential: credentialOptions.autoAcceptCredential, - } - - const { credentialRecord, message } = await this.createOfferProcessing(template) - - // Create and set ~service decorator - const routing = await this.mediationRecipientService.getRouting() - message.service = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - await this.credentialRepository.save(credentialRecord) - await this.didCommMessageRepository.saveAgentMessage({ - agentMessage: message, - role: DidCommMessageRole.Receiver, - associatedRecordId: credentialRecord.id, - }) - this.eventEmitter.emit({ - type: CredentialEventTypes.CredentialStateChanged, - payload: { - credentialRecord, - previousState: null, - }, - }) - return { credentialRecord, message } - } /** * Create a {@link RequestCredentialMessage} as response to a received credential offer. * diff --git a/packages/core/src/modules/credentials/protocol/v2/V2CredentialService.ts b/packages/core/src/modules/credentials/protocol/v2/V2CredentialService.ts index feb90f651b..1835e10eb1 100644 --- a/packages/core/src/modules/credentials/protocol/v2/V2CredentialService.ts +++ b/packages/core/src/modules/credentials/protocol/v2/V2CredentialService.ts @@ -32,7 +32,6 @@ import { Lifecycle, scoped } from 'tsyringe' import { AgentConfig } from '../../../../agent/AgentConfig' import { Dispatcher } from '../../../../agent/Dispatcher' import { EventEmitter } from '../../../../agent/EventEmitter' -import { ServiceDecorator } from '../../../../decorators/service/ServiceDecorator' import { AriesFrameworkError } from '../../../../error' import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage' import { AckStatus } from '../../../common' @@ -328,7 +327,8 @@ export class V2CredentialService extends CredentialService { return { message: credentialProposalMessage, credentialRecord } } /** - * Create a {@link V2OfferCredentialMessage} as beginning of protocol process. + * Create a {@link V2OfferCredentialMessage} as beginning of protocol process. If no connectionId is provided, the + * exchange will be created without a connection for usage in oob and connection-less issuance. * * @param formatService {@link CredentialFormatService} the format service object containing format-specific logic * @param options attributes of the original offer @@ -338,18 +338,15 @@ export class V2CredentialService extends CredentialService { public async createOffer( options: OfferCredentialOptions ): Promise> { - if (!options.connectionId) { - throw new AriesFrameworkError('Connection id missing from offer credential options') - } - const connection = await this.connectionService.getById(options.connectionId) - + const connection = options.connectionId ? await this.connectionService.getById(options.connectionId) : undefined connection?.assertReady() - const formats: CredentialFormatService[] = this.getFormats(options.credentialFormats) + const formats = this.getFormats(options.credentialFormats) - if (!formats || formats.length === 0) { + if (formats.length === 0) { throw new AriesFrameworkError(`Unable to create offer. No supported formats`) } + // Create message const { credentialRecord, message: credentialOfferMessage } = await this.credentialMessageBuilder.createOffer( formats, @@ -369,41 +366,6 @@ export class V2CredentialService extends CredentialService { return { credentialRecord, message: credentialOfferMessage } } - /** - * Create an offer message for an out-of-band (connectionless) credential - * @param credentialOptions the options (parameters) object for the offer - * @returns the credential record and the offer message - */ - public async createOutOfBandOffer( - credentialOptions: OfferCredentialOptions - ): Promise> { - const formats: CredentialFormatService[] = this.getFormats(credentialOptions.credentialFormats) - - if (!formats || formats.length === 0) { - throw new AriesFrameworkError(`Unable to create out of band offer. No supported formats`) - } - // Create message - const { credentialRecord, message: offerCredentialMessage } = await this.credentialMessageBuilder.createOffer( - formats, - credentialOptions - ) - - // Create and set ~service decorator - const routing = await this.mediationRecipientService.getRouting() - offerCredentialMessage.service = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - await this.credentialRepository.save(credentialRecord) - await this.didCommMessageRepository.saveOrUpdateAgentMessage({ - agentMessage: offerCredentialMessage, - role: DidCommMessageRole.Receiver, - associatedRecordId: credentialRecord.id, - }) - await this.emitEvent(credentialRecord) - return { credentialRecord, message: offerCredentialMessage } - } /** * Create a {@link OfferCredentialMessage} as response to a received credential proposal. * To create an offer not bound to an existing credential exchange, use {@link V2CredentialService#createOffer}. @@ -1051,6 +1013,7 @@ export class V2CredentialService extends CredentialService { }, }) } + /** * Retrieve a credential record by connection id and thread id * diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v1connectionless-credentials.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v1connectionless-credentials.test.ts index cb0890b56c..a5fe33784d 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/v1connectionless-credentials.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v1connectionless-credentials.test.ts @@ -94,11 +94,15 @@ describe('credentials', () => { connectionId: '', } // eslint-disable-next-line prefer-const - let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOutOfBandOffer( - offerOptions - ) + let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOffer(offerOptions) - await aliceAgent.receiveMessage(message.toJSON()) + const { message: offerMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberCredentialRecord.id, + message, + domain: 'https://a-domain.com', + }) + + await aliceAgent.receiveMessage(offerMessage.toJSON()) let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { threadId: faberCredentialRecord.threadId, @@ -190,12 +194,16 @@ describe('credentials', () => { connectionId: '', } // eslint-disable-next-line prefer-const - let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOutOfBandOffer( - offerOptions - ) + let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOffer(offerOptions) + + const { message: offerMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberCredentialRecord.id, + message, + domain: 'https://a-domain.com', + }) // Receive Message - await aliceAgent.receiveMessage(message.toJSON()) + await aliceAgent.receiveMessage(offerMessage.toJSON()) // Wait for it to be processed let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2connectionless-credentials.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2connectionless-credentials.test.ts index a72179ddc1..71111e8c6d 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2connectionless-credentials.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2connectionless-credentials.test.ts @@ -96,11 +96,15 @@ describe('credentials', () => { connectionId: '', } // eslint-disable-next-line prefer-const - let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOutOfBandOffer( - offerOptions - ) + let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOffer(offerOptions) - await aliceAgent.receiveMessage(message.toJSON()) + const { message: offerMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberCredentialRecord.id, + message, + domain: 'https://a-domain.com', + }) + + await aliceAgent.receiveMessage(offerMessage.toJSON()) let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { threadId: faberCredentialRecord.threadId, @@ -187,12 +191,16 @@ describe('credentials', () => { connectionId: '', } // eslint-disable-next-line prefer-const - let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOutOfBandOffer( - offerOptions - ) + let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOffer(offerOptions) + + const { message: offerMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberCredentialRecord.id, + message, + domain: 'https://a-domain.com', + }) // Receive Message - await aliceAgent.receiveMessage(message.toJSON()) + await aliceAgent.receiveMessage(offerMessage.toJSON()) // Wait for it to be processed let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { diff --git a/packages/core/src/modules/credentials/services/CredentialService.ts b/packages/core/src/modules/credentials/services/CredentialService.ts index 9f68eaba62..70150b3782 100644 --- a/packages/core/src/modules/credentials/services/CredentialService.ts +++ b/packages/core/src/modules/credentials/services/CredentialService.ts @@ -94,8 +94,6 @@ export abstract class CredentialService { abstract createOffer(options: OfferCredentialOptions): Promise> abstract processOffer(messageContext: HandlerInboundMessage): Promise - abstract createOutOfBandOffer(options: OfferCredentialOptions): Promise> - // methods for request abstract createRequest( credentialRecord: CredentialExchangeRecord, diff --git a/packages/core/src/modules/oob/OutOfBandModule.ts b/packages/core/src/modules/oob/OutOfBandModule.ts index 931f35e7c1..8f0ceb2466 100644 --- a/packages/core/src/modules/oob/OutOfBandModule.ts +++ b/packages/core/src/modules/oob/OutOfBandModule.ts @@ -24,7 +24,8 @@ import { ConnectionInvitationMessage, ConnectionsModule, } from '../../modules/connections' -import { JsonTransformer } from '../../utils' +import { DidCommMessageRepository, DidCommMessageRole } from '../../storage' +import { JsonEncoder, JsonTransformer } from '../../utils' import { parseMessageType, supportsIncomingMessageType } from '../../utils/messageType' import { DidKey } from '../dids' import { didKeyToVerkey } from '../dids/helpers' @@ -82,6 +83,7 @@ export class OutOfBandModule { private outOfBandService: OutOfBandService private mediationRecipientService: MediationRecipientService private connectionsModule: ConnectionsModule + private didCommMessageRepository: DidCommMessageRepository private dispatcher: Dispatcher private messageSender: MessageSender private eventEmitter: EventEmitter @@ -94,6 +96,7 @@ export class OutOfBandModule { outOfBandService: OutOfBandService, mediationRecipientService: MediationRecipientService, connectionsModule: ConnectionsModule, + didCommMessageRepository: DidCommMessageRepository, messageSender: MessageSender, eventEmitter: EventEmitter ) { @@ -103,6 +106,7 @@ export class OutOfBandModule { this.outOfBandService = outOfBandService this.mediationRecipientService = mediationRecipientService this.connectionsModule = connectionsModule + this.didCommMessageRepository = didCommMessageRepository this.messageSender = messageSender this.eventEmitter = eventEmitter this.registerHandlers(dispatcher) @@ -232,6 +236,35 @@ export class OutOfBandModule { return { outOfBandRecord, invitation: convertToOldInvitation(outOfBandRecord.outOfBandInvitation) } } + public async createLegacyConnectionlessInvitation(config: { + recordId: string + message: Message + domain: string + }): Promise<{ message: Message; invitationUrl: string }> { + // Create keys (and optionally register them at the mediator) + const routing = await this.mediationRecipientService.getRouting() + + // Set the service on the message + config.message.service = new ServiceDecorator({ + serviceEndpoint: routing.endpoints[0], + recipientKeys: [routing.recipientKey].map((key) => key.publicKeyBase58), + routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), + }) + + // We need to update the message with the new service, so we can + // retrieve it from storage later on. + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: config.message, + associatedRecordId: config.recordId, + role: DidCommMessageRole.Sender, + }) + + return { + message: config.message, + invitationUrl: `${config.domain}?d_m=${JsonEncoder.toBase64URL(JsonTransformer.toJSON(config.message))}`, + } + } + /** * Parses URL, decodes invitation and calls `receiveMessage` with parsed invitation message. * diff --git a/packages/core/src/storage/__tests__/DidCommMessageRepository.test.ts b/packages/core/src/storage/__tests__/DidCommMessageRepository.test.ts index 732e1c16fd..a584bdb3f5 100644 --- a/packages/core/src/storage/__tests__/DidCommMessageRepository.test.ts +++ b/packages/core/src/storage/__tests__/DidCommMessageRepository.test.ts @@ -71,6 +71,7 @@ describe('Repository', () => { }) expect(invitation).toBeInstanceOf(ConnectionInvitationMessage) }) + it("should return null because the record doesn't exist", async () => { mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([])) @@ -124,6 +125,7 @@ describe('Repository', () => { }) ) }) + it('should transform and update the agent message', async () => { const record = getRecord({ id: 'test-id' }) mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([record])) @@ -133,6 +135,12 @@ describe('Repository', () => { associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', }) + expect(storageMock.findByQuery).toBeCalledWith(DidCommMessageRecord, { + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + messageName: 'invitation', + protocolName: 'connections', + protocolMajorVersion: '1', + }) expect(storageMock.update).toBeCalledWith(record) }) }) diff --git a/packages/core/src/storage/didcomm/DidCommMessageRepository.ts b/packages/core/src/storage/didcomm/DidCommMessageRepository.ts index 6051145e68..e407499695 100644 --- a/packages/core/src/storage/didcomm/DidCommMessageRepository.ts +++ b/packages/core/src/storage/didcomm/DidCommMessageRepository.ts @@ -5,6 +5,7 @@ import type { DidCommMessageRole } from './DidCommMessageRole' import { inject, scoped, Lifecycle } from 'tsyringe' import { InjectionSymbols } from '../../constants' +import { parseMessageType } from '../../utils/messageType' import { Repository } from '../Repository' import { StorageService } from '../StorageService' @@ -27,9 +28,13 @@ export class DidCommMessageRepository extends Repository { } public async saveOrUpdateAgentMessage(options: SaveAgentMessageOptions) { + const { messageName, protocolName, protocolMajorVersion } = parseMessageType(options.agentMessage.type) + const record = await this.findSingleByQuery({ associatedRecordId: options.associatedRecordId, - messageType: options.agentMessage.type, + messageName: messageName, + protocolName: protocolName, + protocolMajorVersion: String(protocolMajorVersion), }) if (record) { diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 2611d0113b..abb0693cda 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -481,11 +481,15 @@ export async function issueConnectionLessCredential({ connectionId: '', } // eslint-disable-next-line prefer-const - let { credentialRecord: issuerCredentialRecord, message } = await issuerAgent.credentials.createOutOfBandOffer( - offerOptions - ) + let { credentialRecord: issuerCredentialRecord, message } = await issuerAgent.credentials.createOffer(offerOptions) + + const { message: offerMessage } = await issuerAgent.oob.createLegacyConnectionlessInvitation({ + recordId: issuerCredentialRecord.id, + domain: 'https://example.org', + message, + }) - await holderAgent.receiveMessage(message.toJSON()) + await holderAgent.receiveMessage(offerMessage.toJSON()) let holderCredentialRecord = await waitForCredentialRecordSubject(holderReplay, { threadId: issuerCredentialRecord.threadId, diff --git a/packages/core/tests/oob.test.ts b/packages/core/tests/oob.test.ts index b3e9afad46..b7c579ac47 100644 --- a/packages/core/tests/oob.test.ts +++ b/packages/core/tests/oob.test.ts @@ -14,6 +14,8 @@ import { OutOfBandEventTypes } from '../src/modules/oob/domain/OutOfBandEvents' import { OutOfBandRole } from '../src/modules/oob/domain/OutOfBandRole' import { OutOfBandState } from '../src/modules/oob/domain/OutOfBandState' import { OutOfBandInvitation } from '../src/modules/oob/messages' +import { DidCommMessageRepository, DidCommMessageRole } from '../src/storage' +import { JsonEncoder } from '../src/utils' import { TestMessage } from './TestMessage' import { getBaseConfig, prepareForIssuance, waitForCredentialRecord } from './helpers' @@ -181,7 +183,7 @@ describe('out of band', () => { }) test('create OOB message only with requests', async () => { - const { message } = await faberAgent.credentials.createOutOfBandOffer(credentialTemplate) + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ label: 'test-connection', handshake: false, @@ -205,7 +207,7 @@ describe('out of band', () => { }) test('create OOB message with both handshake and requests', async () => { - const { message } = await faberAgent.credentials.createOutOfBandOffer(credentialTemplate) + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ label: 'test-connection', handshakeProtocols: [HandshakeProtocol.Connections], @@ -328,7 +330,7 @@ describe('out of band', () => { }) test('process credential offer requests based on OOB message', async () => { - const { message } = await faberAgent.credentials.createOutOfBandOffer(credentialTemplate) + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ ...issueCredentialConfig, messages: [message], @@ -350,7 +352,7 @@ describe('out of band', () => { const eventListener = jest.fn() aliceAgent.events.on(AgentEventTypes.AgentMessageReceived, eventListener) - const { message } = await faberAgent.credentials.createOutOfBandOffer(credentialTemplate) + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ ...makeConnectionConfig, messages: [message], @@ -366,7 +368,7 @@ describe('out of band', () => { }) test('make a connection based on OOB invitation and process requests after the acceptation', async () => { - const { message } = await faberAgent.credentials.createOutOfBandOffer(credentialTemplate) + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) const outOfBandRecord = await faberAgent.oob.createInvitation({ ...makeConnectionConfig, messages: [message], @@ -642,7 +644,7 @@ describe('out of band', () => { }) test('throw an error when a did is used in the out of band message', async () => { - const { message } = await faberAgent.credentials.createOutOfBandOffer(credentialTemplate) + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ ...issueCredentialConfig, messages: [message], @@ -654,4 +656,52 @@ describe('out of band', () => { ) }) }) + + describe('createLegacyConnectionlessInvitation', () => { + test('add ~service decorator to the message and returns invitation url', async () => { + const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) + + const { message: offerMessage, invitationUrl } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: credentialRecord.id, + domain: 'https://test.com', + message, + }) + + expect(offerMessage.service).toMatchObject({ + serviceEndpoint: expect.any(String), + recipientKeys: [expect.any(String)], + routingKeys: [], + }) + + expect(invitationUrl).toEqual(expect.stringContaining('https://test.com?d_m=')) + + const messageBase64 = invitationUrl.split('https://test.com?d_m=')[1] + + expect(JsonEncoder.fromBase64(messageBase64)).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', + }) + }) + + test('updates the message in the didCommMessageRepository', async () => { + const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) + + const didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const saveOrUpdateSpy = jest.spyOn(didCommMessageRepository, 'saveOrUpdateAgentMessage') + saveOrUpdateSpy.mockResolvedValue() + + await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: credentialRecord.id, + domain: 'https://test.com', + message, + }) + + expect(saveOrUpdateSpy).toHaveBeenCalledWith({ + agentMessage: message, + associatedRecordId: credentialRecord.id, + role: DidCommMessageRole.Sender, + }) + }) + }) })