Skip to content

Web5 JS v0.7.0

Pre-release
Pre-release
Compare
Choose a tag to compare
@frankhinek frankhinek released this 16 May 17:10
· 593 commits to main since this release
71937dc

The TBD open source team is thrilled to announce a new version of Web5 JS, 0.7.0. Web5 JS is a software development kit that empowers everyone to create decentralized web applications (DWAs) that return ownership of data and identity to individuals.

If you were using a previous version of Web5 JS in your project, you can get 0.7.0 through a variety of distribution channels, including:

If your project is a you can get Web5 JS 0.7.0 with
Node.js package npm install @tbd54566975/web5
Native web browser app https://unpkg.com/@tbd54566975/[email protected]/dist/browser.js
OR
https://cdn.jsdelivr.net/npm/@tbd54566975/[email protected]/dist/browser.mjs

We invite everyone to help us out by testing this release, and please consider reporting any bugs you might come across. Thanks! 🙏

What's in 0.7.0

💡 Summary

The Web5 JS 0.7.0 release is a significant step towards making the creation of decentralized web applications more accessible and convenient. By simplifying the initial setup process, introducing in-browser and cloud agent capabilities, improving protocol definition readability, and refining typical DWN protocol and record operations, we aim to empower developers to innovate with ease and efficiency. The TBD open source team remains committed to continuous improvement and eagerly anticipates your feedback on this release. So, download, test, and let's work together to return ownership of data and identity back to the individuals. Happy coding!

Simplify Getting Started

Previously, the first thing a developer building a decentralized web application using Web5 JS would do is:

const web5 = new Web5();

const did = await web5.did.create('ion');

await web5.did.manager.set(did.id, {
  connected: true,
  endpoint: 'app://dwn', //this points to the user's local DWN
  keys: {
      ['#dwn']: {
        keyPair: did.keys.find(key => key.id === 'dwn').keyPair,
      },
  },
});

With this release, the developer experience has been simplified to:

const { web5, did } = await Web5.connect();

To begin, the developer connects to a Web5 User Agent. A web5-user-agent acts on behalf of a user to manage identity information, public/private data, and interactions with other entities in a decentralized network. These capabilities are enabled by the underlying profile, DID, key, cryptography, and DWN management components that an Agent interacts with.

Web5 JS can be used to build Agents for a variety of deployment environments, including:

  • Cloud Instances
  • Desktop OS (macOS, Linux, Windows)
  • Mobile OS (Android, iOS)
  • Web Browsers

However, with the 0.7.0 release the focus is on in-browser and cloud agents first with a fast follow release to enable desktop agents.
Returning to our example, connect() returns a web5 instance and did:

  • web5: The web5 instance returned is instantiated with a Web5 User Agent that manages DIDs, keys, an embedded DWN, and persisting the DID / key state to either browser storage or local disk, depending on whether the app is running in a browser or Node.js environment.
  • did: The did returned is the automatically generated DID that is currently active in the connected Web5 User Agent.

It is also worth noting is that, previously, most DWAs had to provide a method for saving and loading profile state information (DID, keys, etc.) so that they weren't regenerated on every app launch or page reload. To simplify the developer experience, saving/loading profile state now happens automatically in both web browser and Node.js environments. A security-focused key management solution will be incorporated in a future release that will make this capability even more secure and flexible.


✨ Simplify Decentralized Web Node Operations

Before this release, creating a record in an app's DWN, writing the record to someone else's DWN, and then querying to confirm it was written might look like the following:

Note In this example, aliceDid and bobDid are the decentralized identifiers (DIDs) of two individuals, Alice and Bob, who are sending data back and forth using Web5 JS and DWNs they control.

// Create a record in Alice's DWN
const { record: aliceRecord, status } = await web5.dwn.records.create(aliceDid, {
  author: aliceDid,
  data: "Hello Web5",
  message: {
    dataFormat: 'text/plain'
  }
});

// Alice writes the same record to Bob's DWN
const { record: bobRecord } = await web5.dwn.records.createFrom(bobDid, {
  author: aliceDid,
  record: aliceRecord
});

// Alice queries Bob's DWN to confirm the record was written
const { entries } = await web5.dwn.records.query(bobDid, {
  author: aliceDid,
  message: {
    filter: {
      recordId: aliceRecord.id,
  },
});
const firstEntry = entries[0];
console.log(aliceRecord.id === firstEntry.id) // prints "true"

Every method call, whether a create() or query(), required specifying:

  • target to direct the record creation or query to
  • author who signs and transmits the request

While it makes sense that you should have to specify the DWN to send messages to for processing when it is a DWN you don't control (e.g., Alice sending to Bob), it is rather verbose to have to repeatedly reiterate aliceDid for target and author for every record operation destined for Alice's local DWN.

With 0.7.0, Web5.connect() returns an instance of web5 with a connected did profile already active. As a result, when you call methods like web5.dwn.records.create(), the agent will assume the operation should be performed on the connected DID's DWN unless you tell it otherwise. To revisit the Alice and Bob example, the code to create a record on Alice's DWN and then write it to Bob no longer requires specifying aliceDid as the target and author: to create the record and to send the same record to Bob becomes a one-liner:

// Create a record in Alice's DWN
const { record: aliceRecord } = await web5.dwn.records.create({
  data: "Hello Web5",
  message: {
    dataFormat: 'text/plain',
  },
});

// Alice writes the same record to Bob's DWN
const { status } = await record.send(bobDid);

For queries (and every web5.dwn.*.* method), the same pattern holds: if you want to perform the operation on your DID's DWN then you don't need to specify any additional properties. Alice querying her own DWN for the record created earlier is simply:

const { records } = await web5.dwn.records.query({
  message: {
    filter: {
      recordId: aliceRecord.id,
  },
});

and to query from Bob's DWN, simply add a from: property specifying the DID you wish to query from:

// Alice queries Bob's DWN to confirm the record was written
const { records } = await web5.dwn.records.query({
  from: bobDid,
  message: {
    filter: {
      recordId: aliceRecord.id,
  },
});

The last example to highlight is likely to be less frequently used, but there may be scenarios in which you intentionally do not want to write a record to your agent's DWN until after the record was written to another entity's DWN. In that case, you can use a store: false directive to skip persisting the record. If the record is successfully written to the third-party then you can either choose to never write a local copy or send it to your own remote DWN as a final step. As mentioned, this is likely to be an unusual sequence, but we'd love to hear from anyone who has this scenario in mind for an app they are building.

// Create the record but do NOT save it to Alice's DWN
const { record: aliceRecord } = await web5.dwn.records.create({
  store: false,
  data: "Hello Web5",
  message: {
    dataFormat: 'text/plain',
  },
});

// Alice writes the record to Bob's DWN
const { status } = await record.send(bobDid);
if (status.code === 202) {
  await record.send(aliceDid);
}

Check out the DWN Methods Cheat Sheet at the end of the release notes.


✨ Enhancements to DSL for Protocol Definitions

DWeb Nodes are designed to act the substrate upon which a wide variety of decentralized applications and services can be written. With an interface like Records alone, a DWeb Node owner and those they permission can write isolated records, but that alone is not enough to support and facilitate decentralized apps.

Protocols introduces a mechanism for declaratively encoding an app or service’s underlying protocol rules, including segmentation of records, relationships between records, data-level requirements, and constraints on how participants interact with a protocol. With the DWN Protocols mechanism, one can model the underpinning protocols for a vast array of use cases in a way that enables interop-by-default between app implementations that ride on top of them.

While this is a change in the underlying DWN SDK that Web5 JS uses to interact with DWNs, the enhancements to make the protocol definition DSL (Domain-Specific Language) were made during the development of the 0.7.0 release to improve the ease with which designers and developers could both create and interpret protocol definitions.

The prior definition format was:

{
  "labels": {
    "credentialApplication": {
      "schema": "https://identity.foundation/credential-manifest/schemas/credential-application"
    },
    "credentialResponse": {
      "schema": "https://identity.foundation/credential-manifest/schemas/credential-response"
    }
  },
  "records": {
    "credentialApplication": {
      "allow": [
        {
          "actor": "anyone",
          "actions": ["write"]
        }
      ],
      "records": {
        "credentialResponse": {
          "allow": [
            {
              "actor": "recipient",
              "protocolPath": "credentialApplication",
              "actions": ["write"]
            }
          ]
        }
      }
    }
  }
}

The revised protocol definition format:

{
  "protocol": "http://credential-issuance-protocol.xyz",
  "types": {
    "credentialApplication": {
      "schema": "https://identity.foundation/credential-manifest/schemas/credential-application",
      "dataFormats": ["application/json"]
    },
    "credentialResponse": {
      "schema": "https://identity.foundation/credential-manifest/schemas/credential-response",
      "dataFormats": ["application/json"]
    }
  },
  "structure": {
    "credentialApplication": {
      "$actions": [
        {
          "who": "anyone",
          "can": "write"
        }
      ],
      "credentialResponse": {
        "$actions": [
          {
            "who": "recipient",
            "of": "credentialApplication",
            "can": "write"
          }
        ]
      }
    }
  }
}

TypeScript logo Conversion to TypeScript

We've transitioned Web5 JS to TypeScript with a strong emphasis on elevating the developer experience. TypeScript's static type checking and inherent predictability bring a significant enhancement to the coding process, especially when paired with the IntelliSense hints in code editors. This upgrade provides developers with real-time feedback, auto-completion, and precise debugging, ensuring a more streamlined, intuitive, and productive development journey.


🛠️ Miscellaneous


📝 Full Changelog

Available here: v0.6.2...v0.7.0


🤓 DWN Methods Cheat Sheet

What follows is a handy reference of DWN protocol and record method operations.

💻 web5.dwn.protocols.configure()

A. Your local

const { protocol, status } = await web5.dwn.protocols.configure({
  message: {
    definition: chatProtocolDefinition
  }
});

B. Your remote

const { protocol, status } = await web5.dwn.protocols.configure({
  message: {
    definition: chatProtocolDefinition
  }
});

protocol.send(aliceDid);

C. Someone else's remote

const { protocol, status } = await web5.dwn.protocols.configure({
  message: {
    definition: chatProtocolDefinition
  }
});

protocol.send(bobDid);

💻 web5.dwn.protocols.query()

A. Your local

const { protocols, status } = await dwn.protocols.query({
  message: {
    filter: {
      protocol: emailProtocolDefinition.protocol
    }
  }
});

B. Your remote

const { protocols, status } = await dwn.protocols.query({
  from: aliceDid,
  message: {
    filter: {
      protocol: emailProtocolDefinition.protocol
    }
  }
});

C. Someone else's remote

const { protocols, status } = await dwn.protocols.query({
  from: bobDid,
  message: {
    filter: {
      protocol: emailProtocolDefinition.protocol
    }
  }
});

💻 web5.dwn.records.create()

A. Your local only:

// write to your agent's DWN (i.e., write to the DWN of the agent an app is connected to)
await web5.dwn.records.create({
  data: 'hi',
  message: {
    "schema": 'whatever'
  }
});

B. Your local + your remote:

// write to your agent's DWN
const { record } = await web5.dwn.records.create({
  data: 'hi',
  message: {
    "schema": 'whatever'
  }
});

// write to your DID-relative DWNs (e.g., cloud DWNs)
await record.send(aliceDid)

C. Your local + your remote + someone else's remote:

// write to your agent's DWN
const { record } = await web5.dwn.records.create({
  data: 'hi',
  message: {
    "schema": 'whatever'
  }
});

// write to your DID-relative DWNs (e.g., cloud DWNs)
await record.send(aliceDid);

// write to Bob's DWN
await record.send(bobDid);

D. Your own local + someone else's remote:

// write to your agent's DWN
const { record } = await web5.dwn.records.create({
  data: 'hi',
  message: {
    "schema": 'whatever'
  }
});

// write to Bob's DWN
await record.send(bobDid);

E. Your remote + someone else's remote:

// write to your DID-relative DWNs (e.g., cloud DWNs)
const { record } = await web5.dwn.records.create({
  store: false,
  data: 'hi',
  message: {
    "schema": 'whatever'
  }
});

// write to your DID-relative DWNs (e.g., cloud DWNs)
await record.send(aliceDid);
// write to Bob's DWN
await record.send(bobDid);

F. Your own remote:

// Create a record but don't write it to your agent's DWN
await web5.dwn.records.create({
  store: false,
  data: 'hi',
  message: {
    "schema": 'whatever'
  }
});

// write only to your DID-relative DWNs (e.g., cloud DWNs)
await record.send(aliceDid);

G. Someone else's remote:

// Create a record but don't write it to your agent's DWN
const { record } = await web5.dwn.records.create({
  store: false,
  data: 'hi',
  message: {
    "schema": 'whatever'
  }
});

// write to Bob's DWN
record.send(bobDid);

💻 web5.dwn.records.query()

A. Your local:

web5.dwn.records.query({
  message: {
    filter: {
      contextId: thread.contextId
    }
  }
});

B. Your remote:

web5.dwn.records.query({
  from: aliceDid,
  message: {
    filter: {
      contextId: thread.contextId
    }
  }
});

C. Someone else's remote:

web5.dwn.records.query({
  from: bobDid,
  message: {
    filter: {
      contextId: thread.contextId
    }
  }
});

💻 web5.dwn.records.read()

  1. Your local
web5.dwn.records.read({
  message: {
    recordId: thread.recordId
  }
});
  1. Your remote
web5.dwn.records.read({
  from: aliceDid,
  message: {
    recordId: thread.recordId
  }
});
  1. Someone else's remote
web5.dwn.records.read({
  from: bobDid,
  message: {
    recordId: thread.recordId
  }
});

💻 web5.dwn.records.delete()

  1. Your local
web5.dwn.records.read({
  message: {
    recordId: thread.recordId
  }
});
  1. Your remote
web5.dwn.records.read({
  from: aliceDid,
  message: {
    recordId: thread.recordId
  }
});
  1. Someone else's remote
web5.dwn.records.read({
  from: bobDid,
  message: {
    recordId: thread.recordId
  }
});