Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
238 changes: 238 additions & 0 deletions packages/core/src/invoke.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import * as API from '@ucanto/interface'
import * as DID from '@ipld/dag-ucan/did'
import * as Invocation from './invocation.js'
import * as Delegation from './delegation.js'
import * as Signature from '@ipld/dag-ucan/signature'
import * as DAG from './dag.js'
import * as CBOR from './cbor.js'
import { sha256 } from 'multiformats/hashes/sha2'

/**
* Represents an Invocation builder that can be used to construct an invocation
* that allows us to defer signing and encoding as needed.
*
* @template {API.InstructionModel} Run
* @template {API.SigAlg} SigAlg
* @template {API.Link<API.ReceiptModel>|API.Receipt} Receipt
*/
class InvocationBuilder {
/**
* @param {object} options
* @param {API.Signer<API.DID, SigAlg>} options.issuer
* @param {Run} options.run
* @param {API.Meta} [options.meta]
* @param {API.Proof[]} [options.proofs]
* @param {Receipt} [options.cause]
*/
constructor({ issuer, run, cause, meta = {}, proofs = [] }) {
this.issuer = issuer
this.run = run
this.cause = cause || null
this.meta = meta
this.proofs = proofs
}
async buildIPLDView({ hasher = sha256, codec = CBOR } = {}) {
const { meta, proofs, run, issuer, cause } = this
const store = DAG.createStore()

const instruction = await codec.write(run)
DAG.addInto(instruction, store)

const prf = []
// copy blocks from proofs into the store
for (const proof of proofs) {
DAG.addEveryInto(DAG.iterate(proof), store)
prf.push(proof.link())
}

// copy blocks from the receipt into the store
DAG.addEveryInto(DAG.iterate(cause), store)

/** @type {API.TaskModel} */
const task = {
run: instruction.cid,
meta,
prf,
...(cause ? { cause: cause.link() } : {}),
}

const { cid } = await codec.write(task)
const auth = await codec.write(await authorize({ issuer, scope: [cid] }))
DAG.addInto(auth, store)

const invocation = { task, auth: auth.cid }
/** @type {API.Block<API.InvocationModel>} */
const root = await codec.write(invocation)
DAG.addInto(root, store)

return new Invocation({
root: { ...root, data: invocation },
store,
})
}
}

class Invocation {
/**
* @param {object} input
* @param {Required<API.Block<API.InvocationModel>>} input.root
* @param {DAG.BlockStore} input.store
*/
constructor({ root, store }) {
this.root = root
this.store = store
}
get task() {
return this.root.data.task
}
get auth() {
const { cid, bytes } = DAG.get(this.root.data.auth, this.store)
const data = CBOR.decode(bytes)
return new Auth({ root: { cid, bytes, data }, store: this.store })
}
}

/**
* @template {API.InstructionModel} Run
* @implements {API.Task<Run>}
*/
export class Task {
/**
* @param {object} input
* @param {API.TaskModel<Run>} input.data
* @param {DAG.BlockStore} input.store
*/
constructor({ data, store }) {
this.data = data
this.store = store
}

get run() {
return this.data.run
}
get meta() {
return this.data.meta
}
get prf() {
return this.data.prf
}

get proofs() {
if (this._proofs) {
return this._proofs
} else {
/** @type {API.Proof[]} */
const proofs = []
const { store } = this
// Iterate over proof links and materialize Delegation views.
for (const link of this.prf) {
const proof = Delegation.view({ root: link, blocks: store }, link)
proofs.push(proof)
}

// we cache result of this computation as this property may get accessed
// more than once.
this._proofs = proofs
return proofs
}
}

get instruction() {
/** @type {API.Block<Run>|null} */
const block = DAG.get(this.run, this.store, null)
if (block === null) {
return this.run
} else {
return new Instruction({
root: {
...block,
data: CBOR.decode(block.bytes),
},
store: this.store,
})
}
}
}

/**
* @template {API.Ability} Op
* @template {API.Resource} URI
* @template {Record<string, unknown>} Input
* @implements {API.Instruction<Op, URI, Input>}
*/
export class Instruction {
/**
* @param {object} input
* @param {Required<API.Block<API.InstructionModel<Op, URI, Input>>>} input.root
* @param {DAG.BlockStore} input.store
*/
constructor({ root, store }) {
this.root = root
this.store = store
}
link() {
return this.root.cid
}

*iterateIPLDBlocks() {
yield this.root
}
/**
* @type {Op}
*/
get op() {
return this.root.data.op
}
get rsc() {
return this.root.data.rsc
}
/**
* @type {Input}
*/
get input() {
return this.root.data.input
}
get nnc() {
return this.root.data.nnc
}

get nonce() {
return this.root.data.nnc
}
}

class Auth {
/**
* @param {object} input
* @param {Required<API.Block<API.AuthorizationModel>>} input.root
* @param {DAG.BlockStore} input.store
*/
constructor({ root, store }) {
this.root = root
this.store = store
}
get scope() {
return this.root.data.scope
}
get s() {
return this.root.data.s
}
}

/**
* @template {API.SigAlg} SigAlg
* @param {object} options
* @param {API.Signer<API.DID, SigAlg>} options.issuer
* @param {API.Link[]} options.scope
* @returns {Promise<API.AuthorizationModel<SigAlg>>}
*/
const authorize = async ({ issuer, ...source }) => {
const scope = source.scope.sort((a, b) =>
a.toString().localeCompare(b.toString())
)

return {
scope,
s: await issuer.sign(CBOR.encode(scope)),
}
}
85 changes: 85 additions & 0 deletions packages/core/src/task.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// @ts-ignore

import * as API from '@ucanto/interface'
import * as DID from '@ipld/dag-ucan/did'
import * as Invocation from './invocation.js'
import * as Delegation from './delegation.js'
import * as Signature from '@ipld/dag-ucan/signature'
import * as DAG from './dag.js'
import * as CBOR from './cbor.js'
import * as Schema from './schema.js'
import { sha256 } from 'multiformats/hashes/sha2'
import { parseLink } from './lib.js'

const instruction = Schema.struct({
rsc: Schema.string(),
op: Schema.string(),
input: Schema.dictionary({
key: Schema.string(),
value: Schema.unknown(),
}),
nnc: Schema.string(),
})

const effects = Schema.struct({
fork: instruction.link().array(),
join: instruction.link().optional(),
})

const receipt = Schema.struct({
/**
* @type {API.Reader<API.Link>}
*/
get ran() {
return invocation.link()
},
out: Schema.variant({
ok: Schema.unknown(),
error: Schema.unknown(),
}),
fx: effects,
})

const task = Schema.struct({
run: instruction.link(),
meta: Schema.dictionary({
key: Schema.string(),
value: Schema.unknown(),
}),
prf: Schema.unknown().link().array(),

cause: receipt.link().optional(),
})

const auth = Schema.struct({
scope: Schema.unknown().link().array(),
s: Schema.unknown(),
})

const invocation = Schema.struct({
task,
auth: auth.link(),
})

const view = invocation.toIPLDView({
link: parseLink('bakfaa'),
store: new Map(),
})

const auth1 = auth.toIPLDBuilder({
scope: [parseLink('bakfaa')],
s: new Uint8Array(),
})

const demo = async () => {
invocation.toIPLDBuilder({
auth: (await auth1.buildIPLDView()).link(),
task: await task
.toIPLDBuilder({
run: parseLink('bakfaa'),
meta: {},
prf: [parseLink('bakfaa')],
})
.buildIPLDView(),
})
}
37 changes: 37 additions & 0 deletions packages/interface/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,43 @@ export interface InstructionModel<
nnc: string
}

/**
* View over an {@link InstructionModel} DAG, providing some sugar for the API.
*/
export interface Instruction<
Op extends Ability = Ability,
URI extends Resource = Resource,
Input extends Record<string, unknown> = Record<string, unknown>
> extends InstructionModel<Op, URI, Input>,
IPLDView<InstructionModel<Op, URI, Input>> {
readonly nonce: string
}

export interface TaskModel<Run extends InstructionModel = InstructionModel> {
run: Link<Run>
meta: Meta
prf: UCANLink[]

cause?: Link<ReceiptModel>
}

export interface Task<Run extends InstructionModel = InstructionModel>
extends TaskModel<Run> {
instruction: Instruction<Run['op'], Run['rsc'], Run['input']> | Link<Run>

readonly proofs: Proof[]
}

export interface InvocationModel {
task: TaskModel
auth: Link<AuthorizationModel>
}

export interface AuthorizationModel<Alg extends SigAlg = SigAlg> {
scope: Link[]
s: Signature<this['scope'], Alg>
}

/**
* Defines result type as per invocation spec
*
Expand Down