From 4308fd2f392b9fcccc52af64432dcb04c8257e0b Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Mon, 20 Jun 2022 12:45:22 -0700 Subject: [PATCH] feat: cherry pick changes from uploads-v2 demo (#43) * chore(ucanto): export more bindings * chore(ucanto): split server options * chore(ucanto): remove prepare scripts * feat(ucanto): make derived capabilities providable * chore: add .env to gitignore * feat(ucanto): handle capability optionals * chore(ucanto): drop @ from package name * chore: update readme to reflect current state * fix: typo in readme --- .gitignore | 1 + Readme.md | 418 ++++++++++++----------- package.json | 2 +- packages/authority/package.json | 1 - packages/client/package.json | 1 - packages/client/test/services/account.js | 4 +- packages/core/package.json | 1 - packages/interface/package.json | 1 - packages/interface/src/capability.ts | 35 +- packages/interface/src/lib.ts | 20 +- packages/server/package.json | 1 - packages/server/src/api.ts | 2 +- packages/server/src/handler.js | 6 +- packages/server/src/lib.js | 5 +- packages/server/src/server.js | 10 +- packages/server/test/service/access.js | 6 +- packages/transport/package.json | 1 - packages/validator/package.json | 1 - packages/validator/src/capability.js | 17 +- packages/validator/src/lib.js | 3 +- 20 files changed, 289 insertions(+), 247 deletions(-) diff --git a/.gitignore b/.gitignore index 8515d58b..c1b40235 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ dist .nyc_output tmp node_modules +.env diff --git a/Readme.md b/Readme.md index 7d427bca..9ac98cc3 100644 --- a/Readme.md +++ b/Readme.md @@ -1,257 +1,285 @@ # 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. +(u)canto is a library for [UCAN][] based [RPC][] that provides: +1. A declarative system for defining capabilities (roughly equivalet of HTTP + routes in REST). +1. A system for binding [capability][] handles (a.k.a providers) to form services with built-in routing. +1. UCAN validation system. +1. Runtime for executing UCAN capability [invocations][]. +1. A pluggable transport layer. +1. Clien 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) +## Quick sample + +To get a taste of the libary we will build up a "filesystem" service, in which: + +1. Top level paths are [did:key][] identifiers, here on referred as (user) drives. +1. Drives are owned by users holding a private key corresponding to the [did:key][] of the drive. +1. Drive owners could mutate filesystem with-in their drives path and delegate that ability to others. + +### Capabilities + +The very first thing we want to do is define set of capabilities our service will provide. Each (cap)[ability][] MUST: -## High level overview +1. Have a `can` field denoting an _action_ it can perform. +2. Have a `with` URI denoting _resource_ it can perform that action on. +3. Be comparable to other capabilities _(with set semantics, as in does capability `a` includes capability `b` ?)_ + +Lets define `file/link` capability, where resources are identified via `file:` URLs and MAY contain `link` to be mapped to a given path. + +```ts +import { capability, URI, Link, Failure } from "@ucanto/server" + +const Add = capability({ + can: "file/link", + with: URI.match({ protocol: "file:" }), + caveats: { link: Link }, + derives: (claimed, delegated) => + // Can be derived if claimed capability path is contained in the delegated + // capability path. + claimed.uri.href.startsWith(ensureTrailingDelimiter(delegated.uri.href)) || + new Failure(`Notebook ${claimed.uri} is not included in ${delegaed.uri}`), +}) + +const ensureTrailingDelimiter = uri => (uri.endsWith("/") ? uri : `${uri}/`) +``` + +> Please note that library gurantees that both `claimed` and `delegated` capabilty will have `{can: "file/link", with: string, uri: URL, caveats: { link?: CID }}` +> type inferred from the definition. +> +> We will explore more complicated case later where capability may be derived from a different capability or even a set. ### 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. +Now that we have a `file/link` capability we can define a service providing it: ```ts -import type {Invocation, Result} from "ucanto" +import { provide, Failure, MalformedCapability } from "@ucanto/server" + +const service = (context: { store: Map }) => { + const add = provide(Add, ({ capability, invocation }) => { + store.set(capability.uri.href, capability.caveats.link) + return { + with: capability.with, + link: capability.caveats.link, + } + }) -type Echo = { - can: "intro/echo" - with: string + return { file: { add } } } +``` -export const echo = async({ capability }: Invocation):Promise> => { - 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 -} +Used `provide` building block will take care of associating a handler to the a +given capability and performing necessary UCAN validation steps when `add` is +invoked. -export const sqrt = async({ capabality }:Invocation):Promise> => { - 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) } -} +### Transport +The library provides a pluggable transport architecture so you can expose a service in various content encodings. To do so you have to provide: -// heirarchical mapping of (cap)abilities with corresponding handlers -// 'intro/echo' -> .intro.echo -// 'math/sqrt' -> .math.sqrt -export const service = { - intro: { echo }, - math: { sqrt } -} +1. `decoder` that will take `{ headers: Record, 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, body: Uint8Array }`. +> 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. -class InvalidInputError extends Error { - constructor(input) { - super(`Invalid input: ${input}`) - } -} +Library comes with several transport layer codecs you can pick from, but you can also bring one yourself. Below we will take invocations encoded in [CAR][] format and produce responses encoded in [DAG-CBOR][] format: + +```ts +import * as Server from "@ucanto/server" +import * as CAR from "@ucanto/transport/car" +import * as CBOR from "@ucanto/transport/cbor" +import { SigningAuthority } from "@ucanto/authority" +import * as HTTP from "node:http" +import * as Buffer from "node:buffer" + +export const server = (context { store = new Map() } : { store: Map }) => + Server.create({ + id: await SigningAuthority.derive(process.env.SERVICE_SECRET), + service: service(context), + decoder: CAR, + encoder: CBOR, + + // We tell server that capability can be self-issued by a drive owner + canIssue: (capability, issuer) => { + if (capability.uri.protocol === "file:") { + const [did] = capability.uri.pathname.split("/") + return did === issuer + } + return false + }, + }) ``` -There are few requirements that all handlers MUST meet: - -1. Handler takes a single argument of type `Service.Invocation` 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`, 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, 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, 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. - - ```ts -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, body: Uint8Array }` format. - 1. Decode them into `Invocation`s. - 1. Route and execute corresponding (cap)[ability][] handler. - 1. Encode results back into ``{ headers: Record, body: Uint8Array }` format. - - All you need to do is simply pass the request: - - ```js - export const handler = async (payload:{headers:Record, body:Uint8Array}):Promise<{headers:Record, 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, 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: +> Please note that server does not do HTTP as bindings may differ across runtimes, so it is up to you to plug one in. + +In nodejs we could expose our service as follows: ```ts -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") +export const listen = ({ port = 8080, context = new Map() }) => { + const fileServer = server(context) -// did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi -const alice = keypair.EdKeypair.fromSecretKey("U+bzp2GaFQHso587iSFWPSeCzbSfn/CbNHEz7ilKRZ1UQMmMS7qq4UhTzKn3X9Nj/4xgrwa+UqhMOeo4Ki8JUw==") + HTTP.createServer(async (request, response), => { + const chunks = [] + for await (const chunk of request) { + chunks.push(chunk) + } + + const { headers, body } = await fileServer.request({ + headers: request.headers, + body: Buffer.concat(chunks) + }) + + response.writeHead(200, headers) + response.write(body) + response.end() + }).listen(port) +} +``` + +## Client + +Client can be used to issue and execute UCAN invocations. Here is an example of +invoking `file/link` capability we've defined earlier + +```ts +import * as Client from "@ucanto/client" +import { SigningAuthority, Authority } from "@ucanto/authority" +import { CID } from "multiformats" +// Service will have a well known DID +const service = Authority.parse(process.env.SERVICE_ID) +// Client keypair +const issuer = SigningAuthority.parse(process.env.MY_KEPAIR) -const demo1 = async (connection) => { - const hello = await Client.invoke({ +const demo1 = async connection => { + const me = await Client.invoke({ issuer: alice, - audience: service - can: "intro/echo", - with: "data:text/plain,hello world" + audience: service, + capability: { + can: "file/link", + with: `file://${issuer.did()}/me/about`, + link: CID.parse(process.env.ME_CID), + }, }) - - const result = await hello.execute(connection) - if (result.ok) { - console.log("got echo back", result.value) - } else { + + const result = await me.execute(connection) + if (result.error) { console.error("oops", result) + } else { + console.log("file got linked", result.link.toString()) } } ``` -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. +> Note that the client will get full type inference on when `connection` captures a type of the service on the other side of the wire. -### Transport +### Connection -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, body: Uint8Array }`. - 2. `decoder` takes `{ headers: Record, 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, body: Uint8Array}):Promise<{headers: Record, body: Uint8Array}> }` - - We could create an in-process connection with our service simply by providing service as a channel: - - ```ts +Just like the server, the client has a pluggable transport layer which you provide when you create a connection. We could create an in-process connection with our service simply by providing service as a channel: + +```ts const connection = Client.connect({ - encoder: Transport.CAR, // encode as CAR because server decods from car + 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 + 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: - - ```ts -import * as Transport from "ucanto/src/transport.js" - - const connection = Client.connect({ - encoder: Transport.CAR, // encode as CAR because server decodes from car +``` + +In practice you probably would want client/server communication to happen across the wire, or at least across processes. You can bring your own transport channel, or choose an existing one. For example: + +```ts +import * as HTTP from "@ucanto/transport/http" +import * as CAR from "@ucanto/transport/car" +import * as CBOR from "@ucanto/transport/cbor" + +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} */ - channel: Transport.HTTP.open({ url: new URL(process.env.SERVICE_URL) }) // simple `fetch` wrapper + /** @type {Transport.Channel>} */ + channel: Transport.HTTP.open({ url: new URL(process.env.SERVICE_URL) }), }) - ``` +``` -> Note: That in that case, you ned to provide type annotations, so the client can provide inference for requests and return types +> Note: That in the seconnd example you need to provide a type annotations, so that client can infer what capabilities can be invoked and what the return types it will correspond to. ### 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: ```ts -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") +import { SigningAuthority, Authority } from "@ucanto/authority" +import * as Client from "@ucanto/client" +import { CID } from "multiformats" -// did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi -const alice = keypair.EdKeypair.fromSecretKey("U+bzp2GaFQHso587iSFWPSeCzbSfn/CbNHEz7ilKRZ1UQMmMS7qq4UhTzKn3X9Nj/4xgrwa+UqhMOeo4Ki8JUw==") -// did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob -const bob = keypair.EdKeypair.fromSecretKey("G4+QCX1b3a45IzQsQd4gFMMe0UB1UOx9bCsh8uOiKLER69eAvVXvc8P2yc4Iig42Bv7JD2zJxhyFALyTKBHipg==") +const service = Authority.parse(process.env.SERVICE_DID) +const alice = SigningAuthority.parse(process.env.ALICE_KEYPAIR) +const bob = SigningAuthority.parse(process.env.BOB_KEYPAIR) - -const demo2 = async (connection) => { - const bye = await Client.invoke({ +const demo2 = async connection => { + // Alice delegates capability to mutate FS under bob's namespace + const proof = await Client.delegate({ issuer: alice, - audience: service - can: "intro/echo", - with: "data:text/plain,bye" + audience: bob.authority, + capabilities: [ + { + can: "file/link", + with: `file://${alice.did()}/friends/${bob.did()}/`, + }, + ], }) - - const sqrt = (n) => Client.invoke({ - issuer: alice, + + const aboutBob = Client.invoke({ + issuer: bob, audience: service, - can: "math/sqrt", - with: alice.did(), - n, - proofs: [UCAN.parse(process.env.UCAN)] + capability: { + can: "file/link", + with: `file://${alice.did()}/friends/${bob.did()}/about`, + link: CID.parse(process.env.BOB_CID), + }, }) - - const [r1, r2] = batch(bye, await sqrt(9)).execute(connection) - - if (r1.ok) { - console.log("got echo back", r1.value) - } else { + + const aboutMallory = Client.invoke({ + issuer: bob, + audience: service, + capability: { + can: "file/link", + with: `file://${alice.did()}/friends/${MALLORY_DID}/about`, + link: CID.parse(process.env.MALLORY_CID), + }, + }) + + const [bobResult, malloryResult] = connection.execute([ + aboutBob, + aboutMallory, + ]) + + if (bobResult.error) { console.error("oops", r1) - } - - if (r2.ok) { - console.log("got sqrt", r2.value) } else { + console.log("about bob is linked", r1) + } + + if (malloryResult.error) { console.log("oops", r2) + } else { + console.log("about mallory is linked", r2) } } ``` -## Future - -Intentions are that in the future we may provide a more powerful GraphQL inspired invocation interface along the lines of: - -```ts -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 }) -}) -``` - - -[UCAN]:https://github.com/ucan-wg/spec/ -[RPC]:https://en.wikipedia.org/wiki/Remote_procedure_call -[capability]:https://github.com/ucan-wg/spec/#23-capability -[invocations]:https://github.com/ucan-wg/spec/#28-invocation -[ability]:https://github.com/ucan-wg/spec/#3242-ability -[type union]:https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types -[CAR]:https://ipld.io/specs/transport/car/carv1/ -[DAG-CBOR]:https://ipld.io/specs/codecs/dag-cbor/ +> In the example above first invocation will succeed, but second will not becasue has not been granted capability to mutate other namespace. Also note that both invocations are send in a single request. + +[ucan]: https://github.com/ucan-wg/spec/ +[rpc]: https://en.wikipedia.org/wiki/Remote_procedure_call +[capability]: https://github.com/ucan-wg/spec/#23-capability +[invocations]: https://github.com/ucan-wg/spec/#28-invocation +[ability]: https://github.com/ucan-wg/spec/#3242-ability +[type union]: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types +[car]: https://ipld.io/specs/transport/car/carv1/ +[dag-cbor]: https://ipld.io/specs/codecs/dag-cbor/ +[cid]: https://docs.ipfs.io/concepts/content-addressing/ +[did:key]: https://w3c-ccg.github.io/did-method-key/ diff --git a/package.json b/package.json index 3129f384..f2df0feb 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@ucanto", + "name": "ucanto", "private": true, "type": "module", "workspaces": [ diff --git a/packages/authority/package.json b/packages/authority/package.json index a55875fe..348ddd8b 100644 --- a/packages/authority/package.json +++ b/packages/authority/package.json @@ -19,7 +19,6 @@ }, "homepage": "https://github.com/web3-storage/ucanto", "scripts": { - "prepare": "tsc --build", "test:web": "playwright-test test/**/*.spec.js --cov && nyc report", "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha test/**/*.spec.js", "test": "npm run test:node", diff --git a/packages/client/package.json b/packages/client/package.json index 684f7dde..0d9353b2 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -21,7 +21,6 @@ }, "homepage": "https://github.com/web3-storage/ucanto", "scripts": { - "prepare": "tsc --build", "test:web": "playwright-test test/**/*.spec.js --cov && nyc report", "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha test/**/*.spec.js", "test": "npm run test:node", diff --git a/packages/client/test/services/account.js b/packages/client/test/services/account.js index 237807c3..36ae8aaa 100644 --- a/packages/client/test/services/account.js +++ b/packages/client/test/services/account.js @@ -94,7 +94,7 @@ const associate = (accounts, from, to, proof, create) => { // account and link all them together. if (!fromAccount && !toAccount) { if (create) { - const account = the(`did:cid:${proof}`) + const account = the(`did:ipld:${proof}`) accounts.set(to, { account, proof }) accounts.set(from, { account, proof }) } else { @@ -105,7 +105,7 @@ const associate = (accounts, from, to, proof, create) => { } else if (fromAccount) { accounts.set(to, { account: fromAccount, proof }) } else if (fromAccount !== toAccount) { - const account = the(`did:cid:${proof}`) + const account = the(`did:ipld:${proof}`) accounts.set(toAccount, { account, proof }) accounts.set(fromAccount, { account, proof }) } diff --git a/packages/core/package.json b/packages/core/package.json index 38bb35e6..29dfe55a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -21,7 +21,6 @@ }, "homepage": "https://github.com/web3-storage/ucanto", "scripts": { - "prepare": "tsc --build", "test:web": "playwright-test test/*.spec.js --cov && nyc report", "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha test/*.spec.js", "test": "npm run test:node", diff --git a/packages/interface/package.json b/packages/interface/package.json index f2b42ee3..6cfe1c74 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -19,7 +19,6 @@ }, "homepage": "https://github.com/web3-storage/ucanto", "scripts": { - "prepare": "tsc --build", "typecheck": "tsc --build" }, "dependencies": { diff --git a/packages/interface/src/capability.ts b/packages/interface/src/capability.ts index f7a0ddc4..cb942109 100644 --- a/packages/interface/src/capability.ts +++ b/packages/interface/src/capability.ts @@ -81,8 +81,8 @@ export type InvalidCapability = UnknownCapability | MalformedCapability export interface DerivedMatch extends Match> {} -export interface DeriveSelector { - to: MatchSelector> +export interface DeriveSelector { + to: TheCapabilityParser> derives: Derives } @@ -125,15 +125,12 @@ export interface View extends Matcher, Selector { */ derive( options: DeriveSelector - ): CapabilityParser> + ): TheCapabilityParser> } -export interface TheCapabilityParser< - A extends Ability = Ability, - C extends Caveats = Caveats, - M extends CapabilityMatch = CapabilityMatch -> extends CapabilityParser { - can: A +export interface TheCapabilityParser> + extends CapabilityParser { + readonly can: M["value"]["can"] } export interface CapabilityParser extends View { @@ -236,14 +233,20 @@ export interface ParsedCapability< caveats: C } -export type InferCaveats = { - [Key in keyof C]: C[Key] extends Decoder - ? T - : C[Key] extends Parser +export type InferCaveats = InferRequiredCaveats & InferOptionalCaveats + +export type InferOptionalCaveats = { + [Key in keyof C as C[Key] extends Decoder + ? Key + : never]?: C[Key] extends Decoder ? T - : // : C[Key] extends (input: unknown) => infer T - // ? Exclude - never + : never +} + +export type InferRequiredCaveats = { + [Key in keyof C as C[Key] extends Decoder + ? never + : Key]: C[Key] extends Decoder ? T : never } export interface Descriptor { diff --git a/packages/interface/src/lib.ts b/packages/interface/src/lib.ts index 34ad9140..fcc1ce5a 100644 --- a/packages/interface/src/lib.ts +++ b/packages/interface/src/lib.ts @@ -165,7 +165,9 @@ export interface ServiceMethod< O, X extends { error: true } > { - (input: Invocation, context: InvocationContext): Await> + (input: Invocation, context: InvocationContext): Await< + Result + > } export type InvocationError = @@ -309,7 +311,7 @@ export interface ConnectionView extends Connection { ): Await> } -export interface Server { +export interface TranpsortOptions { /** * Request decoder which is will be used by a server to decode HTTP Request * into an invocation `Batch` that will be executed using a `service`. @@ -321,27 +323,33 @@ export interface Server { * request. */ readonly encoder: Transport.ResponseEncoder +} +export interface ValidatorOptions { /** * Takes authority parser that can be used to turn an `UCAN.Identity` * into `Ucanto.Authority`. */ readonly authority?: AuthorityParser + readonly canIssue?: CanIssue["canIssue"] + readonly my?: InvocationContext["my"] + readonly resolve?: InvocationContext["resolve"] +} + +export interface ServerOptions extends TranpsortOptions, ValidatorOptions { /** * Service DID which will be used to verify that received invocation * audience matches it. */ readonly id: Identity +} +export interface Server extends ServerOptions { /** * Actual service providing capability handlers. */ readonly service: T - - readonly canIssue?: CanIssue["canIssue"] - readonly my?: InvocationContext["my"] - readonly resolve?: InvocationContext["resolve"] } export interface ServerView extends Server, Transport.Channel { diff --git a/packages/server/package.json b/packages/server/package.json index 208a290a..bdfb7b8a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -20,7 +20,6 @@ }, "homepage": "https://github.com/web3-storage/ucanto", "scripts": { - "prepare": "tsc --build", "test:web": "playwright-test test/**/*.spec.js --cov && nyc report", "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha test/**/*.spec.js", "test": "npm run test:node", diff --git a/packages/server/src/api.ts b/packages/server/src/api.ts index cd066232..388c0785 100644 --- a/packages/server/src/api.ts +++ b/packages/server/src/api.ts @@ -24,7 +24,7 @@ export interface ProviderContext< C extends API.Caveats = API.Caveats > { capability: API.ParsedCapability> - invocation: API.Invocation> + invocation: API.Invocation & API.InferCaveats> context: API.InvocationContext } diff --git a/packages/server/src/handler.js b/packages/server/src/handler.js index 9dd120a6..174aa4dc 100644 --- a/packages/server/src/handler.js +++ b/packages/server/src/handler.js @@ -6,15 +6,15 @@ import { access } from "@ucanto/validator" * @template {API.Caveats} C * @template {API.Resource} R * @template {unknown} U - * @param {API.TheCapabilityParser} capability + * @param {API.TheCapabilityParser>} capability * @param {(input:API.ProviderContext) => API.Await} handler - * @returns {API.ServiceMethod, Exclude, Exclude>|API.InvocationError>} + * @returns {API.ServiceMethod & API.InferCaveats, Exclude, Exclude>>} */ export const provide = (capability, handler) => /** - * @param {API.Invocation>} invocation + * @param {API.Invocation & API.InferCaveats>} invocation * @param {API.InvocationContext} options * @return {Promise, Exclude>|API.InvocationError>>} */ diff --git a/packages/server/src/lib.js b/packages/server/src/lib.js index ad92ad37..51ece74e 100644 --- a/packages/server/src/lib.js +++ b/packages/server/src/lib.js @@ -1,5 +1,4 @@ export * from "./server.js" -export * from "./handler.js" +// @ts-ignore export * from "./api.js" - -export { InvocationError } from "./server.js" +export * from "./handler.js" diff --git a/packages/server/src/server.js b/packages/server/src/server.js index c8f180f2..96c5df1f 100644 --- a/packages/server/src/server.js +++ b/packages/server/src/server.js @@ -1,8 +1,14 @@ import * as API from "@ucanto/interface" export * from "@ucanto/interface" -import { InvalidAudience, UnavailableProof } from "@ucanto/validator" +import { InvalidAudience } from "@ucanto/validator" import { Authority } from "@ucanto/authority" -export { capability, URI, Link, Failure } from "@ucanto/validator" +export { + capability, + URI, + Link, + Failure, + MalformedCapability, +} from "@ucanto/validator" /** * Creates a connection to a service. diff --git a/packages/server/test/service/access.js b/packages/server/test/service/access.js index 9c10fcb8..d61d670e 100644 --- a/packages/server/test/service/access.js +++ b/packages/server/test/service/access.js @@ -17,7 +17,7 @@ const registerCapability = Server.capability({ const linkCapability = Server.capability({ can: "identity/link", - with: Server.URI.match({ protocol: "mailto:" }), + with: Server.URI, derives: (claimed, delegated) => claimed.uri.href === delegated.uri.href || new Server.Failure( @@ -96,7 +96,7 @@ const associate = (accounts, from, to, proof, create) => { // account and link all them together. if (!fromAccount && !toAccount) { if (create) { - const account = /** @type {API.DID} */ (`did:cid:${proof}`) + const account = /** @type {API.DID} */ (`did:ipld:${proof}`) accounts.set(to, { account, proof }) accounts.set(from, { account, proof }) } else { @@ -107,7 +107,7 @@ const associate = (accounts, from, to, proof, create) => { } else if (fromAccount) { accounts.set(to, { account: fromAccount, proof }) } else if (fromAccount !== toAccount) { - const account = /** @type {API.DID} */ (`did:cid:${proof}`) + const account = /** @type {API.DID} */ (`did:ipld:${proof}`) accounts.set(toAccount, { account, proof }) accounts.set(fromAccount, { account, proof }) } diff --git a/packages/transport/package.json b/packages/transport/package.json index 750ab396..4944fb0b 100644 --- a/packages/transport/package.json +++ b/packages/transport/package.json @@ -21,7 +21,6 @@ }, "homepage": "https://github.com/web3-storage/ucanto", "scripts": { - "prepare": "tsc --build", "test:web": "playwright-test test/**/*.spec.js --cov && nyc report", "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha test/**/*.spec.js", "test": "npm run test:node", diff --git a/packages/validator/package.json b/packages/validator/package.json index 7653a79e..25c28009 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -19,7 +19,6 @@ }, "homepage": "https://github.com/web3-storage/ucanto", "scripts": { - "prepare": "tsc --build", "test:web": "playwright-test test/**/*.spec.js --cov && nyc report", "test:node": "c8 --check-coverage --branches 90 --functions 80 --lines 90 mocha test/**/*.spec.js", "test": "npm run test:node", diff --git a/packages/validator/src/capability.js b/packages/validator/src/capability.js index 01c22f78..160ab4ab 100644 --- a/packages/validator/src/capability.js +++ b/packages/validator/src/capability.js @@ -9,9 +9,9 @@ import { /** * @template {API.Ability} A - * @template {API.Caveats} C + * @template {API.Caveats} [C={}] * @param {API.Descriptor} descriptor - * @returns {API.TheCapabilityParser>} + * @returns {API.TheCapabilityParser>} */ export const capability = descriptor => new Capability(descriptor) @@ -35,7 +35,7 @@ export const and = (...selectors) => new And(selectors) * @template {API.Match} M * @template {API.ParsedCapability} T * @param {API.DeriveSelector & { from: API.MatchSelector }} options - * @returns {API.CapabilityParser>} + * @returns {API.TheCapabilityParser>} */ export const derive = ({ from, to, derives }) => new Derive(from, to, derives) @@ -62,7 +62,7 @@ class View { /** * @template {API.ParsedCapability} U * @param {API.DeriveSelector} options - * @returns {API.CapabilityParser>} + * @returns {API.TheCapabilityParser>} */ derive({ derives, to }) { return derive({ derives, to, from: this }) @@ -97,7 +97,7 @@ class Unit extends View { /** * @template {API.Ability} A * @template {API.Caveats} C - * @implements {API.TheCapabilityParser>} + * @implements {API.TheCapabilityParser>} * @extends {Unit>} */ class Capability extends Unit { @@ -220,14 +220,14 @@ class And extends View { /** * @template {API.ParsedCapability} T * @template {API.Match} M - * @implements {API.CapabilityParser>} + * @implements {API.TheCapabilityParser>} * @extends {Unit>} */ class Derive extends Unit { /** * @param {API.MatchSelector} from - * @param {API.MatchSelector>} to + * @param {API.TheCapabilityParser>} to * @param {API.Derives} derives */ constructor(from, to, derives) { @@ -236,6 +236,9 @@ class Derive extends Unit { this.to = to this.derives = derives } + get can() { + return this.to.can + } /** * @param {API.Source} capability * @returns {API.MatchResult>} diff --git a/packages/validator/src/lib.js b/packages/validator/src/lib.js index 2157a8d0..b7abcdb9 100644 --- a/packages/validator/src/lib.js +++ b/packages/validator/src/lib.js @@ -8,10 +8,11 @@ import { InvalidSignature, DelegationError, Failure, + MalformedCapability, li, } from "./error.js" -export { Failure, UnavailableProof } +export { Failure, UnavailableProof, MalformedCapability } export { capability } from "./capability.js"