Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Use schema stuff in the capabilities instead of custom parsing #220

Merged
merged 8 commits into from
Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 44 additions & 65 deletions packages/interface/src/capability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ import {
Await,
IssuedInvocationView,
UCANOptions,
DIDKey,
Verifier,
API,
} from './lib.js'

export interface Source {
capability: Capability
capability: { can: Ability; with: URI; nb?: Caveats }
delegation: Delegation
index: number
}
Expand Down Expand Up @@ -67,7 +65,7 @@ export interface Reader<
}

export interface Caveats {
[key: string]: Reader<any, unknown>
[key: string]: unknown
}

export type MatchResult<M extends Match> = Result<M, InvalidCapability>
Expand All @@ -80,17 +78,12 @@ export type InvalidCapability = UnknownCapability | MalformedCapability
export interface DerivedMatch<T, M extends Match>
extends Match<T, M | DerivedMatch<T, M>> {}

export interface DeriveSelector<M extends Match, T extends ParsedCapability> {
to: TheCapabilityParser<DirectMatch<T>>
derives: Derives<ToDeriveClaim<T>, ToDeriveProof<M['value']>>
}

/**
* Utility type is used to infer the type of the capability passed into
* `derives` handler. It simply makes all `nb` fileds optional because
* `derives` handler. It simply makes all `nb` fields optional because
* in delegation all `nb` fields could be left out implying no restrictions.
*/
export type ToDeriveClaim<T extends ParsedCapability> =
type ToDeriveClaim<T extends ParsedCapability> =
| T
| ParsedCapability<T['can'], T['with'], Partial<T['nb']>>

Expand All @@ -101,18 +94,18 @@ export type ToDeriveClaim<T extends ParsedCapability> =
* all `nb` fields optional, because in delegation all `nb` fields could be
* left out implying no restrictions.
*/
export type ToDeriveProof<T> = T extends ParsedCapability
export type InferDeriveProof<T> = T extends ParsedCapability
? // If it a capability we just make `nb` partial
ToDeriveClaim<T>
InferDelegatedCapability<T>
: // otherwise we need to map tuple
ToDeriveProofs<T>
InferDeriveProofs<T>

/**
* Another helper type which is equivalent of `ToDeriveClaim` except it works
* on tuple of capabilities.
*/
type ToDeriveProofs<T> = T extends [infer U, ...infer E]
? [ToDeriveClaim<U & ParsedCapability>, ...ToDeriveProofs<E>]
type InferDeriveProofs<T> = T extends [infer U, ...infer E]
? [ToDeriveClaim<U & ParsedCapability>, ...InferDeriveProofs<E>]
: T extends never[]
? []
: never
Expand Down Expand Up @@ -154,33 +147,27 @@ export interface View<M extends Match> extends Matcher<M>, Selector<M> {
* })
* ```
*/
derive<T extends ParsedCapability>(
options: DeriveSelector<M, T>
): TheCapabilityParser<DerivedMatch<T, M>>
derive<T extends ParsedCapability>(options: {
to: TheCapabilityParser<DirectMatch<T>>
derives: Derives<T, InferDeriveProof<M['value']>>
}): TheCapabilityParser<DerivedMatch<T, M>>
}

export type InferCaveatParams<T> = keyof T extends never
? never | undefined
: {
[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(
input: InferCreateOptions<M['value']['with'], M['value']['nb']>
): M['value']
): InferCapability<M['value']>

/**
* Creates an invocation of this capability. Function throws exception if
* non-optional fields are omitted.
*/

invoke(
options: InferInvokeOptions<M['value']['with'], M['value']['nb']>
): IssuedInvocationView<M['value']>
): IssuedInvocationView<InferCapability<M['value']>>

/**
* Creates a delegation of this capability. Please note that all the
Expand All @@ -189,12 +176,31 @@ export interface TheCapabilityParser<M extends Match<ParsedCapability>>
*/
delegate(
options: InferDelegationOptions<M['value']['with'], M['value']['nb']>
): Promise<Delegation<[ToDeriveClaim<M['value']>]>>
): Promise<Delegation<[InferDelegatedCapability<M['value']>]>>
}

/**
* When normalize capabilities by removing `nb` if it is a `{}`. This type
* does that normalization at the type level.
*/
export type InferCapability<T extends ParsedCapability> =
keyof T['nb'] extends never
? { can: T['can']; with: T['with'] }
: { can: T['can']; with: T['with']; nb: T['nb'] }

/**
* In delegation capability all the `nb` fields are optional. This type maps
* capability type (as it would be in the invocation) to the form it will be
* in the delegation.
*/
export type InferDelegatedCapability<T extends ParsedCapability> =
keyof T['nb'] extends never
? { can: T['can']; with: T['with'] }
: { can: T['can']; with: T['with']; nb: Partial<T['nb']> }

export type InferCreateOptions<R extends Resource, C extends {} | undefined> =
// If capability has no NB we want to prevent passing it into
// .create funciton so we make `nb` as optional `never` type so
// .create function so we make `nb` as optional `never` type so
// it can not be satisfied
keyof C extends never ? { with: R; nb?: never } : { with: R; nb: C }

Expand All @@ -213,22 +219,13 @@ export type InferDelegationOptions<
}

export type EmptyObject = { [key: string | number | symbol]: never }
type Optionalize<T> = InferRequried<T> & InferOptional<T>

type InferOptional<T> = {
[K in keyof T as T[K] | undefined extends T[K] ? K : never]?: T[K]
}

type InferRequried<T> = {
[K in keyof T as T[K] | undefined extends T[K] ? never : K]: T[K]
}

export interface CapabilityParser<M extends Match = Match> extends View<M> {
/**
* Defines capability that is either `this` or the the given `other`. This
* allows you to compose multiple capabilities into one so that you could
* validate any of one of them without having to maintain list of supported
* capabilities. It is especially useful when dealiving with derived
* capabilities. It is especially useful when dealing with derived
* capability chains when you might derive capability from either one or the
* other.
*/
Expand Down Expand Up @@ -282,7 +279,7 @@ export interface CapabilitiesParser<M extends Match[] = Match[]>
extends View<Amplify<M>> {
/**
* Creates new capability group containing capabilities from this group and
* provedid `other` capability. This method complements `and` method on
* provided `other` capability. This method complements `and` method on
* `Capability` to allow chaining e.g. `read.and(write).and(modify)`.
*/
and<W extends Match>(other: MatchSelector<W>): CapabilitiesParser<[...M, W]>
Expand Down Expand Up @@ -312,39 +309,21 @@ export type InferMatch<Members extends unknown[]> = Members extends []
? [M, ...InferMatch<Rest>]
: never

export type ParsedCapability<
export interface ParsedCapability<
Can extends Ability = Ability,
Resource extends URI = URI,
C extends object = {}
> = keyof C extends never
? { can: Can; with: Resource; nb?: C }
: { can: Can; with: Resource; nb: C }

export type InferCaveats<C> = Optionalize<{
[K in keyof C]: C[K] extends Reader<infer T, unknown, infer _> ? T : never
}>

export interface Descriptor<
A extends Ability,
R extends URI,
C extends Caveats
C extends Caveats = {}
> {
can: A
with: Reader<R, Resource, Failure>

nb?: C

derives?: Derives<
ToDeriveClaim<ParsedCapability<A, R, InferCaveats<C>>>,
ToDeriveClaim<ParsedCapability<A, R, InferCaveats<C>>>
>
can: Can
with: Resource
nb: C
}

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

export interface CanIssue {
/**
Expand Down
6 changes: 3 additions & 3 deletions packages/server/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as API from '@ucanto/interface'
import { InferCaveats, CanIssue, ParsedCapability } from '@ucanto/interface'
import { CanIssue, ParsedCapability } from '@ucanto/interface'

export * from '@ucanto/interface'

Expand All @@ -23,8 +23,8 @@ export interface ProviderContext<
R extends API.URI = API.URI,
C extends API.Caveats = API.Caveats
> {
capability: API.ParsedCapability<A, R, API.InferCaveats<C>>
invocation: API.Invocation<API.Capability<A, R, API.InferCaveats<C>>>
capability: API.ParsedCapability<A, R, C>
invocation: API.Invocation<API.Capability<A, R, C>>

context: API.InvocationContext
}
Expand Down
8 changes: 4 additions & 4 deletions packages/server/src/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import { access } from '@ucanto/validator'
* @template {API.URI} R
* @template {API.Caveats} C
* @template {unknown} U
* @param {API.CapabilityParser<API.Match<API.ParsedCapability<A, R, API.InferCaveats<C>>>>} capability
* @param {(input:API.ProviderInput<API.ParsedCapability<A, R, API.InferCaveats<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}>>>}
* @param {API.CapabilityParser<API.Match<API.ParsedCapability<A, R, C>>>} capability
* @param {(input:API.ProviderInput<API.ParsedCapability<A, R, C>>) => API.Await<U>} handler
* @returns {API.ServiceMethod<API.Capability<A, R, 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, C>>} invocation
* @param {API.InvocationContext} options
*/
async (invocation, options) => {
Expand Down
9 changes: 5 additions & 4 deletions packages/server/test/server.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import * as CBOR from '@ucanto/transport/cbor'
import { alice, bob, mallory, service as w3 } from './fixtures.js'
import * as Service from '../../client/test/service.js'
import { test, assert } from './test.js'
import { Schema } from '@ucanto/validator'

const storeAdd = Server.capability({
can: 'store/add',
with: Server.URI.match({ protocol: 'did:' }),
nb: {
nb: Schema.struct({
link: Server.Link.match().optional(),
},
}),
derives: (claimed, delegated) => {
if (claimed.with !== delegated.with) {
return new Server.Failure(
Expand All @@ -34,9 +35,9 @@ const storeAdd = Server.capability({
const storeRemove = Server.capability({
can: 'store/remove',
with: Server.URI.match({ protocol: 'did:' }),
nb: {
nb: Schema.struct({
link: Server.Link.match().optional(),
},
}),
derives: (claimed, delegated) => {
if (claimed.with !== delegated.with) {
return new Server.Failure(
Expand Down
9 changes: 5 additions & 4 deletions packages/server/test/service/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { provide } from '../../src/handler.js'
import * as API from './api.js'
import * as Access from './access.js'
import { service as issuer } from '../fixtures.js'
import { Schema } from '@ucanto/validator/src/lib.js'

const addCapability = Server.capability({
can: 'store/add',
with: Server.URI.match({ protocol: 'did:' }),
nb: {
nb: Schema.struct({
link: Server.Link.match().optional(),
},
}),
derives: (claimed, delegated) => {
if (claimed.with !== delegated.with) {
return new Server.Failure(
Expand All @@ -34,9 +35,9 @@ const addCapability = Server.capability({
const removeCapability = Server.capability({
can: 'store/remove',
with: Server.URI.match({ protocol: 'did:' }),
nb: {
nb: Schema.struct({
link: Server.Link.match().optional(),
},
}),
derives: (claimed, delegated) => {
if (claimed.with !== delegated.with) {
return new Server.Failure(
Expand Down
Loading