Skip to content

Commit

Permalink
agent resolver refreshes did store and cache
Browse files Browse the repository at this point in the history
  • Loading branch information
LiranCohen committed Sep 26, 2024
1 parent c92159c commit 8a4ac06
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 13 deletions.
13 changes: 10 additions & 3 deletions packages/agent/src/agent-did-resolver-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,18 @@ export class AgentDidResolverCache extends DidResolverCacheLevel implements DidR
const cachedResult = JSON.parse(str);
if (!this._resolving.has(did) && Date.now() >= cachedResult.ttlMillis) {
this._resolving.set(did, true);
if (this.agent.agentDid.uri === did || 'undefined' !== typeof await this.agent.identity.get({ didUri: did })) {

const storedDid = await this.agent.did.get({ didUri: did, tenant: this.agent.agentDid.uri });
if ('undefined' !== typeof storedDid) {
try {
const result = await this.agent.did.resolve(did);
if (!result.didResolutionMetadata.error) {
this.set(did, result);
if (!result.didResolutionMetadata.error && result.didDocument) {
const portableDid = {
...storedDid,
document : result.didDocument,
metadata : result.didDocumentMetadata,
};
await this.agent.did.update({ portableDid, tenant: this.agent.agentDid.uri });
}
} finally {
this._resolving.delete(did);
Expand Down
56 changes: 55 additions & 1 deletion packages/agent/src/did-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ import type {
DidResolverCache,
} from '@web5/dids';

import { BearerDid, Did, UniversalResolver } from '@web5/dids';
import { BearerDid, Did, DidDht, UniversalResolver } from '@web5/dids';

import type { AgentDataStore } from './store-data.js';
import type { AgentKeyManager } from './types/key-manager.js';
import type { ResponseStatus, Web5PlatformAgent } from './types/agent.js';

import { InMemoryDidStore } from './store-did.js';
import { AgentDidResolverCache } from './agent-did-resolver-cache.js';
import { canonicalize } from '@web5/crypto';

export enum DidInterface {
Create = 'Create',
Expand Down Expand Up @@ -256,6 +257,59 @@ export class AgentDidApi<TKeyManager extends AgentKeyManager = AgentKeyManager>
return verificationMethod;
}

public async update({ tenant, portableDid, publish = true }: {
tenant?: string;
portableDid: PortableDid;
publish?: boolean;
}): Promise<BearerDid> {

// Check if the DID exists in the store.
const existingDid = await this.get({ didUri: portableDid.uri, tenant });
if (!existingDid) {
throw new Error(`AgentDidApi: Could not update, DID not found: ${portableDid.uri}`);
}

// If the document has not changed, abort the update.
if (canonicalize(portableDid.document) === canonicalize(existingDid.document)) {
throw new Error('AgentDidApi: No changes detected, update aborted');
}

// If private keys are present in the PortableDid, import the key material into the Agent's key
// manager. Validate that the key material for every verification method in the DID document is
// present in the key manager.
const bearerDid = await BearerDid.import({ keyManager: this.agent.keyManager, portableDid });

// Only the DID URI, document, and metadata are stored in the Agent's DID store.
const { uri, document, metadata } = bearerDid;
const portableDidWithoutKeys: PortableDid = { uri, document, metadata };

// pre-populate the resolution cache with the document and metadata
await this.cache.set(uri, { didDocument: document, didResolutionMetadata: { }, didDocumentMetadata: metadata });

// Store the DID in the agent's DID store.
// Unless an existing `tenant` is specified, a record that includes the DID's URI, document,
// and metadata will be stored under a new tenant controlled by the imported DID.
await this._store.set({
id : portableDidWithoutKeys.uri,
data : portableDidWithoutKeys,
agent : this.agent,
tenant : tenant ?? portableDidWithoutKeys.uri,
updateExisting : true,
useCache : true
});

if (publish) {
const parsedDid = Did.parse(bearerDid.uri);
// currently only supporting DHT as a publishable method.
// TODO: abstract this into the didMethod class so that other publishable methods can be supported.
if (parsedDid && parsedDid.method === 'dht') {
await DidDht.publish({ did: bearerDid });
}
}

return bearerDid;
}

public async import({ portableDid, tenant }: {
portableDid: PortableDid;
tenant?: string;
Expand Down
31 changes: 25 additions & 6 deletions packages/agent/src/store-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,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 { DwnInterface, DwnMessageParams } from './types/dwn.js';
import { ProtocolDefinition } from '@tbd54566975/dwn-sdk-js';

export type DataStoreTenantParams = {
Expand All @@ -26,6 +26,7 @@ export type DataStoreSetParams<TStoreObject> = DataStoreTenantParams & {
id: string;
data: TStoreObject;
preventDuplicates?: boolean;
updateExisting?: boolean;
useCache?: boolean;
}

Expand Down Expand Up @@ -137,7 +138,7 @@ export class DwnDataStore<TStoreObject extends Record<string, any> = Jwk> implem
return storedRecords;
}

public async set({ id, data, tenant, agent, preventDuplicates = true, useCache = false }:
public async set({ id, data, tenant, agent, preventDuplicates = true, updateExisting = false, useCache = false }:
DataStoreSetParams<TStoreObject>
): Promise<void> {
// Determine the tenant identifier (DID) for the set operation.
Expand All @@ -146,15 +147,26 @@ export class DwnDataStore<TStoreObject extends Record<string, any> = Jwk> implem
// 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) {
const messageParams: DwnMessageParams[DwnInterface.RecordsWrite] = { ...this._recordProperties };

if (updateExisting) {
// Look up the DWN record ID of the object in the store with the given `id`.
const matchingRecordId = await this.lookupRecordId({ id, tenantDid, agent });
if (!matchingRecordId) {
throw new Error(`${this.name}: Update failed due to missing entry for: ${id}`);
}

// set the recordId in the messageParams to update the existing record
messageParams.recordId = matchingRecordId;
} else if (preventDuplicates) {
// Look up the DWN record ID of the object in the store with the given `id`.
const matchingRecordId = await this.lookupRecordId({ id, tenantDid, agent });
if (matchingRecordId) {
throw new Error(`${this.name}: Import failed due to duplicate entry for: ${id}`);
}
}


// Convert the store object to a byte array, which will be the data payload of the DWN record.
const dataBytes = Convert.object(data).toUint8Array();

Expand Down Expand Up @@ -340,12 +352,19 @@ export class InMemoryDataStore<TStoreObject extends Record<string, any> = Jwk> i
return result;
}

public async set({ id, data, tenant, agent, preventDuplicates }: DataStoreSetParams<TStoreObject>): Promise<void> {
public async set({ id, data, tenant, agent, preventDuplicates, updateExisting }: DataStoreSetParams<TStoreObject>): Promise<void> {
// Determine the tenant identifier (DID) for the set operation.
const tenantDid = await getDataStoreTenant({ agent, tenant, didUri: id });

// If enabled, check if a record with the given `id` is already present in the store.
if (preventDuplicates) {
if (updateExisting) {
// Look up the DWN record ID of the object in the store with the given `id`.
if (!this.store.has(`${tenantDid}${TENANT_SEPARATOR}${id}`)) {
throw new Error(`${this.name}: Update failed due to missing entry for: ${id}`);
}

// set the recordId in the messageParams to update the existing record
} else if (preventDuplicates) {
const duplicateFound = this.store.has(`${tenantDid}${TENANT_SEPARATOR}${id}`);
if (duplicateFound) {
throw new Error(`${this.name}: Import failed due to duplicate entry for: ${id}`);
Expand Down
5 changes: 2 additions & 3 deletions packages/agent/tests/agent-did-resolver-cach.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,10 @@ describe('AgentDidResolverCache', () => {
});

it('should not call resolve if the DID is not the agent DID or exists as an identity in the agent', async () => {
const did = await DidJwk.create({});
const did = await DidJwk.create();
const getStub = sinon.stub(resolverCache['cache'], 'get').resolves(JSON.stringify({ ttlMillis: Date.now() - 1000, value: { didDocument: { id: did.uri } } }));
const resolveSpy = sinon.spy(testHarness.agent.did, 'resolve');
const resolveSpy = sinon.spy(testHarness.agent.did, 'resolve').withArgs(did.uri);
const nextTickSpy = sinon.stub(resolverCache['cache'], 'nextTick').resolves();
sinon.stub(testHarness.agent.identity, 'get').resolves(undefined);

await resolverCache.get(did.uri),

Expand Down
52 changes: 52 additions & 0 deletions packages/agent/tests/store-data.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,58 @@ describe('AgentDataStore', () => {
expect(error.message).to.include('Failed to install protocol: 500 - Internal Server Error');
}
});

describe('updateExisting', () => {
it('updates an existing record', async () => {
// Create and import a DID.
let bearerDid = await DidJwk.create();
const importedDid = await testHarness.agent.did.import({
portableDid : await bearerDid.export(),
tenant : testHarness.agent.agentDid.uri
});

const portableDid = await importedDid.export();

// update did document's service
const updatedDid = {
...portableDid,
document: {
...portableDid.document,
service: [{ id: 'test-service', type: 'test-type', serviceEndpoint: 'test-endpoint' }]
}
};

// Update the DID in the store.
await testStore.set({
id : importedDid.uri,
data : updatedDid,
agent : testHarness.agent,
updateExisting : true,
tenant : testHarness.agent.agentDid.uri
});

// Verify the DID is in the store.
const storedDid = await testStore.get({ id: importedDid.uri, agent: testHarness.agent, tenant: testHarness.agent.agentDid.uri });
expect(storedDid!.uri).to.equal(updatedDid.uri);
expect(storedDid!.document).to.deep.equal(updatedDid.document);
});

it('throws an error if the record does not exist', async () => {
const did = await DidJwk.create();
const portableDid = await did.export();
try {
await testStore.set({
id : portableDid.uri,
data : portableDid,
agent : testHarness.agent,
updateExisting : true
});
expect.fail('Expected an error to be thrown');
} catch (error: any) {
expect(error.message).to.include(`${TestStore.name}: Update failed due to missing entry for: ${portableDid.uri}`);
}
});
});
});
});
});
Expand Down

0 comments on commit 8a4ac06

Please sign in to comment.