Skip to content
Snippets Groups Projects
agent.utils.ts 10.4 KiB
Newer Older
import {
  Agent,
  AutoAcceptCredential,
  AutoAcceptProof,
Boyan Tsolov's avatar
Boyan Tsolov committed
  BaseEvent,
  ConnectionsModule,
  CredentialsModule,
  DidsModule,
Boyan Tsolov's avatar
Boyan Tsolov committed
  EncryptedMessage,
  KeyType,
Boyan Tsolov's avatar
Boyan Tsolov committed
  ProofEventTypes,
  ProofsModule,
  TypedArrayEncoder,
  V2CredentialProtocol,
  V2ProofProtocol,
  WalletError,
  WalletKeyExistsError,
} 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 C, KeyAlgs } from "@hyperledger/aries-askar-shared";
import { IConfAgent } from "@ocm-engine/config";
import axios from "axios";
Boyan Tsolov's avatar
Boyan Tsolov committed
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;
};
Boyan Tsolov's avatar
Boyan Tsolov committed
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");
  }
};
Boyan Tsolov's avatar
Boyan Tsolov committed

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");
      }
    },
  );
};