From ecd30c588ae78b10af1d8e37dfc469c75f51e9f0 Mon Sep 17 00:00:00 2001 From: Josh Mann Date: Mon, 11 Sep 2023 21:58:02 -0600 Subject: [PATCH] #644 Add invitation_accepted event --- packages/pico-engine-core/src/modules/dido.ts | 968 +++++++++++------- .../pico-engine/krl/io.picolabs.did-o.krl | 9 +- 2 files changed, 585 insertions(+), 392 deletions(-) diff --git a/packages/pico-engine-core/src/modules/dido.ts b/packages/pico-engine-core/src/modules/dido.ts index 6b86505e..67c34c4b 100644 --- a/packages/pico-engine-core/src/modules/dido.ts +++ b/packages/pico-engine-core/src/modules/dido.ts @@ -4,76 +4,115 @@ */ import { krl } from "krl-stdlib"; -import { Message, DIDDoc, DIDResolver, Secret, SecretsResolver, VerificationMethod, IMessage, PackEncryptedMetadata, UnpackMetadata, Attachment, IFromPrior, FromPrior } from "didcomm-node"; +import { + Message, + DIDDoc, + DIDResolver, + Secret, + SecretsResolver, + VerificationMethod, + IMessage, + PackEncryptedMetadata, + UnpackMetadata, + Attachment, + IFromPrior, + FromPrior, +} from "didcomm-node"; import * as cuid from "cuid"; -const bs58 = require('bs58'); -const sodium = require('libsodium-wrappers') -const crypto = require('crypto') -const didregex = /^did:peer:(([01](z)([1-9a-km-zA-HJ-NP-Z]{46,47}))|(2((\.[AEVID](z)([1-9a-km-zA-HJ-NP-Z]{46,47}))+(\.(S)[0-9a-zA-Z=]*)?)))$/ +const bs58 = require("bs58"); +const sodium = require("libsodium-wrappers"); +const crypto = require("crypto"); +const didregex = + /^did:peer:(([01](z)([1-9a-km-zA-HJ-NP-Z]{46,47}))|(2((\.[AEVID](z)([1-9a-km-zA-HJ-NP-Z]{46,47}))+(\.(S)[0-9a-zA-Z=]*)?)))$/; const QUERY_TIMEOUT = 10000; // DID Functions //#region DID Management /** - * Generates a new peer method 2 DID and returns the associated DIDDoc object that contains information about the DID, + * Generates a new peer method 2 DID and returns the associated DIDDoc object that contains information about the DID, * including its authentication keys, key agreements, and service endpoints. */ -const generateDID = krl.Function(['isInvite'], async function (isInvite = false): Promise { +const generateDID = krl.Function( + ["isInvite"], + async function (isInvite = false): Promise { // Create channel for new did const conf = { - eventPolicy: { - allow: [ - { domain: "dido", name: "didcommv2_message" }, - { domain: "dido", name: "dido_send_response" } - ], - deny: [] - }, - queryPolicy: { - allow: [{ rid: this.rsCtx.ruleset.rid, name: "*" }], - deny: [] - }, - tags: isInvite ? ["did_invite"] : [] + eventPolicy: { + allow: [ + { domain: "dido", name: "didcommv2_message" }, + { domain: "dido", name: "dido_send_response" }, + ], + deny: [], + }, + queryPolicy: { + allow: [{ rid: this.rsCtx.ruleset.rid, name: "*" }], + deny: [], + }, + tags: isInvite ? ["did_invite"] : [], }; const channel = await this.rsCtx.newChannel(conf); - const endpoint = `${this.module("meta")!["host"](this)}/sky/event/${channel.id}/none/dido/didcommv2_message`; + const endpoint = `${this.module("meta")!["host"](this)}/sky/event/${ + channel.id + }/none/dido/didcommv2_message`; // Create keypairs for new did const encryptionKeyPair = await crypto.generateKeyPairSync("x25519", { - publicKeyEncoding: { - type: 'spki', - format: 'jwk' - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'jwk', - } + publicKeyEncoding: { + type: "spki", + format: "jwk", + }, + privateKeyEncoding: { + type: "pkcs8", + format: "jwk", + }, }); const signingKeyPair = await crypto.generateKeyPairSync("ed25519", { - publicKeyEncoding: { - type: 'spki', - format: 'jwk' - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'jwk', - } + publicKeyEncoding: { + type: "spki", + format: "jwk", + }, + privateKeyEncoding: { + type: "pkcs8", + format: "jwk", + }, }); - const encryptionPublicKeyMultiCodec = new Uint8Array(Buffer.from(sodium.from_base64(encryptionKeyPair.publicKey.x)).length + 2); + const encryptionPublicKeyMultiCodec = new Uint8Array( + Buffer.from(sodium.from_base64(encryptionKeyPair.publicKey.x)).length + 2 + ); encryptionPublicKeyMultiCodec.set([0xec, 0x01]); - encryptionPublicKeyMultiCodec.set(Buffer.from(sodium.from_base64(encryptionKeyPair.publicKey.x)), 2); - const base58EncryptionPublicKey = bs58.encode(Buffer.from(encryptionPublicKeyMultiCodec)); - const signingPublicKeyMultiCodec = new Uint8Array(Buffer.from(sodium.from_base64(signingKeyPair.publicKey.x)).length + 2); + encryptionPublicKeyMultiCodec.set( + Buffer.from(sodium.from_base64(encryptionKeyPair.publicKey.x)), + 2 + ); + const base58EncryptionPublicKey = bs58.encode( + Buffer.from(encryptionPublicKeyMultiCodec) + ); + const signingPublicKeyMultiCodec = new Uint8Array( + Buffer.from(sodium.from_base64(signingKeyPair.publicKey.x)).length + 2 + ); signingPublicKeyMultiCodec.set([0xed, 0x01]); - signingPublicKeyMultiCodec.set(Buffer.from(sodium.from_base64(signingKeyPair.publicKey.x)), 2); - const base58SigningPublicKey = bs58.encode(Buffer.from(signingPublicKeyMultiCodec)); + signingPublicKeyMultiCodec.set( + Buffer.from(sodium.from_base64(signingKeyPair.publicKey.x)), + 2 + ); + const base58SigningPublicKey = bs58.encode( + Buffer.from(signingPublicKeyMultiCodec) + ); // Create Peer Method 2 DID - const service = ".S" + sodium.to_base64(JSON.stringify({ - "t": "dm", - "s": endpoint, - "a": ["didcomm/v2", "didcomm/aip2;env=rfc587"] - }), sodium.base64_variants.URLSAFE).replace(/=/g, ""); + const service = + ".S" + + sodium + .to_base64( + JSON.stringify({ + t: "dm", + s: endpoint, + a: ["didcomm/v2", "didcomm/aip2;env=rfc587"], + }), + sodium.base64_variants.URLSAFE + ) + .replace(/=/g, ""); const encryption = ".Ez" + base58EncryptionPublicKey; const signing = ".Vz" + base58SigningPublicKey; const did = "did:peer:2" + encryption + signing + service; @@ -83,25 +122,23 @@ const generateDID = krl.Function(['isInvite'], async function (isInvite = false) // Construct Secrets const id = did + "#" + base58EncryptionPublicKey; - const authid = did + "#" + base58SigningPublicKey + const authid = did + "#" + base58SigningPublicKey; - const secret: Secret = - { - id: id, - type: "JsonWebKey2020", - privateKeyJwk: encryptionKeyPair.privateKey + const secret: Secret = { + id: id, + type: "JsonWebKey2020", + privateKeyJwk: encryptionKeyPair.privateKey, }; - const authSecret: Secret = - { - id: authid, - type: "JsonWebKey2020", - privateKeyJwk: signingKeyPair.privateKey + const authSecret: Secret = { + id: authid, + type: "JsonWebKey2020", + privateKeyJwk: signingKeyPair.privateKey, }; // Store Secrets let secrets = await this.rsCtx.getEnt("didSecrets"); if (!secrets) { - secrets = {}; + secrets = {}; } secrets[id] = secret; secrets[authid] = authSecret; @@ -111,128 +148,156 @@ const generateDID = krl.Function(['isInvite'], async function (isInvite = false) await addLabelsToChannel(this, [channel.id, did]); return doc; -}); + } +); /** * Deletes the DIDDoc object associated with the specified DID from the didDocs entity. */ const deleteDID = krl.Function(["did"], async function (did: string) { - const docs = await this.rsCtx.getEnt("didDocs"); - if (docs) { - delete docs[did]; - await this.rsCtx.putEnt("didDocs", docs); - return true; - } - return false; + const docs = await this.rsCtx.getEnt("didDocs"); + if (docs) { + delete docs[did]; + await this.rsCtx.putEnt("didDocs", docs); + return true; + } + return false; }); /** - * Updates the DIDDoc object associated with the specified DID in the didDocs entity. + * Updates the DIDDoc object associated with the specified DID in the didDocs entity. * The function takes two parameters: the DID to be updated and the new DIDDoc object. */ -const updateDID = krl.Function(["did", "newDoc"], async function (did: string, newDoc: DIDDoc) { +const updateDID = krl.Function( + ["did", "newDoc"], + async function (did: string, newDoc: DIDDoc) { const docs = await this.rsCtx.getEnt("didDocs"); if (docs) { - docs[did] = newDoc; - return true; + docs[did] = newDoc; + return true; } return false; -}); + } +); /** - * Creates a new DID to be rotated into use in place of old_did according to the DIDComm v2 spec. - * Pending rotations in the pendingRotations entity are removed when a message is received using the new DID. + * Creates a new DID to be rotated into use in place of old_did according to the DIDComm v2 spec. + * Pending rotations in the pendingRotations entity are removed when a message is received using the new DID. */ -const rotateDID = krl.Function(['old_did'], async function (old_did: string): Promise { +const rotateDID = krl.Function( + ["old_did"], + async function (old_did: string): Promise { const new_doc: DIDDoc = await generateDID(this, []); await updateDidMap(this, [old_did, new_doc["id"]]); let pendingRotations = await this.rsCtx.getEnt("pendingRotations"); if (!pendingRotations) { - pendingRotations = {}; + pendingRotations = {}; } pendingRotations[new_doc["id"]] = { - iss: old_did, - sub: new_doc["id"], - iat: Math.floor(Date.now() / 1000) + iss: old_did, + sub: new_doc["id"], + iat: Math.floor(Date.now() / 1000), }; await this.rsCtx.putEnt("pendingRotations", pendingRotations); return new_doc; -}); + } +); /** * Manages DID rotation and storage for accepting invitations via the Trust Ping Protocol and from_prior DID Rotation. */ -const rotateInviteDID = krl.Function(['my_did', 'their_did'], async function (my_did: string, their_did: string) { +const rotateInviteDID = krl.Function( + ["my_did", "their_did"], + async function (my_did: string, their_did: string) { if (this.getEvent() && this.getEvent()?.eci) { - const channels = this.rsCtx.pico().channels.filter(c => c.id === this.getEvent()?.eci); - if (channels.length > 0) { - const channel = channels[0]; - if (channel.tags.includes("did_invite")) { - const pendingRotations = await this.rsCtx.getEnt("pendingRotations"); - let rotationExists = false; - for (const r in pendingRotations) { - if (pendingRotations[r]["iss"] === my_did) rotationExists = true; - } - if (!rotationExists) { - await storeDidDoc(this, [their_did]); - await mapDid(this, [their_did, my_did]); - await rotateDID(this, [my_did]); - } - } + const channels = this.rsCtx + .pico() + .channels.filter((c) => c.id === this.getEvent()?.eci); + if (channels.length > 0) { + const channel = channels[0]; + if (channel.tags.includes("did_invite")) { + const pendingRotations = await this.rsCtx.getEnt("pendingRotations"); + let rotationExists = false; + for (const r in pendingRotations) { + if (pendingRotations[r]["iss"] === my_did) rotationExists = true; + } + if (!rotationExists) { + await storeDidDoc(this, [their_did]); + await mapDid(this, [their_did, my_did]); + const new_doc = await rotateDID(this, [my_did]); + const invites = await this.rsCtx.getEnt("invites"); + this.rsCtx.raiseEvent("dido", "invitation_accepted", { + role: "invitee", + label: invites[my_did]["label"], + my_did: new_doc["id"], + their_did: their_did, + created_time: invites[my_did]["created_time"], + }); + // Future Task: If invite is reusable, don't delete + delete invites[my_did]; + await this.rsCtx.putEnt("invites", invites); + } } + } } -}); + } +); /** * Deletes all entries from the pendingRotations entity. Pending rotations are created by rotating a DID. */ const clearPendingRotations = krl.Function([], function () { - this.rsCtx.putEnt("pendingRotations", {}); + this.rsCtx.putEnt("pendingRotations", {}); }); /** * Adds an entry to the didMap entity in the following form: - * { their_did: my_did } + * { their_did: my_did } */ -const mapDid = krl.Function(['their_did', 'my_did'], async function (their_did: string, my_did: string) { +const mapDid = krl.Function( + ["their_did", "my_did"], + async function (their_did: string, my_did: string) { let didMap = await this.rsCtx.getEnt("didMap"); if (!didMap) { - didMap = {}; + didMap = {}; } didMap[their_did] = my_did; await this.rsCtx.putEnt("didMap", didMap); -}); + } +); /** * Replaces all occurrences of old_did with new_did in the didMap entity. */ -const updateDidMap = krl.Function(['old_did', 'new_did'], async function (old_did: string, new_did: string) { +const updateDidMap = krl.Function( + ["old_did", "new_did"], + async function (old_did: string, new_did: string) { const map = await this.rsCtx.getEnt("didMap"); for (const key in map) { - if (key === old_did) { - map[new_did] = map[old_did]; - delete map[old_did]; - } - else if (map[key] === old_did) { - map[key] = new_did; - } + if (key === old_did) { + map[new_did] = map[old_did]; + delete map[old_did]; + } else if (map[key] === old_did) { + map[key] = new_did; + } } await this.rsCtx.putEnt("didMap", map); -}); + } +); /** * Deletes all entries from the didMap entity. */ const clearDidMap = krl.Function([], async function () { - await this.rsCtx.putEnt("didMap", {}); -}) + await this.rsCtx.putEnt("didMap", {}); +}); /** * Deletes all entries from the didDocs entity. */ const clearDidDocs = krl.Function([], async function () { - await this.rsCtx.putEnt("didDocs", {}); -}) + await this.rsCtx.putEnt("didDocs", {}); +}); /** * Returns a JSON Web Key (JWK) from the given multibase encoded string and curve. @@ -241,40 +306,45 @@ const clearDidDocs = krl.Function([], async function () { * @returns JWK of given multibase and curve */ const JWKFromMultibase = function (multibase: string, crv: string) { - const multi_decoded = bs58.decode(multibase); - const key = sodium.to_base64(Buffer.from(multi_decoded.slice(2)), sodium.base64_variants.URLSAFE).replace("=", ""); - return { crv: crv, x: key, kty: "OKP" } -} + const multi_decoded = bs58.decode(multibase); + const key = sodium + .to_base64( + Buffer.from(multi_decoded.slice(2)), + sodium.base64_variants.URLSAFE + ) + .replace("=", ""); + return { crv: crv, x: key, kty: "OKP" }; +}; /** - * If input is of type DIDDoc it is stored in the didDocs entity variable, - * else the input is attempted to be parsed as a peer method 2 DID into a - * DIDDoc and is stored the same way. If parsing fails, an error is logged. + * If input is of type DIDDoc it is stored in the didDocs entity variable, + * else the input is attempted to be parsed as a peer method 2 DID into a + * DIDDoc and is stored the same way. If parsing fails, an error is logged. * On successful storage, the stored DIDDoc is returned. */ -const storeDidDoc = krl.Function(['input'], async function (input: any) { - let diddoc: DIDDoc; - if (typeof input === 'object' && typeof input !== 'string') { - diddoc = input; - } else { - try { - diddoc = resolvePeer2Did(input); - } catch (e: any) { - this.log.error(e, input); - return; - } - } - let docs = await this.rsCtx.getEnt("didDocs"); - - if (docs) { - docs[diddoc["id"]] = diddoc; - } else { - docs = {}; - docs[diddoc["id"]] = diddoc; +const storeDidDoc = krl.Function(["input"], async function (input: any) { + let diddoc: DIDDoc; + if (typeof input === "object" && typeof input !== "string") { + diddoc = input; + } else { + try { + diddoc = resolvePeer2Did(input); + } catch (e: any) { + this.log.error(e, input); + return; } - - await this.rsCtx.putEnt("didDocs", docs); - return diddoc; + } + let docs = await this.rsCtx.getEnt("didDocs"); + + if (docs) { + docs[diddoc["id"]] = diddoc; + } else { + docs = {}; + docs[diddoc["id"]] = diddoc; + } + + await this.rsCtx.putEnt("didDocs", docs); + return diddoc; }); /** @@ -283,53 +353,55 @@ const storeDidDoc = krl.Function(['input'], async function (input: any) { * @returns DIDDoc representing provided did */ const resolvePeer2Did = function (did: string): DIDDoc { - if (didregex.test(did)) { - const parts = (did as string).split('.'); - const keyAgs: string[] = []; - const auths: string[] = []; - const verMeths: VerificationMethod[] = []; - const services: any[] = []; - parts.forEach(part => { - if (part[0] === 'E') { - keyAgs.push(did + "#" + part.substring(2)); - verMeths.push({ - id: did + "#" + part.substring(2), - type: "JsonWebKey2020", - controller: did, - publicKeyJwk: JWKFromMultibase(part.substring(2), "X25519") - }); - } else if (part[0] === 'V') { - auths.push(did + "#" + part.substring(2)); - verMeths.push({ - id: did + "#" + part.substring(2), - type: "JsonWebKey2020", - controller: did, - publicKeyJwk: JWKFromMultibase(part.substring(2), "Ed25519") - }); - } else if (part[0] === 'S') { - const service = JSON.parse(Buffer.from(part.substring(1), "base64").toString("utf8")); - services.push({ - id: did + "#didcommmessaging-0", - type: "DIDCommMessaging", - serviceEndpoint: { - uri: service["s"], - routingKeys: service["r"], - accept: service["a"] - } - }); - } + if (didregex.test(did)) { + const parts = (did as string).split("."); + const keyAgs: string[] = []; + const auths: string[] = []; + const verMeths: VerificationMethod[] = []; + const services: any[] = []; + parts.forEach((part) => { + if (part[0] === "E") { + keyAgs.push(did + "#" + part.substring(2)); + verMeths.push({ + id: did + "#" + part.substring(2), + type: "JsonWebKey2020", + controller: did, + publicKeyJwk: JWKFromMultibase(part.substring(2), "X25519"), + }); + } else if (part[0] === "V") { + auths.push(did + "#" + part.substring(2)); + verMeths.push({ + id: did + "#" + part.substring(2), + type: "JsonWebKey2020", + controller: did, + publicKeyJwk: JWKFromMultibase(part.substring(2), "Ed25519"), }); + } else if (part[0] === "S") { + const service = JSON.parse( + Buffer.from(part.substring(1), "base64").toString("utf8") + ); + services.push({ + id: did + "#didcommmessaging-0", + type: "DIDCommMessaging", + serviceEndpoint: { + uri: service["s"], + routingKeys: service["r"], + accept: service["a"], + }, + }); + } + }); - return { - id: did, - keyAgreement: keyAgs, - authentication: auths, - verificationMethod: verMeths, - service: services - } - } else { - throw "Unable to parse DIDDoc"; - } + return { + id: did, + keyAgreement: keyAgs, + authentication: auths, + verificationMethod: verMeths, + service: services, + }; + } else { + throw "Unable to parse DIDDoc"; + } }; //#endregion DID Management @@ -340,204 +412,276 @@ const resolvePeer2Did = function (did: string): DIDDoc { * This class implements the DIDResolver interface, provided by the didcomm-node package, used for resolving DIDs for packing and unpacking messages. */ class PicoDIDResolver implements DIDResolver { - knownDids: any - constructor(knownDids: any) { - this.knownDids = knownDids; - } - async resolve(did: string): Promise { - const doc = this.knownDids[did] || null; - if (doc === null) { - try { - return resolvePeer2Did(did); - } catch (e: any) { - return null; - } - } else { - return doc; - } + knownDids: any; + constructor(knownDids: any) { + this.knownDids = knownDids; + } + async resolve(did: string): Promise { + const doc = this.knownDids[did] || null; + if (doc === null) { + try { + return resolvePeer2Did(did); + } catch (e: any) { + return null; + } + } else { + return doc; } + } } /** * This class implements the SecretsResolver interface, provided by the didcomm-node package, used for retrieving secrets for packing and unpacking messages. */ class PicoSecretsResolver implements SecretsResolver { - knownSecrets: any - constructor(secrets: any) { - this.knownSecrets = secrets; - } - async get_secret(secret_id: string): Promise { - return this.knownSecrets[secret_id] || null; - } - async find_secrets(secret_ids: string[]): Promise { - return secret_ids.filter((id => this.knownSecrets[id])); - } - + knownSecrets: any; + constructor(secrets: any) { + this.knownSecrets = secrets; + } + async get_secret(secret_id: string): Promise { + return this.knownSecrets[secret_id] || null; + } + async find_secrets(secret_ids: string[]): Promise { + return secret_ids.filter((id) => this.knownSecrets[id]); + } } /** - * Attempts to unpack (decrypt) an encrypted JSON Web Message (JWM) using the stored secrets. + * Attempts to unpack (decrypt) an encrypted JSON Web Message (JWM) using the stored secrets. * Returns a tuple of the unpacked message and metadata ([Message, UnpackMetadata]). Refer to didcomm-node package for details. */ -const unpack = krl.Function(['message'], async function (message: any) { - try { - const result: [Message, UnpackMetadata] = await Message.unpack(JSON.stringify(message), new PicoDIDResolver(await this.rsCtx.getEnt("didDocs")), new PicoSecretsResolver(await this.rsCtx.getEnt("didSecrets")), {}) as [Message, UnpackMetadata]; - return result; - } catch (error) { - this.log.error("There was an error unpacking a message: ", { message: message, error: error }); - return null; - } +const unpack = krl.Function(["message"], async function (message: any) { + try { + const result: [Message, UnpackMetadata] = (await Message.unpack( + JSON.stringify(message), + new PicoDIDResolver(await this.rsCtx.getEnt("didDocs")), + new PicoSecretsResolver(await this.rsCtx.getEnt("didSecrets")), + {} + )) as [Message, UnpackMetadata]; + return result; + } catch (error) { + this.log.error("There was an error unpacking a message: ", { + message: message, + error: error, + }); + return null; + } }); /** * Attempts to pack (encrypt) a message from the sender’s DID to the receiver's DID and returns the encrypted message. */ -const pack = krl.Function(['message', '_from', 'to'], async function (message: IMessage, _from: string, to: string) { +const pack = krl.Function( + ["message", "_from", "to"], + async function (message: IMessage, _from: string, to: string) { try { - const _message: Message = new Message(message) - const [enc_msg, packed_meta]: [string, PackEncryptedMetadata] = await _message.pack_encrypted(to, _from, _from, new PicoDIDResolver(await this.rsCtx.getEnt("didDocs")), new PicoSecretsResolver(await this.rsCtx.getEnt("didSecrets")), { forward: false }) as [string, PackEncryptedMetadata]; - return JSON.parse(enc_msg); + const _message: Message = new Message(message); + const [enc_msg, packed_meta]: [string, PackEncryptedMetadata] = + (await _message.pack_encrypted( + to, + _from, + _from, + new PicoDIDResolver(await this.rsCtx.getEnt("didDocs")), + new PicoSecretsResolver(await this.rsCtx.getEnt("didSecrets")), + { forward: false } + )) as [string, PackEncryptedMetadata]; + return JSON.parse(enc_msg); } catch (error) { - this.log.error("There was an error packing a message: ", { message: message, from: _from, to: to, error: error }); - return null; + this.log.error("There was an error packing a message: ", { + message: message, + from: _from, + to: to, + error: error, + }); + return null; } -}); + } +); /** - * Add a route to the routes entity that is used for routing incoming DIDComm v2 messages. - * type is the type of the DIDComm v2 message. domain is the domain of the event that is - * raised when a message of type type is received. rule is the rule name of the event that + * Add a route to the routes entity that is used for routing incoming DIDComm v2 messages. + * type is the type of the DIDComm v2 message. domain is the domain of the event that is + * raised when a message of type type is received. rule is the rule name of the event that * is raised when a message of type type is received. */ -const addRoute = krl.Function(['type', 'domain', 'rule'], async function (type: string, domain: string, rule: string) { +const addRoute = krl.Function( + ["type", "domain", "rule"], + async function (type: string, domain: string, rule: string) { let routes = await this.rsCtx.getEnt("routes"); if (!routes) { - routes = {}; + routes = {}; } routes[type] = { domain: domain, name: rule }; await this.rsCtx.putEnt("routes", routes); return true; -}); + } +); /** - * Unpacks the incoming message and raises the corresponding event according to the type of + * Unpacks the incoming message and raises the corresponding event according to the type of * the message as defined in the routes entity variable. */ -const route = krl.Function(['message'], async function (message: string) { - const unpack_result: IMessage = await unpack(this, [message]); - const unpacked = unpack_result[0].as_value(); - const unpack_meta = unpack_result[1]; - // Rotate incoming DID if from_prior included - if (unpack_meta.from_prior) { - const from_prior = unpack_meta.from_prior as IFromPrior; - await deleteDID(this, [from_prior.iss]); - await storeDidDoc(this, [from_prior.sub]); - await updateDidMap(this, [from_prior.iss, from_prior.sub]); - } - // Delete pending rotation if message received using new DID - if (unpacked.to) { - const pendingRotations = await this.rsCtx.getEnt("pendingRotations"); - if (pendingRotations && Object.keys(pendingRotations).length > 0) { - let hasChanged = false; - unpacked.to.forEach((to: string) => { - if (pendingRotations[to]) { - deleteDID(this, [pendingRotations[to]["iss"]]); - deleteDIDChannel(this, [pendingRotations[to]["iss"]]); - delete pendingRotations[to]; - hasChanged = true; - } - }); - if (hasChanged) { - await this.rsCtx.putEnt("pendingRotations", pendingRotations); - } +const route = krl.Function(["message"], async function (message: string) { + const unpack_result: IMessage = await unpack(this, [message]); + const unpacked = unpack_result[0].as_value(); + const unpack_meta = unpack_result[1]; + // Rotate incoming DID if from_prior included + if (unpack_meta.from_prior) { + const from_prior = unpack_meta.from_prior as IFromPrior; + await deleteDID(this, [from_prior.iss]); + await storeDidDoc(this, [from_prior.sub]); + await updateDidMap(this, [from_prior.iss, from_prior.sub]); + } + // Delete pending rotation if message received using new DID + if (unpacked.to) { + const pendingRotations = await this.rsCtx.getEnt("pendingRotations"); + if (pendingRotations && Object.keys(pendingRotations).length > 0) { + let hasChanged = false; + unpacked.to.forEach((to: string) => { + if (pendingRotations[to]) { + // Future Task: If invite is reusable, don't delete + deleteDID(this, [pendingRotations[to]["iss"]]); + deleteDIDChannel(this, [pendingRotations[to]["iss"]]); + delete pendingRotations[to]; + hasChanged = true; } + }); + if (hasChanged) { + await this.rsCtx.putEnt("pendingRotations", pendingRotations); + } } - const routes = await this.rsCtx.getEnt("routes"); - try { - if (unpacked.type != null) { - // Route Events and Queries - if (unpacked["type"] === "https://picolabs.io/event/1.0/event") { - this.rsCtx.raiseEvent(unpacked["body"]["domain"], unpacked["body"]["name"], unpacked["body"]["attrs"]); - } else if (unpacked["type"] === "https://picolabs.io/query/1.0/query") { - try { - await this.useModule(unpacked["body"]["rid"]); - return await this.krl.assertFunction(this.module(unpacked["body"]["rid"])![unpacked["body"]["name"]])(this, unpacked["body"]["args"]); - } catch (error) { - return "Unable to query: " + error; - } - // Route DIDComm messages - } else { - this.rsCtx.raiseEvent(routes[unpacked.type]["domain"], routes[unpacked.type]["name"], { "message": unpacked, "metadata": unpack_meta }) - } - } else { - this.log.error("Unknown route for message: ", { message: unpacked, metadata: unpack_meta }) + } + const routes = await this.rsCtx.getEnt("routes"); + try { + if (unpacked.type != null) { + // Route Events and Queries + if (unpacked["type"] === "https://picolabs.io/event/1.0/event") { + this.rsCtx.raiseEvent( + unpacked["body"]["domain"], + unpacked["body"]["name"], + unpacked["body"]["attrs"] + ); + } else if (unpacked["type"] === "https://picolabs.io/query/1.0/query") { + try { + await this.useModule(unpacked["body"]["rid"]); + return await this.krl.assertFunction( + this.module(unpacked["body"]["rid"])![unpacked["body"]["name"]] + )(this, unpacked["body"]["args"]); + } catch (error) { + return "Unable to query: " + error; } - } catch (error) { - this.log.error("Error handling route for message: ", { message: unpacked, metadata: unpack_meta, error: error }) + // Route DIDComm messages + } else { + this.rsCtx.raiseEvent( + routes[unpacked.type]["domain"], + routes[unpacked.type]["name"], + { message: unpacked, metadata: unpack_meta } + ); + } + } else { + this.log.error("Unknown route for message: ", { + message: unpacked, + metadata: unpack_meta, + }); } + } catch (error) { + this.log.error("Error handling route for message: ", { + message: unpacked, + metadata: unpack_meta, + error: error, + }); + } }); /** - * Packs the given message and sends it to the endpoint of the given did. - * This function utilizes the autosend feature of the http library. To subscribe + * Packs the given message and sends it to the endpoint of the given did. + * This function utilizes the autosend feature of the http library. To subscribe * to these responses, create a rule that selects on dido dido_send_response. */ -const send = krl.Function(['did', 'message'], async function (did: string, message: any) { +const send = krl.Function( + ["did", "message"], + async function (did: string, message: any) { try { - const docs = await this.rsCtx.getEnt("didDocs"); - const endpoint = docs[did]["service"][0]["serviceEndpoint"]["uri"]; - const _from: string = message["from"]; - this.log.debug("MESSAGE: ", message); - const packed_message = await pack(this, [message, _from, did]); - const eci = this.rsCtx.pico().channels.filter(c => c.tags.indexOf(normalizeDID(_from)) >= 0)[0]["id"]; - await this.krl.assertAction(this.module("http")!["post"])(this, { - "url": endpoint, - "json": packed_message, - "autosend": { - "eci": eci, - "domain": "dido", - "type": "dido_send_response", - "name": "dido_send_response" - } - }); + const docs = await this.rsCtx.getEnt("didDocs"); + const endpoint = docs[did]["service"][0]["serviceEndpoint"]["uri"]; + const _from: string = message["from"]; + this.log.debug("MESSAGE: ", message); + const packed_message = await pack(this, [message, _from, did]); + const eci = this.rsCtx + .pico() + .channels.filter((c) => c.tags.indexOf(normalizeDID(_from)) >= 0)[0][ + "id" + ]; + await this.krl.assertAction(this.module("http")!["post"])(this, { + url: endpoint, + json: packed_message, + autosend: { + eci: eci, + domain: "dido", + type: "dido_send_response", + name: "dido_send_response", + }, + }); } catch (error) { - this.log.error("Error sending did message: ", { error: error, did: did, message: message }) + this.log.error("Error sending did message: ", { + error: error, + did: did, + message: message, + }); } -}); + } +); /** * This function is used by the wrangler:picoQuery function to send a query using a did. */ -const prepareQuery = krl.Function(['did', 'query'], async function (did: string, query: any) { +const prepareQuery = krl.Function( + ["did", "query"], + async function (did: string, query: any) { await this.useModule("io.picolabs.did-o", "didx"); - return await this.krl.assertFunction(this.module("didx")!["sendQuery"])(this, [did, query]); -}) + return await this.krl.assertFunction(this.module("didx")!["sendQuery"])( + this, + [did, query] + ); + } +); /** * Very similar to dido:send, except that it waits for and returns the http response. */ -const sendQuery = krl.Function(['did', 'message'], async function (did: string, message: any) { +const sendQuery = krl.Function( + ["did", "message"], + async function (did: string, message: any) { try { - const docs: any = await this.rsCtx.getEnt("didDocs"); - const endpoint: string = docs[did]["service"][0]["serviceEndpoint"]["uri"]; - const _from: string = message["from"]; - const packed_message = await pack(this, [message, _from, did]); - const response = await Promise.race([this.krl.assertAction(this.module("http")!["post"])(this, { - "url": endpoint, - "json": packed_message, - }), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), QUERY_TIMEOUT)) - ]).catch(function (err) { - return "Query timed out"; - }); - if (response.status_code === 200) { - return JSON.parse(response.content).directives[0].options - } - return response; + const docs: any = await this.rsCtx.getEnt("didDocs"); + const endpoint: string = + docs[did]["service"][0]["serviceEndpoint"]["uri"]; + const _from: string = message["from"]; + const packed_message = await pack(this, [message, _from, did]); + const response = await Promise.race([ + this.krl.assertAction(this.module("http")!["post"])(this, { + url: endpoint, + json: packed_message, + }), + new Promise((_, reject) => + setTimeout(() => reject(new Error("timeout")), QUERY_TIMEOUT) + ), + ]).catch(function (err) { + return "Query timed out"; + }); + if (response.status_code === 200) { + return JSON.parse(response.content).directives[0].options; + } + return response; } catch (error) { - this.log.error("Error sending did query: ", { error: error, did: did, message: message }) + this.log.error("Error sending did query: ", { + error: error, + did: did, + message: message, + }); } - -}); + } +); /** * Generates and returns a DIDComm v2 message with the given messageOptions. @@ -552,44 +696,80 @@ const sendQuery = krl.Function(['did', 'message'], async function (did: string, * expires_time?: number, * attachments?: Array\ * } - * + * * Refer to didcomm-node package for Attachment object specifics. */ -const generateMessage = krl.Function(['messageOptions'], async function (messageOptions: { type: string, body: any, from?: string, to?: Array, thid?: string, pthid?: string, expires_time?: number, attachments?: Array }): Promise { +const generateMessage = krl.Function( + ["messageOptions"], + async function (messageOptions: { + type: string; + body: any; + from?: string; + to?: Array; + thid?: string; + pthid?: string; + expires_time?: number; + attachments?: Array; + }): Promise { const message: IMessage = { - id: cuid(), - typ: "application/didcomm-plain+json", - type: messageOptions.type, - body: messageOptions.body, - from: messageOptions.from, - to: messageOptions.to, - thid: messageOptions.thid, - pthid: messageOptions.pthid, - created_time: Math.floor(Date.now() / 1000), - attachments: messageOptions.attachments - } + id: cuid(), + typ: "application/didcomm-plain+json", + type: messageOptions.type, + body: messageOptions.body, + from: messageOptions.from, + to: messageOptions.to, + thid: messageOptions.thid, + pthid: messageOptions.pthid, + created_time: Math.floor(Date.now() / 1000), + attachments: messageOptions.attachments, + }; if (messageOptions.expires_time) { - message.expires_time = Math.floor(Date.now() / 1000) + messageOptions.expires_time; + message.expires_time = + Math.floor(Date.now() / 1000) + messageOptions.expires_time; } if (messageOptions.from) { - try { - message.from_prior = (await new FromPrior((await this.rsCtx.getEnt("pendingRotations"))[messageOptions.from] as IFromPrior).pack(null, new PicoDIDResolver(await this.rsCtx.getEnt("didDocs")), new PicoSecretsResolver(await this.rsCtx.getEnt("didSecrets"))))[0] ?? undefined; - } catch (e) { - // Drop if there is no pending rotation - } + try { + message.from_prior = + ( + await new FromPrior( + ( + await this.rsCtx.getEnt("pendingRotations") + )[messageOptions.from] as IFromPrior + ).pack( + null, + new PicoDIDResolver(await this.rsCtx.getEnt("didDocs")), + new PicoSecretsResolver(await this.rsCtx.getEnt("didSecrets")) + ) + )[0] ?? undefined; + } catch (e) { + // Drop if there is no pending rotation + } + } + if ( + messageOptions.type === + "https://didcomm.org/out-of-band/2.0/invitation" && + messageOptions.from + ) { + let invites = await this.rsCtx.getEnt("invites"); + if (!invites) { + invites = {}; + } + invites[messageOptions.from] = message; + await this.rsCtx.putEnt("invites", invites); } return message; -}); + } +); /** * Creates and returns the invitation URL for the given base64 invite. */ -const createInviteUrl = krl.Function(['base64'], function (base64: string) { - const host: string = this.module("meta")!["host"](this) - if (host.indexOf("localhost") >= 0) { - return "http://example.com/invite?_oob=" + base64; - } - return host + "/invite?_oob=" + base64; +const createInviteUrl = krl.Function(["base64"], function (base64: string) { + const host: string = this.module("meta")!["host"](this); + if (host.indexOf("localhost") >= 0) { + return "http://example.com/invite?_oob=" + base64; + } + return host + "/invite?_oob=" + base64; }); //#endregion DIDComm @@ -599,20 +779,26 @@ const createInviteUrl = krl.Function(['base64'], function (base64: string) { /** * Concatenates lables to the labels of the channel with the given eci. */ -const addLabelsToChannel = krl.Function(['eci', 'labels'], async function (eci: string, labels: any) { - const channel = this.rsCtx.pico().channels.filter(c => c.id == eci); +const addLabelsToChannel = krl.Function( + ["eci", "labels"], + async function (eci: string, labels: any) { + const channel = this.rsCtx.pico().channels.filter((c) => c.id == eci); const tags = channel[0].tags.concat(labels); - const eventPolicy = channel[0]["eventPolicy"] - const queryPolicy = channel[0]["queryPolicy"] + const eventPolicy = channel[0]["eventPolicy"]; + const queryPolicy = channel[0]["queryPolicy"]; const conf = { tags, eventPolicy, queryPolicy }; await this.rsCtx.putChannel(eci, conf); -}); + } +); /** * Deletes any channel associated with the given did. */ -const deleteDIDChannel = krl.Function(['did'], async function (did: string) { - await this.rsCtx.pico().channels.filter(c => c.tags.indexOf(normalizeDID(did)) >= 0).forEach(async c => await this.rsCtx.delChannel(c.id)); +const deleteDIDChannel = krl.Function(["did"], async function (did: string) { + await this.rsCtx + .pico() + .channels.filter((c) => c.tags.indexOf(normalizeDID(did)) >= 0) + .forEach(async (c) => await this.rsCtx.delChannel(c.id)); }); /** @@ -621,40 +807,40 @@ const deleteDIDChannel = krl.Function(['did'], async function (did: string) { * @returns normalized did */ const normalizeDID = function (did: string) { - return did.replace(/[:.]/g, "-").toLowerCase(); -} + return did.replace(/[:.]/g, "-").toLowerCase(); +}; //#endregion Misc Functions const dido: krl.Module = { - generateDID: generateDID, - deleteDID: deleteDID, - updateDID: updateDID, - rotateDID: rotateDID, - rotateInviteDID: rotateInviteDID, - clearPendingRotations: clearPendingRotations, - mapDid: mapDid, - // updateDidMap: updateDidMap, // Does not need to be exported - clearDidMap: clearDidMap, - clearDidDocs: clearDidDocs, - // JWKFromMultibase: JWKFromMultibase, // Private non-KRL function. Cannot be exported - storeDidDoc: storeDidDoc, - // resolvePeer2Did: resolvePeer2Did, // Private non-KRL function. Cannot be exported - unpack: unpack, - pack: pack, - addRoute: addRoute, - route: route, - send: send, - prepareQuery: prepareQuery, - sendQuery: sendQuery, - generateMessage: generateMessage, - createInviteUrl: createInviteUrl, - addLabelsToChannel: addLabelsToChannel, - // deleteDIDChannel: deleteDIDChannel, // Does not need to be exported - // nomalizeDID: normalizeDID, // Private non-KRL function. Cannot be exported -} + generateDID: generateDID, + deleteDID: deleteDID, + updateDID: updateDID, + rotateDID: rotateDID, + rotateInviteDID: rotateInviteDID, + clearPendingRotations: clearPendingRotations, + mapDid: mapDid, + // updateDidMap: updateDidMap, // Does not need to be exported + clearDidMap: clearDidMap, + clearDidDocs: clearDidDocs, + // JWKFromMultibase: JWKFromMultibase, // Private non-KRL function. Cannot be exported + storeDidDoc: storeDidDoc, + // resolvePeer2Did: resolvePeer2Did, // Private non-KRL function. Cannot be exported + unpack: unpack, + pack: pack, + addRoute: addRoute, + route: route, + send: send, + prepareQuery: prepareQuery, + sendQuery: sendQuery, + generateMessage: generateMessage, + createInviteUrl: createInviteUrl, + addLabelsToChannel: addLabelsToChannel, + // deleteDIDChannel: deleteDIDChannel, // Does not need to be exported + // nomalizeDID: normalizeDID, // Private non-KRL function. Cannot be exported +}; export default dido; /** * DIDO V 1.0.0 - */ \ No newline at end of file + */ diff --git a/packages/pico-engine/krl/io.picolabs.did-o.krl b/packages/pico-engine/krl/io.picolabs.did-o.krl index 69ad85df..f77d9755 100644 --- a/packages/pico-engine/krl/io.picolabs.did-o.krl +++ b/packages/pico-engine/krl/io.picolabs.did-o.krl @@ -33,7 +33,7 @@ DID-O V 1.0.0 global { - ///////////////////////////////////////////// ROUTING ////////////////////////////////////////////// + ///////////////////////////////////////////// ROUTING ////////////////////////////////////////////// /** * Adds a route to the routes stored in the dido.ts library. The type should be a valid Protocol URI. @@ -251,6 +251,13 @@ DID-O V 1.0.0 } fired { raise dido event "send_trust_ping" attributes event:attrs.put("did", invite{"from"}) + raise dido event "invitation_accepted" attributes { + "role": "invitee", + "label": invite{"body"}{"label"}, + "my_did": new_did{"id"}, + "their_did": invite{"from"}, + "created_time": invite{"created_time"} + } } }