diff --git a/packages/core/src/compatibility.ts b/packages/core/src/compatibility.ts index 688ac13..732d5d7 100644 --- a/packages/core/src/compatibility.ts +++ b/packages/core/src/compatibility.ts @@ -5,8 +5,8 @@ import * as semver from "./semver.js" import * as util from "./util.js" import { SUPERUSER } from "./capability/super-user.js" -import { UcanParts, isUcanHeader, isUcanPayload } from "./types.js" -import { my } from "./capability/index.js" +import { isUcanHeader, isUcanPayload, Fact } from "./types.js" +import { Capability, EncodedCapability, isCapability, isEncodedCapability, my } from "./capability/index.js" const VERSION_0_3 = { major: 0, minor: 3, patch: 0 } @@ -44,13 +44,61 @@ function isUcanPayload_0_3_0(obj: unknown): obj is UcanPayload_0_3_0 { && util.hasProp(obj, "ptc") && typeof obj.ptc === "string" && (!util.hasProp(obj, "prf") || typeof obj.prf === "string") } +type UcanHeader_0_8_0 = { + alg: string + typ: string + uav: string +} +type UcanPayload_0_8_0 = { + iss: string + aud: string + exp: number + nbf?: number + nnc?: string + att: Array + fct?: Array + prf: Array +} -export function handleCompatibility(header: unknown, payload: unknown): UcanParts { +function isUcanHeader_0_8_0(obj: unknown): obj is UcanHeader_0_8_0 { + return isUcanHeader_0_3_0(obj) +} + +function isUcanPayload_0_8_0(obj: unknown): obj is UcanPayload_0_8_0 { + return util.isRecord(obj) + && util.hasProp(obj, "iss") && typeof obj.iss === "string" + && util.hasProp(obj, "aud") && typeof obj.aud === "string" + && util.hasProp(obj, "exp") && typeof obj.exp === "number" + && (!util.hasProp(obj, "nbf") || typeof obj.nbf === "number") + && (!util.hasProp(obj, "nnc") || typeof obj.nnc === "string") + && util.hasProp(obj, "att") && Array.isArray(obj.att) && obj.att.every(a => isCapability(a) || isEncodedCapability(a)) + && (!util.hasProp(obj, "fct") || Array.isArray(obj.fct) && obj.fct.every(util.isRecord)) + && util.hasProp(obj, "prf") && Array.isArray(obj.prf) && obj.prf.every(str => typeof str === "string") +} + + +export function handleCompatibility(header: unknown, payload: unknown) { const fail = (place: string, reason: string) => new Error(`Can't parse UCAN ${place}: ${reason}`) if (!util.isRecord(header)) throw fail("header", "Invalid format: Expected a record") + if (util.hasProp(payload, "ucv")) { + // Version >= 0.10 + if (typeof payload.ucv !== "string") { + throw fail("payload", "Invalid format: field 'ucv' is not a string") + } + payload.ucv = semver.parse(payload.ucv) + if (payload.ucv == null) { + throw fail("payload", "Invalid format: 'ucv' string cannot be parsed into a semantic version") + } + if (!isUcanHeader(header)) throw fail("header", "Invalid format") + if (!isUcanPayload(payload)) throw fail("payload", "Invalid format") + return { header, payload } + } + + // Handle < 0.10 (actually, only <= 0.8 are supported, afaik) // + // parse either the "ucv" or "uav" as a version in the header // we translate 'uav: 1.0.0' into 'ucv: 0.3.0' let version: "0.9.1" | "0.3.0" = "0.9.1" @@ -69,13 +117,22 @@ export function handleCompatibility(header: unknown, payload: unknown): UcanPart if (typeof header.ucv !== "string") { throw fail("header", "Invalid format: Missing 'ucv' key or 'ucv' is not a string") } - header.ucv = semver.parse(header.ucv) - if (header.ucv == null) { + const ucv = semver.parse(header.ucv) + if (ucv == null) { throw fail("header", "Invalid format: 'ucv' string cannot be parsed into a semantic version") } - if (!isUcanHeader(header)) throw fail("header", "Invalid format") - if (!isUcanPayload(payload)) throw fail("payload", "Invalid format") - return { header, payload } + if (!isUcanHeader_0_8_0(header)) throw fail("header", "Invalid format") + if (!isUcanPayload_0_8_0(payload)) throw fail("payload", "Invalid format") + return { + header: { + alg: header.alg, + typ: header.typ, + }, + payload: { + ...payload, + ucv: ucv, + } + } } // we know it's version 0.3.0 @@ -86,9 +143,9 @@ export function handleCompatibility(header: unknown, payload: unknown): UcanPart header: { alg: header.alg, typ: header.typ, - ucv: VERSION_0_3, }, payload: { + ucv: VERSION_0_3, iss: payload.iss, aud: payload.aud, nbf: payload.nbf, diff --git a/packages/core/src/token.ts b/packages/core/src/token.ts index b5420ca..617142a 100644 --- a/packages/core/src/token.ts +++ b/packages/core/src/token.ts @@ -15,7 +15,7 @@ import { handleCompatibility } from "./compatibility.js" const TYPE = "JWT" -const VERSION = { major: 0, minor: 8, patch: 1 } +const VERSION = { major: 0, minor: 10, patch: 0 } @@ -29,10 +29,10 @@ const VERSION = { major: 0, minor: 8, patch: 1 } * * `alg`, Algorithm, the type of signature. * `typ`, Type, the type of this data structure, JWT. - * `ucv`, UCAN version. * * ### Payload * + * `ucv`, UCAN Semantic Version. * `att`, Attenuation, a list of resources and capabilities that the ucan grants. * `aud`, Audience, the ID of who it's intended for. * `exp`, Expiry, unix timestamp of when the jwt is no longer valid. @@ -110,6 +110,7 @@ export function buildPayload(params: { // 📦 return { + ucv: VERSION, aud: audience, att: capabilities, exp, @@ -132,7 +133,6 @@ export const sign = (plugins: Plugins) => const header: UcanHeader = { alg: jwtAlg, typ: TYPE, - ucv: VERSION, } // Issuer key type must match UCAN algorithm @@ -195,12 +195,8 @@ export function encode(ucan: Ucan): string { * @returns The header of a UCAN encoded as url-safe base64 JSON */ export function encodeHeader(header: UcanHeader): string { - const headerFormatted = { - ...header, - ucv: semver.format(header.ucv) - } return uint8arrays.toString( - uint8arrays.fromString(JSON.stringify(headerFormatted), "utf8"), + uint8arrays.fromString(JSON.stringify(header), "utf8"), "base64url" ) } @@ -217,6 +213,7 @@ export function encodeHeader(header: UcanHeader): string { export function encodePayload(payload: UcanPayload): string { const payloadWithEncodedCaps = { ...payload, + ucv: semver.format(payload.ucv), att: payload.att.map(capability.encode) } @@ -282,8 +279,8 @@ export function parse(encodedUcan: string): UcanParts { // Ensure proper types/structure const parsedAttenuations = payload.att.reduce((acc: Capability[], cap: unknown): Capability[] => { return isEncodedCapability(cap) - ? [ ...acc, capability.parse(cap) ] - : isCapability(cap) ? [ ...acc, cap ] : acc + ? [...acc, capability.parse(cap)] + : isCapability(cap) ? [...acc, cap] : acc // ?! silently dropping if not valid }, []) // Fin @@ -407,8 +404,8 @@ export const validateProofs = (plugins: Plugins) => throw new Error(`Invalid Proof: Expiration (${proof.payload.exp}) is before parent's 'not before' (${ucan.payload.nbf})`) } - if (checkVersionMonotonic && semver.lt(ucan.header.ucv, proof.header.ucv)) { - throw new Error(`Invalid Proof: Version (${proof.header.ucv}) is higher than parent's version (${ucan.header.ucv})`) + if (checkVersionMonotonic && semver.lt(ucan.payload.ucv, proof.payload.ucv)) { + throw new Error(`Invalid Proof: Version (${proof.payload.ucv}) is higher than parent's version (${ucan.payload.ucv})`) } yield proof @@ -439,4 +436,4 @@ export function isExpired(ucan: Ucan): boolean { export const isTooEarly = (ucan: Ucan): boolean => { if (ucan.payload.nbf == null) return false return ucan.payload.nbf > Math.floor(Date.now() / 1000) -} \ No newline at end of file +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 63044a5..afd6e25 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -30,10 +30,10 @@ export interface UcanParts { export type UcanHeader = { alg: string typ: string - ucv: SemVer } export type UcanPayload = { + ucv: SemVer iss: string aud: string exp: number @@ -156,11 +156,11 @@ export function isUcanHeader(obj: unknown): obj is UcanHeader { return util.isRecord(obj) && util.hasProp(obj, "alg") && typeof obj.alg === "string" && util.hasProp(obj, "typ") && typeof obj.typ === "string" - && util.hasProp(obj, "ucv") && semver.isSemVer(obj.ucv) } export function isUcanPayload(obj: unknown): obj is UcanPayload { return util.isRecord(obj) + && util.hasProp(obj, "ucv") && semver.isSemVer(obj.ucv) && util.hasProp(obj, "iss") && typeof obj.iss === "string" && util.hasProp(obj, "aud") && typeof obj.aud === "string" && util.hasProp(obj, "exp") && typeof obj.exp === "number" @@ -169,4 +169,4 @@ export function isUcanPayload(obj: unknown): obj is UcanPayload { && util.hasProp(obj, "att") && Array.isArray(obj.att) && obj.att.every(a => isCapability(a) || isEncodedCapability(a)) && (!util.hasProp(obj, "fct") || Array.isArray(obj.fct) && obj.fct.every(util.isRecord)) && util.hasProp(obj, "prf") && Array.isArray(obj.prf) && obj.prf.every(str => typeof str === "string") -} \ No newline at end of file +}