Skip to content

Commit

Permalink
merge and push
Browse files Browse the repository at this point in the history
  • Loading branch information
bnonni committed Oct 14, 2024
2 parents bd1cb00 + f49de99 commit b857c4f
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 2 deletions.
4 changes: 4 additions & 0 deletions packages/agent/src/sync-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,8 @@ export class AgentSyncApi implements SyncEngine {
public stopSync(timeout?: number): Promise<void> {
return this._syncEngine.stopSync(timeout);
}

public syncOnce(params: {syncDirection: string } = {syncDirection: 'push'}): Promise<void> {
return this._syncEngine.syncOnce(params);
}

Check warning on line 74 in packages/agent/src/sync-api.ts

View check run for this annotation

Codecov / codecov/patch

packages/agent/src/sync-api.ts#L73-L74

Added lines #L73 - L74 were not covered by tests
}
16 changes: 16 additions & 0 deletions packages/agent/src/sync-engine-level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,22 @@ export class SyncEngineLevel implements SyncEngine {
}
}

public syncOnce({ syncDirection }: { syncDirection: string }): Promise<void> {
return new Promise((resolve, reject) => {
if (syncDirection === 'push') {
this.push();
this.pull();
resolve();
} else if (syncDirection === 'pull') {
this.pull();
this.push();
resolve();
} else {
throw new Error('SyncEngineLevel: Invalid sync direction.');
}
});
}

Check warning on line 397 in packages/agent/src/sync-engine-level.ts

View check run for this annotation

Codecov / codecov/patch

packages/agent/src/sync-engine-level.ts#L384-L397

Added lines #L384 - L397 were not covered by tests

/**
* 202: message was successfully written to the remote DWN
* 204: an initial write message was written without any data, cannot yet be read until a subsequent message is written with data
Expand Down
2 changes: 2 additions & 0 deletions packages/agent/src/types/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,6 @@ export interface SyncEngine {
* @throws {Error} if the sync operation fails to stop before the timeout.
*/
stopSync(timeout?: number): Promise<void>;

syncOnce(params: { syncDirection: string }): Promise<void>;
}
23 changes: 22 additions & 1 deletion packages/api/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,27 @@ export function dataToBlob(data: any, dataFormat?: string): {
return { dataBlob, dataFormat };
}

export function validateRecoverParams(params: any): void {
if (!params || typeof params !== 'object') {
throw new Error('Invalid recovery parameters.');
}
if (!params.password || typeof params.password !== 'string') {
throw new Error('Invalid password.');
}
if (!params.recoveryPhrase || typeof params.recoveryPhrase !== 'string') {
throw new Error('Invalid recoveryPhrase.');
}
if (!params.dwnEndpoints || !Array.isArray(params.dwnEndpoints) || params.dwnEndpoints.length === 0) {
throw new Error('Invalid DWN endpoints.');
}
if (params.dids && (!Array.isArray(params.dids) || params.dids.length === 0)) {
throw new Error('Invalid DIDs.');
}
if (params.sync && typeof params.sync !== 'string') {
throw new Error('Invalid sync interval.');
}
}

Check warning on line 84 in packages/api/src/utils.ts

View check run for this annotation

Codecov / codecov/patch

packages/api/src/utils.ts#L66-L84

Added lines #L66 - L84 were not covered by tests

/**
* The `SendCache` class provides a static caching mechanism to optimize the process of sending
* records to remote DWN targets by minimizing redundant sends.
Expand Down Expand Up @@ -125,4 +146,4 @@ export class SendCache {
targetCache.delete(firstTarget);
}
}
}
}
161 changes: 160 additions & 1 deletion packages/api/src/web5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
/// <reference types="@tbd54566975/dwn-sdk-js" />

import type {
import {
BearerIdentity,
DwnDataEncodedRecordsWriteMessage,
DwnMessagesPermissionScope,
Expand All @@ -23,6 +23,8 @@ import { DidApi } from './did-api.js';
import { DwnApi } from './dwn-api.js';
import { VcApi } from './vc-api.js';
import { PermissionGrant } from './permission-grant.js';
import { validateRecoverParams } from './utils.js';
import { DwnError } from '@tbd54566975/dwn-sdk-js';

/** Override defaults configured during the technical preview phase. */
export type TechPreviewOptions = {
Expand Down Expand Up @@ -212,6 +214,51 @@ export type Web5Params = {
delegateDid?: string;
};

export type Web5RecoverParams = {
/**
* The Web5 app `password` is used to protect data on the device the application is running on.
*
* See {@link Web5ConnectOptions.password} for more information.
*/
password: string;

/**
* The `recoveryPhrase` is a unique, secure key for recovering the identity vault.
*
* See {@link Web5ConnectOptions.recoveryPhrase} for more information.
*/
recoveryPhrase: string;

/**
* The dwnEndpoints are used to register DWN service endpoints to the agent DID allowing for full
* recovery of the agent did and its associated connected dids from the listed DWN(s).
* Also allows for the use of the agentDid as the connectedDid. E.g., server side applications
* that do not need the agentDid/connectedDid(s) pattern.
*
* See {@link TechPreviewOptions.dwnEndpoints} and {@link HdIdentityVaultInitializeParams} for more information.
*/
dwnEndpoints: string[];

/**
* The connected DID(s) to recover from the DWN. Can be 1 or more.
* Leave blank to recover all DIDs from the DWN.
*/
dids?: string[];

/**
*
* Passing a sync interval will engage the sync process after recovery similar to the connect process.
*
* Enable synchronization of DWN records between local and remote DWNs.
* Sync defaults to running every 2 minutes and can be set to any value accepted by `ms()`.
* To disable sync, leave undefined or set to 'off'. Turning sync off will allow the recover function to complete
* and return after recovering the Web5 data.
*
* See {@link Web5ConnectOptions.sync} for more information.
*/
sync?: string;
};

/**
* The main Web5 API interface. It manages the creation of a DID if needed, the connection to the
* local DWN and all the web5 main foundational APIs such as VC, syncing, etc.
Expand Down Expand Up @@ -513,4 +560,116 @@ export class Web5 {
// we expect that any connected protocols will include MessagesQuery and MessagesRead grants that will allow it to sync
return [...connectedProtocols];
}

/**
*
* Recovers a {@link Web5Agent} from a recovery phrase and connects it to the local DWN.
*
* @param params - Params used to recover the agent and connect to the local DWN.
* @param params.password - The Web5 app `password` used to protect data on the device.
* @param params.recoveryPhrase - The `recoveryPhrase` used to recover the identity vault.
* @param params.dwnEndpoints - The DWN service endpoints to register to the agent DID.
* @param params.dids - Optional. The connected DID(s) to recover from the DWN. Can be 1 or more.
* @param params.sync - Optional. Enable synchronization of DWN records between local and remote DWNs.
* @returns A promise that resolves to a {@link Web5} instance.
*/
static async recover({
password, recoveryPhrase, dwnEndpoints, dids, sync
}: Web5RecoverParams): Promise<Web5ConnectResult> {
// Validate the recovery parameters.
validateRecoverParams({ password, recoveryPhrase, dwnEndpoints, dids, sync });

// Get default tech preview hosted nodes if not provided.
dwnEndpoints ??= ['https://dwn.tbddev.org/beta'];

// Create a new agent.
const agent = await Web5UserAgent.create();
if(!await agent.firstLaunch()) {
throw new Error('recover() failed: agent already initialized');
}

// Initialize the agent with the provided recovery phrase, password and dwnEndpoints.
await agent.initialize({ password, recoveryPhrase, dwnEndpoints });

// Start the agent.
await agent.start({ password });

// Enable sync using the specified interval or default.
await agent.sync.syncOnce({ syncDirection: 'pull' })
.catch((error: any) => {
console.error(`Sync pull failed: ${error}`);
});

// Create a new Web5 instance.
const web5 = new Web5({ agent, connectedDid: agent.agentDid.uri });

// Query the DWN for the agent's connected DID records.
const { records, status, cursor } = await web5.dwn.records.query({
from : agent.agentDid.uri,
message : {
filter: {
protocol: 'https://tbd.website/dwn/permissions',
},
},
});

// Check if the DWN query was successful. Throw an error if it was not.
const { code, detail } = status ?? {};
if((code < 200 || code > 299) || (code < 300 && code > 399)) {
console.error(`DWN query failed: ${code} - ${detail}`);
throw new DwnError(code.toString(), `recover() failed: DWN query for did records unsuccessful: ${detail}`);
}

console.log('status', status);
console.log('cursor', cursor);
console.log('records', records);

// Check if any records were found. Throw an error if none are found.
if(!records || !records?.length) {
throw new Error('recover() failed: no DID records found in the DWN');
}

// Get the agent identities and check if any exist.
const agentIdentities = await agent.identity.list();
if (!agentIdentities.length) {
throw new Error('recover() failed: no identities found in the agent');
}

// Filter through the agent identities to find only the dids provided for recovery.
const matchingIdentities: BearerIdentity[] = agentIdentities.filter(
agentIdentity => dids.find(did => did === agentIdentity.did.uri)
);

// Check if any provided dids match those in the agent identities list.
const matchingIdentitiesLength = matchingIdentities.length;
if (!matchingIdentitiesLength) {
throw new Error('recover() failed: no matching dids found in the agent');
}

// If one matching identity is found, use it as the connected did.
// Else reduce the matching identities down to the identity with the most highest versionId.
const identity: BearerIdentity = matchingIdentitiesLength === 1
? matchingIdentities[0]
: matchingIdentities.reduce(
(prevDid, currDid) => prevDid.did.metadata.versionId > currDid.did.metadata.versionId
? prevDid
: currDid
);

const connectedDid = identity.did.uri;

if (sync && sync !== 'off') {
// First, register the user identity for sync.
await agent.sync.registerIdentity({ did: connectedDid });

// Enable sync using the specified interval or default.
sync ??= '2m';
agent.sync.startSync({ interval: sync })
.catch((error: any) => {
console.error(`Sync failed: ${error}`);
});
}

return { web5, did: connectedDid };
}

Check warning on line 674 in packages/api/src/web5.ts

View check run for this annotation

Codecov / codecov/patch

packages/api/src/web5.ts#L577-L674

Added lines #L577 - L674 were not covered by tests
}

0 comments on commit b857c4f

Please sign in to comment.