Skip to content
Snippets Groups Projects
agent.utils.ts 16.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • import {
      Agent,
      AutoAcceptCredential,
      AutoAcceptProof,
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
      BaseEvent,
    
      ConnectionsModule,
    
      CredentialsModule,
    
      DidsModule,
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
      EncryptedMessage,
    
      Key,
    
      KeyType,
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
      ProofEventTypes,
    
      ProofsModule,
    
      TypedArrayEncoder,
      V2CredentialProtocol,
      V2ProofProtocol,
    
      WalletError,
    
      WalletKeyExistsError,
    
      CredentialStateChangedEvent,
      CredentialEventTypes,
      CredentialState,
      ProofExchangeRecord,
    
      JsonTransformer,
      DidDocument,
      WebDidResolver,
      JwkDidResolver,
    
    Zdravko Iliev's avatar
    Zdravko Iliev committed
      TrustPingResponseReceivedEvent,
    
    } from "@aries-framework/core";
    import {
      AnonCredsCredentialFormatService,
      AnonCredsModule,
    
      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 AskarKey, KeyAlgs } from "@hyperledger/aries-askar-shared";
    
    import { IConfAgent } from "@ocm-engine/config";
    
    import axios, { AxiosResponse } from "axios";
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
    import {
      catchError,
      filter,
      lastValueFrom,
      map,
      Observable,
    
      BehaviorSubject,
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
      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 EventBehaviorSubject = BehaviorSubject<BaseEvent>;
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
    export type SubjectMessage = {
      message: EncryptedMessage;
      replySubject?: Subject<SubjectMessage>;
    };
    
    import { Request, Response, Express } from "express";
    
    import url from "url";
    
    
    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 seedBuffer = TypedArrayEncoder.fromString(seed);
    
      try {
        return await agent.wallet.createKey({
    
          seed: seedBuffer,
    
          keyType: KeyType.Ed25519,
        });
      } catch (e) {
        if (e instanceof WalletKeyExistsError) {
    
          const c = AskarKey.fromSeed({
    
            algorithm: KeyAlgs.Ed25519,
    
            seed: seedBuffer,
    
          });
    
          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 generateDidWeb = async ({
      seed,
      agent,
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      peerAddress,
    
    }: {
      seed: string;
      agent: Agent;
      peerAddress: string;
    }) => {
      console.log("Generating did web");
      const pubKey = await generateKey({ seed, agent });
    
      const parsedUrl = url.parse(peerAddress);
      let hostname = parsedUrl.hostname!;
      const port = parsedUrl.port;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const pathname = parsedUrl.pathname?.replace(/^\/+|\/+$/g, "");
    
    
      // If port is specified, encode it
      if (port) {
        hostname += `%3A${port}`;
      }
      // Convert URLs to 'did:web' form
      let didWeb = `did:web:${hostname}`;
      if (pathname) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        didWeb += `:${pathname.replace(/\//g, ":")}`;
    
      }
    
      const verificationMethodKey0Id = `${didWeb}#jwt-key0`;
    
      const jsonDidDoc = {
        "@context": [
          "https://www.w3.org/ns/did/v1",
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          "https://w3id.org/security/suites/ed25519-2018/v1",
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        id: didWeb,
        verificationMethod: [
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            id: verificationMethodKey0Id,
            type: "Ed25519VerificationKey2018",
            controller: didWeb,
            publicKeyBase58: pubKey.publicKeyBase58,
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        authentication: [verificationMethodKey0Id],
        assertionMethod: [verificationMethodKey0Id],
        keyAgreement: [verificationMethodKey0Id],
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const didDocumentInstance = JsonTransformer.fromJSON(jsonDidDoc, DidDocument);
    
    
      const recordId = "did:web";
      const existingRecord = await agent.genericRecords.findById(recordId);
      if (existingRecord) {
        await agent.genericRecords.deleteById(recordId);
      }
      await agent.genericRecords.save({
        id: recordId,
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        content: jsonDidDoc,
    
      });
    
      await agent.dids.import({
        did: didWeb,
        didDocument: didDocumentInstance,
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        overwrite: false,
    
      });
    
      console.log("Generated did:web");
      console.log(didWeb);
      console.log(JSON.stringify(didDocumentInstance.toJSON(), null, 2));
    };
    
    
    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()],
    
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
          // @ts-ignore
          autoCreateLinkSecret: true,
    
        }),
        anoncredsRs: new AnonCredsRsModule({
          anoncreds,
    
          autoCreateLinkSecret: true,
    
        }),
        indyVdr: new IndyVdrModule({
          indyVdr,
          networks,
        }),
        dids: new DidsModule({
          registrars: [new IndyVdrIndyDidRegistrar()],
    
          resolvers: [
            new IndyVdrIndyDidResolver(),
            new KeyDidResolver(),
            new PeerDidResolver(),
    
            new WebDidResolver(),
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            new JwkDidResolver(),
    
        }),
        askar: new AskarModule({
          ariesAskar,
        }),
      } as const;
    };
    
    const INITIAL_EVENT: BaseEvent = {
      type: "unknown",
      payload: {},
      metadata: {
        contextCorrelationId: "-1",
      },
    };
    
    export const setupEventBehaviorSubjects = (
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
      agents: Agent[],
      eventTypes: string[],
    
    ): BehaviorSubject<BaseEvent>[] => {
      const behaviorSubjects: EventBehaviorSubject[] = [];
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
    
      for (const agent of agents) {
    
        const behaviorSubject = new BehaviorSubject<BaseEvent>(INITIAL_EVENT);
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
    
        for (const eventType of eventTypes) {
    
          agent.events.observable(eventType).subscribe(behaviorSubject);
    
        behaviorSubjects.push(behaviorSubject);
    
      return behaviorSubjects;
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
    };
    
    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 {
    
    Zdravko Iliev's avatar
    Zdravko Iliev committed
        console.log(
          `sending data to svdx ${email}, ${ev.payload.proofRecord.connectionId}`,
        );
    
        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;
      }
    
    
      const outOfBandId = ev.payload.connectionRecord.outOfBandId;
    
      if (typeof outOfBandId === "undefined") {
        console.log(JSON.stringify(ev.payload, null, 2));
        console.log("Out of Band id not found, skipping");
    
      const outOfBandRecord = await agent.oob.findById(outOfBandId);
    
      if (!outOfBandRecord) {
        console.log(JSON.stringify(ev.payload, null, 2));
        console.log("No out of band record found");
        return;
      }
    
      if (
        !outOfBandRecord.outOfBandInvitation.goal ||
        !config?.agentOobGoals.includes(outOfBandRecord.outOfBandInvitation.goal)
      ) {
        console.log(JSON.stringify(ev.payload, null, 2));
        console.log("This connection does not have any goals");
        return;
    
        console.log(`Sending proof request, to ${ev.payload.connectionRecord.id}`);
    
        await agent.proofs.requestProof({
          protocolVersion: "v2",
    
          connectionId: ev.payload.connectionRecord.id,
    
          proofFormats: {
            anoncreds: {
              name: "proof-request",
              version: "1.0",
              requested_attributes: {
                email: {
                  name: "email",
                },
              },
            },
          },
        });
      } catch (e) {
        console.log(JSON.stringify(e, null, 2));
    
        console.log("failed to offer credential");
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
    
    export const isProofStateChangedEvent = (
      e: BaseEvent,
    ): e is ProofStateChangedEvent => e.type === ProofEventTypes.ProofStateChanged;
    
    
    export const isCredentialStateChangedEvent = (
      e: BaseEvent,
    ): e is CredentialStateChangedEvent =>
      e.type === CredentialEventTypes.CredentialStateChanged;
    
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
    export const waitForProofExchangeRecordSubject = (
    
      subject: BehaviorSubject<BaseEvent> | Observable<BaseEvent>,
    
        proofRecordId,
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
        threadId,
        parentThreadId,
        state,
        previousState,
        timeoutMs = 10000,
        count = 1,
      }: {
    
        proofRecordId?: string;
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
        threadId?: string;
        parentThreadId?: string;
        state?: ProofState;
        previousState?: ProofState | null;
        timeoutMs?: number;
        count?: number;
      },
    
    ): Promise<ProofExchangeRecord> => {
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
      const observable: Observable<BaseEvent> =
    
        subject instanceof BehaviorSubject ? subject.asObservable() : subject;
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
      return lastValueFrom(
        observable.pipe(
          filter(isProofStateChangedEvent),
          filter(
            (e) =>
    
              (proofRecordId === undefined ||
                e.payload.proofRecord.id === proofRecordId) &&
              (previousState === undefined ||
                e.payload.previousState === previousState) &&
              (threadId === undefined ||
                e.payload.proofRecord.threadId === threadId) &&
              (parentThreadId === undefined ||
                e.payload.proofRecord.parentThreadId === parentThreadId) &&
              (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 waitForCredentialExchangeRecordSubject = (
      subject: BehaviorSubject<BaseEvent> | Observable<BaseEvent>,
      {
        credentialRecordId,
        threadId,
        parentThreadId,
        state,
        previousState,
        timeoutMs = 10000,
        count = 1,
      }: {
        credentialRecordId?: string;
        threadId?: string;
        parentThreadId?: string;
        state?: CredentialState;
        previousState?: CredentialState | null;
        timeoutMs?: number;
        count?: number;
      },
    ) => {
      const observable: Observable<BaseEvent> =
        subject instanceof BehaviorSubject ? subject.asObservable() : subject;
      return lastValueFrom(
        observable.pipe(
          filter(isCredentialStateChangedEvent),
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
          filter(
            (e) =>
    
              (credentialRecordId === undefined ||
                e.payload.credentialRecord.id === credentialRecordId) &&
              (previousState === undefined ||
                e.payload.previousState === previousState) &&
              (threadId === undefined ||
                e.payload.credentialRecord.threadId === threadId) &&
              (parentThreadId === undefined ||
                e.payload.credentialRecord.parentThreadId === parentThreadId) &&
              (state === undefined || e.payload.credentialRecord.state === state),
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
          ),
          timeout(timeoutMs),
          catchError(() => {
            throw new Error(
    
              `CredentialStateChanged event not emitted within specified timeout: ${timeoutMs}
    
    Boyan Tsolov's avatar
    Boyan Tsolov committed
              previousState: ${previousState},
              threadId: ${threadId},
              parentThreadId: ${parentThreadId},
              state: ${state}
            }`,
            );
          }),
          take(count),
    
          map((e) => e.payload.credentialRecord),
    
    
    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");
          }
        },
      );
    };
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    export const attachDidWebHandler = (
      server: Express,
      agent: Agent,
      agentPeerAddress: string,
    ): void => {
    
      const parsedUrl = url.parse(agentPeerAddress);
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const pathname = parsedUrl.pathname?.replace(/^\/+|\/+$/g, "");
    
    
      let serverDidWebPath: string;
      if (pathname) {
        serverDidWebPath = `/did.json`;
      } else {
        serverDidWebPath = "/.well-known/did.json";
      }
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      console.log("Listen did web requests on path " + serverDidWebPath);
      server.get(serverDidWebPath, async (req: Request, res: Response) => {
        try {
          const didWebRecord = await agent.genericRecords.findById("did:web");
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          if (!didWebRecord) {
            return res.status(404).send("Not found");
          }
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          const didWebDoc = didWebRecord.content;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          return res.header("Content-Type", "application/json").send(didWebDoc);
        } catch (error) {
          console.error(error);
          return res.status(500).send("Internal Server Error");
        }
      });
    
    export const webHookHandler = async <T>(
    
    Zdravko Iliev's avatar
    Zdravko Iliev committed
      addr: string,
    
      webHookTopic: string,
      payload: T,
    
    Zdravko Iliev's avatar
    Zdravko Iliev committed
    ) => {
    
      const promises: Promise<AxiosResponse>[] = [];
    
      const tokenUrlPairs = addr.split(";");
    
      for (const pair of tokenUrlPairs) {
        const [token, url] = pair.split("@");
    
    
        const promise = axios.post(`${url}/topic/${webHookTopic}`, payload, {
          headers: {
            "X-Api-Key": token,
    
    
        promises.push(promise);
      }
    
      const promiseResults = await Promise.allSettled(promises);
      for (let index = 0; index < promiseResults.length; index++) {
        const promiseResult = promiseResults[index];
        const [_, url] = tokenUrlPairs[index].split("@");
    
        if (promiseResult.status === "rejected") {
          console.log(
    
            `Failed to send web hook to ${url}/topic/${webHookTopic}. Reason ${promiseResult.reason}`,
    
        console.log(`Successfully sent web hook to ${url}/topic/${webHookTopic}`);