From a52ae700f3239f38ef4aa80f68abada9425e2a0f Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Mon, 15 May 2023 09:03:20 -0400 Subject: [PATCH] Add from: support to dwn.records.query/.delete and protocols.configure/.query Signed-off-by: Frank Hinek --- package-lock.json | 28 ++--- .../web5-user-agent/src/web5-user-agent.ts | 1 - packages/web5/src/dwn-api.ts | 77 +++++++++--- .../fixtures/protocol-definitions/chat.json | 18 --- .../protocol-definitions/message.json | 18 +++ packages/web5/tests/web5-dwn.spec.ts | 110 ++++++++++++++++-- packages/web5/tsconfig.test.json | 2 + 7 files changed, 194 insertions(+), 60 deletions(-) delete mode 100644 packages/web5/tests/fixtures/protocol-definitions/chat.json create mode 100644 packages/web5/tests/fixtures/protocol-definitions/message.json diff --git a/package-lock.json b/package-lock.json index 2540c5141..a45cfda6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -930,9 +930,9 @@ "link": true }, "node_modules/@tbd54566975/dwn-sdk-js": { - "version": "0.0.31-unstable-2023-05-10-816a0ef", - "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.31-unstable-2023-05-10-816a0ef.tgz", - "integrity": "sha512-cIoy+sfwXF00wb1kbi6VwddzI0R6pXe1tT3wDu3l/aV6PUy/jAetX95pEuxylrbe3OqldvbcYYV7/e64HGY7Pg==", + "version": "0.0.31-unstable-2023-05-11-32b5f91", + "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.31-unstable-2023-05-11-32b5f91.tgz", + "integrity": "sha512-MV3AFCy0kCAd1phAzyr1RvaGB74i9V5jFusVQ8YJGJ+unuQW/AYc33ZTAoBdhsxon3TuXeWpiAFS0OWSwCjEQw==", "dependencies": { "@ipld/dag-cbor": "9.0.0", "@js-temporal/polyfill": "0.4.3", @@ -7608,7 +7608,7 @@ "dependencies": { "@decentralized-identity/ion-tools": "1.0.7", "@tbd54566975/crypto": "0.1.0", - "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-10-816a0ef", + "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-11-32b5f91", "cross-fetch": "3.1.5" }, "devDependencies": { @@ -7643,7 +7643,7 @@ "@decentralized-identity/ion-tools": "1.0.7", "@tbd54566975/crypto": "0.1.0", "@tbd54566975/dids": "0.1.0", - "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-10-816a0ef", + "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-11-32b5f91", "@tbd54566975/web5-agent": "0.1.0", "@tbd54566975/web5-proxy-agent": "0.1.0", "@tbd54566975/web5-user-agent": "0.1.0", @@ -7683,7 +7683,7 @@ "name": "@tbd54566975/web5-agent", "version": "0.1.0", "dependencies": { - "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-10-816a0ef", + "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-11-32b5f91", "readable-stream": "4.3.0" }, "devDependencies": { @@ -7732,7 +7732,7 @@ "dependencies": { "@decentralized-identity/ion-tools": "1.0.7", "@tbd54566975/dids": "0.1.0", - "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-10-816a0ef", + "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-11-32b5f91", "abstract-level": "1.0.3", "cross-fetch": "3.1.5", "flat": "5.0.2", @@ -9196,7 +9196,7 @@ "requires": { "@decentralized-identity/ion-tools": "1.0.7", "@tbd54566975/crypto": "0.1.0", - "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-10-816a0ef", + "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-11-32b5f91", "@types/chai": "4.3.0", "@types/eslint": "8.37.0", "@types/mocha": "10.0.1", @@ -9222,9 +9222,9 @@ } }, "@tbd54566975/dwn-sdk-js": { - "version": "0.0.31-unstable-2023-05-10-816a0ef", - "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.31-unstable-2023-05-10-816a0ef.tgz", - "integrity": "sha512-cIoy+sfwXF00wb1kbi6VwddzI0R6pXe1tT3wDu3l/aV6PUy/jAetX95pEuxylrbe3OqldvbcYYV7/e64HGY7Pg==", + "version": "0.0.31-unstable-2023-05-11-32b5f91", + "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.31-unstable-2023-05-11-32b5f91.tgz", + "integrity": "sha512-MV3AFCy0kCAd1phAzyr1RvaGB74i9V5jFusVQ8YJGJ+unuQW/AYc33ZTAoBdhsxon3TuXeWpiAFS0OWSwCjEQw==", "requires": { "@ipld/dag-cbor": "9.0.0", "@js-temporal/polyfill": "0.4.3", @@ -9284,7 +9284,7 @@ "@decentralized-identity/ion-tools": "1.0.7", "@tbd54566975/crypto": "0.1.0", "@tbd54566975/dids": "0.1.0", - "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-10-816a0ef", + "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-11-32b5f91", "@tbd54566975/web5-agent": "0.1.0", "@tbd54566975/web5-proxy-agent": "0.1.0", "@tbd54566975/web5-user-agent": "0.1.0", @@ -9507,7 +9507,7 @@ "@tbd54566975/web5-agent": { "version": "file:packages/web5-agent", "requires": { - "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-10-816a0ef", + "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-11-32b5f91", "@types/eslint": "8.37.0", "@typescript-eslint/eslint-plugin": "5.59.0", "@typescript-eslint/parser": "5.59.0", @@ -9550,7 +9550,7 @@ "@decentralized-identity/ion-tools": "1.0.7", "@playwright/test": "^1.33.0", "@tbd54566975/dids": "0.1.0", - "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-10-816a0ef", + "@tbd54566975/dwn-sdk-js": "0.0.31-unstable-2023-05-11-32b5f91", "@types/flat": "5.0.2", "@typescript-eslint/eslint-plugin": "5.59.0", "@typescript-eslint/parser": "5.59.0", diff --git a/packages/web5-user-agent/src/web5-user-agent.ts b/packages/web5-user-agent/src/web5-user-agent.ts index 6e34751db..cebc3c447 100644 --- a/packages/web5-user-agent/src/web5-user-agent.ts +++ b/packages/web5-user-agent/src/web5-user-agent.ts @@ -13,7 +13,6 @@ import { import { Cid, - DataStream, SignatureInput, RecordsWriteOptions, PrivateJwk as DwnPrivateKeyJwk, diff --git a/packages/web5/src/dwn-api.ts b/packages/web5/src/dwn-api.ts index 03e9477fb..8494175ed 100644 --- a/packages/web5/src/dwn-api.ts +++ b/packages/web5/src/dwn-api.ts @@ -1,6 +1,7 @@ import type { Web5Agent } from '@tbd54566975/web5-agent'; import type { MessageReply, + ProtocolDefinition, ProtocolsConfigureOptions, ProtocolsQueryOptions, RecordsDeleteOptions, @@ -16,17 +17,35 @@ import { DwnInterfaceName, DwnMethodName, DataStream } from '@tbd54566975/dwn-sd import { Record } from './record.js'; import { dataToBytes, isDataSizeUnderCacheLimit, isEmptyObject } from './utils.js'; +// TODO: Export type ProtocolsConfigureDescriptor from dwn-sdk-js. +export type ProtocolsConfigureDescriptor = { + dateCreated: string; + definition: ProtocolDefinition; + interface : DwnInterfaceName.Protocols; + method: DwnMethodName.Configure; + protocol: string; +}; + export type ProtocolsConfigureRequest = { message: Omit; } +export type ProtocolsConfigureResponse = { + status: MessageReply['status']; +} + +export type ProtocolsQueryReplyEntry = { + descriptor: ProtocolsConfigureDescriptor; +}; + export type ProtocolsQueryRequest = { from?: string; message: Omit } -export type RecordsDeleteRequest = { - message: Omit; +export type ProtocolsQueryResponse = { + protocols: ProtocolsQueryReplyEntry[]; + status: MessageReply['status']; } export type RecordsCreateRequest = RecordsWriteRequest; @@ -40,6 +59,11 @@ export type RecordsCreateFromRequest = { record: Record; } +export type RecordsDeleteRequest = { + from?: string; + message: Omit; +} + export type RecordsDeleteResponse = { status: MessageReply['status']; }; @@ -100,25 +124,34 @@ export class DwnApi { /** * TODO: Document method. */ - configure: async (request: ProtocolsConfigureRequest) => { - return await this.web5Agent.processDwnRequest({ + configure: async (request: ProtocolsConfigureRequest): Promise => { + const agentResponse = await this.web5Agent.processDwnRequest({ target : this.connectedDid, author : this.connectedDid, messageOptions : request.message, messageType : DwnInterfaceName.Protocols + DwnMethodName.Configure }); + + const { reply: { status }} = agentResponse; + + return { status }; }, /** * TODO: Document method. */ - query: async (request: ProtocolsQueryRequest) => { - return await this.web5Agent.processDwnRequest({ + query: async (request: ProtocolsQueryRequest): Promise => { + const agentResponse = await this.web5Agent.processDwnRequest({ author : this.connectedDid, messageOptions : request.message, messageType : DwnInterfaceName.Protocols + DwnMethodName.Query, target : this.connectedDid }); + + const { reply: { entries, status } } = agentResponse; + const protocols = entries as ProtocolsQueryReplyEntry[]; + + return { protocols, status }; } }; } @@ -177,20 +210,30 @@ export class DwnApi { * TODO: Document method. */ delete: async (request: RecordsDeleteRequest): Promise => { - const { message: requestMessage } = request; - - const messageOptions: Partial = { - ...requestMessage + const agentRequest = { + author : this.connectedDid, + messageOptions : request.message, + messageType : DwnInterfaceName.Records + DwnMethodName.Delete, + target : request.from || this.connectedDid }; - const agentResponse = await this.web5Agent.processDwnRequest({ - author : this.connectedDid, - messageOptions, - messageType : DwnInterfaceName.Records + DwnMethodName.Delete, - target : this.connectedDid - }); + let agentResponse; + + if (request.from) { + agentResponse = await this.web5Agent.sendDwnRequest(agentRequest); + } else { + agentResponse = await this.web5Agent.processDwnRequest(agentRequest); + } - const { reply: { status } } = agentResponse; + //! TODO: (Frank -> Moe): This quirk is the result of how 4XX errors are being returned by `dwn-server` + //! When DWN SDK returns 404, agentResponse is { status: { code: 404 }} and that's it. + //! Need to decide how to resolve. + let status; + if (agentResponse.reply) { + ({ reply: { status } } = agentResponse); + } else { + ({ status } = agentResponse); + } return { status }; }, diff --git a/packages/web5/tests/fixtures/protocol-definitions/chat.json b/packages/web5/tests/fixtures/protocol-definitions/chat.json deleted file mode 100644 index a1cebee1a..000000000 --- a/packages/web5/tests/fixtures/protocol-definitions/chat.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "recordDefinitions": [ - { - "id": "message", - "schema": "http://example.org/chat/schema/message" - } - ], - "records": { - "message": { - "allow": [ - { - "actor": "anyone", - "actions": ["write"] - } - ] - } - } -} \ No newline at end of file diff --git a/packages/web5/tests/fixtures/protocol-definitions/message.json b/packages/web5/tests/fixtures/protocol-definitions/message.json new file mode 100644 index 000000000..12f46611e --- /dev/null +++ b/packages/web5/tests/fixtures/protocol-definitions/message.json @@ -0,0 +1,18 @@ +{ + "types": { + "message": { + "schema": "https://protocols.xyz/message/schema/message", + "dataFormats": ["text/plain"] + } + }, + "structure": { + "message": { + "$actions": [ + { + "who": "anyone", + "can": "write" + } + ] + } + } +} \ No newline at end of file diff --git a/packages/web5/tests/web5-dwn.spec.ts b/packages/web5/tests/web5-dwn.spec.ts index 053a8020d..18b395fe8 100644 --- a/packages/web5/tests/web5-dwn.spec.ts +++ b/packages/web5/tests/web5-dwn.spec.ts @@ -3,6 +3,7 @@ import { expect } from 'chai'; import { DwnApi } from '../src/dwn-api.js'; import * as testProfile from './fixtures/test-profiles.js'; import { TestAgent, TestProfileOptions } from './test-utils/test-user-agent.js'; +import messageProtocolDefinition from './fixtures/protocol-definitions/message.json' assert { type: 'json' }; let didOnlyAuthz: string; let dwn: DwnApi; @@ -29,18 +30,91 @@ describe('web5.dwn', () => { describe('protocols', () => { describe('configure', () => { - xit('tests needed'); + describe('agent', () => { + it('writes a protocol definition', async () => { + const protocolUri = 'https://protocols.xyz/message/protocol'; + const protocolDefinition = messageProtocolDefinition; + + const response = await dwn.protocols.configure({ + message: { + protocol : protocolUri, + definition : protocolDefinition + } + }); + + expect(response.status.code).to.equal(202); + expect(response.status.detail).to.equal('Accepted'); + }); + }); + + describe('from: did', () => { + xit('test neeed'); + }); }); describe('query', () => { - xit('tests needed'); + describe('agent', () => { + it('should return protocols matching the query', async () => { + let response; + // Write a protocols configure to the connected agent's DWN. + const protocolUri = 'https://protocols.xyz/message/protocol'; + const protocolDefinition = messageProtocolDefinition; + response = await dwn.protocols.configure({ + message: { + protocol : protocolUri, + definition : protocolDefinition + } + }); + expect(response.status.code).to.equal(202); + expect(response.status.detail).to.equal('Accepted'); + + // Query for the protocol just configured. + response = await dwn.protocols.query({ + message: { + filter: { + protocol: protocolUri + } + } + }); + + expect(response.status.code).to.equal(200); + expect(response.protocols.length).to.equal(1); + expect(response.protocols[0].descriptor).to.have.property('protocol'); + expect(response.protocols[0].descriptor).to.have.property('definition'); + expect(response.protocols[0].descriptor.protocol).to.equal(protocolUri); + expect(response.protocols[0].descriptor.definition).to.have.property('types'); + expect(response.protocols[0].descriptor.definition).to.have.property('structure'); + }); + }); + + describe('from: did', () => { + it('returns empty protocols array when no protocols match the filter provided', async () => { + // Create a new DID to represent an external entity who has a remote DWN server defined in their DID document. + const ionCreateOptions = await testProfile.ionCreateOptions.services.dwn.authorization.keys(); + const { id: bobDid } = await testAgent.didIon.create(ionCreateOptions); + + // Query for the protocol just configured. + const response = await dwn.protocols.query({ + from : bobDid, + message : { + filter: { + protocol: 'https://doesnotexist.com/protocol' + } + } + }); + + expect(response.status.code).to.equal(200); + expect(response.protocols).to.exist; + expect(response.protocols.length).to.equal(0); + }); + }); }); }); describe('records', () => { describe('write', () => { describe('agent', () => { - it(`writes a record to alice's local dwn`, async () => { + it('writes a record', async () => { const result = await dwn.records.write({ data : 'Hello, world!', message : { @@ -92,20 +166,20 @@ describe('web5.dwn', () => { describe('from: did', () => { it('returns empty records array when no records match the filter provided', async () => { - // Write the record to the connected agent's DWN. - const { record, status } = await dwn.records.write({ data: 'hi' }); - expect(status.code).to.equal(202); + // // Write the record to the connected agent's DWN. + // const { record, status } = await dwn.records.write({ data: 'hi' }); + // expect(status.code).to.equal(202); // Create a new DID to represent an external entity who has a remote DWN server defined in their DID document. const ionCreateOptions = await testProfile.ionCreateOptions.services.dwn.authorization.keys(); const { id: bobDid } = await testAgent.didIon.create(ionCreateOptions); - // Attempt to query Bob's DWN using the ID of a record that only exists in the connected agent's DWN. + // Attempt to query Bob's DWN using the ID of a record that does not exist. const result = await dwn.records.query({ from : bobDid, message : { filter: { - recordId: record!.id + recordId: 'abcd1234' } } }); @@ -245,7 +319,7 @@ describe('web5.dwn', () => { expect(deleteResult.status.code).to.equal(202); }); - it('returns a 202 if the recordId does not exist', async () => { + it('returns a 202 when the specified record does not exist', async () => { let deleteResult = await dwn.records.delete({ message: { recordId: 'abcd1234' @@ -256,7 +330,23 @@ describe('web5.dwn', () => { }); describe('from: did', () => { - xit('tests needed'); + it('returns a 401 when authentication or authorization fails', async () => { + // Create a new DID to represent an external entity who has a remote DWN server defined in their DID document. + const ionCreateOptions = await testProfile.ionCreateOptions.services.dwn.authorization.keys(); + const { id: bobDid } = await testAgent.didIon.create(ionCreateOptions); + + // Attempt to delete a record from Bob's DWN specifying a recordId that does not exist. + const deleteResult = await dwn.records.delete({ + from : bobDid, + message : { + recordId: 'abcd1234' + } + }); + + //! TODO: Once record.send() has been implemented, add another test to write a record + //! and test a delete to confirm that authn/authz pass and a 202 is returned. + expect(deleteResult.status.code).to.equal(401); + }); }); }); }); diff --git a/packages/web5/tsconfig.test.json b/packages/web5/tsconfig.test.json index 2554f4bfd..3708176a3 100644 --- a/packages/web5/tsconfig.test.json +++ b/packages/web5/tsconfig.test.json @@ -14,6 +14,8 @@ // `NodeNext` will throw compilation errors if relative import paths are missing file extension // reference: https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/#ecmascript-module-support-in-node-js "moduleResolution": "NodeNext", + // allows us to import json files + "resolveJsonModule": true, "esModuleInterop": true }, "include": [