# Web5 Developer Experience
Connecting to and interacting with a DWeb Node should be simple and consistent across different deployment models:
- Desktop Wallet with embedded DWN
- In browser
- Remotely accessible over HTTP/WS
Install `web5-sdk` with npm or your preferred package manager.
```javascript
npm install @tbd54566975/web5
```
Import the web5 SDK into your application:
```javascript
import { connect, DWeb } from '@tbd54566975/web5';
```
## Typical Use Cases
Web5 apps interact with a Web5 Agent. Web5 Agents provide the following capabilities:
- Key Chain
- Generate Private Keys
- Store Private Keys
- Sign data with Private Keys
- DWN
- Write/Query/Delete Records
- Configure/Query Protocols
When reading/writing records, Web5 apps can interact with:
1. Connected Agents
- Accessible over a transport protocol (e.g., HTTP/S, WS)
- Accessible in memory
3. Remote DWNs
- Accessible over a transport protocol (e.g., HTTP/S, WS)
## Connecting
```javascript
connect({
onRequest(response){
// Display PIN to user, which they will confirm
security_code.textContent = response.pin;
},
onConnected(connection){
// Connection succeeded
const connected_did = connection.did;
// Raise UI notification to inform user
},
onDenied(){
// Connection request was denied
// Raise UI notification to inform user
},
onTimeout(){
// Connection request timed out
// Raise UI notification to inform user
},
onError(e){
// Raise UI notification to inform user
console.error(e);
}
})
```
## Records
#### Record Class
```javascript
class Record {
constructor(){
- No data
- No attestation
- No authorization
- Result of functional calls
}
get id (){}
getter for app props
update(){}
commit(){}
delete(){}
}
^ instanceof
const record = Web5.dwn.records.create()
record.read()
record.update({ published: true })
record.delete()
{
id = dagCBOR(descriptor: {})
}
= record.commit(COMMIT_OPTIONS = { parentCidOfWrite })
const commit2: Commit = commit.commit(COMMIT_OPTIONS = { parentCidOfCommit1 })
or
web5.dwn.records.update(record)
web5.dwn.records.delete(record)
new Request({})
^
fetch(request) vs fetch({})
web5.dwn.records.delete({
message: {
recordId: '2344t6g476rhb76'
}
})
Web5.dwn.update() vs write()
```
## Writing Data
### Plain text
```javascript
const response = await DWeb.records.write({
data: 'foo',
message: {
schema: 'foo/bar',
}
});
```
### JSON
```javascript
const response = await DWeb.records.write({
data: { "testing": 123 },
message: {
schema: 'foo/bar',
}
});
```
### Arbitrary Binary Data
```javascript
const response = await DWeb.records.write({
data: // read in data,
message: {
schema: 'foo/bar',
}
});
```
### Image Data
```javascript
const imageFile = // read in image file
const response = await DWeb.records.write({
data: imageFile,
message: {
schema: 'foo/bar',
dataFormat: 'image/png'
}
});
```
## Queries
```javascript
const dataFormat = 'text/plain';
const dataFormat = 'application/json';
const dataFormat = 'image/png';
```
**Execute the Query**
```javascript
const response = await DWeb.records.query({
message: {
filter: {
schema: 'foo/bar',
dataFormat
}
}
});
```
## Protocols
## To Be Done
### Priority 1
- For tbDEX implement author and recipient in protocols to support bid/ask float
- For other non-tbDEX apps implement participants concept in Protocols done so that we can implement Permissions further down the line
- Implement Sync
### Priority 2
- Implement Blockstore pinning/record retention when deletes occur
- What does it get us?
- Search index upgrade to enable more expressive queries (AND, OR, GT, LT, etc.) and to make it more performant. The search
- Message
### Action Item
- Add a back-pointing has for the last configuration.
- Add a version field
---
## Prototyping
```javascript
const myDIDs = new DID()
authorDID.connect({
onRequest(response){
security_code.textContent = response.pin;
},
onConnected(connection){
connected_did.textContent = connection.did;
alert('Connection succeeded!');
},
onDenied(){
alert('Connection was denied');
},
onTimeout(){
alert('The connection request timed out');
},
onError(e){
console.log(e);
}
})
const targetDID = 'did:ion:abc123';
write(targetDID, {
author: authorDID,
message: { }:
data?: data
})
```
```javascript
Web5.registerDID({
did: 'did:example:123',
connected: false,
endpoint: URI,
sign: [
{ kid: 'key-1', signer: KEY_PAIR --> sign(payload, format) || function(payload, format){} },
{ kid: 'dwn', signer: KEY_PAIR --> sign(payload, format) || function(payload, format){} }
],
encrypt: KEY_PAIR --> encrypt(payload, format) || function(payload, format){},
})
Web5.registerDID({
did: 'did:example:123',
connected: false,
endpoint: URI,
keys: {
'key-1': {
sign: function(payload, format){
yubikey.sign()
}
},
'dwn': {
keypair: KEY_PAIR
},
'key-2': {
keypair: KEY_PAIR,
sign: function(payload, format){
myUnsupportedKeyType.sign(payload, format)
}
},
'key-3': {
encrypt: function(payload, format){
otherHSM.sign(payload, format)
}
}
},
})
Web5.registerDID({
did: 'did:example:123',
endpoint: URI,
keys: {
'key-1': {
keypair: KEY_PAIR
},
},
})
Web5.sign({
did: 'did:example:123',
keyId: 'key-1',
payload: '...',
format: 'jws'
})
Web5.encrypt({
did: 'did:example:123',
payload: '...',
format: 'jws'
})
async function sign(options) {
const did = Web5.lookupDID(options.did);
if (did) {
let sign = did.keys[options.keyId].sign || genericSign
const keypair = did.keys[options.keyId].keypair;
sign(payload, format, keypair);
}
}
```
---
## Encryption
The key provided to the EncryptionInput is always the Public Key of the recipient.
```javascript
const encryptionInput: EncryptionInput = {
algorithm : EncryptionAlgorithm.Aes256Ctr,
initializationVector : dataEncryptionInitializationVector,
key : dataEncryptionKey,
keyEncryptionInputs : [{
algorithm : EncryptionAlgorithm.EciesSecp256k1,
derivationScheme : KeyDerivationScheme.Protocols,
publicKey : alice.keyPair.publicJwk // reusing signing key for encryption purely as a convenience
}]
};
const { message, dataStream } = await TestDataGenerator.generateRecordsWrite({
requester : alice,
protocol,
protocolPath : 'email',
schema : 'email',
data : bobMessageEncryptedBytes,
encryptionInput
});
```
In Web5 JS, by default, we will ensure all 4 keyEncryption Inputs.
Supported options:
```javascript
web5.dwn.records.create(targetDid, {
authorDid,
data: bytes,
encryption: boolean,
message: {
},
});
web5.dwn.records.create(targetDid, {
authorDid,
data: bytes,
encryption: publicKeyJwk,
message: {
},
});
```
---
## Sync
Have sync be enabled by default
pick a sensible minimum interval before sync will run again
expose the sync methods to the developer to enable manual sync triggering
manual sync still respects built in minimum interval
minimum sync interval is NOT configurable when instantiating `new Web5()`
we will by default run sync on a short interval (e.g., 10 seconds) but only pull encodedData
---
### ACTIONS - May 2, 2023
- @csuwildcat to draft code walkthrough and diagram of a basic chat example using encryption
- Flow A: Alice pushes the symmetric key to Bob
- Flow B: Bob pulls the key from Alice
- Ensure ION resolver cache is TTL of 24 hours and update the cached entry and reset the timer in the event resolution succeeds. If resolution fails then maintain the cached entry.
-
---
### Example flow
**(1) [AGENT] Creates DID and Keys**
```javascript
const profile = createProfile();
```
Profile Object:
```javascript
profile = {
did: {
id: 'did:ion:abcd1234:eyasdlkf98ajdsf', // long form for ION
internalId: 'did:ion:abcd1234', // short form
keys: [
{
id: "key-1",
type: "JsonWebKey2020",
purposes: ["authentication"],
keypair: {
publicJwk: {...},
privateJwk: {...}
}
}
],
services: [],
methodData: [
{
operation: "create",
recovery: { "publicJwk": {...}, "privateJwk": {...} },
update: { "publicJwk": {...}, "privateJwk": {...} },
content: {
publicKeys: [
{
id: "key-1",
type: "JsonWebKey2020",
purposes: ["authentication"],
publicKeyJwk: {...}
}
]
}
}
]
},
name: 'Personal',
icon: 'person',
connections: [],
dateCreated: '2023-03-08T11:21:43.701Z'
}
```
**(2) [AGENT] Registers DID with Web5 Handler**
```javascript
Web5.did.register({
did: profile.did.internalId,
connected: true, // Assume Agent is always "connected" to its keystore & embedded DWN
endpoint: 'app://dwn',
keys: ['key-1']
});
```
Under the hood, Web5 lib:
```javascript
const didRegistry = {}; // Global
const did = {
register: async (options) => {
didRegistry[options.did] = {
did: options.did,
connected: options.connected,
endpoint: options.endpoint,
keys: options.keys
}
}
}
```
**(3) [CLIENT APP] Connects to Agent**
```javascript
Web5.connect({
onRequest(response){
document.querySelector('#pin_code_text').textContent = response.pin;
},
onConnected(connection){
alert('Connection succeeded! Connected to DID: ' + connection.did);
},
onDenied(){
alert('Connection was denied');
}
})
```
Under the hood, Web5 lib:
```javascript
...
options?.onConnected?.(connection)
Web5.did.register({
did: connection.did,
connected: true,
endpoint: connection.endpoint, // e.g., http://127.0.0.1:55500/dwn,
});
...
```
**(4) [CLIENT APP] Sends Message to Agent**
First, the client app needs to which DID to use as the:
- `author` DID that will sign the message
- `target` DID that the message will be sent to
A menu could be presented to the app user if there are multiple authors, or if there's only
one known DID registered, then use that. It is expected that the target DID would be
represented in the UI by a human-friendly alias.
Let's start with the simple case where there's only a single registered DID and the author
and target are the same:
```javascript
const response = await Web5.records.write('did:ion:abcd1234', {
author: 'did:ion:abcd1234',
data: 'Hi',
message: {
schema: 'foo/bar',
}
});
```
Under the hood, Web5 lib:
```javascript
// context.author value passed in records.write is 'did:ion:abcd1234'
const authorDID = Web5.did.get(context.author);
if (!author?.keys) {
// Keys NOT available for Author DID.
if (author.connected) {
// Remote agent connected. Transporting message to agent.
return await send(author.endpoint, context);
} else {
return { error: { code: 99, message: 'Local keys not available and remote agent not connected'}};
}
}
```
`Web5.did.get()` method:
```javascript
const did = {
get: async (did) => {
return didRegistry[did] || undefined;
}
}
```
**(5) [AGENT] Receives Message via HTTP from Client App**
Agent then sends the message to the target DID:
```javascript
const { target, author, ...message } = // From the 'DWN-MESSAGE' header value;
const data = // If any, extract data from multipart message
return await Web5.send(target, {
author,
data,
message
});
```
**(6) [AGENT] Processes Send Request**
Agent first looks up the author DID to see if it is known/registered:
```javascript
// context.author value passed in records.write is 'did:ion:abcd1234'
const authorDID = Web5.did.get(context.author);
if (!author?.keys) {
...
}
```
The author DID is registered and keys are available since this Agent has the local keystore and DWN, so the DWeb message is created and signed:
```javascript
context.message = await DWeb.createAndSignMessage(author, context.message, context.data);
```
Next, lookup the target DID next and check to see whether it is "connected" (i.e., managed by this agent).
```javascript
const targetDID = await Web5.did.get(target); // target is 'did:ion:abcd1234'
if (targetDID?.connected) {
// Target DID is managed by Agent
return await send(target.endpoint, context);
} else {
// Target DID is NOT managed by Agent
}
```
In this example, the author and target DIDs were the same, `target.connected` is `true`, so send the context (message & data) to the target endpoint.
Since the `endpoint` for the `targetDID` is `app://dwn`, the transport method for `app` will send directly to the DWN embedded in the agent:
```javascript
node.processMessage(target.did, message, stream))
```
# Open Questions
## `Record()` Class
### What is the value of returning a `Record()` instance for the RecordsQuery message?
The response to:
```javascript
const response = web5.dwn.records.query(myDid, {
author: myDid,
message: {
filter: {
schema: 'foo/bar',
dataFormat: 'text/plain'
}
}
})
```
would be:
```javascript
{
entries: [
Record instance, // of a RecordsWrite in DWN
Record instance, // of a RecordsWrite in DWN
Record instance // of a RecordsWrite in DWN
],
record: Record instance // of the RecordsQuery
}
```
The value of returning the `entries` as `Record()` instances is clear because the developer can then interact with those records to read the data, update, or delete.
However, what's the value of returning a `Record()` instance for the RecordsQuery itself? It would only contain the following descriptor properties:
```json
"recordId": null,
"descriptor": {
"interface": "Records",
"method": "Query",
"dateCreated": "2023-03-23T19:33:52.385998Z",
"filter": {
"schema": "foo/bar",
"dataFormat": "text/plain"
}
}
```
Other than the precise `dateCreated` value, the developer passed all of this information in when they made the query (_`interface` and `method` are inferred since its `web5.dwn.records.query`_):
```javascript
const response = web5.dwn.records.query(myDid, {
author: myDid,
message: {
filter: {
schema: 'foo/bar',
dataFormat: 'text/plain'
}
}
})
```