Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: w3 delegation create --base64 & w3 space add <base64> #158

Merged
merged 3 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,11 @@ w3 open bafybeidluj5ub7okodgg5v6l4x3nytpivvcouuxgzuioa6vodg3xt2uqle/olizilla.png

Print information about the current agent.

### `w3 space add <proof.ucan>`
### `w3 space add <proof>`

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]`

Expand Down Expand Up @@ -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`
Expand Down
8 changes: 6 additions & 2 deletions 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 delegating capabilities to this agent. proof is a filesystem path, or a base64 encoded cid string.'
)
.action(addSpace)

Expand Down Expand Up @@ -181,7 +181,7 @@ cli
cli
.command('delegation create <audience-did>')
.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(
Expand All @@ -201,6 +201,10 @@ cli
'-o, --output',
'Path of file to write the exported delegation data to.'
)
.option(
'--base64',
'Format as base64 identity CID string. Useful when saving it as an environment variable.'
)
.action(createDelegation)

cli
Expand Down
54 changes: 49 additions & 5 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 @@
filesize,
filesizeMB,
readProof,
readProofFromBytes,
uploadListResponseToString,
startOfLastMonth,
} from './lib.js'
Expand Down Expand Up @@ -253,11 +256,33 @@
}

/**
* @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)

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 */
}

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)
}
delegation = await readProofFromBytes(cid.multihash.digest)
} else {
delegation = await readProof(proofPathOrCid)
}

const space = await client.addSpace(delegation)
console.log(space.did())
}
Expand Down Expand Up @@ -339,6 +364,7 @@
* @param {number} [opts.expiration]
* @param {string} [opts.output]
* @param {string} [opts.with]
* @param {boolean} [opts.base64]
*/
export async function createDelegation(audienceDID, opts) {
const client = await getClient()
Expand Down Expand Up @@ -367,7 +393,25 @@
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.base64) {
yield chunk
} else {
chunks.push(chunk)
}
}
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))
yield idCid.toString(base64)
},
dest
)

for (const block of delegation.export()) {
// @ts-expect-error
Expand Down Expand Up @@ -520,7 +564,7 @@
}
}

export async function whoami() {

Check warning on line 567 in index.js

View workflow job for this annotation

GitHub Actions / Test

Missing JSDoc comment
const client = await getClient()
console.log(client.did())
}
Expand Down
13 changes: 10 additions & 3 deletions lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,24 +139,31 @@ 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)
}
return readProofFromBytes(bytes)
}

/**
* @param {Uint8Array} bytes Path to the proof file.
*/
export async function readProofFromBytes(bytes) {
const blocks = []
try {
const reader = await CarReader.fromIterable(fs.createReadStream(path))
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)
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 `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()
const bobDID = SpaceDID.from(whosBob.output.trim())
const res = await w3
.args([
'delegation',
'create',
bobDID,
'-c',
'store/*',
'upload/*',
'--base64'
])
.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 --base64': 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',
'--base64'
])
.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