Skip to content

Commit

Permalink
fix: capture attachments links from capabilities and facts (#298)
Browse files Browse the repository at this point in the history
  • Loading branch information
vasco-santos authored May 2, 2023
1 parent f72e654 commit b2157fb
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 20 deletions.
85 changes: 72 additions & 13 deletions packages/core/src/delegation.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,16 +162,48 @@ export class Delegation {
this.root = root
this.blocks = blocks

/** @type {API.AttachedLinkSet} */
this.attachedLinks = new Set()

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
}
Expand All @@ -198,14 +230,15 @@ export class Delegation {
/**
* Attach a block to the delegation 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.
* ⚠️ You can only attach blocks that are referenced from the `capabilities`
* or `facts`.
*
* @param {API.Block} block
*/
attach(block) {
this.attachedLinks.add(`${block.cid}`)
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() {
Expand Down Expand Up @@ -345,7 +378,7 @@ const decode = ({ bytes }) => {
*/

export const delegate = async (
{ issuer, audience, proofs = [], attachedBlocks = new Map, ...input },
{ issuer, audience, proofs = [], attachedBlocks = new Map(), ...input },
options
) => {
const links = []
Expand Down Expand Up @@ -401,12 +434,10 @@ export const exportDAG = function* (root, blocks, attachedLinks) {
for (const link of attachedLinks.values()) {
const block = blocks.get(link)

/* c8 ignore next 3 */
if (!block) {
throw new Error(`Attached block with link ${link} is not in BlockStore`)
if (block) {
// @ts-expect-error can get blocks with v0 and v1
yield block
}
// @ts-expect-error can get blocks with v0 and v1
yield block
}

yield root
Expand Down Expand Up @@ -499,4 +530,32 @@ const proofs = delegation => {
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 }
49 changes: 48 additions & 1 deletion packages/core/test/delegation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,19 +290,50 @@ test('.buildIPLDView() return same value', async () => {
assert.equal(ucan.buildIPLDView(), ucan)
})

test('delegation.attach', async () => {
test('delegation.attach block in capabiliy', async () => {
const block = await getBlock({ test: 'inlineBlock' })
const ucan = await Delegation.delegate({
issuer: alice,
audience: bob,
capabilities: [
{
can: 'store/add',
with: alice.did(),
nb: {
inlineBlock: block.cid.link()
}
},
],
})

ucan.attach(block)

const delegationBlocks = []
for (const b of ucan.iterateIPLDBlocks()) {
delegationBlocks.push(b)
}

assert.ok(delegationBlocks.find(b => b.cid.equals(block.cid)))
})

test('delegation.attach block in facts', async () => {
const block = await getBlock({ test: 'inlineBlock' })
const ucan = await Delegation.delegate({
issuer: alice,
audience: bob,
capabilities: [
{
can: 'store/add',
with: alice.did(),
},
],
facts: [
{ [`${block.cid.link()}`]: block.cid.link() },
// @ts-expect-error Link has fact entry
block.cid.link()
]
})

ucan.attach(block)

const delegationBlocks = []
Expand All @@ -312,3 +343,19 @@ test('delegation.attach', async () => {

assert.ok(delegationBlocks.find(b => b.cid.equals(block.cid)))
})

test('delegation.attach fails to attach block with not attached link', async () => {
const ucan = await Delegation.delegate({
issuer: alice,
audience: bob,
capabilities: [
{
can: 'store/add',
with: alice.did()
},
],
})

const block = await getBlock({ test: 'inlineBlock' })
assert.throws(() => ucan.attach(block))
})
32 changes: 26 additions & 6 deletions packages/core/test/invocation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,30 +32,50 @@ test('encode invocation', async () => {
assert.deepEqual(delegation.audience.did(), w3.did())
})

test('encode invocation with attached block', async () => {
test('encode invocation with attached block in capability nb', async () => {
const block = await getBlock({ test: 'inlineBlock' })
const add = invoke({
issuer: alice,
audience: w3,
capability: {
can: 'store/add',
with: alice.did(),
link: 'bafy...stuff',
nb: {
inlineBlock: block.cid.link()
}
},
proofs: [],
})

const block = await getBlock({ test: 'inlineBlock' })
add.attach(block)

const delegationBlocks = []
/** @type {import('@ucanto/interface').BlockStore<unknown>} */
const blockStore = new Map()
const view = await add.buildIPLDView()
for (const b of view.iterateIPLDBlocks()) {
delegationBlocks.push(b)
blockStore.set(`${b.cid}`, b)
}

assert.ok(delegationBlocks.find(b => b.cid.equals(block.cid)))
// blockstore has attached block
assert.ok(blockStore.get(`${block.cid}`))

const reassembledInvocation = Invocation.view({
root: view.root.cid.link(),
blocks: blockStore
})

/** @type {import('@ucanto/interface').BlockStore<unknown>} */
const reassembledBlockstore = new Map()

for (const b of reassembledInvocation.iterateIPLDBlocks()) {
reassembledBlockstore.set(`${b.cid}`, b)
}

// reassembledBlockstore has attached block
assert.ok(reassembledBlockstore.get(`${block.cid}`))
})


test('expired invocation', async () => {
const expiration = UCAN.now() - 5
const invocation = invoke({
Expand Down

0 comments on commit b2157fb

Please sign in to comment.