diff --git a/packages/core/src/delegation.js b/packages/core/src/delegation.js index 0564afe4..10ebdea3 100644 --- a/packages/core/src/delegation.js +++ b/packages/core/src/delegation.js @@ -1,6 +1,7 @@ import * as UCAN from '@ipld/dag-ucan' import * as API from '@ucanto/interface' import * as Link from './link.js' +import { base64 } from 'multiformats/bases/base64' /** * @deprecated @@ -130,8 +131,61 @@ export class Delegation { iterate() { return it(this) } + + /** + * @returns {API.DelegationJSON} + */ + + toJSON() { + return toJSON(this) + } +} + +/** + * @template {API.Delegation} T + * @param {T} delegation + * @returns {API.DelegationJSON} + */ +export const toJSON = ({ + cid, + issuer, + audience, + version, + signature, + proofs, + capabilities, + expiration, + notBefore, + nonce, + facts, +}) => { + return { + ...Link.toJSON(cid), + version, + issuer: principalToJSON(issuer), + audience: principalToJSON(audience), + capabilities, + expiration: /** @type {API.UCAN.UTCUnixTimestamp|null} */ (expiration), + notBefore, + nonce, + facts, + proofs: proofs.map(proof => + Link.toJSON(isDelegation(proof) ? proof.cid : proof) + ), + signature: { + '/': { bytes: base64.baseEncode(signature) }, + }, + } } +/** + * @template {API.Principal} T + * @param {T} principal + * @returns {API.PrincipalJSON} + */ + +const principalToJSON = principal => principal.did() + /** * @param {API.Delegation} delegation * @returns {IterableIterator} diff --git a/packages/core/src/link.js b/packages/core/src/link.js index b49b4574..30f41d26 100644 --- a/packages/core/src/link.js +++ b/packages/core/src/link.js @@ -1 +1,10 @@ export * from 'multiformats/link' + +/** + * @template {import('multiformats').UnknownLink} Link + * @param {Link} link + */ +export const toJSON = link => + /** @type {import('@ucanto/interface').LinkJSON} */ ({ + '/': link.toString(), + }) diff --git a/packages/core/test/delegation.spec.js b/packages/core/test/delegation.spec.js new file mode 100644 index 00000000..393318c7 --- /dev/null +++ b/packages/core/test/delegation.spec.js @@ -0,0 +1,125 @@ +import { delegate, UCAN } from '../src/lib.js' +import { alice, bob, mallory, service as w3 } from './fixtures.js' +import { assert, test } from './test.js' +import { base64 } from 'multiformats/bases/base64' + +test('toJSON delegation', async () => { + const ucan = await delegate({ + issuer: alice, + audience: w3, + capabilities: [ + { + with: alice.did(), + can: 'test/echo', + nb: { + message: 'data:1', + }, + }, + ], + expiration: Infinity, + }) + + assert.equal( + JSON.stringify(ucan, null, 2), + JSON.stringify( + { + '/': ucan.cid.toString(), + version: ucan.version, + issuer: alice.did(), + audience: w3.did(), + capabilities: [ + { + nb: { + message: 'data:1', + }, + can: 'test/echo', + with: alice.did(), + }, + ], + expiration: null, + facts: [], + proofs: [], + signature: { + '/': { bytes: base64.baseEncode(ucan.signature) }, + }, + }, + null, + 2 + ) + ) +}) + +test('toJSON delegation chain', async () => { + const proof = await delegate({ + issuer: bob, + audience: alice, + capabilities: [ + { + with: bob.did(), + can: 'test/echo', + }, + ], + }) + + const proof2 = await delegate({ + issuer: mallory, + audience: alice, + capabilities: [ + { + with: mallory.did(), + can: 'test/echo', + }, + ], + }) + + const ucan = await delegate({ + issuer: alice, + audience: w3, + capabilities: [ + { + with: bob.did(), + can: 'test/echo', + nb: { + message: 'data:hi', + }, + }, + ], + proofs: [proof, proof2.cid], + }) + + assert.equal( + JSON.stringify(ucan, null, 2), + JSON.stringify( + { + '/': ucan.cid.toString(), + version: ucan.version, + issuer: alice.did(), + audience: w3.did(), + capabilities: [ + { + nb: { + message: 'data:hi', + }, + can: 'test/echo', + with: bob.did(), + }, + ], + expiration: ucan.expiration, + facts: [], + proofs: [ + { + '/': proof.cid.toString(), + }, + { + '/': proof2.cid.toString(), + }, + ], + signature: { + '/': { bytes: base64.baseEncode(ucan.signature) }, + }, + }, + null, + 2 + ) + ) +}) diff --git a/packages/interface/src/lib.ts b/packages/interface/src/lib.ts index fcb0df55..65066881 100644 --- a/packages/interface/src/lib.ts +++ b/packages/interface/src/lib.ts @@ -18,7 +18,7 @@ import { MulticodecCode, SigAlg, } from '@ipld/dag-ucan' -import { Link, Block as IPLDBlock } from 'multiformats' +import { Link, Block as IPLDBlock, ToString, UnknownLink } from 'multiformats' import * as UCAN from '@ipld/dag-ucan' import { CanIssue, @@ -143,6 +143,9 @@ export interface Delegation { readonly bytes: ByteView> readonly data: UCAN.View + version: UCAN.Version + signature: UCAN.Signature + asCID: UCANLink export(): IterableIterator @@ -158,8 +161,34 @@ export interface Delegation { facts: Fact[] proofs: Proof[] iterate(): IterableIterator + + toJSON(): DelegationJSON +} + +export interface DelegationJSON + extends LinkJSON { + version: T['version'] + issuer: PrincipalJSON + audience: PrincipalJSON + capabilities: T['capabilities'] + expiration: UCAN.UTCUnixTimestamp | null + notBefore?: UCAN.UTCUnixTimestamp + nonce?: UCAN.Nonce + facts: Fact[] + proofs: LinkJSON[] + signature: { '/': { bytes: ToString } } +} + +export interface LinkJSON { + '/': ToString } +export interface BytesJSON { + '/': { bytes: ToString } +} + +export type PrincipalJSON = DID & Phantom + /** * An Invocation represents a UCAN that can be presented to a service provider to * invoke or "exercise" a {@link Capability}. You can think of invocations as a diff --git a/packages/validator/test/delegate.spec.js b/packages/validator/test/delegate.spec.js index 16ff551e..31dadf89 100644 --- a/packages/validator/test/delegate.spec.js +++ b/packages/validator/test/delegate.spec.js @@ -1,9 +1,7 @@ -import { capability, DID, URI, Link, unknown, Schema } from '../src/lib.js' -import { invoke, parseLink, delegate, UCAN } from '@ucanto/core' +import { capability, DID, URI, Link, Schema } from '../src/lib.js' +import { parseLink, delegate, UCAN } from '@ucanto/core' import * as API from '@ucanto/interface' import { Failure } from '../src/error.js' -import { the } from '../src/util.js' -import { CID } from 'multiformats' import { test, assert } from './test.js' import { alice, bob, mallory, service as w3 } from './fixtures.js'