UNPKG

@ucanto/core

Version:
640 lines (575 loc) 16.7 kB
import * as UCAN from '@ipld/dag-ucan' import * as API from '@ucanto/interface' import * as Link from './link.js' import * as DAG from './dag.js' import * as CAR from './car.js' import * as CBOR from './cbor.js' import * as Schema from './schema.js' import { ok, error } from './result.js' /** * @deprecated * Import `isLink` from module directly */ export const isLink = /** @type {(value:API.Proof) => value is API.UCANLink} */ (Link.isLink) /** * * @param {API.Proof} proof * @return {proof is API.Delegation} */ export const isDelegation = proof => !Link.isLink(proof) /** * Takes one or more delegations and returns all delegated capabilities in * UCAN 0.10 format, expanding all the special forms like `with: ucan:*` and * `can: *` to explicit forms. * * Note that this function only considers included proofs and ignores linked * proofs. It is up to the user of this function to resolve whatever proofs it * needs and build delegation with them before calling this function. * * Also note that this function does not validate the delegations and may * produce result containing capabilities that escalate, which for the validator * perspective is no different from not including such capabilities. * * @template {[API.Delegation, ...API.Delegation[]]} T * @param {T} delegations * @returns {API.InferAllowedFromDelegations<T>} */ export const allows = (...delegations) => { /** @type {API.Allows} */ let allow = {} for (const delegation of delegations) { for (const { with: uri, can, nb } of iterateCapabilities(delegation)) { const resource = allow[uri] || (allow[uri] = {}) const abilities = resource[can] || (resource[can] = []) abilities.push({ ...nb }) } } return /** @type {API.InferAllowedFromDelegations<T>} */ (allow) } /** * Function takes a delegation and iterates over all the capabilities expanding * all the special forms like `with: ucan:*` and `can: *`. * * Note that this function only considers proofs that are included in the * delegation, linked proofs will not be resolved nor considered. It is up to * the user of this function to resolve whatever proofs it needs to consider * before calling this function. * * @param {API.Delegation} delegation * @returns {Iterable<API.Capability>} */ const iterateCapabilities = function* ({ issuer, capabilities, proofs }) { for (const own of capabilities) { // If `with` field is set to `ucan:*` it implies re-delegation of all own // and delegated capabilities. if (own.with === 'ucan:*') { // Fist we include own capabilities. Note that we can not expand `can` // because it implicitly covers all possible options in the universe. yield { ...own, with: issuer.did(), } // Next we iterate over all delegated capabilities including ones that // match ability in the `own.can` field. for (const proof of proofs) { // We only consider proofs that are included and ignore linked proofs. if (isDelegation(proof)) { for (const capability of iterateCapabilities(proof)) { // We attempt to match `capability.can` against `own.can` field // if there is a match we include the capability otherwise we skip const can = matchAbility(capability.can, own.can) if (can) { yield { ...capability, can, // We do not know capability semantics so it is impossible // for us to eliminate capabilities that do not satisfy imposed // caveats (`own.nb`). Therefore we optimistically assume that // `own.nb` further constraints `capability.nb` and do a shallow // merge of the two. As a result we may include capabilities // that during validation will be considered invalid due to // constraint violations. While that is not ideal validator // will treat them as if they were omitted and therefore it // is a reasonable compromise. nb: { ...capability.nb, ...Object(own.nb) }, } } } } } } else { yield own } } } /** * Function takes `can` field from the delegated capability and attempts to * match it against `can` field of the claimed capability. If there is a match * the function returns more specific `can` field of two, otherwise it returns * `null`. * * @param {API.Ability} provided * @param {API.Ability} claimed */ const matchAbility = (provided, claimed) => { // If provided capability delegates all abilities we can derive any `can` // from it so we return `claimed` as is. if (provided === '*') { return claimed } // If claimed capability delegates all abilities that includes any `can` // so we return `provided` as is. if (claimed === '*') { return provided } // If claimed `can` is a pattern that includes `provided` `can` we return // `provided` as is. if (claimed.endsWith('/*') && provided.startsWith(claimed.slice(0, -1))) { return provided } // If provided `can` is a pattern that includes `claimed` `can` we can derive // `claimed` from it so we return `claimed` as is. if (provided.endsWith('/*') && claimed.startsWith(provided.slice(0, -1))) { return claimed } // If `can` fields are concrete and the same we have a match and can return it. if (provided === claimed) { return provided } // otherwise two are incompatible and we return null. return null } /** * Represents UCAN chain view over the set of DAG UCAN nodes. You can think of * this as UCAN interface of the CAR. * * @template {API.Capabilities} C * @implements {API.Delegation<C>} * @extends {DelegationView<C>} */ export class Delegation { /** * @param {API.UCANBlock<C>} root * @param {DAG.BlockStore} [blocks] */ constructor(root, blocks = new Map()) { this.root = root this.blocks = blocks Object.defineProperties(this, { blocks: { enumerable: false, }, }) } /** * @returns {API.AttachedLinkSet} */ get attachedLinks() { const _attachedLinks = new Set() const ucanView = this.data // Get links from capabilities nb for (const capability of ucanView.capabilities) { /** @type {Link[]} */ const links = getLinksFromObject(capability) for (const link of links) { _attachedLinks.add(`${link}`) } } // Get links from facts values for (const fact of ucanView.facts) { if (Link.isLink(fact)) { _attachedLinks.add(`${fact}`) } else { /** @type {Link[]} */ // @ts-expect-error isLink does not infer value type const links = Object.values(fact).filter(e => Link.isLink(e)) for (const link of links) { _attachedLinks.add(`${link}`) } } } return _attachedLinks } get version() { return this.data.version } get signature() { return this.data.signature } get cid() { return this.root.cid } link() { return this.root.cid } get asCID() { return this.cid } get bytes() { return this.root.bytes } get data() { const data = decode(this.root) Object.defineProperties(this, { data: { value: data, enumerable: false } }) return data } /** * Attach a block to the delegation DAG so it would be included in the * block iterator. * ⚠️ You can only attach blocks that are referenced from the `capabilities` * or `facts`. * * @param {API.Block} block */ attach(block) { if (!this.attachedLinks.has(`${block.cid.link()}`)) { throw new Error(`given block with ${block.cid} is not an attached link`) } this.blocks.set(`${block.cid}`, block) } export() { return exportDAG(this.root, this.blocks, this.attachedLinks) } /** * @returns {API.Await<API.Result<Uint8Array, Error>>} */ archive() { return archive(this) } iterateIPLDBlocks() { return exportDAG(this.root, this.blocks, this.attachedLinks) } /** * @type {API.Proof[]} */ get proofs() { return proofs(this) } /** * @type {API.Principal} */ get issuer() { return this.data.issuer } /** * @type {API.Principal} */ get audience() { return this.data.audience } /** * @returns {C} */ get capabilities() { return /** @type {C} */ (this.data.capabilities) } /** * @returns {number} */ get expiration() { return this.data.expiration } /** * @returns {undefined|number} */ get notBefore() { return this.data.notBefore } /** * @returns {undefined|string} */ get nonce() { return this.data.nonce } /** * @returns {API.Fact[]} */ get facts() { return this.data.facts } /** * Iterate over the proofs * * @returns {IterableIterator<API.Delegation>} */ iterate() { return it(this) } delegate() { return this } buildIPLDView() { return this } /** * @returns {API.DelegationJSON<this>} */ toJSON() { return /** @type {any} */ ({ ...this.data.toJSON(), '/': this.cid.toString(), prf: this.proofs.map(proof => isDelegation(proof) ? proof : { '/': proof.toString() } ), }) } } /** * Writes given `Delegation` chain into a content addressed archive (CAR) * buffer and returns it. * * @param {API.Delegation} delegation} * @returns {Promise<API.Result<Uint8Array, Error>>} */ export const archive = async delegation => { try { // Iterate over all of the blocks in the DAG and add them to the // block store. const store = new Map() for (const block of delegation.iterateIPLDBlocks()) { store.set(`${block.cid}`, block) } // Then we we create a descriptor block to describe what this DAG represents // and it to the block store as well. const variant = await CBOR.write({ [`ucan@${delegation.version}`]: delegation.root.cid, }) store.set(`${variant.cid}`, variant) // And finally we encode the whole thing into a CAR. const bytes = CAR.encode({ roots: [variant], blocks: store, }) return ok(bytes) } catch (cause) { return error(/** @type {Error} */ (cause)) } } export const ArchiveSchema = Schema.variant({ 'ucan@0.9.1': /** @type {Schema.Schema<API.UCANLink>} */ ( Schema.link({ version: 1 }) ), }) /** * Extracts a `Delegation` chain from a given content addressed archive (CAR) * buffer. Assumes that the CAR contains a single root block corresponding to * the delegation variant. * * @param {Uint8Array} archive */ export const extract = async archive => { try { const { roots, blocks } = CAR.decode(archive) const [root] = roots if (root == null) { return Schema.error('CAR archive does not contain a root block') } const { bytes } = root const variant = CBOR.decode(bytes) const [, link] = ArchiveSchema.match(variant) return ok(view({ root: link, blocks })) } catch (cause) { return error(/** @type {Error} */ (cause)) } } /** * @param {API.Delegation} delegation * @returns {IterableIterator<API.Delegation>} */ const it = function* (delegation) { for (const proof of delegation.proofs) { if (isDelegation(proof)) { yield* it(proof) yield proof } } } const decodeCache = new WeakMap() /** * @template {API.Capabilities} C * @param {API.UCANBlock<C>} block * @returns {UCAN.View<C>} */ const decode = ({ bytes }) => { const data = decodeCache.get(bytes) if (!data) { const data = UCAN.decode(bytes) decodeCache.set(bytes, data) return data } return data } /** * Creates a new signed token with a given `options.issuer`. If expiration is * not set it defaults to 30 seconds from now. Returns UCAN in primary - IPLD * representation. * * @template {API.Capabilities} C * @param {API.DelegationOptions<C>} data * @param {API.EncodeOptions} [options] * @returns {Promise<API.Delegation<C>>} */ export const delegate = async ( { issuer, audience, proofs = [], attachedBlocks = new Map(), ...input }, options ) => { const links = [] const blocks = new Map() for (const proof of proofs) { if (!isDelegation(proof)) { links.push(proof) } else { links.push(proof.cid) for (const block of proof.export()) { blocks.set(block.cid.toString(), block) } } } const data = await UCAN.issue({ ...input, issuer, audience, proofs: links, }) const { cid, bytes } = await UCAN.write(data, options) decodeCache.set(cid, data) /** @type {API.Delegation<C>} */ const delegation = new Delegation({ cid, bytes }, blocks) Object.defineProperties(delegation, { proofs: { value: proofs } }) for (const block of attachedBlocks.values()) { delegation.attach(block) } return delegation } /** * @template {API.Capabilities} C * @param {API.UCANBlock<C>} root * @param {DAG.BlockStore} blocks * @param {API.AttachedLinkSet} attachedLinks * @returns {IterableIterator<API.Block>} */ export const exportDAG = function* (root, blocks, attachedLinks) { for (const link of decode(root).proofs) { // Check if block is included in this delegation const root = /** @type {UCAN.Block} */ (blocks.get(`${link}`)) if (root) { yield* exportSubDAG(root, blocks) } } for (const link of attachedLinks.values()) { const block = blocks.get(link) if (block) { // @ts-expect-error can get blocks with v0 and v1 yield block } } yield root } /** * @template {API.Capabilities} C * @param {API.UCANBlock<C>} root * @param {DAG.BlockStore} blocks * @returns {IterableIterator<API.Block>} */ const exportSubDAG = function* (root, blocks) { for (const link of decode(root).proofs) { // Check if block is included in this delegation const root = /** @type {UCAN.Block} */ (blocks.get(`${link}`)) if (root) { yield* exportSubDAG(root, blocks) } } yield root } /** * @template {API.Capabilities} C * @param {Iterable<API.Block>} dag * @returns {API.Delegation<C>} */ export const importDAG = dag => { /** @type {Array<[string, API.Block]>} */ let entries = [] for (const block of dag) { entries.push([block.cid.toString(), block]) } const last = entries.pop() if (!last) { throw new RangeError('Empty DAG can not be turned into a delegation') } else { const [, root] = last return new Delegation( /** @type {API.UCANBlock<C>} */ (root), new Map(entries) ) } } /** * @template {API.Capabilities} C * @param {object} dag * @param {API.UCANBlock<C>} dag.root * @param {DAG.BlockStore} [dag.blocks] * @returns {API.Delegation<C>} */ export const create = ({ root, blocks }) => new Delegation(root, blocks) /** * @template {API.Capabilities} C * @template [E=never] * @param {object} dag * @param {API.UCANLink<C>} dag.root * @param {DAG.BlockStore} dag.blocks * @param {E} [fallback] * @returns {API.Delegation<C>|E} */ export const view = ({ root, blocks }, fallback) => { const block = DAG.get(root, blocks, null) if (block == null) { return fallback !== undefined ? fallback : DAG.notFound(root) } return create({ root: block, blocks }) } /** * @param {API.Delegation} delegation */ const proofs = delegation => { /** @type {API.Proof[]} */ const proofs = [] const { root, blocks } = delegation // Iterate over proof links and materialize Delegation views. for (const link of decode(root).proofs) { // Check if linked proof is included in our blocks if so create delegation // view otherwise use a link const root = /** @type {UCAN.Block} */ (blocks.get(link.toString())) proofs.push(root ? create({ root, blocks }) : link) } // we cache result of this computation as this property may get accessed // more than once. Object.defineProperty(delegation, 'proofs', { value: proofs }) return proofs } /** * @param {API.Capability<API.Ability, `${string}:${string}`, unknown>} obj */ function getLinksFromObject(obj) { /** @type {Link[]} */ const links = [] /** * @param {object} obj */ function recurse(obj) { for (const key in obj) { // @ts-expect-error record type not inferred const value = obj[key] if (Link.isLink(value)) { // @ts-expect-error isLink does not infer value type links.push(value) } else if (value && typeof value === 'object') { recurse(value) } } } recurse(obj) return links } export { Delegation as View }