Skip to content

Commit 97e05e7

Browse files
authored
feat: Connectionless Offer (#293)
Signed-off-by: Curtish <[email protected]>
1 parent 295e14f commit 97e05e7

16 files changed

+544
-266
lines changed

Diff for: .vscode/launch.json

+5-13
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,9 @@
3030
"name": "TESTS",
3131
"type": "node",
3232
"request": "launch",
33-
"program": "${workspaceRoot}/node_modules/jest/bin/jest.js",
33+
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
3434
"args": [
35-
"--colors",
36-
"--workerThreads",
37-
"--detectOpenHandles",
38-
"--maxWorkers",
39-
"1",
35+
"run",
4036
"./tests"
4137
],
4238
"skipFiles": [
@@ -48,14 +44,10 @@
4844
"name": "TEST:file",
4945
"type": "node",
5046
"request": "launch",
51-
"program": "${workspaceRoot}/node_modules/jest/bin/jest.js",
47+
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
5248
"args": [
53-
"--colors",
54-
"--workerThreads",
55-
"--detectOpenHandles",
56-
"--maxWorkers",
57-
"1",
58-
"${fileBasenameNoExtension}",
49+
"run",
50+
"${relativeFile}"
5951
],
6052
"skipFiles": [
6153
"${workspaceRoot}/../../node_modules/**/*",

Diff for: package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"build:dev": "npm run externals:check && rm -rf build && npm run build:agnostic && npm run types",
3838
"build:agnostic": "node esbuild.config.mjs",
3939
"types": "rm -rf build/**/*.d.ts && tsc",
40-
"test": "npx vitest --config vitest.config.mjs --run tests",
40+
"test": "npx vitest --config vitest.config.mjs --run",
4141
"coverage": "npm run test -- --coverage",
4242
"lint": "npx eslint .",
4343
"docs": "npx typedoc --options typedoc.js",
@@ -171,4 +171,4 @@
171171
"overrides": {
172172
"ws": "^8.17.1"
173173
}
174-
}
174+
}

Diff for: src/edge-agent/didcomm/Agent.ts

+22-61
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,17 @@ import { HandleIssueCredential } from "./HandleIssueCredential";
2626
import { HandleOfferCredential } from "./HandleOfferCredential";
2727
import { HandlePresentation } from "./HandlePresentation";
2828
import { CreatePresentation } from "./CreatePresentation";
29-
import { ProtocolType, findProtocolTypeByValue } from "../protocols/ProtocolTypes";
30-
import { DIDCommConnectionRunner } from "../protocols/connection/DIDCommConnectionRunner";
31-
import { DIDCommInvitationRunner } from "../protocols/invitation/v2/DIDCommInvitationRunner";
29+
import { ProtocolType } from "../protocols/ProtocolTypes";
3230
import Pollux from "../../pollux";
3331
import Apollo from "../../apollo";
3432
import Castor from "../../castor";
3533
import * as DIDfns from "../didFunctions";
3634
import { Task } from "../../utils/tasks";
3735
import { DIDCommContext } from "./Context";
3836
import { FetchApi } from "../helpers/FetchApi";
39-
import { isNil } from "../../utils";
37+
import { ParsePrismInvitation } from "./ParsePrismInvitation";
38+
import { ParseInvitation } from "./ParseInvitation";
39+
import { HandleOOBInvitation } from "./HandleOOBInvitation";
4040

4141
enum AgentState {
4242
STOPPED = "stopped",
@@ -246,6 +246,7 @@ export default class DIDCommAgent {
246246

247247
private runTask<T>(task: Task<T>) {
248248
const ctx = new DIDCommContext({
249+
ConnectionManager: this.connectionManager,
249250
MediationHandler: this.mediationHandler,
250251
Mercury: this.mercury,
251252
Api: this.api,
@@ -315,17 +316,8 @@ export default class DIDCommAgent {
315316
* @returns {Promise<InvitationType>}
316317
*/
317318
async parseInvitation(str: string): Promise<InvitationType> {
318-
const json = JSON.parse(str);
319-
const typeString = findProtocolTypeByValue(json.type);
320-
321-
switch (typeString) {
322-
case ProtocolType.PrismOnboarding:
323-
return this.parsePrismInvitation(str);
324-
case ProtocolType.Didcomminvitation:
325-
return this.parseOOBInvitation(new URL(str));
326-
}
327-
328-
throw new Domain.AgentError.UnknownInvitationTypeError();
319+
const task = new ParseInvitation({ value: str });
320+
return this.runTask(task);
329321
}
330322

331323
/**
@@ -355,24 +347,8 @@ export default class DIDCommAgent {
355347
* @returns {Promise<PrismOnboardingInvitation>}
356348
*/
357349
async parsePrismInvitation(str: string): Promise<PrismOnboardingInvitation> {
358-
try {
359-
const prismOnboarding = OutOfBandInvitation.parsePrismOnboardingInvitationFromJson(str);
360-
const service = new Domain.Service(
361-
"#didcomm-1",
362-
["DIDCommMessaging"],
363-
new Domain.ServiceEndpoint(prismOnboarding.onboardEndpoint, ["DIDCommMessaging"])
364-
);
365-
const did = await this.createNewPeerDID([service], true);
366-
prismOnboarding.from = did;
367-
368-
return prismOnboarding;
369-
} catch (e) {
370-
if (e instanceof Error) {
371-
throw new Domain.AgentError.UnknownInvitationTypeError(e.message);
372-
} else {
373-
throw e;
374-
}
375-
}
350+
const task = new ParsePrismInvitation({ value: str });
351+
return this.runTask(task);
376352
}
377353

378354
/**
@@ -406,11 +382,18 @@ export default class DIDCommAgent {
406382
* Asyncronously parse an out of band invitation from a URI as the oob come in format of valid URL
407383
*
408384
* @async
409-
* @param {URL} str
385+
* @param {URL} url
410386
* @returns {Promise<OutOfBandInvitation>}
411387
*/
412-
async parseOOBInvitation(str: URL): Promise<OutOfBandInvitation> {
413-
return new DIDCommInvitationRunner(str).run();
388+
async parseOOBInvitation(url: URL): Promise<OutOfBandInvitation> {
389+
const task = new ParseInvitation({ value: url });
390+
const result = await this.runTask(task);
391+
392+
if (result instanceof OutOfBandInvitation) {
393+
return result;
394+
}
395+
396+
throw new Domain.AgentError.UnknownInvitationTypeError();
414397
}
415398

416399
/**
@@ -424,32 +407,10 @@ export default class DIDCommAgent {
424407
*/
425408
async acceptDIDCommInvitation(
426409
invitation: OutOfBandInvitation,
427-
optionalAlias?: string
410+
alias?: string
428411
): Promise<void> {
429-
if (!this.connectionManager.mediationHandler.mediator) {
430-
throw new Domain.AgentError.NoMediatorAvailableError();
431-
}
432-
const [attachment] = invitation.attachments ?? [];
433-
const ownDID = await this.createNewPeerDID([], true);
434-
435-
if (isNil(attachment)) {
436-
const pair = await new DIDCommConnectionRunner(
437-
invitation,
438-
this.pluto,
439-
ownDID,
440-
this.connectionManager,
441-
optionalAlias
442-
).run();
443-
444-
await this.connectionManager.addConnection(pair);
445-
}
446-
else {
447-
const msg = Domain.Message.fromJson({
448-
...attachment.payload,
449-
to: ownDID.toString()
450-
});
451-
await this.pluto.storeMessage(msg);
452-
}
412+
const task = new HandleOOBInvitation({ invitation, alias });
413+
return this.runTask(task);
453414
}
454415

455416
/**

Diff for: src/edge-agent/didcomm/Context.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { Task } from "../../utils/tasks";
2+
import { ConnectionsManager } from "../connectionsManager/ConnectionsManager";
23
import { MediatorHandler } from "../types";
34

45
interface Deps {
6+
ConnectionManager: ConnectionsManager;
57
MediationHandler: MediatorHandler;
68
}
79

810
export class DIDCommContext extends Task.Context<Deps> {
11+
get ConnectionManager() {
12+
return this.getProp("ConnectionManager");
13+
}
14+
915
get MediationHandler() {
1016
return this.getProp("MediationHandler");
1117
}

Diff for: src/edge-agent/didcomm/HandleOOBInvitation.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as Domain from "../../domain";
2+
import { asArray, isNil, notNil } from "../../utils";
3+
import { Task } from "../../utils/tasks";
4+
import { OutOfBandInvitation } from "../protocols/invitation/v2/OutOfBandInvitation";
5+
import { DIDCommContext } from "./Context";
6+
import { CreatePeerDID } from "./CreatePeerDID";
7+
import { HandshakeRequest } from "../protocols/connection/HandshakeRequest";
8+
9+
/**
10+
* Create a connection from an OutOfBandInvitation
11+
* unless the Invitation has Attachments, then we parse and store those
12+
*/
13+
14+
interface Args {
15+
invitation: OutOfBandInvitation;
16+
alias?: string;
17+
}
18+
19+
export class HandleOOBInvitation extends Task<void, Args> {
20+
async run(ctx: DIDCommContext) {
21+
const attachment = asArray(this.args.invitation.attachments).at(0);
22+
const peerDID = await ctx.run(new CreatePeerDID({ services: [], updateMediator: true }));
23+
const attachedMsg = notNil(attachment)
24+
? Domain.Message.fromJson({ ...attachment.payload, to: peerDID.toString() })
25+
: null;
26+
27+
if (isNil(attachedMsg)) {
28+
if (isNil(ctx.ConnectionManager.mediationHandler.mediator)) {
29+
throw new Domain.AgentError.NoMediatorAvailableError();
30+
}
31+
32+
const request = HandshakeRequest.fromOutOfBand(this.args.invitation, peerDID);
33+
await ctx.ConnectionManager.sendMessage(request.makeMessage());
34+
const alias = this.args.alias ?? "OOBConn";
35+
const pair = new Domain.DIDPair(peerDID, request.to, alias);
36+
await ctx.ConnectionManager.addConnection(pair);
37+
}
38+
else {
39+
await ctx.Pluto.storeMessage(attachedMsg);
40+
}
41+
}
42+
}

Diff for: src/edge-agent/didcomm/ParseInvitation.ts

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { base64 } from "multiformats/bases/base64";
2+
import * as Domain from "../../domain";
3+
import { JsonObj, asJsonObj, expect, isObject, isString } from "../../utils";
4+
import { Task } from "../../utils/tasks";
5+
import { ProtocolType } from "../protocols/ProtocolTypes";
6+
import { ParsePrismInvitation } from "./ParsePrismInvitation";
7+
import { InvalidURLError, InvitationIsInvalidError } from "../../domain/models/errors/Agent";
8+
import { ParseOOBInvitation } from "./ParseOOBInvitation";
9+
import { InvitationType } from "../types";
10+
11+
/**
12+
* Attempt to parse a given invitation based on its Type
13+
* handle different encodings
14+
*/
15+
16+
interface Args {
17+
value: string | URL | JsonObj;
18+
}
19+
20+
export class ParseInvitation extends Task<InvitationType, Args> {
21+
async run(ctx: Task.Context) {
22+
const json = this.decode();
23+
const type = json.type ?? json.piuri;
24+
25+
switch (type) {
26+
case ProtocolType.PrismOnboarding:
27+
return ctx.run(new ParsePrismInvitation({ value: json }));
28+
case ProtocolType.Didcomminvitation:
29+
return ctx.run(new ParseOOBInvitation({ value: json }));
30+
}
31+
32+
throw new Domain.AgentError.UnknownInvitationTypeError();
33+
}
34+
35+
private decode() {
36+
if (this.args.value instanceof URL) {
37+
return expect(this.tryDecodeUrl(this.args.value), InvitationIsInvalidError);
38+
}
39+
40+
if (isObject(this.args.value)) {
41+
return this.args.value;
42+
}
43+
44+
if (isString(this.args.value)) {
45+
return expect(
46+
this.tryDecodeUrl(this.args.value) ?? this.tryDecodeB64(this.args.value),
47+
InvitationIsInvalidError
48+
);
49+
}
50+
51+
throw new InvitationIsInvalidError();
52+
}
53+
54+
private tryDecodeUrl(value: string | URL) {
55+
try {
56+
const url = new URL(value);
57+
const oob = expect(url.searchParams.get("_oob"), InvalidURLError);
58+
return this.tryDecodeB64(oob);
59+
}
60+
catch {
61+
return null;
62+
}
63+
}
64+
65+
private tryDecodeB64(value: string) {
66+
try {
67+
const decoded = base64.baseDecode(value);
68+
const data = Buffer.from(decoded).toString();
69+
return asJsonObj(data);
70+
}
71+
catch {
72+
return null;
73+
}
74+
}
75+
}

Diff for: src/edge-agent/didcomm/ParseOOBInvitation.ts

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as Domain from "../../domain";
2+
import { JsonObj, asArray, asJsonObj, isArray, isNil, isObject, notEmptyString } from "../../utils";
3+
import { Task } from "../../utils/tasks";
4+
import { InvitationIsInvalidError } from "../../domain/models/errors/Agent";
5+
import { OutOfBandInvitation } from "../protocols/invitation/v2/OutOfBandInvitation";
6+
import { ProtocolType } from "../protocols/ProtocolTypes";
7+
8+
/**
9+
* parse OOB invitation
10+
*/
11+
12+
interface Args {
13+
value: string | JsonObj;
14+
}
15+
16+
export class ParseOOBInvitation extends Task<OutOfBandInvitation, Args> {
17+
async run() {
18+
const invitation = this.safeParseBody();
19+
20+
if (invitation.isExpired) {
21+
throw new InvitationIsInvalidError('expired');
22+
}
23+
24+
return invitation;
25+
}
26+
27+
private safeParseBody(): OutOfBandInvitation {
28+
const msg = asJsonObj(this.args.value);
29+
const valid = (
30+
msg.type === ProtocolType.Didcomminvitation
31+
&& notEmptyString(msg.id)
32+
&& notEmptyString(msg.from)
33+
&& isObject(msg.body)
34+
&& isArray(msg.body.accept)
35+
&& msg.body.accept.every(notEmptyString)
36+
&& (isNil(msg.body.goal) || notEmptyString(msg.body.goal))
37+
&& (isNil(msg.body.goal_code) || notEmptyString(msg.body.goal_code))
38+
);
39+
40+
if (valid === false) {
41+
throw new InvitationIsInvalidError();
42+
}
43+
44+
const attachments = asArray(msg.attachments).map((attachment) =>
45+
Domain.AttachmentDescriptor.build(attachment.data, attachment.id, attachment.mediaType)
46+
);
47+
48+
return new OutOfBandInvitation(
49+
msg.body,
50+
msg.from,
51+
msg.id,
52+
attachments,
53+
msg.expires_time
54+
);
55+
}
56+
}

0 commit comments

Comments
 (0)