Skip to content

Commit

Permalink
feat(ucanto): URI protocol type retention & capability constructors
Browse files Browse the repository at this point in the history
* fix(ucanto): binary to CBOR encoding

* feat(ucanto): retain protocol in type parsed URI

* fix(ucanto): typo in the readme

* chore(ucanto): reformat cherry-picked changes

* fix(ucanto): coverage for @ucanto/core

* fix(ucanto): type regressions

* fix(ucanto): failing transport test
  • Loading branch information
Gozala authored Jun 22, 2022
1 parent 6b2e9e0 commit e291544
Show file tree
Hide file tree
Showing 15 changed files with 189 additions and 82 deletions.
5 changes: 2 additions & 3 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,19 +118,18 @@ export const server = (context { store = new Map() } : { store: Map<string, stri
In nodejs we could expose our service as follows:

```ts

export const listen = ({ port = 8080, context = new Map() }) => {
const fileServer = server(context)

HTTP.createServer(async (request, response), => {
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)
body: Buffer.concat(chunks),
})

response.writeHead(200, headers)
Expand Down
74 changes: 47 additions & 27 deletions packages/interface/src/capability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Identity,
Resource,
Ability,
URI,
Capability,
DID,
LinkedProof,
Expand Down Expand Up @@ -54,18 +55,12 @@ export interface MatchSelector<M extends Match>

export interface DirectMatch<T> extends Match<T, DirectMatch<T>> {}

export type Parser<
I extends unknown,
O extends unknown,
X extends { error: true } = Failure
> = (input: I) => Result<O, X>

export interface Decoder<
I extends unknown,
O extends unknown,
X extends { error: true } = Failure
> {
decode: Parser<I, O, X> //(input: I): Result<O, X>
decode: (input: I) => Result<O, X>
}

export interface Caveats
Expand Down Expand Up @@ -128,9 +123,19 @@ export interface View<M extends Match> extends Matcher<M>, Selector<M> {
): TheCapabilityParser<DerivedMatch<T, M>>
}

export type InferCaveatParams<T> = {
[K in keyof T]: T[K] extends { toJSON(): infer U } ? U : T[K]
}

export interface TheCapabilityParser<M extends Match<ParsedCapability>>
extends CapabilityParser<M> {
readonly can: M['value']['can']

create: (
resource: M['value']['uri']['href'],
caveats: InferCaveatParams<M['value']['caveats']>
) => Capability<M['value']['can'], M['value']['uri']['href']> &
M['value']['caveats']
}

export interface CapabilityParser<M extends Match = Match> extends View<M> {
Expand Down Expand Up @@ -225,43 +230,49 @@ export type InferMatch<Members extends unknown[]> = Members extends []

export interface ParsedCapability<
Can extends Ability = Ability,
Resource extends URI = URI,
C extends object = {}
> {
can: Can
with: Resource
uri: URL
with: Resource['href']
uri: Resource
caveats: C
}

export type InferCaveats<C> = InferRequiredCaveats<C> & InferOptionalCaveats<C>

export type InferOptionalCaveats<C> = {
[Key in keyof C as C[Key] extends Decoder<any, infer _ | undefined, any>
? Key
: never]?: C[Key] extends Decoder<unknown, infer T | undefined, infer _>
? T
: never
[K in keyof C as C[K] extends Decoder<unknown, infer _T | undefined, infer _>
? K
: never]?: C[K] extends Decoder<unknown, infer T, infer _> ? T : never
}

export type InferRequiredCaveats<C> = {
[Key in keyof C as C[Key] extends Decoder<any, infer _ | undefined, any>
[K in keyof C as C[K] extends Decoder<unknown, infer _T | undefined, infer _>
? never
: Key]: C[Key] extends Decoder<unknown, infer T, infer _> ? T : never
: K]: C[K] extends Decoder<unknown, infer T, infer _> ? T : never
}

export interface Descriptor<A extends Ability, C extends Caveats> {
export interface Descriptor<
A extends Ability,
R extends URI,
C extends Caveats
> {
can: A
with: Decoder<Resource, URL, Failure>
with: Decoder<Resource, R, Failure>
caveats?: C

derives: Derives<
ParsedCapability<A, InferCaveats<C>>,
ParsedCapability<A, InferCaveats<C>>
ParsedCapability<A, R, InferCaveats<C>>,
ParsedCapability<A, R, InferCaveats<C>>
>
}

export interface CapabilityMatch<A extends Ability, C extends Caveats>
extends DirectMatch<ParsedCapability<A, InferCaveats<C>>> {}
export interface CapabilityMatch<
A extends Ability,
R extends URI,
C extends Caveats
> extends DirectMatch<ParsedCapability<A, R, InferCaveats<C>>> {}

export interface CanIssue {
/**
Expand All @@ -271,26 +282,35 @@ export interface CanIssue {
canIssue(capability: ParsedCapability, issuer: DID): boolean
}

export interface ValidationOptions<
C extends ParsedCapability = ParsedCapability
> extends CanIssue {
export interface AuthorityOptions {
authority: AuthorityParser
}

export interface IssuingOptions {
/**
* You can provide default set of capabilities per did, which is used to
* validate whether claim is satisfied by `{ with: my:*, can: "*" }`. If
* not provided resolves to `[]`.
*/

my?: (issuer: DID) => Capability[]
}

export interface ProofResolver extends AuthorityOptions, IssuingOptions {
/**
* You can provide a proof resolver that validator will call when UCAN
* links to external proof. If resolver is not provided validator may not
* be able to explore correesponding path within a proof chain.
*/
resolve?: (proof: LinkedProof) => Await<Result<Delegation, UnavailableProof>>
}

authority: AuthorityParser
capability: CapabilityParser<Match<C>>
export interface ValidationOptions<C extends ParsedCapability>
extends CanIssue,
IssuingOptions,
AuthorityOptions,
ProofResolver {
capability: CapabilityParser<Match<C, any>>
}

export interface DelegationError extends Failure {
Expand Down
10 changes: 10 additions & 0 deletions packages/interface/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,13 @@ export type Service = Record<
>

export type Await<T> = T | PromiseLike<T> | Promise<T>

export type Protocol<Scheme extends string = string> = `${Scheme}:`
export interface URI<P extends Protocol = Protocol> extends URL {
protocol: P
href: `${P}${string}`
}

export type URIString<P extends URI> = `${URI['protocol']}${string}` & {
protocol?: Protocol
}
6 changes: 3 additions & 3 deletions packages/server/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ export interface ProviderOptions extends CanIssue {

export interface ProviderContext<
A extends API.Ability = API.Ability,
R extends API.Resource = API.Resource,
R extends API.URI = API.URI,
C extends API.Caveats = API.Caveats
> {
capability: API.ParsedCapability<A, API.InferCaveats<C>>
invocation: API.Invocation<API.Capability<A, R> & API.InferCaveats<C>>
capability: API.ParsedCapability<A, R, API.InferCaveats<C>>
invocation: API.Invocation<API.Capability<A, R['href']> & API.InferCaveats<C>>

context: API.InvocationContext
}
14 changes: 9 additions & 5 deletions packages/server/src/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,26 @@ import { access } from '@ucanto/validator'
/**
* @template {API.Ability} A
* @template {API.Caveats} C
* @template {API.Resource} R
* @template {API.URI} R
* @template {unknown} U
* @param {API.TheCapabilityParser<API.CapabilityMatch<A, C>>} capability
* @template {API.Match} Z
* @param {API.CapabilityParser<API.Match<API.ParsedCapability<A, R, API.InferCaveats<C>>, Z>>} capability
* @param {(input:API.ProviderContext<A, R, C>) => API.Await<U>} handler
* @returns {API.ServiceMethod<API.Capability<A, R> & API.InferCaveats<C>, Exclude<U, {error:true}>, Exclude<U, Exclude<U, {error:true}>>>}
* @returns {API.ServiceMethod<API.Capability<A, R['href']> & API.InferCaveats<C>, Exclude<U, {error:true}>, Exclude<U, Exclude<U, {error:true}>>>}
*/

export const provide =
(capability, handler) =>
/**
* @param {API.Invocation<API.Capability<A, R> & API.InferCaveats<C>>} invocation
* @param {API.Invocation<API.Capability<A, R['href']> & API.InferCaveats<C>>} invocation
* @param {API.InvocationContext} options
* @return {Promise<API.Result<Exclude<U, {error:true}>, Exclude<U, Exclude<U, {error:true}>>|API.InvocationError>>}
*/
async (invocation, options) => {
const authorization = await access(invocation, { ...options, capability })
const authorization = await access(invocation, {
...options,
capability,
})
if (authorization.error) {
return authorization
} else {
Expand Down
3 changes: 3 additions & 0 deletions packages/server/src/lib.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export * from './server.js'
export * from '@ucanto/authority'
export * from '@ucanto/core'
// @ts-ignore
export * from './api.js'
export * from './handler.js'
export * as API from './api.js'
1 change: 0 additions & 1 deletion packages/server/src/server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as API from '@ucanto/interface'
export * from '@ucanto/interface'
import { InvalidAudience } from '@ucanto/validator'
import { Authority } from '@ucanto/authority'
export {
Expand Down
1 change: 1 addition & 0 deletions packages/server/test/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const mallory = SigningAuthority.parse(
'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU='
)

/** did:key:z6MkrZ1r5XBFZjBU34qyD8fueMbMRkKw17BZaq2ivKFjnz2z */
export const service = SigningAuthority.parse(
'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8='
)
2 changes: 1 addition & 1 deletion packages/server/test/server.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Client from '@ucanto/client'
import * as Server from '../src/server.js'
import * as Server from '../src/lib.js'
import * as CAR from '@ucanto/transport/car'
import * as CBOR from '@ucanto/transport/cbor'
import { alice, bob, mallory, service as w3 } from './fixtures.js'
Expand Down
4 changes: 4 additions & 0 deletions packages/transport/src/cbor/codec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const prepare = (data, seen) => {
return cid
}

if (ArrayBuffer.isView(data)) {
return data
}

if (Array.isArray(data)) {
seen.add(data)
const items = []
Expand Down
6 changes: 6 additions & 0 deletions packages/transport/test/cbor.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { test, assert } from './test.js'
import * as CBOR from '../src/cbor.js'
import { decode, encode } from '@ipld/dag-cbor'
import * as UTF8 from '../src/utf8.js'
test('encode / decode', async () => {
// @ts-ignore
const response = CBOR.encode([{ ok: true, value: 1 }])
Expand Down Expand Up @@ -70,6 +71,11 @@ test('content-type case', async () => {
})
}

test(`encode / decode bytes`, async () => {
const actual = transcode({ bytes: UTF8.encode('hello') })
assert.deepEqual(actual, { bytes: UTF8.encode('hello') })
})

test('circular objects throw', () => {
const circular = { a: 1, circle: {} }
circular.circle = circular
Expand Down
Loading

0 comments on commit e291544

Please sign in to comment.