diff --git a/.changeset/shiny-students-enjoy.md b/.changeset/shiny-students-enjoy.md new file mode 100644 index 000000000..09ee28a53 --- /dev/null +++ b/.changeset/shiny-students-enjoy.md @@ -0,0 +1,8 @@ +--- +"@web5/agent": minor +"@web5/identity-agent": minor +"@web5/proxy-agent": minor +"@web5/user-agent": minor +--- + +Protocol driven identity, key and did storage diff --git a/packages/agent/src/store-data-protocols.ts b/packages/agent/src/store-data-protocols.ts new file mode 100644 index 000000000..cac873332 --- /dev/null +++ b/packages/agent/src/store-data-protocols.ts @@ -0,0 +1,40 @@ +import type { ProtocolDefinition } from '@tbd54566975/dwn-sdk-js'; + +export const IdentityProtocolDefinition: ProtocolDefinition = { + protocol : 'http://identity.foundation/protocols/web5/identity-store', + published : false, + types : { + portableDid: { + schema : 'https://identity.foundation/schemas/web5/portable-did', + dataFormats : [ + 'application/json' + ] + }, + identityMetadata: { + schema : 'https://identity.foundation/schemas/web5/identity-metadata', + dataFormats : [ + 'application/json' + ] + } + }, + structure: { + portableDid : {}, + identityMetadata : {} + } +}; + +export const JwkProtocolDefinition: ProtocolDefinition = { + protocol : 'http://identity.foundation/protocols/web5/jwk-store', + published : false, + types : { + privateJwk: { + schema : 'https://identity.foundation/schemas/web5/private-jwk', + dataFormats : [ + 'application/json' + ] + }, + }, + structure: { + privateJwk: {} + } +}; \ No newline at end of file diff --git a/packages/agent/src/store-data.ts b/packages/agent/src/store-data.ts index 745c3b5a3..21060e558 100644 --- a/packages/agent/src/store-data.ts +++ b/packages/agent/src/store-data.ts @@ -8,6 +8,7 @@ import type { Web5PlatformAgent } from './types/agent.js'; import { TENANT_SEPARATOR } from './utils-internal.js'; import { getDataStoreTenant } from './utils-internal.js'; import { DwnInterface } from './types/dwn.js'; +import { ProtocolDefinition } from '@tbd54566975/dwn-sdk-js'; export type DataStoreTenantParams = { agent: Web5PlatformAgent; @@ -59,12 +60,22 @@ export class DwnDataStore = Jwk> implem */ protected _index = new TtlCache({ ttl: ms('2 hours'), max: 1000 }); + /** + * Cache of tenant DIDs that have been initialized with the protocol. + * This is used to avoid redundant protocol initialization requests. + */ + protected _protocolInitializedCache: TtlCache = new TtlCache({ ttl: ms('1 hour'), max: 1000 }); + + /** + * The protocol assigned to this storage instance. + */ + protected _recordProtocolDefinition!: ProtocolDefinition; + /** * Properties to use when writing and querying records with the DWN store. */ protected _recordProperties = { - dataFormat : 'application/json', - schema : 'https://identity.foundation/schemas/web5/private-jwk' + dataFormat: 'application/json', }; public async delete({ id, agent, tenant }: DataStoreDeleteParams): Promise { @@ -128,6 +139,9 @@ export class DwnDataStore = Jwk> implem // Determine the tenant identifier (DID) for the set operation. const tenantDid = await getDataStoreTenant({ agent, tenant, didUri: id }); + // initialize the storage protocol if not already done + await this.initialize({ tenant: tenantDid, agent }); + // If enabled, check if a record with the given `id` is already present in the store. if (preventDuplicates) { // Look up the DWN record ID of the object in the store with the given `id`. @@ -163,6 +177,39 @@ export class DwnDataStore = Jwk> implem } } + /** + * Initialize the relevant protocol for the given tenant. + * This confirms that the storage protocol is configured, otherwise it will be installed. + */ + public async initialize({ tenant, agent }: DataStoreTenantParams) { + const tenantDid = await getDataStoreTenant({ agent, tenant }); + if (this._protocolInitializedCache.has(tenantDid)) { + return; + } + + const { reply: { status, entries }} = await agent.dwn.processRequest({ + author : tenantDid, + target : tenantDid, + messageType : DwnInterface.ProtocolsQuery, + messageParams : { + filter: { + protocol: this._recordProtocolDefinition.protocol + } + }, + }); + + if (status.code !== 200) { + throw new Error(`Failed to query for protocols: ${status.code} - ${status.detail}`); + } + + if (entries?.length === 0) { + // protocol is not installed, install it + await this.installProtocol(tenantDid, agent); + } + + this._protocolInitializedCache.set(tenantDid, true); + } + protected async getAllRecords(_params: { agent: Web5PlatformAgent; tenantDid: string; @@ -207,6 +254,24 @@ export class DwnDataStore = Jwk> implem return storeObject; } + /** + * Install the protocol for the given tenant using a `ProtocolsConfigure` message. + */ + private async installProtocol(tenant: string, agent: Web5PlatformAgent) { + const { reply : { status } } = await agent.dwn.processRequest({ + author : tenant, + target : tenant, + messageType : DwnInterface.ProtocolsConfigure, + messageParams : { + definition: this._recordProtocolDefinition + }, + }); + + if (status.code !== 202) { + throw new Error(`Failed to install protocol: ${status.code} - ${status.detail}`); + } + } + private async lookupRecordId({ id, tenantDid, agent }: { id: string; tenantDid: string; diff --git a/packages/agent/src/store-did.ts b/packages/agent/src/store-did.ts index 04ac82c7b..2d23038d7 100644 --- a/packages/agent/src/store-did.ts +++ b/packages/agent/src/store-did.ts @@ -5,20 +5,25 @@ import { Convert } from '@web5/common'; import type { Web5PlatformAgent } from './types/agent.js'; import type { AgentDataStore, DataStoreDeleteParams, DataStoreGetParams, DataStoreListParams, DataStoreSetParams } from './store-data.js'; -import { TENANT_SEPARATOR } from './utils-internal.js'; import { DwnInterface } from './types/dwn.js'; +import { IdentityProtocolDefinition } from './store-data-protocols.js'; import { isPortableDid } from './prototyping/dids/utils.js'; +import { TENANT_SEPARATOR } from './utils-internal.js'; import { DwnDataStore, InMemoryDataStore } from './store-data.js'; export class DwnDidStore extends DwnDataStore implements AgentDataStore { protected name = 'DwnDidStore'; + protected _recordProtocolDefinition = IdentityProtocolDefinition; + /** * Properties to use when writing and querying DID records with the DWN store. */ protected _recordProperties = { - dataFormat : 'application/json', - schema : 'https://identity.foundation/schemas/web5/portable-did' + dataFormat : 'application/json', + protocol : this._recordProtocolDefinition.protocol, + protocolPath : 'portableDid', + schema : this._recordProtocolDefinition.types.portableDid.schema, }; public async delete(params: DataStoreDeleteParams): Promise { diff --git a/packages/agent/src/store-identity.ts b/packages/agent/src/store-identity.ts index 6ea724f22..8c2daa716 100644 --- a/packages/agent/src/store-identity.ts +++ b/packages/agent/src/store-identity.ts @@ -4,8 +4,9 @@ import type { Web5PlatformAgent } from './types/agent.js'; import type { IdentityMetadata } from './types/identity.js'; import type { AgentDataStore, DataStoreDeleteParams, DataStoreGetParams, DataStoreListParams, DataStoreSetParams } from './store-data.js'; -import { TENANT_SEPARATOR } from './utils-internal.js'; import { DwnInterface } from './types/dwn.js'; +import { IdentityProtocolDefinition } from './store-data-protocols.js'; +import { TENANT_SEPARATOR } from './utils-internal.js'; import { DwnDataStore, InMemoryDataStore } from './store-data.js'; export function isIdentityMetadata(obj: unknown): obj is IdentityMetadata { @@ -17,12 +18,16 @@ export function isIdentityMetadata(obj: unknown): obj is IdentityMetadata { export class DwnIdentityStore extends DwnDataStore implements AgentDataStore { protected name = 'DwnIdentityStore'; + protected _recordProtocolDefinition = IdentityProtocolDefinition; + /** * Properties to use when writing and querying Identity records with the DWN store. */ protected _recordProperties = { - dataFormat : 'application/json', - schema : 'https://identity.foundation/schemas/web5/identity-metadata' + dataFormat : 'application/json', + protocol : this._recordProtocolDefinition.protocol, + protocolPath : 'identityMetadata', + schema : this._recordProtocolDefinition.types.identityMetadata.schema, }; public async delete(params: DataStoreDeleteParams): Promise { diff --git a/packages/agent/src/store-key.ts b/packages/agent/src/store-key.ts index 091ad86b3..b5602b5c2 100644 --- a/packages/agent/src/store-key.ts +++ b/packages/agent/src/store-key.ts @@ -5,19 +5,24 @@ import { Convert } from '@web5/common'; import type { Web5PlatformAgent } from './types/agent.js'; -import { TENANT_SEPARATOR } from './utils-internal.js'; import { DwnInterface } from './types/dwn.js'; +import { JwkProtocolDefinition } from './store-data-protocols.js'; +import { TENANT_SEPARATOR } from './utils-internal.js'; import { AgentDataStore, DataStoreDeleteParams, DataStoreGetParams, DataStoreListParams, DataStoreSetParams, DwnDataStore, InMemoryDataStore } from './store-data.js'; export class DwnKeyStore extends DwnDataStore implements AgentDataStore { protected name = 'DwnKeyStore'; + protected _recordProtocolDefinition = JwkProtocolDefinition; + /** * Properties to use when writing and querying Private Key records with the DWN store. */ protected _recordProperties = { - dataFormat : 'application/json', - schema : 'https://identity.foundation/schemas/web5/private-jwk' + dataFormat : 'application/json', + protocol : this._recordProtocolDefinition.protocol, + protocolPath : 'privateJwk', + schema : this._recordProtocolDefinition.types.privateJwk.schema, }; public async delete(params: DataStoreDeleteParams): Promise { diff --git a/packages/agent/src/test-harness.ts b/packages/agent/src/test-harness.ts index 9d6121903..7249c547f 100644 --- a/packages/agent/src/test-harness.ts +++ b/packages/agent/src/test-harness.ts @@ -35,6 +35,12 @@ type PlatformAgentTestHarnessParams = { dwnResumableTaskStore: ResumableTaskStoreLevel; syncStore: AbstractLevel; vaultStore: KeyValueStore; + dwnStores: { + keyStore: DwnKeyStore; + identityStore: DwnIdentityStore; + didStore: DwnDidStore; + clear: () => void; + } } type PlatformAgentTestHarnessSetupParams = { @@ -56,6 +62,18 @@ export class PlatformAgentTestHarness { public syncStore: AbstractLevel; public vaultStore: KeyValueStore; + /** + * Custom DWN Stores for `keyStore`, `identityStore` and `didStore`. + * This allows us to clear the store cache between tests + */ + public dwnStores: { + keyStore: DwnKeyStore; + identityStore: DwnIdentityStore; + didStore: DwnDidStore; + /** clears the protocol initialization caches */ + clear: () => void; + }; + constructor(params: PlatformAgentTestHarnessParams) { this.agent = params.agent; this.agentStores = params.agentStores; @@ -67,6 +85,7 @@ export class PlatformAgentTestHarness { this.syncStore = params.syncStore; this.vaultStore = params.vaultStore; this.dwnResumableTaskStore = params.dwnResumableTaskStore; + this.dwnStores = params.dwnStores; } public async clearStorage(): Promise { @@ -170,6 +189,17 @@ export class PlatformAgentTestHarness { // Instantiate Agent's RPC Client. const rpcClient = new Web5RpcClient(); + const dwnStores = { + keyStore : new DwnKeyStore(), + identityStore : new DwnIdentityStore(), + didStore : new DwnDidStore(), + clear : ():void => { + dwnStores.keyStore['_protocolInitializedCache']?.clear(); + dwnStores.identityStore['_protocolInitializedCache']?.clear(); + dwnStores.didStore['_protocolInitializedCache']?.clear(); + } + }; + const { agentVault, didApi, @@ -179,7 +209,7 @@ export class PlatformAgentTestHarness { vaultStore } = (agentStores === 'memory') ? PlatformAgentTestHarness.useMemoryStores() - : PlatformAgentTestHarness.useDiskStores({ testDataLocation }); + : PlatformAgentTestHarness.useDiskStores({ testDataLocation, stores: dwnStores }); // Instantiate custom stores to use with DWN instance. // Note: There is no in-memory store for DWN, so we always use LevelDB-based disk stores. @@ -233,13 +263,19 @@ export class PlatformAgentTestHarness { dwnEventLog, dwnMessageStore, dwnResumableTaskStore, + dwnStores, syncStore, vaultStore }); } - private static useDiskStores({ agent, testDataLocation }: { + private static useDiskStores({ agent, testDataLocation, stores }: { agent?: Web5PlatformAgent; + stores: { + keyStore: DwnKeyStore; + identityStore: DwnIdentityStore; + didStore: DwnDidStore; + } testDataLocation: string; }) { const testDataPath = (path: string) => `${testDataLocation}/${path}`; @@ -247,6 +283,8 @@ export class PlatformAgentTestHarness { const vaultStore = new LevelStore({ location: testDataPath('VAULT_STORE') }); const agentVault = new HdIdentityVault({ keyDerivationWorkFactor: 1, store: vaultStore }); + const { didStore, identityStore, keyStore } = stores; + // Setup DID Resolver Cache const didResolverCache = new DidResolverCacheLevel({ location: testDataPath('DID_RESOLVERCACHE') @@ -256,12 +294,12 @@ export class PlatformAgentTestHarness { agent : agent, didMethods : [DidDht, DidJwk], resolverCache : didResolverCache, - store : new DwnDidStore() + store : didStore }); - const identityApi = new AgentIdentityApi({ agent, store: new DwnIdentityStore() }); + const identityApi = new AgentIdentityApi({ agent, store: identityStore }); - const keyManager = new LocalKeyManager({ agent, keyStore: new DwnKeyStore() }); + const keyManager = new LocalKeyManager({ agent, keyStore: keyStore }); return { agentVault, didApi, didResolverCache, identityApi, keyManager, vaultStore }; } diff --git a/packages/agent/tests/store-data.spec.ts b/packages/agent/tests/store-data.spec.ts index fc1bc21d2..97d706ce7 100644 --- a/packages/agent/tests/store-data.spec.ts +++ b/packages/agent/tests/store-data.spec.ts @@ -8,22 +8,38 @@ import type { AgentDataStore, DataStoreDeleteParams, DataStoreGetParams, DataSto import { AgentDidApi } from '../src/did-api.js'; import { TestAgent } from './utils/test-agent.js'; import { DwnInterface } from '../src/types/dwn.js'; -import { TENANT_SEPARATOR } from '../src/utils-internal.js'; +import { TENANT_SEPARATOR, getDataStoreTenant } from '../src/utils-internal.js'; import { Web5PlatformAgent } from '../src/types/agent.js'; import { isPortableDid } from '../src/prototyping/dids/utils.js'; import { PlatformAgentTestHarness } from '../src/test-harness.js'; import { DwnDataStore, InMemoryDataStore } from '../src/store-data.js'; -import { RecordsDeleteMessage, RecordsWriteMessage } from '@tbd54566975/dwn-sdk-js'; +import { ProtocolDefinition, RecordsDeleteMessage, RecordsWriteMessage } from '@tbd54566975/dwn-sdk-js'; class DwnTestStore extends DwnDataStore implements AgentDataStore { protected name = 'DwnTestStore'; + protected _recordProtocolDefinition: ProtocolDefinition = { + protocol : 'http://example.org/protocols/web5/test-data', + published : false, + types : { + foo: { + schema : 'https://example.org/schemas/web5/foo', + dataFormats : ['application/json'] + } + }, + structure: { + foo: {} + } + }; + /** * Properties to use when writing and querying Test records with the DWN store. */ protected _recordProperties = { - dataFormat : 'application/json', - schema : 'https://identity.foundation/schemas/web5/test-data' + protocol : this._recordProtocolDefinition.protocol, + protocolPath : 'foo', + dataFormat : 'application/json', + schema : this._recordProtocolDefinition.types.foo.schema }; public async delete(params: DataStoreDeleteParams): Promise { @@ -115,11 +131,13 @@ describe('AgentDataStore', () => { }); beforeEach(async () => { + sinon.restore(); await testHarness.clearStorage(); await testHarness.createAgentDid(); }); after(async () => { + sinon.restore(); await testHarness.clearStorage(); await testHarness.closeStorage(); }); @@ -128,6 +146,12 @@ describe('AgentDataStore', () => { it('must implement the getAllRecords() method', async function() { class InvalidStore extends DwnDataStore implements AgentDataStore { protected name = 'InvalidStore'; + protected _recordProtocolDefinition = { + protocol : 'http://example.org/protocols/web5/test-data', + published : false, + types : {}, + structure : {} + }; } try { @@ -148,7 +172,7 @@ describe('AgentDataStore', () => { let testStore: AgentDataStore; beforeEach(async () => { - testStore = new TestStore(); + testStore = new TestStore(); const didApi = new AgentDidApi({ didMethods : [DidJwk], @@ -399,18 +423,22 @@ describe('AgentDataStore', () => { const didBytes = Convert.string(new Array(102400 + 1).join('0')).toUint8Array(); + // since we are writing directly to the dwn we first initialize the storage protocol + await (testStore as DwnDataStore)['initialize']({ agent: testHarness.agent }); + // Store the DID in the DWN. const response = await testHarness.agent.dwn.processRequest({ author : testHarness.agent.agentDid.uri, target : testHarness.agent.agentDid.uri, messageType : DwnInterface.RecordsWrite, messageParams : { - dataFormat : 'application/json', - schema : 'https://identity.foundation/schemas/web5/test-data' + dataFormat : 'application/json', + protocol : 'http://example.org/protocols/web5/test-data', + protocolPath : 'foo', + schema : 'https://example.org/schemas/web5/foo', }, dataStream: new Blob([didBytes], { type: 'application/json' }) }); - expect(response.reply.status.code).to.equal(202); try { @@ -553,6 +581,9 @@ describe('AgentDataStore', () => { // Skip this test for InMemoryTestStore, as it is only relevant for the DWN store. if (TestStore.name === 'InMemoryTestStore') this.skip(); + // since we are writing directly to the dwn we first initialize the storage protocol + await (testStore as DwnDataStore)['initialize']({ agent: testHarness.agent }); + // Stub the DWN API to return a failed response. const dwnApiStub = sinon.stub(testHarness.agent.dwn, 'processRequest').resolves({ messageCid : 'test-cid', @@ -579,6 +610,98 @@ describe('AgentDataStore', () => { dwnApiStub.restore(); } }); + + it('checks that protocol is installed only once', async function() { + // Scenario: The storage protocol should only need to be installed once + // any operations after the first should not attempt to re-install the protocol. + + // Skip this test for InMemoryTestStore, as checking for protocol installation is not + // relevant given that the store is in-memory. + if (TestStore.name === 'InMemoryTestStore') this.skip(); + + // spy on the installProtocol method + const installProtocolSpy = sinon.spy(testStore as any, 'installProtocol'); + + // create and set did1 + let bearerDid1 = await DidJwk.create(); + const portableDid1 = { uri: bearerDid1.uri, document: bearerDid1.document, metadata: bearerDid1.metadata }; + await testStore.set({ id: portableDid1.uri, data: portableDid1, agent: testHarness.agent }); + expect(installProtocolSpy.calledOnce).to.be.true; + + // create and set did2 + let bearerDid2 = await DidJwk.create(); + const portableDid2 = { uri: bearerDid2.uri, document: bearerDid2.document, metadata: bearerDid2.metadata }; + await testStore.set({ id: portableDid2.uri, data: portableDid2, agent: testHarness.agent }); + expect(installProtocolSpy.calledOnce).to.be.true; // still only called once + + // even after clearing cache + (testStore as DwnDataStore)['_protocolInitializedCache']?.clear(); + + // create and set did3 + let bearerDid3 = await DidJwk.create(); + const portableDid3 = { uri: bearerDid3.uri, document: bearerDid3.document, metadata: bearerDid3.metadata }; + await testStore.set({ id: portableDid3.uri, data: portableDid3, agent: testHarness.agent }); + expect(installProtocolSpy.calledOnce).to.be.true; // still only called once + + // all 3 dids should be in the store + const storedDids = await testStore.list({ agent: testHarness.agent }); + expect(storedDids).to.have.length(3); + expect(storedDids.map(d => d.uri)).has.members([portableDid1.uri, portableDid2.uri, portableDid3.uri]); + }); + + it('throws an error if dwn failed during query for protocol installation', async function () { + // Skip this test for InMemoryTestStore, as it is only relevant for the DWN store. + if (TestStore.name === 'InMemoryTestStore') this.skip(); + + // stub `processRequest` to return a code other than 200 + sinon.stub(testHarness.agent.dwn, 'processRequest').resolves({ + messageCid : 'test-cid', + message : {} as RecordsWriteMessage, + reply : { + status: { + code : 500, + detail : 'Internal Server Error' + } + } + }); + + try { + // create and set did + let bearerDid = await DidJwk.create(); + const portableDid = { uri: bearerDid.uri, document: bearerDid.document, metadata: bearerDid.metadata }; + await testStore.set({ id: portableDid.uri, data: portableDid, agent: testHarness.agent }); + expect.fail('Expected an error to be thrown'); + } catch (error: any) { + expect(error.message).to.include('Failed to query for protocols'); + } + }); + + it('throws an error if dwn failed during protocol installation', async function () { + // Skip this test for InMemoryTestStore, as it is only relevant for the DWN store. + if (TestStore.name === 'InMemoryTestStore') this.skip(); + + // stub `processRequest` to return a code other than 200 + sinon.stub(testHarness.agent.dwn, 'processRequest').resolves({ + messageCid : 'test-cid', + message : {} as RecordsWriteMessage, + reply : { + status: { + code : 500, + detail : 'Internal Server Error' + } + } + }); + + try { + const tenantDid = await getDataStoreTenant({ agent: testHarness.agent }); + + // The DWN will return a 500 error when attempting to install the protocol + await (testStore as DwnDataStore)['installProtocol'](tenantDid, testHarness.agent); + expect.fail('Expected an error to be thrown'); + } catch (error: any) { + expect(error.message).to.include('Failed to install protocol: 500 - Internal Server Error'); + } + }); }); }); }); diff --git a/packages/agent/tests/store-did.spec.ts b/packages/agent/tests/store-did.spec.ts index 1b4a46996..a1a7bcd11 100644 --- a/packages/agent/tests/store-did.spec.ts +++ b/packages/agent/tests/store-did.spec.ts @@ -2,13 +2,14 @@ import { expect } from 'chai'; import { Convert } from '@web5/common'; import { DidJwk, PortableDid } from '@web5/dids'; -import type { AgentDataStore } from '../src/store-data.js'; +import type { AgentDataStore, DwnDataStore } from '../src/store-data.js'; import { AgentDidApi } from '../src/did-api.js'; import { TestAgent } from './utils/test-agent.js'; import { DwnInterface } from '../src/types/dwn.js'; import { PlatformAgentTestHarness } from '../src/test-harness.js'; import { DwnDidStore, InMemoryDidStore } from '../src/store-did.js'; +import { IdentityProtocolDefinition } from '../src/store-data-protocols.js'; describe('DidStore', () => { let testHarness: PlatformAgentTestHarness; @@ -210,18 +211,22 @@ describe('DidStore', () => { const didBytes = Convert.string(new Array(102400 + 1).join('0')).toUint8Array(); + // since we are writing directly to the dwn we first initialize the storage protocol + await (didStore as DwnDataStore)['initialize']({ agent: testHarness.agent }); + // Store the DID in the DWN. const response = await testHarness.agent.dwn.processRequest({ author : testHarness.agent.agentDid.uri, target : testHarness.agent.agentDid.uri, messageType : DwnInterface.RecordsWrite, messageParams : { - dataFormat : 'application/json', - schema : 'https://identity.foundation/schemas/web5/portable-did' + dataFormat : 'application/json', + protocol : IdentityProtocolDefinition.protocol, + protocolPath : 'portableDid', + schema : IdentityProtocolDefinition.types.portableDid.schema, }, dataStream: new Blob([didBytes], { type: 'application/json' }) }); - expect(response.reply.status.code).to.equal(202); try { diff --git a/packages/agent/tests/store-identity.spec.ts b/packages/agent/tests/store-identity.spec.ts index f6a3088a7..ddc3b15ff 100644 --- a/packages/agent/tests/store-identity.spec.ts +++ b/packages/agent/tests/store-identity.spec.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { DidJwk } from '@web5/dids'; import { Convert } from '@web5/common'; -import type { AgentDataStore } from '../src/store-data.js'; +import type { AgentDataStore, DwnDataStore } from '../src/store-data.js'; import type { IdentityMetadata } from '../src/types/identity.js'; import { TestAgent } from './utils/test-agent.js'; @@ -10,6 +10,7 @@ import { DwnInterface } from '../src/types/dwn.js'; import { AgentIdentityApi } from '../src/identity-api.js'; import { PlatformAgentTestHarness } from '../src/test-harness.js'; import { DwnIdentityStore, InMemoryIdentityStore } from '../src/store-identity.js'; +import { IdentityProtocolDefinition } from '../src/store-data-protocols.js'; describe('IdentityStore', () => { let testHarness: PlatformAgentTestHarness; @@ -200,6 +201,9 @@ describe('IdentityStore', () => { // regardless of the size of the data. if (IdentityStore.name === 'InMemoryIdentityStore') this.skip(); + // since we are writing directly to the dwn we first initialize the storage protocol + await (identityStore as DwnDataStore)['initialize']({ agent: testHarness.agent }); + const identityBytes = Convert.string(new Array(102400 + 1).join('0')).toUint8Array(); // Store the Identity in the DWN. @@ -208,8 +212,10 @@ describe('IdentityStore', () => { target : testHarness.agent.agentDid.uri, messageType : DwnInterface.RecordsWrite, messageParams : { - dataFormat : 'application/json', - schema : 'https://identity.foundation/schemas/web5/identity-metadata' + dataFormat : 'application/json', + protocol : IdentityProtocolDefinition.protocol, + protocolPath : 'identityMetadata', + schema : IdentityProtocolDefinition.types.identityMetadata.schema, }, dataStream: new Blob([identityBytes], { type: 'application/json' }) }); diff --git a/packages/agent/tests/store-key.spec.ts b/packages/agent/tests/store-key.spec.ts index 1be9accf8..65168940e 100644 --- a/packages/agent/tests/store-key.spec.ts +++ b/packages/agent/tests/store-key.spec.ts @@ -3,13 +3,14 @@ import type { Jwk } from '@web5/crypto'; import { expect } from 'chai'; import { Convert } from '@web5/common'; -import type { AgentDataStore } from '../src/store-data.js'; +import type { AgentDataStore, DwnDataStore } from '../src/store-data.js'; import { TestAgent } from './utils/test-agent.js'; import { DwnInterface } from '../src/types/dwn.js'; import { LocalKeyManager } from '../src/local-key-manager.js'; import { PlatformAgentTestHarness } from '../src/test-harness.js'; import { DwnKeyStore, InMemoryKeyStore } from '../src/store-key.js'; +import { JwkProtocolDefinition } from '../src/store-data-protocols.js'; describe('KeyStore', () => { let testHarness: PlatformAgentTestHarness; @@ -132,14 +133,19 @@ describe('KeyStore', () => { const keyBytes = Convert.string(new Array(102400 + 1).join('0')).toUint8Array(); + // since we are writing directly to the dwn we first initialize the storage protocol + await (keyStore as DwnDataStore)['initialize']({ agent: testHarness.agent }); + // Store the DID in the DWN. const response = await testHarness.agent.dwn.processRequest({ author : testHarness.agent.agentDid.uri, target : testHarness.agent.agentDid.uri, messageType : DwnInterface.RecordsWrite, messageParams : { - dataFormat : 'application/json', - schema : 'https://identity.foundation/schemas/web5/private-jwk' + dataFormat : 'application/json', + protocol : JwkProtocolDefinition.protocol, + protocolPath : 'privateJwk', + schema : JwkProtocolDefinition.types.privateJwk.schema, }, dataStream: new Blob([keyBytes], { type: 'application/json' }) }); diff --git a/packages/agent/tests/sync-engine-level.spec.ts b/packages/agent/tests/sync-engine-level.spec.ts index f50f46e34..22010c6f6 100644 --- a/packages/agent/tests/sync-engine-level.spec.ts +++ b/packages/agent/tests/sync-engine-level.spec.ts @@ -69,12 +69,14 @@ describe('SyncEngineLevel', () => { randomSchema = cryptoUtils.randomUuid(); sinon.restore(); + await syncEngine.clear(); await testHarness.syncStore.clear(); await testHarness.dwnDataStore.clear(); await testHarness.dwnEventLog.clear(); await testHarness.dwnMessageStore.clear(); await testHarness.dwnResumableTaskStore.clear(); + testHarness.dwnStores.clear(); }); after(async () => { @@ -83,6 +85,12 @@ describe('SyncEngineLevel', () => { }); it('syncs multiple messages in both directions', async () => { + // scenario: Alice installs a protocol only on her local DWN and writes some messages associated with it + // Alice installs a protocol only on her remote DWN and writes some messages associated with it + // Alice registers her DID to be synchronized, and kicks off a sync + // The sync should complete and the same records should exist on both remote and local DWNs + + // create 1 local protocol configure const protocolDefinition1: ProtocolDefinition = { published : true, @@ -741,7 +749,7 @@ describe('SyncEngineLevel', () => { }).slow(1200); // Yellow at 600ms, Red at 1200ms. it('synchronizes records for multiple identities from remote DWN to local DWN', async () => { - // Create a second Identity to author the DWN messages. + // Create a second Identity to author the DWN messages. const bob = await testHarness.createIdentity({ name: 'Bob', testDwnUrls }); // Write a test record to Alice's remote DWN. diff --git a/packages/api/.c8rc.json b/packages/api/.c8rc.json index c44b67aba..2e8aafdf3 100644 --- a/packages/api/.c8rc.json +++ b/packages/api/.c8rc.json @@ -8,6 +8,7 @@ "tests/compiled/**/src/**" ], "exclude": [ + "tests/compiled/**/src/web-features.js", "tests/compiled/**/src/index.js", "tests/compiled/**/src/types.js", "tests/compiled/**/src/types/**" @@ -16,4 +17,4 @@ "cobertura", "text" ] -} \ No newline at end of file +}