Skip to content

Commit

Permalink
feat: upload/* capabilities (#81)
Browse files Browse the repository at this point in the history
* feat: upload/* capabilities

* chore: add upload/add tests

* chore: add test for upload list and remove

* tests: Add case for allowing escalation of delegated capability.

* chore: fix test case for escalating capability.

Co-authored-by: ice.breaker <[email protected]>
  • Loading branch information
Gozala and ice-breaker-tg authored Oct 3, 2022
1 parent 3b09d12 commit 6c0e24f
Show file tree
Hide file tree
Showing 5 changed files with 886 additions and 1 deletion.
49 changes: 48 additions & 1 deletion packages/access/src/capabilities/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { Capability, IPLDLink, DID } from '@ipld/dag-ucan'
import type { Capability, IPLDLink, DID, ToString } from '@ipld/dag-ucan'
import type { Block as IPLDBlock } from '@ucanto/interface'
import { codec as CARCodec } from '@ucanto/transport/car'

type AccountDID = DID
type AgentDID = DID
Expand Down Expand Up @@ -56,3 +58,48 @@ export interface StoreRemove extends Capability<'store/remove', DID> {
}

export interface StoreList extends Capability<'store/list', DID> {}

/**
* Logical represenatation of the CAR.
*/
export interface CAR {
roots: IPLDLink[]
blocks: Map<ToString<IPLDLink>, IPLDBlock>
}

export type CARLink = IPLDLink<CAR, typeof CARCodec.code>

/**
* Capability to add arbitrary CID into an account's upload listing.
*/
export interface UploadAdd extends Capability<'upload/add', AccountDID> {
/**
* CID of the file / directory / DAG root that is uploaded.
*/
root: IPLDLink
/**
* List of CAR links which MAY contain contents of this upload. Please
* note that there is no guarantee that linked CARs actually contain
* content related to this upload, it is whatever user deemed semantically
* relevant.
*/
shards: CARLink[]
}

/**
* Capability to list CIDs in the account's upload list.
*/
export interface UploadList extends Capability<'upload/list', AccountDID> {
// ⚠️ We will likely add more fields here to support paging etc... but that
// will come in the future.
}

/**
* Capability to remove arbitrary CID from the account's upload list.
*/
export interface UploadRemove extends Capability<'upload/remove', AccountDID> {
/**
* CID of the file / directory / DAG root to be removed from the upload list.
*/
root: IPLDLink
}
76 changes: 76 additions & 0 deletions packages/access/src/capabilities/upload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { capability, Link, URI } from '@ucanto/server'
import { codec } from '@ucanto/transport/car'
import { equalWith, List, fail, equal } from './utils.js'
import { any } from './any.js'

/**
* All the `upload/*` capabilities which can also be derived
* from `any` (a.k.a `*`) capability.
*/
export const upload = any.derive({
to: capability({
can: 'upload/*',
with: URI.match({ protocol: 'did:' }),
derives: equalWith,
}),
derives: equalWith,
})

// Right now ucanto does not yet has native `*` support, which means
// `store/add` can not be derived from `*` event though it can be
// derived from `store/*`. As a workaround we just define base capability
// here so all store capabilities could be derived from either `*` or
// `store/*`.
const base = any.or(upload)

const CARLink = Link.match({ code: codec.code, version: 1 })

/**
* `store/add` can be derived from the `store/*` capability
* as long as with fields match.
*/
export const add = base.derive({
to: capability({
can: 'upload/add',
with: URI.match({ protocol: 'did:' }),
caveats: {
root: Link.optional(),
shards: List.of(CARLink).optional(),
},
derives: (self, from) => {
return (
fail(equalWith(self, from)) ||
fail(equal(self.caveats.root, from.caveats.root, 'root')) ||
fail(equal(self.caveats.shards, from.caveats.shards, 'shards')) ||
true
)
},
}),
derives: equalWith,
})

export const remove = base.derive({
to: capability({
can: 'upload/remove',
with: URI.match({ protocol: 'did:' }),
caveats: {
root: Link.optional(),
},
derives: (self, from) => {
return (
fail(equalWith(self, from)) ||
fail(equal(self.caveats.root, from.caveats.root, 'root')) ||
true
)
},
}),
derives: equalWith,
})

export const list = base.derive({
to: capability({
can: 'upload/list',
with: URI.match({ protocol: 'did:' }),
}),
derives: equalWith,
})
56 changes: 56 additions & 0 deletions packages/access/src/capabilities/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ export function equalWith(child, parent) {
)
}

/**
* @param {unknown} child
* @param {unknown} parent
* @param {string} constraint
*/

export function equal(child, parent, constraint) {
if (parent === undefined || parent === '*') {
return true
} else if (String(child) !== String(parent)) {
return new Failure(
`Contastraint vilation: ${child} violates imposed ${constraint} constraint ${parent}`
)
} else {
return true
}
}

/**
* @template {Types.ParsedCapability<"store/add"|"store/remove", Types.URI<'did:'>, {link?: Types.Link<unknown, number, number, 0|1>}>} T
* @param {T} claimed
Expand Down Expand Up @@ -67,3 +85,41 @@ export const derives = (claimed, delegated) => {
export function fail(value) {
return value === true ? undefined : value
}

export const List = {
/**
* @template T
* @param {Types.Decoder<unknown, T>} decoder
* @returns {Types.Decoder<unknown, T[]> & { optional(): Types.Decoder<unknown, undefined|Array<T>>}}
*/
of: (decoder) => ({
decode: (input) => {
if (!Array.isArray(input)) {
return new Failure(`Expected to be an array instead got ${input} `)
}
/** @type {T[]} */
const results = []
for (const item of input) {
const result = decoder.decode(item)
if (result?.error) {
return new Failure(
`Array containts invalid element: ${result.message}`
)
} else {
results.push(result)
}
}
return results
},
optional: () => optional(List.of(decoder)),
}),
}

/**
* @template T
* @param {Types.Decoder<unknown, T>} decoder
* @returns {Types.Decoder<unknown, undefined|T, Types.Failure>}
*/
export const optional = (decoder) => ({
decode: (input) => (input === undefined ? input : decoder.decode(input)),
})
55 changes: 55 additions & 0 deletions packages/access/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ import type {
Failure,
Phantom,
Capabilities,
Link as IPLDLink,
} from '@ucanto/interface'

import * as UCAN from '@ipld/dag-ucan'
import type {
IdentityIdentify,
IdentityRegister,
IdentityValidate,
UploadAdd,
UploadList,
UploadRemove,
} from './capabilities/types'
import { VoucherClaim, VoucherRedeem } from './capabilities/types.js'

Expand Down Expand Up @@ -47,6 +51,21 @@ export interface Service {
>
redeem: ServiceMethod<VoucherRedeem, void, Failure>
}
upload: {
add: ServiceMethod<UploadAdd, UploadAddOk, InvalidUpload>
/**
* Upload list has no defined failure conditions (apart from usual ucanto
* errors) which is why it's error is of type `never`. For unknown accounts
* list MUST be considered empty.
*/
list: ServiceMethod<UploadList, UploadListOk, never>
/**
* Upload remove has no defined failure condition (apart from usual ucanto
* errors) which is why it's error is of type `never`. Removing an upload
* not in the list MUST be considered succesful NOOP.
*/
remove: ServiceMethod<UploadRemove, UploadRemoveOk, never>
}
}

export interface AgentMeta {
Expand Down Expand Up @@ -116,3 +135,39 @@ export interface PullRegisterOptions {
issuer: SigningPrincipal
signal?: AbortSignal
}

/**
* Error MAY occur on `upload/add` if provided `shards` contain invalid CIDs e.g
* non CAR cids.
*/

export interface InvalidUpload extends Failure {
name: 'InvalidUpload'
}

/**
* On succeful upload/add provider will respond back with a `root` CID that
* was added.
*/
export interface UploadAddOk {
root: IPLDLink
}

/**
* On succesful upload/list provider returns `uploads` list of `{root}` elements.
* Please note that by wrapping list in an object we create an opportunity to
* extend type in backwards compatible way to accomodate for paging information
* in the future. Likewise list contains `{root}` objects which also would allow
* us to add more fields in a future like size, date etc...
*/
export interface UploadListOk {
uploads: Array<{ root: IPLDLink }>
}

/**
* On succesful upload/remove provider returns empty object. Please not that
* will allow us to extend result type with more things in the future in a
* backwards compatible way.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface UploadRemoveOk {}
Loading

0 comments on commit 6c0e24f

Please sign in to comment.