From 88c5bbe2ff1cff833bf6b59792e035733c8e102d Mon Sep 17 00:00:00 2001 From: Tim Shamilov Date: Thu, 24 Oct 2024 12:13:18 -0400 Subject: [PATCH] format --- packages/agent/src/connect.ts | 8 +- packages/agent/src/oidc.ts | 56 +++-- packages/agent/tests/connect.spec.ts | 353 ++++++++++++++++++++------- packages/api/src/web5.ts | 141 +++++++---- 4 files changed, 393 insertions(+), 165 deletions(-) diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts index 48a19ef43..8798ed4b3 100644 --- a/packages/agent/src/connect.ts +++ b/packages/agent/src/connect.ts @@ -249,13 +249,7 @@ function createPermissionRequestForProtocol({ /** The permissions being requested for the protocol. Defaults to all. */ permissions?: Permission[]; }) { - permissions ??= [ - 'read', - 'write', - 'delete', - 'query', - 'subscribe', - ]; + permissions ??= ['read', 'write', 'delete', 'query', 'subscribe']; const requests: DwnPermissionScope[] = []; diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index 975ec86c0..147602742 100644 --- a/packages/agent/src/oidc.ts +++ b/packages/agent/src/oidc.ts @@ -12,7 +12,13 @@ import { import { concatenateUrl } from './utils.js'; import { xchacha20poly1305 } from '@noble/ciphers/chacha'; import type { ConnectPermissionRequest } from './connect.js'; -import { DidDocument, DidJwk, DidResolutionResult, PortableDid, type BearerDid } from '@web5/dids'; +import { + DidDocument, + DidJwk, + DidResolutionResult, + PortableDid, + type BearerDid, +} from '@web5/dids'; import { DwnDataEncodedRecordsWriteMessage, DwnInterface, @@ -291,7 +297,7 @@ async function createAuthRequest( async function encryptAuthRequest({ jwt, encryptionKey, - kid + kid, }: { jwt: string; encryptionKey: Uint8Array; @@ -302,7 +308,7 @@ async function encryptAuthRequest({ cty : 'JWT', enc : 'XC20P', typ : 'JWT', - kid + kid, }; const nonce = CryptoUtils.randomBytes(24); const additionalData = Convert.object(protectedHeader).toUint8Array(); @@ -438,7 +444,9 @@ const getAuthRequest = async (request_uri: string, encryption_key: string) => { })) as Web5ConnectAuthRequest; // get the pub DID that represents the client in ECDH and deriving a shared key - const header = Convert.base64Url(jwe.split('.')[0]).toObject() as JweHeaderParams; + const header = Convert.base64Url( + jwe.split('.')[0] + ).toObject() as JweHeaderParams; const clientEcdhDid = await DidJwk.resolve(header.kid!.split('#')[0]); @@ -502,7 +510,9 @@ async function decryptWithPin(clientDid: BearerDid, jwe: string, pin: string) { const jweProviderEcdhDidKid = await DidJwk.resolve(header.kid!.split('#')[0]); if (!jweProviderEcdhDidKid.didDocument) { - throw new Error('Could not resolve provider\'s didd document for shared key derivation'); + throw new Error( + 'Could not resolve provider\'s didd document for shared key derivation' + ); } // derive ECDH shared key using the provider's public key and our clientDid private key @@ -638,7 +648,10 @@ function shouldUseDelegatePermission(scope: DwnPermissionScope): boolean { // In the future only methods that modify state will be delegated and the rest will be normal permissions if (isRecordPermissionScope(scope)) { return true; - } else if (scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure) { + } else if ( + scope.interface === DwnInterfaceName.Protocols && + scope.method === DwnMethodName.Configure + ) { // ProtocolConfigure messages are also delegated, as they modify state return true; } @@ -675,7 +688,9 @@ async function createPermissionGrants( }) ); - logger.log(`Sending ${permissionGrants.length} permission grants to remote DWN...`); + logger.log( + `Sending ${permissionGrants.length} permission grants to remote DWN...` + ); const messagePromises = permissionGrants.map(async (grant) => { // Quirk: we have to pull out encodedData out of the message the schema validator doesn't want it there const { encodedData, ...rawMessage } = grant.message; @@ -718,7 +733,6 @@ async function prepareProtocol( agent: Web5Agent, protocolDefinition: DwnProtocolDefinition ): Promise { - const queryMessage = await agent.processDwnRequest({ author : selectedDid, messageType : DwnInterface.ProtocolsQuery, @@ -731,16 +745,22 @@ async function prepareProtocol( throw new Error( `Could not fetch protocol: ${queryMessage.reply.status.detail}` ); - } else if (queryMessage.reply.entries === undefined || queryMessage.reply.entries.length === 0) { - logger.log(`Protocol does not exist, creating: ${protocolDefinition.protocol}`); + } else if ( + queryMessage.reply.entries === undefined || + queryMessage.reply.entries.length === 0 + ) { + logger.log( + `Protocol does not exist, creating: ${protocolDefinition.protocol}` + ); // send the protocol definition to the remote DWN first, if it passes we can process it locally - const { reply: sendReply, message: configureMessage } = await agent.sendDwnRequest({ - author : selectedDid, - target : selectedDid, - messageType : DwnInterface.ProtocolsConfigure, - messageParams : { definition: protocolDefinition }, - }); + const { reply: sendReply, message: configureMessage } = + await agent.sendDwnRequest({ + author : selectedDid, + target : selectedDid, + messageType : DwnInterface.ProtocolsConfigure, + messageParams : { definition: protocolDefinition }, + }); // check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync if (sendReply.status.code !== 202 && sendReply.status.code !== 409) { @@ -851,7 +871,9 @@ async function submitAuthResponse( }); if (!clientEcdhDid.didDocument?.verificationMethod?.[0].id) { - throw new Error('Unable to resolve the encryption DID used by the client for ECDH'); + throw new Error( + 'Unable to resolve the encryption DID used by the client for ECDH' + ); } const sharedKey = await Oidc.deriveSharedKey( diff --git a/packages/agent/tests/connect.spec.ts b/packages/agent/tests/connect.spec.ts index 4c3eee84c..3ba2d9bec 100644 --- a/packages/agent/tests/connect.spec.ts +++ b/packages/agent/tests/connect.spec.ts @@ -1,7 +1,13 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { CryptoUtils } from '@web5/crypto'; -import { type BearerDid, DidDht, DidJwk, type DidResolutionResult, type PortableDid } from '@web5/dids'; +import { + type BearerDid, + DidDht, + DidJwk, + type DidResolutionResult, + type PortableDid, +} from '@web5/dids'; import { Convert } from '@web5/common'; import { Oidc, @@ -11,7 +17,13 @@ import { import { PlatformAgentTestHarness } from '../src/test-harness.js'; import { TestAgent } from './utils/test-agent.js'; import { testDwnUrl } from './utils/test-config.js'; -import { BearerIdentity, DwnInterface, DwnMessage, DwnProtocolDefinition, WalletConnect } from '../src/index.js'; +import { + BearerIdentity, + DwnInterface, + DwnMessage, + DwnProtocolDefinition, + WalletConnect, +} from '../src/index.js'; import { RecordsPermissionScope } from '@tbd54566975/dwn-sdk-js'; import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js'; @@ -22,14 +34,11 @@ describe('web5 connect', function () { let clientSigningPortableDid: PortableDid; let clientEcdhBearerDid: BearerDid; - let clientEcdhPortableDid: PortableDid; let clientEcdhDidAsResolvedByWallet: DidResolutionResult; let providerSigningBearerDid: BearerDid; - let providerSigningPortableDid: PortableDid; let providerEcdhBearerDid: BearerDid; - let providerEcdhPortableDid: PortableDid; /** The real tenant (identity) of the DWN that the provider had chosen to connect */ let providerIdentity: BearerIdentity; @@ -181,7 +190,11 @@ describe('web5 connect', function () { const encryptionNonce = CryptoUtils.randomBytes(24); const randomPin = '9999'; + let clock: sinon.SinonFakeTimers; + before(async () => { + clock = sinon.useFakeTimers(new Date('2050-01-01T06:36:37.675Z').getTime()); + providerIdentityBearerDid = await DidDht.import({ portableDid: providerIdentityPortableDid, }); @@ -211,6 +224,7 @@ describe('web5 connect', function () { }); after(async () => { + clock.restore(); sinon.restore(); await testHarness.clearStorage(); await testHarness.closeStorage(); @@ -267,7 +281,7 @@ describe('web5 connect', function () { authRequestJwe = await Oidc.encryptAuthRequest({ jwt : authRequestJwt, encryptionKey : authRequestEncryptionKey, - kid : clientEcdhBearerDid.document.verificationMethod![0].id + kid : clientEcdhBearerDid.document.verificationMethod![0].id, }); expect(authRequestJwe).to.be.a('string'); expect(authRequestJwe.split('.')).to.have.lengthOf(5); @@ -298,7 +312,9 @@ describe('web5 connect', function () { Convert.uint8Array(authRequestEncryptionKey).toBase64Url() ); expect(result.authRequest).to.deep.equal(authRequest); - expect(result.clientEcdhDid.didDocument?.id).to.equal(clientEcdhBearerDid.uri); + expect(result.clientEcdhDid.didDocument?.id).to.equal( + clientEcdhBearerDid.uri + ); clientEcdhDidAsResolvedByWallet = result.clientEcdhDid; }); @@ -380,7 +396,9 @@ describe('web5 connect', function () { }); it('should send the encrypted jwe authresponse to the server', async () => { - sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon + .stub(Oidc, 'createPermissionGrants') + .resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); const didJwkStub = sinon.stub(DidJwk, 'create'); @@ -522,7 +540,9 @@ describe('web5 connect', function () { // the wallet should not attempt to re-configure, but instead ensure that the protocol is // sent to the remote DWN for the requesting client to be able to sync it down later - sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon + .stub(Oidc, 'createPermissionGrants') + .resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); @@ -546,14 +566,22 @@ describe('web5 connect', function () { const protocolMessage = {} as DwnMessage[DwnInterface.ProtocolsConfigure]; // spy send request - const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ - messageCid : '', - reply : { status: { code: 202, detail: 'OK' } } - }); + const sendRequestSpy = sinon + .stub(testHarness.agent, 'sendDwnRequest') + .resolves({ + messageCid : '', + reply : { status: { code: 202, detail: 'OK' } }, + }); const processDwnRequestStub = sinon .stub(testHarness.agent, 'processDwnRequest') - .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ protocolMessage ]} }); + .resolves({ + messageCid : '', + reply : { + status : { code: 200, detail: 'OK' }, + entries : [protocolMessage], + }, + }); // call submitAuthResponse await Oidc.submitAuthResponse( @@ -566,11 +594,15 @@ describe('web5 connect', function () { // expect the process request to only be called once for ProtocolsQuery expect(processDwnRequestStub.callCount).to.equal(1); - expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsQuery); + expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal( + DwnInterface.ProtocolsQuery + ); // send request should be called once as a ProtocolsConfigure expect(sendRequestSpy.callCount).to.equal(1); - expect(sendRequestSpy.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); + expect(sendRequestSpy.firstCall.args[0].messageType).to.equal( + DwnInterface.ProtocolsConfigure + ); }); it('should configure the protocol if it does not exist', async () => { @@ -579,7 +611,9 @@ describe('web5 connect', function () { // looks for a response of 404, empty entries array or missing entries array - sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon + .stub(Oidc, 'createPermissionGrants') + .resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); @@ -600,14 +634,19 @@ describe('web5 connect', function () { authRequest = await Oidc.createAuthRequest(options); // spy send request - const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ - messageCid : '', - reply : { status: { code: 202, detail: 'OK' } } - }); + const sendRequestSpy = sinon + .stub(testHarness.agent, 'sendDwnRequest') + .resolves({ + messageCid : '', + reply : { status: { code: 202, detail: 'OK' } }, + }); const processDwnRequestStub = sinon .stub(testHarness.agent, 'processDwnRequest') - .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ ] } }); + .resolves({ + messageCid : '', + reply : { status: { code: 200, detail: 'OK' }, entries: [] }, + }); // call submitAuthResponse await Oidc.submitAuthResponse( @@ -620,19 +659,28 @@ describe('web5 connect', function () { // expect the process request to be called for query and configure expect(processDwnRequestStub.callCount).to.equal(2); - expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsQuery); - expect(processDwnRequestStub.secondCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); + expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal( + DwnInterface.ProtocolsQuery + ); + expect(processDwnRequestStub.secondCall.args[0].messageType).to.equal( + DwnInterface.ProtocolsConfigure + ); // send request should be called once as a ProtocolsConfigure expect(sendRequestSpy.callCount).to.equal(1); - expect(sendRequestSpy.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); + expect(sendRequestSpy.firstCall.args[0].messageType).to.equal( + DwnInterface.ProtocolsConfigure + ); // reset the spys processDwnRequestStub.resetHistory(); sendRequestSpy.resetHistory(); // processDwnRequestStub should resolve a 200 with no entires - processDwnRequestStub.resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' } } }); + processDwnRequestStub.resolves({ + messageCid : '', + reply : { status: { code: 200, detail: 'OK' } }, + }); // call submitAuthResponse await Oidc.submitAuthResponse( @@ -645,16 +693,24 @@ describe('web5 connect', function () { // expect the process request to be called for query and configure expect(processDwnRequestStub.callCount).to.equal(2); - expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsQuery); - expect(processDwnRequestStub.secondCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); + expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal( + DwnInterface.ProtocolsQuery + ); + expect(processDwnRequestStub.secondCall.args[0].messageType).to.equal( + DwnInterface.ProtocolsConfigure + ); // send request should be called once as a ProtocolsConfigure expect(sendRequestSpy.callCount).to.equal(1); - expect(sendRequestSpy.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); + expect(sendRequestSpy.firstCall.args[0].messageType).to.equal( + DwnInterface.ProtocolsConfigure + ); }); it('should fail if the send request fails for newly configured protocol', async () => { - sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon + .stub(Oidc, 'createPermissionGrants') + .resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); @@ -675,15 +731,20 @@ describe('web5 connect', function () { authRequest = await Oidc.createAuthRequest(options); // spy send request - const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ - reply : { status: { code: 500, detail: 'Internal Server Error' } }, - messageCid : '' - }); + const sendRequestSpy = sinon + .stub(testHarness.agent, 'sendDwnRequest') + .resolves({ + reply : { status: { code: 500, detail: 'Internal Server Error' } }, + messageCid : '', + }); // return without any entries const processDwnRequestStub = sinon .stub(testHarness.agent, 'processDwnRequest') - .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' } } }); + .resolves({ + messageCid : '', + reply : { status: { code: 200, detail: 'OK' } }, + }); try { // call submitAuthResponse @@ -697,13 +758,17 @@ describe('web5 connect', function () { expect.fail('should have thrown an error'); } catch (error: any) { - expect(error.message).to.equal('Could not send protocol: Internal Server Error'); + expect(error.message).to.equal( + 'Could not send protocol: Internal Server Error' + ); expect(sendRequestSpy.callCount).to.equal(1); } }); it('should fail if the send request fails for existing protocol', async () => { - sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon + .stub(Oidc, 'createPermissionGrants') + .resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); @@ -727,15 +792,23 @@ describe('web5 connect', function () { const protocolMessage = {} as DwnMessage[DwnInterface.ProtocolsConfigure]; // spy send request - const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ - reply : { status: { code: 500, detail: 'Internal Server Error' } }, - messageCid : '' - }); + const sendRequestSpy = sinon + .stub(testHarness.agent, 'sendDwnRequest') + .resolves({ + reply : { status: { code: 500, detail: 'Internal Server Error' } }, + messageCid : '', + }); // mock returning the protocol entry const processDwnRequestStub = sinon .stub(testHarness.agent, 'processDwnRequest') - .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ protocolMessage ] } }); + .resolves({ + messageCid : '', + reply : { + status : { code: 200, detail: 'OK' }, + entries : [protocolMessage], + }, + }); try { // call submitAuthResponse @@ -749,14 +822,18 @@ describe('web5 connect', function () { expect.fail('should have thrown an error'); } catch (error: any) { - expect(error.message).to.equal('Could not send protocol: Internal Server Error'); + expect(error.message).to.equal( + 'Could not send protocol: Internal Server Error' + ); expect(processDwnRequestStub.callCount).to.equal(1); expect(sendRequestSpy.callCount).to.equal(1); } }); it('should throw if protocol could not be fetched at all', async () => { - sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon + .stub(Oidc, 'createPermissionGrants') + .resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); @@ -777,15 +854,20 @@ describe('web5 connect', function () { authRequest = await Oidc.createAuthRequest(options); // spy send request - const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ - reply : { status: { code: 500, detail: 'Internal Server Error' } }, - messageCid : '' - }); + const sendRequestSpy = sinon + .stub(testHarness.agent, 'sendDwnRequest') + .resolves({ + reply : { status: { code: 500, detail: 'Internal Server Error' } }, + messageCid : '', + }); // mock returning the protocol entry const processDwnRequestStub = sinon .stub(testHarness.agent, 'processDwnRequest') - .resolves({ messageCid: '', reply: { status: { code: 500, detail: 'Some Error'}, } }); + .resolves({ + messageCid : '', + reply : { status: { code: 500, detail: 'Some Error' } }, + }); try { // call submitAuthResponse @@ -806,7 +888,9 @@ describe('web5 connect', function () { }); it('should throw if a grant that is included in the request does not match the protocol definition', async () => { - sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon + .stub(Oidc, 'createPermissionGrants') + .resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); @@ -841,96 +925,189 @@ describe('web5 connect', function () { expect.fail('should have thrown an error'); } catch (error: any) { - expect(error.message).to.equal('All permission scopes must match the protocol uri they are provided with.'); + expect(error.message).to.equal( + 'All permission scopes must match the protocol uri they are provided with.' + ); } }); }); describe('createPermissionRequestForProtocol', () => { it('should add sync permissions to all requests', async () => { - const protocol:DwnProtocolDefinition = { + const protocol: DwnProtocolDefinition = { published : true, protocol : 'https://exmaple.org/protocols/social', types : { note: { schema : 'https://example.org/schemas/note', - dataFormats : [ 'application/json', 'text/plain' ], - } + dataFormats : ['application/json', 'text/plain'], + }, }, structure: { - note: {} - } + note: {}, + }, }; - const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ - definition: protocol, permissions: [] - }); + const permissionRequests = + WalletConnect.createPermissionRequestForProtocol({ + definition : protocol, + permissions : [], + }); expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); expect(permissionRequests.permissionScopes.length).to.equal(4); // only includes the sync permissions + protocol query permission - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Read)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Query)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Query)).to.not.be.undefined; + expect( + permissionRequests.permissionScopes.find( + (scope) => + scope.interface === DwnInterfaceName.Messages && + scope.method === DwnMethodName.Read + ) + ).to.not.be.undefined; + expect( + permissionRequests.permissionScopes.find( + (scope) => + scope.interface === DwnInterfaceName.Messages && + scope.method === DwnMethodName.Query + ) + ).to.not.be.undefined; + expect( + permissionRequests.permissionScopes.find( + (scope) => + scope.interface === DwnInterfaceName.Messages && + scope.method === DwnMethodName.Subscribe + ) + ).to.not.be.undefined; + expect( + permissionRequests.permissionScopes.find( + (scope) => + scope.interface === DwnInterfaceName.Protocols && + scope.method === DwnMethodName.Query + ) + ).to.not.be.undefined; }); it('should add requested permissions to the request', async () => { - const protocol:DwnProtocolDefinition = { + const protocol: DwnProtocolDefinition = { published : true, protocol : 'https://exmaple.org/protocols/social', types : { note: { schema : 'https://example.org/schemas/note', - dataFormats : [ 'application/json', 'text/plain' ], - } + dataFormats : ['application/json', 'text/plain'], + }, }, structure: { - note: {} - } + note: {}, + }, }; - const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ - definition: protocol, permissions: ['write', 'read'] - }); + const permissionRequests = + WalletConnect.createPermissionRequestForProtocol({ + definition : protocol, + permissions : ['write', 'read'], + }); expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); // the 3 sync permissions plus the 2 requested permissions, and a protocol query permission expect(permissionRequests.permissionScopes.length).to.equal(6); - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined; + expect( + permissionRequests.permissionScopes.find( + (scope) => + scope.interface === DwnInterfaceName.Records && + scope.method === DwnMethodName.Read + ) + ).to.not.be.undefined; + expect( + permissionRequests.permissionScopes.find( + (scope) => + scope.interface === DwnInterfaceName.Records && + scope.method === DwnMethodName.Write + ) + ).to.not.be.undefined; }); it('supports requesting `read`, `write`, `delete`, `query`, `subscribe` and `configure` permissions', async () => { - const protocol:DwnProtocolDefinition = { + const protocol: DwnProtocolDefinition = { published : true, protocol : 'https://exmaple.org/protocols/social', types : { note: { schema : 'https://example.org/schemas/note', - dataFormats : [ 'application/json', 'text/plain' ], - } + dataFormats : ['application/json', 'text/plain'], + }, }, structure: { - note: {} - } + note: {}, + }, }; - const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ - definition: protocol, permissions: ['write', 'read', 'delete', 'query', 'subscribe', 'configure'] - }); + const permissionRequests = + WalletConnect.createPermissionRequestForProtocol({ + definition : protocol, + permissions : [ + 'write', + 'read', + 'delete', + 'query', + 'subscribe', + 'configure', + ], + }); expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); // the 3 sync permissions plus the 5 requested permissions expect(permissionRequests.permissionScopes.length).to.equal(10); - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Delete)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Query)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Query)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure)).to.not.be.undefined; + expect( + permissionRequests.permissionScopes.find( + (scope) => + scope.interface === DwnInterfaceName.Records && + scope.method === DwnMethodName.Read + ) + ).to.not.be.undefined; + expect( + permissionRequests.permissionScopes.find( + (scope) => + scope.interface === DwnInterfaceName.Records && + scope.method === DwnMethodName.Write + ) + ).to.not.be.undefined; + expect( + permissionRequests.permissionScopes.find( + (scope) => + scope.interface === DwnInterfaceName.Records && + scope.method === DwnMethodName.Delete + ) + ).to.not.be.undefined; + expect( + permissionRequests.permissionScopes.find( + (scope) => + scope.interface === DwnInterfaceName.Records && + scope.method === DwnMethodName.Query + ) + ).to.not.be.undefined; + expect( + permissionRequests.permissionScopes.find( + (scope) => + scope.interface === DwnInterfaceName.Records && + scope.method === DwnMethodName.Subscribe + ) + ).to.not.be.undefined; + expect( + permissionRequests.permissionScopes.find( + (scope) => + scope.interface === DwnInterfaceName.Protocols && + scope.method === DwnMethodName.Query + ) + ).to.not.be.undefined; + expect( + permissionRequests.permissionScopes.find( + (scope) => + scope.interface === DwnInterfaceName.Protocols && + scope.method === DwnMethodName.Configure + ) + ).to.not.be.undefined; }); }); }); diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index cd7e6f1ce..2d3ffd2a3 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -32,7 +32,7 @@ export type TechPreviewOptions = { export type DidCreateOptions = { /** Override default dwnEndpoints provided during DID creation. */ dwnEndpoints?: string[]; -} +}; /** Optional overrides that can be provided when calling {@link Web5.connect}. */ export type Web5ConnectOptions = { @@ -126,13 +126,13 @@ export type Web5ConnectOptions = { * If registration fails, the `onFailure` callback will be called with the error. * If registration is successful, the `onSuccess` callback will be called. */ - registration? : { + registration?: { /** Called when all of the DWN registrations are successful */ onSuccess: () => void; /** Called when any of the DWN registrations fail */ onFailure: (error: any) => void; - } -} + }; +}; /** * Represents the result of the Web5 connection process, including the Web5 instance, @@ -241,20 +241,25 @@ export class Web5 { password = 'insecure-static-phrase'; console.warn( '%cSECURITY WARNING:%c ' + - 'You have not set a password, which defaults to a static, guessable value. ' + - 'This significantly compromises the security of your data. ' + - 'Please configure a secure, unique password.', + 'You have not set a password, which defaults to a static, guessable value. ' + + 'This significantly compromises the security of your data. ' + + 'Please configure a secure, unique password.', 'font-weight: bold; color: red;', 'font-weight: normal; color: inherit;' ); } // Use the specified DWN endpoints or the latest TBD hosted DWN - const serviceEndpointNodes = techPreview?.dwnEndpoints ?? didCreateOptions?.dwnEndpoints ?? ['https://dwn.tbddev.org/beta']; + const serviceEndpointNodes = techPreview?.dwnEndpoints ?? + didCreateOptions?.dwnEndpoints ?? ['https://dwn.tbddev.org/beta']; // Initialize, if necessary, and start the agent. if (await userAgent.firstLaunch()) { - recoveryPhrase = await userAgent.initialize({ password, recoveryPhrase, dwnEndpoints: serviceEndpointNodes }); + recoveryPhrase = await userAgent.initialize({ + password, + recoveryPhrase, + dwnEndpoints: serviceEndpointNodes, + }); } await userAgent.start({ password }); @@ -263,8 +268,10 @@ export class Web5 { let identity: BearerIdentity; let connectedProtocols: string[] = []; - const isWalletConnect = walletConnectOptions && !walletConnectOptions.exported; - const isWalletExportedConnect = walletConnectOptions && walletConnectOptions.exported; + const isWalletConnect = + walletConnectOptions && !walletConnectOptions.exported; + const isWalletExportedConnect = + walletConnectOptions && walletConnectOptions.exported; if (connectedIdentity) { // TODO: In the future, implement a way to re-connect an already connected identity and apply additional grants/protocols @@ -280,32 +287,41 @@ export class Web5 { registerSync = true; try { - const { delegatePortableDid, connectedDid, delegateGrants } = await WalletConnect.initClient(walletConnectOptions); + const { delegatePortableDid, connectedDid, delegateGrants } = + await WalletConnect.initClient(walletConnectOptions); // Import the delegated DID as an Identity in the User Agent. // Setting the connectedDID in the metadata applies a relationship between the signer identity and the one it is impersonating. - identity = await userAgent.identity.import({ portableIdentity: { - portableDid : delegatePortableDid, - metadata : { - connectedDid, - name : 'Default', - uri : delegatePortableDid.uri, - tenant : agent.agentDid.uri, - } - }}); + identity = await userAgent.identity.import({ + portableIdentity: { + portableDid : delegatePortableDid, + metadata : { + connectedDid, + name : 'Default', + uri : delegatePortableDid.uri, + tenant : agent.agentDid.uri, + }, + }, + }); // Attempts to process the connected grants to be used by the delegateDID // If the process fails, we want to clean up the identity // the connected grants will return a de-duped array of protocol URIs that are used to register sync for those protocols - connectedProtocols = await this.processConnectedGrants({ agent, delegateDid: delegatePortableDid.uri, grants: delegateGrants }); - } catch (error:any) { + connectedProtocols = await this.processConnectedGrants({ + agent, + delegateDid : delegatePortableDid.uri, + grants : delegateGrants, + }); + } catch (error: any) { // clean up the DID and Identity if import fails and throw // TODO: Implement the ability to purge all of our messages as a tenant await this.cleanUpIdentity({ identity, userAgent }); throw new Error(`Failed to connect to wallet: ${error.message}`); } } else if (isWalletExportedConnect) { - throw new Error('Exported connect will be implemented in a separate PR'); + throw new Error( + 'Exported connect will be implemented in a separate PR' + ); } else { // No connected identity found and no connectOptions provided, use local Identities // Query the Agent's DWN tenant for identity records. @@ -329,23 +345,22 @@ export class Web5 { serviceEndpoint : serviceEndpointNodes, enc : '#enc', sig : '#sig', - } + }, ], verificationMethods: [ { algorithm : 'Ed25519', id : 'sig', - purposes : ['assertionMethod', 'authentication'] + purposes : ['assertionMethod', 'authentication'], }, { algorithm : 'secp256k1', id : 'enc', - purposes : ['keyAgreement'] - } - ] - } + purposes : ['keyAgreement'], + }, + ], + }, }); - } else { // If multiple identities are found, use the first one. // TODO: Implement selecting a connectedDid from multiple identities @@ -356,7 +371,9 @@ export class Web5 { // If the stored identity has a connected DID, use it as the connected DID, otherwise use the identity's DID. connectedDid = identity.metadata.connectedDid ?? identity.did.uri; // If the stored identity has a connected DID, use the identity DID as the delegated DID, otherwise it is undefined. - delegateDid = identity.metadata.connectedDid ? identity.did.uri : undefined; + delegateDid = identity.metadata.connectedDid + ? identity.did.uri + : undefined; if (registration !== undefined) { // If a registration object is passed, we attempt to register the AgentDID and the ConnectedDID with the DWN endpoints provided try { @@ -377,7 +394,7 @@ export class Web5 { // If no failures occurred, call the onSuccess callback registration.onSuccess(); - } catch(error) { + } catch (error) { // for any failure, call the onFailure callback with the error registration.onFailure(error); } @@ -393,8 +410,8 @@ export class Web5 { did : connectedDid, options : { delegateDid, - protocols: connectedProtocols - } + protocols: connectedProtocols, + }, }); if (walletConnectOptions !== undefined) { @@ -405,10 +422,9 @@ export class Web5 { // Enable sync using the specified interval or default. sync ??= '2m'; - userAgent.sync.startSync({ interval: sync }) - .catch((error: any) => { - console.error(`Sync failed: ${error}`); - }); + userAgent.sync.startSync({ interval: sync }).catch((error: any) => { + console.error(`Sync failed: ${error}`); + }); } } @@ -421,9 +437,12 @@ export class Web5 { * Cleans up the DID, Keys and Identity. Primarily used by a failed WalletConnect import. * Does not throw on error, but logs to console. */ - private static async cleanUpIdentity({ identity, userAgent }:{ - identity: BearerIdentity, - userAgent: Web5UserAgent + private static async cleanUpIdentity({ + identity, + userAgent, + }: { + identity: BearerIdentity; + userAgent: Web5UserAgent; }): Promise { try { // Delete the DID and the Associated Keys @@ -432,15 +451,19 @@ export class Web5 { tenant : identity.metadata.tenant, deleteKey : true, }); - } catch(error: any) { - console.error(`Failed to delete DID ${identity.did.uri}: ${error.message}`); + } catch (error: any) { + console.error( + `Failed to delete DID ${identity.did.uri}: ${error.message}` + ); } try { // Delete the Identity await userAgent.identity.delete({ didUri: identity.did.uri }); - } catch(error: any) { - console.error(`Failed to delete Identity ${identity.metadata.name}: ${error.message}`); + } catch (error: any) { + console.error( + `Failed to delete Identity ${identity.metadata.name}: ${error.message}` + ); } } @@ -448,22 +471,34 @@ export class Web5 { * Processes connected grants for a delegate DID. * Stores the grants as the DWN owner to be used later when impersonating the connected DID. */ - static async processConnectedGrants({ grants, agent, delegateDid }: { - grants: DwnDataEncodedRecordsWriteMessage[], - agent: Web5Agent, - delegateDid: string, + static async processConnectedGrants({ + grants, + agent, + delegateDid, + }: { + grants: DwnDataEncodedRecordsWriteMessage[]; + agent: Web5Agent; + delegateDid: string; }): Promise { const connectedProtocols = new Set(); for (const grantMessage of grants) { // use the delegateDid as the connectedDid of the grant as they do not yet support impersonation/delegation - const grant = await PermissionGrant.parse({ connectedDid: delegateDid, agent, message: grantMessage }); + const grant = await PermissionGrant.parse({ + connectedDid : delegateDid, + agent, + message : grantMessage, + }); // store the grant as the owner of the DWN, this will allow the delegateDid to use the grant when impersonating the connectedDid const { status } = await grant.store(true); if (status.code !== 202) { - throw new Error(`AgentDwnApi: Failed to process connected grant: ${status.detail}`); + throw new Error( + `AgentDwnApi: Failed to process connected grant: ${status.detail}` + ); } - const protocol = (grant.scope as DwnMessagesPermissionScope | DwnRecordsPermissionScope).protocol; + const protocol = ( + grant.scope as DwnMessagesPermissionScope | DwnRecordsPermissionScope + ).protocol; if (protocol) { connectedProtocols.add(protocol); }