Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ucan-core refactor #82

Merged
merged 26 commits into from
Jul 22, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/jest.config.js → jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = { // eslint-disable-line
export default {
transform: {
".(ts|tsx)": "ts-jest"
},
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@
"private": true,
"workspaces": [
"packages/*"
]
],
"devDependencies": {
"ts-node": "^10.8.2"
}
}
3 changes: 3 additions & 0 deletions packages/core/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import baseConfig from "../../jest.config"

export default baseConfig
40 changes: 23 additions & 17 deletions packages/core/src/attenuation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as token from "./token.js"
import Plugins from "./plugins.js"
import { Capability } from "./capability/index.js"
import { Ucan } from "./types.js"
import { ResourcePointer } from "./capability/resource-pointer.js"
Expand Down Expand Up @@ -119,19 +120,20 @@ export type OwnershipScope
* out different ways of delegating a capability from the attenuations.
* It also makes it possible to return early if a valid delegation chain has been found.
*/
export async function* delegationChains(
semantics: DelegationSemantics,
ucan: Ucan,
isRevoked: (ucan: Ucan) => Promise<boolean> = async () => false
): AsyncIterable<DelegationChain | Error> {
export const delegationChains = (plugins: Plugins) =>
async function* (
semantics: DelegationSemantics,
ucan: Ucan,
isRevoked: (ucan: Ucan) => Promise<boolean> = async () => false,
): AsyncIterable<DelegationChain | Error> {

if (await isRevoked(ucan)) {
yield new Error(`UCAN Revoked: ${token.encode(ucan)}`)
return
}

yield* capabilitiesFromParenthood(ucan)
yield* capabilitiesFromDelegation(semantics, ucan, isRevoked)
yield* capabilitiesFromDelegation(plugins, semantics, ucan, isRevoked)
}


Expand Down Expand Up @@ -265,14 +267,15 @@ function* capabilitiesFromParenthood(ucan: Ucan): Iterable<DelegationChain> {


async function* capabilitiesFromDelegation(
plugins: Plugins,
semantics: DelegationSemantics,
ucan: Ucan,
isRevoked: (ucan: Ucan) => Promise<boolean>
isRevoked: (ucan: Ucan) => Promise<boolean>,
): AsyncIterable<DelegationChain | Error> {

let proofIndex = 0

for await (const proof of token.validateProofs(ucan)) {
for await (const proof of token.validateProofs(plugins)(ucan)) {
if (proof instanceof Error) {
yield proof
continue
Expand All @@ -283,15 +286,15 @@ async function* capabilitiesFromDelegation(
switch (capability.with.scheme.toLowerCase()) {
case "my": continue // cannot be delegated, only introduced by parenthood.
case "as": {
yield* handleAsDelegation(semantics, capability, ucan, proof, isRevoked)
yield* handleAsDelegation(plugins, semantics, capability, ucan, proof, isRevoked)
break
}
case "prf": {
yield* handlePrfDelegation(semantics, capability, ucan, proof, proofIndex, isRevoked)
yield* handlePrfDelegation(plugins, semantics, capability, ucan, proof, proofIndex, isRevoked)
break
}
default: {
yield* handleNormalDelegation(semantics, capability, ucan, proof, isRevoked)
yield* handleNormalDelegation(plugins, semantics, capability, ucan, proof, isRevoked)
}
}
} catch (e) {
Expand All @@ -313,11 +316,12 @@ async function* capabilitiesFromDelegation(


async function* handleAsDelegation(
plugins: Plugins,
semantics: DelegationSemantics,
capability: Capability,
ucan: Ucan,
proof: Ucan,
isRevoked: (ucan: Ucan) => Promise<boolean>
isRevoked: (ucan: Ucan) => Promise<boolean>,
): AsyncIterable<DelegatedOwnership | Error> {
const split = capability.with.hierPart.split(":")
const scheme = split[ split.length - 1 ]
Expand All @@ -326,7 +330,7 @@ async function* handleAsDelegation(
? SUPERUSER
: { scheme, ability: capability.can }

for await (const delegationChain of delegationChains(semantics, proof, isRevoked)) {
for await (const delegationChain of delegationChains(plugins)(semantics, proof, isRevoked)) {
if (delegationChain instanceof Error) {
yield delegationChain
continue
Expand All @@ -352,12 +356,13 @@ async function* handleAsDelegation(


async function* handlePrfDelegation(
plugins: Plugins,
semantics: DelegationSemantics,
capability: Capability,
ucan: Ucan,
proof: Ucan,
proofIndex: number,
isRevoked: (ucan: Ucan) => Promise<boolean>
isRevoked: (ucan: Ucan) => Promise<boolean>,
): AsyncIterable<DelegatedCapability | Error> {
if (
capability.with.hierPart !== SUPERUSER
Expand All @@ -367,7 +372,7 @@ async function* handlePrfDelegation(
// we only process the delegation if proofIndex === 2
return
}
for await (const delegationChain of delegationChains(semantics, proof, isRevoked)) {
for await (const delegationChain of delegationChains(plugins)(semantics, proof, isRevoked)) {
if (delegationChain instanceof Error) {
yield delegationChain
continue
Expand All @@ -385,13 +390,14 @@ async function* handlePrfDelegation(


async function* handleNormalDelegation(
plugins: Plugins,
semantics: DelegationSemantics,
capability: Capability,
ucan: Ucan,
proof: Ucan,
isRevoked: (ucan: Ucan) => Promise<boolean>
isRevoked: (ucan: Ucan) => Promise<boolean>,
): AsyncIterable<DelegatedCapability | Error> {
for await (const delegationChain of delegationChains(semantics, proof, isRevoked)) {
for await (const delegationChain of delegationChains(plugins)(semantics, proof, isRevoked)) {
if (delegationChain instanceof Error) {
yield delegationChain
continue
Expand Down
52 changes: 29 additions & 23 deletions packages/core/src/builder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as token from "./token.js"
import * as util from "./util.js"
import Plugins from "./plugins.js"

import { Keypair, Fact, UcanPayload, isKeypair, Ucan, DidableKey } from "./types.js"
import { Capability, isCapability } from "./capability/index.js"
Expand Down Expand Up @@ -40,6 +41,21 @@ function isCapabilityLookupCapableState(obj: unknown): obj is CapabilityLookupCa
&& util.hasProp(obj, "expiration") && typeof obj.expiration === "number"
}


/**
* Create an empty builder.
* Before finalising the builder, you need to at least call
* - `issuedBy`
* - `toAudience` and
* - `withLifetimeInSeconds` or `withExpiration`.
* To finalise the builder, call its `build` or `buildPayload` method.
*/

export const createBuilder = (plugins: Plugins) =>
(): Builder<Record<string, never>> => {
return new Builder(plugins, {}, { capabilities: [], facts: [], proofs: [], addNonce: false })
}

/**
* A builder API for UCANs.
*
Expand All @@ -59,26 +75,16 @@ function isCapabilityLookupCapableState(obj: unknown): obj is CapabilityLookupCa
*/
export class Builder<State extends Partial<BuildableState>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not

Suggested change
export class Builder<State extends Partial<BuildableState>> {
export const MkBuilder = (plugins: Plugins) => class Builder<State extends Partial<BuildableState>> {

?

And then there's no need for a private plugins: Plugins member.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Mk- based naming here I'm not too sure nor am I attached to it. It may be simpler to just have it be builder or Builder or similar.


private plugins: Plugins
private state: State // portion of the state that's required to be set before building
private defaultable: DefaultableState // portion of the state that has sensible defaults

private constructor(state: State, defaultable: DefaultableState) {
constructor(plugins: Plugins, state: State, defaultable: DefaultableState) {
this.plugins = plugins
this.state = state
this.defaultable = defaultable
}

/**
* Create an empty builder.
* Before finalising the builder, you need to at least call
* - `issuedBy`
* - `toAudience` and
* - `withLifetimeInSeconds` or `withExpiration`.
* To finalise the builder, call its `build` or `buildPayload` method.
*/
static create(): Builder<Record<string, never>> {
return new Builder({}, { capabilities: [], facts: [], proofs: [], addNonce: false })
}

/**
* @param issuer The issuer as a DID string ("did:key:...").
*
Expand All @@ -88,7 +94,7 @@ export class Builder<State extends Partial<BuildableState>> {
if (!isKeypair(issuer)) {
throw new TypeError(`Expected a Keypair, but got ${issuer}`)
}
return new Builder({ ...this.state, issuer }, this.defaultable)
return new Builder(this.plugins, { ...this.state, issuer }, this.defaultable)
}

/**
Expand All @@ -103,7 +109,7 @@ export class Builder<State extends Partial<BuildableState>> {
if (typeof audience !== "string") {
throw new TypeError(`Expected audience DID as string, but got ${audience}`)
}
return new Builder({ ...this.state, audience }, this.defaultable)
return new Builder(this.plugins, { ...this.state, audience }, this.defaultable)
}

/**
Expand All @@ -130,7 +136,7 @@ export class Builder<State extends Partial<BuildableState>> {
if (this.defaultable.notBefore != null && expiration < this.defaultable.notBefore) {
throw new Error(`Can't set expiration to ${expiration} which is before 'notBefore': ${this.defaultable.notBefore}`)
}
return new Builder({ ...this.state, expiration }, this.defaultable)
return new Builder(this.plugins, { ...this.state, expiration }, this.defaultable)
}

/**
Expand All @@ -143,7 +149,7 @@ export class Builder<State extends Partial<BuildableState>> {
if (util.hasProp(this.state, "expiration") && typeof this.state.expiration === "number" && this.state.expiration < notBeforeTimestamp) {
throw new Error(`Can't set 'notBefore' to ${notBeforeTimestamp} which is after expiration: ${this.state.expiration}`)
}
return new Builder(this.state, { ...this.defaultable, notBefore: notBeforeTimestamp })
return new Builder(this.plugins, this.state, { ...this.defaultable, notBefore: notBeforeTimestamp })
}

/**
Expand All @@ -156,7 +162,7 @@ export class Builder<State extends Partial<BuildableState>> {
if (!util.isRecord(fact) || facts.some(fct => !util.isRecord(fct))) {
throw new TypeError(`Expected fact(s) to be a record, but got ${fact}`)
}
return new Builder(this.state, {
return new Builder(this.plugins, this.state, {
...this.defaultable,
facts: [ ...this.defaultable.facts, fact, ...facts ]
})
Expand All @@ -166,7 +172,7 @@ export class Builder<State extends Partial<BuildableState>> {
* Will ensure that the built UCAN includes a number used once.
*/
withNonce(): Builder<State> {
return new Builder(this.state, { ...this.defaultable, addNonce: true })
return new Builder(this.plugins, this.state, { ...this.defaultable, addNonce: true })
}

/**
Expand All @@ -178,7 +184,7 @@ export class Builder<State extends Partial<BuildableState>> {
if (!isCapability(capability)) {
throw new TypeError(`Expected capability, but got ${JSON.stringify(capability, null, " ")}`)
}
return new Builder(this.state, {
return new Builder(this.plugins, this.state, {
...this.defaultable,
capabilities: [ ...this.defaultable.capabilities, capability, ...capabilities ]
})
Expand Down Expand Up @@ -221,7 +227,7 @@ export class Builder<State extends Partial<BuildableState>> {
if (!capabilityCanBeDelegated(semantics, requiredCapability, proof)) {
throw new Error(`Can't add capability to UCAN: Given proof doesn't give required rights to delegate.`)
}
return new Builder(this.state, {
return new Builder(this.plugins, this.state, {
...this.defaultable,
capabilities: [ ...this.defaultable.capabilities, requiredCapability ],
proofs: this.defaultable.proofs.find(p => token.encode(p) === token.encode(ucan)) == null
Expand All @@ -236,7 +242,7 @@ export class Builder<State extends Partial<BuildableState>> {
if (result != null) {
const ucan = result.ucan
const ucanEncoded = token.encode(ucan)
return new Builder(this.state, {
return new Builder(this.plugins, this.state, {
...this.defaultable,
capabilities: [ ...this.defaultable.capabilities, requiredCapability ],
proofs: this.defaultable.proofs.find(proof => token.encode(proof) === ucanEncoded) == null
Expand Down Expand Up @@ -282,7 +288,7 @@ export class Builder<State extends Partial<BuildableState>> {
throw new Error(`Builder is missing one of the required properties before it can be built: issuer, audience and expiration.`)
}
const payload = this.buildPayload()
return await token.signWithKeypair(payload, this.state.issuer)
return await token.signWithKeypair(this.plugins)(payload, this.state.issuer)
}

}
5 changes: 5 additions & 0 deletions packages/core/src/capability/ability.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Superuser, SUPERUSER } from "./super-user.js"
import * as util from "../util.js"

// RE-EXPORTS


export { Superuser, SUPERUSER }


// 💎

Expand Down
9 changes: 4 additions & 5 deletions packages/core/src/capability/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import * as ability from "./ability.js"
import * as resourcePointer from "./resource-pointer.js"
import * as superUser from "./super-user.js"
import * as util from "../util.js"

import { Ability, isAbility } from "./ability.js"
import { ResourcePointer, isResourcePointer } from "./resource-pointer.js"
import { Superuser, SUPERUSER } from "./super-user.js"
import { Superuser } from "./super-user.js"


// RE-EXPORTS


export { ability, resourcePointer, superUser }
export { ability, resourcePointer }



Expand Down Expand Up @@ -53,15 +52,15 @@ export function isEncodedCapability(obj: unknown): obj is EncodedCapability {
export function as(did: string, resource: Superuser | string): Capability {
return {
with: resourcePointer.as(did, resource),
can: SUPERUSER
can: ability.SUPERUSER
}
}


export function my(resource: Superuser | string): Capability {
return {
with: resourcePointer.my(resource),
can: SUPERUSER
can: ability.SUPERUSER
}
}

Expand Down
36 changes: 35 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import Plugins from "./plugins.js"
import * as token from "./token.js"
import * as verifyLib from "./verify.js"
import * as attenuation from "./attenuation.js"
import * as builder from "./builder.js"
import * as store from "./store.js"

export * from "./attenuation.js"
export * from "./builder.js"
export * from "./store.js"
Expand All @@ -7,5 +14,32 @@ export * from "./verify.js"
export * from "./plugins.js"

export * as capability from "./capability/index.js"
export * as ability from "./capability/ability.js"

export { Capability, EncodedCapability, isCapability } from "./capability/index.js"

export const getPluginInjectedApi = (plugins: Plugins) => {
const build = token.build(plugins)
const sign = token.sign(plugins)
const signWithKeypair = token.signWithKeypair(plugins)
const validate = token.validate(plugins)
const validateProofs = token.validateProofs(plugins)
const verify = verifyLib.verify(plugins)
const createBuilder = builder.createBuilder(plugins)
const storeFromTokens = store.storeFromTokens(plugins)
const emptyStore = store.emptyStore(plugins)
const delegationChains = attenuation.delegationChains(plugins)

export { Capability, EncodedCapability, isCapability } from "./capability/index.js"
return {
build,
sign,
signWithKeypair,
validate,
validateProofs,
verify,
createBuilder,
storeFromTokens,
emptyStore,
delegationChains,
}
}
Loading