Documentation
    Preparing search index...

    Module iso-ucan - v0.5.0

    iso-ucan NPM Version License iso-ucan

    Isomorphic UCAN

    pnpm install iso-ucan
    
    import { Capability } from 'iso-ucan/capability'
    import { Store } from 'iso-ucan/store'
    import { MemoryDriver } from 'iso-kv/drivers/memory'
    import { EdDSASigner } from 'iso-signatures/signers/eddsa.js'
    import { z } from 'zod'

    const store = new Store(new MemoryDriver())

    const AccountCreateCap = Capability.from({
    schema: z.object({
    type: z.string(),
    properties: z
    .object({
    name: z.string(),
    })
    .strict(),
    }),
    cmd: '/account/create',
    })

    const AccountCap = Capability.from({
    schema: z.never(),
    cmd: '/account',
    })

    const owner = await EdDSASigner.generate()
    const bob = await EdDSASigner.generate()
    const invoker = await EdDSASigner.generate()

    const nowInSeconds = Math.floor(Date.now() / 1000)

    const ownerDelegation = await AccountCap.delegate({
    iss: owner,
    aud: bob,
    sub: owner,
    pol: [],
    exp: nowInSeconds + 1000,
    })

    await store.set(ownerDelegation)

    const bobDelegation = await AccountCap.delegate({
    iss: bob,
    aud: invoker,
    sub: owner,
    pol: [],
    exp: nowInSeconds + 1000,
    })

    await store.set(bobDelegation)

    const invocation = await AccountCreateCap.invoke({
    iss: invoker,
    sub: owner,
    args: {
    type: 'account',
    properties: {
    name: 'John Doe',
    },
    },
    store,
    exp: nowInSeconds + 1000,
    })

    iso-ucan/rpc is a small request/response layer on top of UCAN invocations. You declare a protocol once as a record of commands (defineCommand), then build matching client and server pairs from it (defineClient / defineServer). Every command's success and error shapes are described by a single receipt schema (receipt, receiptResult, receiptError) which is shared by both ends, so the call sites are fully type-safe and discriminated unions just work on the client.

    Internal server failures (invalid invocation, unknown command, handler threw, …) are surfaced as a generic SERVER_ERROR variant that receipt(...) adds implicitly, so clients can always handle them with one narrow.

    import {
    defineCommand,
    receipt,
    receiptError,
    receiptResult,
    } from 'iso-ucan/rpc'
    import { z } from 'zod'

    const TodoSchema = z.object({
    id: z.string(),
    text: z.string(),
    done: z.boolean(),
    })

    export const Protocol = {
    TodoList: defineCommand({
    cmd: '/todo/list',
    args: z.object({}),
    receipt: receipt(receiptResult(z.object({ todos: z.array(TodoSchema) }))),
    }),
    TodoAdd: defineCommand({
    cmd: '/todo/add',
    args: z.object({ text: z.string().min(1) }),
    receipt: receipt(receiptResult(z.object({ todo: TodoSchema }))),
    }),
    TodoComplete: defineCommand({
    cmd: '/todo/complete',
    args: z.object({ id: z.string() }),
    receipt: receipt(
    receiptResult(z.object({ todo: TodoSchema })),
    receiptError('NOT_FOUND', 'Todo not found.', z.object({ id: z.string() }))
    ),
    }),
    } as const
    import { serve } from '@hono/node-server'
    import { Hono } from 'hono'
    import { MemoryDriver } from 'iso-kv/drivers/memory.js'
    import { defineServer, type ServerHandlers } from 'iso-ucan/rpc'
    import { Store } from 'iso-ucan/store'
    import { Protocol } from './protocol.ts'

    const store = new Store(new MemoryDriver())
    // ...seed `store` with the delegations that authorise your clients...

    const handlers: ServerHandlers<typeof Protocol> = {
    '/todo/list': ({ invocation }) => ({
    cid: invocation.cid,
    result: { todos: [/* ... */] },
    }),
    '/todo/add': ({ args, invocation }) => ({
    cid: invocation.cid,
    result: { todo: { id: '1', text: args.text, done: false } },
    }),
    '/todo/complete': ({ args, invocation }) => {
    // return either { cid, result: ... } or { cid, error: { code, message, data } }
    return {
    cid: invocation.cid,
    error: {
    code: 'NOT_FOUND',
    message: 'Todo not found.',
    data: { id: args.id },
    },
    }
    },
    }

    const rpc = defineServer(Protocol, {
    signer: serverSigner,
    store,
    verifierResolver,
    handlers,
    })

    const app = new Hono()
    app.post('/rpc', (c) => rpc(c.req.raw))
    serve({ fetch: app.fetch, port: 3000 })
    import { defineClient } from 'iso-ucan/rpc'

    const client = defineClient(Protocol, {
    url: 'http://localhost:3000/rpc',
    issuer: cliSigner,
    audience: serverSigner.didObject,
    store,
    verifierResolver,
    })

    // `args` and the returned receipt are typed from the protocol entry for `cmd`.
    const r = await client.request({ cmd: '/todo/complete', args: { id: '42' } })

    if ('result' in r) {
    console.log(r.result.todo)
    } else if (r.error.code === 'NOT_FOUND') {
    console.error(r.error.message, r.error.data) // data: { id: string }
    } else {
    // r.error.code === 'SERVER_ERROR' — always present in every receipt union.
    console.error('server error:', r.error.message)
    }

    A complete runnable example (Hono server + a small CLI client) lives in examples/rpc-todo.

    MIT © Hugo Dias

    Modules

    capability
    delegation
    envelope
    invocation
    policy
    store
    utils
    varsig