UCAN authorization for any service - built on ucanto
npm install @ixo/ucanA framework-agnostic UCAN (User Controlled Authorization Networks) implementation for any service. Built on top of the battle-tested ucanto library and conforming to the UCAN specification.
UCAN is a decentralized authorization system using cryptographically signed tokens. Think of it as "JWT meets capabilities" - users can grant specific permissions to others, who can further delegate (but never escalate) those permissions.
Key concepts:
- Capabilities: What actions can be performed on which resources
- Delegations: Granting capabilities to others (can be chained)
- Invocations: Requests to use a capability
- Attenuation: Permissions can only be narrowed, never expanded
š See the visual flow diagram ā
- š Built on ucanto - Battle-tested UCAN library from Storacha
- šÆ Generic Capabilities - Define your own capabilities with custom schemas
- āļø Caveat Validation - Enforce limits and restrictions on delegations
- š Multi-DID Support - did:key (native) + did:ixo (via blockchain indexer)
- š Framework-Agnostic - Works with Express, Fastify, Hono, NestJS, etc.
- š”ļø Replay Protection - Built-in invocation store prevents replay attacks
``bash`
npm install @ixo/ucanor
pnpm add @ixo/ucan
`typescript
import { defineCapability, Schema } from '@ixo/ucan';
const EmployeesRead = defineCapability({
can: 'employees/read',
protocol: 'myapp:',
nb: { limit: Schema.integer().optional() },
derives: (claimed, delegated) => {
const claimedLimit = claimed.nb?.limit ?? Infinity;
const delegatedLimit = delegated.nb?.limit ?? Infinity;
if (claimedLimit > delegatedLimit) {
return { error: new Error(Limit ${claimedLimit} exceeds allowed ${delegatedLimit}) };`
}
return { ok: {} };
},
});
`typescript
import { createUCANValidator, createIxoDIDResolver } from '@ixo/ucan';
const validator = await createUCANValidator({
serverDid: 'did:ixo:ixo1abc...', // Your server's DID
rootIssuers: ['did:ixo:ixo1admin...'], // DIDs that can issue root capabilities
didResolver: createIxoDIDResolver({
indexerUrl: 'https://blocksync.ixo.earth/graphql',
}),
});
`
`typescript
app.post('/employees', async (req, res) => {
const result = await validator.validate(
req.body.invocation, // Base64-encoded CAR
EmployeesRead,
'myapp:company/acme'
);
if (!result.ok) {
return res.status(403).json({ error: result.error });
}
const limit = result.capability?.nb?.limit ?? 10;
res.json({ employees: getEmployees(limit) });
});
`
`typescript
import { generateKeypair, createDelegation, createInvocation, serializeInvocation } from '@ixo/ucan';
// Generate a keypair for the user
const { signer, did } = await generateKeypair();
// Root creates a delegation for the user
const delegation = await createDelegation({
issuer: rootSigner,
audience: did,
capabilities: [{ can: 'employees/read', with: 'myapp:company/acme', nb: { limit: 50 } }],
expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour
});
// User creates an invocation
const invocation = await createInvocation({
issuer: signer,
audience: serverDid,
capability: { can: 'employees/read', with: 'myapp:company/acme', nb: { limit: 25 } },
proofs: [delegation],
});
// Serialize and send
const serialized = await serializeInvocation(invocation);
await fetch('/employees', {
method: 'POST',
body: JSON.stringify({ invocation: serialized }),
});
`
| Document | Description |
|----------|-------------|
| Flow Diagram | Visual explanation of UCAN delegation and invocation |
| Server Example | Complete Express server with protected routes |
| Client Example | Frontend/client-side usage |
| Capabilities Guide | How to define custom capabilities with caveats |
`typescript`
defineCapability(options: DefineCapabilityOptions)
Define a capability with optional caveat validation.
| Option | Type | Description |
|--------|------|-------------|
| can | string | Action name (e.g., 'employees/read') |protocol
| | string | URI protocol (default: 'urn:') |nb
| | object | Schema for caveats using Schema.* |derives
| | function | Custom validation for attenuation |
`typescript`
createUCANValidator(options: CreateValidatorOptions): Promise
Create a framework-agnostic validator.
| Option | Type | Description |
|--------|------|-------------|
| serverDid | string | Server's DID (any method supported) |rootIssuers
| | string[] | DIDs that can self-issue capabilities |didResolver
| | DIDKeyResolver | Resolver for non-did:key DIDs |invocationStore
| | InvocationStore | Custom store for replay protection |
| Function | Description |
|----------|-------------|
| generateKeypair() | Generate new Ed25519 keypair |parseSigner(privateKey, did?)
| | Parse private key into signer |signerFromMnemonic(mnemonic, did?)
| | Derive signer from BIP39 mnemonic |createDelegation(options)
| | Create a delegation |createInvocation(options)
| | Create an invocation |serializeDelegation(delegation)
| | Serialize to base64 CAR |serializeInvocation(invocation)
| | Serialize to base64 CAR |parseDelegation(serialized)
| | Parse from base64 CAR |
`typescript`
createIxoDIDResolver(config: IxoDIDResolverConfig): DIDKeyResolver
createCompositeDIDResolver(resolvers: DIDKeyResolver[]): DIDKeyResolver
`typescript`
new InMemoryInvocationStore(options?)
createInvocationStore(options?)
| DID Method | Support | Notes |
|------------|---------|-------|
| did:key | ā
Native | Parsed directly from the identifier |did:ixo
| | ā
Via resolver | Resolved via IXO blockchain indexer |did:web
| | š§ Extendable | Implement custom resolver |
`env`For IXO DID resolution
BLOCKSYNC_GRAPHQL_URL=https://blocksync.ixo.earth/graphql
For advanced use cases, you can access the underlying ucanto packages:
`typescript`
import { UcantoServer, UcantoClient, UcantoValidator, ed25519 } from '@ixo/ucan';
Implement the InvocationStore interface for distributed deployments:
`typescript`
interface InvocationStore {
has(cid: string): Promise
add(cid: string, ttlMs?: number): Promise
cleanup?(): Promise
}
See the test script for a complete example of the UCAN flow:
`bash``
pnpm test:ucan
MIT
- ucanto (underlying library)
- UCAN Specification
- IXO Network