From 445e52ddb0c2193bbfb3f4cca3e7f9d0bd6d0e82 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Fri, 6 Sep 2024 15:29:31 -0300 Subject: [PATCH 01/12] feat(client): optional email recovery Signed-off-by: Felipe Forbeck --- packages/w3up-client/src/client.js | 39 ++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 08027fc69..62d571ae7 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -10,6 +10,7 @@ import { Upload as UploadCapabilities, Filecoin as FilecoinCapabilities, } from '@web3-storage/capabilities' +import * as DIDMailto from '@web3-storage/did-mailto' import { Base } from './base.js' import * as Account from './account.js' import { Space } from './space.js' @@ -98,8 +99,9 @@ export class Client extends Base { /* c8 ignore stop */ /** - * List all accounts that agent has stored access to. Returns a dictionary - * of accounts keyed by their `did:mailto` identifier. + * List all accounts that agent has stored access to. + * + * @returns {Record} A dictionary with `did:mailto` as keys and `Account` instances as values. */ accounts() { return Account.list(this) @@ -233,12 +235,39 @@ export class Client extends Base { /** * Create a new space with a given name. - * + * If an account is provided in the options argument, then it creates a delegated recovery account. + * + * @typedef {object} CreateOptions + * @property {string|false} [account] + * * @param {string} name + * @param {CreateOptions} options */ - async createSpace(name) { - return await this._agent.createSpace(name) + async createSpace(name, options = {}) { + const space = await this._agent.createSpace(name) + + const account = Object.entries(this.accounts()) + .filter(([did, account]) => { + const email = did.split(':')[2] // Extract the email from the DID + return email === options.account + }) + .map(([did, account]) => account)[0] || null + + if (account) { + const recovery = await space.createRecovery(account) + + const result = await this.capability.access.delegate({ + space: space.did(), + delegations: [recovery], + }) + + if (result.error) { + throw new Error(`⚠️ Failed to authorize recovery account: ${result.error.name}:${result.error.message}`) + } + } + return space; } + /* c8 ignore stop */ /** From f45288a18d8aa5adca8beb4c07b13cc012ede4bc Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Mon, 9 Sep 2024 17:08:29 -0300 Subject: [PATCH 02/12] applied changes suggested in the PR + tests: wip --- packages/w3up-client/src/client.js | 15 ++++------- packages/w3up-client/test/client.test.js | 32 +++++++++++++++++++++--- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 62d571ae7..5c678203f 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -238,23 +238,17 @@ export class Client extends Base { * If an account is provided in the options argument, then it creates a delegated recovery account. * * @typedef {object} CreateOptions - * @property {string|false} [account] + * @property {Account.Account} [account] * * @param {string} name * @param {CreateOptions} options + * @returns {Promise} The created space owned by the agent. */ async createSpace(name, options = {}) { const space = await this._agent.createSpace(name) - const account = Object.entries(this.accounts()) - .filter(([did, account]) => { - const email = did.split(':')[2] // Extract the email from the DID - return email === options.account - }) - .map(([did, account]) => account)[0] || null - - if (account) { - const recovery = await space.createRecovery(account) + if (options.account) { + const recovery = await space.createRecovery(options.account.did()) const result = await this.capability.access.delegate({ space: space.did(), @@ -264,6 +258,7 @@ export class Client extends Base { if (result.error) { throw new Error(`⚠️ Failed to authorize recovery account: ${result.error.name}:${result.error.message}`) } + } return space; } diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index 831fb1578..67480949a 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -1,6 +1,6 @@ import assert from 'assert' import { parseLink } from '@ucanto/server' -import { AgentData } from '@web3-storage/access/agent' +import { Access, AgentData } from '@web3-storage/access/agent' import { randomBytes, randomCAR } from './helpers/random.js' import { toCAR } from './helpers/car.js' import { File } from './helpers/shims.js' @@ -221,7 +221,7 @@ export const testClient = { assert.equal(current1?.did(), space.did()) }, }, - spaces: { + spaces: Test.withContext({ 'should get agent spaces': async (assert) => { const alice = new Client(await AgentData.create()) @@ -259,7 +259,33 @@ export const testClient = { assert.equal(spaces.length, 1) assert.equal(spaces[0].did(), space.did()) }, - }, + + 'should create a space with recovery account': async (assert, { client, mail, grantAccess }) => { + const account = client.login('alice@web.mail') + + await grantAccess(await mail.take()) + + const alice = await account + + const space = await client.createSpace('recovery-space', { + account: alice, + }) + assert.ok(space) + + const proof = alice.agent.proofs() + .find(p => p.issuer.did() === alice.did()) + if (!proof) { + throw new Error('Recovery Proof not found') + } + + assert.ok(proof) + assert.equal(proof.audience.did(), alice.did()) + //FIXME how to check that there is a recovery delegation/account? + }, + 'should create a space without a recovery account': async (assert, { client, mail, grantAccess }) => { + //TODO + } + }), proofs: { 'should get proofs': async (assert) => { const alice = new Client(await AgentData.create()) From 3dfe60e263722ed231209f2283d297bb12063708 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Wed, 11 Sep 2024 16:53:54 -0300 Subject: [PATCH 03/12] fix test: client.test.js account recovery --- packages/w3up-client/src/client.js | 38 +++++++++---- .../w3up-client/test/client-accounts.test.js | 2 + packages/w3up-client/test/client.test.js | 53 +++++++++++-------- 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 5c678203f..76a9a648f 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -100,7 +100,7 @@ export class Client extends Base { /** * List all accounts that agent has stored access to. - * + * * @returns {Record} A dictionary with `did:mailto` as keys and `Account` instances as values. */ accounts() { @@ -235,32 +235,48 @@ export class Client extends Base { /** * Create a new space with a given name. - * If an account is provided in the options argument, then it creates a delegated recovery account. - * + * If an account is provided in the options argument, then it creates a delegated recovery account + * by provisioning the space and then delegating access to the recovery account. + * * @typedef {object} CreateOptions * @property {Account.Account} [account] - * + * * @param {string} name * @param {CreateOptions} options * @returns {Promise} The created space owned by the agent. */ async createSpace(name, options = {}) { const space = await this._agent.createSpace(name) - - if (options.account) { - const recovery = await space.createRecovery(options.account.did()) + const account = options.account + if (account) { + // Provision the account with the space + const provisionResult = await account.provision(space.did()) + if (provisionResult.error) { + throw new Error( + `⚠️ Failed to provision account: ${provisionResult.error.name}:${provisionResult.error.message}` + ) + } + + // Save the space to authorize the client to use the space + await space.save() + + // Create a recovery for the account + const recovery = await space.createRecovery(account.did()) + + // Delegate space access to the recovery const result = await this.capability.access.delegate({ space: space.did(), delegations: [recovery], }) - + if (result.error) { - throw new Error(`⚠️ Failed to authorize recovery account: ${result.error.name}:${result.error.message}`) + throw new Error( + `⚠️ Failed to authorize recovery account: ${result.error.name}:${result.error.message}` + ) } - } - return space; + return space } /* c8 ignore stop */ diff --git a/packages/w3up-client/test/client-accounts.test.js b/packages/w3up-client/test/client-accounts.test.js index 8515debd0..f22c953c9 100644 --- a/packages/w3up-client/test/client-accounts.test.js +++ b/packages/w3up-client/test/client-accounts.test.js @@ -25,8 +25,10 @@ export const testClientAccounts = Test.withContext({ const accounts = client.accounts() assert.deepEqual(Object.values(accounts).length, 1) + // @ts-ignore FIXME (fforbeck) assert.ok(accounts[Account.fromEmail(email)]) + // @ts-ignore FIXME (fforbeck) const account = accounts[Account.fromEmail(email)] assert.equal(account.toEmail(), email) assert.equal(account.did(), Account.fromEmail(email)) diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index 67480949a..ec8836229 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -1,6 +1,6 @@ import assert from 'assert' import { parseLink } from '@ucanto/server' -import { Access, AgentData } from '@web3-storage/access/agent' +import { AgentData } from '@web3-storage/access/agent' import { randomBytes, randomCAR } from './helpers/random.js' import { toCAR } from './helpers/car.js' import { File } from './helpers/shims.js' @@ -260,31 +260,40 @@ export const testClient = { assert.equal(spaces[0].did(), space.did()) }, - 'should create a space with recovery account': async (assert, { client, mail, grantAccess }) => { - const account = client.login('alice@web.mail') - - await grantAccess(await mail.take()) - - const alice = await account - - const space = await client.createSpace('recovery-space', { - account: alice, + 'should create a space with recovery account': async ( + assert, + { client, mail, connect, grantAccess } + ) => { + // Step 1: Create a client for Alice and login + const aliceEmail = 'alice@web.mail' + const aliceLogin = client.login(aliceEmail) + const message = await mail.take() + assert.deepEqual(message.to, aliceEmail) + await grantAccess(message) + const aliceAccount = await aliceLogin + + // Step 2: Alice creates a space with her account as the recovery account + const space = await client.createSpace('recovery-space-test', { + account: aliceAccount, // The account is the recovery account }) assert.ok(space) - const proof = alice.agent.proofs() - .find(p => p.issuer.did() === alice.did()) - if (!proof) { - throw new Error('Recovery Proof not found') - } - - assert.ok(proof) - assert.equal(proof.audience.did(), alice.did()) - //FIXME how to check that there is a recovery delegation/account? + // Step 3: Verify the recovery account by connecting to a new device + const secondClient = await connect() + const secondLogin = secondClient.login(aliceEmail) + const secondMessage = await mail.take() + assert.deepEqual(secondMessage.to, aliceEmail) + await grantAccess(secondMessage) + const aliceAccount2 = await secondLogin + await secondClient.addSpace( + await space.createAuthorization(aliceAccount2) + ) + await secondClient.setCurrentSpace(space.did()) + + // Step 4: Verify the space is accessible from the new device + const spaceInfo = await secondClient.capability.space.info(space.did()) + assert.ok(spaceInfo) }, - 'should create a space without a recovery account': async (assert, { client, mail, grantAccess }) => { - //TODO - } }), proofs: { 'should get proofs': async (assert) => { From 3cd04f9e3e08fad58969e0704523ded3e72e22c2 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Thu, 12 Sep 2024 14:16:58 -0300 Subject: [PATCH 04/12] new test cases --- packages/w3up-client/test/client.test.js | 100 +++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index ec8836229..c8dbfbe9f 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -294,6 +294,106 @@ export const testClient = { const spaceInfo = await secondClient.capability.space.info(space.did()) assert.ok(spaceInfo) }, + + 'should create a space without recovery account and fail access from another device': + async (assert, { client, mail, connect, grantAccess }) => { + // Step 1: Create a client for Alice and login + const aliceEmail = 'alice@web.mail' + const aliceLogin = client.login(aliceEmail) + const message = await mail.take() + assert.deepEqual(message.to, aliceEmail) + await grantAccess(message) + await aliceLogin + + // Step 2: Alice creates a space without providing a recovery account + const space = await client.createSpace('no-recovery-space-test') + assert.ok(space) + + // Step 3: Attempt to access the space from a new device + const secondClient = await connect() + const secondLogin = secondClient.login(aliceEmail) + const secondMessage = await mail.take() + assert.deepEqual(secondMessage.to, aliceEmail) + await grantAccess(secondMessage) + const aliceAccount2 = await secondLogin + + // Step 4: Add the space to the new device and set it as current space + await secondClient.addSpace( + await space.createAuthorization(aliceAccount2) + ) + await secondClient.setCurrentSpace(space.did()) + + // Step 5: Verify the space is accessible from the new device + await assert.rejects(secondClient.capability.space.info(space.did()), { + message: `no proofs available for resource ${space.did()} and ability space/info`, + }) + }, + + 'should fail to create a space due to provisioning error': async ( + assert, + { client, mail, grantAccess } + ) => { + // Step 1: Create a client for Alice and login + const aliceEmail = 'alice@web.mail' + const aliceLogin = client.login(aliceEmail) + const message = await mail.take() + assert.deepEqual(message.to, aliceEmail) + await grantAccess(message) + const aliceAccount = await aliceLogin + + // Step 2: Mock the provisioning to fail + const originalProvision = aliceAccount.provision + aliceAccount.provision = async () => ({ + error: { name: 'ProvisionError', message: 'Provisioning failed' }, + }) + + // Step 3: Attempt to create a space with the account + await assert.rejects( + client.createSpace('provision-fail-space-test', { + account: aliceAccount, + }), + { + message: + '⚠️ Failed to provision account: ProvisionError:Provisioning failed', + } + ) + + // Restore the original provision method + aliceAccount.provision = originalProvision + }, + + 'should fail to create a space due to delegate access error': async ( + assert, + { client, mail, connect, grantAccess } + ) => { + // Step 1: Create a client for Alice and login + const aliceEmail = 'alice@web.mail' + const aliceLogin = client.login(aliceEmail) + const message = await mail.take() + assert.deepEqual(message.to, aliceEmail) + await grantAccess(message) + const aliceAccount = await aliceLogin + + // Step 2: Mock the delegate access to fail + const originalDelegate = client.capability.access.delegate + client.capability.access.delegate = async () => ({ + error: { name: 'DelegateError', message: 'Delegation failed' }, + }) + + // Step 3: Attempt to create a space with the account + await assert.rejects( + client.createSpace('delegate-fail-space-test', { + account: aliceAccount, + }), + { + message: + '⚠️ Failed to authorize recovery account: DelegateError:Delegation failed', + } + ) + + // Restore the original delegate method + client.capability.access.delegate = originalDelegate + }, }), proofs: { 'should get proofs': async (assert) => { From a01012c75f5a33c7c21a899e68b7f0d0ca7d3214 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Thu, 12 Sep 2024 14:35:58 -0300 Subject: [PATCH 05/12] docs: update space creation steps --- packages/w3up-client/README.md | 229 +++++++++++++++------------------ 1 file changed, 101 insertions(+), 128 deletions(-) diff --git a/packages/w3up-client/README.md b/packages/w3up-client/README.md index 793e1837f..b888780e4 100644 --- a/packages/w3up-client/README.md +++ b/packages/w3up-client/README.md @@ -9,16 +9,15 @@ ## About - `@web3-storage/w3up-client` is a JavaScript library that provides a convenient interface to the w3up platform, a simple "on-ramp" to the content-addressed decentralized IPFS network. This library is the user-facing "porcelain" client for interacting with w3up services from JavaScript. It wraps the lower-level [`@web3-storage/access`][access-client-github] and [`@web3-storage/upload-client`][upload-client-github] client packages, which target individual w3up services. We recommend using `w3up-client` instead of using those "plumbing" packages directly, but you may find them useful if you need more context on w3up's architecture and internals. **`w3up-client` requires Node 18 or higher**. -> ⚠️❗ __Public Data__ 🌎: All data uploaded to w3up is available to anyone who requests it using the correct CID. Do not store any private or sensitive information in an unencrypted form using w3up. +> ⚠️❗ **Public Data** 🌎: All data uploaded to w3up is available to anyone who requests it using the correct CID. Do not store any private or sensitive information in an unencrypted form using w3up. -> ⚠️❗ __Permanent Data__ ♾️: Removing files from w3up will remove them from the file listing for your account, but that doesn’t prevent nodes on the decentralized storage network from retaining copies of the data indefinitely. Do not use w3up for data that may need to be permanently deleted in the future. +> ⚠️❗ **Permanent Data** ♾️: Removing files from w3up will remove them from the file listing for your account, but that doesn’t prevent nodes on the decentralized storage network from retaining copies of the data indefinitely. Do not use w3up for data that may need to be permanently deleted in the future. - [Install](#install) - [Usage](#usage) @@ -92,11 +91,11 @@ flowchart TD All uses of `w3up-client` to upload with web3.storage follow the flow above. This section shows the most basic way to use the client to start storing data. For more complex integration options, check out the [integration options][https://github.com/web3-storage/w3up/blob/main/packages/w3up-client/README.md#integration-options] docs. For reference, check out the [API reference docs][docs] or the source code of the [`w3cli` package][w3cli-github], which uses `w3up-client` throughout. -> By you or your users registering a w3up Space via email confirmation with [web3.storage](http://web3.storage), you agree to the [Terms of Service](https://web3.storage/docs/terms/). +> By you or your users registering a w3up Space via email confirmation with [web3.storage](http://web3.storage), you agree to the [Terms of Service](https://web3.storage/docs/terms/). #### Creating a client object -The package provides a [static `create` function][docs-create] that returns a [`Client` object][docs-Client]. +The package provides a [static `create` function][docs-create] that returns a [`Client` object][docs-Client]. ```js import { create } from '@web3-storage/w3up-client' @@ -126,7 +125,7 @@ A [`Space`][docs-Space] acts as a namespace for your uploads, and what your Agen const account = await client.login('zaphod@beeblebrox.galaxy') ``` -If your account does not yet have a payment plan, you'll be prompted to choose one after your email address has been verified. You will need a payment plan in order to provision your space. You can use the following code to wait for a payment plan to be selected: +If your account doesn't have a payment plan yet, you'll be prompted to select one after verifying your email. A payment plan is required to provision a space. You can use the following loop to wait until a payment plan is selected: ```js // wait for payment plan to be selected @@ -134,47 +133,29 @@ while (true) { const res = await account.plan.get() if (res.ok) break console.log('Waiting for payment plan to be selected...') - await new Promise(resolve => setTimeout(resolve, 1000)) + await new Promise((resolve) => setTimeout(resolve, 1000)) } ``` Spaces can be created using the [`createSpace` client method][docs-client#createSpace]: ```js -const space = await client.createSpace('my-awesome-space') +const space = await client.createSpace('my-awesome-space', { account }) ``` -or using the w3cli's [`w3 space create`](https://github.com/web3-storage/w3cli#w3-space-create-name). - -The name parameter is optional. If provided, it will be stored in your client's local state store and can be used to provide a friendly name for user interfaces. +Alternatively, you can use the w3cli command [`w3 space create`](https://github.com/web3-storage/w3cli#w3-space-create-name). -Before anything can be stored with a space using web3.storage, the space must also be provisioned by a specific account that is responsible for the stored data. Note: after this succeeds, `account`'s payment method will be charged for data stored in `space`. +The `name` parameter is optional. If provided, it will be stored in your client's local state store and can be used to provide a friendly name for user interfaces. -```js -await account.provision(space.did()) -``` - -If provisioning succeeds, you're ready to use the Space. Save your space to your agent's state store: - -```js -await space.save() -``` +If an `account` is provided in the options, a delegated recovery account is automatically created and provisioned, allowing you to store data and delegate access to the recovery account. This means you can access your space from other devices, as long as you have access to your account. -If your agent has no other spaces, saving the space will set it as the "current space" in your agent. If you already have other spaces, you may want to set it as the current: +If this is your Agent's first space, it will automatically be set as the "current space." If you already have spaces and want to set the new one as current, you can do so manually: ```js await client.setCurrentSpace(space.did()) ``` -One last thing - now that you've saved your space locally, it's a good idea to setup recovery, so that when you move to a different device you can still access your space: - -```js -const recovery = await space.createRecovery(account.did()) -await client.capability.access.delegate({ - space: space.did(), - delegations: [recovery], -}) -``` +ℹ️ Note: If you do not create the space passing the account parameter you run the risk of losing access to your space! #### Delegating from Space to Agent @@ -204,7 +185,7 @@ import { CarReader } from '@ipld/car' import * as Client from '@web3-storage/w3up-client' import { StoreMemory } from '@web3-storage/w3up-client/stores/memory' -async function main () { +async function main() { // from "bring your own Agent" example in `Creating a client object" section` // used command line to generate KEY and PROOF (stored in env variables) // KEY: `npx ucan-key ed --json` in command line, which returns private key and DID for Agent (the private key is stored in KEY) @@ -212,17 +193,17 @@ async function main () { const principal = Signer.parse(process.env.KEY) const store = new StoreMemory() const client = await Client.create({ principal, store }) - + // now give Agent the delegation from the Space const proof = await parseProof(process.env.PROOF) const space = await client.addSpace(proof) await client.setCurrentSpace(space.did()) - + // READY to go! } /** @param {string} data Base64 encoded CAR file */ -async function parseProof (data) { +async function parseProof(data) { const blocks = [] const reader = await CarReader.fromBytes(Buffer.from(data, 'base64')) for await (const block of reader.blocks()) { @@ -240,7 +221,7 @@ Call [`uploadFile`][docs-Client#uploadFile] to upload a single file, or [`upload `uploadFile` expects a "Blob like" input, which can be a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) when running in a browser. On node.js, see the [`filesFromPath` library](https://github.com/web3-storage/files-from-path), which can load compatible objects from the local filesystem. -`uploadDirectory` requires `File`-like objects instead of `Blob`s, as the file's `name` property is used to build the directory hierarchy. +`uploadDirectory` requires `File`-like objects instead of `Blob`s, as the file's `name` property is used to build the directory hierarchy. You can control the directory layout and create nested directory structures by using `/` delimited paths in your filenames: @@ -265,10 +246,10 @@ In the example above, `directoryCid` resolves to an IPFS directory with the foll └── main.py ``` - ### Integration options As mentioned, UCAN opens up a number of options in how to integrate with w3up: Should you, the developer, own the Space? Should you delegate permissions to your users? Or should your user own their own Space? Broadly, there are three ways to integrate: + - (Simplest) Client-server: You (the developer) own the Space, and your user uploads to your backend infra before you upload it to the service - (More complex) Delegated: You own the Space, but you give a delegated UCAN token to your user's Agent to upload directly to the service (rather than needing to touch the upload in your backend) - (Most complex) User-owned: Your user owns the Space and registers it and they use it to upload directly with the service; if you want to instrument visibility into what they’re uploading, you’ll have to write separate code in your app for it @@ -276,6 +257,7 @@ As mentioned, UCAN opens up a number of options in how to integrate with w3up: S You can implement each of these in a number of ways, but we talk through some considerations when implementing a given option. #### Client-server + ```mermaid sequenceDiagram participant User @@ -283,6 +265,7 @@ sequenceDiagram User->>w3up-client in backend: Upload data w3up-client in backend->>web3.storage w3up service: Upload data ``` + - For your backend to be scalable, you might consider using serverless workers or a queue in front of a server - In either case, you'll need a registered Space, and your client instance in your backend to have an Agent with a delegation from this Space - (Recommended) It's likely easiest to create and register your Space using [w3cli](https://github.com/web3-storage/w3cli) rather than using `w3up-client` to do so (especially if your backend isn't persistent); you can then generate your own Agent and delegate the ability to upload to your Space using something like [this example](#bringing-your-own-agent-and-delegation) @@ -290,6 +273,7 @@ sequenceDiagram - After this, once your user uploads data to your backend, you can run any of the `upload` methods #### Delegated + ```mermaid sequenceDiagram participant w3up-client in user @@ -301,10 +285,11 @@ sequenceDiagram w3up-client in backend->>w3up-client in user: Send delegation from Space to user's Agent DID w3up-client in user->>web3.storage w3up service: Upload data ``` + - You will likely have `w3up-client` running in your end-user's client code, as well as backend code that's able to generate UCANs that delegate the ability to upload and pass them to your users (e.g., `w3up-client` running in a serverless worker) - For your backend to be scalable, you might consider using serverless workers or a queue in front of a server - As the developer, you'll need a registered Space, and your client instance in your backend to have an Agent with a delegation from this Space - - (Recommended) It's likely easiest to create and register your Space using [w3cli](https://github.com/web3-storage/w3cli) rather than using `w3up-client` to do so (especially if your backend isn't persistent); you can then generate your own Agent and delegate the ability to upload to your Space using something like [this example](#bringing-your-own-agent-and-delegation) + - (Recommended) It's likely easiest to create and register your Space using [w3cli](https://github.com/web3-storage/w3cli) rather than using `w3up-client` to do so (especially if your backend isn't persistent); you can then generate your own Agent and delegate the ability to upload to your Space using something like [this example](#bringing-your-own-agent-and-delegation) - If your backend is persistent, you can do this or do everything in the client directly ([create Space](#creating-and-registering-spaces) and [get delegation](#delegating-from-space-to-agent)) - Your user does not need a registered Space - just an Agent with a delegation from your Space - `w3up-client` in the end user environment should have a unique Agent for each user, which should happen by default (since when `w3up-client` is instantiated it creates a new Agent anyway, or uses the one in local Store) @@ -313,74 +298,75 @@ sequenceDiagram - You can serialize this using `delegation.archive()` and send it to your user - The end user instance of the client should not need to call `client.login(email)`, as it is not claiming any delegations via email address (but rather getting the delegation directly from your backend) - Once your user receives the delegation, they can deserialize it using [`ucanto.Delegation.extract()`](https://github.com/web3-storage/ucanto/blob/c8999a59852b61549d163532a83bac62290b629d/packages/core/src/delegation.js#L399) and pass it in using `client.addSpace()`, and from there they can run any of the `upload` methods - - Note that this alone does not give visibility into which of your end users are uploading what; to track this, you'll probably need them to send you that information separately (e.g., once they've run `upload` and get back a content CID, you can have them send that CID to you for tracking) + - Note that this alone does not give visibility into which of your end users are uploading what; to track this, you'll probably need them to send you that information separately (e.g., once they've run `upload` and get back a content CID, you can have them send that CID to you for tracking) - A code example that does this can be found below ```js -import { CarReader } from '@ipld/car'; -import * as DID from '@ipld/dag-ucan/did'; -import * as Delegation from '@ucanto/core/delegation'; -import { importDAG } from '@ucanto/core/delegation'; -import * as Signer from '@ucanto/principal/ed25519'; -import * as Client from '@web3-storage/w3up-client'; +import { CarReader } from '@ipld/car' +import * as DID from '@ipld/dag-ucan/did' +import * as Delegation from '@ucanto/core/delegation' +import { importDAG } from '@ucanto/core/delegation' +import * as Signer from '@ucanto/principal/ed25519' +import * as Client from '@web3-storage/w3up-client' async function backend(did: string) { // Load client with specific private key - const principal = Signer.parse(process.env.KEY); - const client = await Client.create({ principal }); + const principal = Signer.parse(process.env.KEY) + const client = await Client.create({ principal }) // Add proof that this agent has been delegated capabilities on the space - const proof = await parseProof(process.env.PROOF); - const space = await client.addSpace(proof); - await client.setCurrentSpace(space.did()); + const proof = await parseProof(process.env.PROOF) + const space = await client.addSpace(proof) + await client.setCurrentSpace(space.did()) // Create a delegation for a specific DID - const audience = DID.parse(did); - const abilities = ['blob/add', 'index/add', 'filecoin/offer', 'upload/add']; - const expiration = Math.floor(Date.now() / 1000) + 60 * 60 * 24; // 24 hours from now + const audience = DID.parse(did) + const abilities = ['blob/add', 'index/add', 'filecoin/offer', 'upload/add'] + const expiration = Math.floor(Date.now() / 1000) + 60 * 60 * 24 // 24 hours from now const delegation = await client.createDelegation(audience, abilities, { expiration, - }); + }) // Serialize the delegation and send it to the client - const archive = await delegation.archive(); - return archive.ok; + const archive = await delegation.archive() + return archive.ok } /** @param {string} data Base64 encoded CAR file */ async function parseProof(data) { - const blocks = []; - const reader = await CarReader.fromBytes(Buffer.from(data, 'base64')); + const blocks = [] + const reader = await CarReader.fromBytes(Buffer.from(data, 'base64')) for await (const block of reader.blocks()) { - blocks.push(block); + blocks.push(block) } - return importDAG(blocks); + return importDAG(blocks) } async function frontend() { // Create a new client - const client = await Client.create(); + const client = await Client.create() // Fetch the delegation from the backend - const apiUrl = `/api/w3up-delegation/${client.agent().did()}`; - const response = await fetch(apiUrl); - const data = await response.arrayBuffer(); + const apiUrl = `/api/w3up-delegation/${client.agent().did()}` + const response = await fetch(apiUrl) + const data = await response.arrayBuffer() // Deserialize the delegation - const delegation = await Delegation.extract(new Uint8Array(data)); + const delegation = await Delegation.extract(new Uint8Array(data)) if (!delegation.ok) { - throw new Error('Failed to extract delegation'); + throw new Error('Failed to extract delegation') } // Add proof that this agent has been delegated capabilities on the space - const space = await client.addSpace(delegation.ok); - client.setCurrentSpace(space.did()); + const space = await client.addSpace(delegation.ok) + client.setCurrentSpace(space.did()) // READY to go! } ``` #### User-owned + ```mermaid sequenceDiagram participant User @@ -391,9 +377,10 @@ sequenceDiagram User->>web3.storage w3up service: (If needed) Use Agent email verification to "log in" to Space User->>web3.storage w3up service: Upload data using w3up-client ``` + - If you want your user to own their own Space, you'll likely be relying on the `w3up-client` methods to create a Space, authorize the Space, and authorize the Agent on the end user-side; from there they can run any of the `upload` methods - - Doing this does take some of the UX out of your control; for instance, when web3.storage fully launches with w3up, your users will have to set up their payment methods with web3.storage - - Note that this alone does not give visibility into which of your end users are uploading what; to track this, you'll probably need them to send you that information separately (e.g., once they've run `upload` and get back a content CID, you can have them send that CID to you for tracking) + - Doing this does take some of the UX out of your control; for instance, when web3.storage fully launches with w3up, your users will have to set up their payment methods with web3.storage + - Note that this alone does not give visibility into which of your end users are uploading what; to track this, you'll probably need them to send you that information separately (e.g., once they've run `upload` and get back a content CID, you can have them send that CID to you for tracking) - There is a world of possibilities with your users "bringing their own identity" for their Space; you could explore how crypto wallet private keys, Apple Passkey, and more might map to Space DIDs and have the client use those - If you have code snippet(s) that works for you, please share them in a PR or [Github issue](https://github.com/web3-storage/w3up/issues) and we'll link them here! @@ -431,7 +418,7 @@ We created a `esbuild-plugin` [esbuild-plugin-w3up-client-wasm-import](https://g - [`capability.index.add`](#capabilityindexadd) - [`capability.plan.get`](#capabilityplanget) - [`capability.plan.set`](#capabilityplanset) - - [`capability.plan.createAdminSession`](#capabilityplancreateadminsession) + - [`capability.plan.createAdminSession`](#capabilityplancreateadminsession) - [`capability.space.info`](#capabilityspaceinfo) - [`capability.upload.add`](#capabilityuploadadd) - [`capability.upload.list`](#capabilityuploadlist) @@ -456,7 +443,7 @@ We created a `esbuild-plugin` [esbuild-plugin-w3up-client-wasm-import](https://g ### `create` ```ts -function create (options?: ClientFactoryOptions): Promise +function create(options?: ClientFactoryOptions): Promise ``` Create a new w3up client. @@ -472,7 +459,7 @@ More information: [`ClientFactoryOptions`](#clientfactoryoptions) ### `uploadDirectory` ```ts -function uploadDirectory ( +function uploadDirectory( files: File[], options: { retries?: number @@ -491,7 +478,7 @@ More information: [`ShardStoredCallback`](#shardstoredcallback) ### `uploadFile` ```ts -function uploadFile ( +function uploadFile( file: Blob, options: { retries?: number @@ -510,7 +497,7 @@ More information: [`ShardStoredCallback`](#shardstoredcallback) ### `uploadCAR` ```ts -function uploadCAR ( +function uploadCAR( car: Blob, options: { retries?: number @@ -530,7 +517,7 @@ More information: [`ShardStoredCallback`](#shardstoredcallback) ### `agent` ```ts -function agent (): Signer +function agent(): Signer ``` The user agent. The agent is a signer - an entity that can sign UCANs with keys from a `Principal` using a signing algorithm. @@ -538,7 +525,10 @@ The user agent. The agent is a signer - an entity that can sign UCANs with keys ### `authorize` ```ts -function authorize (email: string, options?: { signal?: AbortSignal }): Promise +function authorize( + email: string, + options?: { signal?: AbortSignal } +): Promise ``` Authorize the current agent to use capabilities granted to the passed email account. @@ -546,7 +536,7 @@ Authorize the current agent to use capabilities granted to the passed email acco ### `accounts` ```ts -function accounts (): Record +function accounts(): Record ``` List all accounts the agent has stored access to. @@ -554,7 +544,7 @@ List all accounts the agent has stored access to. ### `currentSpace` ```ts -function currentSpace (): Space|undefined +function currentSpace(): Space | undefined ``` The current space in use by the agent. @@ -562,7 +552,7 @@ The current space in use by the agent. ### `setCurrentSpace` ```ts -function setCurrentSpace (did: DID): Promise +function setCurrentSpace(did: DID): Promise ``` Use a specific space. @@ -570,7 +560,7 @@ Use a specific space. ### `spaces` ```ts -function spaces (): Space[] +function spaces(): Space[] ``` Spaces available to this agent. @@ -578,7 +568,7 @@ Spaces available to this agent. ### `createSpace` ```ts -async function createSpace (name?: string): Promise +async function createSpace(name?: string): Promise ``` Create a new space with an optional name. @@ -586,7 +576,7 @@ Create a new space with an optional name. ### `addSpace` ```ts -async function addSpace (proof: Delegation): Promise +async function addSpace(proof: Delegation): Promise ``` Add a space from a received proof. Proofs are delegations with an _audience_ matching the agent DID. @@ -594,7 +584,7 @@ Add a space from a received proof. Proofs are delegations with an _audience_ mat ### `proofs` ```ts -function proofs (capabilities?: Capability[]): Delegation[] +function proofs(capabilities?: Capability[]): Delegation[] ``` Get all the proofs matching the capabilities. Proofs are delegations with an _audience_ matching the agent DID. @@ -602,7 +592,7 @@ Get all the proofs matching the capabilities. Proofs are delegations with an _au ### `addProof` ```ts -function addProof (proof: Delegation): Promise +function addProof(proof: Delegation): Promise ``` Add a proof to the agent. Proofs are delegations with an _audience_ matching the agent DID. Note: `addSpace` should be used for delegating from `Space` to `Agent` (i.e., you want the Agent to fully be able to act on behalf of the Space), as it calls `addProof` with some additional client logic. `addProof` is for more generically adding delegations to the Agent (e.g., delegation targets a resource _other_ than a Space). @@ -610,7 +600,7 @@ Add a proof to the agent. Proofs are delegations with an _audience_ matching the ### `delegations` ```ts -function delegations (capabilities?: Capability[]): Delegation[] +function delegations(capabilities?: Capability[]): Delegation[] ``` Get delegations created by the agent for others. Filtered optionally by capability. @@ -618,7 +608,7 @@ Get delegations created by the agent for others. Filtered optionally by capabili ### `createDelegation` ```ts -function createDelegation ( +function createDelegation( audience: Principal, abilities: string[], options?: UCANOptions @@ -645,9 +635,7 @@ Removes association of a content CID with the space. Optionally, also removes as ### `getReceipt` ```ts -function getReceipt ( - taskCid: CID -): Promise +function getReceipt(taskCid: CID): Promise ``` Get a receipt for an executed task by its CID. @@ -655,7 +643,7 @@ Get a receipt for an executed task by its CID. ### `capability.access.authorize` ```ts -function authorize ( +function authorize( email: string, options: { signal?: AbortSignal } = {} ): Promise @@ -666,7 +654,7 @@ Authorize the current agent to use capabilities granted to the passed email acco ### `capability.access.claim` ```ts -function claim (): Promise[]> +function claim(): Promise[]> ``` Claim delegations granted to the account associated with this agent. Note: the received delegations are added to the agent's persistent store. @@ -674,7 +662,7 @@ Claim delegations granted to the account associated with this agent. Note: the r ### `capability.blob.add` ```ts -function add ( +function add( blob: Blob, options: { retries?: number; signal?: AbortSignal } = {} ): Promise @@ -685,7 +673,7 @@ Store a blob to the service. ### `capability.blob.list` ```ts -function list ( +function list( options: { retries?: number; signal?: AbortSignal } = {} ): Promise> ``` @@ -697,7 +685,7 @@ More information: [`BlobListResult`](#bloblistresult), [`ListResponse`](#listres ### `capability.blob.remove` ```ts -function remove ( +function remove( digest: MultihashDigest, options: { retries?: number; signal?: AbortSignal } = {} ): Promise @@ -721,9 +709,7 @@ Required delegated capability proofs: `index/add` ### `capability.plan.get` ```ts -function get ( - account: AccountDID -): Promise +function get(account: AccountDID): Promise ``` Get information about an account's billing plan. @@ -731,10 +717,7 @@ Get information about an account's billing plan. ### `capability.plan.set` ```ts -function set ( - account: AccountDID, - product: DID -): Promise<{}> +function set(account: AccountDID, product: DID): Promise<{}> ``` Switch an account's "plan" to the given product. **This may result in @@ -744,20 +727,20 @@ type of change.** ### `capability.plan.createAdminSession` ```ts -function createAdminSession ( +function createAdminSession( account: AccountDID, returnURL: string -): Promise<{url: string}> +): Promise<{ url: string }> ``` -Create a billing customer portal admin session. Returns a URL that -the customer can visit to administer `account`. Design and implementation driven +Create a billing customer portal admin session. Returns a URL that +the customer can visit to administer `account`. Design and implementation driven by our Stripe integration and may not be supported by all billing providers. ### `capability.upload.add` ```ts -function add ( +function add( root: CID, shards: CID[], options: { retries?: number; signal?: AbortSignal } = {} @@ -792,10 +775,7 @@ Remove a upload by root data CID. ### `capability.filecoin.offer` ```ts -function offer ( - content: CID, - piece: PieceLink, -): Promise +function offer(content: CID, piece: PieceLink): Promise ``` Offer a Filecoin "piece" to be added to an aggregate that will be offered for Filecoin deal(s). @@ -803,9 +783,7 @@ Offer a Filecoin "piece" to be added to an aggregate that will be offered for Fi ### `capability.filecoin.info` ```ts -function info ( - piece: PieceLink -): Promise +function info(piece: PieceLink): Promise ``` Get know deals and aggregate info of a Filecoin "piece" previously offered. @@ -840,7 +818,7 @@ export interface Capability< nb?: Caveats } -export type Ability = `${string}/${string}` | "*" +export type Ability = `${string}/${string}` | '*' export type Resource = `${string}:${string}` ``` @@ -889,7 +867,7 @@ export interface Delegation extends CoreDelegation { * User defined delegation metadata. */ meta(): Record -} +} ``` The `Delegation` type in `w3up-client` extends the `Delegation` type defined by [`ucanto`][ucanto]: @@ -959,26 +937,24 @@ An object representing a storage location. Spaces must be [registered](#register ```ts interface Space { - /** * The given space name. - */ + */ name(): string - + /** * The DID of the space. - */ + */ did(): string - + /** * Whether the space has been registered with the service. - */ + */ registered(): boolean - - + /** * User defined space metadata. - */ + */ meta(): Record } ``` @@ -1007,7 +983,6 @@ Dual-licensed under [MIT + Apache 2.0](https://github.com/web3-storage/w3up/blob [ucanto]: https://github.com/web3-storage/ucanto [car-spec]: https://ipld.io/specs/transport/car/ [web3storage-docs-cars]: https://web3.storage/docs/concepts/car/ - [docs]: https://web3-storage.github.io/w3up/modules/_web3_storage_w3up_client.html [docs-Client]: https://web3-storage.github.io/w3up/classes/_web3_storage_w3up_client.Client.html [docs-Client#agent]: https://web3-storage.github.io/w3up/classes/_web3_storage_w3up_client.Client.html#agent @@ -1017,8 +992,6 @@ Dual-licensed under [MIT + Apache 2.0](https://github.com/web3-storage/w3up/blob [docs-Client#uploadFile]: https://web3-storage.github.io/w3up/classes/_web3_storage_w3up_client.Client.html#uploadFile [docs-Client#uploadDirectory]: https://web3-storage.github.io/w3up/classes/_web3_storage_w3up_client.Client.html#uploadDirectory [docs-Space]: https://web3-storage.github.io/w3up/modules/_web3_storage_access.Space.html - [docs-create]: #create [docs-ClientFactoryOptions]: https://web3-storage.github.io/w3up/interfaces/_web3_storage_w3up_client.unknown.ClientFactoryOptions.html - [access-docs-Agent]: https://web3-storage.github.io/w3up/classes/_web3_storage_access.Agent.html From eed96934a39a4822fa84799a6c26d6f46b984c30 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Thu, 12 Sep 2024 16:03:41 -0300 Subject: [PATCH 06/12] chore: match pnpm version --- .github/workflows/w3up-client.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/w3up-client.yml b/.github/workflows/w3up-client.yml index 4d80288e6..f8f9e49fd 100644 --- a/.github/workflows/w3up-client.yml +++ b/.github/workflows/w3up-client.yml @@ -32,7 +32,7 @@ jobs: - name: Install uses: pnpm/action-setup@v4 with: - version: 9 + version: 9.x.x - name: Setup uses: actions/setup-node@v3 with: From 33a754b96051525f3233bdceedf209ad2eb92fe9 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Fri, 13 Sep 2024 09:13:01 -0300 Subject: [PATCH 07/12] Update packages/w3up-client/src/client.js Co-authored-by: Alan Shaw --- packages/w3up-client/src/client.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 76a9a648f..9e4acd604 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -271,9 +271,7 @@ export class Client extends Base { }) if (result.error) { - throw new Error( - `⚠️ Failed to authorize recovery account: ${result.error.name}:${result.error.message}` - ) + throw new Error(`failed to authorize recovery account: ${result.error.message}`, { cause: result.error }) } } return space From d7422d6560e0de8261e615ac5ad3bd02c627e34b Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Fri, 13 Sep 2024 09:13:10 -0300 Subject: [PATCH 08/12] Update packages/w3up-client/src/client.js Co-authored-by: Alan Shaw --- packages/w3up-client/src/client.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 9e4acd604..716bddfe9 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -253,9 +253,7 @@ export class Client extends Base { // Provision the account with the space const provisionResult = await account.provision(space.did()) if (provisionResult.error) { - throw new Error( - `⚠️ Failed to provision account: ${provisionResult.error.name}:${provisionResult.error.message}` - ) + throw new Error(`failed to provision account: ${provisionResult.error.message}`, { cause: provisionResult.error }) } // Save the space to authorize the client to use the space From 0e6d68323eb8ae7ce84eb8caa5831e928cfbd578 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Fri, 13 Sep 2024 09:41:58 -0300 Subject: [PATCH 09/12] chore: revert formatting changes --- packages/w3up-client/README.md | 195 +++++++++++++++++---------------- 1 file changed, 102 insertions(+), 93 deletions(-) diff --git a/packages/w3up-client/README.md b/packages/w3up-client/README.md index b888780e4..b773d878b 100644 --- a/packages/w3up-client/README.md +++ b/packages/w3up-client/README.md @@ -9,15 +9,16 @@ ## About + `@web3-storage/w3up-client` is a JavaScript library that provides a convenient interface to the w3up platform, a simple "on-ramp" to the content-addressed decentralized IPFS network. This library is the user-facing "porcelain" client for interacting with w3up services from JavaScript. It wraps the lower-level [`@web3-storage/access`][access-client-github] and [`@web3-storage/upload-client`][upload-client-github] client packages, which target individual w3up services. We recommend using `w3up-client` instead of using those "plumbing" packages directly, but you may find them useful if you need more context on w3up's architecture and internals. **`w3up-client` requires Node 18 or higher**. -> ⚠️❗ **Public Data** 🌎: All data uploaded to w3up is available to anyone who requests it using the correct CID. Do not store any private or sensitive information in an unencrypted form using w3up. +> ⚠️❗ __Public Data__ 🌎: All data uploaded to w3up is available to anyone who requests it using the correct CID. Do not store any private or sensitive information in an unencrypted form using w3up. -> ⚠️❗ **Permanent Data** ♾️: Removing files from w3up will remove them from the file listing for your account, but that doesn’t prevent nodes on the decentralized storage network from retaining copies of the data indefinitely. Do not use w3up for data that may need to be permanently deleted in the future. +> ⚠️❗ __Permanent Data__ ♾️: Removing files from w3up will remove them from the file listing for your account, but that doesn’t prevent nodes on the decentralized storage network from retaining copies of the data indefinitely. Do not use w3up for data that may need to be permanently deleted in the future. - [Install](#install) - [Usage](#usage) @@ -91,11 +92,11 @@ flowchart TD All uses of `w3up-client` to upload with web3.storage follow the flow above. This section shows the most basic way to use the client to start storing data. For more complex integration options, check out the [integration options][https://github.com/web3-storage/w3up/blob/main/packages/w3up-client/README.md#integration-options] docs. For reference, check out the [API reference docs][docs] or the source code of the [`w3cli` package][w3cli-github], which uses `w3up-client` throughout. -> By you or your users registering a w3up Space via email confirmation with [web3.storage](http://web3.storage), you agree to the [Terms of Service](https://web3.storage/docs/terms/). +> By you or your users registering a w3up Space via email confirmation with [web3.storage](http://web3.storage), you agree to the [Terms of Service](https://web3.storage/docs/terms/). #### Creating a client object -The package provides a [static `create` function][docs-create] that returns a [`Client` object][docs-Client]. +The package provides a [static `create` function][docs-create] that returns a [`Client` object][docs-Client]. ```js import { create } from '@web3-storage/w3up-client' @@ -185,7 +186,7 @@ import { CarReader } from '@ipld/car' import * as Client from '@web3-storage/w3up-client' import { StoreMemory } from '@web3-storage/w3up-client/stores/memory' -async function main() { +async function main () { // from "bring your own Agent" example in `Creating a client object" section` // used command line to generate KEY and PROOF (stored in env variables) // KEY: `npx ucan-key ed --json` in command line, which returns private key and DID for Agent (the private key is stored in KEY) @@ -193,17 +194,17 @@ async function main() { const principal = Signer.parse(process.env.KEY) const store = new StoreMemory() const client = await Client.create({ principal, store }) - + // now give Agent the delegation from the Space const proof = await parseProof(process.env.PROOF) const space = await client.addSpace(proof) await client.setCurrentSpace(space.did()) - + // READY to go! } /** @param {string} data Base64 encoded CAR file */ -async function parseProof(data) { +async function parseProof (data) { const blocks = [] const reader = await CarReader.fromBytes(Buffer.from(data, 'base64')) for await (const block of reader.blocks()) { @@ -221,7 +222,7 @@ Call [`uploadFile`][docs-Client#uploadFile] to upload a single file, or [`upload `uploadFile` expects a "Blob like" input, which can be a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) when running in a browser. On node.js, see the [`filesFromPath` library](https://github.com/web3-storage/files-from-path), which can load compatible objects from the local filesystem. -`uploadDirectory` requires `File`-like objects instead of `Blob`s, as the file's `name` property is used to build the directory hierarchy. +`uploadDirectory` requires `File`-like objects instead of `Blob`s, as the file's `name` property is used to build the directory hierarchy. You can control the directory layout and create nested directory structures by using `/` delimited paths in your filenames: @@ -246,10 +247,10 @@ In the example above, `directoryCid` resolves to an IPFS directory with the foll └── main.py ``` + ### Integration options As mentioned, UCAN opens up a number of options in how to integrate with w3up: Should you, the developer, own the Space? Should you delegate permissions to your users? Or should your user own their own Space? Broadly, there are three ways to integrate: - - (Simplest) Client-server: You (the developer) own the Space, and your user uploads to your backend infra before you upload it to the service - (More complex) Delegated: You own the Space, but you give a delegated UCAN token to your user's Agent to upload directly to the service (rather than needing to touch the upload in your backend) - (Most complex) User-owned: Your user owns the Space and registers it and they use it to upload directly with the service; if you want to instrument visibility into what they’re uploading, you’ll have to write separate code in your app for it @@ -257,7 +258,6 @@ As mentioned, UCAN opens up a number of options in how to integrate with w3up: S You can implement each of these in a number of ways, but we talk through some considerations when implementing a given option. #### Client-server - ```mermaid sequenceDiagram participant User @@ -265,7 +265,6 @@ sequenceDiagram User->>w3up-client in backend: Upload data w3up-client in backend->>web3.storage w3up service: Upload data ``` - - For your backend to be scalable, you might consider using serverless workers or a queue in front of a server - In either case, you'll need a registered Space, and your client instance in your backend to have an Agent with a delegation from this Space - (Recommended) It's likely easiest to create and register your Space using [w3cli](https://github.com/web3-storage/w3cli) rather than using `w3up-client` to do so (especially if your backend isn't persistent); you can then generate your own Agent and delegate the ability to upload to your Space using something like [this example](#bringing-your-own-agent-and-delegation) @@ -273,7 +272,6 @@ sequenceDiagram - After this, once your user uploads data to your backend, you can run any of the `upload` methods #### Delegated - ```mermaid sequenceDiagram participant w3up-client in user @@ -285,11 +283,10 @@ sequenceDiagram w3up-client in backend->>w3up-client in user: Send delegation from Space to user's Agent DID w3up-client in user->>web3.storage w3up service: Upload data ``` - - You will likely have `w3up-client` running in your end-user's client code, as well as backend code that's able to generate UCANs that delegate the ability to upload and pass them to your users (e.g., `w3up-client` running in a serverless worker) - For your backend to be scalable, you might consider using serverless workers or a queue in front of a server - As the developer, you'll need a registered Space, and your client instance in your backend to have an Agent with a delegation from this Space - - (Recommended) It's likely easiest to create and register your Space using [w3cli](https://github.com/web3-storage/w3cli) rather than using `w3up-client` to do so (especially if your backend isn't persistent); you can then generate your own Agent and delegate the ability to upload to your Space using something like [this example](#bringing-your-own-agent-and-delegation) + - (Recommended) It's likely easiest to create and register your Space using [w3cli](https://github.com/web3-storage/w3cli) rather than using `w3up-client` to do so (especially if your backend isn't persistent); you can then generate your own Agent and delegate the ability to upload to your Space using something like [this example](#bringing-your-own-agent-and-delegation) - If your backend is persistent, you can do this or do everything in the client directly ([create Space](#creating-and-registering-spaces) and [get delegation](#delegating-from-space-to-agent)) - Your user does not need a registered Space - just an Agent with a delegation from your Space - `w3up-client` in the end user environment should have a unique Agent for each user, which should happen by default (since when `w3up-client` is instantiated it creates a new Agent anyway, or uses the one in local Store) @@ -298,75 +295,74 @@ sequenceDiagram - You can serialize this using `delegation.archive()` and send it to your user - The end user instance of the client should not need to call `client.login(email)`, as it is not claiming any delegations via email address (but rather getting the delegation directly from your backend) - Once your user receives the delegation, they can deserialize it using [`ucanto.Delegation.extract()`](https://github.com/web3-storage/ucanto/blob/c8999a59852b61549d163532a83bac62290b629d/packages/core/src/delegation.js#L399) and pass it in using `client.addSpace()`, and from there they can run any of the `upload` methods - - Note that this alone does not give visibility into which of your end users are uploading what; to track this, you'll probably need them to send you that information separately (e.g., once they've run `upload` and get back a content CID, you can have them send that CID to you for tracking) + - Note that this alone does not give visibility into which of your end users are uploading what; to track this, you'll probably need them to send you that information separately (e.g., once they've run `upload` and get back a content CID, you can have them send that CID to you for tracking) - A code example that does this can be found below ```js -import { CarReader } from '@ipld/car' -import * as DID from '@ipld/dag-ucan/did' -import * as Delegation from '@ucanto/core/delegation' -import { importDAG } from '@ucanto/core/delegation' -import * as Signer from '@ucanto/principal/ed25519' -import * as Client from '@web3-storage/w3up-client' +import { CarReader } from '@ipld/car'; +import * as DID from '@ipld/dag-ucan/did'; +import * as Delegation from '@ucanto/core/delegation'; +import { importDAG } from '@ucanto/core/delegation'; +import * as Signer from '@ucanto/principal/ed25519'; +import * as Client from '@web3-storage/w3up-client'; async function backend(did: string) { // Load client with specific private key - const principal = Signer.parse(process.env.KEY) - const client = await Client.create({ principal }) + const principal = Signer.parse(process.env.KEY); + const client = await Client.create({ principal }); // Add proof that this agent has been delegated capabilities on the space - const proof = await parseProof(process.env.PROOF) - const space = await client.addSpace(proof) - await client.setCurrentSpace(space.did()) + const proof = await parseProof(process.env.PROOF); + const space = await client.addSpace(proof); + await client.setCurrentSpace(space.did()); // Create a delegation for a specific DID - const audience = DID.parse(did) - const abilities = ['blob/add', 'index/add', 'filecoin/offer', 'upload/add'] - const expiration = Math.floor(Date.now() / 1000) + 60 * 60 * 24 // 24 hours from now + const audience = DID.parse(did); + const abilities = ['blob/add', 'index/add', 'filecoin/offer', 'upload/add']; + const expiration = Math.floor(Date.now() / 1000) + 60 * 60 * 24; // 24 hours from now const delegation = await client.createDelegation(audience, abilities, { expiration, - }) + }); // Serialize the delegation and send it to the client - const archive = await delegation.archive() - return archive.ok + const archive = await delegation.archive(); + return archive.ok; } /** @param {string} data Base64 encoded CAR file */ async function parseProof(data) { - const blocks = [] - const reader = await CarReader.fromBytes(Buffer.from(data, 'base64')) + const blocks = []; + const reader = await CarReader.fromBytes(Buffer.from(data, 'base64')); for await (const block of reader.blocks()) { - blocks.push(block) + blocks.push(block); } - return importDAG(blocks) + return importDAG(blocks); } async function frontend() { // Create a new client - const client = await Client.create() + const client = await Client.create(); // Fetch the delegation from the backend - const apiUrl = `/api/w3up-delegation/${client.agent().did()}` - const response = await fetch(apiUrl) - const data = await response.arrayBuffer() + const apiUrl = `/api/w3up-delegation/${client.agent().did()}`; + const response = await fetch(apiUrl); + const data = await response.arrayBuffer(); // Deserialize the delegation - const delegation = await Delegation.extract(new Uint8Array(data)) + const delegation = await Delegation.extract(new Uint8Array(data)); if (!delegation.ok) { - throw new Error('Failed to extract delegation') + throw new Error('Failed to extract delegation'); } // Add proof that this agent has been delegated capabilities on the space - const space = await client.addSpace(delegation.ok) - client.setCurrentSpace(space.did()) + const space = await client.addSpace(delegation.ok); + client.setCurrentSpace(space.did()); // READY to go! } ``` #### User-owned - ```mermaid sequenceDiagram participant User @@ -377,10 +373,9 @@ sequenceDiagram User->>web3.storage w3up service: (If needed) Use Agent email verification to "log in" to Space User->>web3.storage w3up service: Upload data using w3up-client ``` - - If you want your user to own their own Space, you'll likely be relying on the `w3up-client` methods to create a Space, authorize the Space, and authorize the Agent on the end user-side; from there they can run any of the `upload` methods - - Doing this does take some of the UX out of your control; for instance, when web3.storage fully launches with w3up, your users will have to set up their payment methods with web3.storage - - Note that this alone does not give visibility into which of your end users are uploading what; to track this, you'll probably need them to send you that information separately (e.g., once they've run `upload` and get back a content CID, you can have them send that CID to you for tracking) + - Doing this does take some of the UX out of your control; for instance, when web3.storage fully launches with w3up, your users will have to set up their payment methods with web3.storage + - Note that this alone does not give visibility into which of your end users are uploading what; to track this, you'll probably need them to send you that information separately (e.g., once they've run `upload` and get back a content CID, you can have them send that CID to you for tracking) - There is a world of possibilities with your users "bringing their own identity" for their Space; you could explore how crypto wallet private keys, Apple Passkey, and more might map to Space DIDs and have the client use those - If you have code snippet(s) that works for you, please share them in a PR or [Github issue](https://github.com/web3-storage/w3up/issues) and we'll link them here! @@ -418,7 +413,7 @@ We created a `esbuild-plugin` [esbuild-plugin-w3up-client-wasm-import](https://g - [`capability.index.add`](#capabilityindexadd) - [`capability.plan.get`](#capabilityplanget) - [`capability.plan.set`](#capabilityplanset) - - [`capability.plan.createAdminSession`](#capabilityplancreateadminsession) + - [`capability.plan.createAdminSession`](#capabilityplancreateadminsession) - [`capability.space.info`](#capabilityspaceinfo) - [`capability.upload.add`](#capabilityuploadadd) - [`capability.upload.list`](#capabilityuploadlist) @@ -443,7 +438,7 @@ We created a `esbuild-plugin` [esbuild-plugin-w3up-client-wasm-import](https://g ### `create` ```ts -function create(options?: ClientFactoryOptions): Promise +function create (options?: ClientFactoryOptions): Promise ``` Create a new w3up client. @@ -459,7 +454,7 @@ More information: [`ClientFactoryOptions`](#clientfactoryoptions) ### `uploadDirectory` ```ts -function uploadDirectory( +function uploadDirectory ( files: File[], options: { retries?: number @@ -478,7 +473,7 @@ More information: [`ShardStoredCallback`](#shardstoredcallback) ### `uploadFile` ```ts -function uploadFile( +function uploadFile ( file: Blob, options: { retries?: number @@ -497,7 +492,7 @@ More information: [`ShardStoredCallback`](#shardstoredcallback) ### `uploadCAR` ```ts -function uploadCAR( +function uploadCAR ( car: Blob, options: { retries?: number @@ -517,7 +512,7 @@ More information: [`ShardStoredCallback`](#shardstoredcallback) ### `agent` ```ts -function agent(): Signer +function agent (): Signer ``` The user agent. The agent is a signer - an entity that can sign UCANs with keys from a `Principal` using a signing algorithm. @@ -525,10 +520,7 @@ The user agent. The agent is a signer - an entity that can sign UCANs with keys ### `authorize` ```ts -function authorize( - email: string, - options?: { signal?: AbortSignal } -): Promise +function authorize (email: string, options?: { signal?: AbortSignal }): Promise ``` Authorize the current agent to use capabilities granted to the passed email account. @@ -536,7 +528,7 @@ Authorize the current agent to use capabilities granted to the passed email acco ### `accounts` ```ts -function accounts(): Record +function accounts (): Record ``` List all accounts the agent has stored access to. @@ -544,7 +536,7 @@ List all accounts the agent has stored access to. ### `currentSpace` ```ts -function currentSpace(): Space | undefined +function currentSpace (): Space|undefined ``` The current space in use by the agent. @@ -552,7 +544,7 @@ The current space in use by the agent. ### `setCurrentSpace` ```ts -function setCurrentSpace(did: DID): Promise +function setCurrentSpace (did: DID): Promise ``` Use a specific space. @@ -560,7 +552,7 @@ Use a specific space. ### `spaces` ```ts -function spaces(): Space[] +function spaces (): Space[] ``` Spaces available to this agent. @@ -568,7 +560,7 @@ Spaces available to this agent. ### `createSpace` ```ts -async function createSpace(name?: string): Promise +async function createSpace(name?: string, options?: {account: Account}): Promise ``` Create a new space with an optional name. @@ -576,7 +568,7 @@ Create a new space with an optional name. ### `addSpace` ```ts -async function addSpace(proof: Delegation): Promise +async function addSpace (proof: Delegation): Promise ``` Add a space from a received proof. Proofs are delegations with an _audience_ matching the agent DID. @@ -584,7 +576,7 @@ Add a space from a received proof. Proofs are delegations with an _audience_ mat ### `proofs` ```ts -function proofs(capabilities?: Capability[]): Delegation[] +function proofs (capabilities?: Capability[]): Delegation[] ``` Get all the proofs matching the capabilities. Proofs are delegations with an _audience_ matching the agent DID. @@ -592,7 +584,7 @@ Get all the proofs matching the capabilities. Proofs are delegations with an _au ### `addProof` ```ts -function addProof(proof: Delegation): Promise +function addProof (proof: Delegation): Promise ``` Add a proof to the agent. Proofs are delegations with an _audience_ matching the agent DID. Note: `addSpace` should be used for delegating from `Space` to `Agent` (i.e., you want the Agent to fully be able to act on behalf of the Space), as it calls `addProof` with some additional client logic. `addProof` is for more generically adding delegations to the Agent (e.g., delegation targets a resource _other_ than a Space). @@ -600,7 +592,7 @@ Add a proof to the agent. Proofs are delegations with an _audience_ matching the ### `delegations` ```ts -function delegations(capabilities?: Capability[]): Delegation[] +function delegations (capabilities?: Capability[]): Delegation[] ``` Get delegations created by the agent for others. Filtered optionally by capability. @@ -608,7 +600,7 @@ Get delegations created by the agent for others. Filtered optionally by capabili ### `createDelegation` ```ts -function createDelegation( +function createDelegation ( audience: Principal, abilities: string[], options?: UCANOptions @@ -635,7 +627,9 @@ Removes association of a content CID with the space. Optionally, also removes as ### `getReceipt` ```ts -function getReceipt(taskCid: CID): Promise +function getReceipt ( + taskCid: CID +): Promise ``` Get a receipt for an executed task by its CID. @@ -643,7 +637,7 @@ Get a receipt for an executed task by its CID. ### `capability.access.authorize` ```ts -function authorize( +function authorize ( email: string, options: { signal?: AbortSignal } = {} ): Promise @@ -654,7 +648,7 @@ Authorize the current agent to use capabilities granted to the passed email acco ### `capability.access.claim` ```ts -function claim(): Promise[]> +function claim (): Promise[]> ``` Claim delegations granted to the account associated with this agent. Note: the received delegations are added to the agent's persistent store. @@ -662,7 +656,7 @@ Claim delegations granted to the account associated with this agent. Note: the r ### `capability.blob.add` ```ts -function add( +function add ( blob: Blob, options: { retries?: number; signal?: AbortSignal } = {} ): Promise @@ -673,7 +667,7 @@ Store a blob to the service. ### `capability.blob.list` ```ts -function list( +function list ( options: { retries?: number; signal?: AbortSignal } = {} ): Promise> ``` @@ -685,7 +679,7 @@ More information: [`BlobListResult`](#bloblistresult), [`ListResponse`](#listres ### `capability.blob.remove` ```ts -function remove( +function remove ( digest: MultihashDigest, options: { retries?: number; signal?: AbortSignal } = {} ): Promise @@ -709,7 +703,9 @@ Required delegated capability proofs: `index/add` ### `capability.plan.get` ```ts -function get(account: AccountDID): Promise +function get ( + account: AccountDID +): Promise ``` Get information about an account's billing plan. @@ -717,7 +713,10 @@ Get information about an account's billing plan. ### `capability.plan.set` ```ts -function set(account: AccountDID, product: DID): Promise<{}> +function set ( + account: AccountDID, + product: DID +): Promise<{}> ``` Switch an account's "plan" to the given product. **This may result in @@ -727,20 +726,20 @@ type of change.** ### `capability.plan.createAdminSession` ```ts -function createAdminSession( +function createAdminSession ( account: AccountDID, returnURL: string -): Promise<{ url: string }> +): Promise<{url: string}> ``` -Create a billing customer portal admin session. Returns a URL that -the customer can visit to administer `account`. Design and implementation driven +Create a billing customer portal admin session. Returns a URL that +the customer can visit to administer `account`. Design and implementation driven by our Stripe integration and may not be supported by all billing providers. ### `capability.upload.add` ```ts -function add( +function add ( root: CID, shards: CID[], options: { retries?: number; signal?: AbortSignal } = {} @@ -775,7 +774,10 @@ Remove a upload by root data CID. ### `capability.filecoin.offer` ```ts -function offer(content: CID, piece: PieceLink): Promise +function offer ( + content: CID, + piece: PieceLink, +): Promise ``` Offer a Filecoin "piece" to be added to an aggregate that will be offered for Filecoin deal(s). @@ -783,7 +785,9 @@ Offer a Filecoin "piece" to be added to an aggregate that will be offered for Fi ### `capability.filecoin.info` ```ts -function info(piece: PieceLink): Promise +function info ( + piece: PieceLink +): Promise ``` Get know deals and aggregate info of a Filecoin "piece" previously offered. @@ -818,7 +822,7 @@ export interface Capability< nb?: Caveats } -export type Ability = `${string}/${string}` | '*' +export type Ability = `${string}/${string}` | "*" export type Resource = `${string}:${string}` ``` @@ -867,7 +871,7 @@ export interface Delegation extends CoreDelegation { * User defined delegation metadata. */ meta(): Record -} +} ``` The `Delegation` type in `w3up-client` extends the `Delegation` type defined by [`ucanto`][ucanto]: @@ -937,24 +941,26 @@ An object representing a storage location. Spaces must be [registered](#register ```ts interface Space { + /** * The given space name. - */ + */ name(): string - + /** * The DID of the space. - */ + */ did(): string - + /** * Whether the space has been registered with the service. - */ + */ registered(): boolean - + + /** * User defined space metadata. - */ + */ meta(): Record } ``` @@ -983,6 +989,7 @@ Dual-licensed under [MIT + Apache 2.0](https://github.com/web3-storage/w3up/blob [ucanto]: https://github.com/web3-storage/ucanto [car-spec]: https://ipld.io/specs/transport/car/ [web3storage-docs-cars]: https://web3.storage/docs/concepts/car/ + [docs]: https://web3-storage.github.io/w3up/modules/_web3_storage_w3up_client.html [docs-Client]: https://web3-storage.github.io/w3up/classes/_web3_storage_w3up_client.Client.html [docs-Client#agent]: https://web3-storage.github.io/w3up/classes/_web3_storage_w3up_client.Client.html#agent @@ -992,6 +999,8 @@ Dual-licensed under [MIT + Apache 2.0](https://github.com/web3-storage/w3up/blob [docs-Client#uploadFile]: https://web3-storage.github.io/w3up/classes/_web3_storage_w3up_client.Client.html#uploadFile [docs-Client#uploadDirectory]: https://web3-storage.github.io/w3up/classes/_web3_storage_w3up_client.Client.html#uploadDirectory [docs-Space]: https://web3-storage.github.io/w3up/modules/_web3_storage_access.Space.html + [docs-create]: #create [docs-ClientFactoryOptions]: https://web3-storage.github.io/w3up/interfaces/_web3_storage_w3up_client.unknown.ClientFactoryOptions.html + [access-docs-Agent]: https://web3-storage.github.io/w3up/classes/_web3_storage_access.Agent.html From b0bd02e2dab2683e4b2aa0bc43b460dbfa776a7d Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Fri, 13 Sep 2024 09:51:40 -0300 Subject: [PATCH 10/12] fix client tests --- packages/w3up-client/test/client.test.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index c8dbfbe9f..3cfb2d6cd 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -353,8 +353,7 @@ export const testClient = { account: aliceAccount, }), { - message: - '⚠️ Failed to provision account: ProvisionError:Provisioning failed', + message: 'failed to provision account: Provisioning failed', } ) @@ -386,8 +385,7 @@ export const testClient = { account: aliceAccount, }), { - message: - '⚠️ Failed to authorize recovery account: DelegateError:Delegation failed', + message: 'failed to authorize recovery account: Delegation failed', } ) From 6f05cdfebd4c8ff0d6c626b9175d0ece7ca59c37 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Fri, 13 Sep 2024 09:55:39 -0300 Subject: [PATCH 11/12] update createSpace jsdocs --- packages/w3up-client/src/client.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 716bddfe9..aa3c77122 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -235,8 +235,9 @@ export class Client extends Base { /** * Create a new space with a given name. - * If an account is provided in the options argument, then it creates a delegated recovery account - * by provisioning the space and then delegating access to the recovery account. + * If an account is not provided, the space is created without any delegation and is not saved, hence it is a temporary space. + * When an account is provided in the options argument, then it creates a delegated recovery account + * by provisioning the space, saving it and then delegating access to the recovery account. * * @typedef {object} CreateOptions * @property {Account.Account} [account] @@ -253,7 +254,10 @@ export class Client extends Base { // Provision the account with the space const provisionResult = await account.provision(space.did()) if (provisionResult.error) { - throw new Error(`failed to provision account: ${provisionResult.error.message}`, { cause: provisionResult.error }) + throw new Error( + `failed to provision account: ${provisionResult.error.message}`, + { cause: provisionResult.error } + ) } // Save the space to authorize the client to use the space @@ -269,7 +273,10 @@ export class Client extends Base { }) if (result.error) { - throw new Error(`failed to authorize recovery account: ${result.error.message}`, { cause: result.error }) + throw new Error( + `failed to authorize recovery account: ${result.error.message}`, + { cause: result.error } + ) } } return space From a46d59d4185b8a903576a2e6fb165ab89ec7b445 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Fri, 13 Sep 2024 09:59:44 -0300 Subject: [PATCH 12/12] chore: version is optional if defined in pkg.json --- .github/workflows/w3up-client.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/w3up-client.yml b/.github/workflows/w3up-client.yml index f8f9e49fd..be8e554fc 100644 --- a/.github/workflows/w3up-client.yml +++ b/.github/workflows/w3up-client.yml @@ -31,8 +31,6 @@ jobs: uses: actions/checkout@v3 - name: Install uses: pnpm/action-setup@v4 - with: - version: 9.x.x - name: Setup uses: actions/setup-node@v3 with: