diff --git a/.eslintrc.js b/.eslintrc.js index 52361c2..4bde45a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,19 +13,20 @@ module.exports = { "plugin:@typescript-eslint/recommended", ], rules: { - "@typescript-eslint/member-delimiter-style": ["error", { + "@typescript-eslint/member-delimiter-style": [ "error", { "multiline": { "delimiter": "none", "requireLast": false }, - }], - "@typescript-eslint/no-use-before-define": ["off"], - "@typescript-eslint/semi": ["error", "never"], + } ], + "@typescript-eslint/no-use-before-define": [ "off" ], + "@typescript-eslint/semi": [ "error", "never" ], "@typescript-eslint/ban-ts-comment": 1, - "@typescript-eslint/quotes": ["error", "double", { + "@typescript-eslint/quotes": [ "error", "double", { allowTemplateLiterals: true - }], + } ], // If you want to *intentionally* run a promise without awaiting, prepend it with "void " instead of "await " - "@typescript-eslint/no-floating-promises": ["error"], + "@typescript-eslint/no-floating-promises": [ "error" ], + "@typescript-eslint/no-inferrable-types": [ "off" ], } } diff --git a/README.md b/README.md index b129287..7633417 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ At a high level, UCANs (β€œUser Controlled Authorization Network”) are an auth No all-powerful authorization server or server of any kind is required for UCANs. Instead, everything a user can do is captured directly in a key or token, which can be sent to anyone who knows how to interpret the UCAN format. Because UCANs are self-contained, they are easy to consume permissionlessly, and they work well offline and in distributed systems. - UCANs work - Server -> Server - Client -> Server @@ -20,14 +19,18 @@ UCANs work Read more in the whitepaper: https://whitepaper.fission.codes/access-control/ucan + ## Structure - ### Header + +### Header + `alg`, Algorithm, the type of signature. `typ`, Type, the type of this data structure, JWT. `uav`, UCAN version. - ### Payload + +### Payload `att`, Attenuation, a list of resources and capabilities that the ucan grants. @@ -44,41 +47,52 @@ Read more in the whitepaper: https://whitepaper.fission.codes/access-control/uca `prf`, Proof, an optional nested token with equal or greater privileges. ### Signature + A signature (using `alg`) of the base64 encoded header and payload concatenated together and delimited by `.` -## Build params -Use `ucan.build` to help in formatting and signing a ucan. It takes the following parameters + + +## Build + +`ucan.build` can be used to help in formatting and signing a UCAN. It takes the following parameters: ```ts -export type BuildParams = { - // to/from - audience: string +type BuildParams = { + // from/to issuer: Keypair + audience: string // capabilities - capabilities: Array + capabilities?: Array // time bounds lifetimeInSeconds?: number // expiration overrides lifetimeInSeconds expiration?: number notBefore?: number - // proof / other info + // proofs / other info facts?: Array - proof?: string - - // in the weeds - ucanVersion?: string + proofs?: Array + addNonce?: boolean } ``` + ### Capabilities -`capabilities` is an array of resources and permission level formatted as: + +`capabilities` is an array of resource pointers and abilities: ```ts { - $TYPE: $IDENTIFIER, - "cap": $CAPABILITY + // `with` is a resource pointer in the form of a URI, which has a `scheme` and `hierPart`. + // β†’ "mailto:boris@fission.codes" + with: { scheme: "mailto", hierPart: "boris@fission.codes" }, + + // `can` is an ability, which always has a namespace and optional segments. + // β†’ "msg/SEND" + can: { namespace: "msg", segments: [ "SEND" ] } } ``` + + ## Installation ### NPM: @@ -95,25 +109,25 @@ yarn add ucans ## Example ```ts -import * as ucan from 'ucans' +import * as ucan from "ucans" // in-memory keypair const keypair = await ucan.EdKeypair.create() const u = await ucan.build({ - audience: "did:key:zabcde...", //recipient DID - issuer: keypair, //signing key + audience: "did:key:zabcde...", // recipient DID + issuer: keypair, // signing key capabilities: [ // permissions for ucan { - "wnfs": "boris.fission.name/public/photos/", - "cap": "OVERWRITE" + with: { scheme: "wnfs", hierPart: "//boris.fission.name/public/photos/" }, + can: { namespace: "wnfs", segments: [ "OVERWRITE" ] } }, { - "wnfs": "boris.fission.name/private/4tZA6S61BSXygmJGGW885odfQwpnR2UgmCaS5CfCuWtEKQdtkRnvKVdZ4q6wBXYTjhewomJWPL2ui3hJqaSodFnKyWiPZWLwzp1h7wLtaVBQqSW4ZFgyYaJScVkBs32BThn6BZBJTmayeoA9hm8XrhTX4CGX5CVCwqvEUvHTSzAwdaR", - "cap": "APPEND" + with: { scheme: "wnfs", hierPart: "//boris.fission.name/private/4tZA6S61BSXygmJGGW885odfQwpnR2UgmCaS5CfCuWtEKQdtkRnvKVdZ4q6wBXYTjhewomJWPL2ui3hJqaSodFnKyWiPZWLwzp1h7wLtaVBQqSW4ZFgyYaJScVkBs32BThn6BZBJTmayeoA9hm8XrhTX4CGX5CVCwqvEUvHTSzAwdaR" }, + can: { namespace: "wnfs", segments: [ "APPEND" ] } }, { - "email": "boris@fission.codes", - "cap": "SEND" + with: { scheme: "mailto", hierPart: "boris@fission.codes" }, + can: { namespace: "wnfs", segments: [ "SEND" ] } } ] }) @@ -124,6 +138,8 @@ const payload = await ucan.buildPayload(...) const u = await ucan.sign(payload, keyType, signingFn) ``` + + ## Sponsors Sponsors that contribute developer time or resources to this implementation of UCANs: @@ -133,4 +149,5 @@ Sponsors that contribute developer time or resources to this implementation of U ## UCAN Toucan + ![](https://ipfs.runfission.com/ipfs/QmcyAwK7AjvLXbGuL4cqG5nufEKJquFmFGo2SDsaAe939Z) diff --git a/package.json b/package.json index 562e3a5..fc4a02b 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,13 @@ "dependencies": { "@stablelib/ed25519": "^1.0.2", "one-webcrypto": "^1.0.1", + "semver": "^7.3.5", "uint8arrays": "^3.0.0" }, "devDependencies": { "@types/jest": "^27.0.1", "@types/node": "^16.9.1", + "@types/semver": "^7.3.9", "@typescript-eslint/eslint-plugin": "^5.5.0", "@typescript-eslint/parser": "^5.5.0", "eslint": "^8.3.0", diff --git a/src/attenuation.ts b/src/attenuation.ts index ddf9c52..41befb1 100644 --- a/src/attenuation.ts +++ b/src/attenuation.ts @@ -1,9 +1,13 @@ -// https://whitepaper.fission.codes/access-control/ucan/jwt-authentication#attenuation -import { Capability, Ucan } from "./types" +// https://github.com/ucan-wg/spec/blob/dd4ac83f893cef109f5a26b07970b2484f23aabf/README.md#325-attenuation-scope +import { Capability } from "./capability" import { Chained } from "./chained" +import { Ucan } from "./types" import * as util from "./util" +// TYPES + + export interface CapabilitySemantics { /** * Try to parse a capability into a representation used for @@ -14,6 +18,7 @@ export interface CapabilitySemantics { * return `null`. */ tryParsing(cap: Capability): A | null + /** * This figures out whether a given `childCap` can be delegated from `parentCap`. * There are three possible results with three return types respectively: @@ -22,9 +27,11 @@ export interface CapabilitySemantics { * - `CapabilityEscalation`: It's clear that `childCap` is meant to be delegated from `parentCap`, but there's a rights escalation. */ tryDelegating(parentCap: A, childCap: A): A | null | CapabilityEscalation - // TODO builders } +export type CapabilityResult + = CapabilityWithInfo + | CapabilityEscalation export interface CapabilityInfo { originator: string // DID @@ -32,52 +39,45 @@ export interface CapabilityInfo { notBefore?: number } - export interface CapabilityWithInfo { info: CapabilityInfo capability: A } - -export type CapabilityResult - = CapabilityWithInfo - | CapabilityEscalation - - export interface CapabilityEscalation { escalation: string // reason capability: A // the capability that escalated rights } + + +// TYPE CHECKING + + export function isCapabilityEscalation(obj: unknown): obj is CapabilityEscalation { return util.isRecord(obj) && util.hasProp(obj, "escalation") && typeof obj.escalation === "string" && util.hasProp(obj, "capability") } -export function hasCapability(semantics: CapabilitySemantics, capability: CapabilityWithInfo, ucan: Chained): CapabilityWithInfo | false { - for (const cap of capabilities(ucan, semantics)) { - if (isCapabilityEscalation(cap)) { - continue - } - const delegatedCapability = semantics.tryDelegating(cap.capability, capability.capability) - if (isCapabilityEscalation(delegatedCapability)) { - continue - } +// PARSING - if (delegatedCapability != null) { - return { - info: delegateCapabilityInfo(capability.info, cap.info), - capability: delegatedCapability, - } - } - } - return false +function parseCapabilityInfo(ucan: Ucan): CapabilityInfo { + return { + originator: ucan.payload.iss, + expiresAt: ucan.payload.exp, + ...(ucan.payload.nbf != null ? { notBefore: ucan.payload.nbf } : {}), + } } + + +// FUNCTIONS + + export function canDelegate(semantics: CapabilitySemantics, capability: A, ucan: Chained): boolean { for (const cap of capabilities(ucan, semantics)) { if (isCapabilityEscalation(cap)) { @@ -98,7 +98,6 @@ export function canDelegate(semantics: CapabilitySemantics, capability: A, return false } - export function capabilities( ucan: Chained, capability: CapabilitySemantics, @@ -171,10 +170,29 @@ function delegateCapabilityInfo(childInfo: CapabilityInfo, parentInfo: Capabilit } } -function parseCapabilityInfo(ucan: Ucan): CapabilityInfo { - return { - originator: ucan.payload.iss, - expiresAt: ucan.payload.exp, - ...(ucan.payload.nbf != null ? { notBefore: ucan.payload.nbf } : {}), +export function hasCapability( + semantics: CapabilitySemantics, + capability: CapabilityWithInfo, + ucan: Chained +): CapabilityWithInfo | false { + for (const cap of capabilities(ucan, semantics)) { + if (isCapabilityEscalation(cap)) { + continue + } + + const delegatedCapability = semantics.tryDelegating(cap.capability, capability.capability) + + if (isCapabilityEscalation(delegatedCapability)) { + continue + } + + if (delegatedCapability != null) { + return { + info: delegateCapabilityInfo(capability.info, cap.info), + capability: delegatedCapability, + } + } } + + return false } diff --git a/src/builder.ts b/src/builder.ts index cd65b0d..441a40b 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -1,6 +1,7 @@ import * as token from "./token" import * as util from "./util" -import { Capability, Keypair, Fact, UcanPayload, isKeypair, isCapability } from "./types" +import { Keypair, Fact, UcanPayload, isKeypair } from "./types" +import { Capability, isCapability } from "./capability" import { CapabilityInfo, CapabilitySemantics, canDelegate } from "./attenuation" import { Chained } from "./chained" import { Store } from "./store" @@ -158,7 +159,7 @@ export class Builder> { } return new Builder(this.state, { ...this.defaultable, - facts: [...this.defaultable.facts, fact, ...facts] + facts: [ ...this.defaultable.facts, fact, ...facts ] }) } @@ -180,7 +181,7 @@ export class Builder> { } return new Builder(this.state, { ...this.defaultable, - capabilities: [...this.defaultable.capabilities, capability, ...capabilities] + capabilities: [ ...this.defaultable.capabilities, capability, ...capabilities ] }) } @@ -231,9 +232,9 @@ export class Builder> { } return new Builder(this.state, { ...this.defaultable, - capabilities: [...this.defaultable.capabilities, requiredCapability], + capabilities: [ ...this.defaultable.capabilities, requiredCapability ], proofs: this.defaultable.proofs.find(proof => proof.encoded() === storeOrProof.encoded()) == null - ? [...this.defaultable.proofs, storeOrProof] + ? [ ...this.defaultable.proofs, storeOrProof ] : this.defaultable.proofs }) } else { @@ -243,9 +244,9 @@ export class Builder> { if (result.success) { return new Builder(this.state, { ...this.defaultable, - capabilities: [...this.defaultable.capabilities, requiredCapability], + capabilities: [ ...this.defaultable.capabilities, requiredCapability ], proofs: this.defaultable.proofs.find(proof => proof.encoded() === result.ucan.encoded()) == null - ? [...this.defaultable.proofs, result.ucan] + ? [ ...this.defaultable.proofs, result.ucan ] : this.defaultable.proofs }) } else { diff --git a/src/capability.ts b/src/capability.ts new file mode 100644 index 0000000..dcd581e --- /dev/null +++ b/src/capability.ts @@ -0,0 +1,103 @@ +import * as ability from "./capability/ability" +import * as resourcePointer from "./capability/resource-pointer" +import * as superUser from "./capability/super-user" +import * as util from "./util" + +import { Ability, isAbility } from "./capability/ability" +import { ResourcePointer, isResourcePointer } from "./capability/resource-pointer" +import { Superuser, SUPERUSER } from "./capability/super-user" + + +// RE-EXPORTS + + +export { ability, resourcePointer, superUser } + + + +// πŸ’Ž + + +export type Capability = { + with: ResourcePointer + can: Ability +} + +export type EncodedCapability = { + with: string + can: string +} + + + +// TYPE CHECKS + + +export function isCapability(obj: unknown): obj is Capability { + return util.isRecord(obj) + && util.hasProp(obj, "with") && isResourcePointer(obj.with) + && util.hasProp(obj, "can") && isAbility(obj.can) +} + +export function isEncodedCapability(obj: unknown): obj is EncodedCapability { + return util.isRecord(obj) + && util.hasProp(obj, "with") && typeof obj.with === "string" + && util.hasProp(obj, "can") && typeof obj.can === "string" +} + + + +// 🌸 + + +export function as(identifier: string): Capability { + return { + with: resourcePointer.as(identifier), + can: SUPERUSER + } +} + + +export function my(resource: Superuser | string): Capability { + return { + with: resourcePointer.my(resource), + can: SUPERUSER + } +} + + +export function prf(selector: Superuser | number, ability: Ability): Capability { + return { + with: resourcePointer.prf(selector), + can: ability + } +} + + + +// ENCODING + + +/** + * Encode the individual parts of a capability. + * + * @param cap The capability to encode + */ +export function encode(cap: Capability): EncodedCapability { + return { + with: resourcePointer.encode(cap.with), + can: ability.encode(cap.can) + } +} + +/** + * Parse an encoded capability. + * + * @param cap The encoded capability + */ +export function parse(cap: EncodedCapability): Capability { + return { + with: resourcePointer.parse(cap.with), + can: ability.parse(cap.can) + } +} \ No newline at end of file diff --git a/src/capability/ability.ts b/src/capability/ability.ts new file mode 100644 index 0000000..eede8fa --- /dev/null +++ b/src/capability/ability.ts @@ -0,0 +1,83 @@ +import { Superuser, SUPERUSER } from "./super-user" +import * as util from "../util" + + +// πŸ’Ž + + +export type Ability + = Superuser + | { namespace: string; segments: string[] } + +export const SEPARATOR: string = "/" + + + +// TYPE CHECKS + + +export function isAbility(obj: unknown): obj is Ability { + return obj === SUPERUSER + || ( + util.isRecord(obj) + && util.hasProp(obj, "namespace") && typeof obj.namespace === "string" + && util.hasProp(obj, "segments") && Array.isArray(obj.segments) && obj.segments.every(s => typeof s === "string") + ) +} + + + +// πŸ›  + + +export function isEqual(a: Ability, b: Ability): boolean { + if (a === SUPERUSER && b === SUPERUSER) return true + if (a === SUPERUSER || b === SUPERUSER) return false + + return ( + a.namespace.toLowerCase() === + b.namespace.toLowerCase() + ) && + ( + joinSegments(a.segments).toLowerCase() === + joinSegments(b.segments).toLowerCase() + ) +} + + +export function joinSegments(segments: string[]): string { + return segments.join(SEPARATOR) +} + + + +// ENCODING + + +/** + * Encode an ability. + * + * @param ability The ability to encode + */ +export function encode(ability: Ability): string { + switch (ability) { + case SUPERUSER: return ability + default: return joinSegments([ ability.namespace, ...ability.segments ]) + } +} + +/** + * Parse an encoded ability. + * + * @param ability The encoded ability + */ +export function parse(ability: string): Ability { + switch (ability) { + case SUPERUSER: + return SUPERUSER + default: { + const [ namespace, ...segments ] = ability.split(SEPARATOR) + return { namespace, segments } + } + } +} \ No newline at end of file diff --git a/src/capability/resource-pointer.ts b/src/capability/resource-pointer.ts new file mode 100644 index 0000000..0801b8a --- /dev/null +++ b/src/capability/resource-pointer.ts @@ -0,0 +1,85 @@ +import { Superuser, SUPERUSER } from "./super-user" +import * as util from "../util" + + +// πŸ’Ž + + +export type ResourcePointer = { + scheme: string + hierPart: Superuser | string +} + +export const SEPARATOR: string = ":" + + + +// TYPE CHECKS + + +export function isResourcePointer(obj: unknown): obj is ResourcePointer { + return util.isRecord(obj) + && util.hasProp(obj, "scheme") && typeof obj.scheme === "string" + && util.hasProp(obj, "hierPart") && (obj.hierPart === SUPERUSER || typeof obj.hierPart === "string") +} + + + +// 🌸 + + +export function as(identifier: string): ResourcePointer { + return { + scheme: "as", + hierPart: identifier + } +} + + +export function my(resource: Superuser | string): ResourcePointer { + return { + scheme: "my", + hierPart: resource + } +} + + +export function prf(selector: Superuser | number): ResourcePointer { + return { + scheme: "prf", + hierPart: selector.toString() + } +} + + + +// πŸ›  + + +export function isEqual(a: ResourcePointer, b: ResourcePointer): boolean { + return a.scheme.toLowerCase() === a.scheme.toLowerCase() && a.hierPart === b.hierPart +} + + + +// ENCODING + + +/** + * Encode a resource pointer. + * + * @param pointer The resource pointer to encode + */ +export function encode(pointer: ResourcePointer): string { + return `${pointer.scheme}${SEPARATOR}${pointer.hierPart}` +} + +/** + * Parse an encoded resource pointer. + * + * @param pointer The encoded resource pointer + */ +export function parse(pointer: string): ResourcePointer { + const [ scheme, ...hierPart ] = pointer.split(SEPARATOR) + return { scheme, hierPart: hierPart.join(SEPARATOR) } +} \ No newline at end of file diff --git a/src/capability/super-user.ts b/src/capability/super-user.ts new file mode 100644 index 0000000..5432133 --- /dev/null +++ b/src/capability/super-user.ts @@ -0,0 +1,10 @@ +export const SUPERUSER: Superuser = "*" +export type Superuser = "*" // maximum ability + + +// TYPE CHECKS + + +export function isSuperuser(obj: unknown): obj is Superuser { + return obj === SUPERUSER +} \ No newline at end of file diff --git a/src/capability/wnfs.ts b/src/capability/wnfs.ts deleted file mode 100644 index eca89bc..0000000 --- a/src/capability/wnfs.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { Capability } from "../types" -import { capabilities, CapabilityEscalation, CapabilitySemantics } from "../attenuation" -import { Chained } from "../chained" - - -export const wnfsCapLevels = { - "SUPER_USER": 0, - "OVERWRITE": -1, - "SOFT_DELETE": -2, - "REVISE": -3, - "CREATE": -4, -} - -export type WnfsCap = keyof typeof wnfsCapLevels - -export function isWnfsCap(obj: unknown): obj is WnfsCap { - return typeof obj === "string" && Object.keys(wnfsCapLevels).includes(obj) -} - - - -///////////////////////////// -// Public WNFS Capabilities -///////////////////////////// - - -export interface WnfsPublicCapability { - user: string // e.g. matheus23.fission.name - publicPath: string[] - cap: WnfsCap -} - -export const wnfsPublicSemantics: CapabilitySemantics = { - - /** - * Example valid public wnfs capability: - * ```js - * { - * wnfs: "boris.fission.name/public/path/to/dir/or/file", - * cap: "OVERWRITE" - * } - * ``` - */ - tryParsing(cap: Capability): WnfsPublicCapability | null { - if (typeof cap.wnfs !== "string" || !isWnfsCap(cap.cap)) return null - - // remove trailing slash - const trimmed = cap.wnfs.endsWith("/") ? cap.wnfs.slice(0, -1) : cap.wnfs - const split = trimmed.split("/") - const user = split[0] - const publicPath = split.slice(2) // drop first two: matheus23.fission.name/public/keep/this - if (user == null || split[1] !== "public") return null - return { - user, - publicPath, - cap: cap.cap, - } - }, - - tryDelegating(parentCap: WnfsPublicCapability, childCap: WnfsPublicCapability): WnfsPublicCapability | null | CapabilityEscalation { - // need to delegate the same user's file system - if (childCap.user !== parentCap.user) return null - - // must not escalate capability level - if (wnfsCapLevels[childCap.cap] > wnfsCapLevels[parentCap.cap]) { - return { - escalation: "Capability level escalation", - capability: childCap, - } - } - - // parentCap path must be a prefix of childCap path - if (childCap.publicPath.length < parentCap.publicPath.length) { - return { - escalation: "WNFS Public path access escalation", - capability: childCap, - } - } - - for (let i = 0; i < parentCap.publicPath.length; i++) { - if (childCap.publicPath[i] !== parentCap.publicPath[i]) { - return { - escalation: "WNFS Public path access escalation", - capability: childCap, - } - } - } - - return childCap - }, - -} - -export function wnfsPublicCapabilities(ucan: Chained) { - return capabilities(ucan, wnfsPublicSemantics) -} - - - -///////////////////////////// -// Private WNFS Capabilities -///////////////////////////// - - -export interface WnfsPrivateCapability { - user: string - requiredINumbers: Set - cap: WnfsCap -} - -const wnfsPrivateSemantics: CapabilitySemantics = { - - /** - * Example valid private wnfs capability: - * - * ```js - * { - * wnfs: "boris.fission.name/private/fccXmZ8HYmpwxkvPSjwW9A", - * cap: "OVERWRITE" - * } - * ``` - */ - tryParsing(cap: Capability): WnfsPrivateCapability | null { - if (typeof cap.wnfs !== "string" || !isWnfsCap(cap.cap)) return null - - // split up "boris.fission.name/private/fccXmZ8HYmpwxkvPSjwW9A" into "/private/" - const split = cap.wnfs.split("/") - const user = split[0] - const inumberBase64url = split[2] - - if (user == null || split[1] !== "private" || inumberBase64url == null) return null - - return { - user, - requiredINumbers: new Set([inumberBase64url]), - cap: cap.cap, - } - }, - - tryDelegating(parentCap: T, childCap: T): T | null | CapabilityEscalation { - // If the users don't match, these capabilities are unrelated. - if (childCap.user !== parentCap.user) return null - - // This escalation *could* be wrong, but we shouldn't assume they're unrelated either. - if (wnfsCapLevels[childCap.cap] > wnfsCapLevels[parentCap.cap]) { - return { - escalation: "Capability level escalation", - capability: childCap, - } - } - - return { - ...childCap, - requiredINumbers: new Set([...childCap.requiredINumbers.values(), ...parentCap.requiredINumbers.values()]), - } - }, - -} - -export function wnfsPrivateCapabilities(ucan: Chained) { - return capabilities(ucan, wnfsPrivateSemantics) -} diff --git a/src/chained.ts b/src/chained.ts index cec4e43..c88eaab 100644 --- a/src/chained.ts +++ b/src/chained.ts @@ -1,4 +1,5 @@ -import { Ucan, Capability, Fact } from "./types" +import { Capability } from "./capability" +import { Ucan, Fact } from "./types" import * as token from "./token" diff --git a/src/compatibility.ts b/src/compatibility.ts index 4e7ef72..425e31d 100644 --- a/src/compatibility.ts +++ b/src/compatibility.ts @@ -1,39 +1,44 @@ // A module to hold all the ugly compatibility logic // for getting from old UCANs to newer version UCANs. + +import * as semver from "semver" + import * as util from "./util" +import { SUPERUSER } from "./capability/super-user" import { UcanParts, isUcanHeader, isUcanPayload } from "./types" +import { my } from "./capability" -type UcanHeader_0_0_1 = { +type UcanHeader_0_3_0 = { alg: string typ: string uav: string } -type UcanPayload_0_0_1 = { +type UcanPayload_0_3_0 = { iss: string aud: string nbf?: number exp: number - rsc: string + rsc: string | Record ptc: string prf?: string } -function isUcanHeader_0_0_1(obj: unknown): obj is UcanHeader_0_0_1 { +function isUcanHeader_0_3_0(obj: unknown): obj is UcanHeader_0_3_0 { return util.isRecord(obj) && util.hasProp(obj, "alg") && typeof obj.alg === "string" && util.hasProp(obj, "typ") && typeof obj.typ === "string" && util.hasProp(obj, "uav") && typeof obj.uav === "string" } -function isUcanPayload_0_0_1(obj: unknown): obj is UcanPayload_0_0_1 { +function isUcanPayload_0_3_0(obj: unknown): obj is UcanPayload_0_3_0 { return util.isRecord(obj) && util.hasProp(obj, "iss") && typeof obj.iss === "string" && util.hasProp(obj, "aud") && typeof obj.aud === "string" && (!util.hasProp(obj, "nbf") || typeof obj.nbf === "number") && util.hasProp(obj, "exp") && typeof obj.exp === "number" - && util.hasProp(obj, "rsc") && typeof obj.rsc === "string" + && util.hasProp(obj, "rsc") && (typeof obj.rsc === "string" || util.isRecord(obj)) && util.hasProp(obj, "ptc") && typeof obj.ptc === "string" && (!util.hasProp(obj, "prf") || typeof obj.prf === "string") } @@ -41,52 +46,60 @@ function isUcanPayload_0_0_1(obj: unknown): obj is UcanPayload_0_0_1 { export function handleCompatibility(header: unknown, payload: unknown): UcanParts { 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") // parse either the "ucv" or "uav" as a version in the header - // we translate 'uav: 1.0.0' into 'ucv: 0.0.1' - // we only support versions 0.7.0 and 0.0.1 - let version: "0.7.0" | "0.0.1" = "0.7.0" + // we translate 'uav: 1.0.0' into 'ucv: 0.3.0' + let version: "0.8.1" | "0.3.0" = "0.8.1" if (!util.hasProp(header, "ucv") || typeof header.ucv !== "string") { if (!util.hasProp(header, "uav") || typeof header.uav !== "string") { throw fail("header", "Invalid format: Missing version indicator") } else if (header.uav !== "1.0.0") { throw fail("header", `Unsupported version 'uav: ${header.uav}'`) } - version = "0.0.1" - } else if (header.ucv !== "0.7.0") { + version = "0.3.0" + } else if (semver.lt(header.ucv, "0.8.0")) { throw fail("header", `Unsupported version 'ucv: ${header.ucv}'`) } - if (version === "0.7.0") { + if (semver.gte(version, "0.8.0")) { if (!isUcanHeader(header)) throw fail("header", "Invalid format") if (!isUcanPayload(payload)) throw fail("payload", "Invalid format") return { header, payload } } - // we know it's version 0.0.1 - - if (!isUcanHeader_0_0_1(header)) throw fail("header", "Invalid version 0.0.1 format") - if (!isUcanPayload_0_0_1(payload)) throw fail("payload", "Invalid version 0.0.1 format") + // we know it's version 0.3.0 + if (!isUcanHeader_0_3_0(header)) throw fail("header", "Invalid version 0.3.0 format") + if (!isUcanPayload_0_3_0(payload)) throw fail("payload", "Invalid version 0.3.0 format") return { header: { alg: header.alg, typ: header.typ, - ucv: "0.0.1", + ucv: "0.3.0", }, payload: { iss: payload.iss, aud: payload.aud, nbf: payload.nbf, exp: payload.exp, - att: [{ - rsc: payload.rsc, - cap: payload.ptc, - }], - prf: payload.prf != null ? [payload.prf] : [] + att: (() => { + if (payload.rsc === SUPERUSER || typeof payload.rsc === "string") return [ + my(SUPERUSER) + ] + + const resources: Record = payload.rsc + return Object.keys(resources).map(rscKey => { + return { + with: { scheme: rscKey, hierPart: resources[ rscKey ] }, + can: payload.ptc === SUPERUSER + ? SUPERUSER + : { namespace: rscKey, segments: [ payload.ptc ] } + } + }) + })(), + prf: payload.prf != null ? [ payload.prf ] : [] }, } } - diff --git a/src/crypto/rsa.ts b/src/crypto/rsa.ts index b7eaf8c..1ae1a45 100644 --- a/src/crypto/rsa.ts +++ b/src/crypto/rsa.ts @@ -6,16 +6,17 @@ export const DEFAULT_KEY_SIZE = 2048 export const DEFAULT_HASH_ALG = "SHA-256" export const SALT_LEGNTH = 128 + export const generateKeypair = async (size: number = DEFAULT_KEY_SIZE): Promise => { return await webcrypto.subtle.generateKey( { name: RSA_ALG, modulusLength: size, - publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + publicExponent: new Uint8Array([ 0x01, 0x00, 0x01 ]), hash: { name: DEFAULT_HASH_ALG } }, false, - ["sign", "verify"] + [ "sign", "verify" ] ) } @@ -30,7 +31,7 @@ export const importKey = async (key: Uint8Array): Promise => { key.buffer, { name: RSA_ALG, hash: { name: DEFAULT_HASH_ALG } }, true, - ["verify"] + [ "verify" ] ) } @@ -67,16 +68,16 @@ export const verify = async (msg: Uint8Array, sig: Uint8Array, pubKey: Uint8Arra * * See https://github.com/ucan-wg/ts-ucan/issues/30 */ -const SPKI_PARAMS_ENCODED = new Uint8Array([48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0]) -const ASN_SEQUENCE_TAG = new Uint8Array([0x30]) -const ASN_BITSTRING_TAG = new Uint8Array([0x03]) +const SPKI_PARAMS_ENCODED = new Uint8Array([ 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0 ]) +const ASN_SEQUENCE_TAG = new Uint8Array([ 0x30 ]) +const ASN_BITSTRING_TAG = new Uint8Array([ 0x03 ]) export const convertRSAPublicKeyToSubjectPublicKeyInfo = (rsaPublicKey: Uint8Array): Uint8Array => { // More info on bitstring encoding: https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-bit-string const bitStringEncoded = uint8arrays.concat([ ASN_BITSTRING_TAG, asn1DERLengthEncode(rsaPublicKey.length + 1), - new Uint8Array([0x00]), // amount of unused bits at the end of our bitstring (counts into length?!) + new Uint8Array([ 0x00 ]), // amount of unused bits at the end of our bitstring (counts into length?!) rsaPublicKey ]) return uint8arrays.concat([ @@ -96,7 +97,7 @@ export const convertSubjectPublicKeyInfoToRSAPublicKey = (subjectPublicKeyInfo: // we expect the bitstring next const bitstringParams = asn1Into(subjectPublicKeyInfo, ASN_BITSTRING_TAG, position) const bitstring = subjectPublicKeyInfo.subarray(bitstringParams.position, bitstringParams.position + bitstringParams.length) - const unusedBitPadding = bitstring[0] + const unusedBitPadding = bitstring[ 0 ] if (unusedBitPadding !== 0) { throw new Error(`Can't convert SPKI to PKCS: Expected bitstring length to be multiple of 8, but got ${unusedBitPadding} unused bits in last byte.`) } @@ -112,7 +113,7 @@ export function asn1DERLengthEncode(length: number): Uint8Array { } if (length <= 127) { - return new Uint8Array([length]) + return new Uint8Array([ length ]) } const octets: number[] = [] @@ -121,15 +122,15 @@ export function asn1DERLengthEncode(length: number): Uint8Array { length = length >>> 8 } octets.reverse() - return new Uint8Array([0x80 | (octets.length & 0xFF), ...octets]) + return new Uint8Array([ 0x80 | (octets.length & 0xFF), ...octets ]) } function asn1DERLengthDecodeWithConsumed(bytes: Uint8Array): { number: number; consumed: number } { - if ((bytes[0] & 0x80) === 0) { - return { number: bytes[0], consumed: 1 } + if ((bytes[ 0 ] & 0x80) === 0) { + return { number: bytes[ 0 ], consumed: 1 } } - const numberBytes = bytes[0] & 0x7F + const numberBytes = bytes[ 0 ] & 0x7F if (bytes.length < numberBytes + 1) { throw new Error(`ASN parsing error: Too few bytes. Expected encoded length's length to be at least ${numberBytes}`) } @@ -137,7 +138,7 @@ function asn1DERLengthDecodeWithConsumed(bytes: Uint8Array): { number: number; c let length = 0 for (let i = 0; i < numberBytes; i++) { length = length << 8 - length = length | bytes[i + 1] + length = length | bytes[ i + 1 ] } return { number: length, consumed: numberBytes + 1 } } diff --git a/src/index.ts b/src/index.ts index f67acb0..ad4f9e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,14 @@ -export * from "./token" +export * from "./attenuation" +export * from "./builder" +export * from "./chained" export * from "./did" -export * as keypair from "./keypair" export * from "./keypair/ed25519" export * from "./keypair/rsa" -export * from "./types" -export * from "./chained" -export * from "./builder" export * from "./store" -export * from "./attenuation" +export * from "./token" +export * from "./types" + +export * as keypair from "./keypair" +export * as capability from "./capability" + +export { Capability, EncodedCapability, isCapability } from "./capability" \ No newline at end of file diff --git a/src/keypair/base.ts b/src/keypair/base.ts index 0ec0be1..ac030b7 100644 --- a/src/keypair/base.ts +++ b/src/keypair/base.ts @@ -2,13 +2,14 @@ import * as uint8arrays from "uint8arrays" import { publicKeyBytesToDid } from "../did/transformers" import { Keypair, KeyType, Encodings, Didable, ExportableKey } from "../types" + export default abstract class BaseKeypair implements Keypair, Didable, ExportableKey { publicKey: Uint8Array keyType: KeyType exportable: boolean - constructor (publicKey: Uint8Array, keyType: KeyType, exportable: boolean) { + constructor(publicKey: Uint8Array, keyType: KeyType, exportable: boolean) { this.publicKey = publicKey this.keyType = keyType this.exportable = exportable diff --git a/src/keypair/ed25519.ts b/src/keypair/ed25519.ts index 15e6f5e..9061377 100644 --- a/src/keypair/ed25519.ts +++ b/src/keypair/ed25519.ts @@ -3,6 +3,7 @@ import * as uint8arrays from "uint8arrays" import BaseKeypair from "./base" import { Encodings } from "../types" + export class EdKeypair extends BaseKeypair { private secretKey: Uint8Array @@ -43,4 +44,5 @@ export class EdKeypair extends BaseKeypair { } + export default EdKeypair diff --git a/src/keypair/rsa.ts b/src/keypair/rsa.ts index 9b2c683..75dd518 100644 --- a/src/keypair/rsa.ts +++ b/src/keypair/rsa.ts @@ -5,6 +5,7 @@ import * as rsa from "../crypto/rsa" import BaseKeypair from "./base" import { Encodings, AvailableCryptoKeyPair, isAvailableCryptoKeyPair } from "../types" + export class RsaKeypair extends BaseKeypair { private keypair: AvailableCryptoKeyPair diff --git a/src/store.ts b/src/store.ts index 51f76b4..27ce3c9 100644 --- a/src/store.ts +++ b/src/store.ts @@ -3,7 +3,7 @@ import { capabilities, CapabilityInfo, CapabilitySemantics, isCapabilityEscalati export interface IndexByAudience { - [audienceDID: string]: Chained[] + [ audienceDID: string ]: Chained[] } export class Store { @@ -24,20 +24,20 @@ export class Store { add(ucan: Chained): void { const audience = ucan.audience() - const byAudience = this.index[audience] ?? [] + const byAudience = this.index[ audience ] ?? [] if (byAudience.find(storedUcan => storedUcan.encoded() === ucan.encoded()) != null) { return } byAudience.push(ucan) - this.index[audience] = byAudience + this.index[ audience ] = byAudience } getByAudience(audience: string): Chained[] { - return [...(this.index[audience] ?? [])] + return [ ...(this.index[ audience ] ?? []) ] } findByAudience(audience: string, predicate: (ucan: Chained) => boolean): Chained | null { - return this.index[audience]?.find(ucan => predicate(ucan)) ?? null + return this.index[ audience ]?.find(ucan => predicate(ucan)) ?? null } findWithCapability( @@ -46,7 +46,7 @@ export class Store { requirementsCap: A, requirementsInfo: (info: CapabilityInfo) => boolean, ): { success: true; ucan: Chained } | FindFailure { - const ucans = this.index[audience] + const ucans = this.index[ audience ] if (ucans == null) { return { success: false, reason: `Couldn't find any UCAN for audience ${audience}` } diff --git a/src/token.ts b/src/token.ts index c8a1059..27a1cd8 100644 --- a/src/token.ts +++ b/src/token.ts @@ -1,17 +1,21 @@ import * as did from "./did" import * as uint8arrays from "uint8arrays" + import * as util from "./util" import { handleCompatibility } from "./compatibility" +import { isUcanHeader, isUcanPayload } from "./types" import { verifySignatureUtf8 } from "./did/validation" -import { Capability, Fact, Keypair, KeyType } from "./types" -import { Ucan, UcanHeader, UcanPayload } from "./types" +import { Capability, isEncodedCapability } from "./capability" +import { Fact, Keypair, KeyType } from "./types" +import { Ucan, UcanHeader, UcanPayload, UcanParts } from "./types" +import { capability, isCapability } from "." // CONSTANTS const TYPE = "JWT" -const VERSION = "0.7.0" +const VERSION = "0.8.1" @@ -29,6 +33,7 @@ const VERSION = "0.7.0" * * ### Payload * + * `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. * `fct`, Facts, an array of extra facts or information to attach to the jwt. @@ -36,7 +41,6 @@ const VERSION = "0.7.0" * `nbf`, Not Before, unix timestamp of when the jwt becomes valid. * `nnc`, Nonce, a randomly generated string, used to ensure the uniqueness of the jwt. * `prf`, Proofs, nested tokens with equal or greater privileges. - * `att`, Attenuation, a list of resources and capabilities that the ucan grants. * */ export async function build(params: { @@ -96,6 +100,10 @@ export function buildPayload(params: { addNonce = false } = params + // Validate + if (!issuer.startsWith("did:")) throw new Error("The issuer must be a DID") + if (!audience.startsWith("did:")) throw new Error("The audience must be a DID") + // Timestamps const currentTimeInSeconds = Math.floor(Date.now() / 1000) const exp = expiration || (currentTimeInSeconds + lifetimeInSeconds) @@ -177,16 +185,17 @@ export function encode(ucan: Ucan): string { const encodedPayload = encodePayload(ucan.payload) return encodedHeader + "." + - encodedPayload + "." + - ucan.signature + encodedPayload + "." + + ucan.signature } /** * Encode the header of a UCAN. * * @param header The UcanHeader to encode + * @returns The header of a UCAN encoded as url-safe base64 JSON */ - export function encodeHeader(header: UcanHeader): string { +export function encodeHeader(header: UcanHeader): string { return uint8arrays.toString(uint8arrays.fromString(JSON.stringify(header), "utf8"), "base64url") } @@ -200,48 +209,69 @@ export function encodePayload(payload: UcanPayload): string { } /** - * Parse an encoded UCAN header. + * Parse an encoded UCAN. * - * @param encodedUcanHeader The encoded UCAN header. + * @param encodedUcan The encoded UCAN. */ -export function parseHeader(encodedUcanHeader: string): unknown { - let decodedUcanHeader: string +export function parse(encodedUcan: string): UcanParts { + const [ encodedHeader, encodedPayload, signature ] = encodedUcan.split(".") + + if (encodedHeader == null || encodedPayload == null || signature == null) { + throw new Error(`Can't parse UCAN: ${encodedUcan}: Expected JWT format: 3 dot-separated base64url-encoded values.`) + } + + // Header + let headerJson: string + let headerObject: unknown + try { - decodedUcanHeader = uint8arrays.toString( - uint8arrays.fromString(encodedUcanHeader, "base64url"), + headerJson = uint8arrays.toString( + uint8arrays.fromString(encodedHeader, "base64url"), "utf8" ) } catch { - throw new Error(`Can't parse UCAN header: ${encodedUcanHeader}: Can't parse as base64url.`) + throw new Error(`Can't parse UCAN header: ${encodedHeader}: Can't parse as base64url.`) } try { - return JSON.parse(decodedUcanHeader) + headerObject = JSON.parse(headerJson) } catch { - throw new Error(`Can't parse UCAN header: ${encodedUcanHeader}: Can't parse base64url encoded JSON inside.`) + throw new Error(`Can't parse UCAN header: ${encodedHeader}: Can't parse encoded JSON inside.`) } -} -/** - * Parse an encoded UCAN payload. - * - * @param encodedUcanPayload The encoded UCAN payload. - */ -export function parsePayload(encodedUcanPayload: string): unknown { - let decodedUcanPayload: string + // Payload + let payloadJson: string + let payloadObject: unknown + try { - decodedUcanPayload = uint8arrays.toString( - uint8arrays.fromString(encodedUcanPayload, "base64url"), + payloadJson = uint8arrays.toString( + uint8arrays.fromString(encodedPayload, "base64url"), "utf8" ) } catch { - throw new Error(`Can't parse UCAN payload: ${encodedUcanPayload}: Can't parse as base64url.`) + throw new Error(`Can't parse UCAN payload: ${encodedPayload}: Can't parse as base64url.`) } try { - return JSON.parse(decodedUcanPayload) + payloadObject = JSON.parse(payloadJson) } catch { - throw new Error(`Can't parse UCAN payload: ${encodedUcanPayload}: Can't parse base64url encoded JSON inside.`) + throw new Error(`Can't parse UCAN payload: ${encodedPayload}: Can't parse encoded JSON inside.`) + } + + // Compatibility layer + const { header, payload } = handleCompatibility(headerObject, payloadObject) + + // 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 + }, []) + + // Fin + return { + header: header, + payload: { ...payload, att: parsedAttenuations } } } @@ -277,15 +307,8 @@ export async function validate(encodedUcan: string, options?: ValidateOptions): const checkIsTooEarly = options?.checkIsTooEarly ?? true const checkSignature = options?.checkSignature ?? true + const { header, payload } = parse(encodedUcan) const [ encodedHeader, encodedPayload, signature ] = encodedUcan.split(".") - if (encodedHeader == null || encodedPayload == null || signature == null) { - throw new Error(`Can't parse UCAN: ${encodedUcan}: Expected JWT format: 3 dot-separated base64url-encoded values.`) - } - - const headerDecoded = parseHeader(encodedHeader) - const payloadDecoded = parsePayload(encodedPayload) - - const { header, payload } = handleCompatibility(headerDecoded, payloadDecoded) if (checkIssuer) { const issuerKeyType = did.didToPublicKey(payload.iss).type diff --git a/src/types.ts b/src/types.ts index 2e8ef58..0973174 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,14 +1,57 @@ import { SupportedEncodings } from "uint8arrays/util/bases" + +import { Capability, isCapability, isEncodedCapability } from "./capability" import * as util from "./util" -export type Encodings = SupportedEncodings -export interface Keypair { - publicKey: Uint8Array - keyType: KeyType - sign: (msg: Uint8Array) => Promise +// πŸ’Ž + + +export type Ucan = { + header: UcanHeader + payload: UcanPayload + signature: string +} + + + +// CHUNKS + + +export interface UcanParts { + header: UcanHeader + payload: UcanPayload +} + +export type UcanHeader = { + alg: string + typ: string + ucv: string } +export type UcanPayload = { + iss: string + aud: string + exp: number + nbf?: number + nnc?: string + att: Array + fct?: Array + prf: Array +} + + + +// FRAGMENTS + + +export type Fact = Record + + + +// CRYPTOGRAPHY + + /** Unlike tslib's CryptoKeyPair, this requires the `privateKey` and `publicKey` fields */ export interface AvailableCryptoKeyPair { privateKey: CryptoKey @@ -24,6 +67,12 @@ export interface ExportableKey { export: (format?: Encodings) => Promise } +export interface Keypair { + publicKey: Uint8Array + keyType: KeyType + sign: (msg: Uint8Array) => Promise +} + export type KeyType = | "rsa" | "p256" @@ -35,46 +84,29 @@ export type KeyType = // https://developer.mozilla.org/en-US/docs/Web/API/EcKeyGenParams export type NamedCurve = "P-256" | "P-384" | "P-521" -export type Fact = Record -export type Capability = { - [rsc: string]: unknown - cap: string -} -export type UcanHeader = { - alg: string - typ: string - ucv: string -} +// MISC -export type UcanPayload = { - iss: string - aud: string - exp: number - nbf?: number - nnc?: string - att: Array - fct?: Array - prf: Array -} -export interface UcanParts { - header: UcanHeader - payload: UcanPayload -} - -export type Ucan = { - header: UcanHeader - payload: UcanPayload - signature: string -} +export type Encodings = SupportedEncodings // TYPE CHECKS +export function isAvailableCryptoKeyPair(keypair: CryptoKeyPair): keypair is AvailableCryptoKeyPair { + return keypair.publicKey != null && keypair.privateKey != null +} + +export function isKeypair(obj: unknown): obj is Keypair { + return util.isRecord(obj) + && util.hasProp(obj, "publicKey") && obj.publicKey instanceof Uint8Array + && util.hasProp(obj, "keyType") && typeof obj.keyType === "string" + && util.hasProp(obj, "sign") && typeof obj.sign === "function" +} + export function isUcanHeader(obj: unknown): obj is UcanHeader { return util.isRecord(obj) && util.hasProp(obj, "alg") && typeof obj.alg === "string" @@ -89,22 +121,7 @@ export function isUcanPayload(obj: unknown): obj is UcanPayload { && 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(isCapability) + && 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 isCapability(obj: unknown): obj is Capability { - return util.isRecord(obj) && util.hasProp(obj, "cap") && typeof obj.cap === "string" -} - -export function isAvailableCryptoKeyPair(keypair: CryptoKeyPair): keypair is AvailableCryptoKeyPair { - return keypair.publicKey != null && keypair.privateKey != null -} - -export function isKeypair(obj: unknown): obj is Keypair { - return util.isRecord(obj) - && util.hasProp(obj, "publicKey") && obj.publicKey instanceof Uint8Array - && util.hasProp(obj, "keyType") && typeof obj.keyType === "string" - && util.hasProp(obj, "sign") && typeof obj.sign === "function" -} diff --git a/src/util.ts b/src/util.ts index e529485..d2285b0 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,9 +1,10 @@ const CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + export const generateNonce = (len = 6): string => { let nonce = "" - for (let i=0; i < len; i++) { - nonce += CHARS[Math.floor(Math.random() * CHARS.length)] + for (let i = 0; i < len; i++) { + nonce += CHARS[ Math.floor(Math.random() * CHARS.length) ] } return nonce } diff --git a/tests/attenuation.test.ts b/tests/attenuation.test.ts index cacbcc9..e54757b 100644 --- a/tests/attenuation.test.ts +++ b/tests/attenuation.test.ts @@ -2,7 +2,7 @@ import { Chained } from "../src/chained" import * as token from "../src/token" import { alice, bob, mallory } from "./fixtures" -import { emailCapabilities } from "./emailCapabilities" +import { emailCapabilities, emailCapability } from "./capability/email" import { maxNbf } from "./utils" @@ -15,34 +15,28 @@ describe("attenuation.emailCapabilities", () => { const leafUcan = await token.build({ issuer: alice, audience: bob.did(), - capabilities: [{ - email: "alice@email.com", - cap: "SEND", - }] + capabilities: [ emailCapability("alice@email.com") ] }) const ucan = await token.build({ issuer: bob, audience: mallory.did(), - capabilities: [{ - email: "alice@email.com", - cap: "SEND", - }], - proofs: [token.encode(leafUcan)] + capabilities: [ emailCapability("alice@email.com") ], + proofs: [ token.encode(leafUcan) ] }) - const emailCaps = Array.from(emailCapabilities(await Chained.fromToken(token.encode(ucan)))) - expect(emailCaps).toEqual([{ + const emailCaps = Array.from( + emailCapabilities(await Chained.fromToken(token.encode(ucan))) + ) + + expect(emailCaps).toEqual([ { info: { originator: alice.did(), expiresAt: Math.min(leafUcan.payload.exp, ucan.payload.exp), notBefore: maxNbf(leafUcan.payload.nbf, ucan.payload.nbf), }, - capability: { - email: "alice@email.com", - cap: "SEND" - } - }]) + capability: emailCapability("alice@email.com") + } ]) }) it("reports the first issuer in the chain as originator", async () => { @@ -57,25 +51,19 @@ describe("attenuation.emailCapabilities", () => { const ucan = await token.build({ issuer: bob, audience: mallory.did(), - capabilities: [{ - email: "bob@email.com", - cap: "SEND", - }], - proofs: [token.encode(leafUcan)] + capabilities: [ emailCapability("bob@email.com") ], + proofs: [ token.encode(leafUcan) ] }) // we implicitly expect the originator to become bob - expect(Array.from(emailCapabilities(await Chained.fromToken(token.encode(ucan))))).toEqual([{ + expect(Array.from(emailCapabilities(await Chained.fromToken(token.encode(ucan))))).toEqual([ { info: { originator: bob.did(), expiresAt: ucan.payload.exp, notBefore: ucan.payload.nbf, }, - capability: { - email: "bob@email.com", - cap: "SEND" - } - }]) + capability: emailCapability("bob@email.com"), + } ]) }) it("finds the right proof chain for the originator", async () => { @@ -85,35 +73,23 @@ describe("attenuation.emailCapabilities", () => { const leafUcanAlice = await token.build({ issuer: alice, audience: mallory.did(), - capabilities: [{ - email: "alice@email.com", - cap: "SEND", - }] + capabilities: [ emailCapability("alice@email.com") ] }) const leafUcanBob = await token.build({ issuer: bob, audience: mallory.did(), - capabilities: [{ - email: "bob@email.com", - cap: "SEND", - }] + capabilities: [ emailCapability("bob@email.com") ] }) const ucan = await token.build({ issuer: mallory, audience: alice.did(), capabilities: [ - { - email: "alice@email.com", - cap: "SEND", - }, - { - email: "bob@email.com", - cap: "SEND", - } + emailCapability("alice@email.com"), + emailCapability("bob@email.com") ], - proofs: [token.encode(leafUcanAlice), token.encode(leafUcanBob)] + proofs: [ token.encode(leafUcanAlice), token.encode(leafUcanBob) ] }) const chained = await Chained.fromToken(token.encode(ucan)) @@ -125,10 +101,7 @@ describe("attenuation.emailCapabilities", () => { expiresAt: Math.min(leafUcanAlice.payload.exp, ucan.payload.exp), notBefore: maxNbf(leafUcanAlice.payload.nbf, ucan.payload.nbf), }, - capability: { - email: "alice@email.com", - cap: "SEND", - } + capability: emailCapability("alice@email.com") }, { info: { @@ -136,10 +109,7 @@ describe("attenuation.emailCapabilities", () => { expiresAt: Math.min(leafUcanBob.payload.exp, ucan.payload.exp), notBefore: maxNbf(leafUcanBob.payload.nbf, ucan.payload.nbf), }, - capability: { - email: "bob@email.com", - cap: "SEND", - } + capability: emailCapability("bob@email.com") } ]) }) @@ -150,28 +120,25 @@ describe("attenuation.emailCapabilities", () => { // and both grant that capability to mallory // a verifier needs to know both to verify valid email access - const aliceEmail = { - email: "alice@email.com", - cap: "SEND", - } + const aliceEmail = emailCapability("alice@email.com") const leafUcanAlice = await token.build({ issuer: alice, audience: mallory.did(), - capabilities: [aliceEmail] + capabilities: [ aliceEmail ] }) const leafUcanBob = await token.build({ issuer: bob, audience: mallory.did(), - capabilities: [aliceEmail] + capabilities: [ aliceEmail ] }) const ucan = await token.build({ issuer: mallory, audience: alice.did(), - capabilities: [aliceEmail], - proofs: [token.encode(leafUcanAlice), token.encode(leafUcanBob)] + capabilities: [ aliceEmail ], + proofs: [ token.encode(leafUcanAlice), token.encode(leafUcanBob) ] }) const chained = await Chained.fromToken(token.encode(ucan)) @@ -183,10 +150,7 @@ describe("attenuation.emailCapabilities", () => { expiresAt: Math.min(leafUcanAlice.payload.exp, ucan.payload.exp), notBefore: maxNbf(leafUcanAlice.payload.nbf, ucan.payload.nbf), }, - capability: { - email: "alice@email.com", - cap: "SEND", - } + capability: emailCapability("alice@email.com") }, { info: { @@ -194,10 +158,7 @@ describe("attenuation.emailCapabilities", () => { expiresAt: Math.min(leafUcanBob.payload.exp, ucan.payload.exp), notBefore: maxNbf(leafUcanBob.payload.nbf, ucan.payload.nbf), }, - capability: { - email: "alice@email.com", - cap: "SEND", - } + capability: emailCapability("alice@email.com") } ]) }) diff --git a/tests/builder.test.ts b/tests/builder.test.ts index bf924a3..9ff6efb 100644 --- a/tests/builder.test.ts +++ b/tests/builder.test.ts @@ -1,6 +1,7 @@ import { Builder } from "../src/builder" -import { wnfsPublicSemantics } from "../src/capability/wnfs" -import { emailSemantics } from "./emailCapabilities" +import { emailCapability } from "./capability/email" +import { wnfsCapability, wnfsPublicSemantics } from "./capability/wnfs" +import { EMAIL_SEMANTICS } from "./capability/email" import { alice, bob, mallory } from "./fixtures" @@ -9,8 +10,8 @@ describe("Builder", () => { it("builds with a simple example", async () => { const fact1 = { test: true } const fact2 = { preimage: "abc", hash: "sth" } - const cap1 = { email: "alice@email.com", cap: "SEND" } - const cap2 = { wnfs: "alice.fission.name/public/", cap: "SUPER_USER" } + const cap1 = emailCapability("alice@email.com") + const cap2 = wnfsCapability("alice.fission.name/public/", "SUPER_USER") const expiration = Math.floor(Date.now() / 1000) + 30 const notBefore = Math.floor(Date.now() / 1000) - 30 @@ -28,8 +29,8 @@ describe("Builder", () => { expect(ucan.audience()).toEqual(bob.did()) expect(ucan.expiresAt()).toEqual(expiration) expect(ucan.notBefore()).toEqual(notBefore) - expect(ucan.facts()).toEqual([fact1, fact2]) - expect(ucan.attenuation()).toEqual([cap1, cap2]) + expect(ucan.facts()).toEqual([ fact1, fact2 ]) + expect(ucan.attenuation()).toEqual([ cap1, cap2 ]) expect(ucan.nonce()).toBeDefined() }) @@ -48,18 +49,18 @@ describe("Builder", () => { .issuedBy(alice) .toAudience(bob.did()) .withLifetimeInSeconds(30) - .claimCapability({ wnfs: "alice.fission.name/public/", cap: "SUPER_USER" }) + .claimCapability(wnfsCapability("alice.fission.name/public/", "SUPER_USER")) .build() const payload = Builder.create() .issuedBy(bob) .toAudience(mallory.did()) .withLifetimeInSeconds(30) - .delegateCapability(wnfsPublicSemantics, { wnfs: "alice.fission.name/public/Apps", cap: "CREATE" }, ucan) - .delegateCapability(wnfsPublicSemantics, { wnfs: "alice.fission.name/public/Documents", cap: "OVERWRITE" }, ucan) + .delegateCapability(wnfsPublicSemantics, wnfsCapability("alice.fission.name/public/Apps", "CREATE"), ucan) + .delegateCapability(wnfsPublicSemantics, wnfsCapability("alice.fission.name/public/Documents", "OVERWRITE"), ucan) .buildPayload() - expect(payload.prf).toEqual([ucan.encoded()]) + expect(payload.prf).toEqual([ ucan.encoded() ]) }) it("throws when it's not ready to be built", () => { @@ -95,7 +96,7 @@ describe("Builder", () => { .issuedBy(alice) .toAudience(bob.did()) .withLifetimeInSeconds(30) - .claimCapability({ email: "alice@email.com", cap: "SEND" }) + .claimCapability(emailCapability("alice@email.com")) .build() expect(() => { @@ -103,7 +104,7 @@ describe("Builder", () => { .issuedBy(bob) .toAudience(mallory.did()) .withLifetimeInSeconds(30) - .delegateCapability(emailSemantics, { email: "bob@email.com", cap: "SEND" }, ucan) + .delegateCapability(EMAIL_SEMANTICS, emailCapability("bob@email.com"), ucan) .buildPayload() }).toThrow() }) diff --git a/tests/capability/email.ts b/tests/capability/email.ts new file mode 100644 index 0000000..debd60c --- /dev/null +++ b/tests/capability/email.ts @@ -0,0 +1,68 @@ +import { Ability } from "../../src/capability/ability" +import { CapabilityResult } from "../../src/attenuation" +import { Capability } from "../../src/capability" +import { Chained } from "../../src/chained" +import { ResourcePointer } from "../../src/capability/resource-pointer" + +import { capabilities, CapabilityEscalation, CapabilitySemantics } from "../../src/attenuation" + +import * as abilities from "../../src/capability/ability" +import * as resourcePointers from "../../src/capability/resource-pointer" + + +// 🌸 + + +export interface EmailCapability { + with: ResourcePointer + can: Ability +} + + + +// πŸ” + + +export const SEND_ABILITY: Ability = { namespace: "msg", segments: [ "SEND" ] } + + +export const EMAIL_SEMANTICS: CapabilitySemantics = { + + tryParsing(cap: Capability): EmailCapability | null { + if ( + cap.with.scheme === "email" && + abilities.isEqual(cap.can, abilities.parse("msg/SEND")) + ) { + return cap + } + return null + }, + + tryDelegating(parentCap: T, childCap: T): T | null | CapabilityEscalation { + // ability is always "msg/SEND" anyway, so doesn't need to be checked + return resourcePointers.isEqual(childCap.with, parentCap.with) ? childCap : null + }, + +} + + + +// πŸ›  + + +export function emailResourcePointer(emailAddress: string): ResourcePointer { + return { scheme: "email", hierPart: emailAddress } +} + + +export function emailCapability(emailAddress: string): Capability { + return { + with: emailResourcePointer(emailAddress), + can: SEND_ABILITY + } +} + + +export function emailCapabilities(ucan: Chained): Iterable> { + return capabilities(ucan, EMAIL_SEMANTICS) +} \ No newline at end of file diff --git a/tests/capabilitiy/wnfs.test.ts b/tests/capability/wnfs.test.ts similarity index 65% rename from tests/capabilitiy/wnfs.test.ts rename to tests/capability/wnfs.test.ts index f0c7879..2698611 100644 --- a/tests/capabilitiy/wnfs.test.ts +++ b/tests/capability/wnfs.test.ts @@ -1,7 +1,7 @@ import * as token from "../../src/token" import { Chained } from "../../src/chained" -import { Capability } from "../../src/types" -import { wnfsPrivateCapabilities, wnfsPublicCapabilities } from "../../src/capability/wnfs" +import { Capability } from "../../src/capability" +import { wnfsCapability, wnfsPrivateCapabilities, wnfsPublicCapabilities } from "./wnfs" import { alice, bob, mallory } from "../fixtures" import { maxNbf } from "../utils" @@ -12,14 +12,8 @@ describe("wnfs public capability", () => { it("works with a simple example", async () => { const { leaf, ucan, chain } = await makeSimpleDelegation( - [{ - wnfs: "boris.fission.name/public/Apps/", - cap: "OVERWRITE", - }], - [{ - wnfs: "boris.fission.name/public/Apps/appinator/", - cap: "REVISE", - }] + [ wnfsCapability("//boris.fission.name/public/Apps/", "OVERWRITE") ], + [ wnfsCapability("//boris.fission.name/public/Apps/appinator/", "REVISE") ] ) expect(Array.from(wnfsPublicCapabilities(chain))).toEqual([ @@ -31,8 +25,8 @@ describe("wnfs public capability", () => { }, capability: { user: "boris.fission.name", - publicPath: ["Apps", "appinator"], - cap: "REVISE", + publicPath: [ "Apps", "appinator" ], + ability: "REVISE", } } ]) @@ -40,39 +34,27 @@ describe("wnfs public capability", () => { it("detects capability escalations", async () => { const { chain } = await makeSimpleDelegation( - [{ - wnfs: "boris.fission.name/public/Apps/", - cap: "CREATE", - }], - [{ - wnfs: "boris.fission.name/public/Apps/appinator/", - cap: "OVERWRITE", - }] + [ wnfsCapability("//boris.fission.name/public/Apps/", "CREATE") ], + [ wnfsCapability("//boris.fission.name/public/Apps/appinator/", "OVERWRITE") ] ) - expect(Array.from(wnfsPublicCapabilities(chain))).toEqual([{ + expect(Array.from(wnfsPublicCapabilities(chain))).toEqual([ { escalation: "Capability level escalation", capability: { user: "boris.fission.name", - publicPath: ["Apps", "appinator"], - cap: "OVERWRITE", + publicPath: [ "Apps", "appinator" ], + ability: "OVERWRITE", } - }]) + } ]) }) it("detects capability escalations, even if there's valid capabilities", async () => { const { leaf, ucan, chain } = await makeSimpleDelegation( - [{ - wnfs: "boris.fission.name/public/Apps/", - cap: "CREATE", - },{ - wnfs: "boris.fission.name/public/Apps/", - cap: "SUPER_USER", - }], - [{ - wnfs: "boris.fission.name/public/Apps/appinator/", - cap: "OVERWRITE", - }] + [ wnfsCapability("//boris.fission.name/public/Apps/", "CREATE"), + wnfsCapability("//boris.fission.name/public/Apps/", "SUPER_USER") + ], + [ wnfsCapability("//boris.fission.name/public/Apps/appinator/", "OVERWRITE") + ] ) expect(Array.from(wnfsPublicCapabilities(chain))).toEqual([ @@ -80,8 +62,8 @@ describe("wnfs public capability", () => { escalation: "Capability level escalation", capability: { user: "boris.fission.name", - publicPath: ["Apps", "appinator"], - cap: "OVERWRITE", + publicPath: [ "Apps", "appinator" ], + ability: "OVERWRITE", } }, { @@ -92,8 +74,8 @@ describe("wnfs public capability", () => { }, capability: { user: "boris.fission.name", - publicPath: ["Apps", "appinator"], - cap: "OVERWRITE", + publicPath: [ "Apps", "appinator" ], + ability: "OVERWRITE", } } ]) @@ -105,14 +87,8 @@ describe("wnfs private capability", () => { it("works with a simple example", async () => { const { leaf, ucan, chain } = await makeSimpleDelegation( - [{ - wnfs: "boris.fission.name/private/abc", - cap: "OVERWRITE", - }], - [{ - wnfs: "boris.fission.name/private/def", - cap: "REVISE", - }] + [ wnfsCapability("//boris.fission.name/private/abc", "OVERWRITE") ], + [ wnfsCapability("//boris.fission.name/private/def", "REVISE") ] ) expect(Array.from(wnfsPrivateCapabilities(chain))).toEqual([ @@ -124,8 +100,8 @@ describe("wnfs private capability", () => { }, capability: { user: "boris.fission.name", - requiredINumbers: new Set(["abc", "def"]), - cap: "REVISE", + requiredINumbers: new Set([ "abc", "def" ]), + ability: "REVISE", } } ]) @@ -133,14 +109,8 @@ describe("wnfs private capability", () => { it("detects capability escalations", async () => { const { chain } = await makeSimpleDelegation( - [{ - wnfs: "boris.fission.name/private/abc", - cap: "OVERWRITE", - }], - [{ - wnfs: "boris.fission.name/private/def", - cap: "SUPER_USER", - }] + [ wnfsCapability("//boris.fission.name/private/abc", "OVERWRITE") ], + [ wnfsCapability("//boris.fission.name/private/def", "SUPER_USER") ] ) expect(Array.from(wnfsPrivateCapabilities(chain))).toEqual([ @@ -148,8 +118,8 @@ describe("wnfs private capability", () => { escalation: "Capability level escalation", capability: { user: "boris.fission.name", - requiredINumbers: new Set(["def"]), - cap: "SUPER_USER", + requiredINumbers: new Set([ "def" ]), + ability: "SUPER_USER", } }, ]) @@ -157,19 +127,10 @@ describe("wnfs private capability", () => { it("detects capability escalations, but still returns valid delegations", async () => { const { leaf, ucan, chain } = await makeSimpleDelegation( - [{ - wnfs: "boris.fission.name/private/abc", - cap: "OVERWRITE", - }], + [ wnfsCapability("//boris.fission.name/private/abc", "OVERWRITE") ], [ - { - wnfs: "boris.fission.name/private/def", - cap: "SUPER_USER", - }, - { - wnfs: "boris.fission.name/private/ghi", - cap: "CREATE", - } + wnfsCapability("//boris.fission.name/private/def", "SUPER_USER"), + wnfsCapability("//boris.fission.name/private/ghi", "CREATE") ] ) @@ -178,8 +139,8 @@ describe("wnfs private capability", () => { escalation: "Capability level escalation", capability: { user: "boris.fission.name", - requiredINumbers: new Set(["def"]), - cap: "SUPER_USER", + requiredINumbers: new Set([ "def" ]), + ability: "SUPER_USER", } }, { @@ -190,8 +151,8 @@ describe("wnfs private capability", () => { }, capability: { user: "boris.fission.name", - requiredINumbers: new Set(["abc", "ghi"]), - cap: "CREATE", + requiredINumbers: new Set([ "abc", "ghi" ]), + ability: "CREATE", } } ]) @@ -200,19 +161,10 @@ describe("wnfs private capability", () => { it("lists all possible inumber combinations", async () => { const { leafAlice, leafBob, ucan, chain } = await makeComplexDelegation( { - alice: [{ - wnfs: "boris.fission.name/private/inumalice", - cap: "OVERWRITE", - }], - bob: [{ - wnfs: "boris.fission.name/private/inumbob", - cap: "OVERWRITE", - }] + alice: [ wnfsCapability("//boris.fission.name/private/inumalice", "OVERWRITE") ], + bob: [ wnfsCapability("//boris.fission.name/private/inumbob", "OVERWRITE") ] }, - [{ - wnfs: "boris.fission.name/private/subinum", - cap: "OVERWRITE", - }] + [ wnfsCapability("//boris.fission.name/private/subinum", "OVERWRITE") ] ) expect(Array.from(wnfsPrivateCapabilities(chain))).toEqual([ @@ -224,8 +176,8 @@ describe("wnfs private capability", () => { }, capability: { user: "boris.fission.name", - requiredINumbers: new Set(["inumalice", "subinum"]), - cap: "OVERWRITE", + requiredINumbers: new Set([ "inumalice", "subinum" ]), + ability: "OVERWRITE", } }, { @@ -236,8 +188,8 @@ describe("wnfs private capability", () => { }, capability: { user: "boris.fission.name", - requiredINumbers: new Set(["inumbob", "subinum"]), - cap: "OVERWRITE", + requiredINumbers: new Set([ "inumbob", "subinum" ]), + ability: "OVERWRITE", } } ]) @@ -246,19 +198,10 @@ describe("wnfs private capability", () => { it("lists all possible inumber combinations except escalations", async () => { const { leafBob, ucan, chain } = await makeComplexDelegation( { - alice: [{ - wnfs: "boris.fission.name/private/inumalice", - cap: "CREATE", - }], - bob: [{ - wnfs: "boris.fission.name/private/inumbob", - cap: "OVERWRITE", - }] + alice: [ wnfsCapability("//boris.fission.name/private/inumalice", "CREATE") ], + bob: [ wnfsCapability("//boris.fission.name/private/inumbob", "OVERWRITE") ] }, - [{ - wnfs: "boris.fission.name/private/subinum", - cap: "OVERWRITE", - }] + [ wnfsCapability("//boris.fission.name/private/subinum", "OVERWRITE") ] ) expect(Array.from(wnfsPrivateCapabilities(chain))).toEqual([ @@ -266,8 +209,8 @@ describe("wnfs private capability", () => { escalation: "Capability level escalation", capability: { user: "boris.fission.name", - requiredINumbers: new Set(["subinum"]), - cap: "OVERWRITE", + requiredINumbers: new Set([ "subinum" ]), + ability: "OVERWRITE", } }, { @@ -278,8 +221,8 @@ describe("wnfs private capability", () => { }, capability: { user: "boris.fission.name", - requiredINumbers: new Set(["inumbob", "subinum"]), - cap: "OVERWRITE", + requiredINumbers: new Set([ "inumbob", "subinum" ]), + ability: "OVERWRITE", } } ]) @@ -290,7 +233,7 @@ describe("wnfs private capability", () => { /** * A linear delegation chain: * alice -> bob -> mallory - * + * * The arguments are the capabilities delegated in the first and second arrow, respectively. */ async function makeSimpleDelegation(aliceCapabilities: Capability[], bobCapabilities: Capability[]) { @@ -304,7 +247,7 @@ async function makeSimpleDelegation(aliceCapabilities: Capability[], bobCapabili issuer: bob, audience: mallory.did(), capabilities: bobCapabilities, - proofs: [token.encode(leaf)] + proofs: [ token.encode(leaf) ] }) const chain = await Chained.fromToken(token.encode(ucan)) @@ -316,7 +259,7 @@ async function makeSimpleDelegation(aliceCapabilities: Capability[], bobCapabili /** * A tree-like delegation ucan: * alice & bob => mallory -> alice - * + * * The first argument are the capabilities delegated in the first two arrows, * the second argument are the capabilities delegated in the last arrow. */ @@ -337,7 +280,7 @@ async function makeComplexDelegation(proofs: { alice: Capability[]; bob: Capabil issuer: mallory, audience: alice.did(), capabilities: final, - proofs: [token.encode(leafAlice), token.encode(leafBob)], + proofs: [ token.encode(leafAlice), token.encode(leafBob) ], }) const chain = await Chained.fromToken(token.encode(ucan)) diff --git a/tests/capability/wnfs.ts b/tests/capability/wnfs.ts new file mode 100644 index 0000000..4af1c85 --- /dev/null +++ b/tests/capability/wnfs.ts @@ -0,0 +1,195 @@ +import { Ability, isAbility } from "../../src/capability/ability" +import { Capability } from "../../src/capability" +import { CapabilityEscalation, CapabilitySemantics, capabilities } from "../../src/attenuation" +import { Chained } from "../../src/chained" +import { SUPERUSER } from "../../src/capability/super-user" + + +export const WNFS_ABILITY_LEVELS = { + "SUPER_USER": 0, + "OVERWRITE": -1, + "SOFT_DELETE": -2, + "REVISE": -3, + "CREATE": -4, +} + +export const WNFS_ABILITIES: string[] = Object.keys(WNFS_ABILITY_LEVELS) + +export type WnfsAbility = keyof typeof WNFS_ABILITY_LEVELS + +export function isWnfsCap(cap: Capability): boolean { + return cap.with.scheme === "wnfs" && isWnfsAbility(cap.can) +} + +export function isWnfsAbility(ability: unknown): ability is WnfsAbility { + if (!isAbility(ability)) return false + if (ability === SUPERUSER) return true + const abilitySegment = ability.segments[ 0 ] + const isWnfsAbilitySegment = !!abilitySegment && WNFS_ABILITIES.includes(abilitySegment) + return isWnfsAbilitySegment && ability.namespace.toLowerCase() === "wnfs" +} + +export function wnfsAbilityFromAbility(ability: Ability): WnfsAbility | null { + if (ability === SUPERUSER) return "SUPER_USER" + if (isWnfsAbility(ability)) return ability.segments[ 0 ] as WnfsAbility + return null +} + +export function wnfsCapability(path: string, ability: WnfsAbility): Capability { + return { + with: { scheme: "wnfs", hierPart: path }, + can: { namespace: "wnfs", segments: [ ability ] } + } +} + + + +////////////////////////////// +// Public WNFS Capabilities // +////////////////////////////// + + +export interface WnfsPublicCapability { + user: string // e.g. matheus23.fission.name + publicPath: string[] + ability: WnfsAbility +} + +export const wnfsPublicSemantics: CapabilitySemantics = { + + /** + * Example valid public wnfs capability: + * ```js + * { + * with: { scheme: "wnfs", hierPart: "//boris.fission.name/public/path/to/dir/or/file" }, + * can: { namespace: "wnfs", segments: [ "OVERWRITE" ] } + * } + * ``` + */ + tryParsing(cap: Capability): WnfsPublicCapability | null { + if (!isWnfsCap(cap)) return null + + // remove trailing slash + const path = cap.with.hierPart.replace(/^\/\//, "") + const trimmed = path.endsWith("/") ? path.slice(0, -1) : path + const split = trimmed.split("/") + const user = split[ 0 ] + const publicPath = split.slice(2) // drop first two: matheus23.fission.name/public/keep/this + if (user == null || split[ 1 ] !== "public") return null + + const ability = wnfsAbilityFromAbility(cap.can) + if (!ability) return null + + return { + user, + publicPath, + ability + } + }, + + tryDelegating(parentCap: WnfsPublicCapability, childCap: WnfsPublicCapability): WnfsPublicCapability | null | CapabilityEscalation { + // need to delegate the same user's file system + if (childCap.user !== parentCap.user) return null + + // must not escalate capability level + if (WNFS_ABILITY_LEVELS[ childCap.ability ] > WNFS_ABILITY_LEVELS[ parentCap.ability ]) { + return { + escalation: "Capability level escalation", + capability: childCap, + } + } + + // parentCap path must be a prefix of childCap path + if (childCap.publicPath.length < parentCap.publicPath.length) { + return { + escalation: "WNFS Public path access escalation", + capability: childCap, + } + } + + for (let i = 0; i < parentCap.publicPath.length; i++) { + if (childCap.publicPath[ i ] !== parentCap.publicPath[ i ]) { + return { + escalation: "WNFS Public path access escalation", + capability: childCap, + } + } + } + + return childCap + }, + +} + +export function wnfsPublicCapabilities(ucan: Chained) { + return capabilities(ucan, wnfsPublicSemantics) +} + + + +/////////////////////////////// +// Private WNFS Capabilities // +/////////////////////////////// + + +export interface WnfsPrivateCapability { + user: string + requiredINumbers: Set + ability: WnfsAbility +} + +const wnfsPrivateSemantics: CapabilitySemantics = { + + /** + * Example valid private wnfs capability: + * + * ```js + * { + * with: { scheme: "wnfs", hierPart: "//boris.fission.name/private/fccXmZ8HYmpwxkvPSjwW9A" }, + * can: { namespace: "wnfs", segments: [ "OVERWRITE" ] } + * } + * ``` + */ + tryParsing(cap: Capability): WnfsPrivateCapability | null { + if (!isWnfsCap(cap)) return null + + // split up "boris.fission.name/private/fccXmZ8HYmpwxkvPSjwW9A" into "/private/" + const split = cap.with.hierPart.replace(/^\/\//, "").split("/") + const user = split[ 0 ] + const inumberBase64url = split[ 2 ] + + if (user == null || split[ 1 ] !== "private" || inumberBase64url == null) return null + + const ability = wnfsAbilityFromAbility(cap.can) + if (!ability) return null + + return { + user, + requiredINumbers: new Set([ inumberBase64url ]), + ability + } + }, + + tryDelegating(parentCap: T, childCap: T): T | null | CapabilityEscalation { + // If the users don't match, these capabilities are unrelated. + if (childCap.user !== parentCap.user) return null + + // This escalation *could* be wrong, but we shouldn't assume they're unrelated either. + if (WNFS_ABILITY_LEVELS[ childCap.ability ] > WNFS_ABILITY_LEVELS[ parentCap.ability ]) { + return { + escalation: "Capability level escalation", + capability: childCap, + } + } + + return { + ...childCap, + requiredINumbers: new Set([ ...childCap.requiredINumbers.values(), ...parentCap.requiredINumbers.values() ]), + } + }, + +} + +export function wnfsPrivateCapabilities(ucan: Chained) { + return capabilities(ucan, wnfsPrivateSemantics) +} diff --git a/tests/compatibility.test.ts b/tests/compatibility.test.ts index c00efc0..185623f 100644 --- a/tests/compatibility.test.ts +++ b/tests/compatibility.test.ts @@ -3,27 +3,27 @@ import * as token from "../src/token" const oldUcan = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsInVhdiI6IjEuMC4wIn0.eyJhdWQiOiJkaWQ6a2V5OnoxM1YzU29nMllhVUtoZEdDbWd4OVVadVcxbzFTaEZKWWM2RHZHWWU3TlR0Njg5Tm9MMXRrZUd3NGMydGFQa2dBdWloUjh0cmg2azg2VHRVaTNIR2ZrNEh1NDg3czNiTWY4V1MzWjJoU3VwRktiNmhnV3VwajFIRzhheUxRdDFmeWJSdThjTGdBMkNKanFRYm16YzRFOEFKU0tKeDNndVFYa2F4c3R2Um5RRGN1eDFkZzhVR1BRS3haN2lLeUFKWkFuQlcyWXJUM2o0TVQxdTJNcWZQWG9RYU01WFZQMk04clBFN0FCSEREOXdMbWlKdjkzUUFDRFR5MllnZkVSS3JualNWaTdFb3RNOFR3NHg3M1pNUXJEQnZRRW01Zm9tTWZVaTZVSmJUTmVaaldDTUJQYllNbXRKUDZQZlRpaWZYZG0zdXprVFg5NnExUkVFOExodkU2Rzg2cUR0Wjg5MzdFYUdXdXFpNkRHVDFvc2FRMUVnR3NFN3Jac2JSdDFLNnRXeTZpYktlNTlKZWtnTWFlNW9XNER2IiwiZXhwIjozMjc0NDE5MTQyMywiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6MTNWM1NvZzJZYVVLaGRHQ21neDlVWnVXMW8xU2hGSlljNkR2R1llN05UdDY4OU5vTDJWanZBR2JXdTFrdmZWUWFyVTVWMXBTUnNjOWFwR2h2dDdaODJmUWg1QWE1NW41Zm0zZGs2SnFuTXczZGU4WG91dWZUV2Z1eHpEVkhrSFNGV0sxOW1SWWI4d205d1VwZkxtUWl4QVdtMndFWVZqU2dENEd6YzhVUDlDSjFxMkY4ZXlpVXViMThGbld4Y2djUWhqdXB3OTNxUlMzWDlXUDViemlSYjE4TTZ0Vm8zaUJ4ZUozb2lrRTNaa3RScEtTZDlkcHU5WWNXZFhoeDZDQmY5NTZ1UXhkTDZoTkppNmVMbmZ1eFY2NEhpZU1rZFVoTTJSeThRd3lqZjQ4ZnZWMVhFVU1zeEM5YWFjNEtCcGJONDJHR3U4UmFkRDU3cjZuMWFOc2IyTjU3RkNOYnFIMXVLdHhNTmVHZHJ2QWlUUGRzVjJBRmppczJvN243ajhMNW41YmJ4TFl4VThNVHB3QVphdFpkSiIsIm5iZiI6MTY0MDE5MTQ1NywicHJmIjoiZXlKaGJHY2lPaUpTVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0lzSW5WaGRpSTZJakV1TUM0d0luMC5leUpoZFdRaU9pSmthV1E2YTJWNU9ub3hNMVl6VTI5bk1sbGhWVXRvWkVkRGJXZDRPVlZhZFZjeGJ6RlRhRVpLV1dNMlJIWkhXV1UzVGxSME5qZzVUbTlNTWxacWRrRkhZbGQxTVd0MlpsWlJZWEpWTlZZeGNGTlNjMk01WVhCSGFIWjBOMW80TW1aUmFEVkJZVFUxYmpWbWJUTmthelpLY1c1TmR6TmtaVGhZYjNWMVpsUlhablY0ZWtSV1NHdElVMFpYU3pFNWJWSlpZamgzYlRsM1ZYQm1URzFSYVhoQlYyMHlkMFZaVm1wVFowUTBSM3BqT0ZWUU9VTktNWEV5UmpobGVXbFZkV0l4T0VadVYzaGpaMk5SYUdwMWNIYzVNM0ZTVXpOWU9WZFFOV0o2YVZKaU1UaE5OblJXYnpOcFFuaGxTak52YVd0Rk0xcHJkRkp3UzFOa09XUndkVGxaWTFka1dHaDROa05DWmprMU5uVlJlR1JNTm1oT1NtazJaVXh1Wm5WNFZqWTBTR2xsVFd0a1ZXaE5NbEo1T0ZGM2VXcG1ORGhtZGxZeFdFVlZUWE40UXpsaFlXTTBTMEp3WWs0ME1rZEhkVGhTWVdSRU5UZHlObTR4WVU1ellqSk9OVGRHUTA1aWNVZ3hkVXQwZUUxT1pVZGtjblpCYVZSUVpITldNa0ZHYW1sek1tODNiamRxT0V3MWJqVmlZbmhNV1hoVk9FMVVjSGRCV21GMFdtUktJaXdpWlhod0lqb3pNamMwTkRFNU1UUXlNeXdpWm1OMElqcGJYU3dpYVhOeklqb2laR2xrT210bGVUcDZNVE5XTTFOdlp6SlpZVlZMYUdSSFEyMW5lRGxWV25WWE1XOHhVMmhHU2xsak5rUjJSMWxsTjA1VWREWTRPVTV2VERKaE5VcE9hMlI0VmpabWJYVm9WbU5SWkRkSVIycHhkRXBRYVc1WlZWQTRRMUp4Y21veVkyVm5hVTFyT1RKUlNIazJRbWRXT1hveVVGQnJWMkZZU0RkUlRsQmlRekphZEUxNWFXbGFjWGRLUkVOd05sZG9VbkZVUzJodVFtaENUbWQ1WkRkTFJuUTNjRkkyTkhCa1ZIQjZUbXRNUlZKNGFHNTNUVUZqZURKcVJGZFlOelpDVG5SS04xUTFWVXQ0TTIxcWRHWTBaak0wWjJwVGRUaHJkME5UY0V0alFuQTRWV2RwU0hkdllVSkhkREUxVkZjNVUzQlNXVkoxYUZKdk1tdEljVFZ5Y0ROTmRFSnFSa2QyVUdZeVRsTlpZbUUzTmxoSGJYcFhlVEZyZUZOelEySTVUSGhqTW5welEwdG1lSEF5ZUd0VVFqWmtPVVJDUlVwVE5sUnhXbFo1WkhKU05GWmFNVkE1ZFhJeGRGcHBlbk5qYWtWd1kzVlViV1EzV0VRemRYSjZVelpqY0RSdU1sZHdSbFZNYjNsMk5tOW5ibWxaZEVOSGFUVlVlbWxEY2pKT1FWRjNWMEZYY25CMldVMWllbVEyVmt0a2RUVmpaekZZUWxoTVZFNWhUQ0lzSW01aVppSTZNVFkwTURFNU1UTTJNeXdpY0hSaklqb2lVMVZRUlZKZlZWTkZVaUlzSW5Kell5STZJaW9pZlEuQ0k5SjlOLVhUZUxQNEM5WTktUl9TcEE1aE80dHdpNUQxNFpTR2lwUzdjNS1jTlJWTVItc285Z0JZMlQzSFNaTHFmQ2xyMEtlQVJicFk2TFBwSm1NRGQ1ODdvck1TVVRnMndqN043eUNVeksxSWhOazhQMkQ3RGVlSHNxQ1lsTVotdXpjMHBSbnFJb3dPTWl6MVFkbHZXaTZ0UHNxZkZVYnl4bEx1bXRHdjV1a1hqc1FZcmYzdko3aU5DMkJibWotMGhTV25wNTNBN01TQTllLWFXVGpLUWEwSkpXVVVhWG5XS19CNjRaa3NyTWRXdW5mVFNuSE9lR2o3MFRuSXhieVcxbFhodk5pcnhIUV90ZVlKZ2xIZTRBbldEQXdUa2dnaVotdkp0WUhsYnVwQkt4S1YtNm9OMTlXS3dUT3U3QnpPX2QyUHAtWVVyY1RSSS1KZ0F2NUpnIiwicHRjIjoiU1VQRVJfVVNFUiIsInJzYyI6IioifQ.CRLB4gBBHhnsbfUhLALiCfo6mHnHSlEUczyZsWhh9TNjv9UxdgvsSWQsehGIT4XR0jQeYZo2OhasEVaF-Gtt_qqtUQIrducKngd0qzmpfjVbicsQPKVdJjlcTwm9dqhLSEtL195El0oucLzYdqMEZMf-txEmyhCd_Q8CaNExhAnwN32v1salnO6vrAw33ZJID7ZaFmBleoGUXBHQwnkv9_m_P6Fh-UGIKjaOuNmBkGXGn-4irm-eXrne2OPZCoPjhiaf0xTONu4ROrQQYykG8CppvsSXeiylOFY11Ot0sdAlHGSlyZk1_chJ3ud17K9S-CKWK9NtqiMNcUdQGFnNQQ" -const [header, payload, signature] = oldUcan.split(".").map((x, i) => i < 2 ? JSON.parse(uint8arrays.toString(uint8arrays.fromString(x, "base64url"))) : x) +const [ header, payload, signature ] = oldUcan.split(".").map((x, i) => i < 2 ? JSON.parse(uint8arrays.toString(uint8arrays.fromString(x, "base64url"))) : x) describe("compatibility", () => { - it("allows parsing UCANs with 'uav: 1.0.0' into 'ucv: 0.0.1'", async () => { + it("allows parsing UCANs with 'uav: 1.0.0' into 'ucv: 0.3.0'", async () => { const ucan = await token.validate(oldUcan, { checkIsExpired: false, checkIsTooEarly: false, checkSignature: false }) expect(ucan).toEqual({ header: { alg: header.alg, // "RS256", typ: header.typ, // "JWT", - ucv: "0.0.1" // we translate uav: 1.0.0 to ucv: 0.0.1 + ucv: "0.3.0" // we translate uav: 1.0.0 to ucv: 0.3.0 }, payload: { iss: payload.iss, // "did:key:z13V3Sog2YaUKhdGCmgx9UZuW1o1ShFJYc6DvGYe7NTt689NoL2VjvAGbWu1kvfVQarU5V1pSRsc9apGhvt7Z82fQh5Aa55n5fm3dk6JqnMw3de8XouufTWfuxzDVHkHSFWK19mRYb8wm9wUpfLmQixAWm2wEYVjSgD4Gzc8UP9CJ1q2F8eyiUub18FnWxcgcQhjupw93qRS3X9WP5bziRb18M6tVo3iBxeJ3oikE3ZktRpKSd9dpu9YcWdXhx6CBf956uQxdL6hNJi6eLnfuxV64HieMkdUhM2Ry8Qwyjf48fvV1XEUMsxC9aac4KBpbN42GGu8RadD57r6n1aNsb2N57FCNbqH1uKtxMNeGdrvAiTPdsV2AFjis2o7n7j8L5n5bbxLYxU8MTpwAZatZdJ", aud: payload.aud, // "did:key:z13V3Sog2YaUKhdGCmgx9UZuW1o1ShFJYc6DvGYe7NTt689NoL1tkeGw4c2taPkgAuihR8trh6k86TtUi3HGfk4Hu487s3bMf8WS3Z2hSupFKb6hgWupj1HG8ayLQt1fybRu8cLgA2CJjqQbmzc4E8AJSKJx3guQXkaxstvRnQDcux1dg8UGPQKxZ7iKyAJZAnBW2YrT3j4MT1u2MqfPXoQaM5XVP2M8rPE7ABHDD9wLmiJv93QACDTy2YgfERKrnjSVi7EotM8Tw4x73ZMQrDBvQEm5fomMfUi6UJbTNeZjWCMBPbYMmtJP6PfTiifXdm3uzkTX96q1REE8LhvE6G86qDtZ8937EaGWuqi6DGT1osaQ1EgGsE7rZsbRt1K6tWy6ibKe59JekgMae5oW4Dv", nbf: payload.nbf, // 1640191457, exp: payload.exp, // 32744191423, - att: [{ - rsc: payload.rsc, // "*", - cap: payload.ptc, // "SUPER_USER", - }], + att: [ { + can: payload.rsc, // "*", + with: { scheme: "my", hierPart: payload.rsc } + } ], prf: [ payload.prf, // "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsInVhdiI6IjEuMC4wIn0.eyJhdWQiOiJkaWQ6a2V5OnoxM1YzU29nMllhVUtoZEdDbWd4OVVadVcxbzFTaEZKWWM2RHZHWWU3TlR0Njg5Tm9MMlZqdkFHYld1MWt2ZlZRYXJVNVYxcFNSc2M5YXBHaHZ0N1o4MmZRaDVBYTU1bjVmbTNkazZKcW5NdzNkZThYb3V1ZlRXZnV4ekRWSGtIU0ZXSzE5bVJZYjh3bTl3VXBmTG1RaXhBV20yd0VZVmpTZ0Q0R3pjOFVQOUNKMXEyRjhleWlVdWIxOEZuV3hjZ2NRaGp1cHc5M3FSUzNYOVdQNWJ6aVJiMThNNnRWbzNpQnhlSjNvaWtFM1prdFJwS1NkOWRwdTlZY1dkWGh4NkNCZjk1NnVReGRMNmhOSmk2ZUxuZnV4VjY0SGllTWtkVWhNMlJ5OFF3eWpmNDhmdlYxWEVVTXN4QzlhYWM0S0JwYk40MkdHdThSYWRENTdyNm4xYU5zYjJONTdGQ05icUgxdUt0eE1OZUdkcnZBaVRQZHNWMkFGamlzMm83bjdqOEw1bjViYnhMWXhVOE1UcHdBWmF0WmRKIiwiZXhwIjozMjc0NDE5MTQyMywiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6MTNWM1NvZzJZYVVLaGRHQ21neDlVWnVXMW8xU2hGSlljNkR2R1llN05UdDY4OU5vTDJhNUpOa2R4VjZmbXVoVmNRZDdIR2pxdEpQaW5ZVVA4Q1JxcmoyY2VnaU1rOTJRSHk2QmdWOXoyUFBrV2FYSDdRTlBiQzJadE15aWlacXdKRENwNldoUnFUS2huQmhCTmd5ZDdLRnQ3cFI2NHBkVHB6TmtMRVJ4aG53TUFjeDJqRFdYNzZCTnRKN1Q1VUt4M21qdGY0ZjM0Z2pTdThrd0NTcEtjQnA4VWdpSHdvYUJHdDE1VFc5U3BSWVJ1aFJvMmtIcTVycDNNdEJqRkd2UGYyTlNZYmE3NlhHbXpXeTFreFNzQ2I5THhjMnpzQ0tmeHAyeGtUQjZkOURCRUpTNlRxWlZ5ZHJSNFZaMVA5dXIxdFppenNjakVwY3VUbWQ3WEQzdXJ6UzZjcDRuMldwRlVMb3l2Nm9nbmlZdENHaTVUemlDcjJOQVF3V0FXcnB2WU1iemQ2VktkdTVjZzFYQlhMVE5hTCIsIm5iZiI6MTY0MDE5MTM2MywicHRjIjoiU1VQRVJfVVNFUiIsInJzYyI6IioifQ.CI9J9N-XTeLP4C9Y9-R_SpA5hO4twi5D14ZSGipS7c5-cNRVMR-so9gBY2T3HSZLqfClr0KeARbpY6LPpJmMDd587orMSUTg2wj7N7yCUzK1IhNk8P2D7DeeHsqCYlMZ-uzc0pRnqIowOMiz1QdlvWi6tPsqfFUbyxlLumtGv5ukXjsQYrf3vJ7iNC2Bbmj-0hSWnp53A7MSA9e-aWTjKQa0JJWUUaXnWK_B64ZksrMdWunfTSnHOeGj70TnIxbyW1lXhvNirxHQ_teYJglHe4AnWDAwTkggiZ-vJtYHlbupBKxKV-6oN19WKwTOu7BzO_d2Pp-YUrcTRI-JgAv5Jg", ], diff --git a/tests/emailCapabilities.ts b/tests/emailCapabilities.ts deleted file mode 100644 index 087bbdb..0000000 --- a/tests/emailCapabilities.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { capabilities, CapabilityEscalation, CapabilitySemantics } from "../src/attenuation" -import { Chained } from "../src/chained" -import { Capability } from "../src/types" - - -/* Very simple example capability semantics */ -export interface EmailCapability { - email: string - cap: "SEND" -} - -export const emailSemantics: CapabilitySemantics = { - - tryParsing(cap: Capability): EmailCapability | null { - if (typeof cap.email === "string" && cap.cap === "SEND") { - return { - email: cap.email, - cap: cap.cap, - } - } - return null - }, - - tryDelegating(parentCap: T, childCap: T): T | null | CapabilityEscalation { - // potency is always "SEND" anyway, so doesn't need to be checked - return childCap.email === parentCap.email ? childCap : null - }, - -} - -export function emailCapabilities(ucan: Chained) { - return capabilities(ucan, emailSemantics) -} diff --git a/tests/store.test.ts b/tests/store.test.ts index 3ada73e..ae9cff0 100644 --- a/tests/store.test.ts +++ b/tests/store.test.ts @@ -1,7 +1,7 @@ import { Store } from "../src/store" import { Builder } from "../src/builder" import { alice, bob, mallory } from "./fixtures" -import { wnfsPublicSemantics } from "../src/capability/wnfs" +import { wnfsCapability, wnfsPublicSemantics } from "./capability/wnfs" describe("Store.add", () => { @@ -47,7 +47,7 @@ describe("Store.add", () => { const store = await Store.fromTokens([]) store.add(ucan) store.add(ucan) - expect(store.getByAudience(ucan.audience())).toEqual([ucan]) + expect(store.getByAudience(ucan.audience())).toEqual([ ucan ]) }) }) @@ -67,7 +67,7 @@ describe("Store.findByAudience", () => { .withLifetimeInSeconds(30) .build() - const store = await Store.fromTokens([ucanBob, ucanAlice].map(ucan => ucan.encoded())) + const store = await Store.fromTokens([ ucanBob, ucanAlice ].map(ucan => ucan.encoded())) expect(store.findByAudience(mallory.did(), () => true)).toEqual(null) expect(store.findByAudience(bob.did(), () => true)?.encoded()).toEqual(ucanBob.encoded()) expect(store.findByAudience(alice.did(), () => true)?.encoded()).toEqual(ucanAlice.encoded()) @@ -82,15 +82,15 @@ describe("Store.findWithCapability", () => { .issuedBy(alice) .toAudience(bob.did()) .withLifetimeInSeconds(30) - .claimCapability({ wnfs: "alice.fission.name/public/", cap: "SUPER_USER" }) + .claimCapability(wnfsCapability("alice.fission.name/public/", "SUPER_USER")) .build() - const store = await Store.fromTokens([ucan.encoded()]) + const store = await Store.fromTokens([ ucan.encoded() ]) const result = store.findWithCapability(bob.did(), wnfsPublicSemantics, { user: "alice.fission.name", - publicPath: ["Apps"], - cap: "OVERWRITE", + publicPath: [ "Apps" ], + ability: "OVERWRITE", }, () => true) if (!result.success) { @@ -106,7 +106,7 @@ describe("Store.findWithCapability", () => { .issuedBy(alice) .toAudience(bob.did()) .withLifetimeInSeconds(30) - .claimCapability({ wnfs: "alice.fission.name/public/", cap: "SUPER_USER" }) + .claimCapability(wnfsCapability("alice.fission.name/public/", "SUPER_USER")) .build() const ucanAlice = await Builder.create() @@ -115,12 +115,12 @@ describe("Store.findWithCapability", () => { .withLifetimeInSeconds(30) .build() - const store = await Store.fromTokens([ucanAlice.encoded(), ucanBob.encoded()]) + const store = await Store.fromTokens([ ucanAlice.encoded(), ucanBob.encoded() ]) const result = store.findWithCapability(alice.did(), wnfsPublicSemantics, { user: "alice.fission.name", - publicPath: ["Apps"], - cap: "OVERWRITE", + publicPath: [ "Apps" ], + ability: "OVERWRITE", }, () => true) expect(result.success).toEqual(false) diff --git a/tests/token.test.ts b/tests/token.test.ts index f2a7206..2889193 100644 --- a/tests/token.test.ts +++ b/tests/token.test.ts @@ -55,16 +55,16 @@ describe("token.validate", () => { issuer: alice, capabilities: [ { - "wnfs": "boris.fission.name/public/photos/", - "cap": "OVERWRITE" + "with": { scheme: "wnfs", hierPart: "//boris.fission.name/public/photos/" }, + "can": { namespace: "crud", segments: [ "DELETE" ] } }, { - "wnfs": "boris.fission.name/private/4tZA6S61BSXygmJGGW885odfQwpnR2UgmCaS5CfCuWtEKQdtkRnvKVdZ4q6wBXYTjhewomJWPL2ui3hJqaSodFnKyWiPZWLwzp1h7wLtaVBQqSW4ZFgyYaJScVkBs32BThn6BZBJTmayeoA9hm8XrhTX4CGX5CVCwqvEUvHTSzAwdaR", - "cap": "APPEND" + "with": { scheme: "wnfs", hierPart: "//boris.fission.name/private/84MZ7aqwKn7sNiMGsSbaxsEa6EPnQLoKYbXByxNBrCEr" }, + "can": { namespace: "wnfs", segments: [ "APPEND" ] } }, { - "email": "boris@fission.codes", - "cap": "SEND" + "with": { scheme: "mailto", hierPart: "boris@fission.codes" }, + "can": { namespace: "msg", segments: [ "SEND" ] } } ] }) @@ -130,7 +130,7 @@ describe("token.validate", () => { describe("verifySignatureUtf8", () => { it("works with an example", async () => { - const [header, payload, signature] = token.encode(await token.build({ + const [ header, payload, signature ] = token.encode(await token.build({ issuer: alice, audience: bob.did(), })).split(".") diff --git a/tsconfig.json b/tsconfig.json index 3a6e248..cf6546d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,4 +13,4 @@ "include": [ "src/**/*" ] -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 4628527..e2400cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -691,6 +691,11 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.3.2.tgz#fc8c2825e4ed2142473b4a81064e6e081463d1b3" integrity sha512-eI5Yrz3Qv4KPUa/nSIAi0h+qX0XyewOliug5F2QAtuRg6Kjg6jfmxe1GIwoIRhZspD1A0RP8ANrPwvEXXtRFog== +"@types/semver@^7.3.9": + version "7.3.9" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc" + integrity sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ== + "@types/stack-utils@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"