Newer
Older
import {
Agent,
AutoAcceptCredential,
AutoAcceptProof,
ConnectionStateChangedEvent,
CredentialState,
CredentialStateChangedEvent,
DidDocument,
DidExchangeRole,
KeyDidResolver,
PeerDidResolver,
ProofState,
ProofStateChangedEvent,
TypedArrayEncoder,
V2CredentialProtocol,
V2ProofProtocol,
WebDidResolver,
JwkDidResolver,
import {
AnonCredsCredentialFormatService,
AnonCredsModule,
AnonCredsProof,
import {
IndyVdrAnonCredsRegistry,
IndyVdrIndyDidRegistrar,
IndyVdrIndyDidResolver,
IndyVdrModule,
import { indyVdr } from "@hyperledger/indy-vdr-nodejs";
import { anoncreds } from '@hyperledger/anoncreds-nodejs'
import { AskarModule } from "@credo-ts/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";
import {
catchError,
filter,
lastValueFrom,
map,
Observable,
Subject,
take,
timeout,
} from "rxjs";
import { SubjectInboundTransport } from "./askar/transports/agent.subject.inbound.transport";
import { SubjectOutboundTransport } from "./askar/transports/agent.subject.outbound.transport";
import { S3TailsFileService } from './credo/revocation/TailFileService';
export type EventBehaviorSubject = BehaviorSubject<BaseEvent>;
export type SubjectMessage = {
message: EncryptedMessage;
replySubject?: Subject<SubjectMessage>;
};
import { Request, Response, Express } from "express";
import { JsonLdCredentialFormatService } from "./credo/JsonLdCredentialFormatService";
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({
keyType: KeyType.Ed25519,
});
} catch (e) {
if (e instanceof WalletKeyExistsError) {
algorithm: KeyAlgs.Ed25519,
});
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,
}: {
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;
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) {
}
const verificationMethodKey0Id = `${didWeb}#jwt-key0`;
const jsonDidDoc = {
"@context": [
"https://www.w3.org/ns/did/v1",
id: verificationMethodKey0Id,
type: "Ed25519VerificationKey2018",
controller: didWeb,
publicKeyBase58: pubKey.publicKeyBase58,
authentication: [verificationMethodKey0Id],
assertionMethod: [verificationMethodKey0Id],
keyAgreement: [verificationMethodKey0Id],
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,
});
await agent.dids.import({
did: didWeb,
didDocument: didDocumentInstance,
});
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) => {
const tailsServerBaseUrl = process.env["TAILS_SERVER_BASE_URL"] || undefined;
const s3AccessKey = process.env["TAILS_SERVER_KEY"] || undefined;
const s3Secret = process.env["TAILS_SERVER_SECRET"] || undefined;
const tailsServerBucketName = process.env["TAILS_SERVER_BUCKET_NAME"] || undefined;
if (
!tailsServerBaseUrl ||
!s3AccessKey ||
!s3Secret ||
!tailsServerBucketName
) {
throw new Error("Tails Storage Information not provided.");
}
return {
connections: new ConnectionsModule({
autoAcceptConnections: true,
}),
credentials: new CredentialsModule({
autoAcceptCredentials: AutoAcceptCredential.ContentApproved,
credentialProtocols: [
new V2CredentialProtocol({
credentialFormats: [
new AnonCredsCredentialFormatService(),
new JsonLdCredentialFormatService(),
],
}),
],
}),
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,
tailsFileService: new S3TailsFileService({
tailsServerBaseUrl,
s3AccessKey,
s3Secret,
tailsServerBucketName,
}),
}),
indyVdr: new IndyVdrModule({
indyVdr,
networks,
}),
dids: new DidsModule({
registrars: [new IndyVdrIndyDidRegistrar()],
resolvers: [
new IndyVdrIndyDidResolver(),
new KeyDidResolver(),
new PeerDidResolver(),
],
}),
askar: new AskarModule({
ariesAskar,
}),
const INITIAL_EVENT: BaseEvent = {
type: "unknown",
payload: {},
metadata: {
contextCorrelationId: "-1",
},
};
export const setupEventBehaviorSubjects = (
): BehaviorSubject<BaseEvent>[] => {
const behaviorSubjects: EventBehaviorSubject[] = [];
const behaviorSubject = new BehaviorSubject<BaseEvent>(INITIAL_EVENT);
agent.events.observable(eventType).subscribe(behaviorSubject);
behaviorSubjects.push(behaviorSubject);
};
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));
}
};
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
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 {
console.log(
`sending data to svdx ${email}, ${ev.payload.proofRecord.connectionId}`,
);
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
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");
return;
}
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;
}
try {
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");
}
};
export const isProofStateChangedEvent = (
e: BaseEvent,
): e is ProofStateChangedEvent => e.type === ProofEventTypes.ProofStateChanged;
export const isCredentialStateChangedEvent = (
e: BaseEvent,
): e is CredentialStateChangedEvent =>
e.type === CredentialEventTypes.CredentialStateChanged;
export const waitForProofExchangeRecordSubject = (
subject: BehaviorSubject<BaseEvent> | Observable<BaseEvent>,
threadId,
parentThreadId,
state,
previousState,
timeoutMs = 10000,
count = 1,
}: {
threadId?: string;
parentThreadId?: string;
state?: ProofState;
previousState?: ProofState | null;
timeoutMs?: number;
count?: number;
},
subject instanceof BehaviorSubject ? subject.asObservable() : subject;
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),
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
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),
(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),
),
timeout(timeoutMs),
catchError(() => {
throw new Error(
`CredentialStateChanged event not emitted within specified timeout: ${timeoutMs}
previousState: ${previousState},
threadId: ${threadId},
parentThreadId: ${parentThreadId},
state: ${state}
}`,
);
}),
take(count),
map((e) => e.payload.credentialRecord),
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
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");
}
},
);
};
export const attachDidWebHandler = (
server: Express,
agent: Agent,
agentPeerAddress: string,
): void => {
const parsedUrl = url.parse(agentPeerAddress);
const pathname = parsedUrl.pathname?.replace(/^\/+|\/+$/g, "");
let serverDidWebPath: string;
if (pathname) {
serverDidWebPath = `/did.json`;
} else {
serverDidWebPath = "/.well-known/did.json";
}
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");
if (!didWebRecord) {
return res.status(404).send("Not found");
}
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>(
webHookTopic: string,
payload: T,
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}`);