@dcxp/root
Version:
DCX: Decentralized Credential Exchange. DWN protocol for verifiable credential exchange.
337 lines (294 loc) • 11 kB
text/typescript
import {
applicationSchema,
DcxApplicantParams,
DcxApplicantProcessRecordParams,
DcxDwnError,
DcxManager,
DcxOptions,
DcxRecordsQueryResponse,
DcxRecordsReadResponse,
DwnError,
DwnUtils,
FileSystem,
Logger,
manifestSchema,
Mnemonic,
RecordsParams,
responseSchema
} from '@dcx-protocol/common';
import { Web5PlatformAgent } from '@web5/agent';
import {
ProtocolsConfigureResponse,
ProtocolsQueryResponse,
Record,
RecordsCreateResponse,
Web5
} from '@web5/api';
import { PresentationExchange } from '@web5/credentials';
import { applicant, applicantConfig, applicantOptions, DcxApplicantConfig } from './index.js';
/**
* DWN manager handles interactions between the DCX server and the DWN
*/
export class DcxApplicant implements DcxManager {
options : DcxOptions;
config : DcxApplicantConfig;
isSetup : boolean = false;
isInitialized : boolean = false;
public static did : string;
public static web5 : Web5;
public static agent : Web5PlatformAgent;
constructor(params: DcxApplicantParams = {}) {
this.config = { ...applicantConfig, ...params.config };
this.options = params.options ?? applicantOptions;
}
/**
* Query DWN for credential-applicant protocol
* @returns Protocol[]; see {@link Protocol}
*/
public async queryProtocols(): Promise<ProtocolsQueryResponse> {
// Query DWN for credential-applicant protocol
const { status: query, protocols = [] } = await DcxApplicant.web5.dwn.protocols.query({
message : {
filter : {
protocol : applicant.protocol,
},
},
});
if (DwnUtils.isFailure(query.code)) {
const { code, detail } = query;
Logger.error('DWN protocols query failed', query);
throw new DwnError(code, detail);
}
Logger.log(`DWN has ${protocols.length} protocols available`);
return { status: query, protocols };
}
/**
* Configure DWN for credential-applicant protocol
* @returns DwnResponseStatus; see {@link DwnResponseStatus}
*/
public async configureProtocols(): Promise<ProtocolsConfigureResponse> {
const { status: configure, protocol } = await DcxApplicant.web5.dwn.protocols.configure({
message : { definition: applicant },
});
if (DwnUtils.isFailure(configure.code) || !protocol) {
const { code, detail } = configure;
Logger.error('DWN protocol configure fail', configure, protocol);
throw new DwnError(code, detail);
}
const { status: send } = await protocol.send(DcxApplicant.did);
if (DwnUtils.isFailure(send.code)) {
const { code, detail } = send;
Logger.error('DWN protocols send failed', send);
throw new DwnError(code, detail);
}
Logger.log('Sent protocol to remote DWN', send);
return { status: send, protocol };
}
public static async queryRecords(): Promise<DcxRecordsQueryResponse> {
const { status, records = [], cursor } = await DcxApplicant.web5.dwn.records.query({
message : {
filter : {
protocol : applicant.protocol,
protocolPath : 'application/response',
schema : responseSchema.$id,
dataFormat : 'application/json',
},
},
});
if (DwnUtils.isFailure(status.code)) {
const { code, detail } = status;
Logger.error('DWN manifest records query failed', status);
throw new DwnError(code, detail);
}
return { status, records, cursor };
}
/**
* Filter manifest records
* @param applicationResponseRecords Record[]; see {@link Record}
* @returns applicationResponses[]; see {@link responseSchema}
*/
public async readRecords({ records, type }: RecordsParams & { type: string }): Promise<DcxRecordsReadResponse> {
const recordReads = await Promise.all(
records.map(async (record: Record) => {
const baseReadRequest = {
message : {
filter : {
recordId : record.id,
},
},
};
const readRequest = type === 'manifest'
? { ...baseReadRequest, from: record.author }
: baseReadRequest;
const { record: read } = await DcxApplicant.web5.dwn.records.read(readRequest);
return read.data.json();
}),
);
return { records: recordReads };
}
/**
* Query records
*/
public async queryRecords({ from }: { from: string }): Promise<DcxRecordsQueryResponse> {
const { status, records = [], cursor } = await DcxApplicant.web5.dwn.records.query({
from,
message : {
filter : {
protocol : applicant.protocol,
protocolPath : 'manifest',
schema : manifestSchema.$id,
dataFormat : 'application/json',
},
},
});
if (DwnUtils.isFailure(status.code)) {
const { code, detail } = status;
Logger.error('DWN manifest records query failed', status);
throw new DwnError(code, detail);
}
return { status, records, cursor };
}
/**
*
* { vcJwts: string[], presentationDefinition: PresentationDefinitionV2 }
* @param pex Presentation Exchange object; see {@link PresentationExchangeArgs}
* @param pex.vcJwts The list of Verifiable Credentials (VCs) in JWT format to be evaluated.
* @param pex.presentationDefinition The Presentation Definition V2 to match the VCs against.
* @param issuerDid The DID of the issuer to send the application record to.
*/
public static async processRecord(
{ pex, recipient }: DcxApplicantProcessRecordParams
): Promise<RecordsCreateResponse> {
const presentationResult = PresentationExchange.createPresentationFromCredentials(pex);
const { record, status: create } = await DcxApplicant.web5.dwn.records.create({
store : true,
data : presentationResult.presentation,
message : {
recipient,
schema : applicationSchema.$id,
dataFormat : 'application/json',
protocol : applicant.protocol,
protocolPath : 'application'
}
});
if (DwnUtils.isFailure(create.code)) {
const { code, detail } = create;
Logger.error('Failed to create missing manifest record', create);
throw new DwnError(code, detail);
}
if (!record) {
throw new DcxDwnError(`Failed to create application record: ${create.code} - ${create.detail}`);
}
const { status: local } = await record.send();
if (DwnUtils.isFailure(local.code)) {
const { code, detail } = local;
Logger.error('Failed to send dwn application record to local', local);
throw new DwnError(code, detail);
}
const { status: remote } = await record.send(recipient);
if (DwnUtils.isFailure(remote.code)) {
const { code, detail } = remote;
Logger.error('Failed to send dwn application record to remote', remote);
throw new DwnError(code, detail);
}
Logger.debug('Sent application record to remote dwn', remote);
return { status: remote, record };
}
/**
* Setup DWN with credential-applicant protocol and manifest records
* @returns boolean indicating success or failure
*/
public async setupDwn(): Promise<void> {
// Logger.log('Setting up dwn ...');
try {
// Query DWN for credential-applicant protocols
const { protocols } = await this.queryProtocols();
Logger.log(`Found ${protocols.length} dcx applicant protocol in dwn`, protocols);
// Configure DWN with credential-applicant protocol if not found
if (!protocols.length) {
Logger.log('Configuring dwn with dcx applicant protocol ...');
const { status, protocol } = await this.configureProtocols();
Logger.log(
`Configured credential applicant protocol in dwn: ${status.code} - ${status.detail}`,
protocol,
);
}
this.isSetup = true;
Logger.log('DWN Setup Complete!');
} catch (error: any) {
Logger.error(`DWN Setup Failed!`, error);
throw error;
}
}
/**
*
* Checks the state of the password and recovery phrase
*
* @param firstLaunch A boolean indicating if this is the first launch of the agent
* @returns { password: string, recoveryPhrase?: string }
* @throws DcxServerError
*
*/
public async checkWeb5Config(): Promise<{ password: string; recoveryPhrase?: string }> {
const web5Password = this.config.web5Password;
const web5RecoveryPhrase = this.config.web5RecoveryPhrase;
// TODO: consider generating a new recovery phrase if one is not provided
// this.config.APPLICANT_WEB5_RECOVERY_PHRASE = Mnemonic.createRecoveryPhrase();
if (!web5Password && !web5RecoveryPhrase) {
Logger.security(
'APPLICANT_WEB5_PASSWORD and APPLICANT_WEB5_RECOVERY_PHRASE not found on first launch! ' +
'New APPLICANT_WEB5_PASSWORD saved to applicant.password.key file. ' +
'New APPLICANT_WEB5_RECOVERY_PHRASE saved to applicant.recovery.key file.',
);
const password = Mnemonic.createPassword();
await FileSystem.overwrite('applicant.password.key', password);
const recoveryPhrase = Mnemonic.createRecoveryPhrase();
await FileSystem.overwrite('applicant.recovery.key', recoveryPhrase);
this.config.web5Password = password;
this.config.web5RecoveryPhrase = recoveryPhrase;
return { password, recoveryPhrase };
}
if (web5Password && !web5RecoveryPhrase) {
Logger.warn(
'APPLICANT_WEB5_PASSWORD found without APPLICANT_WEB5_RECOVERY_PHRASE! ' +
'Attempting to unlock the vault with APPLICANT_WEB5_PASSWORD.',
);
return { password: web5Password };
}
return {
password : web5Password,
recoveryPhrase : web5RecoveryPhrase,
};
}
/**
*
* Configures the DCX server by creating a new password, initializing Web5,
* connecting to the remote DWN and configuring the DWN with the DCX applicant protocol
*
*/
public async initializeWeb5(): Promise<void> {
Logger.log('Initializing Web5 for DcxApplicant ... ');
// Check the state of the password and recovery phrase
const { password: userPassword, recoveryPhrase: userRecoveryPhrase } = await this.checkWeb5Config();
// Toggle the initialization options based on the presence of a recovery phrase
const dwnEndpoints = this.options.dwns!;
const connectParams = !userRecoveryPhrase
? {
password : userPassword,
didCreateOptions : { dwnEndpoints }
}
: {
password : userPassword,
recoveryPhrase : userRecoveryPhrase,
didCreateOptions : { dwnEndpoints }
};
const { web5, did } = await Web5.connect(connectParams);
const agent = web5.agent as Web5PlatformAgent;
// Set the DcxManager properties
DcxApplicant.web5 = web5;
DcxApplicant.agent = agent;
DcxApplicant.did = did;
// Set the server initialized flag
this.isInitialized = true;
}
}