diff --git a/src/api.ts b/src/api.ts index 8a1eccb4..6758a21f 100644 --- a/src/api.ts +++ b/src/api.ts @@ -31,14 +31,6 @@ export type Proof = | UCAN.Proof | Delegation -export interface Instruction { - issuer: UCAN.DID - audience: UCAN.DID - capabilities: [T] - - proofs?: Proof[] -} - export interface Invocation< Capability extends UCAN.Capability = UCAN.Capability > { @@ -136,7 +128,7 @@ export type ServiceInvocations = Invocation & type SubServiceInvocations = { [Key in keyof T]: T[Key] extends ( - input: Instruction + input: Invocation ) => Await> ? Invocation : SubServiceInvocations @@ -157,7 +149,7 @@ export type InvocationService< ? { [Key in Base]: InvocationService } : { [Key in Ability]: ( - input: Instruction + input: Invocation ) => Await> } @@ -167,7 +159,7 @@ export type ExecuteInvocation< Ability extends string = Capability["can"] > = Ability extends `${infer Base}/${infer Path}` ? ExecuteInvocation - : T[Ability] extends (input: Instruction) => infer Out + : T[Ability] extends (input: Invocation) => infer Out ? Out : never @@ -176,14 +168,14 @@ export type Result = | (E & { ok?: false }) type StoreAdd = ( - input: Instruction<{ can: "store/add"; with: UCAN.DID; link: UCAN.Link }> + input: Invocation<{ can: "store/add"; with: UCAN.DID; link: UCAN.Link }> ) => Result< | { status: "done"; with: UCAN.DID; link: UCAN.Link } | { status: "pending"; with: UCAN.DID; link: UCAN.Link; url: string } > type StoreRemove = ( - input: Instruction<{ can: "store/remove"; with: UCAN.DID; link: UCAN.Link }> + input: Invocation<{ can: "store/remove"; with: UCAN.DID; link: UCAN.Link }> ) => Result type Store = { @@ -232,20 +224,11 @@ export declare function connection(): Connection export type Service = Record< string, - (input: Instruction) => Promise> + (input: Invocation) => Promise> > export type Await = T | PromiseLike | Promise -// export declare function invoke( -// connection: Connection, -// input: In -// ): { -// [Key in keyof T]: T[Key] extends (input: ToInstruction) => infer Out -// ? Out -// : never -// }[keyof T] - export declare function invoke( input: IssuedInvocation ): IssuedInvocationView @@ -332,7 +315,7 @@ export type InstructionHandler< In extends Input = Input, T = unknown, X extends Error = Error -> = (instruction: Instruction) => Result +> = (instruction: Invocation) => Result export declare function query(query: In): Query @@ -347,7 +330,7 @@ export declare function select( selector?: S ): S extends true ? Select : Select -type Match = { +type Match = { [Key in keyof T]: T[Key] extends (input: In) => infer Out ? Out : never }[keyof T] @@ -432,15 +415,13 @@ const q = query({ }) const r2 = q.queryService().store.remove({ - issuer: alice.did(), - audience: bob.did(), - capabilities: [ - { - can: "store/remove", - with: alice.did(), - link: car, - }, - ], + issuer: alice, + audience: bob, + capability: { + can: "store/remove", + with: alice.did(), + link: car, + }, }) const r3 = q.execute(host) diff --git a/src/claim/access.js b/src/claim/access.js new file mode 100644 index 00000000..112d2f66 --- /dev/null +++ b/src/claim/access.js @@ -0,0 +1,22 @@ +import * as API from "./api.js" +import { ok, the } from "../../test/services/util.js" +export * from "./api.js" + +/** + * @template {API.CapabilityView} C + * @param {API.Capability} capability + * @param {API.UCANView} ucan + * @returns {API.Result, API.InvalidClaim>} + */ +export const access = (capability, ucan) => { + return ok({ + ok: the(true), + capability: ucan.capabilities[0], + to: ucan.ucan.audience, + proof: { + by: ucan.ucan.issuer, + granted: [], + proof: null, + }, + }) +} diff --git a/src/claim/api.ts b/src/claim/api.ts index 58b5fe06..894c53b3 100644 --- a/src/claim/api.ts +++ b/src/claim/api.ts @@ -1,8 +1,8 @@ -import * as UCAN from '@ipld/dag-ucan' -import type { Result } from '../api.js' +import * as UCAN from "@ipld/dag-ucan" +import type { Result } from "../api.js" export type { Result } -export * from '@ipld/dag-ucan' +export * from "@ipld/dag-ucan" /** * Function checks if the claimed capability is met by given set of capaibilites. Note that function takes capability @@ -24,7 +24,7 @@ export * from '@ipld/dag-ucan' export declare function claim( capability: C, given: C[] -): Result>, EscalationError[]> +): Result>, ClaimError> /** * Represents succesfully parsed capability. Idea is that user could provide capability parser that UCAN @@ -44,13 +44,17 @@ interface Evidence { capaibilites: C[] } +export interface ClaimError extends Error { + esclacations: EscalationError[] +} + /** * Represents capability escalation and contains non empty set of * contstraint violations. */ export interface EscalationError extends RangeError { - readonly name: 'EscalationError' + readonly name: "EscalationError" /** * claimed capability @@ -72,7 +76,7 @@ export interface EscalationError extends RangeError { * Represents specific constraint violation by the claimed capability. */ export interface ConstraintViolationError extends RangeError { - readonly name: 'ConstraintViolationError' + readonly name: "ConstraintViolationError" /** * Constraint that was violated. */ @@ -101,13 +105,14 @@ export interface Constraint { * Access internally utilized `claim` function and walks up the proof chain until it is able to proove that claim is unfounded. */ -declare function access( +export declare function access( capability: UCAN.Capability, ucan: UCANView ): Result, InvalidClaim> -interface InvalidClaim { - readonly name: 'InvalidClaim' +export interface InvalidClaim + extends Error { + readonly name: "InvalidClaim" readonly claim: C readonly by: UCAN.DID // I know to is broken english but "from" is too ambigius as it can be "claim from gozala" or "gozala claimed car from robox" @@ -122,7 +127,7 @@ interface InvalidClaim { } interface ExpriedClaim { - readonly name: 'ExpriedClaim' + readonly name: "ExpriedClaim" readonly by: UCAN.DID readonly to: UCAN.DID @@ -132,7 +137,7 @@ interface ExpriedClaim { } interface InactiveClaim { - readonly name: 'InactiveClaim' + readonly name: "InactiveClaim" readonly from: UCAN.DID readonly to: UCAN.DID @@ -142,7 +147,7 @@ interface InactiveClaim { } interface UnfundedClaim { - readonly name: 'UnfundedClaim' + readonly name: "UnfundedClaim" readonly claim: C readonly by: UCAN.DID @@ -150,7 +155,7 @@ interface UnfundedClaim { } interface ViolatingClaim { - readonly name: 'ViolatingClaim' + readonly name: "ViolatingClaim" readonly from: UCAN.DID readonly to: UCAN.DID @@ -158,15 +163,15 @@ interface ViolatingClaim { readonly escalates: EscalationError[] } -interface Access { +export interface Access { ok: true capability: C - to: UCAN.DID + to: UCAN.DIDView proof: Authorization } -interface Authorization { - by: UCAN.DID +export interface Authorization { + by: UCAN.DIDView granted: C[] proof: Authorization | null @@ -185,7 +190,11 @@ export interface CapabilityParser { /** * Returns either succesfully parsed capability or unknown capability back */ - parse(capability: UCAN.Capability): Result + parse(capability: UCAN.Capability): Result +} + +interface UnknownCapability extends Error { + capability: UCAN.Capability } type Time = number diff --git a/test/api.ts b/test/api.ts index a6e2321c..f04d74cf 100644 --- a/test/api.ts +++ b/test/api.ts @@ -171,3 +171,8 @@ export interface DoesNotHasError extends RangeError { export interface UnknownDIDError extends RangeError { name: "UnknownDIDError" } + +export interface InvalidInvocation extends Error { + name: "InvalidInvocation" + link: Link +} diff --git a/test/server.spec.js b/test/server.spec.js index 1f1eb8af..0ba000ef 100644 --- a/test/server.spec.js +++ b/test/server.spec.js @@ -38,34 +38,34 @@ describe("server", () => { const service = { store: { /** - * @param {Client.Instruction} ucan + * @param {Client.Invocation} ucan * @returns {Promise>} */ async add(ucan) { - const [action] = ucan.capabilities - if (action.with === ucan.issuer) { + const { capability } = ucan + if (capability.with === ucan.issuer.did()) { // can do it } else { } return { ok: true, value: { - with: action.with, - link: action.link, + with: capability.with, + link: capability.link, status: "upload", url: "http://localhost:9090/", }, } }, /** - * @param {Client.Instruction} ucan + * @param {Client.Invocation} ucan * @returns {Promise>} */ async remove(ucan) { - const [action] = ucan.capabilities + const { capability } = ucan return { ok: true, - value: action, + value: capability, } }, }, diff --git a/test/service.js b/test/service.js index 9febd672..75ed4313 100644 --- a/test/service.js +++ b/test/service.js @@ -1,5 +1,8 @@ import * as UCAN from "@ipld/dag-ucan" -import * as API from "../src/api.js" +import * as API from "./api.js" +import * as Auth from "../src/claim/access.js" + +import { ok, the } from "./services/util.js" /** * @typedef {{ @@ -27,57 +30,89 @@ import * as API from "../src/api.js" * link: UCAN.Link * }} Remove */ -const service = { - store: { - /** - * @param {API.Instruction} ucan - * @returns {Promise>} - */ - async add(ucan) { - const [action] = ucan.capabilities - if (action.with === ucan.issuer) { - // can do it + +/** + * @typedef {{ + * store: API.StorageProvider + * }} Model + */ + +class StorageService { + /** + * @param {Model} config + */ + constructor(config) { + this.store = config.store + } + /** + * @param {API.Invocation} ucan + * @returns {Promise|Auth.InvalidClaim>} + */ + async add(ucan) { + const { capability } = ucan + const auth = await Auth.access(capability, /** @type {any} */ (ucan)) + if (auth.ok) { + const result = await this.store.add( + capability.with, + capability.link, + /** @type {any} */ (ucan) + ) + if (result.ok) { + if (result.value.status === "in-s3") { + return ok({ + ...capability, + status: the("done"), + }) + } else { + return ok({ + ...capability, + status: the("upload"), + url: "http://localhost:9090/", + }) + } } else { + return result } - return { - ok: true, - value: { - with: action.with, - link: action.link, - status: "upload", - url: "http://localhost:9090/", - }, - } - }, - /** - * @param {API.Instruction} ucan - * @returns {Promise>} - */ - async remove(ucan) { - const [action] = ucan.capabilities - return { - ok: true, - value: action, + } else { + return auth + } + } + /** + * @param {API.Invocation} ucan + * @returns {Promise>} + */ + async remove(ucan) { + const { capability } = ucan + const access = await Auth.access(capability, /** @type {any} */ (ucan)) + if (access.ok) { + const remove = await this.store.remove( + capability.with, + capability.link, + /** @type {any} */ (capability) + ) + if (remove.ok) { + return ok(capability) + } else { + return remove } - }, - }, + } else { + return access + } + } } - -class CarSetProvider { +class Main { /** - * Service will call this once it verified the UCAN to associate link with a - * given DID. Service is unaware if given `DID` is associated with some account - * or not, if it is not `StoreProvider` MUST return `UnauthorizedDIDError`. + * @param {Model} config */ - add( - group: DID, - link: Link, - proof: Proof - ): Result - remove( - group: DID, - link: Link, - ucan: Proof - ): Result + constructor(config) { + this.storage = new StorageService(config) + } } + +/** + * @typedef {Main} Service + * @param {Model} config + * @returns {Service} + */ +export const create = config => new Main(config) diff --git a/test/services/util.js b/test/services/util.js index a99a0993..3d1d52d7 100644 --- a/test/services/util.js +++ b/test/services/util.js @@ -1,12 +1,9 @@ -/** - * @template [T=undefined] - * @param {T} value - */ -export const ok = (value = /** @type {any} */ (undefined)) => ({ - ok: the(true), - value, -}) +export const ok = + /** @type {(...args:Args) => Args extends [T] ? {ok:true, value:T} : {ok:true, value:undefined}}} */ ( + value => (value === undefined ? Ok : { ok: true, value }) + ) +const Ok = { ok: true, value: undefined } /** * @template {string|boolean|number} T * @param {T} value