From 8cce55ba42dd40055e6683d2f5219d1898816b11 Mon Sep 17 00:00:00 2001 From: Oli Evans Date: Mon, 15 Jan 2024 16:27:09 +0000 Subject: [PATCH 1/3] feat: `w3 delegation create --stringify` and `w3 space add ` **export** a delegation as base64 encoded identity CID with `w3 delegation create --stringify` ```shell $ w3 delegation create did:key:z6MkviAsUfBwegmB57byQ7SZTFtX4jNjo31delegation create did:key:z6MkviAsUfBwegmB57byQ7SZTFtX4jNjo315EvgurjWYoTRX --can 'store/add' --can 'upload/add' --stringify mAYIEAO0OEaJlcm9vdHOAZ3ZlcnNpb24BvQUBcRIgL7w/mAWPOV8Wt/B0ygdaOI+20/ZG8pX+5YzZ5X9U4y6oYXNYRO2hA0CbPDJlxyrorHHdNAUnRUDA4xU7KHgHHstkM8tBxq+6KaQP5xLCknOh9TjkR0S0yuK/fiFxKwRDUHfECFEWQn4DYXZlMC45LjFjYXR0hqJjY2FuZ3NwYWNlLypkd2l0aHg4ZGlkOmtleTp6Nk1raGZ6VHdaSjI4YVJvYkNwNzZ1WFJxenNqSDZHTnUxOFdGd011bWtBRjVvaleiY2NhbmdzdG9yZS8qZHdpdGh4OGRpZDprZXk6ejZNa2hmelR3WkoyOGFSb2JDcDc2dVhScXpzakg2R051MThXRndNdW1rQUY1b2pXomNjYW5odXBsb2FkLypkd2l0aHg4ZGlkOmtleTp6Nk1raGZ6VHdaSjI4YVJvYkNwNzZ1WFJxenNqSDZHTnUxOFdGd011bWtBRjVvaleiY2NhbmhhY2Nlc3MvKmR3aXRoeDhkaWQ6a2V5Ono2TWtoZnpUd1pKMjhhUm9iQ3A3NnVYUnF6c2pINkdOdTE4V0Z3TXVta0FGNW9qV6JjY2FuamZpbGVjb2luLypkd2l0aHg4ZGlkOmtleTp6Nk1raGZ6VHdaSjI4YVJvYkNwNzZ1WFJxenNqSDZHTnUxOFdGd011bWtBRjVvaleiY2Nhbmd1c2FnZS8qZHdpdGh4OGRpZDprZXk6ejZNa2hmelR3WkoyOGFSb2JDcDc2dVhScXpzakg2R051MThXRndNdW1rQUY1b2pXY2F1ZFgi7QHob+19JDMUBs+u1e646vN2MLovQUXA7xJeFs2THUcb+mNleHAaZzctGmNmY3SBoWVzcGFjZaFkbmFtZWV0b290c2Npc3NYIu0BL9X+p4Uyz05zSH0ol8TYPXpwU9EljNRo1O18uYbWlvljcHJmgL0FAXESIC+8P5gFjzlfFrfwdMoHWjiPttP2RvKV/uWM2eV/VOMuqGFzWETtoQNAmzwyZccq6Kxx3TQFJ0VAwOMVOyh4Bx7LZDPLQcavuimkD+cSwpJzofU45EdEtMriv34hcSsEQ1B3xAhRFkJ+A2F2ZTAuOS4xY2F0dIaiY2NhbmdzcGFjZS8qZHdpdGh4OGRpZDprZXk6ejZNa2hmelR3WkoyOGFSb2JDcDc2dVhScXpzakg2R051MThXRndNdW1rQUY1b2pXomNjYW5nc3RvcmUvKmR3aXRoeDhkaWQ6a2V5Ono2TWtoZnpUd1pKMjhhUm9iQ3A3NnVYUnF6c2pINkdOdTE4V0Z3TXVta0FGNW9qV6JjY2FuaHVwbG9hZC8qZHdpdGh4OGRpZDprZXk6ejZNa2hmelR3WkoyOGFSb2JDcDc2dVhScXpzakg2R051MThXRndNdW1rQUY1b2pXomNjYW5oYWNjZXNzLypkd2l0aHg4ZGlkOmtleTp6Nk1raGZ6VHdaSjI4YVJvYkNwNzZ1WFJxenNqSDZHTnUxOFdGd011bWtBRjVvaleiY2NhbmpmaWxlY29pbi8qZHdpdGh4OGRpZDprZXk6ejZNa2hmelR3WkoyOGFSb2JDcDc2dVhScXpzakg2R051MThXRndNdW1rQUY1b2pXomNjYW5ndXNhZ2UvKmR3aXRoeDhkaWQ6a2V5Ono2TWtoZnpUd1pKMjhhUm9iQ3A3NnVYUnF6c2pINkdOdTE4V0Z3TXVta0FGNW9qV2NhdWRYIu0B6G/tfSQzFAbPrtXuuOrzdjC6L0FFwO8SXhbNkx1HG/pjZXhwGmc3LRpjZmN0gaFlc3BhY2WhZG5hbWVldG9vdHNjaXNzWCLtAS/V/qeFMs9Oc0h9KJfE2D16cFPRJYzUaNTtfLmG1pb5Y3ByZoDbAwFxEiB9iHpD1ttdKEQCvBZ8jJBD7Wqw1abOtYwNCKAKYALXMqhhc1hE7aEDQJ7U8I+a4Au/eb10r9T89weG/Nl2jccEUXHs8wq+i2tU0Iaik8KaKvovDqqE57JU8ZoY0JAzOBW7cMLGcV6/UwthdmUwLjkuMWNhdHSComNjYW5pc3RvcmUvYWRkZHdpdGh4OGRpZDprZXk6ejZNa2hmelR3WkoyOGFSb2JDcDc2dVhScXpzakg2R051MThXRndNdW1rQUY1b2pXomNjYW5qdXBsb2FkL2FkZGR3aXRoeDhkaWQ6a2V5Ono2TWtoZnpUd1pKMjhhUm9iQ3A3NnVYUnF6c2pINkdOdTE4V0Z3TXVta0FGNW9qV2NhdWRYIu0B8YzaLs8NDe7oZt6rlpsW6iMh8XsoXZLvkHPqtftYRXZjZXhw9mNmY3SBoWVzcGFjZaFkbmFtZWV0b290c2Npc3NYIu0B6G/tfSQzFAbPrtXuuOrzdjC6L0FFwO8SXhbNkx1HG/pjcHJmgtgqWCUAAXESIC+8P5gFjzlfFrfwdMoHWjiPttP2RvKV/uWM2eV/VOMu2CpYJQABcRIgL7w/mAWPOV8Wt/B0ygdaOI+20/ZG8pX+5YzZ5X9U4y4 ``` ...yes, what if we put the CAR _in_ the CID! That way we can detect when the input has been truncted, and the screed is self describing... you can paste it into cid.ipfs.tech and it'll tell you it's CAR flavour identity hashed bytes! **import** a space from a stringified proof ```shell $ w3 space $PROOF did:key:z6MkhfzTwZJ28aRobCp76uXRqzsjH6GNu18WFwMumkAF5ojW ``` see: https://github.com/web3-storage/w3cli/issues/154#issuecomment-1885042715 License: MIT Signed-off-by: Oli Evans --- bin.js | 6 ++++- index.js | 59 +++++++++++++++++++++++++++++++++++++----- lib.js | 23 +++++++++++++++++ test/bin.spec.js | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 8 deletions(-) diff --git a/bin.js b/bin.js index 6ed89d1..c8975fb 100755 --- a/bin.js +++ b/bin.js @@ -142,7 +142,7 @@ cli cli .command('space add ') .describe( - 'Add a space to the agent. The proof is a CAR encoded delegation to _this_ agent.' + 'Import a space from a proof: a CAR encoded UCAN delegation to this agent. proof is a stringified CAR or a filesystem path to one, as created by `w3 delegation create`' ) .action(addSpace) @@ -201,6 +201,10 @@ cli '-o, --output', 'Path of file to write the exported delegation data to.' ) + .option( + '--stringify', + 'Encode output as a string. Useful when saving it as an environment variable.' + ) .action(createDelegation) cli diff --git a/index.js b/index.js index da259fb..a0d1e80 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,9 @@ import fs from 'fs' import ora, { oraPromise } from 'ora' -import { Readable } from 'stream' +import { pipeline } from 'node:stream/promises' import { CID } from 'multiformats/cid' +import { base64 } from 'multiformats/bases/base64' +import { identity } from 'multiformats/hashes/identity' import * as DID from '@ipld/dag-ucan/did' import * as dagJSON from '@ipld/dag-json' import { CarWriter } from '@ipld/car' @@ -15,6 +17,7 @@ import { filesize, filesizeMB, readProof, + readProofFromBytes, uploadListResponseToString, startOfLastMonth, } from './lib.js' @@ -253,13 +256,36 @@ export async function createSpace(name) { } /** - * @param {string} proofPath + * @param {string} proofPathOrCid */ -export async function addSpace(proofPath) { +export async function addSpace(proofPathOrCid) { const client = await getClient() - const delegation = await readProof(proofPath) - const space = await client.addSpace(delegation) - console.log(space.did()) + + let cid + try { + cid = CID.parse(proofPathOrCid, base64) + } catch (/** @type {any} */ err) { + if (err?.message?.includes('Unexpected end of data')) { + console.error(`Error: failed to read proof. The string has been truncated.`) + process.exit(1) + } + /* otherwise, try as path */ + } + + if (cid) { + if (cid.multihash.code !== identity.code) { + console.error(`Error: failed to read proof. Must be identity CID. Fetching of remote proof CARs not supported by this command yet`) + process.exit(1) + } + const delegation = await readProofFromBytes(cid.multihash.digest) + const space = await client.addSpace(delegation) + console.log(space.did()) + + } else { + const delegation = await readProof(proofPathOrCid) + const space = await client.addSpace(delegation) + console.log(space.did()) + } } /** @@ -339,6 +365,7 @@ Providers: ${providers || chalk.dim('none')}`) * @param {number} [opts.expiration] * @param {string} [opts.output] * @param {string} [opts.with] + * @param {boolean} [opts.stringify] */ export async function createDelegation(audienceDID, opts) { const client = await getClient() @@ -367,7 +394,25 @@ export async function createDelegation(audienceDID, opts) { const { writer, out } = CarWriter.create() const dest = opts.output ? fs.createWriteStream(opts.output) : process.stdout - Readable.from(out).pipe(dest) + pipeline( + out, + async function* maybeBaseEncode(src) { + const chunks = [] + for await (const chunk of src) { + if (!opts.stringify) { + yield chunk + } else { + chunks.push(chunk) + } + } + if (!opts.stringify) return + const blob = new Blob(chunks) + const bytes = new Uint8Array(await blob.arrayBuffer()) + const idCid = CID.createV1(ucanto.CAR.code, identity.digest(bytes)) + yield idCid.toString(base64) + }, + dest + ) for (const block of delegation.export()) { // @ts-expect-error diff --git a/lib.js b/lib.js index 49b5cda..7cf1ab0 100644 --- a/lib.js +++ b/lib.js @@ -166,6 +166,29 @@ export async function readProof(path) { } } +/** + * @param {Uint8Array} bytes Path to the proof file. + */ +export async function readProofFromBytes(bytes) { + const blocks = [] + try { + const reader = await CarReader.fromBytes(bytes) + for await (const block of reader.blocks()) { + blocks.push(block) + } + } catch (/** @type {any} */ err) { + console.error(`Error: failed to parse proof: ${err.message}`) + process.exit(1) + } + try { + // @ts-expect-error + return importDAG(blocks) + } catch (/** @type {any} */ err) { + console.error(`Error: failed to import proof: ${err.message}`) + process.exit(1) + } +} + /** * @param {UploadListSuccess} res * @param {object} [opts] diff --git a/test/bin.spec.js b/test/bin.spec.js index e498849..d6abd26 100644 --- a/test/bin.spec.js +++ b/test/bin.spec.js @@ -18,6 +18,7 @@ import { UCAN, Provider } from '@web3-storage/capabilities' import * as ED25519 from '@ucanto/principal/ed25519' import { sha256, delegate } from '@ucanto/core' import * as Result from '@web3-storage/w3up-client/result' +import { base64 } from 'multiformats/bases/base64' const w3 = Command.create('./bin.js') @@ -349,6 +350,34 @@ export const testSpace = { assert.ok(listSome.output.includes(spaceDID)) }), + 'w3 space add `stringified proof car`': test(async (assert, context) => { + const { env } = context + const spaceDID = await loginAndCreateSpace(context, { env: env.alice }) + const whosBob = await w3.args(['whoami']).env(env.bob).join() + const bobDID = SpaceDID.from(whosBob.output.trim()) + const res = await w3 + .args([ + 'delegation', + 'create', + bobDID, + '-c', + 'store/*', + 'upload/*', + '--stringify' + ]) + .env(env.alice) + .join() + + const listNone = await w3.args(['space', 'ls']).env(env.bob).join() + assert.ok(!listNone.output.includes(spaceDID)) + + const add = await w3.args(['space', 'add', res.output]).env(env.bob).join() + assert.equal(add.output.trim(), spaceDID) + + const listSome = await w3.args(['space', 'ls']).env(env.bob).join() + assert.ok(listSome.output.includes(spaceDID)) + }), + 'w3 space add invalid/path': test(async (assert, context) => { const fail = await w3 .args(['space', 'add', 'djcvbii']) @@ -784,6 +813,44 @@ export const testDelegation = { assert.equal(delegate.status.success(), true) }), + 'w3 delegation create -c store/add -c upload/add --stringify': test( + async (assert, context) => { + const env = context.env.alice + const { bob } = Test + const spaceDID = await loginAndCreateSpace(context) + const res = await w3 + .args([ + 'delegation', + 'create', + bob.did(), + '-c', + 'store/add', + '-c', + 'upload/add', + '--stringify' + ]) + .env(env) + .join() + + assert.equal(res.status.success(), true) + + const identityCid = parseLink(res.output, base64) + const reader = await CarReader.fromBytes(identityCid.multihash.digest) + const blocks = [] + for await (const block of reader.blocks()) { + blocks.push(block) + } + + // @ts-expect-error + const delegation = importDAG(blocks) + assert.equal(delegation.audience.did(), bob.did()) + assert.equal(delegation.capabilities[0].can, 'store/add') + assert.equal(delegation.capabilities[0].with, spaceDID) + assert.equal(delegation.capabilities[1].can, 'upload/add') + assert.equal(delegation.capabilities[1].with, spaceDID) + } + ), + 'w3 delegation ls --json': test(async (assert, context) => { const { mallory } = Test From 234eef1e9950d1521e4ae0a9b7da7de29f58010c Mon Sep 17 00:00:00 2001 From: Oli Evans Date: Mon, 15 Jan 2024 18:03:20 +0000 Subject: [PATCH 2/3] chore: `--base64` and readme License: MIT Signed-off-by: Oli Evans --- README.md | 19 +++++++++---------- bin.js | 8 ++++---- index.js | 19 +++++++++---------- test/bin.spec.js | 8 ++++---- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 04f8417..9604f7b 100644 --- a/README.md +++ b/README.md @@ -112,9 +112,11 @@ w3 open bafybeidluj5ub7okodgg5v6l4x3nytpivvcouuxgzuioa6vodg3xt2uqle/olizilla.png Print information about the current agent. -### `w3 space add ` +### `w3 space add ` -Add a space to the agent. The proof is a CAR encoded delegation to _this_ agent. +Add a space to the agent. The proof is a CAR encoded UCAN delegating capabilities over a space to _this_ agent. + +`proof` is a filesystem path to a CAR encoded UCAN, as generated by `w3 delegation create` _or_ a base64 identity CID string as created by `w3 delegation create --base64`. ### `w3 space create [name]` @@ -144,17 +146,14 @@ Create a delegation to the passed audience for the given abilities with the _cur - `--name` Human readable name for the audience receiving the delegation. - `--type` Type of the audience receiving the delegation, one of: device, app, service. - `--output` Path of file to write the exported delegation data to. +- `--base64` Format as base64 identity CID string. Useful when saving it as an environment variable. ```bash -# delegate space/info to did:key:z6MkrwtRceSo2bE6vAY4gi8xPNfNszSpvf8MpAHnxVfMYreN -w3 delegation create did:key:z6MkrwtRceSo2bE6vAY4gi8xPNfNszSpvf8MpAHnxVfMYreN --can space/info - -# delegate store/* and upload/* to did:key:z6MkrwtRceSo2bE6vAY4gi8xPNfNszSpvf8MpAHnxVfMYreN -w3 delegation create did:key:z6MkrwtRceSo2bE6vAY4gi8xPNfNszSpvf8MpAHnxVfMYreN --can 'store/*' --can 'upload/*' +# delegate space/info to did:key:z6M..., output as a CAR +w3 delegation create did:key:z6M... --can space/info --output ./info.ucan -# delegate all capabilities to did:key:z6MkrwtRceSo2bE6vAY4gi8xPNfNszSpvf8MpAHnxVfMYreN -# WARNING - this is bad practice and should generally only be done in testing and development -w3 delegation create did:key:z6MkrwtRceSo2bE6vAY4gi8xPNfNszSpvf8MpAHnxVfMYreN --can '*' +# delegate store/* and upload/* to did:key:z6M..., output as a string +w3 delegation create did:key:z6M... --can 'store/*' --can 'upload/*' --base64 ``` ### `w3 delegation ls` diff --git a/bin.js b/bin.js index c8975fb..fca4116 100755 --- a/bin.js +++ b/bin.js @@ -142,7 +142,7 @@ cli cli .command('space add ') .describe( - 'Import a space from a proof: a CAR encoded UCAN delegation to this agent. proof is a stringified CAR or a filesystem path to one, as created by `w3 delegation create`' + 'Import a space from a proof: a CAR encoded UCAN delegating capabilities to this agent. proof is a filesystem path, or a base64 encoded cid string.' ) .action(addSpace) @@ -181,7 +181,7 @@ cli cli .command('delegation create ') .describe( - 'Create a delegation to the passed audience for the given abilities with the _current_ space as the resource.' + 'Output a CAR encoded UCAN that delegates capabilities to the audience for the current space.' ) .option('-c, --can', 'One or more abilities to delegate.') .option( @@ -202,8 +202,8 @@ cli 'Path of file to write the exported delegation data to.' ) .option( - '--stringify', - 'Encode output as a string. Useful when saving it as an environment variable.' + '--base64', + 'Format as base64 identity CID string. Useful when saving it as an environment variable.' ) .action(createDelegation) diff --git a/index.js b/index.js index a0d1e80..2cd8633 100644 --- a/index.js +++ b/index.js @@ -272,20 +272,19 @@ export async function addSpace(proofPathOrCid) { /* otherwise, try as path */ } + let delegation if (cid) { if (cid.multihash.code !== identity.code) { console.error(`Error: failed to read proof. Must be identity CID. Fetching of remote proof CARs not supported by this command yet`) process.exit(1) } - const delegation = await readProofFromBytes(cid.multihash.digest) - const space = await client.addSpace(delegation) - console.log(space.did()) - + delegation = await readProofFromBytes(cid.multihash.digest) } else { - const delegation = await readProof(proofPathOrCid) - const space = await client.addSpace(delegation) - console.log(space.did()) + delegation = await readProof(proofPathOrCid) } + + const space = await client.addSpace(delegation) + console.log(space.did()) } /** @@ -365,7 +364,7 @@ Providers: ${providers || chalk.dim('none')}`) * @param {number} [opts.expiration] * @param {string} [opts.output] * @param {string} [opts.with] - * @param {boolean} [opts.stringify] + * @param {boolean} [opts.base64] */ export async function createDelegation(audienceDID, opts) { const client = await getClient() @@ -399,13 +398,13 @@ export async function createDelegation(audienceDID, opts) { async function* maybeBaseEncode(src) { const chunks = [] for await (const chunk of src) { - if (!opts.stringify) { + if (!opts.base64) { yield chunk } else { chunks.push(chunk) } } - if (!opts.stringify) return + if (!opts.base64) return const blob = new Blob(chunks) const bytes = new Uint8Array(await blob.arrayBuffer()) const idCid = CID.createV1(ucanto.CAR.code, identity.digest(bytes)) diff --git a/test/bin.spec.js b/test/bin.spec.js index d6abd26..9479109 100644 --- a/test/bin.spec.js +++ b/test/bin.spec.js @@ -350,7 +350,7 @@ export const testSpace = { assert.ok(listSome.output.includes(spaceDID)) }), - 'w3 space add `stringified proof car`': test(async (assert, context) => { + 'w3 space add `base64 proof car`': test(async (assert, context) => { const { env } = context const spaceDID = await loginAndCreateSpace(context, { env: env.alice }) const whosBob = await w3.args(['whoami']).env(env.bob).join() @@ -363,7 +363,7 @@ export const testSpace = { '-c', 'store/*', 'upload/*', - '--stringify' + '--base64' ]) .env(env.alice) .join() @@ -813,7 +813,7 @@ export const testDelegation = { assert.equal(delegate.status.success(), true) }), - 'w3 delegation create -c store/add -c upload/add --stringify': test( + 'w3 delegation create -c store/add -c upload/add --base64': test( async (assert, context) => { const env = context.env.alice const { bob } = Test @@ -827,7 +827,7 @@ export const testDelegation = { 'store/add', '-c', 'upload/add', - '--stringify' + '--base64' ]) .env(env) .join() From 86b2b24b7dfbbc5c2c94e904cbc92739e4f0f6e3 Mon Sep 17 00:00:00 2001 From: Oli Evans Date: Mon, 15 Jan 2024 19:04:17 +0000 Subject: [PATCH 3/3] chore: less repeat License: MIT Signed-off-by: Oli Evans --- lib.js | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/lib.js b/lib.js index 7cf1ab0..24cc359 100644 --- a/lib.js +++ b/lib.js @@ -139,31 +139,15 @@ export function getClient() { * @param {string} path Path to the proof file. */ export async function readProof(path) { + let bytes try { - await fs.promises.access(path, fs.constants.R_OK) + const buff = await fs.promises.readFile(path) + bytes = new Uint8Array(buff.buffer) } catch (/** @type {any} */ err) { console.error(`Error: failed to read proof: ${err.message}`) process.exit(1) } - - const blocks = [] - try { - const reader = await CarReader.fromIterable(fs.createReadStream(path)) - for await (const block of reader.blocks()) { - blocks.push(block) - } - } catch (/** @type {any} */ err) { - console.error(`Error: failed to parse proof: ${err.message}`) - process.exit(1) - } - - try { - // @ts-expect-error - return importDAG(blocks) - } catch (/** @type {any} */ err) { - console.error(`Error: failed to import proof: ${err.message}`) - process.exit(1) - } + return readProofFromBytes(bytes) } /**