npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

ucanto

v0.1.0-beta

Published

UCAN RPC

Downloads

8

Readme

ucanto

(u)canto is a library for UCAN based RPC which provides:

  1. A system for defining services as a map of UCAN capability handlers.
  2. A runtime for executing capabilities through UCAN invocations.
  3. A pluggable transport layer.
  4. A capability based routing system.
  5. Batched invocation with precise type inference.

the name ucanto is a word play on UCAN and canto (one of the major divisions of a long poem)

High level overview

Services

This library defines a "service" as a hierarchical mapping of (cap)ability (The can field of the capability) to a handler. To make it more clear, lets define a simple service that provides { can: "intro/echo", with: "data:*" } capability which echoes back the message, and { can: "math/sqrt", with: "*", n: number } capability which returns square root of a given number.

import type {Invocation, Result} from "ucanto"

type Echo = {
  can: "intro/echo"
  with: string
}

export const echo = async({ capability }: Invocation<Echo>):Promise<Result<string, InvalidInputError>> => {
  const result = !capability.with.startsWith('data:')
    ? new InvalidInputError(`Capability "intro/echo" expects with to be a data URL, instead got ${capability.with}`)
    : !capability.with.startsWith('data:text/plain,')
    ? new InvalidInputError(`Capability "intro/echo" currently only support data URLs in text/plain encoding`)
    : { ok: true, value: capability.with.slice('data:text/plain,'.length) }
      
  return result
}

export const sqrt = async({ capabality }:Invocation<Sqrt>):Promise<Result<number, InvalidInputError>> => {
  const result = capability.n < 0
    ? new InvalidInputError(`Capability "math/sqrt" only operates on positive numbers, instead got ${capability.n}`)
    : { ok: true, Math.sqrt(capability.n) }
}


// heirarchical mapping of (cap)abilities with corresponding handlers
// 'intro/echo' -> .intro.echo
// 'math/sqrt' -> .math.sqrt
export const service = {
  intro: { echo },
  math: { sqrt }
}


class InvalidInputError extends Error {
  constructor(input) {
     super(`Invalid input: ${input}`)
  }
}

There are few requirements that all handlers MUST meet:

  1. Handler takes a single argument of type Service.Invocation<Capability> which is a deserialized representation of a UCAN invocation with a single concrete capability. Although the invocation must take a single capability, you can use a type union to accept multiple types of input data.

    Right now it MUST have can field but that requirement may be removed in the future.

  2. Handler MUST return Result type

    Errors happen, and it's best to specify what kind in types. While you can simply do Result<T, Error>, it's recommended to be more specific.

Please note:

  1. We have not done any UCAN validation here to keep things simple (but also "intro/echo" capability can be self issued :P). That is something you MUST do in your handler though.

  2. We defined our service as { intro: { echo }, math: { sqrt } } which maps with corresponding (cap)abilities and provides definitions for the routing system.

Transport

The library provides a pluggable transport architecture so you can expose a service in various transport encodings. To do so you have to provide:

  1. decoder that will take { headers: Record<string, string>, body: Uint8Array } object and decode it into { invocations: Invocation[] }.
  2. encoder that will take unknown[] (corresponding to values returned by handlers) and encode it into { headers: Record<string, string>, body: Uint8Array }.
  3. service implementation

Note that the actual encoder / decoder types are more complicated as they capture capability types, the number of invocations, and corresponding return types. This allows them to provide good type inference. But ignoring those details, that is what they are in a nutshell.

In the example below we create a server which will take invocations encoded in CAR format and produce responses encoded in DAG-CBOR format. There are a few other options provided by tbe library, and you could also bring your own.

import * as Server from "ucanto/src/server.js"
import * as Transport from "ucanto/src/transport.js"

const server = Server.create({
 service,
 decoder: Transport.CAR,
 encoder: Transport.CBOR,
})

Routing

The server defined above can:

  1. Take requests in { headers: Record<string, string>, body: Uint8Array } format.
  2. Decode them into Invocations.
  3. Route and execute corresponding (cap)ability handler.
  4. Encode results back into ``{ headers: Record<string, string>, body: Uint8Array }` format.

All you need to do is simply pass the request:

export const handler = async (payload:{headers:Record<string, string>, body:Uint8Array}):Promise<{headers:Record<string, string>, body:Uint8Array}> =>
  server.request(payload)

Please note: this library intentionally does not deal with any networking, so that you could plug it into whatever runtime you need as long as you can represent request responses as { headers: Record<string, string>, body: Uint8Array }

Streaming is not currently supported, but may be added in the future.

Client

Client implementation can be used to issue and execute UCAN invocations. Here is an example of invoking capabilities defined by our service earlier:

import * as Client from "ucanto/src/client.js"
import * as DID from "@ipld/dag-ucan"
import { keypair } from "ucans"

const service = DID.parse("did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169")

// did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi
const alice = keypair.EdKeypair.fromSecretKey("U+bzp2GaFQHso587iSFWPSeCzbSfn/CbNHEz7ilKRZ1UQMmMS7qq4UhTzKn3X9Nj/4xgrwa+UqhMOeo4Ki8JUw==")


const demo1 = async (connection) => {
  const hello = await Client.invoke({
    issuer: alice,
    audience: service
    can: "intro/echo",
    with: "data:text/plain,hello world"
  })
  
  const result = await hello.execute(connection)  
  if (result.ok) {
    console.log("got echo back", result.value)
  } else {
    console.error("oops", result)
  }
}

Note that the client will get complete type inference as long as connection captures a type of the service on the other side of the wire.

Transport

Just like the server, the client has a pluggable transport layer which you provide when you create a connection. The transport layer consists of:

  1. encoder takes { invocations: IssuedInvocation[] } objects and turn them into { headers: Record<string, string>, body: Uint8Array }.
  2. decoder takes { headers: Record<string, string>, body: Uint8Array } and turns it into unknown[] (that correspond to return values for invocations).
  3. channel transport channel that takes request delivers it to the server and returns promise of the response when one is received from the server, which looks like { request(payload:{headers: Record<string, string>, body: Uint8Array}):Promise<{headers: Record<string, string>, body: Uint8Array}> }

We could create an in-process connection with our service simply by providing service as a channel:

const connection = Client.connect({
 encoder: Transport.CAR,  // encode as CAR because server decods from car
 decoder: Transport.CBOR, // decode as CBOR because server encodes as CBOR
 channel: server,         // simply pass the server
})

In practice you probably would want client/server communication to happen across a wire, or at least across processes. You can bring your own transport channel, or choose an existing one. For example:

import * as Transport from "ucanto/src/transport.js"

const connection = Client.connect({
 encoder: Transport.CAR,  // encode as CAR because server decodes from car
 decoder: Transport.CBOR, // decode as CBOR because server encodes as CBOR
 /** @type {Transport.Channel<typeof service>} */
 channel: Transport.HTTP.open({ url: new URL(process.env.SERVICE_URL) }) // simple `fetch` wrapper 
})

Note: That in that case, you ned to provide type annotations, so the client can provide inference for requests and return types

Batching & Proof chains

The library supports batch invocations and takes care of all the nitty gritty details when it comes to UCAN delegation chains, specifically taking chains apart to encode as blocks in CAR and putting them back together into a chain on the other side. All you need to do is provide a delegation in the proofs:

import * as Client from "ucanto/src/client.js"
import * as DID from "@ipld/dag-ucan"
import { keypair } from "ucans"

const service = DID.parse("did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169")

// did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi
const alice = keypair.EdKeypair.fromSecretKey("U+bzp2GaFQHso587iSFWPSeCzbSfn/CbNHEz7ilKRZ1UQMmMS7qq4UhTzKn3X9Nj/4xgrwa+UqhMOeo4Ki8JUw==")
// did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob
const bob = keypair.EdKeypair.fromSecretKey("G4+QCX1b3a45IzQsQd4gFMMe0UB1UOx9bCsh8uOiKLER69eAvVXvc8P2yc4Iig42Bv7JD2zJxhyFALyTKBHipg==")


const demo2 = async (connection) => {
  const bye = await Client.invoke({
    issuer: alice,
    audience: service
    can: "intro/echo",
    with: "data:text/plain,bye"
  })
  
  const sqrt = (n) => Client.invoke({
    issuer: alice,
    audience: service,
    can: "math/sqrt",
    with: alice.did(),
    n,
    proofs: [UCAN.parse(process.env.UCAN)]
  })
  
  const [r1, r2] = batch(bye, await sqrt(9)).execute(connection)
  
  if (r1.ok) {
    console.log("got echo back", r1.value)
  } else {
    console.error("oops", r1)
  }
  
  if (r2.ok) {
    console.log("got sqrt", r2.value)
  } else {
    console.log("oops", r2)
  }
}

Future

Intentions are that in the future we may provide a more powerful GraphQL inspired invocation interface along the lines of:

Client.query({
  r1: select({ intro: { echo: { with: "data:text/plain,hello beautiful" } } }),
  // pass a request and specify which fields to select
  r2: select({ store: { add: { with: alice.did(), link: cid } }, { url: true, status: true })
})