-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: utility exports for better UX (#1505)
This PR re-exports some ucanto exports and a utility function to parse a proof (in any current or legacy format). This should make working with the client easier as all the things you need are available (no additional deps to install), makes the docs much more succinct and easier to follow, and actually allows you to import a base64 encoded delegation successfully from any period in time you obtained it. Here's one of many code snippets from the docs site that will be improved by this PR: ### Before ```js import * as Client from '@web3-storage/w3up-client' import { StoreMemory } from '@web3-storage/w3up-client/stores/memory' import { importDAG } from '@ucanto/core/delegation' import { CarReader } from '@ipld/car' import * as Signer from '@ucanto/principal/ed25519' async function main () { // Load client with specific private key const principal = Signer.parse(process.env.KEY) const store = new StoreMemory() const client = await Client.create({ principal, store }) // 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()) // READY to go! } /** @param {string} data Base64 encoded CAR file */ async function parseProof (data) { const blocks = [] const reader = await CarReader.fromBytes(Buffer.from(data, 'base64')) for await (const block of reader.blocks()) { blocks.push(block) } return importDAG(blocks) } ``` ### After ```js import * as Client from '@web3-storage/w3up-client' import { Signer } from '@web3-storage/w3up-client/principal/ed25519' import { StoreMemory } from '@web3-storage/w3up-client/stores/memory' import * as Proof from '@web3-storage/w3up-client/proof' async function main () { // Load client with specific private key const principal = Signer.parse(process.env.KEY) const store = new StoreMemory() const client = await Client.create({ principal, store }) // Add proof that this agent has been delegated capabilities on the space const proof = await Proof.parse(process.env.PROOF) const space = await client.addSpace(proof) await client.setCurrentSpace(space.did()) // READY to go! } ```
- Loading branch information
Alan Shaw
authored
Jun 13, 2024
1 parent
02e3bca
commit 54b0d93
Showing
8 changed files
with
208 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from '@ucanto/principal/ed25519' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from '@ucanto/principal' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from '@ucanto/principal/rsa' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { importDAG, extract } from '@ucanto/core/delegation' | ||
import * as CAR from '@ucanto/transport/car' | ||
import { CarReader } from '@ipld/car' | ||
import * as Link from 'multiformats/link' | ||
import { base64 } from 'multiformats/bases/base64' | ||
import { identity } from 'multiformats/hashes/identity' | ||
|
||
/** | ||
* Parses a base64 encoded CIDv1 CAR of proofs (delegations). | ||
* | ||
* @param {string} str Base64 encoded CAR file. | ||
*/ | ||
export const parse = async (str) => { | ||
try { | ||
const cid = Link.parse(str, base64) | ||
if (cid.code !== CAR.codec.code) { | ||
throw new Error(`non CAR codec found: 0x${cid.code.toString(16)}`) | ||
} | ||
if (cid.multihash.code !== identity.code) { | ||
throw new Error( | ||
`non identity multihash: 0x${cid.multihash.code.toString(16)}` | ||
) | ||
} | ||
|
||
try { | ||
const { ok, error } = await extract(cid.multihash.digest) | ||
if (error) | ||
throw new Error('failed to extract delegation', { cause: error }) | ||
return ok | ||
} catch { | ||
// Before `delegation.archive()` we used `delegation.export()` to create | ||
// a plain CAR file of blocks. | ||
return legacyExtract(cid.multihash.digest) | ||
} | ||
} catch { | ||
// At one point we recommended piping output directly to base64 encoder: | ||
// `w3 delegation create did:key... --can 'store/add' | base64` | ||
return legacyExtract(base64.baseDecode(str)) | ||
} | ||
} | ||
|
||
/** | ||
* Reads a plain CAR file, assuming the last block is the delegation root. | ||
* | ||
* @param {Uint8Array} bytes | ||
*/ | ||
const legacyExtract = async (bytes) => { | ||
const blocks = [] | ||
const reader = await CarReader.fromBytes(bytes) | ||
for await (const block of reader.blocks()) { | ||
blocks.push(block) | ||
} | ||
return importDAG(blocks) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import * as Test from './test.js' | ||
import * as CAR from '@ucanto/transport/car' | ||
import * as Link from 'multiformats/link' | ||
import { base64 } from 'multiformats/bases/base64' | ||
import { identity } from 'multiformats/hashes/identity' | ||
import { sha256 } from 'multiformats/hashes/sha2' | ||
import { Signer } from '../src/principal/ed25519.js' | ||
import { delegate } from '../src/delegation.js' | ||
import { parse } from '../src/proof.js' | ||
import * as Result from '../src/result.js' | ||
|
||
/** | ||
* @type {Test.Suite} | ||
*/ | ||
export const testProof = { | ||
'should parse a base64 encoded CIDv1 "proof"': async (assert) => { | ||
const alice = await Signer.generate() | ||
const bob = await Signer.generate() | ||
const delegation = await delegate({ | ||
issuer: alice, | ||
audience: bob, | ||
capabilities: [{ can: 'test/thing', with: alice.did() }], | ||
}) | ||
|
||
const bytes = Result.unwrap(await delegation.archive()) | ||
const str = Link.create(CAR.codec.code, identity.digest(bytes)).toString( | ||
base64 | ||
) | ||
|
||
const proof = await parse(str) | ||
assert.equal(proof.issuer.did(), delegation.issuer.did()) | ||
assert.equal(proof.audience.did(), delegation.audience.did()) | ||
assert.equal(proof.capabilities[0].can, delegation.capabilities[0].can) | ||
assert.equal(proof.capabilities[0].with, delegation.capabilities[0].with) | ||
}, | ||
|
||
'should fail to parse if CID is not CAR codec': async (assert) => { | ||
const alice = await Signer.generate() | ||
const bob = await Signer.generate() | ||
const delegation = await delegate({ | ||
issuer: alice, | ||
audience: bob, | ||
capabilities: [{ can: 'test/thing', with: alice.did() }], | ||
}) | ||
|
||
const bytes = Result.unwrap(await delegation.archive()) | ||
const str = Link.create(12345, identity.digest(bytes)).toString(base64) | ||
|
||
await assert.rejects(parse(str)) | ||
}, | ||
|
||
'should fail to parse if multihash is not identity hash': async (assert) => { | ||
const alice = await Signer.generate() | ||
const bob = await Signer.generate() | ||
const delegation = await delegate({ | ||
issuer: alice, | ||
audience: bob, | ||
capabilities: [{ can: 'test/thing', with: alice.did() }], | ||
}) | ||
|
||
const bytes = Result.unwrap(await delegation.archive()) | ||
const str = Link.create( | ||
CAR.codec.code, | ||
await sha256.digest(bytes) | ||
).toString(base64) | ||
|
||
await assert.rejects(parse(str)) | ||
}, | ||
|
||
'should parse a base64 encoded CIDv1 "proof" as plain CAR (legacy)': async ( | ||
assert | ||
) => { | ||
const alice = await Signer.generate() | ||
const bob = await Signer.generate() | ||
const delegation = await delegate({ | ||
issuer: alice, | ||
audience: bob, | ||
capabilities: [{ can: 'test/thing', with: alice.did() }], | ||
}) | ||
|
||
const blocks = new Map() | ||
for (const block of delegation.export()) { | ||
blocks.set(block.cid.toString(), block) | ||
} | ||
|
||
const bytes = CAR.codec.encode({ blocks }) | ||
const str = Link.create(CAR.codec.code, identity.digest(bytes)).toString( | ||
base64 | ||
) | ||
|
||
const proof = await parse(str) | ||
assert.equal(proof.issuer.did(), delegation.issuer.did()) | ||
assert.equal(proof.audience.did(), delegation.audience.did()) | ||
assert.equal(proof.capabilities[0].can, delegation.capabilities[0].can) | ||
assert.equal(proof.capabilities[0].with, delegation.capabilities[0].with) | ||
}, | ||
|
||
'should parse a base64 encoded "proof" as plain CAR (legacy)': async ( | ||
assert | ||
) => { | ||
const alice = await Signer.generate() | ||
const bob = await Signer.generate() | ||
const delegation = await delegate({ | ||
issuer: alice, | ||
audience: bob, | ||
capabilities: [{ can: 'test/thing', with: alice.did() }], | ||
}) | ||
|
||
const blocks = new Map() | ||
for (const block of delegation.export()) { | ||
blocks.set(block.cid.toString(), block) | ||
} | ||
|
||
const bytes = CAR.codec.encode({ blocks }) | ||
const str = base64.baseEncode(bytes) | ||
|
||
const proof = await parse(str) | ||
assert.equal(proof.issuer.did(), delegation.issuer.did()) | ||
assert.equal(proof.audience.did(), delegation.audience.did()) | ||
assert.equal(proof.capabilities[0].can, delegation.capabilities[0].can) | ||
assert.equal(proof.capabilities[0].with, delegation.capabilities[0].with) | ||
}, | ||
} | ||
|
||
Test.test({ Proof: testProof }) |