import { Agent, AutoAcceptCredential, AutoAcceptProof, BaseEvent, ConnectionsModule, ConnectionStateChangedEvent, CredentialsModule, DidExchangeRole, DidsModule, EncryptedMessage, Key, KeyDidResolver, KeyType, PeerDidResolver, ProofEventTypes, ProofsModule, ProofState, ProofStateChangedEvent, TypedArrayEncoder, V2CredentialProtocol, V2ProofProtocol, WalletError, WalletKeyExistsError, OutOfBandState, } from "@aries-framework/core"; import { AnonCredsCredentialFormatService, AnonCredsModule, AnonCredsProof, AnonCredsProofFormatService, } from "@aries-framework/anoncreds"; import { IndyVdrAnonCredsRegistry, IndyVdrIndyDidRegistrar, IndyVdrIndyDidResolver, IndyVdrModule, } from "@aries-framework/indy-vdr"; import { AnonCredsRsModule } from "@aries-framework/anoncreds-rs"; import { anoncreds } from "@hyperledger/anoncreds-nodejs"; import { indyVdr } from "@hyperledger/indy-vdr-nodejs"; import { AskarModule } from "@aries-framework/askar"; import { ariesAskar } from "@hyperledger/aries-askar-nodejs"; import { Key as C, KeyAlgs } from "@hyperledger/aries-askar-shared"; import { IConfAgent } from "@ocm-engine/config"; import axios from "axios"; import { catchError, filter, lastValueFrom, map, Observable, ReplaySubject, Subject, take, timeout, } from "rxjs"; import { SubjectInboundTransport } from "./askar/transports/agent.subject.inbound.transport"; import { SubjectOutboundTransport } from "./askar/transports/agent.subject.outbound.transport"; export type EventReplaySubject = ReplaySubject<BaseEvent>; export type SubjectMessage = { message: EncryptedMessage; replySubject?: Subject<SubjectMessage>; }; import { Request, Response, Express } from "express"; export const importDidsToWallet = async ( agent: Agent, dids: Array<string>, ): Promise<void> => { for (const did in dids) { try { await agent.dids.import({ did: dids[did], overwrite: false, }); } catch (e) { console.log((e as Error).message); } } }; export const generateKey = async ({ seed, agent, }: { seed: string; agent: Agent; }): Promise<Key> => { if (!seed) { throw Error("No seed provided"); } const privateKey = TypedArrayEncoder.fromString(seed); try { return await agent.wallet.createKey({ seed: privateKey, keyType: KeyType.Ed25519, }); } catch (e) { if (e instanceof WalletKeyExistsError) { const c = C.fromSecretBytes({ algorithm: KeyAlgs.Ed25519, secretKey: TypedArrayEncoder.fromString(seed), }); return Key.fromPublicKey(c.publicBytes, KeyType.Ed25519); } if (e instanceof WalletError) { throw new Error(`Failed to create key - ${e.message}`); } throw new Error("Unknown"); } }; export const generateDidFromKey = (key: Key): string => { if (!key) { throw new Error("Key not provided"); } return TypedArrayEncoder.toBase58(key.publicKey.slice(0, 16)); }; //eslint-disable-next-line export const getAskarAnonCredsIndyModules = (networks: any) => { return { connections: new ConnectionsModule({ autoAcceptConnections: true, }), credentials: new CredentialsModule({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, credentialProtocols: [ new V2CredentialProtocol({ credentialFormats: [new AnonCredsCredentialFormatService()], }), ], }), proofs: new ProofsModule({ autoAcceptProofs: AutoAcceptProof.ContentApproved, proofProtocols: [ new V2ProofProtocol({ proofFormats: [new AnonCredsProofFormatService()], }), ], }), anoncreds: new AnonCredsModule({ registries: [new IndyVdrAnonCredsRegistry()], }), anoncredsRs: new AnonCredsRsModule({ anoncreds, }), indyVdr: new IndyVdrModule({ indyVdr, networks, }), dids: new DidsModule({ registrars: [new IndyVdrIndyDidRegistrar()], resolvers: [ new IndyVdrIndyDidResolver(), new KeyDidResolver(), new PeerDidResolver(), ], }), askar: new AskarModule({ ariesAskar, }), } as const; }; export const setupEventReplaySubjects = ( agents: Agent[], eventTypes: string[], ): ReplaySubject<BaseEvent>[] => { const replaySubjects: EventReplaySubject[] = []; for (const agent of agents) { const replaySubject = new ReplaySubject<BaseEvent>(); for (const eventType of eventTypes) { agent.events.observable(eventType).subscribe(replaySubject); } replaySubjects.push(replaySubject); } return replaySubjects; }; export const setupSubjectTransports = (agents: Agent[]) => { const subjectMap: Record<string, Subject<SubjectMessage>> = {}; for (const agent of agents) { const messages = new Subject<SubjectMessage>(); subjectMap[agent.config.endpoints[0]] = messages; agent.registerInboundTransport(new SubjectInboundTransport(messages)); agent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)); } }; export const svdxProofStateChangeHandler = async ( ev: ProofStateChangedEvent, agent: Agent, config?: IConfAgent, ) => { if (ProofState.Done !== ev.payload.proofRecord.state) { return; } const presentationMessage = await agent.proofs.findPresentationMessage( ev.payload.proofRecord.id, ); console.log(JSON.stringify(presentationMessage, null, 2)); if (!presentationMessage) { console.log("No presentation message found"); return; } const attachmentId = presentationMessage.formats[0].attachmentId; const attachment = presentationMessage.getPresentationAttachmentById(attachmentId); console.log(JSON.stringify(attachment, null, 2)); if (!attachment) { console.log("No attachment found"); return; } const email = attachment.getDataAsJson<AnonCredsProof>()?.requested_proof.revealed_attrs[ "email" ].raw; if (!config?.agentSVDXWebHook) { console.log("Agent SVDX web hook not set"); return; } try { await axios.post( config?.agentSVDXWebHook, { email, connectionId: ev.payload.proofRecord.connectionId, }, { auth: { username: config?.agentSVDXBasicUser, password: config?.agentSVDXBasicPass, }, }, ); } catch (e) { console.log(JSON.stringify(e, null, 2)); } }; export const svdxConnectionStateChangeHandler = async ( ev: ConnectionStateChangedEvent, agent: Agent, config?: IConfAgent, ) => { if ( ev.payload.connectionRecord.role === DidExchangeRole.Responder && ev.payload.connectionRecord.state !== "completed" ) { return; } await agent.connections.addConnectionType( ev.payload.connectionRecord.id, ev.payload.connectionRecord.theirLabel || "svdx", ); console.log("connection accepted", JSON.stringify(ev, null, 2)); const connections = await agent.connections.findAllByConnectionTypes([ ev.payload.connectionRecord.theirLabel || "svdx", ]); if (connections.length < 2) { return; } connections.sort( (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(), ); while (connections.length > 1) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const con = connections.pop()!; console.log(`deleting ${con.id}`); await agent.connections.deleteById(con.id); } try { await agent.proofs.requestProof({ protocolVersion: "v2", connectionId: connections[0].id, proofFormats: { anoncreds: { name: "proof-request", version: "1.0", requested_attributes: { email: { name: "email", restrictions: [ { schema_id: config?.agentSVDXSchemaId, cred_def_id: config?.agentSVDXCredDefId, }, ], }, }, }, }, }); } catch (e) { console.log(JSON.stringify(e, null, 2)); console.log("failed to issue credential"); } }; export const isProofStateChangedEvent = ( e: BaseEvent, ): e is ProofStateChangedEvent => e.type === ProofEventTypes.ProofStateChanged; export const waitForProofExchangeRecordSubject = ( subject: ReplaySubject<BaseEvent> | Observable<BaseEvent>, { threadId, parentThreadId, state, previousState, timeoutMs = 10000, count = 1, }: { threadId?: string; parentThreadId?: string; state?: ProofState; previousState?: ProofState | null; timeoutMs?: number; count?: number; }, ) => { const observable: Observable<BaseEvent> = subject instanceof ReplaySubject ? subject.asObservable() : subject; return lastValueFrom( observable.pipe( filter(isProofStateChangedEvent), filter( (e) => previousState === undefined || e.payload.previousState === previousState, ), filter( (e) => threadId === undefined || e.payload.proofRecord.threadId === threadId, ), filter( (e) => parentThreadId === undefined || e.payload.proofRecord.parentThreadId === parentThreadId, ), filter( (e) => state === undefined || e.payload.proofRecord.state === state, ), timeout(timeoutMs), catchError(() => { throw new Error( `ProofStateChangedEvent event not emitted within specified timeout: ${timeoutMs} previousState: ${previousState}, threadId: ${threadId}, parentThreadId: ${parentThreadId}, state: ${state} }`, ); }), take(count), map((e) => e.payload.proofRecord), ), ); }; export const attachShortUrlHandler = (server: Express, agent: Agent): void => { server.get( "/invitations/:invitationId", async (req: Request, res: Response) => { try { const { invitationId } = req.params; const outOfBandRecord = await agent.oob.findByCreatedInvitationId( invitationId, ); if ( !outOfBandRecord || outOfBandRecord.state !== OutOfBandState.AwaitResponse ) { return res.status(404).send("Not found"); } const invitationJson = outOfBandRecord.outOfBandInvitation.toJSON(); return res .header("Content-Type", "application/json") .send(invitationJson); } catch (error) { console.error(error); return res.status(500).send("Internal Server Error"); } }, ); };