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: upload/* capabilities #81

Merged
merged 5 commits into from
Oct 3, 2022
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
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