Skip to content

Commit

Permalink
feat: w3 delegation create --stringify and `w3 space add <stringifi…
Browse files Browse the repository at this point in the history
…ed proof>`

**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: #154 (comment)

License: MIT
Signed-off-by: Oli Evans <[email protected]>
  • Loading branch information
olizilla committed Jan 15, 2024
1 parent 55f546b commit 8cce55b
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 8 deletions.
6 changes: 5 additions & 1 deletion bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ cli
cli
.command('space add <proof>')
.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)

Expand Down Expand Up @@ -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
Expand Down
59 changes: 52 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -15,6 +17,7 @@ import {
filesize,
filesizeMB,
readProof,
readProofFromBytes,
uploadListResponseToString,
startOfLastMonth,
} from './lib.js'
Expand Down Expand Up @@ -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())
}
}

/**
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
67 changes: 67 additions & 0 deletions test/bin.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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'])
Expand Down Expand Up @@ -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

Expand Down

0 comments on commit 8cce55b

Please sign in to comment.