From 8fb60da96c14357c90ae49fb0a4ab3a48d97bb8b Mon Sep 17 00:00:00 2001
From: Irakli Gozalishvili <contact@gozala.io>
Date: Wed, 14 Dec 2022 11:52:20 -0800
Subject: [PATCH 1/2] fix: toJSON behavior on the ucan.data

---
 packages/core/package.json            |   2 +-
 packages/core/test/delegation.spec.js | 134 ++++++++++++++++++++++++++
 packages/interface/package.json       |   2 +-
 packages/interface/src/lib.ts         |   4 +-
 packages/principal/package.json       |   2 +-
 pnpm-lock.yaml                        |  16 +--
 6 files changed, 148 insertions(+), 12 deletions(-)
 create mode 100644 packages/core/test/delegation.spec.js

diff --git a/packages/core/package.json b/packages/core/package.json
index 552b5d32..b596906a 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -31,7 +31,7 @@
   "dependencies": {
     "@ipld/car": "^5.0.0",
     "@ipld/dag-cbor": "^8.0.0",
-    "@ipld/dag-ucan": "^3.0.1",
+    "@ipld/dag-ucan": "^3.1.1",
     "@ucanto/interface": "^4.0.3",
     "multiformats": "^10.0.2"
   },
diff --git a/packages/core/test/delegation.spec.js b/packages/core/test/delegation.spec.js
new file mode 100644
index 00000000..5fa38308
--- /dev/null
+++ b/packages/core/test/delegation.spec.js
@@ -0,0 +1,134 @@
+import { assert, test } from './test.js'
+import { Delegation, UCAN, isDelegation, parseLink } from '../src/lib.js'
+import { alice, bob, mallory, service } from './fixtures.js'
+import { base64 } from 'multiformats/bases/base64'
+const utf8 = new TextEncoder()
+
+const link = parseLink(
+  'bafybeid4cy7pj33wuead6zioxdtx3zwalhr6hd572tgqubgmy2ahrmi6vu'
+)
+/**
+ * @param {unknown} value
+ */
+const toJSON = value => JSON.parse(JSON.stringify(value))
+
+test('delegation.data.toJSON', async () => {
+  const ucan = await Delegation.delegate({
+    issuer: alice,
+    audience: bob,
+    capabilities: [
+      {
+        can: 'store/add',
+        with: alice.did(),
+      },
+    ],
+  })
+
+  assert.deepEqual(toJSON(ucan.data), {
+    v: UCAN.VERSION,
+    iss: alice.did(),
+    aud: bob.did(),
+    att: [
+      {
+        can: 'store/add',
+        with: alice.did(),
+      },
+    ],
+    exp: ucan.expiration,
+    prf: [],
+    s: { '/': { bytes: base64.baseEncode(ucan.signature) } },
+  })
+})
+
+test('delegation.data.toJSON with proofs', 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],
+  })
+
+  assert.deepEqual(toJSON(ucan.data), {
+    v: UCAN.VERSION,
+    iss: bob.did(),
+    aud: mallory.did(),
+    att: [
+      {
+        can: 'store/add',
+        with: alice.did(),
+        root: { '/': link.toString() },
+      },
+    ],
+    exp: ucan.expiration,
+    prf: [
+      {
+        '/': proof.cid.toString(),
+      },
+    ],
+    s: { '/': { bytes: base64.baseEncode(ucan.signature) } },
+  })
+})
+
+test('delegation.data.toJSON with bytes', async () => {
+  const content = utf8.encode('hello world')
+  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: content,
+      },
+    ],
+    proofs: [proof],
+  })
+
+  assert.deepEqual(toJSON(ucan.data), {
+    v: UCAN.VERSION,
+    iss: bob.did(),
+    aud: mallory.did(),
+    att: [
+      {
+        can: 'store/add',
+        with: alice.did(),
+        root: { '/': { bytes: base64.baseEncode(content) } },
+      },
+    ],
+    exp: ucan.expiration,
+    prf: [
+      {
+        '/': proof.cid.toString(),
+      },
+    ],
+    s: { '/': { bytes: base64.baseEncode(ucan.signature) } },
+  })
+})
diff --git a/packages/interface/package.json b/packages/interface/package.json
index 650040ec..4c9f6253 100644
--- a/packages/interface/package.json
+++ b/packages/interface/package.json
@@ -23,7 +23,7 @@
     "build": "tsc --build"
   },
   "dependencies": {
-    "@ipld/dag-ucan": "^3.0.1",
+    "@ipld/dag-ucan": "^3.1.1",
     "multiformats": "^10.0.2"
   },
   "devDependencies": {
diff --git a/packages/interface/src/lib.ts b/packages/interface/src/lib.ts
index 0997d4cb..ad4ab2e7 100644
--- a/packages/interface/src/lib.ts
+++ b/packages/interface/src/lib.ts
@@ -151,7 +151,7 @@ export interface Delegation<C extends Capabilities = Capabilities> {
   issuer: UCAN.Principal
   audience: UCAN.Principal
   capabilities: C
-  expiration?: UCAN.UTCUnixTimestamp
+  expiration: UCAN.UTCUnixTimestamp
   notBefore?: UCAN.UTCUnixTimestamp
 
   nonce?: UCAN.Nonce
@@ -159,6 +159,8 @@ export interface Delegation<C extends Capabilities = Capabilities> {
   facts: Fact[]
   proofs: Proof[]
   iterate(): IterableIterator<Delegation>
+
+  signature: Signature
 }
 
 /**
diff --git a/packages/principal/package.json b/packages/principal/package.json
index 79f96b59..a81d4769 100644
--- a/packages/principal/package.json
+++ b/packages/principal/package.json
@@ -27,7 +27,7 @@
     "build": "tsc --build"
   },
   "dependencies": {
-    "@ipld/dag-ucan": "^3.0.1",
+    "@ipld/dag-ucan": "^3.1.1",
     "@noble/ed25519": "^1.7.1",
     "@ucanto/interface": "^4.0.3",
     "multiformats": "^10.0.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 57d50563..8abaaed8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -54,7 +54,7 @@ importers:
     specifiers:
       '@ipld/car': ^5.0.0
       '@ipld/dag-cbor': ^8.0.0
-      '@ipld/dag-ucan': ^3.0.1
+      '@ipld/dag-ucan': ^3.1.1
       '@types/chai': ^4.3.3
       '@types/mocha': ^9.1.0
       '@ucanto/interface': ^4.0.3
@@ -69,7 +69,7 @@ importers:
     dependencies:
       '@ipld/car': 5.0.0
       '@ipld/dag-cbor': 8.0.0
-      '@ipld/dag-ucan': 3.0.1
+      '@ipld/dag-ucan': 3.1.1
       '@ucanto/interface': link:../interface
       multiformats: 10.0.2
     devDependencies:
@@ -85,18 +85,18 @@ importers:
 
   packages/interface:
     specifiers:
-      '@ipld/dag-ucan': ^3.0.1
+      '@ipld/dag-ucan': ^3.1.1
       multiformats: ^10.0.2
       typescript: ^4.8.4
     dependencies:
-      '@ipld/dag-ucan': 3.0.1
+      '@ipld/dag-ucan': 3.1.1
       multiformats: 10.0.2
     devDependencies:
       typescript: 4.8.4
 
   packages/principal:
     specifiers:
-      '@ipld/dag-ucan': ^3.0.1
+      '@ipld/dag-ucan': ^3.1.1
       '@noble/ed25519': ^1.7.1
       '@types/chai': ^4.3.3
       '@types/mocha': ^9.1.0
@@ -110,7 +110,7 @@ importers:
       playwright-test: ^8.1.1
       typescript: ^4.8.4
     dependencies:
-      '@ipld/dag-ucan': 3.0.1
+      '@ipld/dag-ucan': 3.1.1
       '@noble/ed25519': 1.7.1
       '@ucanto/interface': link:../interface
       multiformats: 10.0.2
@@ -474,8 +474,8 @@ packages:
       multiformats: 10.0.2
     dev: false
 
-  /@ipld/dag-ucan/3.0.1:
-    resolution: {integrity: sha512-71YwJeRHxwX3diPXfwiuzhJTjmJSqi8XW/x5Xglp82UqpM5xwtNojB07VhmDXTZXhKi42bZHyQIOLaca/t9IHw==}
+  /@ipld/dag-ucan/3.1.1:
+    resolution: {integrity: sha512-iTIME/QZNGMSXvM8aPDMR9zp6d5orCEAL0BqgAobm9jDt0RV0CLITLWgvxb5MSc0vjRw7GR51Bva1RdZw2qs9w==}
     dependencies:
       '@ipld/dag-cbor': 8.0.0
       '@ipld/dag-json': 9.0.1

From ead73a9c0bd205d02f0c2f19a4e356a92b788f30 Mon Sep 17 00:00:00 2001
From: Irakli Gozalishvili <contact@gozala.io>
Date: Wed, 14 Dec 2022 13:34:25 -0800
Subject: [PATCH 2/2] feat: delegation to-json

---
 package.json                          |   2 +-
 packages/core/src/delegation.js       |  13 +++
 packages/core/test/delegation.spec.js | 121 +++++++++++++++++++++++++-
 packages/interface/src/lib.ts         |  42 ++++++++-
 pnpm-lock.yaml                        |  28 +++---
 5 files changed, 185 insertions(+), 21 deletions(-)

diff --git a/package.json b/package.json
index 6c72138a..9e4ffdc8 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,7 @@
   "devDependencies": {
     "mocha": "^10.1.0",
     "prettier": "2.7.1",
-    "typescript": "4.8.3"
+    "typescript": "^4.8.4"
   },
   "prettier": {
     "trailingComma": "es5",
diff --git a/packages/core/src/delegation.js b/packages/core/src/delegation.js
index 0564afe4..07398831 100644
--- a/packages/core/src/delegation.js
+++ b/packages/core/src/delegation.js
@@ -130,6 +130,19 @@ export class Delegation {
   iterate() {
     return it(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() }
+      ),
+    })
+  }
 }
 
 /**
diff --git a/packages/core/test/delegation.spec.js b/packages/core/test/delegation.spec.js
index 5fa38308..da13091e 100644
--- a/packages/core/test/delegation.spec.js
+++ b/packages/core/test/delegation.spec.js
@@ -1,6 +1,6 @@
 import { assert, test } from './test.js'
-import { Delegation, UCAN, isDelegation, parseLink } from '../src/lib.js'
-import { alice, bob, mallory, service } from './fixtures.js'
+import { Delegation, UCAN, delegate, parseLink } from '../src/lib.js'
+import { alice, bob, mallory, service as w3 } from './fixtures.js'
 import { base64 } from 'multiformats/bases/base64'
 const utf8 = new TextEncoder()
 
@@ -132,3 +132,120 @@ test('delegation.data.toJSON with bytes', async () => {
     s: { '/': { bytes: base64.baseEncode(ucan.signature) } },
   })
 })
+
+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.deepEqual(toJSON(ucan), {
+    '/': ucan.cid.toString(),
+    v: ucan.version,
+    iss: alice.did(),
+    aud: w3.did(),
+    att: [
+      {
+        nb: {
+          message: 'data:1',
+        },
+        can: 'test/echo',
+        with: alice.did(),
+      },
+    ],
+    exp: null,
+    prf: [],
+    s: {
+      '/': { bytes: base64.baseEncode(ucan.signature) },
+    },
+  })
+})
+
+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.deepEqual(toJSON(ucan), {
+    '/': ucan.cid.toString(),
+    v: ucan.version,
+    iss: alice.did(),
+    aud: w3.did(),
+    att: [
+      {
+        with: bob.did(),
+        can: 'test/echo',
+        nb: {
+          message: 'data:hi',
+        },
+      },
+    ],
+    exp: ucan.expiration,
+    prf: [
+      {
+        '/': proof.cid.toString(),
+        iss: bob.did(),
+        aud: alice.did(),
+        att: [
+          {
+            with: bob.did(),
+            can: 'test/echo',
+          },
+        ],
+        exp: proof.expiration,
+        v: proof.version,
+        s: { '/': { bytes: base64.baseEncode(proof.signature) } },
+        prf: [],
+      },
+      {
+        '/': proof2.cid.toString(),
+      },
+    ],
+    s: {
+      '/': { bytes: base64.baseEncode(ucan.signature) },
+    },
+  })
+})
diff --git a/packages/interface/src/lib.ts b/packages/interface/src/lib.ts
index ad4ab2e7..f97d2b07 100644
--- a/packages/interface/src/lib.ts
+++ b/packages/interface/src/lib.ts
@@ -17,8 +17,13 @@ import {
   Principal,
   MulticodecCode,
   SigAlg,
+  ToJSON,
+  SignatureJSON,
+  JSONUnknown,
+  IntoJSON,
+  JSONObject,
 } from '@ipld/dag-ucan'
-import { Link, Block as IPLDBlock } from 'multiformats'
+import { Link, UnknownLink, Block as IPLDBlock, ToString } from 'multiformats'
 import * as UCAN from '@ipld/dag-ucan'
 import {
   CanIssue,
@@ -59,6 +64,10 @@ export type {
   MultibaseEncoder,
   MulticodecCode,
   Principal,
+  ToJSON,
+  ToString,
+  UnknownLink,
+  JSONUnknown,
 }
 export * as UCAN from '@ipld/dag-ucan'
 
@@ -161,8 +170,39 @@ export interface Delegation<C extends Capabilities = Capabilities> {
   iterate(): IterableIterator<Delegation>
 
   signature: Signature
+  version: UCAN.Version
+
+  toJSON(): DelegationJSON<this>
 }
 
+export type DelegationJSON<T extends Delegation = Delegation> = ToJSON<
+  T,
+  {
+    '/': ToString<T['cid']>
+    v: T['version']
+    iss: DID
+    aud: DID
+    att: ToJSON<
+      T['capabilities'],
+      T['capabilities'] &
+        UCAN.Tuple<{ with: UCAN.Resource; can: UCAN.Ability; nb?: JSONObject }>
+    >
+    exp: T['expiration']
+    nbf?: T['notBefore'] & {}
+    nnc?: T['nonce'] & {}
+    fct: ToJSON<T['facts']>
+    prf: ProofJSON[] & JSONUnknown[]
+    s: SignatureJSON<T['signature']>
+  }
+>
+
+export type ProofJSON = DelegationJSON | LinkJSON<UCANLink>
+
+export type LinkJSON<T extends UnknownLink = UnknownLink> = ToJSON<
+  T,
+  { '/': ToString<T> }
+>
+
 /**
  * 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/pnpm-lock.yaml b/pnpm-lock.yaml
index 8abaaed8..28ad5ae3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -9,11 +9,11 @@ importers:
     specifiers:
       mocha: ^10.1.0
       prettier: 2.7.1
-      typescript: 4.8.3
+      typescript: ^4.8.4
     devDependencies:
       mocha: 10.1.0
       prettier: 2.7.1
-      typescript: 4.8.3
+      typescript: 4.9.4
 
   packages/client:
     specifiers:
@@ -48,7 +48,7 @@ importers:
       mocha: 10.1.0
       nyc: 15.1.0
       playwright-test: 8.1.1
-      typescript: 4.8.4
+      typescript: 4.9.4
 
   packages/core:
     specifiers:
@@ -81,7 +81,7 @@ importers:
       mocha: 10.1.0
       nyc: 15.1.0
       playwright-test: 8.1.1
-      typescript: 4.8.4
+      typescript: 4.9.4
 
   packages/interface:
     specifiers:
@@ -92,7 +92,7 @@ importers:
       '@ipld/dag-ucan': 3.1.1
       multiformats: 10.0.2
     devDependencies:
-      typescript: 4.8.4
+      typescript: 4.9.4
 
   packages/principal:
     specifiers:
@@ -123,7 +123,7 @@ importers:
       mocha: 10.1.0
       nyc: 15.1.0
       playwright-test: 8.1.1
-      typescript: 4.8.4
+      typescript: 4.9.4
 
   packages/server:
     specifiers:
@@ -166,7 +166,7 @@ importers:
       multiformats: 10.0.2
       nyc: 15.1.0
       playwright-test: 8.1.1
-      typescript: 4.8.4
+      typescript: 4.9.4
 
   packages/transport:
     specifiers:
@@ -201,7 +201,7 @@ importers:
       mocha: 10.1.0
       nyc: 15.1.0
       playwright-test: 8.1.1
-      typescript: 4.8.4
+      typescript: 4.9.4
 
   packages/validator:
     specifiers:
@@ -240,7 +240,7 @@ importers:
       mocha: 10.1.0
       nyc: 15.1.0
       playwright-test: 8.1.1
-      typescript: 4.8.4
+      typescript: 4.9.4
 
 packages:
 
@@ -2969,14 +2969,8 @@ packages:
       is-typedarray: 1.0.0
     dev: true
 
-  /typescript/4.8.3:
-    resolution: {integrity: sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==}
-    engines: {node: '>=4.2.0'}
-    hasBin: true
-    dev: true
-
-  /typescript/4.8.4:
-    resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==}
+  /typescript/4.9.4:
+    resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==}
     engines: {node: '>=4.2.0'}
     hasBin: true
     dev: true