UNPKG

@gleif-it/vlei-verifier-workflows

Version:

Workflows for vLEI users and vLEI credentials for the vLEI-verifier service

465 lines (464 loc) 16.8 kB
import assert from 'assert'; import SignifyClient from 'signify-ts'; import { retry } from './retry.js'; import { resolveEnvironment } from './resolve-env.js'; import { WorkflowState } from '../workflow-state.js'; import { getIdentifierData, } from './handle-json-config.js'; export function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } export async function admitSinglesig(client, aidName, recipientAid) { const grantMsgSaid = await waitAndMarkNotification(client, '/exn/ipex/grant'); const [admit, sigs, aend] = await client.ipex().admit({ senderName: aidName, message: '', grantSaid: grantMsgSaid, recipient: recipientAid.prefix, }); await client .ipex() .submitAdmit(aidName, admit, sigs, aend, [recipientAid.prefix]); } /** * Assert that all operations were waited for. * <p>This is a postcondition check to make sure all long-running operations have been waited for * @see waitOperation */ export async function assertOperations(...clients) { for (const client of clients) { const operations = await client.operations().list(); assert(operations.length === 0); } } /** * Assert that all notifications were handled. * <p>This is a postcondition check to make sure all notifications have been handled * @see markNotification * @see markAndRemoveNotification */ export async function assertNotifications(...clients) { for (const client of clients) { const res = await client.notifications().list(); const notes = res.notes.filter((i) => i.r === false); assert(notes.length === 0); } } export async function createAid(client, name) { const [prefix, oobi] = await getOrCreateIdentifier(client, name); return { prefix, oobi, name }; } export async function createAID(client, name) { await getOrCreateIdentifier(client, name); const aid = await client.identifiers().get(name); console.log(name, 'AID:', aid.prefix); return aid; } export function createTimestamp() { return new Date().toISOString().replace('Z', '000+00:00'); } /** * Get list of end role authorizations for a Keri idenfitier */ export async function getEndRoles(client, alias, role) { const path = role !== undefined ? `/identifiers/${alias}/endroles/${role}` : `/identifiers/${alias}/endroles`; const response = await client.fetch(path, 'GET', null); if (!response.ok) throw new Error(await response.text()); const result = await response.json(); // console.log("getEndRoles", result); return result; } export async function getGrantedCredential(client, credId) { const credentialList = await client.credentials().list({ filter: { '-d': credId }, }); let credential; if (credentialList.length > 0) { assert(credentialList.length === 1); credential = credentialList[0]; } return credential; } export async function getIssuedCredential(issuerClient, issuerAID, recipientAID, schemaSAID) { const credentialList = await issuerClient.credentials().list({ filter: { '-i': issuerAID.prefix, '-s': schemaSAID, '-a-i': recipientAID.prefix, }, }); assert(credentialList.length <= 1); return credentialList[0]; } export async function getOrCreateAID(client, name, kargs) { if (!client) { throw new Error("getOrCreateAID: client doesn't exist"); } try { return await client.identifiers().get(name); } catch { console.log('Creating AID', name, ': ', kargs); const result = await client .identifiers() .create(name, kargs); await waitOperation(client, await result.op()); const aid = await client.identifiers().get(name); const op = await client .identifiers() .addEndRole(name, 'agent', client?.agent?.pre ?? undefined); await waitOperation(client, await op.op()); console.log(name, 'AID:', aid.prefix); return aid; } } /** * Connect or boot a SignifyClient instance */ export async function getOrCreateClient(bran = undefined, getOnly = false) { const env = resolveEnvironment(); await SignifyClient.ready(); bran ??= SignifyClient.randomPasscode(); bran = bran.padEnd(21, '_'); const client = new SignifyClient.SignifyClient(env.url, bran, SignifyClient.Tier.low, env.bootUrl); try { await client.connect(); } catch (e) { if (!getOnly) { const res = await client.boot(); if (!res.ok) throw new Error(); await client.connect(); } else { throw new Error('Could not connect to client w/ bran ' + bran + e.message); } } console.log('client', { agent: client.agent?.pre, controller: client.controller.pre, }); return client; } /** * Connect or boot a number of SignifyClient instances * @example * <caption>Create two clients with random secrets</caption> * let client1: SignifyClient, client2: SignifyClient; * beforeAll(async () => { * [client1, client2] = await getOrCreateClients(2); * }); * @example * <caption>Launch jest from shell with pre-defined secrets</caption> */ export async function getOrCreateClients(count, brans = undefined, getOnly = false) { const tasks = []; for (let i = 0; i < count; i++) { tasks.push(getOrCreateClient(brans?.at(i) ?? undefined, getOnly)); } const clients = await Promise.all(tasks); console.log(`secrets="${clients.map((i) => i.bran).join(',')}"`); return clients; } /** * Get or resolve a Keri contact * @example * <caption>Create a Keri contact before running tests</caption> * let contact1_id: string; * beforeAll(async () => { * contact1_id = await getOrCreateContact(client2, "contact1", name1_oobi); * }); */ export async function getOrCreateContact(client, name, oobi) { const list = await client.contacts().list(undefined, 'alias', `^${name}$`); // console.log("contacts.list", list); if (list.length > 0) { const contact = list[0]; if (contact.oobi === oobi) { // console.log("contacts.id", contact.id); return contact.id; } } let op = await client.oobis().resolve(oobi, name); op = await waitOperation(client, op); return op.response.i; } /** * Get or create a Keri identifier. Uses default witness config from `resolveEnvironment` * @example * <caption>Create a Keri identifier before running tests</caption> * let name1_id: string, name1_oobi: string; * beforeAll(async () => { * [name1_id, name1_oobi] = await getOrCreateIdentifier(client1, "name1"); * }); * @see resolveEnvironment */ export async function getOrCreateIdentifier(client, name, kargs = undefined) { let id = undefined; try { const identfier = await client.identifiers().get(name); // console.log("identifiers.get", identfier); id = identfier.prefix; } catch { const env = resolveEnvironment(); kargs ??= env.witnessIds.length > 0 ? { toad: env.witnessIds.length, wits: env.witnessIds } : {}; const result = await client .identifiers() .create(name, kargs); let op = await result.op(); op = await waitOperation(client, op); // console.log("identifiers.create", op); id = op.response.i; } const eid = client.agent?.pre ?? ''; // considering this used to be a non-null assertion, presumably it will never end up being '' if (!(await hasEndRole(client, name, 'agent', eid))) { const result = await client .identifiers() .addEndRole(name, 'agent', eid); let op = await result.op(); op = await waitOperation(client, op); console.log('identifiers.addEndRole', op); } const oobi = await client.oobis().get(name, 'agent'); const result = [id, oobi.oobis[0]]; console.log(name, result); return result; } export async function getOrIssueCredential(issuerClient, issuerAid, recipientAid, issuerRegistry, credData, schema, rules, source, privacy = false) { const credentialList = await issuerClient.credentials().list(); if (credentialList.length > 0) { const credential = credentialList.find((cred) => cred.sad.s === schema && cred.sad.i === issuerAid.prefix && cred.sad.a.i === recipientAid.prefix && cred.sad.a.AID === credData.AID && cred.status.et != 'rev'); if (credential) return credential; } const issResult = await issuerClient.credentials().issue(issuerAid.name, { ri: issuerRegistry.regk, s: schema, u: privacy ? new SignifyClient.Salter({}).qb64 : undefined, a: { i: recipientAid.prefix, u: privacy ? new SignifyClient.Salter({}).qb64 : undefined, ...credData, }, r: rules, e: source, }); await waitOperation(issuerClient, issResult.op); const credential = await issuerClient.credentials().get(issResult.acdc.ked.d); return credential; } export async function revokeCredential(issuerClient, issuerAid, credentialSaid) { const revResult = await issuerClient .credentials() .revoke(issuerAid.name, credentialSaid); await waitOperation(issuerClient, revResult.op); const credential = await issuerClient.credentials().get(credentialSaid); return credential; } export async function getStates(client, prefixes) { const participantStates = await Promise.all(prefixes.map((p) => client.keyStates().get(p))); return participantStates.map((s) => s[0]); } /** * Test if end role is authorized for a Keri identifier */ export async function hasEndRole(client, alias, role, eid) { const list = await getEndRoles(client, alias, role); for (const i of list) { if (i.role === role && i.eid === eid) { return true; } } return false; } /** * Logs a warning for each un-handled notification. * <p>Replace warnNotifications with assertNotifications when test handles all notifications * @see assertNotifications */ export async function warnNotifications(...clients) { let count = 0; for (const client of clients) { const res = await client.notifications().list(); const notes = res.notes.filter((i) => i.r === false); if (notes.length > 0) { count += notes.length; console.warn('notifications', notes); } } assert(count > 0); } export async function deleteOperations(client, op) { if (op.metadata?.depends) { await deleteOperations(client, op.metadata.depends); } await client.operations().delete(op.name); } export async function getReceivedCredential(client, credId) { const credentialList = await client.credentials().list({ filter: { '-d': credId, }, }); let credential; if (credentialList.length > 0) { assert(credentialList.length === 1); credential = credentialList[0]; } return credential; } /** * Mark and remove notification. */ export async function markAndRemoveNotification(client, note) { try { await client.notifications().mark(note.i); } finally { await client.notifications().delete(note.i); } } /** * Mark notification as read. */ export async function markNotification(client, note) { await client.notifications().mark(note.i); } export async function resolveOobi(client, oobi, alias) { const op = await client.oobis().resolve(oobi, alias); await waitOperation(client, op); } export async function waitForCredential(client, credSAID, MAX_RETRIES = 10) { let retryCount = 0; while (retryCount < MAX_RETRIES) { const cred = await getReceivedCredential(client, credSAID); if (cred) return cred; await new Promise((resolve) => setTimeout(resolve, 1000)); console.log(` retry-${retryCount}: No credentials yet...`); retryCount = retryCount + 1; } throw Error('Credential SAID: ' + credSAID + ' has not been received'); } export async function waitAndMarkNotification(client, route) { const notes = await waitForNotifications(client, route); await Promise.all(notes.map((note) => { client.notifications().mark(note.i); })); return notes[notes.length - 1]?.a.d ?? ''; } export async function waitForNotifications(client, route, options = {}) { return retry(async () => { const response = await client .notifications() .list(); const notes = response.notes.filter((note) => note.a.r === route && note.r === false); if (!notes.length) { throw new Error(`No notifications with route ${route}`); } return notes; }, options); } /** * Poll for operation to become completed. * Removes completed operation */ export async function waitOperation(client, op, signal) { if (typeof op === 'string') { op = await client.operations().get(op); } op = await client .operations() .wait(op, { signal: signal ?? AbortSignal.timeout(60000) }); await deleteOperations(client, op); return op; } export async function getOrCreateRegistry(client, aid, registryName) { let registries = await client.registries().list(aid.name); registries = registries.filter((reg) => reg.name == registryName); if (registries.length > 0) { assert(registries.length === 1); } else { const regResult = await client .registries() .create({ name: aid.name, registryName: registryName }); await waitOperation(client, await regResult.op()); registries = await client.registries().list(aid.name); registries = registries.filter((reg) => reg.name == registryName); } console.log(registries); return registries[0]; } export async function sendGrantMessage(senderClient, senderAid, recipientAid, credential) { const [grant, gsigs, gend] = await senderClient.ipex().grant({ senderName: senderAid.name, acdc: new SignifyClient.Serder(credential.sad), anc: new SignifyClient.Serder(credential.anc), iss: new SignifyClient.Serder(credential.iss), ancAttachment: credential.ancAttachment, recipient: recipientAid.prefix, datetime: createTimestamp(), }); const op = await senderClient .ipex() .submitGrant(senderAid.name, grant, gsigs, gend, [recipientAid.prefix]); await waitOperation(senderClient, op); } export async function sendAdmitMessage(senderClient, senderAid, recipientAid) { const notifications = await waitForNotifications(senderClient, '/exn/ipex/grant'); assert(notifications.length > 0); const grantNotification = notifications[0]; const [admit, sigs, aend] = await senderClient.ipex().admit({ senderName: senderAid.name, message: '', grantSaid: grantNotification.a.d ?? '', // presumably, since this was originally a non-null assertion, it will never be '' recipient: recipientAid.prefix, datetime: createTimestamp(), }); const op = await senderClient .ipex() .submitAdmit(senderAid.name, admit, sigs, aend, [recipientAid.prefix]); await waitOperation(senderClient, op); await markAndRemoveNotification(senderClient, grantNotification); } export async function getRootOfTrust(configJson, rot_aid, rot_member_aid) { const workflow_state = WorkflowState.getInstance(); // Use the rot_member_aid if provided, otherwise fall back to rot_aid const identifierToUse = rot_member_aid || rot_aid; const identifierData = getIdentifierData(configJson, identifierToUse); const client = workflow_state.clients.get(identifierData.agent.name); if (!client) { throw new Error(`Failed to initialize client for identifier: ${identifierToUse}`); } const rootOfTrustIdentifierName = rot_aid; const rootOfTrustAid = await client .identifiers() .get(rootOfTrustIdentifierName); const oobi = await client.oobis().get(rootOfTrustIdentifierName); let oobiUrl = oobi.oobis[0]; console.log(`Root of trust OOBI: ${oobiUrl}`); const url = new URL(oobiUrl); if (url.hostname === 'keria') oobiUrl = oobiUrl.replace('keria', 'localhost'); const oobiResp = await fetch(oobiUrl); const oobiRespBody = await oobiResp.text(); return { vlei: oobiRespBody, aid: rootOfTrustAid.prefix, oobi: oobiUrl, }; }