# Web5 JS Architecture ### Table of Contents - [Current State](#Current-State) - [Proposed Architecture](#Proposed-Architecture) - [Context](#Context) - [Proposal Diagrams](#Proposal-Diagrams) - [App Developer Experience](#App-Developer-Experience) - [SDK Developer Experience](#SDK-Developer-Experience) - [Open Questions](#Open-Questions) --- The current Web5 JS architecture looks something like the following: ![](https://hackmd.io/_uploads/SyuRQhE8h.png) ## Current State ### Connect A developer getting started begins with: ```typescript const { web5, did } = Web5.connect() ``` - The `did` string that is returned is the ION DID of the Web5 User Agent that was connected to. - The `web5` instance that is returned provides `web5.did.*` and `web5.dwn.*` methods, which interface with the `DwnApi` and `DidApi`. - `DwnApi` takes a Web5 Agent and connected DID as input and enables interaction with the Protocols and Records of the Agent's embedded DWN. - `DidApi` takes a DID Resolver and map of DID Method APIs as input and enables creation, update, and resolution of DIDs. ### Web5 Agent The Web5 Agent instance that is provided as input when creating a `Web5` instance is currently a Web5 User Agent which has several capabilities: - `didResolver` - `dwn` - `dwnRpcClient` - `profileManager` - `syncManager` #### `didResolver` An instance of `DidResolver` from the `@tbd54566975/dids` package that provides a single method, `resolve()`, which currently resolves `did:key` and `did:ion` DIDs. Resolved DIDs are cached for 15 minutes. As implemented, the same `DidResolver` instance is used by both the Web5 User Agent and the `DidApi` when accessed via `web5.did.resolve()`. #### `dwn` An instance of `Dwn` from the `@tbd54566975/dwn` package that is "embedded" in the Web5 User Agent. All `web5.dwn.*` method invocations interact with the Agent's embedded DWN using the connected DID as the tenant target. #### `dwnRpcClient` The `@tbd54566975/web5-agent` package defines a `DwnRpc` interface: - `transportProtocols()`: Getter that returns an array of implemented protocol schemes (e.g., `http:`, `https:`) to implementations of `DwnRpc` - `sendDwnRequest()`: Method that takes a `DwnRpcRequest` and returns a `DwnRpcResponse`. The agent's `dwnRpcClient` is an instance of `DwnRpcClient` which implements the `DwnRpc` interface. By default, it is instantiated with a single client, `HttpDwnRpcClient`, which can be used to communicate with `dwn-server` instances via HTTP or HTTPS. #### `profileManager` `ProfileManager` is an interface from the `@tbd54566975/web5-user-agent` package which defines three methods: - `createProfile()` - `getProfile()` - `listProfiles()` `ProfileApi` implements the `ProfileManager` interface, and by default, uses a LevelDB backed store implemented as `ProfileStore`. #### `syncManager` `SyncManager` is an interface from the `@tbd54566975/web5-user-agent` package which defines three methods: - `registerProfile()` - `pull()` - `push()` `SyncApi` implements the `SyncManager` interface and uses a LevelDB backed store that cannot be overriden upon instantiation (or than specifying the store location/path). [🔝 _Back To Top_ ](#Web5-JS-Architecture) ## Proposed Architecture ### Context If we think about Web5 as a platform consisting three major pillars: 1. **identifiers** 1. **credentials** 1. **data stores** Which the Web5 SDK is implementing using: 1. **Decentralized Identifiers** (DID) 1. **Verifiable Credentials** (VC) 1. **Decentralized Web Nodes** (DWN) The following picture emerges: ![](https://hackmd.io/_uploads/BkhRXRZvh.png) with **Networks** enabling the resolution of DIDs and transport of credential exchange and DWeb Messages over network boundaries. [🔝 _Back To Top_ ](#Web5-JS-Architecture) ### Proposal Diagrams Mapping the Web5 SDK components to the diagram: ![](https://hackmd.io/_uploads/B12FlGmD2.png) SDK component organization: ![](https://hackmd.io/_uploads/SyimezQD3.png) [🔝 _Back To Top_ ](#Web5-JS-Architecture) ### App Developer Experience **In-Browser or Desktop User Agent** ```typescript const { web5, did } = Web5.connect(); ``` When no options are passed, `Web5.connect()` will try each of the following, terminating at a given step if an agent connection is successful: 1. Check to see if local AppData is present (i.e., in LocalStorage/IndexedDb), and if so, load the stored app data and attempt to reconnect to the most recently used agent (e.g., desktop agent via `web5://`, local in-browser agent, or a remote agent via a supported protocol, such as `https://` or `wss://`). 1. Initiate **`DID Connect`** / **`Web5 Connect`** to `web5://`. This begins by scanning local ports for a listening agent, and if found, triggering a custom URL handler. The rest of the **`Connect`** protocol is nearly identical for every supported transport medium (i.e., `web5://` vs. `https://` vs. `wss://`). If successfully connected, a Web5 Proxy Agent is instantiated with the connected agent details. 1. If there is no listening agent OR the connection attempt was denied/blocked, then instantiate an in-browser User Agent using any `Web5ConnectOptions` passed in at runtime and a default agent configuration. **Remote Agent** ```typescript // Connect by dereferencing a DID and using a transport client based on // the service endpoint URL. // Examples: https://dwn.yourdomain.com, wss://dwnhostingprovider.com const { web5, did } = Web5.connect({ url: 'did:ion:abcd1234' }); // Connect to a specific http/https URL const { web5, did } = Web5.connect({ url: 'https://dwn.yourdomain.com' }); ``` When a DID or URL are provided as input, `Web5.connect()` will try each of the following, terminating at a given step if an agent connection is successful: 1. Check to see if local AppStorage is present (e.g., LocalStorage/IndexedDb), and if so, load the stored app data, look for the specified DID/URL, and if found, attempt to reconnect to the specified agent endpoint. 1. Initiate **`DID Connect`** / **`Web5 Connect`** using the specified transport protocol. If successfully connected, a Web5 Proxy Agent is instantiated with the connected agent details. 1. If the connection attempt failed (e.g., no or bad response) OR was denied/blocked, then instantiate an in-browser User Agent using any `Web5ConnectOptions` passed in at runtime and a default agent configuration. **Using a Secure `AppDataVault`** To a passphrase protected and encrypted [`AppDataVault`](#Secure-AppDataVault), the developer must initialize and unlock the application data vault and then pass the instance to `Web5.connect()`. ```typescript // Initialize secure AppDataVault. const appData = new AppDataVault(); // Attempt to load saved app data. const appDataKey = 'WEB5_APP_DATA'; if (appData.load(appDataKey)) { // Present UI to end user to enter passphrase. // In future, the passphrase could be protected by fingerprint/face unlock. const passphrase = // Get passphrase. if (!appData.unlock(passphrase)) { // retry and eventually fail after N attempts. } } else { // If no app data was present, or loading failed, initialize defaults. appData.initialize(appDataKey); // Present UI to end user to enter passphrase. const passphrase = // Get passphrase. appData.changePassphrase(null, passphrase); // In future, present UI to optionally enable fingerprint/face unlock. } const { web5 } = Web5.connect({ appData }); ``` [🔝 _Back To Top_ ](#Web5-JS-Architecture) ### SDK Developer Experience #### `Web5.connect()` If the `{ appData }` property of the `options` object is undefined, a default, insecure AppData implementation will be used. ```typescript static async connect(options: Web5ConnectOptions = {}) { // Use specified AppData implementation instance or default. let appData; if (options.appData) { appData = options.appData; } else { appData = new AppData(); // Attempt to load saved app data. const appDataKey = options.appDataKey ?? Web5.APP_DATA_KEY; // If no app data was present, or loading failed, initialize defaults. if (!appData.load(appDataKey)) { appData.initialize(appDataKey); } } if (appData.getStatus() !== appData.STATUS_READY) { throw new Error(`failed to load application data. Status: ${appData.getStatus()}`'`); } let agent; // Attempt DID Connect to specified endpoint or default to local/fallback. agent = Web5ProxyAgent.create({ did : appData.getDid(), auk : appData.getPrivateKey(), agentOptions : options.agentOptions, }); agent.connect(options.url); if (!agent.isConnected()) { agent = Web5UserAgent.create({ did : appData.getDid(), auk : appData.getPrivateKey(), agentOptions : options.agentOptions, }); } const web5 = new Web5({ agent, appData }); return { web5 } ``` [🔝 _Back To Top_ ](#Web5-JS-Architecture) ### AppData Alternative names: - `AppConfig` & `AppConfigVault` - `Insecure` & `Secure`/`Vault` prefix/suffix to drive the point that not securing the appliation configuration/data with encrytion carries risks and should only be done for learning / POC purposes. #### Type Definitions ```typescript interface AppData { /** * Attempts to retrieve the application data associated with the * provided key from storage. It returns a Promise that resolves * to a boolean indicating whether the operation was successful. */ load(key: string): Promise<boolean>; /** * Initializes the AppData object by creating a new `did:key` DID, * setting default configuration options, and saving the data to application * storage under the given key. It returns a Promise that resolves * to a boolean indicating whether the operation was successful. */ initialize(key: string): Promise<boolean>; /** * Attempts to saves the provided configuration under the given key * to application storage. It returns a Promise that resolves to a * boolean indicating whether the operation was successful. */ save(key: string, config: AppData): Promise<boolean>; /** * Returns a promise that resolves to a string, which is the App DID. */ getDid(): Promise<string> /** * Returns a promise that resolves to a CryptoKey object, which * represents the public key associated with the App DID. */ getPublicKey(): Promise<CryptoKey> /** * Returns a promise that resolves to a CryptoKey object, which * represents the private key associated with the App DID. */ getPrivateKey(): Promise<CryptoKey> /** * Returns a promise that resolves to a AppDataStatus object, which * provides information about the current status of the AppData instance. */ getStatus(): Promise<AppDataStatus> /** * Creates a backup of the current state of `AppData` and * returns a Promise that resolves to an `AppDataBackup` object. */ backup(): Promise<AppDataBackup>; /** * Restores `AppData` to the state in the provided `AppDataBackup` * object. It returns a Promise that resolves to a boolean indicating * whether the restore was successful. */ restore(backup: AppDataBackup): Promise<boolean>; } interface AppDataVault extends AppData { /** * Creates an encrypted backup of the current state of `AppData` and * returns a Promise that resolves to an `AppDataBackup` object. */ backup(passphrase: string): Promise<AppDataBackup>; /** * Restores `AppData` to the state in the provided `AppDataBackup` object. * It requires a passphrase to decrypt the backup and returns a Promise that * resolves to a boolean indicating whether the restore was successful. */ restore(backup: AppDataBackup, passphrase: string): Promise<boolean>; /** * Locks the `AppDataVault`, secured by a passphrase * that must be entered to unlock. */ lock(): Promise<void>; /** * Attempts to unlock the `AppDataVault` with the provided * passphrase. It returns a Promise that resolves to a * boolean indicating whether the unlock was successful. */ unlock(passphrase: string): Promise<boolean>; /** * Attempts to change the passphrase of the `AppDataVault`. * It requires the old passphrase for verification and returns * a Promise that resolves to a boolean indicating whether the * passphrase change was successful. */ changePassphrase(oldPassphrase: string, newPassphrase: string): Promise<boolean>; } ``` #### Secure AppDataVault **Key Derivation Overview** ![](https://hackmd.io/_uploads/HyG46uzv2.png) 1. First an Ed25519 key pair is generated that is unique[^1] to the runtime environment of the Web5 application. 1. A `did:key` DID is created from the key pair generated in Step 1. This DID will be subsequently referred to as the _**App DID**_. 1. A _non-secret_ static `info` value is combined with the _non-secret_ App DID `public key` as input to a Hash-based Key Derivation Function (HKDF) to create a new 32-byte `salt`. 1. The `salt` value derived in the prior step and a `passphrase` entered by the end user are used as input to the PBKDF2 algorithm to generate a secret key that will be referred to as your `Account Unlock Key` (AUK). 1. The private key associated with the App DID is then encrypted using the derived AUK using AES-GCM and written to the AppDataVault store. **App Config Data Encryption and Decryption** To encrypt data: - The AUK, a non-secret randomly generated 12-byte initialization vector (IV), and the plaintext data are input to a AES-GCM encrypt() function to produce encrypted ciphertext. To decrypt data: - The AUK, the stored IV, and the encrypted ciphertext are input to a AES-GCM decrypt() function to recover the original plaintext. [^1]: Accessing a DWA (e.g., https://dignal.xyz) in Chrome and Firefox on the same computer would result in each browser generating and using a different key pair. [🔝 _Back To Top_ ](#Web5-JS-Architecture) ## Migration / Recovery ### Association Between Account and App DIDs Each time a Web5 App is accessed on a device, a new App DID is generated. For decentralized web applications (DWA), a new App DID is generated each time the DWA is accessed from a different web browser. To detail how this would work in practice, here is an example: ![](https://hackmd.io/_uploads/rJ8Hm-UP2.png) # Open Questions ### `Web5.connect()` For the `Web5.connect()` sequence, how is it the same or different on mobile in the following scenarios? - Web5 app run from a mobile browser - Native iOS / Android mobile app [🔝 _Back To Top_ ](#Web5-JS-Architecture) ## Web5 VC ### `web5.vc.*` API: Objects: - Verifiable Credentials (create, sign, verify, etc.) - Credential Manifest (create) - Credential Schemas (create - Presentation Exchange (create, ) - Presentation Definition Previous API scaffolding: - `web5.vc.request()` - args: - presentation exchange object - `web5.vc.apply()` - args: - DID-relative URL || Cred Manifest Object - `web5.vc.issue()` - args: - credential object ("@context", status, subject, expirationDate, id, issuanceDate, issuer, type, etc.) - `web5.vc.verify()` - args: - signed credential object ### SDK Internals **Create a Verifiable Credential** Switch from this: ```typescript const vcBuilder = new VerifiableCredentialBuilder(credentialSubject, issuer); vcBuilder.setId('customID'); vcBuilder.addContext(['newContext']); vcBuilder.addType('newType'); vcBuilder.setIssuer({ id: 'customIssuer' }); vcBuilder.setIssuanceDate('2023-12-31T23:59:59Z'); vcBuilder.setExpirationDate('2024-12-31T23:59:59Z'); const vc = vcBuilder.build(); ``` to this: ```typescript const vcApi = new VerifiableCredentialApi(); const v1 = { id: 'customID', context: ['newContext'], type: 'newType', issuer: { id: 'customIssuer' }, issuanceDate: '2023-12-31T23:59:59Z', expirationDate: 'blahblahblah' } vcApi.createCredential(v1) vcApi.createCredential({ ...v1, expirationDate: 'alsdkfjl') ``` #### SDK Internals In `packages/src/credentials`: ```typescript export interface CredentialCreateOptions { issuer: ...; credentialSubject: ...; type?: ...; '@context': ...; issuanceDate: ...; expirationDate?: ...; credentialStatus?: ...; id?: ...; } export class VerifiableCredential { async create(options: CredentialCreateOptions = {}): Promise<VerifiableCredential> { ... return credential; } } ``` In `packages/src/web5`: ```typescript export class VcApi { #agent: Web5Agent; #connectedDid: string; constructor(agent: Web5Agent, connectedDid: string) { this.#agent = agent; this.#connectedDid = connectedDid; } async createCredential<T extends keyof CreateMethodOptions>(type: T, options?: CreateOptions<T>): Promise<VerifiableCredential> { const vcCreator = this.credentialCreators.get(type); if (!vcCreator) { throw new Error(`no creator available for ${type}`); } return vcCreator.create(options); } async createPresentation } ``` [🔝 _Back To Top_ ](#Web5-JS-Architecture)