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!: ipld integration to schemas #307

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
8 changes: 6 additions & 2 deletions packages/core/src/cbor.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import * as API from '@ucanto/interface'
import * as CBOR from '@ipld/dag-cbor'
export { code, name, decode } from '@ipld/dag-cbor'
import { sha256 } from 'multiformats/hashes/sha2'
export { decode } from '@ipld/dag-cbor'
import * as sha256 from './sha256.js'
import { create as createLink, isLink } from 'multiformats/link'

// @see https://www.iana.org/assignments/media-types/application/vnd.ipld.dag-cbor
export const contentType = 'application/vnd.ipld.dag-cbor'

/** @type {API.MulticodecCode<typeof CBOR.code, typeof CBOR.name>} */
export const code = CBOR.code
export const name = CBOR.name

/**
* @param {unknown} data
* @param {Set<unknown>} seen
Expand Down
14 changes: 7 additions & 7 deletions packages/core/src/dag.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,18 +106,18 @@ export const notFound = link => {
/**
* @template T
* @template {T} U
* @template {API.MulticodecCode} C
* @template {API.MulticodecCode} A
* @template {MF.BlockEncoder<number, U>} [Codec=MF.BlockEncoder<API.MulticodecCode<typeof CBOR.code, typeof CBOR.name>, U>]
* @template {MF.MultihashHasher} [Hasher=MF.MultihashHasher<API.MulticodecCode<typeof sha256.code, typeof sha256.name>>]
* @param {U} source
* @param {BlockStore<T>} store
* @param {object} options
* @param {MF.BlockEncoder<C, unknown>} [options.codec]
* @param {MF.MultihashHasher<A>} [options.hasher]
* @returns {Promise<API.Block<U, C, A> & { data: U }>}
* @param {Codec} [options.codec]
* @param {Hasher} [options.hasher]
* @returns {Promise<API.Block<U, Codec['code'], Hasher['code']> & { data: U }>}
*/
export const writeInto = async (source, store, options = {}) => {
const codec = /** @type {MF.BlockEncoder<C, U>} */ (options.codec || CBOR)
const hasher = /** @type {MF.MultihashHasher<A>} */ (options.hasher || sha256)
const codec = /** @type {Codec} */ (options.codec || CBOR)
const hasher = /** @type {Hasher} */ (options.hasher || sha256)

const bytes = codec.encode(source)
const digest = await hasher.digest(bytes)
Expand Down
140 changes: 56 additions & 84 deletions packages/core/src/delegation.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,41 +173,6 @@ export class Delegation {
})
}

/**
* @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
}
Expand All @@ -231,22 +196,8 @@ export class Delegation {
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)
return exportDAG(this)
}

/**
Expand All @@ -257,7 +208,7 @@ export class Delegation {
}

iterateIPLDBlocks() {
return exportDAG(this.root, this.blocks, this.attachedLinks)
return exportDAG(this)
}

/**
Expand Down Expand Up @@ -385,7 +336,7 @@ export const archive = async delegation => {

export const ArchiveSchema = Schema.variant({
'[email protected]': /** @type {Schema.Schema<API.UCANLink>} */ (
Schema.link({ version: 1 })
Schema.unknown().link({ version: 1 })
),
})

Expand Down Expand Up @@ -453,11 +404,34 @@ const decode = ({ bytes }) => {
*/

export const delegate = async (
{ issuer, audience, proofs = [], attachedBlocks = new Map(), ...input },
{ issuer, audience, proofs = [], facts = [], ...input },
options
) => {
const links = []
/** @type {Map<string, API.Block>} */
const blocks = new Map()
for (const capability of input.capabilities) {
for (const node of [...Object.values(capability.nb || {}), capability.nb]) {
for (const block of DAG.iterate(node)) {
blocks.set(block.cid.toString(), block)
}
}
}

for (const fact of facts) {
if (fact) {
for (const node of [...Object.values(fact), fact]) {
for (const block of DAG.iterate(node)) {
blocks.set(block.cid.toString(), block)
}
}
}
}

const attachments = [...blocks.values()]
.map(block => block.cid)
.sort((left, right) => left.toString().localeCompare(right.toString()))

for (const proof of proofs) {
if (!isDelegation(proof)) {
links.push(proof)
Expand All @@ -469,11 +443,20 @@ export const delegate = async (
}
}

// We may not have codecs to traverse all the blocks on the other side of the
// wire, which is why we capture all the links in a flat list which we include
// in facts. That way recipient will know all of the blocks without having to
// know to know how to traverse the graph.
if (attachments.length > 0) {
facts.push({ 'ucan/attachments': attachments })
}

const data = await UCAN.issue({
...input,
issuer,
audience,
proofs: links,
facts,
})
const { cid, bytes } = await UCAN.write(data, options)
decodeCache.set(cid, data)
Expand All @@ -482,31 +465,29 @@ export const delegate = async (
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
* @param {object} source
* @param {API.UCANBlock<C>} source.root
* @param {DAG.BlockStore} source.blocks
* @returns {IterableIterator<API.Block>}
*/

export const exportDAG = function* (root, blocks, attachedLinks) {
for (const link of decode(root).proofs) {
export const exportDAG = function* ({ root, blocks }) {
const { proofs, facts, capabilities } = decode(root)
for (const link of 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 links = new Set([...iterateLinks({ facts, capabilities }, blocks)])
for (const link of links) {
const block = blocks.get(link)

if (block) {
Expand Down Expand Up @@ -609,31 +590,22 @@ const proofs = delegation => {
}

/**
* @param {API.Capability<API.Ability, `${string}:${string}`, unknown>} obj
* Traverses source and yields all links found.
*
* @param {unknown} source
* @param {Schema.Region} region
* @returns {IterableIterator<string>}
*/
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)
const iterateLinks = function* (source, region) {
if (source && typeof source === 'object') {
if (Link.isLink(source)) {
yield `${source}`
} else {
for (const member of Object.values(source)) {
yield* iterateLinks(member, region)
}
}
}

recurse(obj)

return links
}

export { Delegation as View }
9 changes: 9 additions & 0 deletions packages/core/src/identity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as API from '@ucanto/interface'

import { identity } from 'multiformats/hashes/identity'

/** @type {API.MulticodecCode<typeof identity.code, typeof identity.name>} */
export const code = identity.code
export const name = identity.name
export const { encode } = identity
export const digest = identity.digest.bind(identity)
16 changes: 0 additions & 16 deletions packages/core/src/invocation.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,6 @@ class IssuedInvocation {
this.notBefore = notBefore
this.nonce = nonce
this.facts = facts

/** @type {API.BlockStore<unknown>} */
this.attachedBlocks = new Map()
}

/**
* Attach a block to the invocation DAG so it would be included in the
* block iterator.
* ⚠️ You should only attach blocks that are referenced from the `capabilities`
* or `facts`, if that is not the case you probably should reconsider.
* ⚠️ Once a delegation is de-serialized the attached blocks will not be re-attached.
*
* @param {API.Block} block
*/
attach(block) {
this.attachedBlocks.set(`${block.cid}`, block)
}

delegate() {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export {
parse as parseLink,
decode as decodeLink,
} from './link.js'
export { sha256 } from 'multiformats/hashes/sha2'
export * as sha256 from './sha256.js'
export * as UCAN from '@ipld/dag-ucan'
export * as DID from '@ipld/dag-ucan/did'
export * as Signature from '@ipld/dag-ucan/signature'
Expand Down
15 changes: 8 additions & 7 deletions packages/core/src/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ import * as Schema from './schema.js'

export const MessageSchema = Schema.variant({
'ucanto/[email protected]': Schema.struct({
execute: Schema.link().array().optional(),
delegate: Schema.dictionary({
execute: Schema.unknown().link().array().optional(),
report: Schema.dictionary({
key: Schema.string(),
value: /** @type {API.Reader<API.Link<API.ReceiptModel>>} */ (
Schema.link()
value: /** @type {Schema.Convert<API.Link<API.ReceiptModel>>} */ (
Schema.unknown().link()
),
})
.array()
.optional(),
}).optional(),
}),
})

Expand Down Expand Up @@ -178,6 +176,9 @@ class Message {
this._invocations = invocations
this._receipts = receipts
}
link() {
return this.root.cid
}
*iterateIPLDBlocks() {
for (const invocation of this.invocations) {
yield* invocation.iterateIPLDBlocks()
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/receipt.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class Receipt {
this._issuer = issuer
}

link() {
return this.root.cid
}
/**
* @returns {Ran|ReturnType<Ran['link']>}
*/
Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/schema.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
export * as URI from './schema/uri.js'
export * as Link from './schema/link.js'
export * as DID from './schema/did.js'
export * as Text from './schema/text.js'
export * from './schema/schema.js'
export { match as link } from './schema/link.js'
export { match as did } from './schema/did.js'
export { match as uri } from './schema/uri.js'
export { match as text } from './schema/text.js'
Loading