Skip to content

Commit

Permalink
feat: implement toJSON methods (#72)
Browse files Browse the repository at this point in the history
* chore: add toJSON to DID module

* feat: add to/from JSON to signatures

* chore: add toJSON method

* fix: test coverage
  • Loading branch information
Gozala authored Dec 14, 2022
1 parent 46bc09f commit c71603c
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 7 deletions.
16 changes: 15 additions & 1 deletion src/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { ByteView, MulticodecCode } from "./ucan.js"
import type {
ByteView,
MulticodecCode,
ToJSON,
ToString,
JSONUnknown,
UCAN,
} from "./ucan.js"

/**
* Multicodec code corresponding to the byteprefix of the [VarSig]. It is
Expand Down Expand Up @@ -51,6 +58,13 @@ export interface Signature<T = unknown, Alg extends SigAlg = SigAlg>
raw: Uint8Array
}

export type SignatureJSON<T extends Signature = Signature> = ToJSON<
T,
{
"/": { bytes: ToString<T> }
}
>

/**
* Just like {@link Verifier}, except definitely async.
*/
Expand Down
6 changes: 5 additions & 1 deletion src/did.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const decode = bytes => {
case RSA:
case P384:
case P521:
return /** @type {UCAN.PrincipalView<ID>} */ (
return /** @type {UCAN.PrincipalView<any>} */ (
new DIDKey(buffer, byteOffset, byteLength)
)
case DID_CORE:
Expand Down Expand Up @@ -118,6 +118,10 @@ class DID extends Uint8Array {
const bytes = new Uint8Array(this.buffer, this.byteOffset + METHOD_OFFSET)
return /** @type {ID} */ (`did:${UTF8.decode(bytes)}`)
}

toJSON() {
return this.did()
}
}

/**
Expand Down
23 changes: 22 additions & 1 deletion src/signature.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as UCAN from "./ucan.js"
import { varint } from "multiformats"
import { base64url } from "multiformats/bases/base64"
import { base64url, base64 } from "multiformats/bases/base64"
import * as UTF8 from "./utf8.js"

export const NON_STANDARD = 0xd000
Expand Down Expand Up @@ -105,6 +105,10 @@ class Signature extends Uint8Array {
Object.defineProperties(this, { raw: { value } })
return value
}

toJSON() {
return toJSON(this)
}
}

/**
Expand Down Expand Up @@ -255,3 +259,20 @@ export const parse = (signature, base) =>
/** @type {UCAN.Signature<T, A>} */ (
decode((base || base64url).decode(signature))
)

/**
* @template {UCAN.Signature} Signature
* @param {Signature} signature
* @returns {UCAN.SignatureJSON<Signature>}
*/
export const toJSON = signature => ({
"/": { bytes: base64.baseEncode(signature) },
})

/**
* @template {UCAN.Signature} Signature
* @param {UCAN.SignatureJSON<Signature>} json
* @returns {Signature}
*/
export const fromJSON = json =>
/** @type {Signature} */ (decode(base64.baseDecode(json["/"].bytes)))
44 changes: 44 additions & 0 deletions src/ucan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,22 @@ export interface Model<C extends Capabilities = Capabilities>
s: Crypto.Signature<string>
}

export type UCANJSON<T extends UCAN = UCAN> = ToJSON<
T,
{
v: Version
iss: DID
aud: DID
s: Crypto.SignatureJSON
att: ToJSON<T["att"]>
prf: { "/": ToString<Link> }[]
exp: UTCUnixTimestamp
fct?: ToJSON<T["fct"]>
nnc?: Nonce
nbf?: UTCUnixTimestamp
}
>

export interface FromJWT<C extends Capabilities = Capabilities>
extends Model<C> {
jwt: JWT<C>
Expand Down Expand Up @@ -310,6 +326,34 @@ export type ToString<In, Out extends string = string> = Encoded<In, Out>
*/
export type ToJSONString<In, Out extends string = string> = Encoded<In, Out>

export type JSONScalar = null | boolean | number | string
export type JSONObject = {
[key: string]: JSONUnknown | Phantom<unknown>
}
export type JSONUnknown = JSONScalar | JSONObject | JSONUnknown[]

/**
* JSON representation
*/
export type ToJSON<In, Out extends JSONUnknown = IntoJSON<In>> = Encoded<
In,
Out
>

export type IntoJSON<T> = T extends JSONScalar
? T
: T extends { toJSON(): infer U }
? IntoJSON<U>
: T extends Array<infer U>
? IntoJSON<U>[]
: T extends JSONObject
? IntoJSONObject<T>
: never

export type IntoJSONObject<T extends JSONObject> = {
[K in keyof T]: IntoJSON<T[K]>
}

/**
* [Multicodec code] usually used to tag [multiformat].
*
Expand Down
29 changes: 29 additions & 0 deletions src/view.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import * as UCAN from "./ucan.js"
import * as DID from "./did.js"
import { encode as encodeJSON } from "@ipld/dag-json"
import { decode as decodeUTF8 } from "./utf8.js"

/**
* @param {unknown} data
*/
const toJSON = data => JSON.parse(decodeUTF8(encodeJSON(data)))

/**
* @template {UCAN.Capabilities} C
Expand Down Expand Up @@ -108,4 +115,26 @@ export class View {
get prf() {
return this.model.prf
}

/**
* @returns {UCAN.ToJSON<UCAN.UCAN<C>, UCAN.UCANJSON<this>>}
*/
toJSON() {
const { v, iss, aud, s, att, prf, exp, fct, nnc, nbf } = this.model

return {
iss,
aud,
v,
s,
exp,
...toJSON({
att,
prf,
...(fct.length > 0 && { fct }),
}),
...(nnc != null && { nnc }),
...(nbf && { nbf }),
}
}
}
4 changes: 4 additions & 0 deletions test/did.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ describe("DID", () => {
it("DID.encode <-> DID.decode", () => {
assert.equal(DID.decode(DID.encode(alice)).did(), alice.did())
})

it("JSON.stringify", () => {
assert.deepEqual(JSON.stringify(DID.parse(alice.did())), `"${alice.did()}"`)
})
})
100 changes: 97 additions & 3 deletions test/lib.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { assert } from "chai"
import { alice, bob, mallory, JWT_UCAN, JWT_UCAN_SIG } from "./fixtures.js"
import * as TSUCAN from "./ts-ucan.cjs"
import * as CBOR from "../src/codec/cbor.js"
import * as Link from "multiformats/link"
import { encode as encodeCBOR } from "@ipld/dag-cbor"
import * as RAW from "multiformats/codecs/raw"
import * as UTF8 from "../src/utf8.js"
import * as DID from "../src/did.js"
import { identity } from "multiformats/hashes/identity"
import { base64url } from "multiformats/bases/base64"
import { base64, base64url } from "multiformats/bases/base64"
import {
decodeAuthority,
createRSAIssuer,
Expand Down Expand Up @@ -1065,8 +1066,6 @@ describe("encode <-> decode", () => {

const { iss, aud, s, nnc, ...body } = model

console.log(body)

const bytes = encodeCBOR({
...body,
iss: DID.encode(iss),
Expand Down Expand Up @@ -1420,3 +1419,98 @@ describe("verify", () => {
)
})
})

describe("JSON.stringify", () => {
it("basic", async () => {
const expiration = UCAN.now()

const ucan = await UCAN.issue({
issuer: alice,
audience: bob,
capabilities: [
{
can: "store/put",
with: alice.did(),
},
],
expiration,
})

assert.deepEqual(JSON.parse(JSON.stringify(ucan)), {
iss: alice.did(),
aud: bob.did(),
v: UCAN.VERSION,
s: { "/": { bytes: base64.baseEncode(ucan.signature) } },
exp: expiration,
att: [
{
can: "store/put",
with: alice.did(),
},
],
prf: [],
})
})

it("proof-chain", async () => {
const nbf = UCAN.now()
const expiration = nbf + 100
const link = Link.parse(
"bafybeifpzwjvqgnael34taig2dqbp7snfhodyeh4yrmoqvvp2sks23n2hu"
)
/** @type {Link.Link<any, number, number, 1>} */
const proof = Link.parse(
"bafybeiduj5jlxuw6gfkqjivtn5vdin3ydqgshxiqplgrtyqscwisfi4iky"
)
const none = Link.parse("bafkqaaa")
const bytes = UTF8.encode("hello world")

const ucan = await UCAN.issue({
issuer: alice,
audience: bob,
capabilities: [
{
can: "store/put",
with: alice.did(),
nb: { link, bytes },
},
],
notBefore: nbf,
nonce: "something",
expiration,
proofs: [proof],
facts: [{ none }, { bytes }],
})

assert.deepEqual(JSON.parse(JSON.stringify(ucan)), {
iss: alice.did(),
aud: bob.did(),
v: UCAN.VERSION,
s: { "/": { bytes: base64.baseEncode(ucan.signature) } },
exp: expiration,
nbf,
nnc: "something",
att: [
{
can: "store/put",
with: alice.did(),
nb: {
link: { "/": link.toString() },
bytes: { "/": { bytes: base64.baseEncode(bytes) } },
},
},
],
prf: [
{
"/": proof.toString(),
},
],
fct: [
{ none: { "/": none.toString() } },
{
bytes: { "/": { bytes: base64.baseEncode(bytes) } },
},
],
})
})
})
7 changes: 6 additions & 1 deletion test/signature.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { alice, bob, mallory, JWT_UCAN, JWT_UCAN_SIG } from "./fixtures.js"
import * as Signature from "../src/signature.js"
import { varint } from "multiformats"
import * as UTF8 from "../src/utf8.js"
import { base64url } from "multiformats/bases/base64"
import { base64url, base64 } from "multiformats/bases/base64"

describe("Signature", () => {
const dataset = [
Expand Down Expand Up @@ -69,13 +69,18 @@ describe("Signature", () => {
assert.deepEqual(sig.code, code)
assert.deepEqual(sig.algorithm, algorithm)
assert.deepEqual(sig.raw, raw)
assert.deepEqual(
JSON.stringify(sig),
JSON.stringify({ "/": { bytes: base64.baseEncode(sig) } })
)
})

it(`roundtrip ${title}`, () => {
const raw = UTF8.encode(`parse<->format ${title}`)
const sig = Signature.createNamed(algorithm, raw)

assert.deepEqual(sig, Signature.parse(Signature.format(sig)))
assert.deepEqual(sig, Signature.fromJSON(Signature.toJSON(sig)))
})
}

Expand Down

0 comments on commit c71603c

Please sign in to comment.