-
Notifications
You must be signed in to change notification settings - Fork 6
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: archive/extract api for delegations #287
Changes from 5 commits
f732e52
dc5f834
30dd2d6
7e746c9
cad29ec
d2f4b3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,10 @@ 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 | ||
|
@@ -212,6 +216,13 @@ export class Delegation { | |
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) | ||
} | ||
|
@@ -304,6 +315,70 @@ export class Delegation { | |
} | ||
} | ||
|
||
/** | ||
* 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({ | ||
'[email protected]': /** @type {Schema.Schema<API.UCANLink>} */ ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this version number be a constant somewhere? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is the |
||
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>} | ||
|
@@ -345,7 +420,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 = [] | ||
|
@@ -466,16 +541,19 @@ export const create = ({ root, blocks }) => new Delegation(root, blocks) | |
|
||
/** | ||
* @template {API.Capabilities} C | ||
* @template [T=undefined] | ||
* @template [E=never] | ||
* @param {object} dag | ||
* @param {API.UCANLink<C>} dag.root | ||
* @param {DAG.BlockStore} dag.blocks | ||
* @param {T} [fallback] | ||
* @returns {API.Delegation<C>|T} | ||
* @param {E} [fallback] | ||
* @returns {API.Delegation<C>|E} | ||
*/ | ||
export const view = ({ root, blocks }, fallback) => { | ||
const block = DAG.get(root, blocks, null) | ||
return block ? create({ root: block, blocks }) : /** @type {T} */ (fallback) | ||
if (block == null) { | ||
return fallback !== undefined ? fallback : DAG.notFound(root) | ||
} | ||
return create({ root: block, blocks }) | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import { assert, test } from './test.js' | ||
import { Delegation, UCAN, delegate, parseLink } from '../src/lib.js' | ||
import { CAR, CBOR, delegate, Delegation, parseLink, UCAN } from '../src/lib.js' | ||
import { alice, bob, mallory, service as w3 } from './fixtures.js' | ||
import { base64 } from 'multiformats/bases/base64' | ||
import { getBlock } from './utils.js' | ||
|
@@ -290,6 +290,143 @@ test('.buildIPLDView() return same value', async () => { | |
assert.equal(ucan.buildIPLDView(), ucan) | ||
}) | ||
|
||
test('delegation archive', async () => { | ||
const ucan = await delegate({ | ||
issuer: alice, | ||
audience: w3, | ||
capabilities: [ | ||
{ | ||
with: alice.did(), | ||
can: 'test/echo', | ||
nb: { | ||
message: 'data:1', | ||
}, | ||
}, | ||
], | ||
}) | ||
|
||
const archive = await ucan.archive() | ||
if (archive.error) { | ||
return assert.fail(archive.error.message) | ||
} | ||
|
||
const extract = await Delegation.extract(archive.ok) | ||
if (extract.error) { | ||
return assert.fail(extract.error.message) | ||
} | ||
|
||
assert.deepEqual(extract.ok, ucan) | ||
}) | ||
|
||
test('fail to extract wrong version', async () => { | ||
const ucan = await delegate({ | ||
issuer: alice, | ||
audience: w3, | ||
capabilities: [ | ||
{ | ||
with: alice.did(), | ||
can: 'test/echo', | ||
nb: { | ||
message: 'data:1', | ||
}, | ||
}, | ||
], | ||
}) | ||
|
||
const root = await CBOR.write({ | ||
['[email protected]']: ucan.root.cid, | ||
}) | ||
ucan.blocks.set(`${root.cid}`, root) | ||
|
||
const bytes = CAR.encode({ | ||
roots: [root], | ||
blocks: ucan.blocks, | ||
}) | ||
|
||
const result = await Delegation.extract(bytes) | ||
if (result.ok) { | ||
return assert.fail('should not be ok') | ||
} | ||
|
||
assert.match( | ||
result.error.message, | ||
/[email protected] instead got object with key [email protected]/ | ||
) | ||
|
||
const badbytes = await Delegation.extract(new Uint8Array([0, 1, 1])) | ||
assert.match(badbytes.error?.message || '', /invalid car/i) | ||
|
||
const noroot = CAR.encode({ blocks: ucan.blocks }) | ||
const noRootResult = await Delegation.extract(noroot) | ||
assert.match(noRootResult.error?.message || '', /does not contain a root/i) | ||
|
||
const nonvariantroot = await Delegation.extract( | ||
CAR.encode({ | ||
roots: [ucan.root], | ||
blocks: ucan.blocks, | ||
}) | ||
) | ||
|
||
assert.match(nonvariantroot.error?.message || '', /object with a single key/i) | ||
|
||
const okroot = await CBOR.write({ | ||
['[email protected]']: ucan.root.cid, | ||
}) | ||
|
||
const missingblocks = await Delegation.extract( | ||
CAR.encode({ roots: [okroot] }) | ||
) | ||
|
||
assert.match(missingblocks.error?.message || '', /Block .* not found/i) | ||
}) | ||
|
||
test('fail archive with bad input', async () => { | ||
const result = await Delegation.archive( | ||
// @ts-expect-error | ||
{} | ||
) | ||
assert.equal(result.error instanceof Error, true) | ||
}) | ||
|
||
test('archive delegation chain', async () => { | ||
const proof = await Delegation.delegate({ | ||
issuer: alice, | ||
audience: bob, | ||
capabilities: [ | ||
{ | ||
can: 'store/add', | ||
with: alice.did(), | ||
}, | ||
], | ||
}) | ||
|
||
const ucan = await Delegation.delegate({ | ||
issuer: bob, | ||
audience: mallory, | ||
capabilities: [ | ||
{ | ||
can: 'store/add', | ||
with: alice.did(), | ||
root: link, | ||
}, | ||
], | ||
proofs: [proof], | ||
}) | ||
|
||
const archive = await ucan.archive() | ||
if (archive.error) { | ||
return assert.fail(archive.error.message) | ||
} | ||
|
||
const extract = await Delegation.extract(archive.ok) | ||
if (extract.error) { | ||
return assert.fail(extract.error.message) | ||
} | ||
|
||
assert.deepEqual(extract.ok, ucan) | ||
assert.deepEqual(extract.ok.proofs[0], proof) | ||
}) | ||
|
||
test('delegation.attach', async () => { | ||
const ucan = await Delegation.delegate({ | ||
issuer: alice, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like in
CAR.encode
the root blocks are included (i.e. I think you might get the root block twice in the encoded CAR).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No you won't and I plan on changing CAR codec so that roots are no longer blocks but rather CIDs. Current approach of roots been blocks are kind of falling apart if you get a car from someone that has a root that isn't inside the car