From fc8bb795b54de569f7b3bf5ca9276538b66045b4 Mon Sep 17 00:00:00 2001 From: Kolezhniuk Date: Sun, 15 Sep 2024 10:19:49 +0200 Subject: [PATCH 1/5] Directives. Init --- flow.drawio | 40 ++++ src/iden3comm/handlers/auth.ts | 21 +- src/iden3comm/handlers/contract-request.ts | 9 +- src/iden3comm/handlers/credential-proposal.ts | 64 +++++- src/iden3comm/handlers/message-handler.ts | 48 +++- src/iden3comm/types/index.ts | 2 + src/iden3comm/types/packer.ts | 9 +- src/iden3comm/types/protocol/auth.ts | 12 +- src/iden3comm/types/protocol/credentials.ts | 16 +- src/iden3comm/types/protocol/directive.ts | 31 +++ src/iden3comm/types/protocol/messages.ts | 3 +- src/iden3comm/types/protocol/metadata.ts | 8 + src/iden3comm/types/protocol/proof.ts | 8 +- .../types/protocol/proposal-request.ts | 3 +- src/iden3comm/types/protocol/revocation.ts | 4 +- src/storage/entities/index.ts | 1 + src/storage/entities/metadata.ts | 9 + src/storage/interfaces/data-storage.ts | 2 + src/storage/interfaces/index.ts | 1 + src/storage/interfaces/metadata.ts | 6 + src/storage/shared/index.ts | 1 + src/storage/shared/metadata-storage.ts | 61 ++++++ tests/handlers/contract-request.test.ts | 9 +- tests/handlers/directives.test.ts | 207 ++++++++++++++++++ tests/handlers/revocation-status.test.ts | 3 +- tests/helpers.ts | 25 ++- 26 files changed, 567 insertions(+), 36 deletions(-) create mode 100644 flow.drawio create mode 100644 src/iden3comm/types/protocol/directive.ts create mode 100644 src/iden3comm/types/protocol/metadata.ts create mode 100644 src/storage/entities/metadata.ts create mode 100644 src/storage/interfaces/metadata.ts create mode 100644 src/storage/shared/metadata-storage.ts create mode 100644 tests/handlers/directives.test.ts diff --git a/flow.drawio b/flow.drawio new file mode 100644 index 00000000..171441b6 --- /dev/null +++ b/flow.drawio @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/iden3comm/handlers/auth.ts b/src/iden3comm/handlers/auth.ts index ff13cb72..33837a84 100644 --- a/src/iden3comm/handlers/auth.ts +++ b/src/iden3comm/handlers/auth.ts @@ -21,6 +21,7 @@ import { byteDecoder, byteEncoder } from '../../utils'; import { processZeroKnowledgeProofRequests } from './common'; import { CircuitId } from '../../circuits'; import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; +import { IMetadataStorage } from '../../storage'; /** * createAuthorizationRequest is a function to create protocol authorization request @@ -186,12 +187,22 @@ export class AuthHandler */ constructor( private readonly _packerMgr: IPackageManager, - private readonly _proofService: IProofService + private readonly _proofService: IProofService, + private readonly _opts?: { + metadataStorage: IMetadataStorage; + } ) { super(); } - handle(message: BasicMessage, ctx: AuthMessageHandlerOptions): Promise { + async handle( + message: BasicMessage, + ctx: AuthMessageHandlerOptions + ): Promise { + await this.processMessageAttachments(message, { + metadataStorage: this._opts?.metadataStorage + }); + switch (message.type) { case PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE: return this.handleAuthRequest( @@ -286,7 +297,7 @@ export class AuthHandler throw new Error(`jws packer options are required for ${MediaType.SignedMessage}`); } - const authResponse = await this.handleAuthRequest(authRequest, { + const authResponse = await this.handle(authRequest, { senderDid: did, mediaType: opts.mediaType }); @@ -307,7 +318,7 @@ export class AuthHandler }) ); - return { authRequest, authResponse, token }; + return { authRequest, authResponse: authResponse as AuthorizationResponseMessage, token }; } private async handleAuthResponse( @@ -408,7 +419,7 @@ export class AuthHandler request: AuthorizationRequestMessage; response: AuthorizationResponseMessage; }> { - const authResp = (await this.handleAuthResponse(response, { + const authResp = (await this.handle(response, { request, acceptedStateTransitionDelay: opts?.acceptedStateTransitionDelay, acceptedProofGenerationDelay: opts?.acceptedProofGenerationDelay diff --git a/src/iden3comm/handlers/contract-request.ts b/src/iden3comm/handlers/contract-request.ts index 5c356f53..dc1286bf 100644 --- a/src/iden3comm/handlers/contract-request.ts +++ b/src/iden3comm/handlers/contract-request.ts @@ -4,7 +4,7 @@ import { PROTOCOL_MESSAGE_TYPE } from '../constants'; import { BasicMessage, IPackageManager, ZeroKnowledgeProofResponse } from '../types'; import { ContractInvokeRequest, ContractInvokeResponse } from '../types/protocol/contract-request'; import { DID, ChainIds } from '@iden3/js-iden3-core'; -import { FunctionSignatures, IOnChainZKPVerifier } from '../../storage'; +import { FunctionSignatures, IMetadataStorage, IOnChainZKPVerifier } from '../../storage'; import { Signer } from 'ethers'; import { processZeroKnowledgeProofRequests } from './common'; import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; @@ -81,7 +81,8 @@ export class ContractRequestHandler constructor( private readonly _packerMgr: IPackageManager, private readonly _proofService: IProofService, - private readonly _zkpVerifier: IOnChainZKPVerifier + private readonly _zkpVerifier: IOnChainZKPVerifier, + private readonly _opts?: { MetadataStorage: IMetadataStorage } ) { super(); } @@ -90,6 +91,7 @@ export class ContractRequestHandler message: BasicMessage, ctx: ContractMessageHandlerOptions ): Promise { + await this.processMessageAttachments(message, { metadataStorage: this._opts?.MetadataStorage }); switch (message.type) { case PROTOCOL_MESSAGE_TYPE.CONTRACT_INVOKE_REQUEST_MESSAGE_TYPE: { const ciMessage = message as ContractInvokeRequest; @@ -184,6 +186,9 @@ export class ContractRequestHandler request: ContractInvokeRequest, txHashToZkpResponseMap: Map ): Promise { + if (!request.to) { + throw new Error('Invalid contract invoke request'); + } const contractInvokeResponse: ContractInvokeResponse = { id: request.id, thid: request.thid, diff --git a/src/iden3comm/handlers/credential-proposal.ts b/src/iden3comm/handlers/credential-proposal.ts index ae2496ec..68b6280a 100644 --- a/src/iden3comm/handlers/credential-proposal.ts +++ b/src/iden3comm/handlers/credential-proposal.ts @@ -1,3 +1,4 @@ +import { Iden3Metadata, Iden3MetadataType } from '../types/protocol/metadata'; import { PROTOCOL_MESSAGE_TYPE, MediaType } from '../constants'; import { BasicMessage, @@ -21,12 +22,14 @@ import { IIdentityWallet } from '../../identity'; import { byteEncoder } from '../../utils'; import { W3CCredential } from '../../verifiable'; import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; +import { IMetadataStorage } from '../../storage'; /** @beta ProposalRequestCreationOptions represents proposal-request creation options */ export type ProposalRequestCreationOptions = { credentials: ProposalRequestCredential[]; - metadata?: { type: string; data?: JsonDocumentObject }; + metadata?: { type: string; data?: JsonDocumentObject } | Iden3Metadata; did_doc?: JsonDocumentObject; + thid?: string; }; /** @@ -45,7 +48,7 @@ export function createProposalRequest( const uuidv4 = uuid.v4(); const request: ProposalRequestMessage = { id: uuidv4, - thid: uuidv4, + thid: opts.thid ?? uuidv4, from: sender.string(), to: receiver.string(), typ: MediaType.PlainMessage, @@ -98,6 +101,20 @@ export interface ICredentialProposalHandler { */ parseProposalRequest(request: Uint8Array): Promise; + /** + * @beta + * creates proposal-request + * @param {ProposalRequestCreationOptions} params - creation options + * @returns `Promise` + */ + createProposalRequestPacked( + params: { + thid: string; + sender: DID; + receiver: DID; + } & ProposalRequestCreationOptions + ): Promise; + /** * @beta * handle proposal-request @@ -140,6 +157,7 @@ export type CredentialProposalHandlerParams = { agentUrl: string; proposalResolverFn: (context: string, type: string) => Promise; packerParams: PackerParams; + metadataStorage?: IMetadataStorage; }; /** @@ -169,6 +187,48 @@ export class CredentialProposalHandler super(); } + /** + * @inheritdoc ICredentialProposalHandler#createProposalRequest + */ + async createProposalRequestPacked( + params: { + thid: string; + sender: DID; + receiver: DID; + } & ProposalRequestCreationOptions + ): Promise { + if (!this._params.metadataStorage) { + throw new Error('metadata storage is required'); + } + + const directives = + await this._params.metadataStorage.getUnprocessedMetadataForThreadIdAndPurpose( + params.thid, + PROTOCOL_MESSAGE_TYPE.PROPOSAL_REQUEST_MESSAGE_TYPE + ); + + const metadata = directives.length + ? ({ + type: 'Iden3Metadata', + data: directives.map((directive) => { + const directiveData = JSON.parse(directive.jsonString); + delete directiveData.purpose; + return directiveData; + }) + } as Iden3Metadata) + : params.metadata; + + const request = createProposalRequest(params.sender, params.receiver, { + credentials: params.credentials, + metadata, + + did_doc: params.did_doc, + thid: params.thid + }); + + return request; + } + public async handle( message: BasicMessage, context: ProposalRequestHandlerOptions diff --git a/src/iden3comm/handlers/message-handler.ts b/src/iden3comm/handlers/message-handler.ts index 69ab8ce1..57340355 100644 --- a/src/iden3comm/handlers/message-handler.ts +++ b/src/iden3comm/handlers/message-handler.ts @@ -1,4 +1,4 @@ -import { BasicMessage, IPackageManager } from '../types'; +import { BasicMessage, Iden3AttachmentType, IPackageManager } from '../types'; import { AuthMessageHandlerOptions } from './auth'; import { RevocationStatusMessageHandlerOptions } from './revocation-status'; import { ContractMessageHandlerOptions } from './contract-request'; @@ -6,6 +6,9 @@ import { PaymentHandlerOptions, PaymentRequestMessageHandlerOptions } from './pa import { MediaType } from '../constants'; import { proving } from '@iden3/js-jwz'; import { DID } from '@iden3/js-iden3-core'; +import { IMetadataStorage } from '../../storage'; +import * as uuid from 'uuid'; + /** * iden3 Protocol message handler interface */ @@ -44,6 +47,49 @@ export abstract class AbstractMessageHandler implements IProtocolMessageHandler if (this.nextMessageHandler) return this.nextMessageHandler.handle(message, context); return Promise.reject('Message handler not provided or message not supported'); } + + public async processMessageAttachments( + message: BasicMessage, + opts?: { metadataStorage?: IMetadataStorage } + ): Promise { + if (!message.attachments) { + return; + } + const metadataStorage = opts?.metadataStorage; + const threadId = message.thid; + + if (!threadId) { + console.warn('message should contain thid to process attachments'); + return; + } + + const directives = message.attachments.filter( + (attachment) => attachment.data['type'] === Iden3AttachmentType.Iden3Directives + ).flatMap((attachment) => attachment.data.directives); + + if (!directives.length) { + return; + } + + if (!metadataStorage) { + console.warn('Metadata storage not provided but required for processing message attachments'); + return; + } + + const metadataPromises = directives.map((directive) => { + return metadataStorage.save(threadId, { + id: uuid.v4(), + thid: threadId, + purpose: directive.purpose, + date: new Date().toISOString(), + type: 'directive', + status: 'pending', + jsonString: JSON.stringify(directive) + }); + }); + + await Promise.all(metadataPromises); + } } /** diff --git a/src/iden3comm/types/index.ts b/src/iden3comm/types/index.ts index 9f08e4cb..cc0bffb8 100644 --- a/src/iden3comm/types/index.ts +++ b/src/iden3comm/types/index.ts @@ -6,6 +6,8 @@ export * from './protocol/revocation'; export * from './protocol/contract-request'; export * from './protocol/proposal-request'; export * from './protocol/payment'; +export * from './protocol/directive'; +export * from './protocol/metadata'; export * from './packer'; export * from './models'; diff --git a/src/iden3comm/types/packer.ts b/src/iden3comm/types/packer.ts index db4ca2a6..8931220e 100644 --- a/src/iden3comm/types/packer.ts +++ b/src/iden3comm/types/packer.ts @@ -2,9 +2,11 @@ import { DID } from '@iden3/js-iden3-core'; import { DataPrepareHandlerFunc, VerificationHandlerFunc } from '../packers'; import { ProvingMethodAlg } from '@iden3/js-jwz'; import { CircuitId } from '../../circuits'; -import { MediaType, PROTOCOL_MESSAGE_TYPE } from '../constants'; +import { PROTOCOL_MESSAGE_TYPE, MediaType } from '../constants'; import { DIDDocument, VerificationMethod } from 'did-resolver'; import { StateVerificationOpts } from './models'; +import { Iden3Metadata } from './protocol/metadata'; +import { Attachment } from './protocol/directive'; /** * Protocol message type @@ -43,9 +45,10 @@ export type BasicMessage = { typ?: MediaType; type: ProtocolMessage; thid?: string; - body?: unknown; - from?: string; + body?: unknown | { metadata?: Iden3Metadata }; + from: string; to?: string; + attachments?: Attachment[]; }; /** diff --git a/src/iden3comm/types/protocol/auth.ts b/src/iden3comm/types/protocol/auth.ts index 6f787cdb..7448a743 100644 --- a/src/iden3comm/types/protocol/auth.ts +++ b/src/iden3comm/types/protocol/auth.ts @@ -5,32 +5,30 @@ import { PROTOCOL_MESSAGE_TYPE } from '../../constants'; /** AuthorizationResponseMessage is struct the represents iden3message authorization response */ export type AuthorizationResponseMessage = BasicMessage & { body: AuthorizationMessageResponseBody; - from: string; - to: string; type: typeof PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_RESPONSE_MESSAGE_TYPE; + to: string; }; /** AuthorizationMessageResponseBody is struct the represents authorization response data */ -export type AuthorizationMessageResponseBody = { +export type AuthorizationMessageResponseBody = BasicMessage['body'] & { did_doc?: JsonDocumentObject; message?: string; - scope: Array; + scope: ZeroKnowledgeProofResponse[]; }; /** AuthorizationRequestMessage is struct the represents iden3message authorization request */ export type AuthorizationRequestMessage = BasicMessage & { body: AuthorizationRequestMessageBody; - from: string; type: typeof PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE; }; /** AuthorizationRequestMessageBody is body for authorization request */ -export type AuthorizationRequestMessageBody = { +export type AuthorizationRequestMessageBody = BasicMessage['body'] & { callbackUrl: string; reason?: string; message?: string; did_doc?: JsonDocumentObject; - scope: Array; + scope: ZeroKnowledgeProofRequest[]; }; /** ZeroKnowledgeProofRequest represents structure of zkp request object */ diff --git a/src/iden3comm/types/protocol/credentials.ts b/src/iden3comm/types/protocol/credentials.ts index 099a7832..146d088d 100644 --- a/src/iden3comm/types/protocol/credentials.ts +++ b/src/iden3comm/types/protocol/credentials.ts @@ -11,15 +11,17 @@ export type CredentialIssuanceRequestMessageBody = { }; /** CredentialIssuanceRequestMessage represent Iden3message for credential request */ -export type CredentialIssuanceRequestMessage = Required & { +export type CredentialIssuanceRequestMessage = BasicMessage & { body: CredentialIssuanceRequestMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.CREDENTIAL_ISSUANCE_REQUEST_MESSAGE_TYPE; + to: string; }; /** CredentialsOfferMessage represent Iden3message for credential offer */ -export type CredentialsOfferMessage = Required & { +export type CredentialsOfferMessage = BasicMessage & { body: CredentialsOfferMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.CREDENTIAL_OFFER_MESSAGE_TYPE; + to: string; }; /** CredentialsOfferMessageBody is struct the represents offer message */ @@ -29,9 +31,10 @@ export type CredentialsOfferMessageBody = { }; /** CredentialsOnchainOfferMessage represent Iden3message for credential onchain offer message */ -export type CredentialsOnchainOfferMessage = Required & { +export type CredentialsOnchainOfferMessage = BasicMessage & { body: CredentialsOnchainOfferMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.CREDENTIAL_ONCHAIN_OFFER_MESSAGE_TYPE; + to: string; }; /** CredentialsOnchainOfferMessageBody is struct the represents onchain offer message body */ @@ -55,9 +58,10 @@ export type CredentialOffer = { }; /** CredentialIssuanceMessage represent Iden3message for credential issuance */ -export type CredentialIssuanceMessage = Required & { +export type CredentialIssuanceMessage = BasicMessage & { body: IssuanceMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.CREDENTIAL_ISSUANCE_RESPONSE_MESSAGE_TYPE; + to: string; }; /** IssuanceMessageBody is struct the represents message when credential is issued */ @@ -69,6 +73,7 @@ export type IssuanceMessageBody = { export type CredentialFetchRequestMessage = BasicMessage & { body: CredentialFetchRequestMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.CREDENTIAL_FETCH_REQUEST_MESSAGE_TYPE; + to: string; }; /** CredentialFetchRequestMessageBody is msg body for fetch request */ @@ -84,9 +89,10 @@ export type Schema = { }; /** CredentialRefreshMessage represent Iden3message for credential refresh request */ -export type CredentialRefreshMessage = Required & { +export type CredentialRefreshMessage = BasicMessage & { body: CredentialRefreshMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.CREDENTIAL_REFRESH_MESSAGE_TYPE; + to: string; }; /** CredentialRefreshMessageBody is msg body for refresh request */ diff --git a/src/iden3comm/types/protocol/directive.ts b/src/iden3comm/types/protocol/directive.ts new file mode 100644 index 00000000..e864301e --- /dev/null +++ b/src/iden3comm/types/protocol/directive.ts @@ -0,0 +1,31 @@ +import { PROTOCOL_MESSAGE_TYPE } from '../../constants'; + +export type Iden3DirectiveType = 'TransparentPaymentDirective'; + +export type TransparentPaymentDirective = { + type: Iden3DirectiveType; + purpose: (typeof PROTOCOL_MESSAGE_TYPE)[keyof typeof PROTOCOL_MESSAGE_TYPE]; + credential: { + type: string; + context: string; + }[]; + paymentData: { + txId: string; + }; +}; + +export enum Iden3AttachmentType { + Iden3Directives = 'Iden3Directives' +} + +export type Iden3Directive = TransparentPaymentDirective; // Union type if more directive types are added later + +export type Iden3Directives = { + type: Iden3AttachmentType; + context: string; + directives: Iden3Directive[]; +}; + +export type Attachment = { + data: Iden3Directives; +}; diff --git a/src/iden3comm/types/protocol/messages.ts b/src/iden3comm/types/protocol/messages.ts index d06cf1bc..826d4b3e 100644 --- a/src/iden3comm/types/protocol/messages.ts +++ b/src/iden3comm/types/protocol/messages.ts @@ -2,9 +2,10 @@ import { BasicMessage } from '../'; import { PROTOCOL_MESSAGE_TYPE } from '../../constants'; /** MessageFetchRequestMessage represent Iden3message for message fetch request. */ -export type MessageFetchRequestMessage = Required & { +export type MessageFetchRequestMessage = BasicMessage & { body: MessageFetchRequestMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.CREDENTIAL_FETCH_REQUEST_MESSAGE_TYPE; + to: string; }; /** MessageFetchRequestMessageBody is struct the represents body for message fetch request. */ diff --git a/src/iden3comm/types/protocol/metadata.ts b/src/iden3comm/types/protocol/metadata.ts new file mode 100644 index 00000000..62ea2c6b --- /dev/null +++ b/src/iden3comm/types/protocol/metadata.ts @@ -0,0 +1,8 @@ +import { PaymentRequestInfo } from './payment'; + +export type Iden3MetadataType = 'Iden3Metadata'; +export type Iden3MetadataItem = PaymentRequestInfo; +export type Iden3Metadata = { + type: Iden3MetadataType; + data: Iden3MetadataItem[]; +}; diff --git a/src/iden3comm/types/protocol/proof.ts b/src/iden3comm/types/protocol/proof.ts index 45c559e4..fe9f5b00 100644 --- a/src/iden3comm/types/protocol/proof.ts +++ b/src/iden3comm/types/protocol/proof.ts @@ -3,9 +3,10 @@ import { PROTOCOL_MESSAGE_TYPE } from '../../constants'; import { ZeroKnowledgeProofRequest, ZeroKnowledgeProofResponse } from './auth'; /** ProofGenerationRequestMessage is struct the represents body for proof generation request */ -export type ProofGenerationRequestMessage = Required & { +export type ProofGenerationRequestMessage = BasicMessage & { body: ProofGenerationRequestMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.PROOF_GENERATION_REQUEST_MESSAGE_TYPE; + to: string; }; /** ProofGenerationRequestMessageBody is struct the represents body for proof generation request */ @@ -14,12 +15,13 @@ export type ProofGenerationRequestMessageBody = { }; /** ProofGenerationResponseMessage is struct the represents body for proof generation request */ -export type ProofGenerationResponseMessage = Required & { +export type ProofGenerationResponseMessage = BasicMessage & { body: ResponseMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.PROOF_GENERATION_RESPONSE_MESSAGE_TYPE; + to: string; }; /** ResponseMessageBody is struct the represents request for revocation status */ export type ResponseMessageBody = { - scope: Array; + scope: ZeroKnowledgeProofResponse[]; }; diff --git a/src/iden3comm/types/protocol/proposal-request.ts b/src/iden3comm/types/protocol/proposal-request.ts index 300d2046..b05c7131 100644 --- a/src/iden3comm/types/protocol/proposal-request.ts +++ b/src/iden3comm/types/protocol/proposal-request.ts @@ -1,5 +1,6 @@ import { BasicMessage, JsonDocumentObject } from '../'; import { PROTOCOL_MESSAGE_TYPE } from '../../constants'; +import { Iden3Metadata } from './metadata'; /** @beta ProposalRequestMessage is struct the represents proposal-request message */ export type ProposalRequestMessage = BasicMessage & { @@ -10,7 +11,7 @@ export type ProposalRequestMessage = BasicMessage & { /** @beta ProposalRequestMessageBody is struct the represents body for proposal-request */ export type ProposalRequestMessageBody = { credentials: ProposalRequestCredential[]; - metadata?: { type: string; data?: JsonDocumentObject }; + metadata?: { type: string; data?: JsonDocumentObject } | Iden3Metadata; did_doc?: JsonDocumentObject; }; diff --git a/src/iden3comm/types/protocol/revocation.ts b/src/iden3comm/types/protocol/revocation.ts index d1024f4e..8691a3aa 100644 --- a/src/iden3comm/types/protocol/revocation.ts +++ b/src/iden3comm/types/protocol/revocation.ts @@ -6,6 +6,7 @@ import { PROTOCOL_MESSAGE_TYPE } from '../../constants'; export type RevocationStatusRequestMessage = BasicMessage & { body: RevocationStatusRequestMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.REVOCATION_STATUS_REQUEST_MESSAGE_TYPE; + to: string; }; /** RevocationStatusRequestMessageBody is struct the represents request for revocation status */ @@ -14,9 +15,10 @@ export type RevocationStatusRequestMessageBody = { }; /** RevocationStatusResponseMessage is struct the represents body for proof generation request */ -export type RevocationStatusResponseMessage = Required & { +export type RevocationStatusResponseMessage = BasicMessage & { body: RevocationStatusResponseMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.REVOCATION_STATUS_RESPONSE_MESSAGE_TYPE; + to: string; }; /** RevocationStatusResponseMessageBody is struct the represents request for revocation status */ diff --git a/src/storage/entities/index.ts b/src/storage/entities/index.ts index 32d0363a..e4d28ab8 100644 --- a/src/storage/entities/index.ts +++ b/src/storage/entities/index.ts @@ -2,3 +2,4 @@ export * from './identity'; export * from './mt'; export * from './state'; export * from './circuitData'; +export * from './metadata'; diff --git a/src/storage/entities/metadata.ts b/src/storage/entities/metadata.ts new file mode 100644 index 00000000..9b07679f --- /dev/null +++ b/src/storage/entities/metadata.ts @@ -0,0 +1,9 @@ +export type Metadata = { + id: string; + date: string; + thid: string; + type: 'directive'; + purpose: string; + status: 'pending' | 'completed' | 'failed' | 'not applicable'; + jsonString: string; +}; diff --git a/src/storage/interfaces/data-storage.ts b/src/storage/interfaces/data-storage.ts index f1d0fbda..de8f2628 100644 --- a/src/storage/interfaces/data-storage.ts +++ b/src/storage/interfaces/data-storage.ts @@ -1,6 +1,7 @@ import { ICredentialStorage } from './credentials'; import { IIdentityStorage } from './identity'; import { IMerkleTreeStorage } from './merkletree'; +import { IMetadataStorage } from './metadata'; import { IStateStorage } from './state'; /** @@ -14,4 +15,5 @@ export interface IDataStorage { identity: IIdentityStorage; mt: IMerkleTreeStorage; states: IStateStorage; + metadata?: IMetadataStorage; } diff --git a/src/storage/interfaces/index.ts b/src/storage/interfaces/index.ts index a24d2597..8cb68cad 100644 --- a/src/storage/interfaces/index.ts +++ b/src/storage/interfaces/index.ts @@ -7,3 +7,4 @@ export * from './data-source'; export * from './circuits'; export * from './onchain-zkp-verifier'; export * from './onchain-revocation'; +export * from './metadata'; diff --git a/src/storage/interfaces/metadata.ts b/src/storage/interfaces/metadata.ts new file mode 100644 index 00000000..efd2c752 --- /dev/null +++ b/src/storage/interfaces/metadata.ts @@ -0,0 +1,6 @@ +import { Metadata } from '../entities/metadata'; +import { IDataSource } from './data-source'; + +export interface IMetadataStorage extends IDataSource { + getUnprocessedMetadataForThreadIdAndPurpose(thid: string, purpose: string): Promise; +} diff --git a/src/storage/shared/index.ts b/src/storage/shared/index.ts index 2aca329a..c09ec28a 100644 --- a/src/storage/shared/index.ts +++ b/src/storage/shared/index.ts @@ -1,3 +1,4 @@ export * from './circuit-storage'; export * from './credential-storage'; export * from './identity-storage'; +export * from './metadata-storage'; diff --git a/src/storage/shared/metadata-storage.ts b/src/storage/shared/metadata-storage.ts new file mode 100644 index 00000000..b1495f9c --- /dev/null +++ b/src/storage/shared/metadata-storage.ts @@ -0,0 +1,61 @@ +import { Metadata } from '../entities/metadata'; +import { IDataSource } from '../interfaces'; +import { IMetadataStorage } from '../interfaces/metadata'; +/** + * Represents a storage for metadata. + */ +export class MetadataStorage implements IMetadataStorage { + private static keyName = 'id'; + static readonly storageKey = 'metadata'; + + /** + * Creates an instance of MetadataStorage. + * @param {IDataSource} _dataSource - The data source to store metadata. + */ + constructor(private readonly _dataSource: IDataSource) {} + + /** + * Retrieves all unprocessed metadata. + * @returns {Promise} A promise that resolves to an array of unprocessed metadata. + */ + async getUnprocessedMetadataForThreadIdAndPurpose(thid: string): Promise { + const data = await this._dataSource.load(); + return data.filter((metadata) => metadata.status === 'pending' && metadata.thid === thid); + } + + /** + * Loads the metadata from the data source. + * @returns {Promise} A promise that resolves to an array of metadata. + */ + load(): Promise { + return this._dataSource.load(); + } + + /** + * Saves the metadata with the specified key. + * @param {string} key - The key to save the metadata under. + * @param {Metadata} value - The metadata to save. + * @returns {Promise} A promise that resolves when the metadata is saved. + */ + save(key: string, value: Metadata): Promise { + return this._dataSource.save(key, value, MetadataStorage.keyName); + } + + /** + * Retrieves the metadata with the specified key. + * @param {string} key - The key of the metadata to retrieve. + * @returns {Promise} A promise that resolves to the retrieved metadata, or undefined if not found. + */ + get(key: string): Promise { + return this._dataSource.get(key, MetadataStorage.keyName); + } + + /** + * Deletes the metadata with the specified key. + * @param {string} key - The key of the metadata to delete. + * @returns {Promise} A promise that resolves when the metadata is deleted. + */ + delete(key: string): Promise { + return this._dataSource.delete(key, MetadataStorage.keyName); + } +} diff --git a/tests/handlers/contract-request.test.ts b/tests/handlers/contract-request.test.ts index ef9142e2..218835d5 100644 --- a/tests/handlers/contract-request.test.ts +++ b/tests/handlers/contract-request.test.ts @@ -299,7 +299,8 @@ describe('contract-request', () => { typ: MediaType.PlainMessage, type: PROTOCOL_MESSAGE_TYPE.CONTRACT_INVOKE_REQUEST_MESSAGE_TYPE, thid: id, - body: ciRequestBody + body: ciRequestBody, + from: issuerDID.string() }; const ethSigner = new ethers.Wallet(walletKey); @@ -444,7 +445,8 @@ describe('contract-request', () => { typ: MediaType.PlainMessage, type: PROTOCOL_MESSAGE_TYPE.CONTRACT_INVOKE_REQUEST_MESSAGE_TYPE, thid: id, - body: ciRequestBody + body: ciRequestBody, + from: issuerDID.string() }; const ethSigner = new ethers.Wallet(walletKey); @@ -778,7 +780,8 @@ describe('contract-request', () => { typ: MediaType.PlainMessage, type: PROTOCOL_MESSAGE_TYPE.CONTRACT_INVOKE_REQUEST_MESSAGE_TYPE, thid: id, - body: ciRequestBody + body: ciRequestBody, + from: issuerDID.string() }; const ethSigner = new ethers.Wallet(walletKey); diff --git a/tests/handlers/directives.test.ts b/tests/handlers/directives.test.ts new file mode 100644 index 00000000..a3e166b0 --- /dev/null +++ b/tests/handlers/directives.test.ts @@ -0,0 +1,207 @@ +/* eslint-disable no-console */ + +import path from 'path'; +import { + IDataStorage, + CredentialWallet, + ProofService, + CircuitId, + AuthHandler, + FSCircuitStorage, + IAuthHandler, + IdentityWallet, + CredentialStatusType, + AuthorizationRequestMessage, + IPackageManager, + RHSResolver, + CredentialStatusResolverRegistry, + PROTOCOL_CONSTANTS, + InMemoryDataSource, + Metadata, + MetadataStorage, + Iden3AttachmentType, + IMetadataStorage, + CredentialProposalHandler, + ICredentialProposalHandler +} from '../../src'; +import { DID } from '@iden3/js-iden3-core'; +import { expect } from 'chai'; +import * as uuid from 'uuid'; +import { + getInMemoryDataStorage, + registerKeyProvidersInMemoryKMS, + IPFS_URL, + getPackageMgr, + createIdentity, + SEED_USER, + MOCK_STATE_STORAGE +} from '../helpers'; +import { MediaType } from '../../src/iden3comm/constants'; + +describe('directives', () => { + let idWallet: IdentityWallet; + let credWallet: CredentialWallet; + + let dataStorage: IDataStorage; + let proofService: ProofService; + let authHandler: IAuthHandler; + let packageMgr: IPackageManager; + let userDID: DID; + let issuerDID: DID; + let metadataStorage: IMetadataStorage; + let proposalRequestHandler: ICredentialProposalHandler; + + beforeEach(async () => { + const kms = registerKeyProvidersInMemoryKMS(); + dataStorage = getInMemoryDataStorage(MOCK_STATE_STORAGE); + const circuitStorage = new FSCircuitStorage({ + dirname: path.join(__dirname, '../proofs/testdata') + }); + + const resolvers = new CredentialStatusResolverRegistry(); + resolvers.register( + CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + new RHSResolver(dataStorage.states) + ); + credWallet = new CredentialWallet(dataStorage, resolvers); + idWallet = new IdentityWallet(kms, dataStorage, credWallet); + + proofService = new ProofService(idWallet, credWallet, circuitStorage, MOCK_STATE_STORAGE, { + ipfsNodeURL: IPFS_URL + }); + + packageMgr = await getPackageMgr( + await circuitStorage.loadCircuitData(CircuitId.AuthV2), + proofService.generateAuthV2Inputs.bind(proofService), + proofService.verifyState.bind(proofService) + ); + metadataStorage = new MetadataStorage(new InMemoryDataSource()); + authHandler = new AuthHandler(packageMgr, proofService, { + metadataStorage + }); + + proposalRequestHandler = new CredentialProposalHandler(packageMgr, idWallet, { + agentUrl: 'http://localhost:8001/api/v1/agent', + proposalResolverFn: async (context: string, type: string) => { + if ( + context === + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-nonmerklized.jsonld' && + type === 'KYCAgeCredential' + ) { + return { + credentials: [ + { + type, + context + } + ], + type: 'WebVerificationForm', + url: 'http://issuer-agent.com/verify?anyUniqueIdentifierOfSession=55', + description: + 'you can pass the verification on our KYC provider by following the next link' + }; + } + + throw new Error(`not supported credential, type: ${type}, context: ${context}`); + }, + packerParams: { + mediaType: MediaType.PlainMessage + }, + metadataStorage + }); + + const { did: didUser } = await createIdentity(idWallet, { + seed: SEED_USER + }); + userDID = didUser; + + const { did: didIssuer } = await createIdentity(idWallet); + issuerDID = didIssuer; + }); + + it.only('request-response flow', async () => { + // verifier sends auth request to user, user has no credential yet, but auth request contains directive + const id = uuid.v4(); + const authReq: AuthorizationRequestMessage = { + id, + typ: PROTOCOL_CONSTANTS.MediaType.PlainMessage, + type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, + thid: id, + body: { + callbackUrl: 'http://localhost:8080/callback?id=1234442-123123-123123', + reason: 'reason', + message: 'message', + did_doc: {}, + scope: [ + { + id: 1, + circuitId: CircuitId.AtomicQueryV3, + optional: false, + query: { + allowedIssuers: ['*'], + type: 'KYCAgeCredential', + context: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-nonmerklized.jsonld', + credentialSubject: { + documentType: { + $eq: 99 + } + } + } + } + ] + }, + from: issuerDID.string(), + attachments: [ + { + data: { + type: Iden3AttachmentType.Iden3Directives, + context: 'https://directive.iden3.io/v1/context.json', + directives: [ + { + type: 'TransparentPaymentDirective', + purpose: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.PROPOSAL_REQUEST_MESSAGE_TYPE, + credential: [ + { + context: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-nonmerklized.jsonld', + type: 'KYCAgeCredential' + } + ], + paymentData: { + txId: '0x123' + } + } + ] + } + } + ] + }; + + const msgBytes = await packageMgr.packMessage(authReq.typ as MediaType, authReq, {}); + await expect(authHandler.handleAuthorizationRequest(userDID, msgBytes)).to.rejectedWith( + 'no credential satisfied query' + ); + expect(authReq.thid).not.to.be.undefined; + const metadata = await metadataStorage.get(authReq.thid!); + console.log('metadata', metadata); + expect( + await metadataStorage.getUnprocessedMetadataForThreadIdAndPurpose( + authReq.thid!, + PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.PROPOSAL_REQUEST_MESSAGE_TYPE + ) + ).not.to.be.undefined; + + // user wants to issue credential + + // user sends proposal request to issuer + const proposalReq = await proposalRequestHandler.createProposalRequestPacked({ + thid: authReq.thid!, + sender: userDID, + receiver: issuerDID, + credentials: [] + }); + console.log('proposalReq', proposalReq); + expect(proposalReq).not.to.be.undefined; + }); +}); diff --git a/tests/handlers/revocation-status.test.ts b/tests/handlers/revocation-status.test.ts index f8afb224..503f7c14 100644 --- a/tests/handlers/revocation-status.test.ts +++ b/tests/handlers/revocation-status.test.ts @@ -108,7 +108,8 @@ describe('revocation status', () => { body: { revocation_nonce: 1000 }, - to: issuerDID.string() + to: issuerDID.string(), + from: userDID.string() }; const msgBytes = byteEncoder.encode(JSON.stringify(rsReq)); diff --git a/tests/helpers.ts b/tests/helpers.ts index cc89b6fd..4a06c721 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -32,7 +32,8 @@ import { W3CCredential, ZKPPacker, byteEncoder, - VerifyOpts + VerifyOpts, + Proposal } from '../src'; import { proving } from '@iden3/js-jwz'; import { JsonRpcProvider } from 'ethers'; @@ -266,3 +267,25 @@ export const TEST_VERIFICATION_OPTS: VerifyOpts = { acceptedStateTransitionDelay: 5 * 60 * 1000, // 5 minutes acceptedProofGenerationDelay: 10 * 365 * 24 * 60 * 60 * 1000 // 10 years }; + +export const PROPOSAL_RESOLVER_FN_STUB = (context: string, type: string): Promise => { + if ( + context === + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-nonmerklized.jsonld' && + type === 'KYCAgeCredential' + ) { + return Promise.resolve({ + credentials: [ + { + type, + context + } + ], + type: 'WebVerificationForm', + url: 'http://issuer-agent.com/verify?anyUniqueIdentifierOfSession=55', + description: 'you can pass the verification on our KYC provider by following the next link' + }); + } + + throw new Error(`not supported credential, type: ${type}, context: ${context}`); +}; From 940ae0f49349e8f6050032f3ea4a5a75be91b952 Mon Sep 17 00:00:00 2001 From: Kolezhniuk Date: Sun, 15 Sep 2024 10:22:28 +0200 Subject: [PATCH 2/5] Fix linters --- src/iden3comm/handlers/credential-proposal.ts | 2 +- src/iden3comm/handlers/message-handler.ts | 6 +++--- tests/handlers/directives.test.ts | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/iden3comm/handlers/credential-proposal.ts b/src/iden3comm/handlers/credential-proposal.ts index 68b6280a..dfbc2263 100644 --- a/src/iden3comm/handlers/credential-proposal.ts +++ b/src/iden3comm/handlers/credential-proposal.ts @@ -1,4 +1,4 @@ -import { Iden3Metadata, Iden3MetadataType } from '../types/protocol/metadata'; +import { Iden3Metadata } from '../types/protocol/metadata'; import { PROTOCOL_MESSAGE_TYPE, MediaType } from '../constants'; import { BasicMessage, diff --git a/src/iden3comm/handlers/message-handler.ts b/src/iden3comm/handlers/message-handler.ts index 57340355..0d051ee9 100644 --- a/src/iden3comm/handlers/message-handler.ts +++ b/src/iden3comm/handlers/message-handler.ts @@ -63,9 +63,9 @@ export abstract class AbstractMessageHandler implements IProtocolMessageHandler return; } - const directives = message.attachments.filter( - (attachment) => attachment.data['type'] === Iden3AttachmentType.Iden3Directives - ).flatMap((attachment) => attachment.data.directives); + const directives = message.attachments + .filter((attachment) => attachment.data['type'] === Iden3AttachmentType.Iden3Directives) + .flatMap((attachment) => attachment.data.directives); if (!directives.length) { return; diff --git a/tests/handlers/directives.test.ts b/tests/handlers/directives.test.ts index a3e166b0..325fa46d 100644 --- a/tests/handlers/directives.test.ts +++ b/tests/handlers/directives.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable no-console */ import path from 'path'; From f11918fc64bb55df814b347ed47ace1b4d757cfe Mon Sep 17 00:00:00 2001 From: Kolezhniuk Date: Wed, 18 Sep 2024 06:53:12 +0200 Subject: [PATCH 3/5] Update message storage --- src/iden3comm/handlers/auth.ts | 8 +-- src/iden3comm/handlers/contract-request.ts | 6 +- src/iden3comm/handlers/credential-proposal.ts | 56 +++++++++++++------ src/iden3comm/handlers/message-handler.ts | 41 ++++++-------- .../types/protocol/proposal-request.ts | 3 +- src/storage/entities/index.ts | 2 +- src/storage/entities/message-model.ts | 16 ++++++ src/storage/entities/metadata.ts | 9 --- src/storage/interfaces/data-storage.ts | 4 +- src/storage/interfaces/index.ts | 2 +- src/storage/interfaces/message.ts | 30 ++++++++++ src/storage/interfaces/metadata.ts | 6 -- src/storage/shared/metadata-storage.ts | 40 +++++++++---- tests/handlers/directives.test.ts | 38 ++++++++----- 14 files changed, 166 insertions(+), 95 deletions(-) create mode 100644 src/storage/entities/message-model.ts delete mode 100644 src/storage/entities/metadata.ts create mode 100644 src/storage/interfaces/message.ts delete mode 100644 src/storage/interfaces/metadata.ts diff --git a/src/iden3comm/handlers/auth.ts b/src/iden3comm/handlers/auth.ts index 33837a84..267e3e4d 100644 --- a/src/iden3comm/handlers/auth.ts +++ b/src/iden3comm/handlers/auth.ts @@ -21,7 +21,7 @@ import { byteDecoder, byteEncoder } from '../../utils'; import { processZeroKnowledgeProofRequests } from './common'; import { CircuitId } from '../../circuits'; import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; -import { IMetadataStorage } from '../../storage'; +import { IIden3MessageStorage } from '../../storage'; /** * createAuthorizationRequest is a function to create protocol authorization request @@ -189,7 +189,7 @@ export class AuthHandler private readonly _packerMgr: IPackageManager, private readonly _proofService: IProofService, private readonly _opts?: { - metadataStorage: IMetadataStorage; + messageStorage: IIden3MessageStorage; } ) { super(); @@ -199,8 +199,8 @@ export class AuthHandler message: BasicMessage, ctx: AuthMessageHandlerOptions ): Promise { - await this.processMessageAttachments(message, { - metadataStorage: this._opts?.metadataStorage + await this.processMessage(message, { + messageStorage: this._opts?.messageStorage }); switch (message.type) { diff --git a/src/iden3comm/handlers/contract-request.ts b/src/iden3comm/handlers/contract-request.ts index dc1286bf..bfe2c341 100644 --- a/src/iden3comm/handlers/contract-request.ts +++ b/src/iden3comm/handlers/contract-request.ts @@ -4,7 +4,7 @@ import { PROTOCOL_MESSAGE_TYPE } from '../constants'; import { BasicMessage, IPackageManager, ZeroKnowledgeProofResponse } from '../types'; import { ContractInvokeRequest, ContractInvokeResponse } from '../types/protocol/contract-request'; import { DID, ChainIds } from '@iden3/js-iden3-core'; -import { FunctionSignatures, IMetadataStorage, IOnChainZKPVerifier } from '../../storage'; +import { FunctionSignatures, IIden3MessageStorage, IOnChainZKPVerifier } from '../../storage'; import { Signer } from 'ethers'; import { processZeroKnowledgeProofRequests } from './common'; import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; @@ -82,7 +82,7 @@ export class ContractRequestHandler private readonly _packerMgr: IPackageManager, private readonly _proofService: IProofService, private readonly _zkpVerifier: IOnChainZKPVerifier, - private readonly _opts?: { MetadataStorage: IMetadataStorage } + private readonly _opts?: { messageStorage: IIden3MessageStorage } ) { super(); } @@ -91,7 +91,7 @@ export class ContractRequestHandler message: BasicMessage, ctx: ContractMessageHandlerOptions ): Promise { - await this.processMessageAttachments(message, { metadataStorage: this._opts?.MetadataStorage }); + await this.processMessage(message, { messageStorage: this._opts?.messageStorage }); switch (message.type) { case PROTOCOL_MESSAGE_TYPE.CONTRACT_INVOKE_REQUEST_MESSAGE_TYPE: { const ciMessage = message as ContractInvokeRequest; diff --git a/src/iden3comm/handlers/credential-proposal.ts b/src/iden3comm/handlers/credential-proposal.ts index dfbc2263..e949e8f5 100644 --- a/src/iden3comm/handlers/credential-proposal.ts +++ b/src/iden3comm/handlers/credential-proposal.ts @@ -1,4 +1,3 @@ -import { Iden3Metadata } from '../types/protocol/metadata'; import { PROTOCOL_MESSAGE_TYPE, MediaType } from '../constants'; import { BasicMessage, @@ -22,12 +21,12 @@ import { IIdentityWallet } from '../../identity'; import { byteEncoder } from '../../utils'; import { W3CCredential } from '../../verifiable'; import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; -import { IMetadataStorage } from '../../storage'; +import { IIden3MessageStorage } from '../../storage'; /** @beta ProposalRequestCreationOptions represents proposal-request creation options */ export type ProposalRequestCreationOptions = { credentials: ProposalRequestCredential[]; - metadata?: { type: string; data?: JsonDocumentObject } | Iden3Metadata; + metadata?: { type: string; data?: JsonDocumentObject | JsonDocumentObject[] }; did_doc?: JsonDocumentObject; thid?: string; }; @@ -157,7 +156,7 @@ export type CredentialProposalHandlerParams = { agentUrl: string; proposalResolverFn: (context: string, type: string) => Promise; packerParams: PackerParams; - metadataStorage?: IMetadataStorage; + metadataStorage?: IIden3MessageStorage; }; /** @@ -201,29 +200,54 @@ export class CredentialProposalHandler throw new Error('metadata storage is required'); } - const directives = - await this._params.metadataStorage.getUnprocessedMetadataForThreadIdAndPurpose( - params.thid, - PROTOCOL_MESSAGE_TYPE.PROPOSAL_REQUEST_MESSAGE_TYPE + const messages: BasicMessage[] = ( + await this._params.metadataStorage.getMessageByThreadId(params.thid, 'pending') + ).map((message) => JSON.parse(message.jsonString)); + + //TODO: handle multiple messages + if (messages.length > 1) { + throw new Error('multiple messages are not supported'); + } + + const directives = (messages[0].attachments ?? []) + // .flatMap((message) => message.attachments ?? []) + .filter( + (attachment) => + attachment?.data?.type === 'Iden3Directives' && attachment?.data?.directives.length + ) + .flatMap((attachment) => attachment.data.directives ?? []) + .filter( + (directive) => directive.purpose === PROTOCOL_MESSAGE_TYPE.PROPOSAL_REQUEST_MESSAGE_TYPE ); const metadata = directives.length - ? ({ + ? { type: 'Iden3Metadata', - data: directives.map((directive) => { - const directiveData = JSON.parse(directive.jsonString); - delete directiveData.purpose; - return directiveData; + data: directives.map((directive: JsonDocumentObject) => { + delete directive.purpose; + return directive; }) - } as Iden3Metadata) + } : params.metadata; const request = createProposalRequest(params.sender, params.receiver, { credentials: params.credentials, metadata, - did_doc: params.did_doc, - thid: params.thid + thid: uuid.v4() + }); + + // todo: is it correct and right place to save the message before sending it? + // + await this._params.metadataStorage.save(params.thid, { + id: request.id, + thid: request.thid, + createdAt: new Date().toISOString(), + status: 'pending', + correlationId: messages[0].id, + type: request.type, + correlationThid: messages[0].thid, + jsonString: JSON.stringify(request) }); return request; diff --git a/src/iden3comm/handlers/message-handler.ts b/src/iden3comm/handlers/message-handler.ts index 0d051ee9..9f6c44a4 100644 --- a/src/iden3comm/handlers/message-handler.ts +++ b/src/iden3comm/handlers/message-handler.ts @@ -1,4 +1,4 @@ -import { BasicMessage, Iden3AttachmentType, IPackageManager } from '../types'; +import { BasicMessage, IPackageManager } from '../types'; import { AuthMessageHandlerOptions } from './auth'; import { RevocationStatusMessageHandlerOptions } from './revocation-status'; import { ContractMessageHandlerOptions } from './contract-request'; @@ -6,7 +6,7 @@ import { PaymentHandlerOptions, PaymentRequestMessageHandlerOptions } from './pa import { MediaType } from '../constants'; import { proving } from '@iden3/js-jwz'; import { DID } from '@iden3/js-iden3-core'; -import { IMetadataStorage } from '../../storage'; +import { IIden3MessageStorage } from '../../storage'; import * as uuid from 'uuid'; /** @@ -48,14 +48,14 @@ export abstract class AbstractMessageHandler implements IProtocolMessageHandler return Promise.reject('Message handler not provided or message not supported'); } - public async processMessageAttachments( + public async processMessage( message: BasicMessage, - opts?: { metadataStorage?: IMetadataStorage } + opts?: { messageStorage?: IIden3MessageStorage } ): Promise { if (!message.attachments) { return; } - const metadataStorage = opts?.metadataStorage; + const messageStorage = opts?.messageStorage; const threadId = message.thid; if (!threadId) { @@ -63,32 +63,25 @@ export abstract class AbstractMessageHandler implements IProtocolMessageHandler return; } - const directives = message.attachments - .filter((attachment) => attachment.data['type'] === Iden3AttachmentType.Iden3Directives) - .flatMap((attachment) => attachment.data.directives); - - if (!directives.length) { + if (!messageStorage) { + console.warn('Metadata storage not provided but required for processing message attachments'); return; } - if (!metadataStorage) { - console.warn('Metadata storage not provided but required for processing message attachments'); + const alreadySavedMessage = await messageStorage.get(message.id); + if (alreadySavedMessage) { + console.warn('Message already saved'); return; } - const metadataPromises = directives.map((directive) => { - return metadataStorage.save(threadId, { - id: uuid.v4(), - thid: threadId, - purpose: directive.purpose, - date: new Date().toISOString(), - type: 'directive', - status: 'pending', - jsonString: JSON.stringify(directive) - }); + await messageStorage.save(threadId, { + id: uuid.v4(), + thid: threadId, + createdAt: new Date().toISOString(), + type: message.type, + status: 'pending', + jsonString: JSON.stringify(message) }); - - await Promise.all(metadataPromises); } } diff --git a/src/iden3comm/types/protocol/proposal-request.ts b/src/iden3comm/types/protocol/proposal-request.ts index b05c7131..be4cabdb 100644 --- a/src/iden3comm/types/protocol/proposal-request.ts +++ b/src/iden3comm/types/protocol/proposal-request.ts @@ -1,6 +1,5 @@ import { BasicMessage, JsonDocumentObject } from '../'; import { PROTOCOL_MESSAGE_TYPE } from '../../constants'; -import { Iden3Metadata } from './metadata'; /** @beta ProposalRequestMessage is struct the represents proposal-request message */ export type ProposalRequestMessage = BasicMessage & { @@ -11,7 +10,7 @@ export type ProposalRequestMessage = BasicMessage & { /** @beta ProposalRequestMessageBody is struct the represents body for proposal-request */ export type ProposalRequestMessageBody = { credentials: ProposalRequestCredential[]; - metadata?: { type: string; data?: JsonDocumentObject } | Iden3Metadata; + metadata?: { type: string; data?: JsonDocumentObject | JsonDocumentObject[] }; did_doc?: JsonDocumentObject; }; diff --git a/src/storage/entities/index.ts b/src/storage/entities/index.ts index e4d28ab8..df639dd1 100644 --- a/src/storage/entities/index.ts +++ b/src/storage/entities/index.ts @@ -2,4 +2,4 @@ export * from './identity'; export * from './mt'; export * from './state'; export * from './circuitData'; -export * from './metadata'; +export * from './message-model'; diff --git a/src/storage/entities/message-model.ts b/src/storage/entities/message-model.ts new file mode 100644 index 00000000..def67c01 --- /dev/null +++ b/src/storage/entities/message-model.ts @@ -0,0 +1,16 @@ +export type MessageModel = { + id: string; + createdAt: string; + thid?: string; + correlationId?: string; + correlationThid?: string; + type: string; + status: 'pending' | 'processed' | 'failed'; + jsonString: string; +}; + +// message history +// from to +// parent thid +// cred offer request gets new thid +// diff --git a/src/storage/entities/metadata.ts b/src/storage/entities/metadata.ts deleted file mode 100644 index 9b07679f..00000000 --- a/src/storage/entities/metadata.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type Metadata = { - id: string; - date: string; - thid: string; - type: 'directive'; - purpose: string; - status: 'pending' | 'completed' | 'failed' | 'not applicable'; - jsonString: string; -}; diff --git a/src/storage/interfaces/data-storage.ts b/src/storage/interfaces/data-storage.ts index de8f2628..f3004170 100644 --- a/src/storage/interfaces/data-storage.ts +++ b/src/storage/interfaces/data-storage.ts @@ -1,7 +1,7 @@ import { ICredentialStorage } from './credentials'; import { IIdentityStorage } from './identity'; import { IMerkleTreeStorage } from './merkletree'; -import { IMetadataStorage } from './metadata'; +import { IIden3MessageStorage } from './message'; import { IStateStorage } from './state'; /** @@ -15,5 +15,5 @@ export interface IDataStorage { identity: IIdentityStorage; mt: IMerkleTreeStorage; states: IStateStorage; - metadata?: IMetadataStorage; + message?: IIden3MessageStorage; } diff --git a/src/storage/interfaces/index.ts b/src/storage/interfaces/index.ts index 8cb68cad..ad34d9f4 100644 --- a/src/storage/interfaces/index.ts +++ b/src/storage/interfaces/index.ts @@ -7,4 +7,4 @@ export * from './data-source'; export * from './circuits'; export * from './onchain-zkp-verifier'; export * from './onchain-revocation'; -export * from './metadata'; +export * from './message'; diff --git a/src/storage/interfaces/message.ts b/src/storage/interfaces/message.ts new file mode 100644 index 00000000..148ba827 --- /dev/null +++ b/src/storage/interfaces/message.ts @@ -0,0 +1,30 @@ +import { MessageModel } from '../entities/message-model'; +import { IDataSource } from './data-source'; + +/** + * Represents the interface for the Iden3 message storage. + * Extends the IDataSource interface with additional methods for retrieving messages. + */ +export interface IIden3MessageStorage extends IDataSource { + /** + * Retrieves messages by thread ID. + * @param thid - The thread ID. + * @param status - Optional. The status of the messages to retrieve. + * @returns A promise that resolves to an array of MessageModel objects. + */ + getMessageByThreadId( + thid: string, + status?: 'pending' | 'processed' | 'failed' + ): Promise; + + /** + * Retrieves messages by correlation ID. + * @param correlationId - The correlation ID. + * @param status - Optional. The status of the messages to retrieve. + * @returns A promise that resolves to an array of MessageModel objects. + */ + getMessagesByCorrelationId( + correlationId: string, + status?: 'pending' | 'processed' | 'failed' + ): Promise; +} diff --git a/src/storage/interfaces/metadata.ts b/src/storage/interfaces/metadata.ts deleted file mode 100644 index efd2c752..00000000 --- a/src/storage/interfaces/metadata.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Metadata } from '../entities/metadata'; -import { IDataSource } from './data-source'; - -export interface IMetadataStorage extends IDataSource { - getUnprocessedMetadataForThreadIdAndPurpose(thid: string, purpose: string): Promise; -} diff --git a/src/storage/shared/metadata-storage.ts b/src/storage/shared/metadata-storage.ts index b1495f9c..8c81f4a7 100644 --- a/src/storage/shared/metadata-storage.ts +++ b/src/storage/shared/metadata-storage.ts @@ -1,10 +1,10 @@ -import { Metadata } from '../entities/metadata'; +import { MessageModel } from '../entities/message-model'; import { IDataSource } from '../interfaces'; -import { IMetadataStorage } from '../interfaces/metadata'; +import { IIden3MessageStorage } from '../interfaces/message'; /** * Represents a storage for metadata. */ -export class MetadataStorage implements IMetadataStorage { +export class Iden3MessageStorage implements IIden3MessageStorage { private static keyName = 'id'; static readonly storageKey = 'metadata'; @@ -12,22 +12,38 @@ export class MetadataStorage implements IMetadataStorage { * Creates an instance of MetadataStorage. * @param {IDataSource} _dataSource - The data source to store metadata. */ - constructor(private readonly _dataSource: IDataSource) {} + constructor(private readonly _dataSource: IDataSource) {} /** * Retrieves all unprocessed metadata. * @returns {Promise} A promise that resolves to an array of unprocessed metadata. */ - async getUnprocessedMetadataForThreadIdAndPurpose(thid: string): Promise { + async getMessageByThreadId(thid: string, status = 'pending'): Promise { const data = await this._dataSource.load(); - return data.filter((metadata) => metadata.status === 'pending' && metadata.thid === thid); + return data.filter((metadata) => metadata.status === status && metadata.thid === thid); + } + + /** + * Retrieves messages by correlation ID and status. + * @param correlationId - The correlation ID to filter messages by. + * @param status - The status of the messages to filter by. Defaults to 'pending'. + * @returns A promise that resolves to an array of MessageModel objects matching the specified correlation ID and status. + */ + async getMessagesByCorrelationId( + correlationId: string, + status = 'pending' + ): Promise { + const data = await this._dataSource.load(); + return data.filter( + (metadata) => metadata.status === status && metadata.correlationId === correlationId + ); } /** * Loads the metadata from the data source. * @returns {Promise} A promise that resolves to an array of metadata. */ - load(): Promise { + load(): Promise { return this._dataSource.load(); } @@ -37,8 +53,8 @@ export class MetadataStorage implements IMetadataStorage { * @param {Metadata} value - The metadata to save. * @returns {Promise} A promise that resolves when the metadata is saved. */ - save(key: string, value: Metadata): Promise { - return this._dataSource.save(key, value, MetadataStorage.keyName); + save(key: string, value: MessageModel): Promise { + return this._dataSource.save(key, value, Iden3MessageStorage.keyName); } /** @@ -46,8 +62,8 @@ export class MetadataStorage implements IMetadataStorage { * @param {string} key - The key of the metadata to retrieve. * @returns {Promise} A promise that resolves to the retrieved metadata, or undefined if not found. */ - get(key: string): Promise { - return this._dataSource.get(key, MetadataStorage.keyName); + get(key: string): Promise { + return this._dataSource.get(key, Iden3MessageStorage.keyName); } /** @@ -56,6 +72,6 @@ export class MetadataStorage implements IMetadataStorage { * @returns {Promise} A promise that resolves when the metadata is deleted. */ delete(key: string): Promise { - return this._dataSource.delete(key, MetadataStorage.keyName); + return this._dataSource.delete(key, Iden3MessageStorage.keyName); } } diff --git a/tests/handlers/directives.test.ts b/tests/handlers/directives.test.ts index 325fa46d..7c8369cd 100644 --- a/tests/handlers/directives.test.ts +++ b/tests/handlers/directives.test.ts @@ -18,12 +18,12 @@ import { CredentialStatusResolverRegistry, PROTOCOL_CONSTANTS, InMemoryDataSource, - Metadata, - MetadataStorage, + Iden3MessageStorage, Iden3AttachmentType, - IMetadataStorage, + IIden3MessageStorage, CredentialProposalHandler, - ICredentialProposalHandler + ICredentialProposalHandler, + MessageModel } from '../../src'; import { DID } from '@iden3/js-iden3-core'; import { expect } from 'chai'; @@ -49,7 +49,7 @@ describe('directives', () => { let packageMgr: IPackageManager; let userDID: DID; let issuerDID: DID; - let metadataStorage: IMetadataStorage; + let messageStorage: IIden3MessageStorage; let proposalRequestHandler: ICredentialProposalHandler; beforeEach(async () => { @@ -76,9 +76,9 @@ describe('directives', () => { proofService.generateAuthV2Inputs.bind(proofService), proofService.verifyState.bind(proofService) ); - metadataStorage = new MetadataStorage(new InMemoryDataSource()); + messageStorage = new Iden3MessageStorage(new InMemoryDataSource()); authHandler = new AuthHandler(packageMgr, proofService, { - metadataStorage + messageStorage: messageStorage }); proposalRequestHandler = new CredentialProposalHandler(packageMgr, idWallet, { @@ -108,7 +108,7 @@ describe('directives', () => { packerParams: { mediaType: MediaType.PlainMessage }, - metadataStorage + metadataStorage: messageStorage }); const { did: didUser } = await createIdentity(idWallet, { @@ -184,14 +184,9 @@ describe('directives', () => { 'no credential satisfied query' ); expect(authReq.thid).not.to.be.undefined; - const metadata = await metadataStorage.get(authReq.thid!); + const metadata = await messageStorage.get(authReq.thid!); console.log('metadata', metadata); - expect( - await metadataStorage.getUnprocessedMetadataForThreadIdAndPurpose( - authReq.thid!, - PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.PROPOSAL_REQUEST_MESSAGE_TYPE - ) - ).not.to.be.undefined; + expect(await messageStorage.getMessageByThreadId(authReq.thid!, 'pending')).not.to.be.undefined; // user wants to issue credential @@ -204,5 +199,18 @@ describe('directives', () => { }); console.log('proposalReq', proposalReq); expect(proposalReq).not.to.be.undefined; + const [authRequest, proposalRequest] = (await messageStorage.load()).sort( + (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); + + expect(authRequest.type).to.be.eq( + PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE + ); + expect(proposalRequest.type).to.be.eq( + PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.PROPOSAL_REQUEST_MESSAGE_TYPE + ); + expect(proposalRequest.thid).not.to.be.eq(authRequest.thid); + expect(proposalRequest.correlationId).to.be.eq(authReq.id); + expect(proposalRequest.correlationThid).to.be.eq(authReq.thid); }); }); From 3e20795cbb8f71cfb11dfa5104fa00ff6ac7083b Mon Sep 17 00:00:00 2001 From: Kolezhniuk Date: Wed, 18 Sep 2024 17:06:47 +0200 Subject: [PATCH 4/5] Fix key --- src/storage/shared/metadata-storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/shared/metadata-storage.ts b/src/storage/shared/metadata-storage.ts index 8c81f4a7..484568c0 100644 --- a/src/storage/shared/metadata-storage.ts +++ b/src/storage/shared/metadata-storage.ts @@ -6,7 +6,7 @@ import { IIden3MessageStorage } from '../interfaces/message'; */ export class Iden3MessageStorage implements IIden3MessageStorage { private static keyName = 'id'; - static readonly storageKey = 'metadata'; + static readonly storageKey = 'messages'; /** * Creates an instance of MetadataStorage. From 0e0e11561ce9704c79291f3330514ea61c7f67e6 Mon Sep 17 00:00:00 2001 From: Kolezhniuk Date: Wed, 25 Sep 2024 08:10:45 +0200 Subject: [PATCH 5/5] FIxes with working flow --- flow.drawio | 20 +++- index.html | 3 + src/iden3comm/handlers/auth.ts | 8 +- src/iden3comm/handlers/credential-proposal.ts | 113 +++++++++++++----- src/iden3comm/handlers/message-handler.ts | 5 +- src/iden3comm/types/protocol/directive.ts | 8 +- src/storage/entities/message-model.ts | 8 +- src/storage/interfaces/message.ts | 8 +- src/storage/shared/credential-storage.ts | 2 +- src/storage/shared/metadata-storage.ts | 16 ++- src/verifiable/credential.ts | 8 +- tests/handlers/directives.test.ts | 56 ++++----- 12 files changed, 168 insertions(+), 87 deletions(-) diff --git a/flow.drawio b/flow.drawio index 171441b6..a142ba23 100644 --- a/flow.drawio +++ b/flow.drawio @@ -1,6 +1,6 @@ - + @@ -34,6 +34,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/index.html b/index.html index 7f1a9150..e4eb847e 100644 --- a/index.html +++ b/index.html @@ -74,6 +74,9 @@ publishState: async () => { return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; }, + getRpcProvider: () => { + return {}; + }, publishStateGeneric: async () => { return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; }, diff --git a/src/iden3comm/handlers/auth.ts b/src/iden3comm/handlers/auth.ts index 267e3e4d..b25a7640 100644 --- a/src/iden3comm/handlers/auth.ts +++ b/src/iden3comm/handlers/auth.ts @@ -199,9 +199,11 @@ export class AuthHandler message: BasicMessage, ctx: AuthMessageHandlerOptions ): Promise { - await this.processMessage(message, { - messageStorage: this._opts?.messageStorage - }); + if (this._opts?.messageStorage) { + await this.processMessage(message, { + messageStorage: this._opts.messageStorage + }); + } switch (message.type) { case PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE: diff --git a/src/iden3comm/handlers/credential-proposal.ts b/src/iden3comm/handlers/credential-proposal.ts index e949e8f5..bd6bc1ea 100644 --- a/src/iden3comm/handlers/credential-proposal.ts +++ b/src/iden3comm/handlers/credential-proposal.ts @@ -3,6 +3,7 @@ import { BasicMessage, CredentialOffer, CredentialsOfferMessage, + Iden3DirectiveType, IPackageManager, JsonDocumentObject, PackerParams @@ -18,7 +19,7 @@ import { ProposalMessage } from '../types/protocol/proposal-request'; import { IIdentityWallet } from '../../identity'; -import { byteEncoder } from '../../utils'; +import { byteDecoder, byteEncoder } from '../../utils'; import { W3CCredential } from '../../verifiable'; import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; import { IIden3MessageStorage } from '../../storage'; @@ -112,7 +113,7 @@ export interface ICredentialProposalHandler { sender: DID; receiver: DID; } & ProposalRequestCreationOptions - ): Promise; + ): Promise<{ request: ProposalRequestMessage; token: string }>; /** * @beta @@ -154,9 +155,13 @@ export type ProposalHandlerOptions = { /** @beta CredentialProposalHandlerParams represents credential proposal handler params */ export type CredentialProposalHandlerParams = { agentUrl: string; - proposalResolverFn: (context: string, type: string) => Promise; + proposalResolverFn: ( + context: string, + type: string, + request?: ProposalRequestMessage + ) => Promise; packerParams: PackerParams; - metadataStorage?: IIden3MessageStorage; + messageStorage?: IIden3MessageStorage; }; /** @@ -195,22 +200,41 @@ export class CredentialProposalHandler sender: DID; receiver: DID; } & ProposalRequestCreationOptions - ): Promise { - if (!this._params.metadataStorage) { + ): Promise<{ request: ProposalRequestMessage; token: string }> { + if (!this._params.messageStorage) { throw new Error('metadata storage is required'); } + const thid = uuid.v4(); + const messages: BasicMessage[] = ( - await this._params.metadataStorage.getMessageByThreadId(params.thid, 'pending') + await this._params.messageStorage.getMessagesByThreadId(params.thid, 'pending') ).map((message) => JSON.parse(message.jsonString)); + if (!messages.length) { + throw new Error('no pending messages found'); + // const request = createProposalRequest(params.sender, params.receiver, { + // credentials: params.credentials, + // metadata: params.metadata, + // did_doc: params.did_doc, + // thid + // }); + // const msgBytes = byteEncoder.encode(JSON.stringify(request)); + // const token = byteDecoder.decode( + // await this._packerMgr.pack(MediaType.PlainMessage, msgBytes, { + // senderDID: params.sender, + // ...this._params.packerParams + // }) + // ); + // return { request, token }; + } + //TODO: handle multiple messages if (messages.length > 1) { throw new Error('multiple messages are not supported'); } const directives = (messages[0].attachments ?? []) - // .flatMap((message) => message.attachments ?? []) .filter( (attachment) => attachment?.data?.type === 'Iden3Directives' && attachment?.data?.directives.length @@ -220,37 +244,66 @@ export class CredentialProposalHandler (directive) => directive.purpose === PROTOCOL_MESSAGE_TYPE.PROPOSAL_REQUEST_MESSAGE_TYPE ); - const metadata = directives.length - ? { - type: 'Iden3Metadata', - data: directives.map((directive: JsonDocumentObject) => { - delete directive.purpose; - return directive; - }) + const credentialsToRequest: ProposalRequestCredential[] = [...params.credentials]; + // let metadata = {...params.metadata}; + //TODO: how to merge metadata? + + const result = directives.reduce<{ + metadata: { + type: string; + data: JsonDocumentObject[]; + }; + credentialsToRequest: ProposalRequestCredential[]; + }>( + (acc, directive) => { + if (directive.type !== Iden3DirectiveType.TransparentPaymentDirective) { + return acc; } - : params.metadata; + const directiveCredentials: ProposalRequestCredential[] = directive.credentials ?? []; + acc.credentialsToRequest = [...acc.credentialsToRequest, ...directiveCredentials]; + delete directive.purpose; + acc.metadata.data = [...acc.metadata.data, directive]; + return acc; + }, + { + metadata: { + type: 'Iden3Metadata', + data: [] + }, + credentialsToRequest + } + ); - const request = createProposalRequest(params.sender, params.receiver, { - credentials: params.credentials, - metadata, + const msg = createProposalRequest(params.sender, params.receiver, { + credentials: result.credentialsToRequest, + metadata: result.metadata, did_doc: params.did_doc, - thid: uuid.v4() + thid }); // todo: is it correct and right place to save the message before sending it? // - await this._params.metadataStorage.save(params.thid, { - id: request.id, - thid: request.thid, + await this._params.messageStorage.save(msg.id, { + id: msg.id, + thid: msg.thid, createdAt: new Date().toISOString(), status: 'pending', correlationId: messages[0].id, - type: request.type, - correlationThid: messages[0].thid, - jsonString: JSON.stringify(request) + type: msg.type, + correlationThId: messages[0].thid, + jsonString: JSON.stringify(msg) }); - return request; + const msgBytes = byteEncoder.encode(JSON.stringify(msg)); + + const token = byteDecoder.decode( + await this._packerMgr.pack(messages[0]?.typ ?? MediaType.PlainMessage, msgBytes, { + senderDID: params.sender, + ...this._params.packerParams + }) + ); + + return { request: msg, token }; } public async handle( @@ -350,7 +403,11 @@ export class CredentialProposalHandler } // credential not found in the wallet, prepare proposal protocol message - const proposal = await this._params.proposalResolverFn(cred.context, cred.type); + const proposal = await this._params.proposalResolverFn( + cred.context, + cred.type, + proposalRequest + ); if (!proposal) { throw new Error(`can't resolve Proposal for type: ${cred.type}, context: ${cred.context}`); } diff --git a/src/iden3comm/handlers/message-handler.ts b/src/iden3comm/handlers/message-handler.ts index 9f6c44a4..cef97966 100644 --- a/src/iden3comm/handlers/message-handler.ts +++ b/src/iden3comm/handlers/message-handler.ts @@ -7,7 +7,6 @@ import { MediaType } from '../constants'; import { proving } from '@iden3/js-jwz'; import { DID } from '@iden3/js-iden3-core'; import { IIden3MessageStorage } from '../../storage'; -import * as uuid from 'uuid'; /** * iden3 Protocol message handler interface @@ -74,8 +73,8 @@ export abstract class AbstractMessageHandler implements IProtocolMessageHandler return; } - await messageStorage.save(threadId, { - id: uuid.v4(), + await messageStorage.save(message.id, { + id: message.id, thid: threadId, createdAt: new Date().toISOString(), type: message.type, diff --git a/src/iden3comm/types/protocol/directive.ts b/src/iden3comm/types/protocol/directive.ts index e864301e..7d3fafb5 100644 --- a/src/iden3comm/types/protocol/directive.ts +++ b/src/iden3comm/types/protocol/directive.ts @@ -1,11 +1,13 @@ import { PROTOCOL_MESSAGE_TYPE } from '../../constants'; -export type Iden3DirectiveType = 'TransparentPaymentDirective'; +export enum Iden3DirectiveType { + TransparentPaymentDirective = 'TransparentPaymentDirective' +} export type TransparentPaymentDirective = { type: Iden3DirectiveType; - purpose: (typeof PROTOCOL_MESSAGE_TYPE)[keyof typeof PROTOCOL_MESSAGE_TYPE]; - credential: { + purpose?: (typeof PROTOCOL_MESSAGE_TYPE)[keyof typeof PROTOCOL_MESSAGE_TYPE]; + credentials: { type: string; context: string; }[]; diff --git a/src/storage/entities/message-model.ts b/src/storage/entities/message-model.ts index def67c01..b42c8284 100644 --- a/src/storage/entities/message-model.ts +++ b/src/storage/entities/message-model.ts @@ -3,14 +3,8 @@ export type MessageModel = { createdAt: string; thid?: string; correlationId?: string; - correlationThid?: string; + correlationThId?: string; type: string; status: 'pending' | 'processed' | 'failed'; jsonString: string; }; - -// message history -// from to -// parent thid -// cred offer request gets new thid -// diff --git a/src/storage/interfaces/message.ts b/src/storage/interfaces/message.ts index 148ba827..57daf182 100644 --- a/src/storage/interfaces/message.ts +++ b/src/storage/interfaces/message.ts @@ -8,12 +8,12 @@ import { IDataSource } from './data-source'; export interface IIden3MessageStorage extends IDataSource { /** * Retrieves messages by thread ID. - * @param thid - The thread ID. + * @param corelationThreadId - The thread ID. * @param status - Optional. The status of the messages to retrieve. * @returns A promise that resolves to an array of MessageModel objects. */ - getMessageByThreadId( - thid: string, + getMessagesByThreadId( + corelationThreadId: string, status?: 'pending' | 'processed' | 'failed' ): Promise; @@ -27,4 +27,6 @@ export interface IIden3MessageStorage extends IDataSource { correlationId: string, status?: 'pending' | 'processed' | 'failed' ): Promise; + + updateStatusByThId(thid: string, status: 'pending' | 'processed' | 'failed'): Promise; } diff --git a/src/storage/shared/credential-storage.ts b/src/storage/shared/credential-storage.ts index 35211a20..3b240d70 100644 --- a/src/storage/shared/credential-storage.ts +++ b/src/storage/shared/credential-storage.ts @@ -31,7 +31,7 @@ export class CredentialStorage implements ICredentialStorage { /** @inheritdoc */ async saveCredential(credential: W3CCredential): Promise { - return this._dataSource.save(credential.id, credential.toJSON()); + return this._dataSource.save(credential.id, credential.toJSON() as W3CCredential); } /** {@inheritdoc ICredentialStorage.listCredentials } */ diff --git a/src/storage/shared/metadata-storage.ts b/src/storage/shared/metadata-storage.ts index 484568c0..06a670c2 100644 --- a/src/storage/shared/metadata-storage.ts +++ b/src/storage/shared/metadata-storage.ts @@ -9,7 +9,7 @@ export class Iden3MessageStorage implements IIden3MessageStorage { static readonly storageKey = 'messages'; /** - * Creates an instance of MetadataStorage. + * Creates an instance of MessageStorage. * @param {IDataSource} _dataSource - The data source to store metadata. */ constructor(private readonly _dataSource: IDataSource) {} @@ -18,7 +18,7 @@ export class Iden3MessageStorage implements IIden3MessageStorage { * Retrieves all unprocessed metadata. * @returns {Promise} A promise that resolves to an array of unprocessed metadata. */ - async getMessageByThreadId(thid: string, status = 'pending'): Promise { + async getMessagesByThreadId(thid: string, status = 'pending'): Promise { const data = await this._dataSource.load(); return data.filter((metadata) => metadata.status === status && metadata.thid === thid); } @@ -57,6 +57,18 @@ export class Iden3MessageStorage implements IIden3MessageStorage { return this._dataSource.save(key, value, Iden3MessageStorage.keyName); } + async updateStatusByThId( + thId: string, + status: 'pending' | 'processed' | 'failed' + ): Promise { + const data = await this.load(); + const metadata = data.filter((metadata) => metadata.thid === thId); + metadata.forEach((m) => { + m.status = status; + }); + await Promise.all(metadata.map((m) => this.save(m.id, m))); + } + /** * Retrieves the metadata with the specified key. * @param {string} key - The key of the metadata to retrieve. diff --git a/src/verifiable/credential.ts b/src/verifiable/credential.ts index 2fdd31d7..f4a13eda 100644 --- a/src/verifiable/credential.ts +++ b/src/verifiable/credential.ts @@ -53,8 +53,14 @@ export class W3CCredential { proof?: object | unknown[]; toJSON() { + const cred: W3CCredential = Object.entries(this).reduce((acc: W3CCredential, [key, v]) => { + if (v !== undefined && v !== null) { + acc[key as keyof W3CCredential] = v; + } + return acc; + }, new W3CCredential()); return { - ...this, + ...cred, proof: Array.isArray(this.proof) ? this.proof.map(this.proofToJSON) : this.proofToJSON(this.proof) diff --git a/tests/handlers/directives.test.ts b/tests/handlers/directives.test.ts index 7c8369cd..ceaf514c 100644 --- a/tests/handlers/directives.test.ts +++ b/tests/handlers/directives.test.ts @@ -23,7 +23,9 @@ import { IIden3MessageStorage, CredentialProposalHandler, ICredentialProposalHandler, - MessageModel + MessageModel, + Proposal, + Iden3DirectiveType } from '../../src'; import { DID } from '@iden3/js-iden3-core'; import { expect } from 'chai'; @@ -83,32 +85,11 @@ describe('directives', () => { proposalRequestHandler = new CredentialProposalHandler(packageMgr, idWallet, { agentUrl: 'http://localhost:8001/api/v1/agent', - proposalResolverFn: async (context: string, type: string) => { - if ( - context === - 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-nonmerklized.jsonld' && - type === 'KYCAgeCredential' - ) { - return { - credentials: [ - { - type, - context - } - ], - type: 'WebVerificationForm', - url: 'http://issuer-agent.com/verify?anyUniqueIdentifierOfSession=55', - description: - 'you can pass the verification on our KYC provider by following the next link' - }; - } - - throw new Error(`not supported credential, type: ${type}, context: ${context}`); - }, + proposalResolverFn: () => Promise.resolve({} as Proposal), packerParams: { mediaType: MediaType.PlainMessage }, - metadataStorage: messageStorage + messageStorage: messageStorage }); const { did: didUser } = await createIdentity(idWallet, { @@ -120,7 +101,7 @@ describe('directives', () => { issuerDID = didIssuer; }); - it.only('request-response flow', async () => { + it('request-response flow', async () => { // verifier sends auth request to user, user has no credential yet, but auth request contains directive const id = uuid.v4(); const authReq: AuthorizationRequestMessage = { @@ -160,9 +141,9 @@ describe('directives', () => { context: 'https://directive.iden3.io/v1/context.json', directives: [ { - type: 'TransparentPaymentDirective', + type: Iden3DirectiveType.TransparentPaymentDirective, purpose: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.PROPOSAL_REQUEST_MESSAGE_TYPE, - credential: [ + credentials: [ { context: 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-nonmerklized.jsonld', @@ -179,6 +160,8 @@ describe('directives', () => { ] }; + console.log(JSON.stringify(authReq, null, 2)); + const msgBytes = await packageMgr.packMessage(authReq.typ as MediaType, authReq, {}); await expect(authHandler.handleAuthorizationRequest(userDID, msgBytes)).to.rejectedWith( 'no credential satisfied query' @@ -186,17 +169,19 @@ describe('directives', () => { expect(authReq.thid).not.to.be.undefined; const metadata = await messageStorage.get(authReq.thid!); console.log('metadata', metadata); - expect(await messageStorage.getMessageByThreadId(authReq.thid!, 'pending')).not.to.be.undefined; + expect(await messageStorage.getMessagesByThreadId(authReq.thid!, 'pending')).not.to.be + .undefined; // user wants to issue credential // user sends proposal request to issuer - const proposalReq = await proposalRequestHandler.createProposalRequestPacked({ - thid: authReq.thid!, - sender: userDID, - receiver: issuerDID, - credentials: [] - }); + const { request: proposalReq, token } = + await proposalRequestHandler.createProposalRequestPacked({ + thid: authReq.thid!, + sender: userDID, + receiver: issuerDID, + credentials: [] + }); console.log('proposalReq', proposalReq); expect(proposalReq).not.to.be.undefined; const [authRequest, proposalRequest] = (await messageStorage.load()).sort( @@ -211,6 +196,7 @@ describe('directives', () => { ); expect(proposalRequest.thid).not.to.be.eq(authRequest.thid); expect(proposalRequest.correlationId).to.be.eq(authReq.id); - expect(proposalRequest.correlationThid).to.be.eq(authReq.thid); + expect(proposalRequest.correlationThId).to.be.eq(authReq.thid); + expect(token).not.to.be.undefined; }); });