(u)canto is a library for UCAN based RPC that provides:
- A declarative system for defining [capabilities][] and [abilities][] (roughly equivalent to HTTP routes in REST).
- A system for binding capability handles (a.k.a providers) to form services with built-in routing.
- A UCAN validation system.
- A runtime for executing UCAN capability invocations.
- A pluggable transport layer.
- A client supporting batched invocations and full type inference.
the name ucanto is a word play on UCAN and canto (one of the major divisions of a long poem)
Most developers will use ucanto to connect to existing UCAN services. Here's how to get started:
npm install @le-space/ucanto-client @le-space/ucanto-principal @le-space/ucanto-transportimport * as Client from '@le-space/ucanto-client'
import * as HTTP from '@le-space/ucanto-transport/http'
import { CAR } from '@le-space/ucanto-transport'
import { ed25519 } from '@le-space/ucanto-principal'
// Connect to a UCAN service (e.g., w3up, your company's API, etc.)
const connection = Client.connect({
id: { did: () => 'did:web:api.example.com' }, // Service's public DID
codec: CAR.outbound,
channel: HTTP.open({ url: new URL('https://api.example.com') }),
})
// Generate or load your client keys
const agent = await ed25519.generate()
// Invoke a capability on the service
const invocation = Client.invoke({
issuer: agent,
audience: connection.id,
capability: {
can: 'store/add',
with: agent.did(),
nb: {
link: 'bafybeigwflfnv7tjgpuy52ep45cbbgkkb2makd3bwhbj3ueabvt3eq43ca'
}
}
})
// Execute the invocation
const result = await invocation.execute(connection)
if (result.error) {
console.error('Operation failed:', result.error)
} else {
console.log('Success:', result.out)
}π Tested in:
packages/client/test/client.spec.js:22- Client invocation and execution
UCAN services often require delegated permissions. Here's how to use them:
// Example 1: Using a DID (identity-based resource)
const delegation = await Client.delegate({
issuer: serviceAgent, // Who granted the permission
audience: agent, // You (the recipient)
capabilities: [{
can: 'store/add',
with: 'did:key:zAlice' // Resource: Alice's storage (must match serviceAgent.did())
}]
})
// Example 2: Using a resource URI (file-based resource)
const fileDelegation = await Client.delegate({
issuer: alice, // Alice owns the file
audience: bob, // Bob gets access
capabilities: [{
can: 'file/write',
with: 'file:///home/alice/documents/important.txt' // Specific file resource
}]
})
// Use the delegation as proof in your invocation
const invocation = Client.invoke({
issuer: agent,
audience: connection.id,
capability: {
can: 'store/add',
with: 'did:key:zAlice', // Must match the delegated resource
nb: { link: 'bafybeig...' }
},
proofs: [delegation] // Proof you have permission
})
const result = await invocation.execute(connection)π Tested in:
packages/client/test/client.spec.js:70- Delegation creation and usagepackages/server/test/readme-integration.spec.js:160- Delegation with server validation
You can send multiple invocations in a single request:
const uploadFile = Client.invoke({
issuer: agent,
audience: connection.id,
capability: { can: 'store/add', with: agent.did(), nb: { link: fileCID } }
})
const deleteFile = Client.invoke({
issuer: agent,
audience: connection.id,
capability: { can: 'store/remove', with: agent.did(), nb: { link: oldFileCID } }
})
// Execute both operations together
const [uploadResult, deleteResult] = await connection.execute([uploadFile, deleteFile])π Tested in:
packages/client/test/client.spec.js:102- Batch invocation execution
UCAN supports complex delegation scenarios where users can grant permissions to others:
// Alice delegates capability to Bob for a specific namespace
const proof = await Client.delegate({
issuer: alice,
audience: bob,
capabilities: [
{
can: 'file/link',
with: `file:///tmp/${alice.did()}/friends/${bob.did()}/`,
},
],
})
// Bob can now use the delegated permission
const aboutBob = Client.invoke({
issuer: bob,
audience: serviceKey,
capability: {
can: 'file/link',
with: `file:///tmp/${alice.did()}/friends/${bob.did()}/about`,
nb: { link: testCID },
},
proofs: [proof], // Include the delegation proof
})
// Bob tries to access Mallory's namespace (should fail)
const aboutMallory = Client.invoke({
issuer: bob,
audience: serviceKey,
capability: {
can: 'file/link',
with: `file:///tmp/${alice.did()}/friends/${MALLORY_DID}/about`,
nb: { link: malloryCID },
},
proofs: [proof], // Same proof, but wrong namespace
})
// Execute both operations
const [bobResult, malloryResult] = await connection.execute([
aboutBob,
aboutMallory,
])
// Bob's operation succeeds, Mallory's fails
if (bobResult.error) {
console.error('Bob operation failed:', bobResult.error)
} else {
console.log('Bob operation succeeded:', bobResult.out)
}
if (malloryResult.error) {
console.log('Mallory operation failed (expected):', malloryResult.error)
} else {
console.log('Mallory operation succeeded (unexpected)')
}This demonstrates how UCAN's delegation system provides fine-grained access control where:
- β Bob succeeds - He has delegated permission for his namespace
- β Mallory fails - Bob doesn't have permission for Mallory's namespace
- π Security - The service validates the delegation chain and resource ownership
π Tested in:
packages/server/test/readme-integration.spec.js:99- Advanced delegation patterns with namespace validation
Different UCAN services will have different capabilities. Check their documentation for specifics:
- w3up (Web3.Storage): w3up documentation
- Custom Services: See your service's API documentation
To create your own UCAN service, see the @le-space/ucanto-server documentation. This covers:
- Defining capabilities
- Creating service handlers
- Setting up transport layers
- Deployment and security
import * as Transport from '@le-space/ucanto-transport'
const connection = Client.connect({
id: service,
codec: Transport.outbound({
encoders: { 'application/car': CAR.request },
decoders: { 'application/dag-cbor': CBOR.response }
}),
channel: yourCustomChannel
})import { ed25519 } from '@le-space/ucanto-principal'
// Generate new keys
const agent = await ed25519.generate()
// Save keys (browser)
localStorage.setItem('agent', agent.toString())
// Load keys (browser)
const savedAgent = ed25519.parse(localStorage.getItem('agent'))
// Save keys (Node.js)
import fs from 'fs/promises'
await fs.writeFile('agent.key', agent.toString())
// Load keys (Node.js)
const keyData = await fs.readFile('agent.key', 'utf-8')
const loadedAgent = ed25519.parse(keyData)π Tested in:
packages/server/test/readme-examples.spec.js:54- Key generation, formatting, and parsing
@le-space/ucanto-client- Connect to and invoke UCAN services@le-space/ucanto-server- Build your own UCAN services@le-space/ucanto-transport- Transport layer implementations@le-space/ucanto-principal- Cryptographic identity management@le-space/ucanto-core- Core UCAN primitives@le-space/ucanto-validator- UCAN validation logic