Newer
Older
import {
Agent,
AutoAcceptCredential,
AutoAcceptProof,
ConnectionStateChangedEvent,
DidExchangeRole,
KeyDidResolver,
PeerDidResolver,
ProofState,
ProofStateChangedEvent,
TypedArrayEncoder,
V2CredentialProtocol,
V2ProofProtocol,
CredentialStateChangedEvent,
CredentialEventTypes,
CredentialState,
ProofExchangeRecord,
JsonTransformer,
DidDocument,
WebDidResolver,
JwkDidResolver,
} 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 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";
export type EventBehaviorSubject = BehaviorSubject<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 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));
};
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
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,
}),
indyVdr: new IndyVdrModule({
indyVdr,
networks,
}),
dids: new DidsModule({
registrars: [new IndyVdrIndyDidRegistrar()],
resolvers: [
new IndyVdrIndyDidResolver(),
new KeyDidResolver(),
new PeerDidResolver(),
],
}),
askar: new AskarModule({
ariesAskar,
}),
} as const;
};
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));
}
};
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
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}`,
);
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
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),
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
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),
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
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}`);