@0xpolygonid/js-sdk
Version:
SDK to work with Polygon ID
365 lines (334 loc) • 11.3 kB
text/typescript
import { PROTOCOL_MESSAGE_TYPE, MediaType } from '../constants';
import {
Attachment,
BasicMessage,
CredentialOffer,
CredentialsOfferMessage,
DIDDocument,
IPackageManager
} from '../types';
import { DID, getUnixTimestamp } from '@iden3/js-iden3-core';
import * as uuid from 'uuid';
import {
Proposal,
ProposalRequestMessage,
ProposalMessage,
ProposalRequestCredential
} from '../types/protocol/proposal-request';
import { IIdentityWallet } from '../../identity';
import { byteEncoder } from '../../utils';
import { W3CCredential } from '../../verifiable';
import {
AbstractMessageHandler,
BasicHandlerOptions,
IProtocolMessageHandler,
getProvingMethodAlgFromJWZ
} from './message-handler';
import { HandlerPackerParams, initDefaultPackerOptions, verifyExpiresTime } from './common';
/** @beta ProposalRequestCreationOptions represents proposal-request creation options */
export type ProposalRequestCreationOptions = {
credentials: ProposalRequestCredential[];
did_doc?: DIDDocument;
expires_time?: Date;
attachments?: Attachment[];
};
/** @beta ProposalCreationOptions represents proposal creation options */
export type ProposalCreationOptions = {
expires_time?: Date;
};
/**
* @beta
* createProposalRequest is a function to create protocol proposal-request protocol message
* @param {DID} sender - sender did
* @param {DID} receiver - receiver did
* @param {ProposalRequestCreationOptions} opts - creation options
* @returns `Promise<ProposalRequestMessage>`
*/
export function createProposalRequest(
sender: DID,
receiver: DID,
opts: ProposalRequestCreationOptions
): ProposalRequestMessage {
const uuidv4 = uuid.v4();
const request: ProposalRequestMessage = {
id: uuidv4,
thid: uuidv4,
from: sender.string(),
to: receiver.string(),
typ: MediaType.PlainMessage,
type: PROTOCOL_MESSAGE_TYPE.PROPOSAL_REQUEST_MESSAGE_TYPE,
body: {
credentials: opts.credentials,
did_doc: opts.did_doc
},
created_time: getUnixTimestamp(new Date()),
expires_time: opts?.expires_time ? getUnixTimestamp(opts.expires_time) : undefined,
attachments: opts.attachments
};
return request;
}
/**
* @beta
* createProposal is a function to create protocol proposal protocol message
* @param {DID} sender - sender did
* @param {DID} receiver - receiver did
* @param {Proposal[]} proposals - proposals
* @returns `Promise<ProposalRequestMessage>`
*/
export function createProposal(
sender: DID,
receiver: DID,
proposals?: Proposal[],
opts?: ProposalCreationOptions
): ProposalMessage {
const uuidv4 = uuid.v4();
const request: ProposalMessage = {
id: uuidv4,
thid: uuidv4,
from: sender.string(),
to: receiver.string(),
typ: MediaType.PlainMessage,
type: PROTOCOL_MESSAGE_TYPE.PROPOSAL_MESSAGE_TYPE,
body: {
proposals: proposals || []
},
created_time: getUnixTimestamp(new Date()),
expires_time: opts?.expires_time ? getUnixTimestamp(opts.expires_time) : undefined
};
return request;
}
/**
* @beta
* Interface that allows the processing of the proposal-request
*
* @interface ICredentialProposalHandler
*/
export interface ICredentialProposalHandler {
/**
* @beta
* unpacks proposal-request
* @param {Uint8Array} request - raw byte message
* @returns `Promise<ProposalRequestMessage>`
*/
parseProposalRequest(request: Uint8Array): Promise<ProposalRequestMessage>;
/**
* @beta
* handle proposal-request
* @param {Uint8Array} request - raw byte message
* @param {ProposalRequestHandlerOptions} opts - handler options
* @returns {Promise<Uint8Array>}` - proposal response message
*/
handleProposalRequest(
request: Uint8Array,
opts?: ProposalRequestHandlerOptions
): Promise<Uint8Array>;
/**
* @beta
* handle proposal protocol message
* @param {ProposalMessage} proposal - proposal message
* @param {ProposalHandlerOptions} opts - options
* @returns `Promise<{
proposal: ProposalMessage;
}>`
*/
handleProposal(
proposal: ProposalMessage,
opts?: ProposalHandlerOptions
): Promise<{
proposal: ProposalMessage;
}>;
}
/** @beta ProposalRequestHandlerOptions represents proposal-request handler options */
export type ProposalRequestHandlerOptions = BasicHandlerOptions;
/** @beta ProposalHandlerOptions represents proposal handler options */
export type ProposalHandlerOptions = BasicHandlerOptions & {
proposalRequest?: ProposalRequestMessage;
};
/** @beta CredentialProposalHandlerParams represents credential proposal handler params */
export type CredentialProposalHandlerParams = {
agentUrl: string;
proposalResolverFn: (
context: string,
type: string,
opts?: { msg?: BasicMessage }
) => Promise<Proposal>;
packerParams: HandlerPackerParams;
};
/**
*
* Allows to process ProposalRequest protocol message
* @beta
* @class CredentialProposalHandler
* @implements implements ICredentialProposalHandler interface
*/
export class CredentialProposalHandler
extends AbstractMessageHandler
implements ICredentialProposalHandler, IProtocolMessageHandler
{
/**
* @beta Creates an instance of CredentialProposalHandler.
* @param {IPackageManager} _packerMgr - package manager to unpack message envelope
* @param {IIdentityWallet} _identityWallet - identity wallet
* @param {CredentialProposalHandlerParams} _params - credential proposal handler params
*
*/
constructor(
private readonly _packerMgr: IPackageManager,
private readonly _identityWallet: IIdentityWallet,
private readonly _params: CredentialProposalHandlerParams
) {
super();
}
public async handle(
message: BasicMessage,
context: ProposalRequestHandlerOptions
): Promise<BasicMessage | null> {
switch (message.type) {
case PROTOCOL_MESSAGE_TYPE.PROPOSAL_REQUEST_MESSAGE_TYPE:
return (await this.handleProposalRequestMessage(
message as unknown as ProposalRequestMessage,
context
)) as BasicMessage;
default:
return super.handle(message, context as { [key: string]: unknown });
}
}
/**
* @inheritdoc ICredentialProposalHandler#parseProposalRequest
*/
async parseProposalRequest(request: Uint8Array): Promise<ProposalRequestMessage> {
const { unpackedMessage: message } = await this._packerMgr.unpack(request);
const proposalRequest = message as unknown as ProposalRequestMessage;
if (message.type !== PROTOCOL_MESSAGE_TYPE.PROPOSAL_REQUEST_MESSAGE_TYPE) {
throw new Error('Invalid media type');
}
return proposalRequest;
}
private async handleProposalRequestMessage(
proposalRequest: ProposalRequestMessage,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ctx?: ProposalRequestHandlerOptions
): Promise<ProposalMessage | CredentialsOfferMessage | undefined> {
if (!proposalRequest.to) {
throw new Error(`failed request. empty 'to' field`);
}
if (!proposalRequest.from) {
throw new Error(`failed request. empty 'from' field`);
}
if (!proposalRequest.body?.credentials?.length) {
throw new Error(`failed request. no 'credentials' in body`);
}
let credOfferMessage: CredentialsOfferMessage | undefined = undefined;
let proposalMessage: ProposalMessage | undefined = undefined;
for (let i = 0; i < proposalRequest.body.credentials.length; i++) {
const cred = proposalRequest.body.credentials[i];
// check if there is credentials in the wallet
let credsFromWallet: W3CCredential[] = [];
try {
credsFromWallet = await this._identityWallet.credentialWallet.findByQuery({
credentialSubject: {
id: {
$eq: proposalRequest.from
}
},
type: cred.type,
context: cred.context,
allowedIssuers: [proposalRequest.to]
});
} catch (e) {
if ((e as Error).message !== 'no credential satisfied query') {
throw e;
}
}
if (credsFromWallet.length) {
const guid = uuid.v4();
if (!credOfferMessage) {
credOfferMessage = {
id: guid,
typ: this._params.packerParams.mediaType,
type: PROTOCOL_MESSAGE_TYPE.CREDENTIAL_OFFER_MESSAGE_TYPE,
thid: proposalRequest.thid ?? guid,
body: {
url: this._params.agentUrl,
credentials: []
},
from: proposalRequest.to,
to: proposalRequest.from
};
}
credOfferMessage.body.credentials.push(
...credsFromWallet.map<CredentialOffer>((c) => ({
id: c.id,
description: ''
}))
);
continue;
}
// credential not found in the wallet, prepare proposal protocol message
const proposal = await this._params.proposalResolverFn(cred.context, cred.type, {
msg: proposalRequest
});
if (!proposal) {
throw new Error(`can't resolve Proposal for type: ${cred.type}, context: ${cred.context}`);
}
if (!proposalMessage) {
const guid = uuid.v4();
proposalMessage = {
id: guid,
typ: this._params.packerParams.mediaType,
type: PROTOCOL_MESSAGE_TYPE.PROPOSAL_MESSAGE_TYPE,
thid: proposalRequest.thid ?? guid,
body: {
proposals: []
},
from: proposalRequest.to,
to: proposalRequest.from
};
}
proposalMessage.body?.proposals.push(proposal);
}
return proposalMessage ?? credOfferMessage;
}
/**
* @inheritdoc ICredentialProposalHandler#handleProposalRequest
*/
async handleProposalRequest(
request: Uint8Array,
//eslint-disable-next-line @typescript-eslint/no-unused-vars
opts?: ProposalRequestHandlerOptions
): Promise<Uint8Array> {
const proposalRequest = await this.parseProposalRequest(request);
if (!proposalRequest.from) {
throw new Error(`failed request. empty 'from' field`);
}
if (!opts?.allowExpiredMessages) {
verifyExpiresTime(proposalRequest);
}
const senderDID = DID.parse(proposalRequest.from);
const message = await this.handleProposalRequestMessage(proposalRequest);
const response = byteEncoder.encode(JSON.stringify(message));
const packerOpts = initDefaultPackerOptions(
this._params.packerParams.mediaType,
this._params.packerParams.packerOptions,
{
provingMethodAlg: await getProvingMethodAlgFromJWZ(request),
senderDID
}
);
return this._packerMgr.pack(this._params.packerParams.mediaType, response, packerOpts);
}
/**
* @inheritdoc ICredentialProposalHandler#handleProposal
*/
async handleProposal(proposal: ProposalMessage, opts?: ProposalHandlerOptions) {
if (!opts?.allowExpiredMessages) {
verifyExpiresTime(proposal);
}
if (opts?.proposalRequest && opts.proposalRequest.from !== proposal.to) {
throw new Error(
`sender of the request is not a target of response - expected ${opts.proposalRequest.from}, given ${proposal.to}`
);
}
return { proposal };
}
}