Skip to content

Commit

Permalink
feat: add access/delegate capability parser exported from @web3-stora…
Browse files Browse the repository at this point in the history
…ge/capabilities (#420)

Motivation:
* #414

---------

Co-authored-by: Irakli Gozalishvili <contact@gozala.io>
  • Loading branch information
gobengo and Gozala authored Feb 7, 2023
1 parent cddb44a commit e8e2b1a
Show file tree
Hide file tree
Showing 3 changed files with 386 additions and 3 deletions.
9 changes: 8 additions & 1 deletion packages/capabilities/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,14 @@
"unicorn/prefer-number-properties": "off",
"unicorn/prefer-export-from": "off",
"unicorn/no-array-reduce": "off",
"jsdoc/no-undefined-types": "error"
"jsdoc/no-undefined-types": [
"error",
{
"definedTypes": [
"Iterable"
]
}
]
},
"env": {
"mocha": true
Expand Down
99 changes: 98 additions & 1 deletion packages/capabilities/src/access.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*
* @module
*/
import { capability, URI, DID } from '@ucanto/validator'
import { capability, URI, DID, Schema, Failure } from '@ucanto/validator'
// @ts-ignore
// eslint-disable-next-line no-unused-vars
import * as Types from '@ucanto/interface'
Expand Down Expand Up @@ -108,3 +108,100 @@ export const claim = base.derive({
}),
derives: equalWith,
})

// https://github.com/web3-storage/specs/blob/main/w3-access.md#accessdelegate
export const delegate = base.derive({
to: capability({
can: 'access/delegate',
/**
* Field MUST be a space DID with a storage provider. Delegation will be stored just like any other DAG stored using store/add capability.
*
* @see https://github.com/web3-storage/specs/blob/main/w3-access.md#delegate-with
*/
with: DID.match({ method: 'key' }),
nb: {
// keys SHOULD be CIDs, but we won't require it in the schema
/**
* @type {Schema.Schema<AccessDelegateDelegations>}
*/
delegations: Schema.dictionary({
value: Schema.Link.match(),
}),
},
derives: (claim, proof) => {
return (
fail(equalWith(claim, proof)) ||
fail(subsetsNbDelegations(claim, proof)) ||
true
)
},
}),
derives: (claim, proof) => {
// no need to check claim.nb.delegations is subset of proof
// because the proofs types here never include constraints on the nb.delegations set
return fail(equalWith(claim, proof)) || true
},
})

/**
* @typedef {Schema.Dictionary<string, Types.Link<unknown, number, number, 0 | 1>>} AccessDelegateDelegations
*/

/**
* Parsed Capability for access/delegate
*
* @typedef {object} ParsedAccessDelegate
* @property {string} can
* @property {object} nb
* @property {AccessDelegateDelegations} [nb.delegations]
*/

/**
* returns whether the claimed ucan is proves by the proof ucan.
* both are access/delegate, or at least have same semantics for `nb.delegations`, which is a set of delegations.
* checks that the claimed delegation set is equal to or less than the proven delegation set.
* usable with {import('@ucanto/interface').Derives}.
*
* @param {ParsedAccessDelegate} claim
* @param {ParsedAccessDelegate} proof
*/
function subsetsNbDelegations(claim, proof) {
const missingProofs = setDifference(
delegatedCids(claim),
new Set(delegatedCids(proof))
)
if (missingProofs.size > 0) {
return new Failure(
`unauthorized nb.delegations ${[...missingProofs].join(', ')}`
)
}
return true
}

/**
* iterate delegated UCAN CIDs from an access/delegate capability.nb.delegations value.
*
* @param {ParsedAccessDelegate} delegate
* @returns {Iterable<string>}
*/
function* delegatedCids(delegate) {
for (const d of Object.values(delegate.nb.delegations || {})) {
yield d.toString()
}
}

/**
* @template S
* @param {Iterable<S>} minuend - set to subtract from
* @param {Set<S>} subtrahend - subtracted from minuend
*/
function setDifference(minuend, subtrahend) {
/** @type {Set<S>} */
const difference = new Set()
for (const e of minuend) {
if (!subtrahend.has(e)) {
difference.add(e)
}
}
return difference
}
Loading

0 comments on commit e8e2b1a

Please sign in to comment.