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: utility exports for better UX #1505

Merged
merged 2 commits into from
Jun 13, 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
20 changes: 20 additions & 0 deletions packages/w3up-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@
"types": "./dist/src/account.d.ts",
"import": "./dist/src/account.js"
},
"./delegation": {
"types": "./dist/src/delegation.d.ts",
"import": "./dist/src/delegation.js"
},
"./principal": {
"types": "./dist/src/principal/index.d.ts",
"import": "./dist/src/principal/index.js"
},
"./principal/ed25519": {
"types": "./dist/src/principal/ed25519.d.ts",
"import": "./dist/src/principal/ed25519.js"
},
"./principal/rsa": {
"types": "./dist/src/principal/rsa.d.ts",
"import": "./dist/src/principal/rsa.js"
},
"./proof": {
"types": "./dist/src/proof.d.ts",
"import": "./dist/src/proof.js"
},
"./space": {
"types": "./dist/src/space.d.ts",
"import": "./dist/src/space.js"
Expand Down
2 changes: 1 addition & 1 deletion packages/w3up-client/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { Base } from './base.js'
import * as Account from './account.js'
import { Space } from './space.js'
import { Delegation as AgentDelegation } from './delegation.js'
import { AgentDelegation } from './delegation.js'
import { BlobClient } from './capability/blob.js'
import { IndexClient } from './capability/index.js'
import { StoreClient } from './capability/store.js'
Expand Down
8 changes: 5 additions & 3 deletions packages/w3up-client/src/delegation.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Delegation as CoreDelegation } from '@ucanto/core/delegation'
import { Delegation } from '@ucanto/core/delegation'

export * from '@ucanto/core/delegation'

/* c8 ignore start */
/**
* @template {import('./types.js').Capabilities} C
* @extends {CoreDelegation<C>}
* @extends {Delegation<C>}
*/
export class Delegation extends CoreDelegation {
export class AgentDelegation extends Delegation {
/* c8 ignore stop */
/** @type {Record<string, any>} */
#meta
Expand Down
1 change: 1 addition & 0 deletions packages/w3up-client/src/principal/ed25519.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '@ucanto/principal/ed25519'
1 change: 1 addition & 0 deletions packages/w3up-client/src/principal/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '@ucanto/principal'
1 change: 1 addition & 0 deletions packages/w3up-client/src/principal/rsa.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '@ucanto/principal/rsa'
54 changes: 54 additions & 0 deletions packages/w3up-client/src/proof.js
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)
}
125 changes: 125 additions & 0 deletions packages/w3up-client/test/proof.test.js
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 })
Loading