Skip to content

Commit

Permalink
feat: Connectionless Offer (#293)
Browse files Browse the repository at this point in the history
Signed-off-by: Curtish <ch@curtish.me>
  • Loading branch information
curtis-h authored Sep 23, 2024
1 parent 295e14f commit 97e05e7
Show file tree
Hide file tree
Showing 16 changed files with 544 additions and 266 deletions.
18 changes: 5 additions & 13 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,9 @@
"name": "TESTS",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/node_modules/jest/bin/jest.js",
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
"args": [
"--colors",
"--workerThreads",
"--detectOpenHandles",
"--maxWorkers",
"1",
"run",
"./tests"
],
"skipFiles": [
Expand All @@ -48,14 +44,10 @@
"name": "TEST:file",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/node_modules/jest/bin/jest.js",
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
"args": [
"--colors",
"--workerThreads",
"--detectOpenHandles",
"--maxWorkers",
"1",
"${fileBasenameNoExtension}",
"run",
"${relativeFile}"
],
"skipFiles": [
"${workspaceRoot}/../../node_modules/**/*",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"build:dev": "npm run externals:check && rm -rf build && npm run build:agnostic && npm run types",
"build:agnostic": "node esbuild.config.mjs",
"types": "rm -rf build/**/*.d.ts && tsc",
"test": "npx vitest --config vitest.config.mjs --run tests",
"test": "npx vitest --config vitest.config.mjs --run",
"coverage": "npm run test -- --coverage",
"lint": "npx eslint .",
"docs": "npx typedoc --options typedoc.js",
Expand Down Expand Up @@ -171,4 +171,4 @@
"overrides": {
"ws": "^8.17.1"
}
}
}
83 changes: 22 additions & 61 deletions src/edge-agent/didcomm/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,17 @@ import { HandleIssueCredential } from "./HandleIssueCredential";
import { HandleOfferCredential } from "./HandleOfferCredential";
import { HandlePresentation } from "./HandlePresentation";
import { CreatePresentation } from "./CreatePresentation";
import { ProtocolType, findProtocolTypeByValue } from "../protocols/ProtocolTypes";
import { DIDCommConnectionRunner } from "../protocols/connection/DIDCommConnectionRunner";
import { DIDCommInvitationRunner } from "../protocols/invitation/v2/DIDCommInvitationRunner";
import { ProtocolType } from "../protocols/ProtocolTypes";
import Pollux from "../../pollux";
import Apollo from "../../apollo";
import Castor from "../../castor";
import * as DIDfns from "../didFunctions";
import { Task } from "../../utils/tasks";
import { DIDCommContext } from "./Context";
import { FetchApi } from "../helpers/FetchApi";
import { isNil } from "../../utils";
import { ParsePrismInvitation } from "./ParsePrismInvitation";
import { ParseInvitation } from "./ParseInvitation";
import { HandleOOBInvitation } from "./HandleOOBInvitation";

enum AgentState {
STOPPED = "stopped",
Expand Down Expand Up @@ -246,6 +246,7 @@ export default class DIDCommAgent {

private runTask<T>(task: Task<T>) {
const ctx = new DIDCommContext({
ConnectionManager: this.connectionManager,
MediationHandler: this.mediationHandler,
Mercury: this.mercury,
Api: this.api,
Expand Down Expand Up @@ -315,17 +316,8 @@ export default class DIDCommAgent {
* @returns {Promise<InvitationType>}
*/
async parseInvitation(str: string): Promise<InvitationType> {
const json = JSON.parse(str);
const typeString = findProtocolTypeByValue(json.type);

switch (typeString) {
case ProtocolType.PrismOnboarding:
return this.parsePrismInvitation(str);
case ProtocolType.Didcomminvitation:
return this.parseOOBInvitation(new URL(str));
}

throw new Domain.AgentError.UnknownInvitationTypeError();
const task = new ParseInvitation({ value: str });
return this.runTask(task);
}

/**
Expand Down Expand Up @@ -355,24 +347,8 @@ export default class DIDCommAgent {
* @returns {Promise<PrismOnboardingInvitation>}
*/
async parsePrismInvitation(str: string): Promise<PrismOnboardingInvitation> {
try {
const prismOnboarding = OutOfBandInvitation.parsePrismOnboardingInvitationFromJson(str);
const service = new Domain.Service(
"#didcomm-1",
["DIDCommMessaging"],
new Domain.ServiceEndpoint(prismOnboarding.onboardEndpoint, ["DIDCommMessaging"])
);
const did = await this.createNewPeerDID([service], true);
prismOnboarding.from = did;

return prismOnboarding;
} catch (e) {
if (e instanceof Error) {
throw new Domain.AgentError.UnknownInvitationTypeError(e.message);
} else {
throw e;
}
}
const task = new ParsePrismInvitation({ value: str });
return this.runTask(task);
}

/**
Expand Down Expand Up @@ -406,11 +382,18 @@ export default class DIDCommAgent {
* Asyncronously parse an out of band invitation from a URI as the oob come in format of valid URL
*
* @async
* @param {URL} str
* @param {URL} url
* @returns {Promise<OutOfBandInvitation>}
*/
async parseOOBInvitation(str: URL): Promise<OutOfBandInvitation> {
return new DIDCommInvitationRunner(str).run();
async parseOOBInvitation(url: URL): Promise<OutOfBandInvitation> {
const task = new ParseInvitation({ value: url });
const result = await this.runTask(task);

if (result instanceof OutOfBandInvitation) {
return result;
}

throw new Domain.AgentError.UnknownInvitationTypeError();
}

/**
Expand All @@ -424,32 +407,10 @@ export default class DIDCommAgent {
*/
async acceptDIDCommInvitation(
invitation: OutOfBandInvitation,
optionalAlias?: string
alias?: string
): Promise<void> {
if (!this.connectionManager.mediationHandler.mediator) {
throw new Domain.AgentError.NoMediatorAvailableError();
}
const [attachment] = invitation.attachments ?? [];
const ownDID = await this.createNewPeerDID([], true);

if (isNil(attachment)) {
const pair = await new DIDCommConnectionRunner(
invitation,
this.pluto,
ownDID,
this.connectionManager,
optionalAlias
).run();

await this.connectionManager.addConnection(pair);
}
else {
const msg = Domain.Message.fromJson({
...attachment.payload,
to: ownDID.toString()
});
await this.pluto.storeMessage(msg);
}
const task = new HandleOOBInvitation({ invitation, alias });
return this.runTask(task);
}

/**
Expand Down
6 changes: 6 additions & 0 deletions src/edge-agent/didcomm/Context.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { Task } from "../../utils/tasks";
import { ConnectionsManager } from "../connectionsManager/ConnectionsManager";
import { MediatorHandler } from "../types";

interface Deps {
ConnectionManager: ConnectionsManager;
MediationHandler: MediatorHandler;
}

export class DIDCommContext extends Task.Context<Deps> {
get ConnectionManager() {
return this.getProp("ConnectionManager");
}

get MediationHandler() {
return this.getProp("MediationHandler");
}
Expand Down
42 changes: 42 additions & 0 deletions src/edge-agent/didcomm/HandleOOBInvitation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as Domain from "../../domain";
import { asArray, isNil, notNil } from "../../utils";
import { Task } from "../../utils/tasks";
import { OutOfBandInvitation } from "../protocols/invitation/v2/OutOfBandInvitation";
import { DIDCommContext } from "./Context";
import { CreatePeerDID } from "./CreatePeerDID";
import { HandshakeRequest } from "../protocols/connection/HandshakeRequest";

/**
* Create a connection from an OutOfBandInvitation
* unless the Invitation has Attachments, then we parse and store those
*/

interface Args {
invitation: OutOfBandInvitation;
alias?: string;
}

export class HandleOOBInvitation extends Task<void, Args> {
async run(ctx: DIDCommContext) {
const attachment = asArray(this.args.invitation.attachments).at(0);
const peerDID = await ctx.run(new CreatePeerDID({ services: [], updateMediator: true }));
const attachedMsg = notNil(attachment)
? Domain.Message.fromJson({ ...attachment.payload, to: peerDID.toString() })
: null;

if (isNil(attachedMsg)) {
if (isNil(ctx.ConnectionManager.mediationHandler.mediator)) {
throw new Domain.AgentError.NoMediatorAvailableError();
}

const request = HandshakeRequest.fromOutOfBand(this.args.invitation, peerDID);
await ctx.ConnectionManager.sendMessage(request.makeMessage());
const alias = this.args.alias ?? "OOBConn";
const pair = new Domain.DIDPair(peerDID, request.to, alias);
await ctx.ConnectionManager.addConnection(pair);
}
else {
await ctx.Pluto.storeMessage(attachedMsg);
}
}
}
75 changes: 75 additions & 0 deletions src/edge-agent/didcomm/ParseInvitation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { base64 } from "multiformats/bases/base64";
import * as Domain from "../../domain";
import { JsonObj, asJsonObj, expect, isObject, isString } from "../../utils";
import { Task } from "../../utils/tasks";
import { ProtocolType } from "../protocols/ProtocolTypes";
import { ParsePrismInvitation } from "./ParsePrismInvitation";
import { InvalidURLError, InvitationIsInvalidError } from "../../domain/models/errors/Agent";
import { ParseOOBInvitation } from "./ParseOOBInvitation";
import { InvitationType } from "../types";

/**
* Attempt to parse a given invitation based on its Type
* handle different encodings
*/

interface Args {
value: string | URL | JsonObj;
}

export class ParseInvitation extends Task<InvitationType, Args> {
async run(ctx: Task.Context) {
const json = this.decode();
const type = json.type ?? json.piuri;

switch (type) {
case ProtocolType.PrismOnboarding:
return ctx.run(new ParsePrismInvitation({ value: json }));
case ProtocolType.Didcomminvitation:
return ctx.run(new ParseOOBInvitation({ value: json }));
}

throw new Domain.AgentError.UnknownInvitationTypeError();
}

private decode() {
if (this.args.value instanceof URL) {
return expect(this.tryDecodeUrl(this.args.value), InvitationIsInvalidError);
}

if (isObject(this.args.value)) {
return this.args.value;
}

if (isString(this.args.value)) {
return expect(
this.tryDecodeUrl(this.args.value) ?? this.tryDecodeB64(this.args.value),
InvitationIsInvalidError
);
}

throw new InvitationIsInvalidError();
}

private tryDecodeUrl(value: string | URL) {
try {
const url = new URL(value);
const oob = expect(url.searchParams.get("_oob"), InvalidURLError);
return this.tryDecodeB64(oob);
}
catch {
return null;
}
}

private tryDecodeB64(value: string) {
try {
const decoded = base64.baseDecode(value);
const data = Buffer.from(decoded).toString();
return asJsonObj(data);
}
catch {
return null;
}
}
}
56 changes: 56 additions & 0 deletions src/edge-agent/didcomm/ParseOOBInvitation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as Domain from "../../domain";
import { JsonObj, asArray, asJsonObj, isArray, isNil, isObject, notEmptyString } from "../../utils";
import { Task } from "../../utils/tasks";
import { InvitationIsInvalidError } from "../../domain/models/errors/Agent";
import { OutOfBandInvitation } from "../protocols/invitation/v2/OutOfBandInvitation";
import { ProtocolType } from "../protocols/ProtocolTypes";

/**
* parse OOB invitation
*/

interface Args {
value: string | JsonObj;
}

export class ParseOOBInvitation extends Task<OutOfBandInvitation, Args> {
async run() {
const invitation = this.safeParseBody();

if (invitation.isExpired) {
throw new InvitationIsInvalidError('expired');
}

return invitation;
}

private safeParseBody(): OutOfBandInvitation {
const msg = asJsonObj(this.args.value);
const valid = (
msg.type === ProtocolType.Didcomminvitation
&& notEmptyString(msg.id)
&& notEmptyString(msg.from)
&& isObject(msg.body)
&& isArray(msg.body.accept)
&& msg.body.accept.every(notEmptyString)
&& (isNil(msg.body.goal) || notEmptyString(msg.body.goal))
&& (isNil(msg.body.goal_code) || notEmptyString(msg.body.goal_code))
);

if (valid === false) {
throw new InvitationIsInvalidError();
}

const attachments = asArray(msg.attachments).map((attachment) =>
Domain.AttachmentDescriptor.build(attachment.data, attachment.id, attachment.mediaType)
);

return new OutOfBandInvitation(
msg.body,
msg.from,
msg.id,
attachments,
msg.expires_time
);
}
}
Loading

1 comment on commit 97e05e7

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines Statements Branches Functions
Coverage: 76%
76.36% (3544/4641) 66.45% (1545/2325) 79.98% (855/1069)

Please sign in to comment.