From 34a9720a5f8bc3236f38d46af36d0cffc81614a4 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Sun, 11 Dec 2022 15:35:47 -0800 Subject: [PATCH 01/17] prototype better invocation API --- package.json | 1 + packages/agent/package.json | 65 ++++++ packages/agent/src/api.js | 0 packages/agent/src/api.ts | 377 ++++++++++++++++++++++++++++++ packages/agent/test/infer.spec.js | 375 +++++++++++++++++++++++++++++ packages/agent/test/test.js | 6 + packages/agent/tsconfig.json | 109 +++++++++ pnpm-lock.yaml | 43 ++++ 8 files changed, 976 insertions(+) create mode 100644 packages/agent/package.json create mode 100644 packages/agent/src/api.js create mode 100644 packages/agent/src/api.ts create mode 100644 packages/agent/test/infer.spec.js create mode 100644 packages/agent/test/test.js create mode 100644 packages/agent/tsconfig.json diff --git a/package.json b/package.json index 6c72138a..6da63b8e 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "type": "module", "workspaces": [ + "packages/agent", "packages/interface", "packages/core", "packages/client", diff --git a/packages/agent/package.json b/packages/agent/package.json new file mode 100644 index 00000000..c65c1b46 --- /dev/null +++ b/packages/agent/package.json @@ -0,0 +1,65 @@ +{ + "name": "@ucanto/agent", + "description": "UCAN Agent", + "version": "4.0.2", + "types": "./dist/src/lib.d.ts", + "main": "./src/lib.js", + "keywords": [ + "UCAN", + "RPC", + "JWT", + "server" + ], + "files": [ + "src", + "dist/src" + ], + "repository": { + "type": "git", + "url": "https://github.com/web3-storage/ucanto.git" + }, + "homepage": "https://github.com/web3-storage/ucanto", + "scripts": { + "test:web": "playwright-test test/**/*.spec.js --cov && nyc report", + "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha test/**/*.spec.js", + "test": "npm run test:node", + "coverage": "c8 --reporter=html mocha test/**/*.spec.js && npm_config_yes=true npx st -d coverage -p 8080", + "check": "tsc --build", + "build": "tsc --build" + }, + "dependencies": { + "@ucanto/core": "^4.0.2", + "@ucanto/interface": "^4.0.2", + "@ucanto/validator": "^4.0.2" + }, + "devDependencies": { + "@types/chai": "^4.3.3", + "@types/chai-subset": "^1.3.3", + "@types/mocha": "^9.1.0", + "@ucanto/principal": "^4.0.2", + "@ucanto/client": "^4.0.2", + "@ucanto/transport": "^4.0.2", + "@web-std/fetch": "^4.1.0", + "@web-std/file": "^3.0.2", + "c8": "^7.11.0", + "chai": "^4.3.6", + "chai-subset": "^1.6.0", + "mocha": "^10.1.0", + "multiformats": "^10.0.2", + "nyc": "^15.1.0", + "playwright-test": "^8.1.1", + "typescript": "^4.8.4" + }, + "exports": { + ".": { + "types": "./dist/src/lib.d.ts", + "import": "./src/lib.js" + }, + "./server": { + "types": "./dist/src/server.d.ts", + "import": "./src/server.js" + } + }, + "type": "module", + "license": "(Apache-2.0 AND MIT)" +} diff --git a/packages/agent/src/api.js b/packages/agent/src/api.js new file mode 100644 index 00000000..e69de29b diff --git a/packages/agent/src/api.ts b/packages/agent/src/api.ts new file mode 100644 index 00000000..4a103283 --- /dev/null +++ b/packages/agent/src/api.ts @@ -0,0 +1,377 @@ +import * as API from '@ucanto/interface' +export * from '@ucanto/interface' +import { + UCAN, + DID, + Link, + Signer, + Delegation, + CapabilityParser, + TheCapabilityParser, + Capability, + InferCaveats, + Match, + Await, + ParsedCapability, + Ability as Can, + InvocationError, + Failure, + Result, + URI, + Caveats, + Verifier, + Reader, + Invocation, + Proof, + IssuedInvocationView, +} from '@ucanto/interface' + +// This is the interface of the module we'll have +export interface AgentModule { + create(options: CreateAgent): Agent + + resource( + resource: Reader, + abilities: Abilities + ): Resource> + + ability< + In, + Out, + Fail extends { error: true } = { error: true; message: string } + >( + input: Reader, + output: Reader> + ): Ability +} + +export interface Ability< + In extends unknown = unknown, + Out extends unknown = unknown, + Fail extends { error: true } = { error: true }, + With extends URI = URI +> { + uri: With + in: Reader + out: Reader> +} + +export interface Resource< + ID extends URI, + Abilities extends ResourceAbilities, + Context extends {} = {} +> { + from(at: At): From + query>(query: Q): Batch + + with(context: CTX): Resource + + provide

>( + provider: P + ): Provider + + and< + ID2 extends URI, + Abilities2 extends ResourceAbilities, + Context2 extends {} + >( + other: Resource + ): Resource +} + +export interface Provider< + ID extends URI = URI, + Abilities extends ResourceAbilities = ResourceAbilities, + Context extends {} = {} +> { + uri: ID + abilities: Abilities + context: Context +} + +type ProviderOf< + Abilities extends ResourceAbilities = ResourceAbilities, + Context extends {} = {} +> = { + [K in keyof Abilities & string]: Abilities[K] extends Reader< + Result & { uri: infer ID } + > + ? (uri: ID, context: Context) => Await> + : Abilities[K] extends Ability + ? (uri: URI, input: In, context: Context) => Await> + : Abilities[K] extends ResourceAbilities + ? ProviderOf + : never +} + +type With< + ID extends URI, + Abilities extends ResourceAbilities = ResourceAbilities +> = { + [K in keyof Abilities & string]: Abilities[K] extends Reader< + Result + > + ? Reader> & { uri: ID } + : Abilities[K] extends Ability + ? Ability + : Abilities[K] extends ResourceAbilities + ? With + : never +} + +type Query< + ID extends URI = URI, + Abilities extends ResourceAbilities = ResourceAbilities +> = + | { + [K: PropertyKey]: + | Selector + | Selection + } + | [ + Selector, + ...Selector[] + ] + +interface Batch { + query: Query + + decode(): { + [K in keyof Q]: Q[K] extends Selector< + infer With, + infer Can, + infer In, + infer Out, + infer Fail + > + ? Result + : Q[K] extends Selection< + infer With, + infer Can, + infer In, + infer Out, + infer Fail, + infer Query + > + ? Result + : never + } +} + +export type From< + At extends URI, + Can extends string, + Abilities extends ResourceAbilities +> = { + [K in keyof Abilities & string]: Abilities[K] extends Reader< + Result + > + ? () => Selector + : Abilities[K] extends Ability + ? (input: Input) => Selector + : Abilities[K] extends ResourceAbilities + ? From + : never +} + +export type Input = + | T + | Selector + | Selection + +export type ResourceAbilities = { + [K: string]: + | Reader> + | Ability + | ResourceAbilities +} + +export interface Selector< + At extends URI, + Can extends API.Ability, + In extends unknown, + Out extends unknown, + Fail extends { error: true } +> { + with: At + can: Can + in: In + + encode(): Uint8Array + + decode(bytes: Uint8Array): Promise> + select>( + query: Q + ): Selection, Fail, Q> + + invoke(options: InvokeOptions): IssuedInvocationView<{ + with: At + can: Can + nb: In + }> +} + +export interface InvokeOptions { + issuer: Signer + audience: API.Principal + + lifetimeInSeconds?: number + expiration?: UCAN.UTCUnixTimestamp + notBefore?: UCAN.UTCUnixTimestamp + + nonce?: UCAN.Nonce + + facts?: UCAN.Fact[] + proofs?: Proof[] +} + +export interface Selection< + At extends URI, + Can extends API.Ability, + In extends unknown, + Out extends unknown, + Fail extends { error: true }, + Query +> extends Selector { + query: Query + + embed(): Promise<{ + link: Link<{ with: At; can: Can; in: In; query: Query }> + }> +} + +export type Select> = { + [K in keyof Query & keyof Out]: Query[K] extends true + ? Out[K] + : Query[K] extends object + ? Select + : never +} + +type QueryFor = Partial<{ + [K in keyof Out]: true | QueryFor +}> + +export interface CreateAgent { + /** + * Signer will be used to sign all the invocation receipts and to check + * principal alignment on incoming delegations. + */ + signer: Signer + /** + * Agent may act on behalf of some other authority e.g. in case of + * web3.storage we'd like to `root -> manager -> worker` chain of + * command if this agents acts as `worker` it will need a delegation + * chain it could use when signing receipts. + * + * @see https://github.com/web3-storage/w3protocol/issues/265 + */ + delegations?: Delegation[] +} + +export interface AgentConnect< + ID extends DID, + Capabilities extends [CapabilityParser, ...CapabilityParser[]] +> { + principal: Verifier + + delegations?: Delegation[] + + capabilities?: Capabilities +} + +/** + * @template ID - DID this agent has + * @template Context - Any additional context agent will hold + */ +export interface Agent< + ID extends DID = DID, + Context extends {} = {}, + Provides = () => never +> { + signer: Signer + context: Context + + connect< + ID extends DID, + Capabilities extends [CapabilityParser, ...CapabilityParser[]] + >( + options: AgentConnect + ): AgentConnection + + /** + * Attaches some context to the agent. + */ + with(context: Ext): Agent + /** + * Initialized agent with a given function which will extend the context with + * a result. + */ + init(start: () => API.Await): Agent + + // provide< + // Can extends ProvidedAbility, + // With extends URI, + // NB extends Caveats, + // Out, + // Problem extends Failure + // >( + // capability: CapabilityParser< + // Match>> + // >, + // handler: ( + // input: HandlerInput< + // ParsedCapability>, + // Context + // > + // ) => Await> + // ): Agent< + // ID, + // Context, + // Provides & + // (( + // input: Capability> + // ) => Await>) + // > + + resource( + resource: Reader, + abilities: Abilities + ): Resource + invoke: Provides +} + +export type QueryEndpoint< + Capabilities extends [CapabilityParser, ...CapabilityParser[]] +> = Capabilities extends [CapabilityParser] ? never : never + +export interface AgentConnection< + ID extends DID, + Capabilities extends [CapabilityParser, ...CapabilityParser[]] +> { + did(): ID +} + +export interface HandlerInput { + capability: T + context: Context + + invocation: API.ServiceInvocation + agent: Agent +} + +export interface Application< + Can extends ProvidedAbility = ProvidedAbility, + With extends URI = URI, + In extends object = {}, + Out extends Result = Result< + unknown, + { error: true } + > +> { + can: Can + with: With + + in: InferCaveats + out: Out +} diff --git a/packages/agent/test/infer.spec.js b/packages/agent/test/infer.spec.js new file mode 100644 index 00000000..9309b7da --- /dev/null +++ b/packages/agent/test/infer.spec.js @@ -0,0 +1,375 @@ +import * as API from '../src/api.js' +import { capability, Schema, DID, URI, Text, Link } from '@ucanto/validator' +import { ed25519 } from '@ucanto/principal' +import { test } from './test.js' +import { CAR } from '@ucanto/transport' + +test('demo', () => + /** + * @param {object} $ + * @param {API.AgentModule} $.Agent + * @param {API.Verifier} $.w3 + * @param {API.Signer} $.me + */ + async ({ Agent, me }) => { + // Can delegate everything!! + const All = capability({ + with: DID.match({}), + can: '*', + }) + + const Store = capability({ + with: DID.match({}), + can: 'store/*', + }) + + const Upload = capability({ + with: DID.match({}), + can: 'upload/*', + }) + + const root = await ed25519.generate() + const w3 = root.withDID('did:web:web3.storage') + + const manager = await ed25519.generate() + + const uploadWorker = await ed25519.generate() + const storeWorker = await ed25519.generate() + + const authorization = await Upload.delegate({ + issuer: manager, + audience: uploadWorker, + with: w3.did(), + expiration: Infinity, + proofs: [ + await All.delegate({ + issuer: w3, + audience: manager, + with: w3.did(), + }), + ], + }) + + const UploadAdd = capability({ + with: DID.match({ method: 'key' }), + can: 'upload/add', + nb: { + root: Link.match({ version: 1 }), + shards: Link.match({ version: 1 }).array().optional(), + }, + }) + + const UploadList = capability({ + with: DID.match({ method: 'key' }), + can: 'upload/list', + nb: { + cursor: Schema.string().optional(), + /** + * Maximum number of items per page. + */ + size: Schema.integer().optional(), + }, + }) + + const uploadAgent = Agent.create({ + signer: uploadWorker, + delegations: [authorization], + }) + + export const service = uploadAgent.connect({ + principal: w3, + capabilities: [Echo, Version], + }) + + + + + + // .init(async () => ({ + // password: 'hello', + // debug: false, + // })) + // .provide(UploadAdd, ({ capability, invocation, context, agent }) => { + + + // agent.delegate({ + // can: 'voucher/redeem', + // with: agent.did() + // }) + + // return null + // }) + // .provide(UploadList, ({ capability, context, agent }) => { + // return { entries: [Link.parse('bafkqaaa')] } + // }) + + + + + const Echo = capability({ + with: DID.match({}), + can: 'test/echo', + nb: { + text: Text, + }, + }) + + const Version = capability({ + with: DID.match({}), + can: 'test/version', + }) + + + + service.invoke({ + can: 'upload/add', + with: + }) + + + + + uploadAgent.invoke() + + const msg = await Echo.invoke({ + issuer: me, + audience: agent.signer, + with: me.did(), + nb: { + text: 'hello', + }, + }).delegate() + + const ver = await Version.delegate({ + issuer: me, + audience: agent.signer, + with: me.did(), + }) + + const echo = await agent.invoke(msg) + + if (!echo.error) { + echo + } + + const v = await agent.invoke({ + can: 'system/info', + with: alice.did(), + }) + + const Send = Agent.with(DID.match({ method: 'key' })).can('inbox/add', { + message: Schema.Text.text(), + }) + + const a = Agent.match(DID.match({ method: 'key ' }), { + // '*': {}, + // foo: { x: 1, f() {} }, + foo: Schema.never(), + 'system/info': Schema.struct({}), + + 'test/echo': Schema.struct({ + msg: Schema.string(), + }), + }) + + a.capabilities + + Send.invoke + }) + +const fail = Schema.struct({ error: true }) + + +/** + * @template T + * @template {{}} [X={message:string}] + * @param {Schema.Reader} ok + * @param {Schema.Reader} error + * @returns {Schema.Schema>} + */ +const result = (ok, error = Schema.struct({ message: Schema.string() })) => Schema.or(/** @type {Schema.Reader} */(ok), fail.and(error)) + +// /** +// * @template In +// * @template Out +// * @template {{error:true}} [Fail={error:true, message:string}] +// * @param {Schema.Reader} input +// * @param {Schema.Reader>} output +// * @returns {API.Ability} +// */ +// const ability = (input, output) => ({ in: input, out: output }) + +test('demo', () => + /** + * @param {object} $ + * @param {API.AgentModule} $.Agent + * @param {API.Verifier} $.w3 + * @param {API.Signer} $.me + */ + async ({ Agent, me, w3 }) => { + + + + + // agent.query({ + // version: agent.from(alice.did()).test.version() + + // }) + // me.did()).select('test/version', ) + + + const version = result(Schema.integer(), Schema.struct({ message: Schema.string() })) + + const source = Agent.resource(DID, { + test: { + version: result(Schema.integer()), + echo: { + in: Schema.string(), + out: result(Schema.struct({ + text: Schema.string(), + version: Schema.integer() + }) + } + }, + }) + + + const CARLink = Link.match({ code: CAR.codec.code, version: 1 }) + + const Root = Schema.struct({ + root: Link + }) + + const Shards = Schema.struct({ + shards: CARLink.array().optional() + }) + + const Upload = Root.and(Shards) + + const Cursor = Schema.struct({ + cursor: Schema.string().optional(), + size: Schema.integer().optional() + }) + + /** + * @template T + * @param {API.Reader} result + */ + const list = (result) => Cursor.and(Schema.struct({ + results: Schema.array(result) + })) + + const UploadProtocol = Agent.resource(URI.match({ protocol: 'did:' }), { + upload: { + add: Agent.ability(Upload, result(Upload)), + remove: Agent.ability(Root, result(Schema.struct({}))), + list: Agent.ability(Cursor, result(list(Upload))) + } + }) + + const UploadAdd = Agent.resource(DID, { + upload: { + add: Agent.ability(Upload, result(Upload)) + } + }) + + resource(DID). + + const ConsoleProtocol = Agent.resource(DID.match({ method: 'key' }), { + console: { + log: Agent.ability(Upload, result(Schema.string())) + } + }) + + + const service = UploadProtocol.and(ConsoleProtocol) + .with({ password: 'secret' }) + .provide({ + upload: { + add: async (uri, upload, context) => { + return upload + }, + remove: (uri, {root}, context) => { + return { root } + }, + list: (uri, cursor, context) => { + return { ...cursor, results: [] } + } + }, + console: { + log: (uri, upload, context) => { + return uri + } + } + }) + + + const alice = UploadProtocol.from('did:key:zAlice') + + + + const add = alice.upload.add({ + root: Link.parse('bafkqaaa') + }) + + const q = UploadProtocol.query({ + a: add, + b: add, + info: alice.console.log(add) + }) + + const tp = UploadProtocol.query([add, add]) + + + + const r = q.decode() + + if (!r.info.error) { + r.info.toLocaleLowerCase() + } + + if (!r.a.error) { + r.a.root + } + if (!r.b.error) { + r.b + } + + // const add = UploadCapabilities.from('did:key:zAlice').upload.add({ + // root: Link.parse('bafkqaaa') + // }) + + // const ver = source.from(me.did()).test.version() + + // const a = source.from('did:key:zAlice') + // const selector = a.test.echo("hello").select({ text: true }) + + // const s = { ...selector} + + // const echo = selector.invoke({ + // issuer: me, + // audience: w3 + // }) + + // const out = await selector.decode(new Uint8Array()) + // if (!out.error) { + // const data = { ...out } + // } + + // const resource = Agent.resource(DID).field( + // 'test/version', + // Schema.unknown().optional(), + // version + // ).field('test/echo', Schema.string().optional(), + // result(Schema.string())) + + // const v = resource.from(me.did()).abilities['test/version'].invoke(null) + // if (!v.error) { + // v + // } + + // const e = resource.from(me.did()).abilities['test/echo'].invoke('hello') + // if (!e.error) { + // e.toLocaleLowerCase() + // } + }) diff --git a/packages/agent/test/test.js b/packages/agent/test/test.js new file mode 100644 index 00000000..dbe6d8ec --- /dev/null +++ b/packages/agent/test/test.js @@ -0,0 +1,6 @@ +import { assert, use } from 'chai' +import subset from 'chai-subset' +use(subset) + +export const test = it +export { assert } diff --git a/packages/agent/tsconfig.json b/packages/agent/tsconfig.json new file mode 100644 index 00000000..3e549c1a --- /dev/null +++ b/packages/agent/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Projects */ + "incremental": true /* Enable incremental compilation */, + "composite": true /* Enable constraints that allow a TypeScript project to be used with project references. */, + // "tsBuildInfoFile": "./dist", /* Specify the folder for .tsbuildinfo incremental compilation files. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ + // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + + /* Modules */ + "module": "ES2020" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */, + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "resolveJsonModule": true, /* Enable importing .json files */ + // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, + "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "declarationMap": true /* Create sourcemaps for d.ts files. */, + "emitDeclarationOnly": true /* Only output d.ts files and not JavaScript files. */, + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist/" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ + // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ + // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src", "test"], + "references": [ + { "path": "../interface" }, + { "path": "../core" }, + { "path": "../transport" }, + { "path": "../client" }, + { "path": "../validator" } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0514e63e..20ae897a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,49 @@ importers: prettier: 2.7.1 typescript: 4.8.3 + packages/agent: + specifiers: + '@types/chai': ^4.3.3 + '@types/chai-subset': ^1.3.3 + '@types/mocha': ^9.1.0 + '@ucanto/client': ^4.0.2 + '@ucanto/core': ^4.0.2 + '@ucanto/interface': ^4.0.2 + '@ucanto/principal': ^4.0.2 + '@ucanto/transport': ^4.0.2 + '@ucanto/validator': ^4.0.2 + '@web-std/fetch': ^4.1.0 + '@web-std/file': ^3.0.2 + c8: ^7.11.0 + chai: ^4.3.6 + chai-subset: ^1.6.0 + mocha: ^10.1.0 + multiformats: ^10.0.2 + nyc: ^15.1.0 + playwright-test: ^8.1.1 + typescript: ^4.8.4 + dependencies: + '@ucanto/core': link:../core + '@ucanto/interface': link:../interface + '@ucanto/validator': link:../validator + devDependencies: + '@types/chai': 4.3.3 + '@types/chai-subset': 1.3.3 + '@types/mocha': 9.1.1 + '@ucanto/client': link:../client + '@ucanto/principal': link:../principal + '@ucanto/transport': link:../transport + '@web-std/fetch': 4.1.0 + '@web-std/file': 3.0.2 + c8: 7.12.0 + chai: 4.3.6 + chai-subset: 1.6.0 + mocha: 10.1.0 + multiformats: 10.0.2 + nyc: 15.1.0 + playwright-test: 8.1.1 + typescript: 4.8.4 + packages/client: specifiers: '@types/chai': ^4.3.3 From b3525c374f009e87ef6ffc6035c452cadf8567e8 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Thu, 15 Dec 2022 21:23:26 -0800 Subject: [PATCH 02/17] chore: update packages --- package.json | 2 +- packages/agent/package.json | 2 +- packages/client/package.json | 2 +- packages/core/package.json | 2 +- packages/interface/package.json | 2 +- packages/principal/package.json | 2 +- packages/server/package.json | 2 +- packages/transport/package.json | 2 +- packages/validator/package.json | 2 +- pnpm-lock.yaml | 158 ++++++++++++++++++++------------ 10 files changed, 107 insertions(+), 69 deletions(-) diff --git a/package.json b/package.json index 6da63b8e..4644a3d2 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "devDependencies": { "mocha": "^10.1.0", "prettier": "2.7.1", - "typescript": "4.8.3" + "typescript": "^4.9.4" }, "prettier": { "trailingComma": "es5", diff --git a/packages/agent/package.json b/packages/agent/package.json index c65c1b46..380bd899 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -48,7 +48,7 @@ "multiformats": "^10.0.2", "nyc": "^15.1.0", "playwright-test": "^8.1.1", - "typescript": "^4.8.4" + "typescript": "^4.9.4" }, "exports": { ".": { diff --git a/packages/client/package.json b/packages/client/package.json index 6dadf2b4..0d4d72a0 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -45,7 +45,7 @@ "mocha": "^10.1.0", "nyc": "^15.1.0", "playwright-test": "^8.1.1", - "typescript": "^4.8.4" + "typescript": "^4.9.4" }, "type": "module", "main": "src/lib.js", diff --git a/packages/core/package.json b/packages/core/package.json index c963e407..502ce579 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -44,7 +44,7 @@ "mocha": "^10.1.0", "nyc": "^15.1.0", "playwright-test": "^8.1.1", - "typescript": "^4.8.4" + "typescript": "^4.9.4" }, "type": "module", "main": "src/lib.js", diff --git a/packages/interface/package.json b/packages/interface/package.json index 22f19c62..f5b1654f 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -27,7 +27,7 @@ "multiformats": "^10.0.2" }, "devDependencies": { - "typescript": "^4.8.4" + "typescript": "^4.9.4" }, "exports": { ".": { diff --git a/packages/principal/package.json b/packages/principal/package.json index 15273ab1..86a1362e 100644 --- a/packages/principal/package.json +++ b/packages/principal/package.json @@ -41,7 +41,7 @@ "mocha": "^10.1.0", "nyc": "^15.1.0", "playwright-test": "^8.1.1", - "typescript": "^4.8.4" + "typescript": "^4.9.4" }, "type": "module", "main": "src/lib.js", diff --git a/packages/server/package.json b/packages/server/package.json index 819fe7de..b8b79a7a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -48,7 +48,7 @@ "multiformats": "^10.0.2", "nyc": "^15.1.0", "playwright-test": "^8.1.1", - "typescript": "^4.8.4" + "typescript": "^4.9.4" }, "exports": { ".": { diff --git a/packages/transport/package.json b/packages/transport/package.json index 68dcd0a2..538831e2 100644 --- a/packages/transport/package.json +++ b/packages/transport/package.json @@ -45,7 +45,7 @@ "mocha": "^10.1.0", "nyc": "^15.1.0", "playwright-test": "^8.1.1", - "typescript": "^4.8.4" + "typescript": "^4.9.4" }, "type": "module", "typesVersions": { diff --git a/packages/validator/package.json b/packages/validator/package.json index 024ea67d..05b621c8 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -45,7 +45,7 @@ "mocha": "^10.1.0", "nyc": "^15.1.0", "playwright-test": "^8.1.1", - "typescript": "^4.8.4" + "typescript": "^4.9.4" }, "type": "module", "main": "src/lib.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20ae897a..d9eaff6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,11 @@ importers: specifiers: mocha: ^10.1.0 prettier: 2.7.1 - typescript: 4.8.3 + typescript: ^4.9.4 devDependencies: mocha: 10.1.0 prettier: 2.7.1 - typescript: 4.8.3 + typescript: 4.9.4 packages/agent: specifiers: @@ -35,18 +35,18 @@ importers: multiformats: ^10.0.2 nyc: ^15.1.0 playwright-test: ^8.1.1 - typescript: ^4.8.4 + typescript: ^4.9.4 dependencies: - '@ucanto/core': link:../core - '@ucanto/interface': link:../interface - '@ucanto/validator': link:../validator + '@ucanto/core': 4.0.3 + '@ucanto/interface': 4.0.3 + '@ucanto/validator': 4.0.3 devDependencies: '@types/chai': 4.3.3 '@types/chai-subset': 1.3.3 '@types/mocha': 9.1.1 - '@ucanto/client': link:../client - '@ucanto/principal': link:../principal - '@ucanto/transport': link:../transport + '@ucanto/client': 4.0.3 + '@ucanto/principal': 4.0.3 + '@ucanto/transport': 4.0.3 '@web-std/fetch': 4.1.0 '@web-std/file': 3.0.2 c8: 7.12.0 @@ -56,7 +56,7 @@ importers: multiformats: 10.0.2 nyc: 15.1.0 playwright-test: 8.1.1 - typescript: 4.8.4 + typescript: 4.9.4 packages/client: specifiers: @@ -74,16 +74,16 @@ importers: multiformats: ^10.0.2 nyc: ^15.1.0 playwright-test: ^8.1.1 - typescript: ^4.8.4 + typescript: ^4.9.4 dependencies: - '@ucanto/interface': link:../interface + '@ucanto/interface': 4.0.3 multiformats: 10.0.2 devDependencies: '@types/chai': 4.3.3 '@types/mocha': 9.1.1 - '@ucanto/core': link:../core - '@ucanto/principal': link:../principal - '@ucanto/transport': link:../transport + '@ucanto/core': 4.0.3 + '@ucanto/principal': 4.0.3 + '@ucanto/transport': 4.0.3 '@web-std/fetch': 4.1.0 '@web-std/file': 3.0.2 c8: 7.12.0 @@ -91,7 +91,7 @@ importers: mocha: 10.1.0 nyc: 15.1.0 playwright-test: 8.1.1 - typescript: 4.8.4 + typescript: 4.9.4 packages/core: specifiers: @@ -108,34 +108,34 @@ importers: multiformats: ^10.0.2 nyc: ^15.1.0 playwright-test: ^8.1.1 - typescript: ^4.8.4 + typescript: ^4.9.4 dependencies: '@ipld/car': 5.0.0 '@ipld/dag-cbor': 8.0.0 '@ipld/dag-ucan': 3.0.1 - '@ucanto/interface': link:../interface + '@ucanto/interface': 4.0.3 multiformats: 10.0.2 devDependencies: '@types/chai': 4.3.3 '@types/mocha': 9.1.1 - '@ucanto/principal': link:../principal + '@ucanto/principal': 4.0.3 c8: 7.12.0 chai: 4.3.6 mocha: 10.1.0 nyc: 15.1.0 playwright-test: 8.1.1 - typescript: 4.8.4 + typescript: 4.9.4 packages/interface: specifiers: '@ipld/dag-ucan': ^3.0.1 multiformats: ^10.0.2 - typescript: ^4.8.4 + typescript: ^4.9.4 dependencies: '@ipld/dag-ucan': 3.0.1 multiformats: 10.0.2 devDependencies: - typescript: 4.8.4 + typescript: 4.9.4 packages/principal: specifiers: @@ -151,11 +151,11 @@ importers: nyc: ^15.1.0 one-webcrypto: ^1.0.3 playwright-test: ^8.1.1 - typescript: ^4.8.4 + typescript: ^4.9.4 dependencies: '@ipld/dag-ucan': 3.0.1 '@noble/ed25519': 1.7.1 - '@ucanto/interface': link:../interface + '@ucanto/interface': 4.0.3 multiformats: 10.0.2 one-webcrypto: 1.0.3 devDependencies: @@ -166,7 +166,7 @@ importers: mocha: 10.1.0 nyc: 15.1.0 playwright-test: 8.1.1 - typescript: 4.8.4 + typescript: 4.9.4 packages/server: specifiers: @@ -188,18 +188,18 @@ importers: multiformats: ^10.0.2 nyc: ^15.1.0 playwright-test: ^8.1.1 - typescript: ^4.8.4 + typescript: ^4.9.4 dependencies: - '@ucanto/core': link:../core - '@ucanto/interface': link:../interface - '@ucanto/validator': link:../validator + '@ucanto/core': 4.0.3 + '@ucanto/interface': 4.0.3 + '@ucanto/validator': 4.0.3 devDependencies: '@types/chai': 4.3.3 '@types/chai-subset': 1.3.3 '@types/mocha': 9.1.1 - '@ucanto/client': link:../client - '@ucanto/principal': link:../principal - '@ucanto/transport': link:../transport + '@ucanto/client': 4.0.3 + '@ucanto/principal': 4.0.3 + '@ucanto/transport': 4.0.3 '@web-std/fetch': 4.1.0 '@web-std/file': 3.0.2 c8: 7.12.0 @@ -209,7 +209,7 @@ importers: multiformats: 10.0.2 nyc: 15.1.0 playwright-test: 8.1.1 - typescript: 4.8.4 + typescript: 4.9.4 packages/transport: specifiers: @@ -227,24 +227,24 @@ importers: multiformats: ^10.0.2 nyc: ^15.1.0 playwright-test: ^8.1.1 - typescript: ^4.8.4 + typescript: ^4.9.4 dependencies: '@ipld/car': 5.0.0 '@ipld/dag-cbor': 8.0.0 - '@ucanto/core': link:../core - '@ucanto/interface': link:../interface + '@ucanto/core': 4.0.3 + '@ucanto/interface': 4.0.3 multiformats: 10.0.2 devDependencies: '@types/chai': 4.3.3 '@types/mocha': 9.1.1 - '@ucanto/principal': link:../principal + '@ucanto/principal': 4.0.3 '@web-std/fetch': 4.1.0 c8: 7.12.0 chai: 4.3.6 mocha: 10.1.0 nyc: 15.1.0 playwright-test: 8.1.1 - typescript: 4.8.4 + typescript: 4.9.4 packages/validator: specifiers: @@ -264,26 +264,26 @@ importers: multiformats: ^10.0.2 nyc: ^15.1.0 playwright-test: ^8.1.1 - typescript: ^4.8.4 + typescript: ^4.9.4 dependencies: '@ipld/car': 5.0.0 '@ipld/dag-cbor': 8.0.0 - '@ucanto/core': link:../core - '@ucanto/interface': link:../interface + '@ucanto/core': 4.0.3 + '@ucanto/interface': 4.0.3 multiformats: 10.0.2 devDependencies: '@types/chai': 4.3.3 '@types/chai-subset': 1.3.3 '@types/mocha': 9.1.1 - '@ucanto/client': link:../client - '@ucanto/principal': link:../principal + '@ucanto/client': 4.0.3 + '@ucanto/principal': 4.0.3 c8: 7.12.0 chai: 4.3.6 chai-subset: 1.6.0 mocha: 10.1.0 nyc: 15.1.0 playwright-test: 8.1.1 - typescript: 4.8.4 + typescript: 4.9.4 packages: @@ -499,7 +499,6 @@ packages: cborg: 1.9.5 multiformats: 10.0.2 varint: 6.0.0 - dev: false /@ipld/dag-cbor/8.0.0: resolution: {integrity: sha512-VfedC21yAD/ZIahcrHTeMcc17kEVRlCmHQl0JY9/Rwbd102v0QcuXtBN8KGH8alNO82S89+H6MM/hxP85P4Veg==} @@ -507,7 +506,6 @@ packages: dependencies: cborg: 1.9.5 multiformats: 10.0.2 - dev: false /@ipld/dag-json/9.0.1: resolution: {integrity: sha512-dL5Xhrk0XXoq3lSsY2LNNraH2Nxx4nlgQwSarl2J3oir2jBDQEiBDW8bjgr30ni8/epdWDhXm5mdxat8dFWwGQ==} @@ -515,7 +513,6 @@ packages: dependencies: cborg: 1.9.5 multiformats: 10.0.2 - dev: false /@ipld/dag-ucan/3.0.1: resolution: {integrity: sha512-71YwJeRHxwX3diPXfwiuzhJTjmJSqi8XW/x5Xglp82UqpM5xwtNojB07VhmDXTZXhKi42bZHyQIOLaca/t9IHw==} @@ -523,7 +520,6 @@ packages: '@ipld/dag-cbor': 8.0.0 '@ipld/dag-json': 9.0.1 multiformats: 10.0.2 - dev: false /@istanbuljs/load-nyc-config/1.1.0: resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} @@ -581,7 +577,6 @@ packages: /@noble/ed25519/1.7.1: resolution: {integrity: sha512-Rk4SkJFaXZiznFyC/t77Q0NKS4FL7TLJJsVG2V2oiEq3kJVeTdxysEe/yRWSpnWMe808XRDJ+VFh5pt/FN5plw==} - dev: false /@nodelib/fs.scandir/2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -630,6 +625,58 @@ packages: resolution: {integrity: sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==} dev: true + /@ucanto/client/4.0.3: + resolution: {integrity: sha512-Kr+6A9VB/m2sFEatEKWzJk7Ccwg7AURdqdFyzhzUGU6YvUe4Z/wcly+yz1hswHmGc77dVPo+b19k2U/jjwnSKA==} + dependencies: + '@ucanto/interface': 4.0.3 + multiformats: 10.0.2 + dev: true + + /@ucanto/core/4.0.3: + resolution: {integrity: sha512-5Uc6vdmKZzlA9NFvAN6BC1Tp1Npz0sepp2up1ZWU4BqArQ0w4U0YMtL9KPdBnL3TDAyDNgS9PgK+vHpjcSoeiQ==} + dependencies: + '@ipld/car': 5.0.0 + '@ipld/dag-cbor': 8.0.0 + '@ipld/dag-ucan': 3.0.1 + '@ucanto/interface': 4.0.3 + multiformats: 10.0.2 + + /@ucanto/interface/4.0.3: + resolution: {integrity: sha512-ip1ZziMUhi9nFm9jPLEDLs8zX4HleYsuHHITH5w8GjST7chbRz1LBSq43A3nMUgea17cuIp+rr7i4QcOSFgXHw==} + dependencies: + '@ipld/dag-ucan': 3.0.1 + multiformats: 10.0.2 + + /@ucanto/principal/4.0.3: + resolution: {integrity: sha512-mR9BTkXWDDSFDCf5gminNeDte/jwurohjFJE8oVfGfUnkzSjYwfm4h5GQ47qeze6xgm17SS5pQwipSvCGHfvkg==} + dependencies: + '@ipld/dag-ucan': 3.0.1 + '@noble/ed25519': 1.7.1 + '@ucanto/interface': 4.0.3 + multiformats: 10.0.2 + one-webcrypto: 1.0.3 + dev: true + + /@ucanto/transport/4.0.3: + resolution: {integrity: sha512-yrJoqoxmMCpPElR+iEb2AKIjUEmM+JGCcM1TZLXVbMlzaAt6ndYDMPajfnh3PBQMk7edIodZi+UxCLKvc8yelg==} + dependencies: + '@ipld/car': 5.0.0 + '@ipld/dag-cbor': 8.0.0 + '@ucanto/core': 4.0.3 + '@ucanto/interface': 4.0.3 + multiformats: 10.0.2 + dev: true + + /@ucanto/validator/4.0.3: + resolution: {integrity: sha512-GLsOIq4R7ixu4D1NMNEJZhOelLPIpd/qtTyOjpxqrrSfsDfOoCsHkSxBy0gTwS/4ZIFMM5sa2LBPJw+ZXobgzw==} + dependencies: + '@ipld/car': 5.0.0 + '@ipld/dag-cbor': 8.0.0 + '@ucanto/core': 4.0.3 + '@ucanto/interface': 4.0.3 + multiformats: 10.0.2 + dev: false + /@web-std/blob/3.0.4: resolution: {integrity: sha512-+dibyiw+uHYK4dX5cJ7HA+gtDAaUUe6JsOryp2ZpAC7h4ICsh49E34JwHoEKPlPvP0llCrNzz45vvD+xX5QDBg==} dependencies: @@ -897,7 +944,6 @@ packages: /cborg/1.9.5: resolution: {integrity: sha512-fLBv8wmqtlXqy1Yu+pHzevAIkW6k2K0ZtMujNzWphLsA34vzzg9BHn+5GmZqOJkSA9V7EMKsWrf6K976c1QMjQ==} hasBin: true - dev: false /chai-subset/1.6.0: resolution: {integrity: sha512-K3d+KmqdS5XKW5DWPd5sgNffL3uxdDe+6GdnJh3AYPhwnBGRY5urfvfcbRtWIvvpz+KxkL9FeBB6MZewLUNwug==} @@ -2385,7 +2431,6 @@ packages: /one-webcrypto/1.0.3: resolution: {integrity: sha512-fu9ywBVBPx0gS9K0etIROTiCkvI5S1TDjFsYFb3rC1ewFxeOqsbzq7aIMBHsYfrTHBcGXJaONXXjTl8B01cW1Q==} - dev: false /onetime/5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} @@ -3012,14 +3057,8 @@ packages: is-typedarray: 1.0.0 dev: true - /typescript/4.8.3: - resolution: {integrity: sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==} - engines: {node: '>=4.2.0'} - hasBin: true - dev: true - - /typescript/4.8.4: - resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==} + /typescript/4.9.4: + resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==} engines: {node: '>=4.2.0'} hasBin: true dev: true @@ -3082,7 +3121,6 @@ packages: /varint/6.0.0: resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} - dev: false /wcwidth/1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} From c536442d9f9eba879e0c2d1b6cabfd7e79fa4344 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Thu, 15 Dec 2022 21:24:10 -0800 Subject: [PATCH 03/17] chore: save current changes --- packages/agent/src/api.ts | 75 ++++++++++++++++++++++++++++--- packages/agent/test/infer.spec.js | 75 +++++++++++++++++++++++++------ 2 files changed, 131 insertions(+), 19 deletions(-) diff --git a/packages/agent/src/api.ts b/packages/agent/src/api.ts index 4a103283..fd1c43f1 100644 --- a/packages/agent/src/api.ts +++ b/packages/agent/src/api.ts @@ -26,6 +26,8 @@ import { IssuedInvocationView, } from '@ucanto/interface' +import { Schema } from '@ucanto/validator' + // This is the interface of the module we'll have export interface AgentModule { create(options: CreateAgent): Agent @@ -33,7 +35,7 @@ export interface AgentModule { resource( resource: Reader, abilities: Abilities - ): Resource> + ): Resource> & ResourceCapabilities ability< In, @@ -56,11 +58,22 @@ export interface Ability< out: Reader> } +export interface CapabilitySchema< + URI extends API.URI = API.URI, + Ability extends API.Ability = API.Ability, + Input extends { [key: string]: Reader } = {} +> extends Schema.StructSchema<{ + can: Reader + with: Reader + nb: Schema.StructSchema + }> {} + export interface Resource< ID extends URI, Abilities extends ResourceAbilities, Context extends {} = {} > { + capabilities: ResourceCapabilities from(at: At): From query>(query: Q): Batch @@ -79,6 +92,50 @@ export interface Resource< ): Resource } +export type ResourceCapabilities< + At extends URI, + Can extends string, + Abilities extends ResourceAbilities +> = { + [K in keyof Abilities & string]: Abilities[K] extends Reader< + Result + > + ? ResourceCapability + : Abilities[K] extends Ability + ? ResourceCapability + : Abilities[K] extends ResourceAbilities + ? ResourceCapabilities + : Abilities[K] +} + +export interface ResourceCapability< + At extends URI, + Can extends API.Ability, + Input extends unknown +> extends ResourceSchema { + can: Can + + delegate>( + uri: At, + input?: In + ): Promise]>> + + invoke( + uri: At, + input: In + ): IssuedInvocationView> +} + +export interface ResourceSchema< + At extends URI, + Can extends API.Ability, + Input extends unknown +> extends Schema.StructSchema<{ + with: Schema.Reader + can: Schema.Reader + input: Schema.Reader + }> {} + export interface Provider< ID extends URI = URI, Abilities extends ResourceAbilities = ResourceAbilities, @@ -179,13 +236,21 @@ export type Input = | Selector | Selection +/** + * Resource abilities is defined as a trees structure where leaves are query + * sources and paths leading to them define ability of the capability. + */ export type ResourceAbilities = { - [K: string]: - | Reader> - | Ability - | ResourceAbilities + [K: string]: Source | ResourceAbilities } +export type Source = + // If query source takes no input it is defined as a reader + | Reader + // If query source takes an input and returns output it is defined + // as ability + | Ability + export interface Selector< At extends URI, Can extends API.Ability, diff --git a/packages/agent/test/infer.spec.js b/packages/agent/test/infer.spec.js index 9309b7da..83b57516 100644 --- a/packages/agent/test/infer.spec.js +++ b/packages/agent/test/infer.spec.js @@ -208,18 +208,6 @@ test('demo', () => */ async ({ Agent, me, w3 }) => { - - - - // agent.query({ - // version: agent.from(alice.did()).test.version() - - // }) - // me.did()).select('test/version', ) - - - const version = result(Schema.integer(), Schema.struct({ message: Schema.string() })) - const source = Agent.resource(DID, { test: { version: result(Schema.integer()), @@ -267,13 +255,34 @@ test('demo', () => } }) + const Base = Agent.resource(URI.match({ protocol: 'did:' }), { + '*': Schema.struct({}), + upload: { + add: Schema.struct({ + root: Schema.Link + }), + info: Schema.struct({}) + } + }) + + const a1 = Base.upload.add.delegate('did:key:zAlice', {}) + const a2 = Base.upload.add.invoke('did:key:zAlice', { root: Link.parse('bafkqaaa')}) + + + + + + + + + const UploadAdd = Agent.resource(DID, { upload: { add: Agent.ability(Upload, result(Upload)) } }) - resource(DID). + const ConsoleProtocol = Agent.resource(DID.match({ method: 'key' }), { console: { @@ -304,7 +313,9 @@ test('demo', () => }) - const alice = UploadProtocol.from('did:key:zAlice') + + + const alice = UploadProtocol.and(ConsoleProtocol).from('did:key:zAlice') @@ -373,3 +384,39 @@ test('demo', () => // e.toLocaleLowerCase() // } }) + + +/** + * @param {object} input + * @param {API.AgentModule} input.Agent + */ +const w3protocol = async ({ Agent }) => { + const Space = DID.match({ method: 'key' }) + + const Add = Schema.struct({ + link: Schema.Link, + size: Schema.integer(), + origin: Schema.Link.optional() + }) + + const AddDone = Schema.struct({ + status: 'done', + with: Space, + link: Schema.Link + }) + + const AddHandOff = Schema.struct({ + status: 'upload', + with: Space, + link: Schema.Link, + url: Schema.URI, + headers: Schema. + }) + + return Agent.resource(Space, { + store: { + _: Schema.struct({}), + add: Agent.ability(Add, ) + } + }) +} From fc98b36963e8b52229a0e8b379175bf0c21e3642 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Fri, 16 Dec 2022 16:16:16 -0800 Subject: [PATCH 04/17] feat: reconcile API --- packages/agent/src/api.ts | 154 ++++---- packages/agent/test/infer.spec.js | 561 ++++++++++-------------------- 2 files changed, 255 insertions(+), 460 deletions(-) diff --git a/packages/agent/src/api.ts b/packages/agent/src/api.ts index fd1c43f1..59854afa 100644 --- a/packages/agent/src/api.ts +++ b/packages/agent/src/api.ts @@ -37,20 +37,32 @@ export interface AgentModule { abilities: Abilities ): Resource> & ResourceCapabilities - ability< + task< In, Out, - Fail extends { error: true } = { error: true; message: string } - >( - input: Reader, - output: Reader> - ): Ability + Fail extends { error: true } = { error: true; message: string }, + Event extends unknown = never + >(options: { + in: Reader + out: Reader> + events?: Reader + }): Task + + task< + Out, + Fail extends { error: true } = { error: true; message: string }, + Event extends unknown = never + >(options: { + out: Reader> + events?: Reader + }): Task } -export interface Ability< +export interface Task< In extends unknown = unknown, Out extends unknown = unknown, Fail extends { error: true } = { error: true }, + Event extends unknown = never, With extends URI = URI > { uri: With @@ -69,13 +81,16 @@ export interface CapabilitySchema< }> {} export interface Resource< - ID extends URI, - Abilities extends ResourceAbilities, + ID extends URI = URI, + Abilities extends ResourceAbilities = ResourceAbilities, Context extends {} = {} > { + id: ID + abilities: Abilities + capabilities: ResourceCapabilities from(at: At): From - query>(query: Q): Batch + query(query: Q): Batch with(context: CTX): Resource @@ -94,20 +109,24 @@ export interface Resource< export type ResourceCapabilities< At extends URI, - Can extends string, + NS extends string, Abilities extends ResourceAbilities > = { [K in keyof Abilities & string]: Abilities[K] extends Reader< Result > - ? ResourceCapability - : Abilities[K] extends Ability - ? ResourceCapability + ? ResourceCapability, In> + : Abilities[K] extends Task + ? ResourceCapability, In> : Abilities[K] extends ResourceAbilities - ? ResourceCapabilities + ? ResourceCapabilities, Abilities[K]> : Abilities[K] } +type ToCan = NS extends '' + ? T + : `${NS extends '_' ? '*' : NS}/${T extends '_' ? '*' : T}` + export interface ResourceCapability< At extends URI, Can extends API.Ability, @@ -151,10 +170,10 @@ type ProviderOf< Context extends {} = {} > = { [K in keyof Abilities & string]: Abilities[K] extends Reader< - Result & { uri: infer ID } - > + Result + > & { uri: infer ID } ? (uri: ID, context: Context) => Await> - : Abilities[K] extends Ability + : Abilities[K] extends Task ? (uri: URI, input: In, context: Context) => Await> : Abilities[K] extends ResourceAbilities ? ProviderOf @@ -169,29 +188,26 @@ type With< Result > ? Reader> & { uri: ID } - : Abilities[K] extends Ability - ? Ability + : Abilities[K] extends Task + ? Task : Abilities[K] extends ResourceAbilities ? With : never } -type Query< - ID extends URI = URI, - Abilities extends ResourceAbilities = ResourceAbilities -> = +type Query = | { [K: PropertyKey]: - | Selector - | Selection + | Selector + | Selection } | [ - Selector, - ...Selector[] + Selector, + ...Selector[] ] -interface Batch { - query: Query +interface Batch { + query: Q decode(): { [K in keyof Q]: Q[K] extends Selector< @@ -224,7 +240,7 @@ export type From< Result > ? () => Selector - : Abilities[K] extends Ability + : Abilities[K] extends Task ? (input: Input) => Selector : Abilities[K] extends ResourceAbilities ? From @@ -249,7 +265,7 @@ export type Source = | Reader // If query source takes an input and returns output it is defined // as ability - | Ability + | Task export interface Selector< At extends URI, @@ -264,7 +280,7 @@ export interface Selector< encode(): Uint8Array - decode(bytes: Uint8Array): Promise> + decode(bytes: Uint8Array): Result select>( query: Q ): Selection, Fail, Q> @@ -310,7 +326,7 @@ export type Select> = { ? Out[K] : Query[K] extends object ? Select - : never + : { debug: Query[K] } } type QueryFor = Partial<{ @@ -323,6 +339,8 @@ export interface CreateAgent { * principal alignment on incoming delegations. */ signer: Signer + + authority?: API.Principal /** * Agent may act on behalf of some other authority e.g. in case of * web3.storage we'd like to `root -> manager -> worker` chain of @@ -336,9 +354,9 @@ export interface CreateAgent { export interface AgentConnect< ID extends DID, - Capabilities extends [CapabilityParser, ...CapabilityParser[]] + Capabilities extends Resource = never > { - principal: Verifier + principal: API.Principal delegations?: Delegation[] @@ -352,15 +370,12 @@ export interface AgentConnect< export interface Agent< ID extends DID = DID, Context extends {} = {}, - Provides = () => never + Abilities extends ResourceAbilities = ResourceAbilities > { signer: Signer context: Context - connect< - ID extends DID, - Capabilities extends [CapabilityParser, ...CapabilityParser[]] - >( + connect( options: AgentConnect ): AgentConnection @@ -374,36 +389,14 @@ export interface Agent< */ init(start: () => API.Await): Agent - // provide< - // Can extends ProvidedAbility, - // With extends URI, - // NB extends Caveats, - // Out, - // Problem extends Failure - // >( - // capability: CapabilityParser< - // Match>> - // >, - // handler: ( - // input: HandlerInput< - // ParsedCapability>, - // Context - // > - // ) => Await> - // ): Agent< - // ID, - // Context, - // Provides & - // (( - // input: Capability> - // ) => Await>) - // > + provide( + capabilities: Resource, + provider: ProviderOf + ): Agent['abilities']> - resource( - resource: Reader, - abilities: Abilities - ): Resource - invoke: Provides + resource(uri: At): From + + query: Resource['query'] } export type QueryEndpoint< @@ -412,9 +405,12 @@ export type QueryEndpoint< export interface AgentConnection< ID extends DID, - Capabilities extends [CapabilityParser, ...CapabilityParser[]] + Capabilities extends Resource > { did(): ID + capabilities: Capabilities + + query: Capabilities['query'] } export interface HandlerInput { @@ -424,19 +420,3 @@ export interface HandlerInput { invocation: API.ServiceInvocation agent: Agent } - -export interface Application< - Can extends ProvidedAbility = ProvidedAbility, - With extends URI = URI, - In extends object = {}, - Out extends Result = Result< - unknown, - { error: true } - > -> { - can: Can - with: With - - in: InferCaveats - out: Out -} diff --git a/packages/agent/test/infer.spec.js b/packages/agent/test/infer.spec.js index 83b57516..34663d94 100644 --- a/packages/agent/test/infer.spec.js +++ b/packages/agent/test/infer.spec.js @@ -1,422 +1,237 @@ import * as API from '../src/api.js' +import { DID as Principal } from '@ucanto/core' import { capability, Schema, DID, URI, Text, Link } from '@ucanto/validator' import { ed25519 } from '@ucanto/principal' -import { test } from './test.js' import { CAR } from '@ucanto/transport' -test('demo', () => - /** - * @param {object} $ - * @param {API.AgentModule} $.Agent - * @param {API.Verifier} $.w3 - * @param {API.Signer} $.me - */ - async ({ Agent, me }) => { - // Can delegate everything!! - const All = capability({ - with: DID.match({}), - can: '*', - }) - - const Store = capability({ - with: DID.match({}), - can: 'store/*', - }) - - const Upload = capability({ - with: DID.match({}), - can: 'upload/*', - }) - - const root = await ed25519.generate() - const w3 = root.withDID('did:web:web3.storage') - - const manager = await ed25519.generate() - - const uploadWorker = await ed25519.generate() - const storeWorker = await ed25519.generate() - - const authorization = await Upload.delegate({ - issuer: manager, - audience: uploadWorker, - with: w3.did(), - expiration: Infinity, - proofs: [ - await All.delegate({ - issuer: w3, - audience: manager, - with: w3.did(), - }), - ], - }) - - const UploadAdd = capability({ - with: DID.match({ method: 'key' }), - can: 'upload/add', - nb: { - root: Link.match({ version: 1 }), - shards: Link.match({ version: 1 }).array().optional(), - }, - }) - - const UploadList = capability({ - with: DID.match({ method: 'key' }), - can: 'upload/list', - nb: { - cursor: Schema.string().optional(), - /** - * Maximum number of items per page. - */ - size: Schema.integer().optional(), - }, - }) - - const uploadAgent = Agent.create({ - signer: uploadWorker, - delegations: [authorization], - }) - - export const service = uploadAgent.connect({ - principal: w3, - capabilities: [Echo, Version], - }) - - - - - - // .init(async () => ({ - // password: 'hello', - // debug: false, - // })) - // .provide(UploadAdd, ({ capability, invocation, context, agent }) => { - - - // agent.delegate({ - // can: 'voucher/redeem', - // with: agent.did() - // }) - - // return null - // }) - // .provide(UploadList, ({ capability, context, agent }) => { - // return { entries: [Link.parse('bafkqaaa')] } - // }) - - - - - const Echo = capability({ - with: DID.match({}), - can: 'test/echo', - nb: { - text: Text, - }, - }) - - const Version = capability({ - with: DID.match({}), - can: 'test/version', - }) - - - - service.invoke({ - can: 'upload/add', - with: - }) - - - - - uploadAgent.invoke() - - const msg = await Echo.invoke({ - issuer: me, - audience: agent.signer, - with: me.did(), - nb: { - text: 'hello', - }, - }).delegate() - - const ver = await Version.delegate({ - issuer: me, - audience: agent.signer, - with: me.did(), - }) - - const echo = await agent.invoke(msg) - - if (!echo.error) { - echo - } - - const v = await agent.invoke({ - can: 'system/info', - with: alice.did(), - }) - - const Send = Agent.with(DID.match({ method: 'key' })).can('inbox/add', { - message: Schema.Text.text(), - }) - - const a = Agent.match(DID.match({ method: 'key ' }), { - // '*': {}, - // foo: { x: 1, f() {} }, - foo: Schema.never(), - 'system/info': Schema.struct({}), - - 'test/echo': Schema.struct({ - msg: Schema.string(), - }), - }) - - a.capabilities - - Send.invoke - }) - const fail = Schema.struct({ error: true }) - /** * @template T * @template {{}} [X={message:string}] * @param {Schema.Reader} ok - * @param {Schema.Reader} error + * @param {Schema.Reader} error * @returns {Schema.Schema>} */ -const result = (ok, error = Schema.struct({ message: Schema.string() })) => Schema.or(/** @type {Schema.Reader} */(ok), fail.and(error)) - -// /** -// * @template In -// * @template Out -// * @template {{error:true}} [Fail={error:true, message:string}] -// * @param {Schema.Reader} input -// * @param {Schema.Reader>} output -// * @returns {API.Ability} -// */ -// const ability = (input, output) => ({ in: input, out: output }) +const result = (ok, error = Schema.struct({ message: Schema.string() })) => + Schema.or( + /** @type {Schema.Reader} */ (ok), + fail.and(error) + ) -test('demo', () => +/** + * @param {object} input + * @param {API.AgentModule} input.Agent + */ +const testW3protocol = async ({ Agent }) => { + const Space = DID.match({ method: 'key' }) /** - * @param {object} $ - * @param {API.AgentModule} $.Agent - * @param {API.Verifier} $.w3 - * @param {API.Signer} $.me + * Schema representing a link (a.k.a CID) to a CAR file. Enforces CAR codec code and CID v1. */ - async ({ Agent, me, w3 }) => { - - const source = Agent.resource(DID, { - test: { - version: result(Schema.integer()), - echo: { - in: Schema.string(), - out: result(Schema.struct({ - text: Schema.string(), - version: Schema.integer() - }) - } - }, - }) + const CARLink = Link.match({ code: CAR.codec.code, version: 1 }) + const Add = Schema.struct({ + link: CARLink, + size: Schema.integer(), + origin: Schema.Link.optional(), + }) - const CARLink = Link.match({ code: CAR.codec.code, version: 1 }) - - const Root = Schema.struct({ - root: Link - }) - - const Shards = Schema.struct({ - shards: CARLink.array().optional() - }) - - const Upload = Root.and(Shards) - - const Cursor = Schema.struct({ - cursor: Schema.string().optional(), - size: Schema.integer().optional() - }) - - /** - * @template T - * @param {API.Reader} result - */ - const list = (result) => Cursor.and(Schema.struct({ - results: Schema.array(result) - })) - - const UploadProtocol = Agent.resource(URI.match({ protocol: 'did:' }), { - upload: { - add: Agent.ability(Upload, result(Upload)), - remove: Agent.ability(Root, result(Schema.struct({}))), - list: Agent.ability(Cursor, result(list(Upload))) - } - }) + const AddDone = Schema.struct({ + status: 'done', + with: Space, + link: CARLink, + }) - const Base = Agent.resource(URI.match({ protocol: 'did:' }), { - '*': Schema.struct({}), - upload: { - add: Schema.struct({ - root: Schema.Link - }), - info: Schema.struct({}) - } - }) + // Should be a dict instead, workaround for now + // https://github.com/web3-storage/ucanto/pull/192 + const headers = Schema.tuple([Schema.string(), Schema.string()]).array() - const a1 = Base.upload.add.delegate('did:key:zAlice', {}) - const a2 = Base.upload.add.invoke('did:key:zAlice', { root: Link.parse('bafkqaaa')}) + const AddHandOff = Schema.struct({ + status: 'upload', + with: Space, + link: CARLink, + url: Schema.URI, + headers, + }) + const SpaceHasNoStorageProvider = Schema.struct({ + name: 'SpaceHasNoStorageProvider', + }) + const ExceedsStorageCapacity = Schema.struct({ + name: 'ExceedsStorageCapacity', + }) + const MalformedCapability = Schema.struct({ + name: 'MalformedCapability', + }) + const InvocationError = Schema.struct({ + name: 'InvocationError', + }) + const AddError = SpaceHasNoStorageProvider.or(ExceedsStorageCapacity) + .or(MalformedCapability) + .or(InvocationError) + const Remove = Schema.struct({ + link: Link, + }) + const Cursor = Schema.struct({ + cursor: Schema.string().optional(), + size: Schema.integer().optional(), + }) + /** + * @template T + * @param {API.Reader} result + */ + const list = result => + Cursor.and( + Schema.struct({ + results: Schema.array(result), + }) + ) + const UploadRoot = Schema.struct({ + root: Schema.Link, + }) + const UploadShards = Schema.struct({ + shards: CARLink.array().optional(), + }) + const Upload = UploadRoot.and(UploadShards) + const Unit = Schema.struct({}) + const store = Agent.resource(Space, { + store: { + _: Unit, + add: Agent.task({ + in: Add, + out: result(AddDone.or(AddHandOff), AddError), + }), + remove: Agent.task({ + in: Remove, + out: result(Remove, MalformedCapability.or(InvocationError)), + }), + list: Agent.task({ + in: Cursor, + out: result(list(Add), InvocationError), + }), + }, + }) - const UploadAdd = Agent.resource(DID, { - upload: { - add: Agent.ability(Upload, result(Upload)) - } - }) + const upload = Agent.resource(Space, { + upload: { + _: Unit, + add: Agent.task({ + in: Upload, + out: result(Upload), + }), + remove: Agent.task({ + in: UploadRoot, + out: result(Upload), + }), + list: Agent.task({ + in: Cursor, + out: result(list(Upload)), + }), + }, + }) + const Info = Schema.struct({ + did: DID, + }) + const debug = Agent.resource(Schema.URI, { + debug: { + info: Agent.task({ out: result(Info) }), + }, + }) - const ConsoleProtocol = Agent.resource(DID.match({ method: 'key' }), { - console: { - log: Agent.ability(Upload, result(Schema.string())) + const agent = Agent.create({ + authority: Principal.parse('did:web:web3.storage'), + signer: await ed25519.generate(), + delegations: [], + }) + .init(async () => { + return { + password: 'secret', } }) - - - const service = UploadProtocol.and(ConsoleProtocol) - .with({ password: 'secret' }) - .provide({ - upload: { - add: async (uri, upload, context) => { - return upload - }, - remove: (uri, {root}, context) => { - return { root } - }, - list: (uri, cursor, context) => { - return { ...cursor, results: [] } + .provide(store, { + store: { + add: async (uri, input, context) => { + return { + status: 'done', + link: input.link, + with: uri, } }, - console: { - log: (uri, upload, context) => { - return uri + list: async (uri, input, context) => { + return { + results: [], } - } - }) - - - - - const alice = UploadProtocol.and(ConsoleProtocol).from('did:key:zAlice') - - - - const add = alice.upload.add({ - root: Link.parse('bafkqaaa') - }) - - const q = UploadProtocol.query({ - a: add, - b: add, - info: alice.console.log(add) + }, + remove: async (uri, input, context) => { + return { link: input.link } + }, + _: () => { + throw new Error('Capability can not be invoked capability') + }, + }, }) - const tp = UploadProtocol.query([add, add]) - - - - const r = q.decode() - - if (!r.info.error) { - r.info.toLocaleLowerCase() - } - - if (!r.a.error) { - r.a.root - } - if (!r.b.error) { - r.b - } - - // const add = UploadCapabilities.from('did:key:zAlice').upload.add({ - // root: Link.parse('bafkqaaa') - // }) - - // const ver = source.from(me.did()).test.version() - - // const a = source.from('did:key:zAlice') - // const selector = a.test.echo("hello").select({ text: true }) - - // const s = { ...selector} - - // const echo = selector.invoke({ - // issuer: me, - // audience: w3 - // }) - - // const out = await selector.decode(new Uint8Array()) - // if (!out.error) { - // const data = { ...out } - // } + const { test } = agent + .query({ + test: agent.resource('did:key:zSpace').store.list({ + cursor: 'hello', + }), + }) + .decode() - // const resource = Agent.resource(DID).field( - // 'test/version', - // Schema.unknown().optional(), - // version - // ).field('test/echo', Schema.string().optional(), - // result(Schema.string())) - - // const v = resource.from(me.did()).abilities['test/version'].invoke(null) - // if (!v.error) { - // v - // } + if (!test.error) { + test.results + } - // const e = resource.from(me.did()).abilities['test/echo'].invoke('hello') - // if (!e.error) { - // e.toLocaleLowerCase() - // } + const worker = agent.connect({ + principal: Principal.parse('did:web:web3.storage'), + capabilities: upload.and(debug), }) + const space = worker.capabilities.from('did:key:zSpace') -/** - * @param {object} input - * @param {API.AgentModule} input.Agent - */ -const w3protocol = async ({ Agent }) => { - const Space = DID.match({ method: 'key' }) + const listUploads = await space.upload + .list({ + cursor: 'last', + }) + .select({ + results: true, + }) - const Add = Schema.struct({ - link: Schema.Link, - size: Schema.integer(), - origin: Schema.Link.optional() - }) + const out = listUploads.decode(new Uint8Array()) + if (!out.error) { + out.results[0].root + } - const AddDone = Schema.struct({ - status: 'done', - with: Space, - link: Schema.Link - }) + const { first, second } = worker + .query({ + first: space.upload + .list({ + cursor: 'last', + }) + .select({ + results: true, + cursor: true, + }), + second: space.debug.info(), + }) + .decode() - const AddHandOff = Schema.struct({ - status: 'upload', - with: Space, - link: Schema.Link, - url: Schema.URI, - headers: Schema. - }) + if (!first.error) { + first.results[0].root + first.cursor + } - return Agent.resource(Space, { - store: { - _: Schema.struct({}), - add: Agent.ability(Add, ) - } - }) + if (!second.error) { + second.did + } } From 98a057ac441355ebe46628c7bf6072880abff092 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Tue, 17 Jan 2023 14:32:12 -0800 Subject: [PATCH 05/17] chore: save current edits --- packages/agent/src/agent.js | 195 ++++++++++++++++++++++++++++++ packages/agent/src/api.ts | 116 ++++++++++++------ packages/agent/test/infer.spec.js | 46 +++---- 3 files changed, 296 insertions(+), 61 deletions(-) create mode 100644 packages/agent/src/agent.js diff --git a/packages/agent/src/agent.js b/packages/agent/src/agent.js new file mode 100644 index 00000000..833ed7aa --- /dev/null +++ b/packages/agent/src/agent.js @@ -0,0 +1,195 @@ +import * as API from '../src/api.js' +import { Schema, URI } from '@ucanto/validator' + +export const fail = Schema.struct({ error: true }) + +/** + * Creates a schema for the task result by specifying ok and error types + * + * @template T + * @template {{}} [X={message:string}] + * @param {Schema.Reader} ok + * @param {Schema.Reader} error + * @returns {Schema.Schema>} + */ +export const result = ( + ok, + error = Schema.struct({ message: Schema.string() }) +) => + Schema.or( + /** @type {Schema.Reader} */ (ok), + fail.and(error) + ) + +/** + * @template {API.CreateTask} Create + * @param {Create} options + * @returns {API.Task ? T : void, Schema.Infer & { error?: never}, Schema.Infer & { error: true}>} + */ +export const task = options => + /** @type {any} */ ({ + in: options.in, + out: options.out, + mail: options.mail, + }) + +/** + * @template {API.URI} URI + * @template {API.ResourceAbilities} Abilities + * @param {API.Reader} resource + * @param {Abilities} abilities + * @returns {API.Resource} + */ +export const resource = (resource, abilities) => { + return new Resource({ resource, abilities, context: {} }) +} + +/** + * @template {API.URI} URI + * @template {API.ResourceAbilities} Abilities + * @template {{}} Context + */ + +class Resource { + /** + * @type {{new (state:T): API.From }|undefined} + */ + #API + + /** + * @param {object} source + * @param {API.Reader} source.uri + * @param {Abilities} source.abilities + * @param {Context} source.context + */ + constructor(source) { + this.source = source + } + get abilities() { + return this.source.abilities + } + + /** + * @param {URI} at + * @returns {API.From}, + */ + from(at) { + const uri = this.source.uri.read(at) + if (uri.error) { + throw uri + } + + if (!this.#API) { + this.#API = gen('', this.source.abilities) + } + + return new this.#API({ uri }) + } + /** + * @template Q + * @param {Q} query + * @returns {API.Batch} + */ + query(query) { + throw 'query' + } + /** + * @template ContextExt + * @param {ContextExt} context + * @returns {Resource} + */ + with(context) { + return new Resource({ + ...this.source, + context: { ...this.source.context, ...context }, + }) + } + + /** + * @template {API.URI} URIExt + * @template {API.ResourceAbilities} AbilitiesExt + * @template {{}} ContextExt + * @param {Resource} other + * @returns {Resource} + */ + and(other) { + return new Resource({ + uri: Schema.or(this.source.uri, other.source.uri), + context: { ...this.source.context, ...other.source.context }, + // we need to actually merge these + abilities: { ...this.source.abilities, ...other.source.abilities }, + }) + } +} + +/** + * @template {API.ResourceAbilities} Abilities + * @param {string} at + * @param {Abilities} abilities + * @returns {{new (state:T): API.From }} + */ + +const gen = (at, abilities) => { + /** + * @template {{ uri: API.URI }} State + */ + class ResourceAPI { + #state + /** + * @param {State} state + */ + constructor(state) { + this.#state = state + } + } + + const descriptor = {} + + for (const [key, source] of Object.entries(abilities)) { + const path = `${at}/key` + if (isReader(source)) { + descriptor[key] = { + get: function () { + const selector = new Selector(this.#state, { path, source }) + Object.defineProperty(this, key, { value: selector }) + return selector + }, + } + } else if (isTask(source)) { + descriptor[key] = { + get: function () { + const selector = new TaskSelector(this.$state, { path, source }) + Object.defineProperty(this, key, { value: selector }) + return selector + }, + } + } else { + const SubAPI = gen(path, source) + descriptor[key] = { + get: function () { + const selector = new SubAPI(this.$state) + Object.defineProperty(this, key, { value: selector }) + return selector + }, + } + } + } + + Object.defineProperties(ResourceAPI.prototype, descriptor) + + return ResourceAPI +} + +/** + * @template {API.Reader} Reader + * @param {Reader|unknown} value + * @returns {value is Reader} + */ +const isReader = value => true + +/** + * @template {API.Task} Task + * @param {Task|unknown} value + * @returns {value is Task} + */ +const isTask = value => true diff --git a/packages/agent/src/api.ts b/packages/agent/src/api.ts index 59854afa..e26614e3 100644 --- a/packages/agent/src/api.ts +++ b/packages/agent/src/api.ts @@ -35,27 +35,7 @@ export interface AgentModule { resource( resource: Reader, abilities: Abilities - ): Resource> & ResourceCapabilities - - task< - In, - Out, - Fail extends { error: true } = { error: true; message: string }, - Event extends unknown = never - >(options: { - in: Reader - out: Reader> - events?: Reader - }): Task - - task< - Out, - Fail extends { error: true } = { error: true; message: string }, - Event extends unknown = never - >(options: { - out: Reader> - events?: Reader - }): Task + ): Resource> // & ResourceCapabilities } export interface Task< @@ -66,8 +46,32 @@ export interface Task< With extends URI = URI > { uri: With + + in: Reader + event: Reader + + ok: Reader + error: Reader + out: Reader> +} + +export type CreateTask< + In = unknown, + Out = unknown, + Fail extends { error: true } = { error: true }, + Mail = unknown +> = TaskWithInput | TaskWithoutInput + +export interface TaskWithInput { in: Reader out: Reader> + mail?: Reader +} + +export interface TaskWithoutInput { + in?: undefined + out: Reader> + mail?: Reader } export interface CapabilitySchema< @@ -85,10 +89,10 @@ export interface Resource< Abilities extends ResourceAbilities = ResourceAbilities, Context extends {} = {} > { - id: ID - abilities: Abilities + // id: ID + // abilities: Abilities - capabilities: ResourceCapabilities + // capabilities: ResourceCapabilities from(at: At): From query(query: Q): Batch @@ -116,7 +120,13 @@ export type ResourceCapabilities< Result > ? ResourceCapability, In> - : Abilities[K] extends Task + : Abilities[K] extends Task< + infer In, + infer _Out, + infer _Fail, + infer _Mail, + infer _AT + > ? ResourceCapability, In> : Abilities[K] extends ResourceAbilities ? ResourceCapabilities, Abilities[K]> @@ -173,7 +183,13 @@ type ProviderOf< Result > & { uri: infer ID } ? (uri: ID, context: Context) => Await> - : Abilities[K] extends Task + : Abilities[K] extends Task< + infer In, + infer Out, + infer Fail, + infer _Mail, + infer URI + > ? (uri: URI, input: In, context: Context) => Await> : Abilities[K] extends ResourceAbilities ? ProviderOf @@ -188,8 +204,14 @@ type With< Result > ? Reader> & { uri: ID } - : Abilities[K] extends Task - ? Task + : Abilities[K] extends Task< + infer In, + infer Out, + infer Fail, + infer Mail, + infer _At + > + ? Task : Abilities[K] extends ResourceAbilities ? With : never @@ -206,7 +228,7 @@ type Query = ...Selector[] ] -interface Batch { +export interface Batch { query: Q decode(): { @@ -240,7 +262,13 @@ export type From< Result > ? () => Selector - : Abilities[K] extends Task + : Abilities[K] extends Task< + infer In, + infer Out, + infer Fail, + infer _Mail, + infer _At + > ? (input: Input) => Selector : Abilities[K] extends ResourceAbilities ? From @@ -265,7 +293,7 @@ export type Source = | Reader // If query source takes an input and returns output it is defined // as ability - | Task + | Task export interface Selector< At extends URI, @@ -354,13 +382,13 @@ export interface CreateAgent { export interface AgentConnect< ID extends DID, - Capabilities extends Resource = never + Abilities extends ResourceAbilities = {} > { principal: API.Principal delegations?: Delegation[] - capabilities?: Capabilities + capabilities?: Resource } /** @@ -375,9 +403,9 @@ export interface Agent< signer: Signer context: Context - connect( - options: AgentConnect - ): AgentConnection + connect( + options: AgentConnect + ): AgentConnection /** * Attaches some context to the agent. @@ -405,12 +433,13 @@ export type QueryEndpoint< export interface AgentConnection< ID extends DID, - Capabilities extends Resource + Abilities extends ResourceAbilities > { did(): ID - capabilities: Capabilities - query: Capabilities['query'] + resource(uri: At): From + + query: Resource['query'] } export interface HandlerInput { @@ -420,3 +449,12 @@ export interface HandlerInput { invocation: API.ServiceInvocation agent: Agent } + +type Unit = Partial<{ [key: string]: never }> +export type Outcome = KeyedUnion<{ ok: T } | { error: X }> + +type Empty = { + [K in keyof T]?: never +} + +type KeyedUnion = Empty & T diff --git a/packages/agent/test/infer.spec.js b/packages/agent/test/infer.spec.js index 34663d94..69df7373 100644 --- a/packages/agent/test/infer.spec.js +++ b/packages/agent/test/infer.spec.js @@ -3,21 +3,7 @@ import { DID as Principal } from '@ucanto/core' import { capability, Schema, DID, URI, Text, Link } from '@ucanto/validator' import { ed25519 } from '@ucanto/principal' import { CAR } from '@ucanto/transport' - -const fail = Schema.struct({ error: true }) - -/** - * @template T - * @template {{}} [X={message:string}] - * @param {Schema.Reader} ok - * @param {Schema.Reader} error - * @returns {Schema.Schema>} - */ -const result = (ok, error = Schema.struct({ message: Schema.string() })) => - Schema.or( - /** @type {Schema.Reader} */ (ok), - fail.and(error) - ) +import { result, task } from '../src/agent.js' /** * @param {object} input @@ -104,15 +90,15 @@ const testW3protocol = async ({ Agent }) => { const store = Agent.resource(Space, { store: { _: Unit, - add: Agent.task({ + add: task({ in: Add, out: result(AddDone.or(AddHandOff), AddError), }), - remove: Agent.task({ + remove: task({ in: Remove, out: result(Remove, MalformedCapability.or(InvocationError)), }), - list: Agent.task({ + list: task({ in: Cursor, out: result(list(Add), InvocationError), }), @@ -122,15 +108,15 @@ const testW3protocol = async ({ Agent }) => { const upload = Agent.resource(Space, { upload: { _: Unit, - add: Agent.task({ + add: task({ in: Upload, out: result(Upload), }), - remove: Agent.task({ + remove: task({ in: UploadRoot, out: result(Upload), }), - list: Agent.task({ + list: task({ in: Cursor, out: result(list(Upload)), }), @@ -141,9 +127,11 @@ const testW3protocol = async ({ Agent }) => { did: DID, }) + const info = task({ out: result(Info) }) + const debug = Agent.resource(Schema.URI, { debug: { - info: Agent.task({ out: result(Info) }), + info: task({ in: undefined, out: result(Info) }), }, }) @@ -235,3 +223,17 @@ const testW3protocol = async ({ Agent }) => { second.did } } + +/** + * @param {object} input + * @param {API.Outcome} input.out + */ +export const testResult = ({ out }) => { + const { ok, error } = out + if (!error) { + ok + } else { + error + ok + } +} From 566bc289d53ff7142f0b404d8467c55bc8419ca0 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Tue, 24 Jan 2023 09:32:20 -0800 Subject: [PATCH 06/17] chore: stash current draft --- packages/agent/src/agent.js | 98 ++++++++++++++++++++++++------- packages/agent/src/api.ts | 61 +++++-------------- packages/agent/test/basic.spec.js | 69 ++++++++++++++++++++++ packages/agent/test/infer.spec.js | 13 +++- 4 files changed, 173 insertions(+), 68 deletions(-) create mode 100644 packages/agent/test/basic.spec.js diff --git a/packages/agent/src/agent.js b/packages/agent/src/agent.js index 833ed7aa..a1176c22 100644 --- a/packages/agent/src/agent.js +++ b/packages/agent/src/agent.js @@ -30,7 +30,6 @@ export const task = options => /** @type {any} */ ({ in: options.in, out: options.out, - mail: options.mail, }) /** @@ -58,7 +57,7 @@ class Resource { /** * @param {object} source - * @param {API.Reader} source.uri + * @param {API.Reader} source.resource * @param {Abilities} source.abilities * @param {Context} source.context */ @@ -70,11 +69,12 @@ class Resource { } /** - * @param {URI} at - * @returns {API.From}, + * @template {URI} At + * @param {At} at + * @returns {API.From}, */ from(at) { - const uri = this.source.uri.read(at) + const uri = this.source.resource.read(at) if (uri.error) { throw uri } @@ -83,7 +83,7 @@ class Resource { this.#API = gen('', this.source.abilities) } - return new this.#API({ uri }) + return new this.#API({ uri: /** @type {At} */ (uri) }) } /** * @template Q @@ -114,12 +114,20 @@ class Resource { */ and(other) { return new Resource({ - uri: Schema.or(this.source.uri, other.source.uri), + resource: Schema.or(this.source.resource, other.source.resource), context: { ...this.source.context, ...other.source.context }, // we need to actually merge these abilities: { ...this.source.abilities, ...other.source.abilities }, }) } + + // /** + // * @template {API.ProviderOf} Provider + // * @param {Provider} provider + // */ + // provide(provider) { + + // } } /** @@ -134,40 +142,46 @@ const gen = (at, abilities) => { * @template {{ uri: API.URI }} State */ class ResourceAPI { - #state /** * @param {State} state */ constructor(state) { - this.#state = state + this.state = state } } + /** @type {PropertyDescriptorMap} */ const descriptor = {} for (const [key, source] of Object.entries(abilities)) { - const path = `${at}/key` + const path = `${at}/${key}` if (isReader(source)) { descriptor[key] = { - get: function () { - const selector = new Selector(this.#state, { path, source }) - Object.defineProperty(this, key, { value: selector }) - return selector + value: function () { + return new Selector(this.state, { path, schema: source }) + // Object.defineProperty(this, key, { value: selector }) + // return selector }, } } else if (isTask(source)) { descriptor[key] = { - get: function () { - const selector = new TaskSelector(this.$state, { path, source }) - Object.defineProperty(this, key, { value: selector }) - return selector + value: function (input) { + return new TaskSelector({ + with: this.state.uri, + path, + schema: source, + input, + in: source.in.read(input), + }) + // Object.defineProperty(this, key, { value: selector }) + // return selector }, } } else { const SubAPI = gen(path, source) descriptor[key] = { get: function () { - const selector = new SubAPI(this.$state) + const selector = new SubAPI(this.state) Object.defineProperty(this, key, { value: selector }) return selector }, @@ -177,7 +191,37 @@ const gen = (at, abilities) => { Object.defineProperties(ResourceAPI.prototype, descriptor) - return ResourceAPI + return /** @type {any} */ (ResourceAPI) +} + +class Selector { + /** + * @param {{ uri: API.URI }} state + */ + constructor(state, { path, source }) { + this.path = path + this.source = source + this.state = state + } +} + +class TaskSelector { + /** + * @param {object} state + */ + constructor(source) { + this.source = source + } + + get with() { + return this.source.with + } + get can() { + return this.source.path.slice(1) + } + get in() { + return this.source.in + } } /** @@ -185,11 +229,21 @@ const gen = (at, abilities) => { * @param {Reader|unknown} value * @returns {value is Reader} */ -const isReader = value => true +const isReader = value => + value != null && + typeof value === 'object' && + 'read' in value && + typeof value.read === 'function' /** * @template {API.Task} Task * @param {Task|unknown} value * @returns {value is Task} */ -const isTask = value => true +const isTask = value => + value != null && + typeof value === 'object' && + 'in' in value && + isReader(value.in) && + 'out' in value && + isReader(value.out) diff --git a/packages/agent/src/api.ts b/packages/agent/src/api.ts index e26614e3..a686e1dd 100644 --- a/packages/agent/src/api.ts +++ b/packages/agent/src/api.ts @@ -42,13 +42,11 @@ export interface Task< In extends unknown = unknown, Out extends unknown = unknown, Fail extends { error: true } = { error: true }, - Event extends unknown = never, With extends URI = URI > { uri: With in: Reader - event: Reader ok: Reader error: Reader @@ -58,20 +56,17 @@ export interface Task< export type CreateTask< In = unknown, Out = unknown, - Fail extends { error: true } = { error: true }, - Mail = unknown -> = TaskWithInput | TaskWithoutInput + Fail extends { error: true } = { error: true } +> = TaskWithInput | TaskWithoutInput -export interface TaskWithInput { +export interface TaskWithInput { in: Reader out: Reader> - mail?: Reader } -export interface TaskWithoutInput { +export interface TaskWithoutInput { in?: undefined out: Reader> - mail?: Reader } export interface CapabilitySchema< @@ -92,15 +87,15 @@ export interface Resource< // id: ID // abilities: Abilities - // capabilities: ResourceCapabilities + capabilities: ResourceCapabilities from(at: At): From query(query: Q): Batch with(context: CTX): Resource - provide

>( - provider: P - ): Provider + // provide

>( + // provider: P + // ): Provider and< ID2 extends URI, @@ -120,13 +115,7 @@ export type ResourceCapabilities< Result > ? ResourceCapability, In> - : Abilities[K] extends Task< - infer In, - infer _Out, - infer _Fail, - infer _Mail, - infer _AT - > + : Abilities[K] extends Task ? ResourceCapability, In> : Abilities[K] extends ResourceAbilities ? ResourceCapabilities, Abilities[K]> @@ -175,7 +164,7 @@ export interface Provider< context: Context } -type ProviderOf< +export type ProviderOf< Abilities extends ResourceAbilities = ResourceAbilities, Context extends {} = {} > = { @@ -183,13 +172,7 @@ type ProviderOf< Result > & { uri: infer ID } ? (uri: ID, context: Context) => Await> - : Abilities[K] extends Task< - infer In, - infer Out, - infer Fail, - infer _Mail, - infer URI - > + : Abilities[K] extends Task ? (uri: URI, input: In, context: Context) => Await> : Abilities[K] extends ResourceAbilities ? ProviderOf @@ -204,14 +187,8 @@ type With< Result > ? Reader> & { uri: ID } - : Abilities[K] extends Task< - infer In, - infer Out, - infer Fail, - infer Mail, - infer _At - > - ? Task + : Abilities[K] extends Task + ? Task : Abilities[K] extends ResourceAbilities ? With : never @@ -262,13 +239,7 @@ export type From< Result > ? () => Selector - : Abilities[K] extends Task< - infer In, - infer Out, - infer Fail, - infer _Mail, - infer _At - > + : Abilities[K] extends Task ? (input: Input) => Selector : Abilities[K] extends ResourceAbilities ? From @@ -293,7 +264,7 @@ export type Source = | Reader // If query source takes an input and returns output it is defined // as ability - | Task + | Task export interface Selector< At extends URI, @@ -420,7 +391,7 @@ export interface Agent< provide( capabilities: Resource, provider: ProviderOf - ): Agent['abilities']> + ): Agent> resource(uri: At): From diff --git a/packages/agent/test/basic.spec.js b/packages/agent/test/basic.spec.js new file mode 100644 index 00000000..20fc3874 --- /dev/null +++ b/packages/agent/test/basic.spec.js @@ -0,0 +1,69 @@ +import * as API from '../src/api.js' +import { DID as Principal } from '@ucanto/core' +import { capability, Schema, DID, URI, Text, Link } from '@ucanto/validator' +import { ed25519 } from '@ucanto/principal' +import { CAR } from '@ucanto/transport' +import { result, task } from '../src/agent.js' +import * as Agent from '../src/agent.js' +import { test, assert } from './test.js' + +test('create resource', () => { + const Space = DID.match({ method: 'key' }) + const Unit = Schema.struct({}) + const Echo = Schema.struct({ + message: Schema.string(), + }) + const EchoError = Schema.struct({ + name: 'EchoError', + }) + + const Add = Schema.struct({ + size: Schema.integer(), + link: Schema.Link.link(), + origin: Schema.Link.link().optional(), + }) + + const Allocate = Schema.struct({ + size: Schema.integer(), + link: Schema.Link.link(), + + proof: Schema.enum(['store/add']).optional(), + }) + + const out = Allocate.shape.proof.read({}) + if (!out?.error) { + const out2 = { ...out } + } + + const api = Agent.resource(Space, { + debug: { + _: Unit, + echo: task({ + in: Echo, + out: result(Echo, EchoError), + }), + }, + }) + + api.capabilities.debug.echo + + assert.equal(typeof api.from, 'function') + assert.throws( + () => + // @ts-expect-error + api.from('did:web:web3.storage'), + /Expected a did:key: but got "did:web/ + ) + + const space = api.from('did:key:zAlice') + assert.equal(typeof space.debug, 'object') + assert.equal(typeof space.debug.echo, 'function') + const echo = space.debug.echo({ message: 'hello world' }) + + assert.equal(echo.can, 'debug/echo') + assert.equal(echo.with, 'did:key:zAlice') + assert.deepEqual(echo.in, { + message: 'hello world', + }) + console.log(echo) +}) diff --git a/packages/agent/test/infer.spec.js b/packages/agent/test/infer.spec.js index 69df7373..c8745615 100644 --- a/packages/agent/test/infer.spec.js +++ b/packages/agent/test/infer.spec.js @@ -105,6 +105,15 @@ const testW3protocol = async ({ Agent }) => { }, }) + const t1 = store.from('did:key:zAlice').store.add({}).select({ + with: true, + }) + + const t2 = { ...t1.decode(new Uint8Array()) } + if (!t2.error) { + t2.with + } + const upload = Agent.resource(Space, { upload: { _: Unit, @@ -123,6 +132,8 @@ const testW3protocol = async ({ Agent }) => { }, }) + upload.from('did:key:zAlice').upload.list({}) + const Info = Schema.struct({ did: DID, }) @@ -185,7 +196,7 @@ const testW3protocol = async ({ Agent }) => { capabilities: upload.and(debug), }) - const space = worker.capabilities.from('did:key:zSpace') + const space = worker.resource('did:key:zSpace') const listUploads = await space.upload .list({ From 88befadae92b35406b1ef77c0b2ef0afbe0e940b Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Tue, 24 Jan 2023 09:33:54 -0800 Subject: [PATCH 07/17] chore: stash current draft --- packages/core/src/capability.js | 827 ++++++++++++++++ packages/core/src/error.js | 319 ++++++ packages/core/src/lib.js | 3 + packages/core/src/protocol.js | 0 packages/core/src/protocol.ts | 60 ++ packages/core/src/schema.js | 6 + packages/core/src/schema/did.js | 39 + packages/core/src/schema/link.js | 79 ++ packages/core/src/schema/result.js | 20 + packages/core/src/schema/schema.js | 1280 +++++++++++++++++++++++++ packages/core/src/schema/text.js | 34 + packages/core/src/schema/type.js | 0 packages/core/src/schema/type.ts | 142 +++ packages/core/src/schema/uri.js | 64 ++ packages/core/src/util.js | 53 + packages/core/test/protocol.spec.js | 190 ++++ packages/core/test/util.js | 43 + packages/core/tsconfig.json | 2 +- packages/interface/src/capability.ts | 3 +- packages/server/src/agent.js | 56 ++ packages/validator/src/capability.js | 21 + packages/validator/src/schema/task.js | 45 + 22 files changed, 3284 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/capability.js create mode 100644 packages/core/src/error.js create mode 100644 packages/core/src/protocol.js create mode 100644 packages/core/src/protocol.ts create mode 100644 packages/core/src/schema.js create mode 100644 packages/core/src/schema/did.js create mode 100644 packages/core/src/schema/link.js create mode 100644 packages/core/src/schema/result.js create mode 100644 packages/core/src/schema/schema.js create mode 100644 packages/core/src/schema/text.js create mode 100644 packages/core/src/schema/type.js create mode 100644 packages/core/src/schema/type.ts create mode 100644 packages/core/src/schema/uri.js create mode 100644 packages/core/src/util.js create mode 100644 packages/core/test/protocol.spec.js create mode 100644 packages/core/test/util.js create mode 100644 packages/server/src/agent.js create mode 100644 packages/validator/src/schema/task.js diff --git a/packages/core/src/capability.js b/packages/core/src/capability.js new file mode 100644 index 00000000..6edcf411 --- /dev/null +++ b/packages/core/src/capability.js @@ -0,0 +1,827 @@ +import * as API from '@ucanto/interface' +import { entries, combine, intersection } from './util.js' +import { + EscalatedCapability, + MalformedCapability, + UnknownCapability, + DelegationError as MatchError, + Failure, +} from './error.js' +import { invoke } from './invocation.js' +import { delegate } from './delegation.js' + +/** + * @template {API.Ability} A + * @template {API.URI} R + * @template {API.Caveats} [C={}] + * @param {API.Descriptor} descriptor + * @returns {API.TheCapabilityParser>} + */ +export const capability = descriptor => new Capability(descriptor) + +/** + * @template {API.Match} M + * @template {API.Match} W + * @param {API.Matcher} left + * @param {API.Matcher} right + * @returns {API.CapabilityParser} + */ +export const or = (left, right) => new Or(left, right) + +/** + * @template {API.MatchSelector[]} Selectors + * @param {Selectors} selectors + * @returns {API.CapabilitiesParser>} + */ +export const and = (...selectors) => new And(selectors) + +/** + * @template {API.Match} M + * @template {API.ParsedCapability} T + * @param {API.DeriveSelector & { from: API.MatchSelector }} options + * @returns {API.TheCapabilityParser>} + */ +export const derive = ({ from, to, derives }) => new Derive(from, to, derives) + +/** + * @template {API.Match} M + * @implements {API.View} + */ +class View { + /** + * @param {API.Source} source + * @returns {API.MatchResult} + */ + /* c8 ignore next 3 */ + match(source) { + return new UnknownCapability(source.capability) + } + + /** + * @param {API.Source[]} capabilities + */ + select(capabilities) { + return select(this, capabilities) + } + + /** + * @template {API.ParsedCapability} U + * @param {API.DeriveSelector} options + * @returns {API.TheCapabilityParser>} + */ + derive({ derives, to }) { + return derive({ derives, to, from: this }) + } +} + +/** + * @template {API.Match} M + * @implements {API.CapabilityParser} + * @extends {View} + */ +class Unit extends View { + /** + * @template {API.Match} W + * @param {API.MatchSelector} other + * @returns {API.CapabilityParser} + */ + or(other) { + return or(this, other) + } + + /** + * @template {API.Match} W + * @param {API.CapabilityParser} other + * @returns {API.CapabilitiesParser<[M, W]>} + */ + and(other) { + return and(/** @type {API.CapabilityParser} */ (this), other) + } +} + +/** + * @template {API.Ability} A + * @template {API.URI} R + * @template {API.Caveats} C + * @implements {API.TheCapabilityParser>>>} + * @extends {Unit>>>} + */ +class Capability extends Unit { + /** + * @param {API.Descriptor} descriptor + */ + constructor(descriptor) { + super() + this.descriptor = { derives, ...descriptor } + } + + /** + * @param {unknown} source + */ + read(source) { + try { + const result = this.create(/** @type {any} */ (source)) + return /** @type {API.Result>, API.Failure>} */ ( + result + ) + } catch (error) { + return /** @type {any} */ (error).cause + } + } + + /** + * @param {API.InferCreateOptions>} options + */ + create(options) { + const { descriptor, can } = this + const decoders = descriptor.nb + const data = /** @type {API.InferCaveats} */ (options.nb || {}) + + const resource = descriptor.with.read(options.with) + if (resource.error) { + throw Object.assign(new Error(`Invalid 'with' - ${resource.message}`), { + cause: resource, + }) + } + + const capabality = + /** @type {API.ParsedCapability>} */ + ({ can, with: resource }) + + for (const [name, decoder] of Object.entries(decoders || {})) { + const key = /** @type {keyof data & string} */ (name) + const value = decoder.read(data[key]) + if (value?.error) { + throw Object.assign( + new Error(`Invalid 'nb.${key}' - ${value.message}`), + { cause: value } + ) + } else if (value !== undefined) { + const nb = + capabality.nb || + (capabality.nb = /** @type {API.InferCaveats} */ ({})) + + const key = /** @type {keyof nb} */ (name) + nb[key] = /** @type {typeof nb[key]} */ (value) + } + } + + return capabality + } + + /** + * @param {API.InferInvokeOptions>} options + */ + invoke({ with: with_, nb, ...options }) { + return invoke({ + ...options, + capability: this.create( + /** @type {API.InferCreateOptions>} */ + ({ with: with_, nb }) + ), + }) + } + + /** + * @param {API.InferDelegationOptions>} options + */ + async delegate({ with: with_, nb, ...options }) { + const { descriptor, can } = this + const readers = descriptor.nb + const data = /** @type {API.InferCaveats} */ (nb || {}) + + const resource = descriptor.with.read(with_) + if (resource.error) { + throw Object.assign(new Error(`Invalid 'with' - ${resource.message}`), { + cause: resource, + }) + } + + const capabality = + /** @type {API.ParsedCapability>} */ + ({ can, with: resource }) + + for (const [name, reader] of Object.entries(readers || {})) { + const key = /** @type {keyof data & string} */ (name) + const source = data[key] + // omit undefined fields in the delegation + const value = source === undefined ? source : reader.read(data[key]) + if (value?.error) { + throw Object.assign( + new Error(`Invalid 'nb.${key}' - ${value.message}`), + { cause: value } + ) + } else if (value !== undefined) { + const nb = + capabality.nb || + (capabality.nb = /** @type {API.InferCaveats} */ ({})) + + const key = /** @type {keyof nb} */ (name) + nb[key] = /** @type {typeof nb[key]} */ (value) + } + } + + return await delegate({ + capabilities: [capabality], + ...options, + }) + } + + get can() { + return this.descriptor.can + } + + /** + * @param {API.Source} source + * @returns {API.MatchResult>>>} + */ + match(source) { + const result = parse(this, source) + return result.error ? result : new Match(source, result, this.descriptor) + } + toString() { + return JSON.stringify({ can: this.descriptor.can }) + } +} + +/** + * @template {API.Match} M + * @template {API.Match} W + * @implements {API.CapabilityParser} + * @extends {Unit} + */ +class Or extends Unit { + /** + * @param {API.Matcher} left + * @param {API.Matcher} right + */ + constructor(left, right) { + super() + this.left = left + this.right = right + } + + /** + * @param {API.Source} capability + * @return {API.MatchResult} + */ + match(capability) { + const left = this.left.match(capability) + if (left.error) { + const right = this.right.match(capability) + if (right.error) { + return right.name === 'MalformedCapability' + ? // + right + : // + left + } else { + return right + } + } else { + return left + } + } + + toString() { + return `${this.left.toString()}|${this.right.toString()}` + } +} + +/** + * @template {API.MatchSelector[]} Selectors + * @implements {API.CapabilitiesParser>} + * @extends {View>>} + */ +class And extends View { + /** + * @param {Selectors} selectors + */ + constructor(selectors) { + super() + this.selectors = selectors + } + /** + * @param {API.Source} capability + * @returns {API.MatchResult>>} + */ + match(capability) { + const group = [] + for (const selector of this.selectors) { + const result = selector.match(capability) + if (result.error) { + return result + } else { + group.push(result) + } + } + + return new AndMatch(/** @type {API.InferMembers} */ (group)) + } + + /** + * @param {API.Source[]} capabilities + */ + select(capabilities) { + return selectGroup(this, capabilities) + } + /** + * @template E + * @template {API.Match} X + * @param {API.MatchSelector>} other + * @returns {API.CapabilitiesParser<[...API.InferMembers, API.Match]>} + */ + and(other) { + return new And([...this.selectors, other]) + } + toString() { + return `[${this.selectors.map(String).join(', ')}]` + } +} + +/** + * @template {API.ParsedCapability} T + * @template {API.Match} M + * @implements {API.TheCapabilityParser>} + * @extends {Unit>} + */ + +class Derive extends Unit { + /** + * @param {API.MatchSelector} from + * @param {API.TheCapabilityParser>} to + * @param {API.Derives, API.ToDeriveProof>} derives + */ + constructor(from, to, derives) { + super() + this.from = from + this.to = to + this.derives = derives + } + + /** + * @param {unknown} source + */ + read(source) { + return this.to.read(source) + } + + /** + * @type {typeof this.to['create']} + */ + create(options) { + return this.to.create(options) + } + /** + * @type {typeof this.to['invoke']} + */ + invoke(options) { + return this.to.invoke(options) + } + /** + * @type {typeof this.to['delegate']} + */ + delegate(options) { + return this.to.delegate(options) + } + get can() { + return this.to.can + } + /** + * @param {API.Source} capability + * @returns {API.MatchResult>} + */ + match(capability) { + const match = this.to.match(capability) + if (match.error) { + return match + } else { + return new DerivedMatch(match, this.from, this.derives) + } + } + toString() { + return this.to.toString() + } +} + +/** + * @template {API.Ability} A + * @template {API.URI} R + * @template {API.Caveats} C + * @implements {API.DirectMatch>>} + */ +class Match { + /** + * @param {API.Source} source + * @param {API.ParsedCapability>} value + * @param {API.Descriptor} descriptor + */ + constructor(source, value, descriptor) { + this.source = [source] + this.value = value + this.descriptor = { derives, ...descriptor } + } + get can() { + return this.value.can + } + + get proofs() { + const proofs = [this.source[0].delegation] + Object.defineProperties(this, { + proofs: { value: proofs }, + }) + return proofs + } + + /** + * @param {API.CanIssue} context + * @returns {API.DirectMatch>>|null} + */ + prune(context) { + if (context.canIssue(this.value, this.source[0].delegation.issuer.did())) { + return null + } else { + return this + } + } + + /** + * @param {API.Source[]} capabilities + * @returns {API.Select>>>} + */ + select(capabilities) { + const unknown = [] + const errors = [] + const matches = [] + for (const capability of capabilities) { + const result = parse(this, capability, true) + if (!result.error) { + const claim = this.descriptor.derives(this.value, result) + if (claim.error) { + errors.push( + new MatchError( + [new EscalatedCapability(this.value, result, claim)], + this + ) + ) + } else { + matches.push(new Match(capability, result, this.descriptor)) + } + } else { + switch (result.name) { + case 'UnknownCapability': + unknown.push(result.capability) + break + case 'MalformedCapability': + default: + errors.push(new MatchError([result], this)) + } + } + } + + return { matches, unknown, errors } + } + toString() { + const { nb } = this.value + return JSON.stringify({ + can: this.descriptor.can, + with: this.value.with, + nb: nb && Object.keys(nb).length > 0 ? nb : undefined, + }) + } +} + +/** + * @template {API.ParsedCapability} T + * @template {API.Match} M + * @implements {API.DerivedMatch} + */ + +class DerivedMatch { + /** + * @param {API.DirectMatch} selected + * @param {API.MatchSelector} from + * @param {API.Derives, API.ToDeriveProof>} derives + */ + constructor(selected, from, derives) { + this.selected = selected + this.from = from + this.derives = derives + } + get can() { + return this.value.can + } + get source() { + return this.selected.source + } + get proofs() { + const proofs = [] + for (const { delegation } of this.selected.source) { + proofs.push(delegation) + } + Object.defineProperties(this, { proofs: { value: proofs } }) + return proofs + } + get value() { + return this.selected.value + } + + /** + * @param {API.CanIssue} context + */ + prune(context) { + const selected = + /** @type {API.DirectMatch|null} */ + (this.selected.prune(context)) + return selected ? new DerivedMatch(selected, this.from, this.derives) : null + } + + /** + * @param {API.Source[]} capabilities + */ + select(capabilities) { + const { derives, selected, from } = this + const { value } = selected + + const direct = selected.select(capabilities) + + const derived = from.select(capabilities) + const matches = [] + const errors = [] + for (const match of derived.matches) { + // If capability can not be derived it escalates + const result = derives(value, match.value) + if (result.error) { + errors.push( + new MatchError( + [new EscalatedCapability(value, match.value, result)], + this + ) + ) + } else { + matches.push(match) + } + } + + return { + unknown: intersection(direct.unknown, derived.unknown), + errors: [ + ...errors, + ...direct.errors, + ...derived.errors.map(error => new MatchError([error], this)), + ], + matches: [ + ...direct.matches.map(match => new DerivedMatch(match, from, derives)), + ...matches, + ], + } + } + + toString() { + return this.selected.toString() + } +} + +/** + * @template {API.MatchSelector[]} Selectors + * @implements {API.Amplify>} + */ +class AndMatch { + /** + * @param {API.Match[]} matches + */ + constructor(matches) { + this.matches = matches + } + get selectors() { + return this.matches + } + /** + * @returns {API.Source[]} + */ + get source() { + const source = [] + + for (const match of this.matches) { + source.push(...match.source) + } + Object.defineProperties(this, { source: { value: source } }) + return source + } + + /** + * @param {API.CanIssue} context + */ + prune(context) { + const matches = [] + for (const match of this.matches) { + const pruned = match.prune(context) + if (pruned) { + matches.push(pruned) + } + } + return matches.length === 0 ? null : new AndMatch(matches) + } + + get proofs() { + const proofs = [] + + for (const { delegation } of this.source) { + proofs.push(delegation) + } + + Object.defineProperties(this, { proofs: { value: proofs } }) + return proofs + } + /** + * @type {API.InferValue>} + */ + get value() { + const value = [] + + for (const match of this.matches) { + value.push(match.value) + } + Object.defineProperties(this, { value: { value } }) + return /** @type {any} */ (value) + } + /** + * @param {API.Source[]} capabilities + */ + select(capabilities) { + return selectGroup(this, capabilities) + } + toString() { + return `[${this.matches.map(match => match.toString()).join(', ')}]` + } +} + +/** + * Parses capability `source` using a provided capability `parser`. By default + * invocation parsing occurs, which respects a capability schema, failing if + * any non-optional field is missing. If `optional` argument is `true` it will + * parse capability as delegation, in this case all `nb` fields are considered + * optional. + * + * @template {API.Ability} A + * @template {API.URI} R + * @template {API.Caveats} C + * @param {{descriptor: API.Descriptor}} parser + * @param {API.Source} source + * @param {boolean} [optional=false] + * @returns {API.Result>, API.InvalidCapability>} + */ + +const parse = (parser, source, optional = false) => { + const { can, with: withReader, nb: readers } = parser.descriptor + const { delegation } = source + const capability = /** @type {API.Capability>} */ ( + source.capability + ) + + if (capability.can !== can) { + return new UnknownCapability(capability) + } + + const uri = withReader.read(capability.with) + if (uri.error) { + return new MalformedCapability(capability, uri) + } + + const nb = /** @type {API.InferCaveats} */ ({}) + + if (readers) { + /** @type {Partial>} */ + const caveats = capability.nb || {} + for (const [name, reader] of entries(readers)) { + const key = /** @type {keyof caveats & keyof nb & string} */ (name) + if (key in caveats || !optional) { + const result = reader.read(caveats[key]) + if (result?.error) { + return new MalformedCapability(capability, result) + } else if (result != null) { + nb[key] = /** @type {any} */ (result) + } + } + } + } + + return new CapabilityView(can, capability.with, nb, delegation) +} + +/** + * @template {API.Ability} A + * @template {API.URI} R + * @template C + */ +class CapabilityView { + /** + * @param {A} can + * @param {R} with_ + * @param {API.InferCaveats} nb + * @param {API.Delegation} delegation + */ + constructor(can, with_, nb, delegation) { + this.can = can + this.with = with_ + this.delegation = delegation + this.nb = nb + } +} + +/** + * @template {API.Match} M + * @param {API.Matcher} matcher + * @param {API.Source[]} capabilities + */ + +const select = (matcher, capabilities) => { + const unknown = [] + const matches = [] + const errors = [] + for (const capability of capabilities) { + const result = matcher.match(capability) + if (result.error) { + switch (result.name) { + case 'UnknownCapability': + unknown.push(result.capability) + break + case 'MalformedCapability': + default: + errors.push(new MatchError([result], result.capability)) + } + } else { + matches.push(result) + } + } + + return { matches, errors, unknown } +} + +/** + * @template {API.Selector[]} S + * @param {{selectors:S}} self + * @param {API.Source[]} capabilities + */ + +const selectGroup = (self, capabilities) => { + let unknown + const data = [] + const errors = [] + for (const selector of self.selectors) { + const selected = selector.select(capabilities) + unknown = unknown + ? intersection(unknown, selected.unknown) + : selected.unknown + + for (const error of selected.errors) { + errors.push(new MatchError([error], self)) + } + + data.push(selected.matches) + } + + const matches = combine(data).map(group => new AndMatch(group)) + + return { + unknown: + /* c8 ignore next */ + unknown || [], + errors, + matches, + } +} + +/** + * @template {API.ParsedCapability} T + * @template {API.ParsedCapability} U + * @param {T} claimed + * @param {U} delegated + * @return {API.Result} + */ +const derives = (claimed, delegated) => { + if (delegated.with.endsWith('*')) { + if (!claimed.with.startsWith(delegated.with.slice(0, -1))) { + return new Failure( + `Resource ${claimed.with} does not match delegated ${delegated.with} ` + ) + } + } else if (delegated.with !== claimed.with) { + return new Failure( + `Resource ${claimed.with} is not contained by ${delegated.with}` + ) + } + + /* c8 ignore next 2 */ + const caveats = delegated.nb || {} + const nb = claimed.nb || {} + const kv = entries(caveats) + + for (const [name, value] of kv) { + if (nb[name] != value) { + return new Failure(`${String(name)}: ${nb[name]} violates ${value}`) + } + } + + return true +} diff --git a/packages/core/src/error.js b/packages/core/src/error.js new file mode 100644 index 00000000..82d2c000 --- /dev/null +++ b/packages/core/src/error.js @@ -0,0 +1,319 @@ +import * as API from '@ucanto/interface' +import { the } from './util.js' +import { isLink } from 'multiformats/link' + +/** + * @implements {API.Failure} + */ +export class Failure extends Error { + /** @type {true} */ + get error() { + return true + } + /* c8 ignore next 3 */ + describe() { + return this.name + } + get message() { + return this.describe() + } + + toJSON() { + const { error, name, message, stack } = this + return { error, name, message, stack } + } +} + +export class EscalatedCapability extends Failure { + /** + * @param {API.ParsedCapability} claimed + * @param {object} delegated + * @param {API.Failure} cause + */ + constructor(claimed, delegated, cause) { + super() + this.claimed = claimed + this.delegated = delegated + this.cause = cause + this.name = the('EscalatedCapability') + } + describe() { + return `Constraint violation: ${this.cause.message}` + } +} + +/** + * @implements {API.DelegationError} + */ +export class DelegationError extends Failure { + /** + * @param {(API.InvalidCapability | API.EscalatedDelegation | API.DelegationError)[]} causes + * @param {object} context + */ + constructor(causes, context) { + super() + this.name = the('InvalidClaim') + this.causes = causes + this.context = context + } + describe() { + return [ + `Can not derive ${this.context} from delegated capabilities:`, + ...this.causes.map(cause => li(cause.message)), + ].join('\n') + } + + /** + * @type {API.InvalidCapability | API.EscalatedDelegation | API.DelegationError} + */ + get cause() { + /* c8 ignore next 9 */ + if (this.causes.length !== 1) { + return this + } else { + const [cause] = this.causes + const value = cause.name === 'InvalidClaim' ? cause.cause : cause + Object.defineProperties(this, { cause: { value } }) + return value + } + } +} + +/** + * @implements {API.InvalidSignature} + */ +export class InvalidSignature extends Failure { + /** + * @param {API.Delegation} delegation + * @param {API.Verifier} verifier + */ + constructor(delegation, verifier) { + super() + this.name = the('InvalidSignature') + this.delegation = delegation + this.verifier = verifier + } + get issuer() { + return this.delegation.issuer + } + get audience() { + return this.delegation.audience + } + get key() { + return this.verifier.toDIDKey() + } + describe() { + const issuer = this.issuer.did() + const key = this.key + return ( + issuer.startsWith('did:key') + ? [ + `Proof ${this.delegation.cid} does not has a valid signature from ${key}`, + ] + : [ + `Proof ${this.delegation.cid} issued by ${issuer} does not has a valid signature from ${key}`, + ` ℹ️ Probably issuer signed with a different key, which got rotated, invalidating delegations that were issued with prior keys`, + ] + ).join('\n') + } +} + +/** + * @implements {API.UnavailableProof} + */ +export class UnavailableProof extends Failure { + /** + * @param {API.UCAN.Link} link + * @param {Error} [cause] + */ + constructor(link, cause) { + super() + this.name = the('UnavailableProof') + this.link = link + this.cause = cause + } + describe() { + return [ + `Linked proof '${this.link}' is not included and could not be resolved`, + ...(this.cause + ? [li(`Proof resolution failed with: ${this.cause.message}`)] + : []), + ].join('\n') + } +} + +export class DIDKeyResolutionError extends Failure { + /** + * @param {API.UCAN.DID} did + * @param {API.Unauthorized} [cause] + */ + constructor(did, cause) { + super() + this.name = the('DIDKeyResolutionError') + this.did = did + this.cause = cause + } + describe() { + return [ + `Unable to resolve '${this.did}' key`, + ...(this.cause ? [li(`Resolution failed: ${this.cause.message}`)] : []), + ].join('\n') + } +} + +/** + * @implements {API.InvalidAudience} + */ +export class InvalidAudience extends Failure { + /** + * @param {API.UCAN.Principal} audience + * @param {API.Delegation} delegation + */ + constructor(audience, delegation) { + super() + this.name = the('InvalidAudience') + this.audience = audience + this.delegation = delegation + } + describe() { + return `Delegation audience is '${this.delegation.audience.did()}' instead of '${this.audience.did()}'` + } + toJSON() { + const { error, name, audience, message, stack } = this + return { + error, + name, + audience: audience.did(), + delegation: { audience: this.delegation.audience.did() }, + message, + stack, + } + } +} + +/** + * @implements {API.MalformedCapability} + */ +export class MalformedCapability extends Failure { + /** + * @param {API.Capability} capability + * @param {API.Failure} cause + */ + constructor(capability, cause) { + super() + this.name = the('MalformedCapability') + this.capability = capability + this.cause = cause + } + describe() { + return [ + `Encountered malformed '${this.capability.can}' capability: ${format( + this.capability + )}`, + li(this.cause.message), + ].join('\n') + } +} + +export class UnknownCapability extends Failure { + /** + * @param {API.Capability} capability + */ + constructor(capability) { + super() + this.name = the('UnknownCapability') + this.capability = capability + } + /* c8 ignore next 3 */ + describe() { + return `Encountered unknown capability: ${format(this.capability)}` + } +} + +export class Expired extends Failure { + /** + * @param {API.Delegation & { expiration: number }} delegation + */ + constructor(delegation) { + super() + this.name = the('Expired') + this.delegation = delegation + } + describe() { + return `Proof ${this.delegation.cid} has expired on ${new Date( + this.delegation.expiration * 1000 + )}` + } + get expiredAt() { + return this.delegation.expiration + } + toJSON() { + const { error, name, expiredAt, message, stack } = this + return { + error, + name, + message, + expiredAt, + stack, + } + } +} + +export class NotValidBefore extends Failure { + /** + * @param {API.Delegation & { notBefore: number }} delegation + */ + constructor(delegation) { + super() + this.name = the('NotValidBefore') + this.delegation = delegation + } + describe() { + return `Proof ${this.delegation.cid} is not valid before ${new Date( + this.delegation.notBefore * 1000 + )}` + } + get validAt() { + return this.delegation.notBefore + } + toJSON() { + const { error, name, validAt, message, stack } = this + return { + error, + name, + message, + validAt, + stack, + } + } +} + +/** + * @param {unknown} capability + * @param {string|number} [space] + */ + +const format = (capability, space) => + JSON.stringify( + capability, + (_key, value) => { + /* c8 ignore next 2 */ + if (isLink(value)) { + return value.toString() + } else { + return value + } + }, + space + ) + +/** + * @param {string} message + */ +export const indent = (message, indent = ' ') => + `${indent}${message.split('\n').join(`\n${indent}`)}` + +/** + * @param {string} message + */ +export const li = message => indent(`- ${message}`) diff --git a/packages/core/src/lib.js b/packages/core/src/lib.js index 96492458..909d3a6b 100644 --- a/packages/core/src/lib.js +++ b/packages/core/src/lib.js @@ -10,3 +10,6 @@ export { } from './link.js' export * as UCAN from '@ipld/dag-ucan' export * as DID from '@ipld/dag-ucan/did' +export * from './capability.js' +export * from './error.js' +export * as Schema from './schema.js' diff --git a/packages/core/src/protocol.js b/packages/core/src/protocol.js new file mode 100644 index 00000000..e69de29b diff --git a/packages/core/src/protocol.ts b/packages/core/src/protocol.ts new file mode 100644 index 00000000..c25a3cd4 --- /dev/null +++ b/packages/core/src/protocol.ts @@ -0,0 +1,60 @@ +import * as API from '@ucanto/interface' + +interface Protocol { + api: Abilities + and( + protocol: Protocol + ): Protocol +} + +interface Task< + In extends API.Capability = API.Capability, + Out extends API.Result = API.Result< + unknown, + { error: true } + > +> { + can: In['can'] + uri: API.Reader + in: API.Reader + out: API.Reader +} + +type ProvidedCapabilities = { + [K: string]: Task | ProvidedCapabilities +} + +declare function task< + In extends API.Capability, + Ok extends {}, + Fail extends { error: true } +>(source: { + in: API.Reader + ok: API.Reader + error?: API.Reader +}): Task> + +declare function protocol( + tasks: Tasks +): InferProtocolAPI + +declare function api(task: T): InferTaskAPI + +type InferProtocolAPI = Tasks extends [infer T] + ? InferTaskAPI + : Tasks extends [infer T, ...infer TS] + ? InferTaskAPI & InferProtocolAPI + : never + +type InferTaskAPI = T extends Task + ? InferTaskPath + : never + +type InferTaskPath< + Path extends string, + T extends Task +> = Path extends `${infer Key}/${infer Rest}` + ? { [K in Key]: InferTaskPath } + : { [K in Path]: T } + +export { task, protocol, api } diff --git a/packages/core/src/schema.js b/packages/core/src/schema.js new file mode 100644 index 00000000..a2e1e64b --- /dev/null +++ b/packages/core/src/schema.js @@ -0,0 +1,6 @@ +export * as URI from './schema/uri.js' +export * as Link from './schema/link.js' +export * as DID from './schema/did.js' +export * as Text from './schema/text.js' +export { result } from './schema/result.js' +export * from './schema/schema.js' diff --git a/packages/core/src/schema/did.js b/packages/core/src/schema/did.js new file mode 100644 index 00000000..69277c58 --- /dev/null +++ b/packages/core/src/schema/did.js @@ -0,0 +1,39 @@ +import * as API from '@ucanto/interface' +import * as Schema from './schema.js' + +/** + * @template {string} Method + * @extends {Schema.API & API.URI<"did:">, string, void|Method>} + */ +class DIDSchema extends Schema.API { + /** + * @param {string} source + * @param {void|Method} method + */ + readWith(source, method) { + const prefix = method ? `did:${method}:` : `did:` + if (!source.startsWith(prefix)) { + return Schema.error(`Expected a ${prefix} but got "${source}" instead`) + } else { + return /** @type {API.DID} */ (source) + } + } +} + +const schema = Schema.string().refine(new DIDSchema()) + +export const did = () => schema +/** + * + * @param {unknown} input + */ +export const read = input => schema.read(input) + +/** + * @template {string} Method + * @param {{method?: Method}} options + */ +export const match = options => + /** @type {Schema.Schema & API.URI<"did:">>} */ ( + Schema.string().refine(new DIDSchema(options.method)) + ) diff --git a/packages/core/src/schema/link.js b/packages/core/src/schema/link.js new file mode 100644 index 00000000..a458b650 --- /dev/null +++ b/packages/core/src/schema/link.js @@ -0,0 +1,79 @@ +import * as API from '@ucanto/interface' +import { create, createLegacy, isLink, parse } from '../link.js' +import * as Schema from './schema.js' + +export { create, createLegacy, isLink, parse } + +/** + * @template {number} [Code=number] + * @template {number} [Alg=number] + * @template {1|0} [Version=0|1] + * @typedef {{code?:Code, algorithm?:Alg, version?:Version}} Settings + */ + +/** + * @template {number} Code + * @template {number} Alg + * @template {1|0} Version + * @extends {Schema.API, unknown, Settings>} + */ +class LinkSchema extends Schema.API { + /** + * + * @param {unknown} cid + * @param {Settings} settings + * @returns {Schema.ReadResult>} + */ + readWith(cid, { code, algorithm, version }) { + if (cid == null) { + return Schema.error(`Expected link but got ${cid} instead`) + } else { + if (!isLink(cid)) { + return Schema.error(`Expected link to be a CID instead of ${cid}`) + } else { + if (code != null && cid.code !== code) { + return Schema.error( + `Expected link to be CID with 0x${code.toString(16)} codec` + ) + } + if (algorithm != null && cid.multihash.code !== algorithm) { + return Schema.error( + `Expected link to be CID with 0x${algorithm.toString( + 16 + )} hashing algorithm` + ) + } + + if (version != null && cid.version !== version) { + return Schema.error( + `Expected link to be CID version ${version} instead of ${cid.version}` + ) + } + + // @ts-expect-error - can't infer version, code etc. + return cid + } + } + } +} + +/** @type {Schema.Schema, unknown>} */ +export const schema = new LinkSchema({}) + +export const link = () => schema + +/** + * @template {number} Code + * @template {number} Alg + * @template {1|0} Version + * @param {Settings} options + * @returns {Schema.Schema>} + */ +export const match = (options = {}) => new LinkSchema(options) + +/** + * @param {unknown} input + */ +export const read = input => schema.read(input) + +export const optional = () => schema.optional() diff --git a/packages/core/src/schema/result.js b/packages/core/src/schema/result.js new file mode 100644 index 00000000..6ddad2ff --- /dev/null +++ b/packages/core/src/schema/result.js @@ -0,0 +1,20 @@ +import * as API from '@ucanto/interface' +import * as Schema from './schema.js' +export const fail = Schema.struct({ error: true }) + +/** + * Creates a schema for the task result by specifying ok and error types + * + * @template T + * @template {{}} [X={message:string}] + * @param {{ok: Schema.Reader, error?: Schema.Reader}} source + * @returns {Schema.Schema>} + */ +export const result = ({ + ok, + error = Schema.struct({ message: Schema.string() }), +}) => + Schema.or( + /** @type {Schema.Reader} */ (ok), + fail.and(error) + ) diff --git a/packages/core/src/schema/schema.js b/packages/core/src/schema/schema.js new file mode 100644 index 00000000..c952fde8 --- /dev/null +++ b/packages/core/src/schema/schema.js @@ -0,0 +1,1280 @@ +import * as Schema from './type.js' + +export * from './type.js' + +/** + * @abstract + * @template [T=unknown] + * @template [I=unknown] + * @template [Settings=void] + * @extends {Schema.Base} + * @implements {Schema.Schema} + */ +export class API { + /** + * @param {Settings} settings + */ + constructor(settings) { + /** @protected */ + this.settings = settings + } + + toString() { + return `new ${this.constructor.name}()` + } + /** + * @abstract + * @param {I} input + * @param {Settings} settings + * @returns {Schema.ReadResult} + */ + /* c8 ignore next 3 */ + readWith(input, settings) { + throw new Error(`Abstract method readWith must be implemented by subclass`) + } + /** + * @param {I} input + * @returns {Schema.ReadResult} + */ + read(input) { + return this.readWith(input, this.settings) + } + + /** + * @param {unknown} value + * @returns {value is T} + */ + is(value) { + return !this.read(/** @type {I} */ (value))?.error + } + + /** + * @param {unknown} value + * @return {T} + */ + from(value) { + const result = this.read(/** @type {I} */ (value)) + if (result?.error) { + throw result + } else { + return result + } + } + + /** + * @returns {Schema.Schema} + */ + optional() { + return optional(this) + } + + /** + * @returns {Schema.Schema} + */ + nullable() { + return nullable(this) + } + + /** + * @returns {Schema.Schema} + */ + array() { + return array(this) + } + /** + * @template U + * @param {Schema.Reader} schema + * @returns {Schema.Schema} + */ + + or(schema) { + return or(this, schema) + } + + /** + * @template U + * @param {Schema.Reader} schema + * @returns {Schema.Schema} + */ + and(schema) { + return and(this, schema) + } + + /** + * @template {T} U + * @param {Schema.Reader} schema + * @returns {Schema.Schema} + */ + refine(schema) { + return refine(this, schema) + } + + /** + * @template {string} Kind + * @param {Kind} [kind] + * @returns {Schema.Schema, I>} + */ + brand(kind) { + return /** @type {Schema.Schema, I>} */ (this) + } + + /** + * @param {Schema.NotUndefined} value + * @returns {Schema.DefaultSchema, I>} + */ + default(value) { + // ⚠️ this.from will throw if wrong default is provided + const fallback = this.from(value) + // we also check that fallback is not undefined because that is the point + // of having a fallback + if (fallback === undefined) { + throw new Error(`Value of type undefined is not a vaild default`) + } + + const schema = new Default({ + reader: /** @type {Schema.Reader} */ (this), + value: /** @type {Schema.NotUndefined} */ (fallback), + }) + + return /** @type {Schema.DefaultSchema, I>} */ ( + schema + ) + } +} + +/** + * @template [I=unknown] + * @extends {API} + * @implements {Schema.Schema} + */ +class Never extends API { + toString() { + return 'never()' + } + /** + * @param {I} input + * @returns {Schema.ReadResult} + */ + read(input) { + return typeError({ expect: 'never', actual: input }) + } +} + +/** + * @template [I=unknown] + * @returns {Schema.Schema} + */ +export const never = () => new Never() + +/** + * @template [I=unknown] + * @extends API + * @implements {Schema.Schema} + */ +class Unknown extends API { + /** + * @param {I} input + */ + read(input) { + return /** @type {Schema.ReadResult}*/ (input) + } + toString() { + return 'unknown()' + } +} + +/** + * @template [I=unknown] + * @returns {Schema.Schema} + */ +export const unknown = () => new Unknown() + +/** + * @template O + * @template [I=unknown] + * @extends {API>} + * @implements {Schema.Schema} + */ +class Nullable extends API { + /** + * @param {I} input + * @param {Schema.Reader} reader + */ + readWith(input, reader) { + const result = reader.read(input) + if (result?.error) { + return input === null + ? null + : new UnionError({ + causes: [result, typeError({ expect: 'null', actual: input })], + }) + } else { + return result + } + } + toString() { + return `${this.settings}.nullable()` + } +} + +/** + * @template O + * @template [I=unknown] + * @param {Schema.Reader} schema + * @returns {Schema.Schema} + */ +export const nullable = schema => new Nullable(schema) + +/** + * @template O + * @template [I=unknown] + * @extends {API>} + * @implements {Schema.Schema} + */ +class Optional extends API { + optional() { + return this + } + /** + * @param {I} input + * @param {Schema.Reader} reader + * @returns {Schema.ReadResult} + */ + readWith(input, reader) { + const result = reader.read(input) + return result?.error && input === undefined ? undefined : result + } + toString() { + return `${this.settings}.optional()` + } +} + +/** + * @template {unknown} O + * @template [I=unknown] + * @extends {API, value:O & Schema.NotUndefined}>} + * @implements {Schema.DefaultSchema} + */ +class Default extends API { + /** + * @returns {Schema.DefaultSchema, I>} + */ + optional() { + // Short circuit here as we there is no point in wrapping this in optional. + return /** @type {Schema.DefaultSchema, I>} */ ( + this + ) + } + /** + * @param {I} input + * @param {object} options + * @param {Schema.Reader} options.reader + * @param {O} options.value + * @returns {Schema.ReadResult} + */ + readWith(input, { reader, value }) { + if (input === undefined) { + return /** @type {Schema.ReadResult} */ (value) + } else { + const result = reader.read(input) + + return /** @type {Schema.ReadResult} */ ( + result === undefined ? value : result + ) + } + } + toString() { + return `${this.settings.reader}.default(${JSON.stringify( + this.settings.value + )})` + } + + get value() { + return this.settings.value + } +} + +/** + * @template O + * @template [I=unknown] + * @param {Schema.Reader} schema + * @returns {Schema.Schema} + */ +export const optional = schema => new Optional(schema) + +/** + * @template O + * @template [I=unknown] + * @extends {API>} + * @implements {Schema.ArraySchema} + */ +class ArrayOf extends API { + /** + * @param {I} input + * @param {Schema.Reader} schema + */ + readWith(input, schema) { + if (!Array.isArray(input)) { + return typeError({ expect: 'array', actual: input }) + } + /** @type {O[]} */ + const results = [] + for (const [index, value] of input.entries()) { + const result = schema.read(value) + if (result?.error) { + return memberError({ at: index, cause: result }) + } else { + results.push(result) + } + } + return results + } + get element() { + return this.settings + } + toString() { + return `array(${this.element})` + } +} + +/** + * @template O + * @template [I=unknown] + * @param {Schema.Reader} schema + * @returns {Schema.ArraySchema} + */ +export const array = schema => new ArrayOf(schema) + +/** + * @template {Schema.Reader} T + * @template {[T, ...T[]]} U + * @template [I=unknown] + * @extends {API, I, U>} + * @implements {Schema.Schema, I>} + */ +class Tuple extends API { + /** + * @param {I} input + * @param {U} shape + * @returns {Schema.ReadResult>} + */ + readWith(input, shape) { + if (!Array.isArray(input)) { + return typeError({ expect: 'array', actual: input }) + } + if (input.length !== this.shape.length) { + return new SchemaError( + `Array must contain exactly ${this.shape.length} elements` + ) + } + + const results = [] + for (const [index, reader] of shape.entries()) { + const result = reader.read(input[index]) + if (result?.error) { + return memberError({ at: index, cause: result }) + } else { + results[index] = result + } + } + + return /** @type {Schema.InferTuple} */ (results) + } + + /** @type {U} */ + get shape() { + return this.settings + } + + toString() { + return `tuple([${this.shape.map(reader => reader.toString()).join(', ')}])` + } +} + +/** + * @template {Schema.Reader} T + * @template {[T, ...T[]]} U + * @template [I=unknown] + * @param {U} shape + * @returns {Schema.Schema, I>} + */ +export const tuple = shape => new Tuple(shape) + +/** + * @template {[unknown, ...unknown[]]} T + * @template [I=unknown] + * @extends {API}>} + * @implements {Schema.Schema} + */ +class Enum extends API { + /** + * @param {I} input + * @param {{type:string, variants:Set}} settings + * @returns {Schema.ReadResult} + */ + readWith(input, { variants, type }) { + if (variants.has(input)) { + return /** @type {Schema.ReadResult} */ (input) + } else { + return typeError({ expect: type, actual: input }) + } + } + toString() { + return this.settings.type + } +} + +/** + * @template {string} T + * @template {[T, ...T[]]} U + * @template [I=unknown] + * @param {U} variants + * @returns {Schema.Schema} + */ +const createEnum = variants => + new Enum({ + type: variants.join('|'), + variants: new Set(variants), + }) +export { createEnum as enum } + +/** + * @template {Schema.Reader} T + * @template {[T, ...T[]]} U + * @template [I=unknown] + * @extends {API, I, U>} + * @implements {Schema.Schema, I>} + */ +class Union extends API { + /** + * @param {I} input + * @param {U} variants + */ + readWith(input, variants) { + const causes = [] + for (const reader of variants) { + const result = reader.read(input) + if (result?.error) { + causes.push(result) + } else { + return result + } + } + return new UnionError({ causes }) + } + + get variants() { + return this.settings + } + toString() { + return `union([${this.variants.map(type => type.toString()).join(', ')}])` + } +} + +/** + * @template {Schema.Reader} T + * @template {[T, ...T[]]} U + * @template [I=unknown] + * @param {U} variants + * @returns {Schema.Schema, I>} + */ +const union = variants => new Union(variants) + +/** + * @template T, U + * @template [I=unknown] + * @param {Schema.Reader} left + * @param {Schema.Reader} right + * @returns {Schema.Schema} + */ +export const or = (left, right) => union([left, right]) + +/** + * @template {Schema.Reader} T + * @template {[T, ...T[]]} U + * @template [I=unknown] + * @extends {API, I, U>} + * @implements {Schema.Schema, I>} + */ +class Intersection extends API { + /** + * @param {I} input + * @param {U} schemas + * @returns {Schema.ReadResult>} + */ + readWith(input, schemas) { + const causes = [] + for (const schema of schemas) { + const result = schema.read(input) + if (result?.error) { + causes.push(result) + } + } + + return causes.length > 0 + ? new IntersectionError({ causes }) + : /** @type {Schema.ReadResult>} */ (input) + } + toString() { + return `intersection([${this.settings + .map(type => type.toString()) + .join(',')}])` + } +} + +/** + * @template {Schema.Reader} T + * @template {[T, ...T[]]} U + * @template [I=unknown] + * @param {U} variants + * @returns {Schema.Schema, I>} + */ +export const intersection = variants => new Intersection(variants) + +/** + * @template T, U + * @template [I=unknown] + * @param {Schema.Reader} left + * @param {Schema.Reader} right + * @returns {Schema.Schema} + */ +export const and = (left, right) => intersection([left, right]) + +/** + * @template [I=unknown] + * @extends {API} + */ +class Boolean extends API { + /** + * @param {I} input + */ + readWith(input) { + switch (input) { + case true: + case false: + return /** @type {boolean} */ (input) + default: + return typeError({ + expect: 'boolean', + actual: input, + }) + } + } + + toString() { + return `boolean()` + } +} + +/** @type {Schema.Schema} */ +const anyBoolean = new Boolean() + +export const boolean = () => anyBoolean + +/** + * @template {number} [O=number] + * @template [I=unknown] + * @template [Settings=void] + * @extends {API} + * @implements {Schema.NumberSchema} + */ +class UnknownNumber extends API { + /** + * @param {number} n + */ + greaterThan(n) { + return this.refine(greaterThan(n)) + } + /** + * @param {number} n + */ + lessThan(n) { + return this.refine(lessThan(n)) + } + + /** + * @template {O} U + * @param {Schema.Reader} schema + * @returns {Schema.NumberSchema} + */ + refine(schema) { + return new RefinedNumber({ base: this, schema }) + } +} + +/** + * @template [I=unknown] + * @extends {UnknownNumber} + * @implements {Schema.NumberSchema} + */ +class AnyNumber extends UnknownNumber { + /** + * @param {I} input + * @returns {Schema.ReadResult} + */ + readWith(input) { + return typeof input === 'number' + ? input + : typeError({ expect: 'number', actual: input }) + } + toString() { + return `number()` + } +} + +/** @type {Schema.NumberSchema} */ +const anyNumber = new AnyNumber() +export const number = () => anyNumber + +/** + * @template {number} [T=number] + * @template {T} [O=T] + * @template [I=unknown] + * @extends {UnknownNumber, schema:Schema.Reader}>} + * @implements {Schema.NumberSchema} + */ +class RefinedNumber extends UnknownNumber { + /** + * @param {I} input + * @param {{base:Schema.Reader, schema:Schema.Reader}} settings + * @returns {Schema.ReadResult} + */ + readWith(input, { base, schema }) { + const result = base.read(input) + return result?.error ? result : schema.read(result) + } + toString() { + return `${this.settings.base}.refine(${this.settings.schema})` + } +} + +/** + * @template {number} T + * @extends {API} + */ +class LessThan extends API { + /** + * @param {T} input + * @param {number} number + * @returns {Schema.ReadResult} + */ + readWith(input, number) { + if (input < number) { + return input + } else { + return error(`Expected ${input} < ${number}`) + } + } + toString() { + return `lessThan(${this.settings})` + } +} + +/** + * @template {number} T + * @param {number} n + * @returns {Schema.Schema} + */ +export const lessThan = n => new LessThan(n) + +/** + * @template {number} T + * @extends {API} + */ +class GreaterThan extends API { + /** + * @param {T} input + * @param {number} number + * @returns {Schema.ReadResult} + */ + readWith(input, number) { + if (input > number) { + return input + } else { + return error(`Expected ${input} > ${number}`) + } + } + toString() { + return `greaterThan(${this.settings})` + } +} + +/** + * @template {number} T + * @param {number} n + * @returns {Schema.Schema} + */ +export const greaterThan = n => new GreaterThan(n) + +const Integer = { + /** + * @param {number} input + * @returns {Schema.ReadResult} + */ + read(input) { + return Number.isInteger(input) + ? /** @type {Schema.Integer} */ (input) + : typeError({ + expect: 'integer', + actual: input, + }) + }, + toString() { + return `Integer` + }, +} + +const anyInteger = anyNumber.refine(Integer) +export const integer = () => anyInteger + +const Float = { + /** + * @param {number} number + * @returns {Schema.ReadResult} + */ + read(number) { + return Number.isFinite(number) + ? /** @type {Schema.Float} */ (number) + : typeError({ + expect: 'Float', + actual: number, + }) + }, + toString() { + return 'Float' + }, +} + +const anyFloat = anyNumber.refine(Float) +export const float = () => anyFloat + +/** + * @template {string} [O=string] + * @template [I=unknown] + * @template [Settings=void] + * @extends {API} + */ +class UnknownString extends API { + /** + * @template {O|unknown} U + * @param {Schema.Reader} schema + * @returns {Schema.StringSchema} + */ + refine(schema) { + const other = /** @type {Schema.Reader} */ (schema) + const rest = new RefinedString({ + base: this, + schema: other, + }) + + return /** @type {Schema.StringSchema} */ (rest) + } + /** + * @template {string} Prefix + * @param {Prefix} prefix + */ + startsWith(prefix) { + return this.refine(startsWith(prefix)) + } + /** + * @template {string} Suffix + * @param {Suffix} suffix + */ + endsWith(suffix) { + return this.refine(endsWith(suffix)) + } + toString() { + return `string()` + } +} + +/** + * @template O + * @template {string} [T=string] + * @template [I=unknown] + * @extends {UnknownString, schema:Schema.Reader}>} + * @implements {Schema.StringSchema} + */ +class RefinedString extends UnknownString { + /** + * @param {I} input + * @param {{base:Schema.Reader, schema:Schema.Reader}} settings + * @returns {Schema.ReadResult} + */ + readWith(input, { base, schema }) { + const result = base.read(input) + return result?.error + ? result + : /** @type {Schema.ReadResult} */ (schema.read(result)) + } + toString() { + return `${this.settings.base}.refine(${this.settings.schema})` + } +} + +/** + * @template [I=unknown] + * @extends {UnknownString} + * @implements {Schema.StringSchema} + */ +class AnyString extends UnknownString { + /** + * @param {I} input + * @returns {Schema.ReadResult} + */ + readWith(input) { + return typeof input === 'string' + ? input + : typeError({ expect: 'string', actual: input }) + } +} + +/** @type {Schema.StringSchema} */ +const anyString = new AnyString() +export const string = () => anyString + +/** + * @template {string} Prefix + * @template {string} Body + * @extends {API} + * @implements {Schema.Schema} + */ +class StartsWith extends API { + /** + * @param {Body} input + * @param {Prefix} prefix + */ + readWith(input, prefix) { + return input.startsWith(prefix) + ? /** @type {Schema.ReadResult} */ (input) + : error(`Expect string to start with "${prefix}" instead got "${input}"`) + } + get prefix() { + return this.settings + } + toString() { + return `startsWith("${this.prefix}")` + } +} + +/** + * @template {string} Prefix + * @template {string} Body + * @param {Prefix} prefix + * @returns {Schema.Schema<`${Prefix}${string}`, string>} + */ +export const startsWith = prefix => new StartsWith(prefix) + +/** + * @template {string} Suffix + * @template {string} Body + * @extends {API} + */ +class EndsWith extends API { + /** + * @param {Body} input + * @param {Suffix} suffix + */ + readWith(input, suffix) { + return input.endsWith(suffix) + ? /** @type {Schema.ReadResult} */ (input) + : error(`Expect string to end with "${suffix}" instead got "${input}"`) + } + get suffix() { + return this.settings + } + toString() { + return `endsWith("${this.suffix}")` + } +} + +/** + * @template {string} Suffix + * @param {Suffix} suffix + * @returns {Schema.Schema<`${string}${Suffix}`, string>} + */ +export const endsWith = suffix => new EndsWith(suffix) + +/** + * @template T + * @template {T} U + * @template [I=unknown] + * @extends {API, schema: Schema.Reader }>} + * @implements {Schema.Schema} + */ + +class Refine extends API { + /** + * @param {I} input + * @param {{ base: Schema.Reader, schema: Schema.Reader }} settings + */ + readWith(input, { base, schema }) { + const result = base.read(input) + return result?.error ? result : schema.read(result) + } + toString() { + return `${this.settings.base}.refine(${this.settings.schema})` + } +} + +/** + * @template T + * @template {T} U + * @template [I=unknown] + * @param {Schema.Reader} base + * @param {Schema.Reader} schema + * @returns {Schema.Schema} + */ +export const refine = (base, schema) => new Refine({ base, schema }) + +/** + * @template {null|boolean|string|number} T + * @template [I=unknown] + * @extends {API} + * @implements {Schema.LiteralSchema} + */ +class Literal extends API { + /** + * @param {I} input + * @param {T} expect + * @returns {Schema.ReadResult} + */ + readWith(input, expect) { + return input !== /** @type {unknown} */ (expect) + ? new LiteralError({ expect, actual: input }) + : expect + } + get value() { + return /** @type {Exclude} */ (this.settings) + } + /** + * @template {Schema.NotUndefined} U + * @param {U} value + */ + default(value = /** @type {U} */ (this.value)) { + return super.default(value) + } + toString() { + return `literal(${displayTypeName(this.value)})` + } +} + +/** + * @template {null|boolean|string|number} T + * @template [I=unknown] + * @param {T} value + * @returns {Schema.LiteralSchema} + */ +export const literal = value => new Literal(value) + +/** + * @template {{[key:string]: Schema.Reader}} U + * @template [I=unknown] + * @extends {API, I, U>} + */ +class Struct extends API { + /** + * @param {I} input + * @param {U} shape + * @returns {Schema.ReadResult>} + */ + readWith(input, shape) { + if (typeof input != 'object' || input === null || Array.isArray(input)) { + return typeError({ + expect: 'object', + actual: input, + }) + } + + const source = /** @type {{[K in keyof U]: unknown}} */ (input) + + const struct = /** @type {{[K in keyof U]: Schema.Infer}} */ ({}) + const entries = + /** @type {{[K in keyof U]: [K & string, U[K]]}[keyof U][]} */ ( + Object.entries(shape) + ) + + for (const [at, reader] of entries) { + const result = reader.read(source[at]) + if (result?.error) { + return memberError({ at, cause: result }) + } + // skip undefined because they mess up CBOR and are generally useless. + else if (result !== undefined) { + struct[at] = /** @type {Schema.Infer} */ (result) + } + } + + return struct + } + + /** @type {U} */ + get shape() { + // @ts-ignore - We declared `settings` private but we access it here + return this.settings + } + + toString() { + return [ + `struct({ `, + ...Object.entries(this.shape).map( + ([key, schema]) => `${key}: ${schema}, ` + ), + `})`, + ].join('') + } + + /** + * @param {Schema.InferStructSource} data + */ + create(data) { + return this.from(data || {}) + } + + /** + * @template {{[key:string]: Schema.Reader}} E + * @param {E} extension + * @returns {Schema.StructSchema} + */ + extend(extension) { + return new Struct({ ...this.shape, ...extension }) + } +} + +/** + * @template {null|boolean|string|number} T + * @template {{[key:string]: T|Schema.Reader}} U + * @template {{[K in keyof U]: U[K] extends Schema.Reader ? U[K] : Schema.LiteralSchema}} V + * @template [I=unknown] + * @param {U} fields + * @returns {Schema.StructSchema} + */ +export const struct = fields => { + const shape = + /** @type {{[K in keyof U]: Schema.Reader}} */ ({}) + /** @type {[keyof U & string, T|Schema.Reader][]} */ + const entries = Object.entries(fields) + + for (const [key, field] of entries) { + switch (typeof field) { + case 'number': + case 'string': + case 'boolean': + shape[key] = literal(field) + break + case 'object': + shape[key] = field === null ? literal(null) : field + break + default: + throw new Error( + `Invalid struct field "${key}", expected schema or literal, instead got ${typeof field}` + ) + } + } + + return new Struct(/** @type {V} */ (shape)) +} + +/** + * @param {string} message + * @returns {Schema.Error} + */ +export const error = message => new SchemaError(message) + +class SchemaError extends Error { + get name() { + return 'SchemaError' + } + /** @type {true} */ + get error() { + return true + } + /* c8 ignore next 3 */ + describe() { + return this.name + } + get message() { + return this.describe() + } + + toJSON() { + const { error, name, message, stack } = this + return { error, name, message, stack } + } +} + +class TypeError extends SchemaError { + /** + * @param {{expect:string, actual:unknown}} data + */ + constructor({ expect, actual }) { + super() + this.expect = expect + this.actual = actual + } + get name() { + return 'TypeError' + } + describe() { + return `Expected value of type ${this.expect} instead got ${displayTypeName( + this.actual + )}` + } +} + +/** + * @param {object} data + * @param {string} data.expect + * @param {unknown} data.actual + * @returns {Schema.Error} + */ +export const typeError = data => new TypeError(data) + +/** + * + * @param {unknown} value + */ +const displayTypeName = value => { + const type = typeof value + switch (type) { + case 'boolean': + case 'string': + return JSON.stringify(value) + // if these types we do not want JSON.stringify as it may mess things up + // eg turn NaN and Infinity to null + case 'bigint': + return `${value}n` + case 'number': + case 'symbol': + case 'undefined': + return String(value) + case 'object': + return value === null ? 'null' : Array.isArray(value) ? 'array' : 'object' + default: + return type + } +} + +class LiteralError extends SchemaError { + /** + * @param {{ + * expect:string|number|boolean|null + * actual:unknown + * }} data + */ + constructor({ expect, actual }) { + super() + this.expect = expect + this.actual = actual + } + get name() { + return 'LiteralError' + } + describe() { + return `Expected literal ${displayTypeName( + this.expect + )} instead got ${displayTypeName(this.actual)}` + } +} + +class ElementError extends SchemaError { + /** + * @param {{at:number, cause:Schema.Error}} data + */ + constructor({ at, cause }) { + super() + this.at = at + this.cause = cause + } + get name() { + return 'ElementError' + } + describe() { + return [ + `Array contains invalid element at ${this.at}:`, + li(this.cause.message), + ].join('\n') + } +} + +class FieldError extends SchemaError { + /** + * @param {{at:string, cause:Schema.Error}} data + */ + constructor({ at, cause }) { + super() + this.at = at + this.cause = cause + } + get name() { + return 'FieldError' + } + describe() { + return [ + `Object contains invalid field "${this.at}":`, + li(this.cause.message), + ].join('\n') + } +} + +/** + * @param {object} options + * @param {string|number} options.at + * @param {Schema.Error} options.cause + * @returns {Schema.Error} + */ +export const memberError = ({ at, cause }) => + typeof at === 'string' + ? new FieldError({ at, cause }) + : new ElementError({ at, cause }) + +class UnionError extends SchemaError { + /** + * @param {{causes: Schema.Error[]}} data + */ + constructor({ causes }) { + super() + this.causes = causes + } + get name() { + return 'UnionError' + } + describe() { + const { causes } = this + return [ + `Value does not match any type of the union:`, + ...causes.map(cause => li(cause.message)), + ].join('\n') + } +} + +class IntersectionError extends SchemaError { + /** + * @param {{causes: Schema.Error[]}} data + */ + constructor({ causes }) { + super() + this.causes = causes + } + get name() { + return 'IntersectionError' + } + describe() { + const { causes } = this + return [ + `Value does not match following types of the intersection:`, + ...causes.map(cause => li(cause.message)), + ].join('\n') + } +} + +/** + * @param {string} message + */ +const indent = (message, indent = ' ') => + `${indent}${message.split('\n').join(`\n${indent}`)}` + +/** + * @param {string} message + */ +const li = message => indent(`- ${message}`) diff --git a/packages/core/src/schema/text.js b/packages/core/src/schema/text.js new file mode 100644 index 00000000..546950bb --- /dev/null +++ b/packages/core/src/schema/text.js @@ -0,0 +1,34 @@ +import * as Schema from './schema.js' + +const schema = Schema.string() + +export const text = () => schema + +/** + * @param {{pattern: RegExp}} options + */ +export const match = ({ pattern }) => schema.refine(new Match(pattern)) + +/** + * @param {unknown} input + */ +export const read = input => schema.read(input) + +/** + * @extends {Schema.API} + */ +class Match extends Schema.API { + /** + * @param {string} source + * @param {RegExp} pattern + */ + readWith(source, pattern) { + if (!pattern.test(source)) { + return Schema.error( + `Expected to match ${pattern} but got "${source}" instead` + ) + } else { + return source + } + } +} diff --git a/packages/core/src/schema/type.js b/packages/core/src/schema/type.js new file mode 100644 index 00000000..e69de29b diff --git a/packages/core/src/schema/type.ts b/packages/core/src/schema/type.ts new file mode 100644 index 00000000..0bce1e55 --- /dev/null +++ b/packages/core/src/schema/type.ts @@ -0,0 +1,142 @@ +import { Failure as Error, Result, Phantom } from '@ucanto/interface' + +export interface Reader< + O = unknown, + I = unknown, + X extends { error: true } = Error +> { + read(input: I): Result +} + +export type { Error } + +export type ReadResult = Result + +export interface Schema< + O extends unknown = unknown, + I extends unknown = unknown +> extends Reader { + optional(): Schema + nullable(): Schema + array(): Schema + default(value: NotUndefined): DefaultSchema, I> + or(other: Reader): Schema + and(other: Reader): Schema + refine(schema: Reader): Schema + + brand(kind?: K): Schema, I> + + is(value: unknown): value is O + from(value: I): O +} + +export interface DefaultSchema< + O extends unknown = unknown, + I extends unknown = unknown +> extends Schema { + readonly value: O & NotUndefined + optional(): DefaultSchema, I> +} + +export type NotUndefined = Exclude + +export interface ArraySchema extends Schema { + element: Reader +} + +export interface LiteralSchema< + T extends string | number | boolean | null, + I = unknown +> extends Schema { + default(value?: T): DefaultSchema, I> + readonly value: T +} + +export interface NumberSchema< + N extends number = number, + I extends unknown = unknown +> extends Schema { + greaterThan(n: number): NumberSchema + lessThan(n: number): NumberSchema + + refine(schema: Reader): NumberSchema +} + +export interface StructSchema< + U extends { [key: string]: Reader } = {}, + I extends unknown = unknown +> extends Schema, I> { + shape: U + + create(input: MarkEmptyOptional>): InferStruct + extend( + extension: E + ): StructSchema +} + +export interface StringSchema + extends Schema { + startsWith( + prefix: Prefix + ): StringSchema + endsWith( + suffix: Suffix + ): StringSchema + refine(schema: Reader): StringSchema +} + +declare const Marker: unique symbol +export type Branded = T & { + [Marker]: T +} + +export type Integer = number & Phantom<{ typeof: 'integer' }> +export type Float = number & Phantom<{ typeof: 'float' }> + +export type Infer = T extends Reader ? T : never + +export type InferIntesection = { + [K in keyof U]: (input: Infer) => void +}[number] extends (input: infer T) => void + ? T + : never + +export type InferUnion = Infer + +export type InferTuple = { + [K in keyof U]: Infer +} + +export type InferStruct = MarkOptionals<{ + [K in keyof U]: Infer +}> + +export type InferStructSource = + // MarkEmptyOptional< + MarkOptionals<{ + [K in keyof U]: InferSource + }> +// > + +export type InferSource = U extends DefaultSchema + ? T | undefined + : U extends StructSchema + ? InferStructSource + : U extends Reader + ? T + : never + +export type MarkEmptyOptional = RequiredKeys extends never + ? T | void + : T + +type MarkOptionals = Pick> & + Partial>> + +type RequiredKeys = { + [k in keyof T]: undefined extends T[k] ? never : k +}[keyof T] & {} + +type OptionalKeys = { + [k in keyof T]: undefined extends T[k] ? k : never +}[keyof T] & {} diff --git a/packages/core/src/schema/uri.js b/packages/core/src/schema/uri.js new file mode 100644 index 00000000..64796640 --- /dev/null +++ b/packages/core/src/schema/uri.js @@ -0,0 +1,64 @@ +import * as API from '@ucanto/interface' +import * as Schema from './schema.js' + +/** + * @template {API.Protocol} [P=API.Protocol] + * @typedef {{protocol: P}} Options + */ + +/** + * @template {Options} O + * @extends {Schema.API, unknown, Partial>} + */ +class URISchema extends Schema.API { + /** + * @param {unknown} input + * @param {Partial} options + * @returns {Schema.ReadResult>} + */ + readWith(input, { protocol } = {}) { + if (typeof input !== 'string' && !(input instanceof URL)) { + return Schema.error( + `Expected URI but got ${input === null ? 'null' : typeof input}` + ) + } + + try { + const url = new URL(String(input)) + if (protocol != null && url.protocol !== protocol) { + return Schema.error(`Expected ${protocol} URI instead got ${url.href}`) + } else { + return /** @type {API.URI} */ (url.href) + } + } catch (_) { + return Schema.error(`Invalid URI`) + } + } +} + +const schema = new URISchema({}) + +/** + * @returns {Schema.Schema} + */ +export const uri = () => schema + +/** + * @param {unknown} input + */ +export const read = input => schema.read(input) + +/** + * @template {API.Protocol} P + * @template {Options

} O + * @param {O} options + * @returns {Schema.Schema, unknown>} + */ +export const match = options => new URISchema(options) + +/** + * @template {string} [Scheme=string] + * @param {`${Scheme}:${string}`} input + */ +export const from = input => + /** @type {API.URI<`${Scheme}:`>} */ (schema.from(input)) diff --git a/packages/core/src/util.js b/packages/core/src/util.js new file mode 100644 index 00000000..efc740b4 --- /dev/null +++ b/packages/core/src/util.js @@ -0,0 +1,53 @@ +/** + * @template {string|boolean|number|[unknown, ...unknown[]]} T + * @param {T} value + * @returns {T} + */ +export const the = value => value + +/** + * @template {{}} O + * @param {O} object + * @returns {({ [K in keyof O]: [K, O[K]][] }[keyof O])|[[never, never]]} + */ + +export const entries = object => /** @type {any} */ (Object.entries(object)) + +/** + * @template T + * @param {T[][]} dataset + * @returns {T[][]} + */ +export const combine = ([first, ...rest]) => { + const results = first.map(value => [value]) + for (const values of rest) { + const tuples = results.splice(0) + for (const value of values) { + for (const tuple of tuples) { + results.push([...tuple, value]) + } + } + } + return results +} + +/** + * @template T + * @param {T[]} left + * @param {T[]} right + * @returns {T[]} + */ +export const intersection = (left, right) => { + const [result, other] = + left.length < right.length + ? [new Set(left), new Set(right)] + : [new Set(right), new Set(left)] + + for (const item of result) { + if (!other.has(item)) { + result.delete(item) + } + } + + return [...result] +} diff --git a/packages/core/test/protocol.spec.js b/packages/core/test/protocol.spec.js new file mode 100644 index 00000000..dc67baa1 --- /dev/null +++ b/packages/core/test/protocol.spec.js @@ -0,0 +1,190 @@ +import { test, assert } from './test.js' +import * as Protocol from '../src/protocol.js' +import { capability, Failure, Schema } from '../src/lib.js' +import * as API from '@ucanto/interface' +import { equalWith, equalLink } from './util.js' + +/** + * Represents the top `{ can: '*', with: 'did:key:zAlice' }` capability, which we often + * also call account linking. + * + * @see {@link https://github.com/ucan-wg/spec#52-top} + */ +export const top = capability({ + can: '*', + with: Schema.DID, +}) + +/** + * Capability can only be delegated (but not invoked) allowing audience to + * derived any `store/` prefixed capability for the (memory) space identified + * by did:key in the `with` field. + */ +export const store = top.derive({ + to: capability({ + can: 'store/*', + /** + * did:key identifier of the (memory) space where CAR is intended to + * be stored. + */ + with: Schema.DID, + }), + /** + * `store/*` can be derived from the `*` capability as long as `with` field + * is the same. + */ + derives: equalWith, +}) + +// Right now ucanto does not yet has native `*` support, which means +// `store/add` can not be derived from `*` event though it can be +// derived from `store/*`. As a workaround we just define base capability +// here so all store capabilities could be derived from either `*` or +// `store/*`. +const base = top.or(store) + +const add = base.derive({ + to: capability({ + can: 'store/add', + /** + * did:key identifier of the (memory) space where CAR is intended to + * be stored. + */ + with: Schema.DID, + nb: { + /** + * CID of the CAR file to be stored. Service will provision write target + * for this exact CAR file for agent to PUT or POST it. Attempt to write + * any other content will fail. + */ + link: Schema.Link, + /** + * Size of the CAR file to be stored. Service will provision write target + * for this exact size. Attempt to write a larger CAR file will fail. + */ + size: Schema.integer(), + /** + * Agent may optionally provide a link to a related CAR file using `origin` + * field. This is useful when storing large DAGs, agent could shard it + * across multiple CAR files and then link each shard with a previous one. + * + * Providing this relation tells service that given CAR is shard of the + * larger DAG as opposed to it being intentionally partial DAG. When DAG is + * not sharded, there will be only one `store/add` with `origin` left out. + */ + origin: Schema.Link.optional(), + }, + derives: (claim, from) => { + const result = equalLink(claim, from) + if (result.error) { + return result + } else if (claim.nb.size !== undefined && from.nb.size !== undefined) { + return claim.nb.size > from.nb.size + ? new Failure( + `Size constraint violation: ${claim.nb.size} > ${from.nb.size}` + ) + : true + } else { + return true + } + }, + }), + /** + * `store/add` can be derived from the `store/*` & `*` capability + * as long as the `with` fields match. + */ + derives: equalWith, +}) + +/** + * Capability can be used to remove the stored CAR file from the (memory) + * space identified by `with` field. + */ +export const remove = base.derive({ + to: capability({ + can: 'store/remove', + /** + * did:key identifier of the (memory) space where CAR is intended to + * be stored. + */ + with: URI.match({ protocol: 'did:' }), + nb: { + /** + * CID of the CAR file to be removed from the store. + */ + link: Link, + }, + derives: equalLink, + }), + /** + * `store/remove` can be derived from the `store/*` & `*` capability + * as long as the `with` fields match. + */ + derives: equalWith, +}) + +/** + * Capability can be invoked to request a list of stored CAR files in the + * (memory) space identified by `with` field. + */ +export const list = base.derive({ + to: capability({ + can: 'store/list', + /** + * did:key identifier of the (memory) space where CAR is intended to + * be stored. + */ + with: URI.match({ protocol: 'did:' }), + nb: { + /** + * A pointer that can be moved back and forth on the list. + * It can be used to paginate a list for instance. + */ + cursor: Schema.string().optional(), + /** + * Maximum number of items per page. + */ + size: Schema.integer().optional(), + }, + derives: (claimed, delegated) => { + if (claimed.with !== delegated.with) { + return new Failure( + `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` + ) + } + return true + }, + }), + /** + * `store/list` can be derived from the `store/*` & `*` capability + * as long as the `with` fields match. + */ + derives: equalWith, +}) + +test('api', () => { + const api = Protocol.protocol([ + Protocol.task({ + in: add, + ok: Schema.struct({ link: Schema.Link }), + }), + Protocol.task({ + in: list, + ok: Schema.struct({ + cursor: Schema.string().optional(), + results: Schema.array( + Schema.struct({ + link: Schema.Link, + size: Schema.integer(), + }) + ), + }), + }), + Protocol.task({ + in: remove, + ok: Schema.struct({}), + }), + ]) + + const out = { ...api.out.read({}) } +}) diff --git a/packages/core/test/util.js b/packages/core/test/util.js new file mode 100644 index 00000000..825094c5 --- /dev/null +++ b/packages/core/test/util.js @@ -0,0 +1,43 @@ +import * as API from '@ucanto/interface' +import { Failure } from '../src/lib.js' + +/** + * @template {API.ParsedCapability<"store/add"|"store/remove", API.URI<'did:'>, {link?: API.Link}>} T + * @param {T} claimed + * @param {T} delegated + * @returns {API.Result} + */ +export const equalLink = (claimed, delegated) => { + if (claimed.with !== delegated.with) { + return new Failure( + `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` + ) + } else if ( + delegated.nb.link && + `${delegated.nb.link}` !== `${claimed.nb.link}` + ) { + return new Failure( + `Link ${claimed.nb.link ? `${claimed.nb.link}` : ''} violates imposed ${ + delegated.nb.link + } constraint.` + ) + } else { + return true + } +} + +/** + * Checks that `with` on claimed capability is the same as `with` + * in delegated capability. Note this will ignore `can` field. + * + * @param {API.ParsedCapability} child + * @param {API.ParsedCapability} parent + */ +export function equalWith(child, parent) { + return ( + child.with === parent.with || + new Failure( + `Can not derive ${child.can} with ${child.with} from ${parent.with}` + ) + ) +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 3ccd24b2..87d86b0d 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -99,5 +99,5 @@ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": ["src", "test"], - "references": [{ "path": "../interface" }, { "path": "../principal" }] + "references": [{ "path": "../interface" }, { "path": "../principal" }, { "path": "../validator" } ] } diff --git a/packages/interface/src/capability.ts b/packages/interface/src/capability.ts index bee368a1..bc16865d 100644 --- a/packages/interface/src/capability.ts +++ b/packages/interface/src/capability.ts @@ -166,7 +166,8 @@ export type InferCaveatParams = keyof T extends never } export interface TheCapabilityParser> - extends CapabilityParser { + extends CapabilityParser, + Reader { readonly can: M['value']['can'] create( diff --git a/packages/server/src/agent.js b/packages/server/src/agent.js new file mode 100644 index 00000000..9b5d3bc2 --- /dev/null +++ b/packages/server/src/agent.js @@ -0,0 +1,56 @@ +import * as API from '@ucanto/interface' + +/** + * @template {API.DID} ID + * @param {{ + * signer: API.Signer + * authority?: API.Principal + * proofs?: API.Delegation[] + * }} options + */ +export const create = ({ signer, authority = signer, proofs = [] }) => + new Agent({ signer, authority, proofs, context: {} }) + +/** + * @template {API.DID} ID + * @template {{}} Context + */ +class Agent { + /** + * @param {object} options + * @param {API.Signer} options.signer + * @param {API.Principal} options.authority + * @param {API.Delegation[]} options.proofs + * @param {Context} options.context + */ + constructor(options) { + this.options = options + } + get context() { + return this.options.context + } + + did() { + return this.options.signer.did() + } + + /** + * @template T + * @param {API.ByteView} payload + */ + + sign(payload) { + return this.options.signer.sign(payload) + } + /** + * @template {{}} Extra + * @param {Extra} extra + * @returns {Agent} + */ + with(extra) { + return new Agent({ + ...this.options, + context: { ...this.options.context, ...extra }, + }) + } +} diff --git a/packages/validator/src/capability.js b/packages/validator/src/capability.js index 68154db2..e0c539b6 100644 --- a/packages/validator/src/capability.js +++ b/packages/validator/src/capability.js @@ -114,6 +114,20 @@ class Capability extends Unit { this.descriptor = { derives, ...descriptor } } + /** + * @param {unknown} source + */ + read(source) { + try { + const result = this.create(/** @type {any} */ (source)) + return /** @type {API.Result>, API.Failure>} */ ( + result + ) + } catch (error) { + return /** @type {any} */ (error).cause + } + } + /** * @param {API.InferCreateOptions>} options */ @@ -344,6 +358,13 @@ class Derive extends Unit { this.derives = derives } + /** + * @param {unknown} source + */ + read(source) { + return this.to.read(source) + } + /** * @type {typeof this.to['create']} */ diff --git a/packages/validator/src/schema/task.js b/packages/validator/src/schema/task.js new file mode 100644 index 00000000..ee27c702 --- /dev/null +++ b/packages/validator/src/schema/task.js @@ -0,0 +1,45 @@ +import * as API from '@ucanto/interface' +import * as Schema from './schema.js' + +/** + * @template {API.URI} URI + * @template {API.Ability} Ability + * @template {{}} Caveats + * @param {{with: Schema.Reader, can: Ability, nb: Schema.StructSchema}} source + * @returns {{ can: Ability, schema: Schema.StructSchema<{with:Schema.Reader, can: Schema.Reader, nb:Schema.StructSchema}>}} + */ +const capability = source => ({ + can: source.can, + schema: Schema.struct({ + with: source.with, + can: Schema.literal(source.can), + nb: source.nb, + }), +}) + +/** + * @template {API.URI} URI + * @template {API.Ability} Ability + * @template {{}} Caveats + */ +class Capability { + /** + * @param {{with: Schema.Reader, can: Ability, nb: Schema.StructSchema}} source + */ + constructor(source) { + this.can = source.can + this.schema = Schema.struct({ + with: source.with, + can: Schema.literal(source.can), + nb: source.nb, + }) + } +} + +/** + * @template {{}} In + * @template {unknown} Out + * @template {{error: true}} Error + * @param {{in: API.Reader, out: API.Reader, unknown>}} options + */ +export const task = options => ({ ...options }) From 1e10270c2c3e8eeb3d610defd78de256444a0878 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Tue, 24 Jan 2023 12:40:47 -0800 Subject: [PATCH 08/17] Revert "chore: add last-release-sha to fix release-please" This reverts commit 0b04d0dacba27673049e236d83b6c52918a4f6ed. --- .github/release-please-config.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/release-please-config.json b/.github/release-please-config.json index d4f813ec..b311e6ac 100644 --- a/.github/release-please-config.json +++ b/.github/release-please-config.json @@ -2,7 +2,6 @@ "plugins": [ "node-workspace" ], - "last-release-sha": "e6f8d809f4e06fc2920dcb28e1b5926bb73f4e46", "bootstrap-sha": "f814e75a89d3ed7c3488a8cb7af8d94f0cfba440", "packages": { "packages/principal": {}, From 9266c8e9504f96cb30582221d1929fc8776ba61d Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Tue, 24 Jan 2023 21:57:07 -0800 Subject: [PATCH 09/17] implement protocol composition logic --- packages/core/src/capability.js | 11 + packages/core/src/lib.js | 1 + packages/core/src/protocol.ts | 60 ---- packages/core/src/protocol/protocol.js | 66 +++++ .../src/{protocol.js => protocol/type.js} | 0 packages/core/src/protocol/type.ts | 32 +++ packages/core/src/schema.js | 1 + packages/core/src/schema/task.js | 52 ++++ packages/core/src/schema/type.ts | 18 +- packages/core/test/capabilities/any.js | 10 + packages/core/test/capabilities/store.js | 150 ++++++++++ packages/core/test/capabilities/upload.js | 161 +++++++++++ packages/core/test/{ => capabilities}/util.js | 27 +- packages/core/test/protocol.spec.js | 257 ++++++------------ packages/interface/src/capability.ts | 8 +- packages/validator/src/capability.js | 11 + 16 files changed, 632 insertions(+), 233 deletions(-) delete mode 100644 packages/core/src/protocol.ts create mode 100644 packages/core/src/protocol/protocol.js rename packages/core/src/{protocol.js => protocol/type.js} (100%) create mode 100644 packages/core/src/protocol/type.ts create mode 100644 packages/core/src/schema/task.js create mode 100644 packages/core/test/capabilities/any.js create mode 100644 packages/core/test/capabilities/store.js create mode 100644 packages/core/test/capabilities/upload.js rename packages/core/test/{ => capabilities}/util.js (66%) diff --git a/packages/core/src/capability.js b/packages/core/src/capability.js index 6edcf411..8a8ffbc3 100644 --- a/packages/core/src/capability.js +++ b/packages/core/src/capability.js @@ -115,6 +115,13 @@ class Capability extends Unit { this.descriptor = { derives, ...descriptor } } + /** + * @type {API.Reader} + */ + get with() { + return this.descriptor.with + } + /** * @param {unknown} source */ @@ -387,6 +394,10 @@ class Derive extends Unit { get can() { return this.to.can } + get with() { + return this.to.with + } + /** * @param {API.Source} capability * @returns {API.MatchResult>} diff --git a/packages/core/src/lib.js b/packages/core/src/lib.js index 909d3a6b..e9030adb 100644 --- a/packages/core/src/lib.js +++ b/packages/core/src/lib.js @@ -13,3 +13,4 @@ export * as DID from '@ipld/dag-ucan/did' export * from './capability.js' export * from './error.js' export * as Schema from './schema.js' +export * from './protocol/protocol.js' diff --git a/packages/core/src/protocol.ts b/packages/core/src/protocol.ts deleted file mode 100644 index c25a3cd4..00000000 --- a/packages/core/src/protocol.ts +++ /dev/null @@ -1,60 +0,0 @@ -import * as API from '@ucanto/interface' - -interface Protocol { - api: Abilities - and( - protocol: Protocol - ): Protocol -} - -interface Task< - In extends API.Capability = API.Capability, - Out extends API.Result = API.Result< - unknown, - { error: true } - > -> { - can: In['can'] - uri: API.Reader - in: API.Reader - out: API.Reader -} - -type ProvidedCapabilities = { - [K: string]: Task | ProvidedCapabilities -} - -declare function task< - In extends API.Capability, - Ok extends {}, - Fail extends { error: true } ->(source: { - in: API.Reader - ok: API.Reader - error?: API.Reader -}): Task> - -declare function protocol( - tasks: Tasks -): InferProtocolAPI - -declare function api(task: T): InferTaskAPI - -type InferProtocolAPI = Tasks extends [infer T] - ? InferTaskAPI - : Tasks extends [infer T, ...infer TS] - ? InferTaskAPI & InferProtocolAPI - : never - -type InferTaskAPI = T extends Task - ? InferTaskPath - : never - -type InferTaskPath< - Path extends string, - T extends Task -> = Path extends `${infer Key}/${infer Rest}` - ? { [K in Key]: InferTaskPath } - : { [K in Path]: T } - -export { task, protocol, api } diff --git a/packages/core/src/protocol/protocol.js b/packages/core/src/protocol/protocol.js new file mode 100644 index 00000000..45d95e68 --- /dev/null +++ b/packages/core/src/protocol/protocol.js @@ -0,0 +1,66 @@ +import * as The from './type.js' + +/** + * @template {[The.Task, ...The.Task[]]} Tasks + * @param {Tasks} tasks + * @returns {The.Protocol>} + */ +export const protocol = tasks => new Protocol(build(tasks)) + +/** + * @template {[The.Task, ...The.Task[]]} Tasks + * @param {Tasks} tasks + * @returns {The.InferAbilities} + */ +const build = tasks => { + const abilities = /** @type {The.InferAbilities} */ ({}) + + for (const task of tasks) { + const path = task.can.split('/') + const name = path.pop() + if (!name) { + throw new RangeError( + `Expected task that has a valid 'can' field instead got '${task.can}'` + ) + } + const namespace = buildNamespace(abilities, path) + if (namespace[name] && namespace[name] !== task) { + throw new RangeError( + `All tasks must have unique 'can' fields, but multiple tasks with "can: '${task.can}'" had been provided` + ) + } + namespace[name] = task + } + + return abilities +} + +/** + * @template {Record} T + * @param {T} source + * @param {string[]} path + */ +const buildNamespace = (source, path) => { + /** @type {Record} */ + let target = source + for (const name of path) { + if (target[name] == null) { + target[name] = {} + } + target = /** @type {Record} */ (target[name]) + } + return target +} + +/** + * @template {The.TaskGroup} Abilities + * @implements {The.Protocol} + */ +class Protocol { + /** + * @param {Abilities} abilities + */ + constructor(abilities) { + this.abilities = abilities + } +} diff --git a/packages/core/src/protocol.js b/packages/core/src/protocol/type.js similarity index 100% rename from packages/core/src/protocol.js rename to packages/core/src/protocol/type.js diff --git a/packages/core/src/protocol/type.ts b/packages/core/src/protocol/type.ts new file mode 100644 index 00000000..f0188667 --- /dev/null +++ b/packages/core/src/protocol/type.ts @@ -0,0 +1,32 @@ +import type { Task } from '../schema.js' +import type { Ability } from '@ucanto/interface' + +export type { Task, Ability } + +export interface Protocol { + abilities: Abilities + // and( + // protocol: Protocol + // ): Protocol +} + +export type TaskGroup = { + [K: string]: Task | TaskGroup +} + +export type InferAbilities = Tasks extends [infer T] + ? InferAbility + : Tasks extends [infer T, ...infer TS] + ? InferAbility & InferAbilities + : never + +type InferAbility = T extends Task + ? InferNamespacedAbility + : never + +type InferNamespacedAbility< + Path extends string, + T extends Task +> = Path extends `${infer Key}/${infer Rest}` + ? { [K in Key]: InferNamespacedAbility } + : { [K in Path]: T } diff --git a/packages/core/src/schema.js b/packages/core/src/schema.js index a2e1e64b..2ee85725 100644 --- a/packages/core/src/schema.js +++ b/packages/core/src/schema.js @@ -4,3 +4,4 @@ export * as DID from './schema/did.js' export * as Text from './schema/text.js' export { result } from './schema/result.js' export * from './schema/schema.js' +export * from './schema/task.js' diff --git a/packages/core/src/schema/task.js b/packages/core/src/schema/task.js new file mode 100644 index 00000000..d20c0ba3 --- /dev/null +++ b/packages/core/src/schema/task.js @@ -0,0 +1,52 @@ +import * as API from '@ucanto/interface' +import { result } from './result.js' +import * as Schema from './type.js' + +/** + * @template {API.Capability} In - Input of the task, which (currently) + * corresponds to some capability. + * @template {{}} Ok - Ok type signifies successful task result. It encodes best + * practice by requiring empty map extension, this way API changes are rarely + * backwards incompatible & in most cases could be avoided using new named + * field. + * @template {{error:true}} [Error=API.Failure] - Error type signifies failed + * task result. It simply requires `error: true` field to allow differentiating + * from Ok type. + * + * @param {object} source + * @param {API.CapabilitySchema} source.in + * @param {API.Reader} source.ok + * @param {API.Reader} [source.error] + * @returns {Schema.Task>} + */ +export const task = source => new Task(source) + +/** + * Class is an implementation of {@link Schema.Task} interface. We use class so + * we could add some convenience methods. + * + * @template {API.Capability} In + * @template {{}} Ok + * @template {{error:true}} Error + */ +class Task { + /** + * @param {object} source + * @param {API.CapabilitySchema} source.in + * @param {API.Reader} source.ok + * @param {API.Reader} [source.error] + */ + constructor(source) { + this.source = source + this.out = result(source) + } + get can() { + return this.source.in.can + } + get with() { + return this.source.in.with + } + get in() { + return this.source.in + } +} diff --git a/packages/core/src/schema/type.ts b/packages/core/src/schema/type.ts index 0bce1e55..a3d5a83b 100644 --- a/packages/core/src/schema/type.ts +++ b/packages/core/src/schema/type.ts @@ -1,4 +1,9 @@ -import { Failure as Error, Result, Phantom } from '@ucanto/interface' +import { + Failure as Error, + Result, + Capability, + Phantom, +} from '@ucanto/interface' export interface Reader< O = unknown, @@ -140,3 +145,14 @@ type RequiredKeys = { type OptionalKeys = { [k in keyof T]: undefined extends T[k] ? k : never }[keyof T] & {} + +export interface Task< + In extends Capability = Capability, + Out extends Result = Result +> { + can: In['can'] + with: Reader + in: Reader + + out: Reader +} diff --git a/packages/core/test/capabilities/any.js b/packages/core/test/capabilities/any.js new file mode 100644 index 00000000..6f04800d --- /dev/null +++ b/packages/core/test/capabilities/any.js @@ -0,0 +1,10 @@ +import { capability, Schema } from '../../src/lib.js' + +/** + * Represents the top `{ can: '*', with: 'did:key:zAlice' }` capability, which we often + * also call account linking. + */ +export const _ = capability({ + can: '*', + with: Schema.DID, +}) diff --git a/packages/core/test/capabilities/store.js b/packages/core/test/capabilities/store.js new file mode 100644 index 00000000..6e8a3966 --- /dev/null +++ b/packages/core/test/capabilities/store.js @@ -0,0 +1,150 @@ +import { capability, Failure, Schema } from '../../src/lib.js' +import { equalWith, equalLink } from './util.js' +import * as Any from './any.js' + +/** + * Capability can only be delegated (but not invoked) allowing audience to + * derived any `store/` prefixed capability for the (memory) space identified + * by did:key in the `with` field. + */ +export const _ = Any._.derive({ + to: capability({ + can: 'store/*', + /** + * did:key identifier of the (memory) space where CAR is intended to + * be stored. + */ + with: Schema.DID, + }), + /** + * `store/*` can be derived from the `*` capability as long as `with` field + * is the same. + */ + derives: equalWith, +}) + +// Right now ucanto does not yet has native `*` support, which means +// `store/add` can not be derived from `*` event though it can be +// derived from `store/*`. As a workaround we just define base capability +// here so all store capabilities could be derived from either `*` or +// `store/*`. +const base = _.or(Any._) + +export const add = base.derive({ + to: capability({ + can: 'store/add', + /** + * did:key identifier of the (memory) space where CAR is intended to + * be stored. + */ + with: Schema.DID.match({ method: 'key' }), + nb: { + /** + * CID of the CAR file to be stored. Service will provision write target + * for this exact CAR file for agent to PUT or POST it. Attempt to write + * any other content will fail. + */ + link: Schema.Link, + /** + * Size of the CAR file to be stored. Service will provision write target + * for this exact size. Attempt to write a larger CAR file will fail. + */ + size: Schema.integer(), + /** + * Agent may optionally provide a link to a related CAR file using `origin` + * field. This is useful when storing large DAGs, agent could shard it + * across multiple CAR files and then link each shard with a previous one. + * + * Providing this relation tells service that given CAR is shard of the + * larger DAG as opposed to it being intentionally partial DAG. When DAG is + * not sharded, there will be only one `store/add` with `origin` left out. + */ + origin: Schema.Link.optional(), + }, + derives: (claim, from) => { + const result = equalLink(claim, from) + if (result.error) { + return result + } else if (claim.nb.size !== undefined && from.nb.size !== undefined) { + return claim.nb.size > from.nb.size + ? new Failure( + `Size constraint violation: ${claim.nb.size} > ${from.nb.size}` + ) + : true + } else { + return true + } + }, + }), + /** + * `store/add` can be derived from the `store/*` & `*` capability + * as long as the `with` fields match. + */ + derives: equalWith, +}) + +/** + * Capability can be used to remove the stored CAR file from the (memory) + * space identified by `with` field. + */ +export const remove = base.derive({ + to: capability({ + can: 'store/remove', + /** + * did:key identifier of the (memory) space where CAR is intended to + * be stored. + */ + with: Schema.URI.match({ protocol: 'did:' }), + nb: { + /** + * CID of the CAR file to be removed from the store. + */ + link: Schema.Link, + }, + derives: equalLink, + }), + /** + * `store/remove` can be derived from the `store/*` & `*` capability + * as long as the `with` fields match. + */ + derives: equalWith, +}) + +/** + * Capability can be invoked to request a list of stored CAR files in the + * (memory) space identified by `with` field. + */ +export const list = base.derive({ + to: capability({ + can: 'store/list', + /** + * did:key identifier of the (memory) space where CAR is intended to + * be stored. + */ + with: Schema.URI.match({ protocol: 'did:' }), + nb: { + /** + * A pointer that can be moved back and forth on the list. + * It can be used to paginate a list for instance. + */ + cursor: Schema.string().optional(), + /** + * Maximum number of items per page. + */ + size: Schema.integer().optional(), + }, + derives: (claimed, delegated) => { + if (claimed.with !== delegated.with) { + return new Failure( + `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` + ) + } + return true + }, + }), + /** + * `store/list` can be derived from the `store/*` & `*` capability + * as long as the `with` fields match. + */ + derives: equalWith, +}) diff --git a/packages/core/test/capabilities/upload.js b/packages/core/test/capabilities/upload.js new file mode 100644 index 00000000..b8185565 --- /dev/null +++ b/packages/core/test/capabilities/upload.js @@ -0,0 +1,161 @@ +/** + * Upload Capabilities + * + * These can be imported directly with: + * ```js + * import * as Account from '@web3-storage/capabilities/upload' + * ``` + * + * @module + */ +import { capability, Schema } from '../../src/lib.js' +import { equalWith, fail, equal } from './util.js' +import { _ as top } from './any.js' + +/** + * Schema representing a link (a.k.a CID) to a CAR file. Enforces CAR codec code and CID v1. + */ +export const CARLink = Schema.Link.match({ code: 0x0202, version: 1 }) + +export const Space = Schema.DID.match({ method: 'key ' }) + +/** + * Capability can only be delegated (but not invoked) allowing audience to + * derived any `upload/` prefixed capability for the (memory) space identified + * by did:key in the `with` field. + */ +export const _ = top.derive({ + to: capability({ + can: 'upload/*', + /** + * did:key identifier of the (memory) space where upload is add to the + * upload list. + */ + with: Space, + derives: equalWith, + }), + /** + * `upload/*` can be derived from the `*` capability as long as `with` field + * is the same. + */ + derives: equalWith, +}) + +// Right now ucanto does not yet has native `*` support, which means +// `upload/add` can not be derived from `*` event though it can be +// derived from `upload/*`. As a workaround we just define base capability +// here so all store capabilities could be derived from either `*` or +// `upload/*`. +const base = top.or(_) + +/** + * Capability allows an agent to add an arbitrary DAG (root) to the upload list + * of the specified (memory) space (identified by did:key in the `with` field). + * It is recommended to provide an optional list of shard links that contain + * fragments of this DAG, as it allows system to optimize block discovery, it is + * also a way to communicate DAG partiality - this upload contains partial DAG + * identified by the given `root`. + * + * Usually when agent wants to upload a DAG it will encode it as a one or more + * CAR files (shards) and invoke `store/add` capability for each one. Once all + * shards are stored it will invoke `upload/add` capability (providing link to + * a DAG root and all the shards) to add it the upload list. + * + * That said `upload/add` could be invoked without invoking `store/add`s e.g. + * because another (memory) space may already have those CARs. + * + * Note: If DAG with the given root is already in the upload list, invocation + * will simply update `shards` to be a union of existing and new shards. + */ +export const add = base.derive({ + to: capability({ + can: 'upload/add', + /** + * did:key identifier of the (memory) space where uploaded is added. + */ + with: Space, + nb: { + /** + * Root CID of the DAG to be added to the upload list. + */ + root: Schema.Link, + /** + * CIDs to the CAR files that contain blocks of the DAG. + */ + shards: CARLink.array().optional(), + }, + derives: (self, from) => { + return ( + fail(equalWith(self, from)) || + fail(equal(self.nb.root, from.nb.root, 'root')) || + fail(equal(self.nb.shards, from.nb.shards, 'shards')) || + true + ) + }, + }), + /** + * `upload/add` can be derived from the `upload/*` & `*` capability + * as long as `with` fields match. + */ + derives: equalWith, +}) + +/** + * Capability removes an upload (identified by it's root CID) from the upload + * list. Please note that removing an upload does not delete corresponding shards + * from the store, however that could be done via `store/remove` invocations. + */ +export const remove = base.derive({ + to: capability({ + can: 'upload/remove', + /** + * did:key identifier of the (memory) space where uploaded is removed from. + */ + with: Space, + nb: { + /** + * Root CID of the DAG to be removed from the upload list. + */ + root: Schema.Link, + }, + derives: (self, from) => { + return ( + fail(equalWith(self, from)) || + fail(equal(self.nb.root, from.nb.root, 'root')) || + true + ) + }, + }), + /** + * `upload/remove` can be derived from the `upload/*` & `*` capability + * as long as `with` fields match. + */ + derives: equalWith, +}) + +/** + * Capability can be invoked to request a list of uploads in the (memory) space + * identified by the `with` field. + */ +export const list = base.derive({ + to: capability({ + can: 'upload/list', + with: Space, + nb: { + /** + * A pointer that can be moved back and forth on the list. + * It can be used to paginate a list for instance. + */ + cursor: Schema.string().optional(), + /** + * Maximum number of items per page. + */ + size: Schema.integer().optional(), + }, + }), + /** + * `upload/list` can be derived from the `upload/*` & `*` capability + * as long as with fields match. + */ + derives: equalWith, +}) diff --git a/packages/core/test/util.js b/packages/core/test/capabilities/util.js similarity index 66% rename from packages/core/test/util.js rename to packages/core/test/capabilities/util.js index 825094c5..aeae1693 100644 --- a/packages/core/test/util.js +++ b/packages/core/test/capabilities/util.js @@ -1,5 +1,5 @@ import * as API from '@ucanto/interface' -import { Failure } from '../src/lib.js' +import { Failure } from '../../src/lib.js' /** * @template {API.ParsedCapability<"store/add"|"store/remove", API.URI<'did:'>, {link?: API.Link}>} T @@ -41,3 +41,28 @@ export function equalWith(child, parent) { ) ) } + +/** + * @param {unknown} child + * @param {unknown} parent + * @param {string} constraint + */ + +export function equal(child, parent, constraint) { + if (parent === undefined || parent === '*') { + return true + } else if (String(child) === String(parent)) { + return true + } else { + return new Failure( + `Constrain violation: ${child} violates imposed ${constraint} constraint ${parent}` + ) + } +} + +/** + * @param {API.Failure | true} value + */ +export function fail(value) { + return value === true ? undefined : value +} diff --git a/packages/core/test/protocol.spec.js b/packages/core/test/protocol.spec.js index dc67baa1..de08cd3e 100644 --- a/packages/core/test/protocol.spec.js +++ b/packages/core/test/protocol.spec.js @@ -1,190 +1,107 @@ import { test, assert } from './test.js' -import * as Protocol from '../src/protocol.js' -import { capability, Failure, Schema } from '../src/lib.js' +import { capability, Failure, Schema, protocol } from '../src/lib.js' import * as API from '@ucanto/interface' -import { equalWith, equalLink } from './util.js' +import * as Store from './capabilities/store.js' +import * as Upload from './capabilities/upload.js' /** - * Represents the top `{ can: '*', with: 'did:key:zAlice' }` capability, which we often - * also call account linking. - * - * @see {@link https://github.com/ucan-wg/spec#52-top} + * @template T + * @param {API.Reader} item */ -export const top = capability({ - can: '*', - with: Schema.DID, +const list = item => + Schema.struct({ + cursor: Schema.string().optional(), + size: Schema.integer(), + results: Schema.array(item), + }) + +const car = Schema.struct({ + link: Schema.Link, + size: Schema.integer(), + origin: Schema.Link.optional(), }) -/** - * Capability can only be delegated (but not invoked) allowing audience to - * derived any `store/` prefixed capability for the (memory) space identified - * by did:key in the `with` field. - */ -export const store = top.derive({ - to: capability({ - can: 'store/*', - /** - * did:key identifier of the (memory) space where CAR is intended to - * be stored. - */ - with: Schema.DID, - }), - /** - * `store/*` can be derived from the `*` capability as long as `with` field - * is the same. - */ - derives: equalWith, +const unit = Schema.struct({}) + +const up = Schema.struct({ + root: Schema.Link, + shards: Upload.CARLink.array().optional(), }) -// Right now ucanto does not yet has native `*` support, which means -// `store/add` can not be derived from `*` event though it can be -// derived from `store/*`. As a workaround we just define base capability -// here so all store capabilities could be derived from either `*` or -// `store/*`. -const base = top.or(store) +const CARAdded = Schema.struct({ + status: 'done', + with: Schema.DID.match({ method: 'key' }), + link: Schema.Link, +}) -const add = base.derive({ - to: capability({ - can: 'store/add', - /** - * did:key identifier of the (memory) space where CAR is intended to - * be stored. - */ - with: Schema.DID, - nb: { - /** - * CID of the CAR file to be stored. Service will provision write target - * for this exact CAR file for agent to PUT or POST it. Attempt to write - * any other content will fail. - */ - link: Schema.Link, - /** - * Size of the CAR file to be stored. Service will provision write target - * for this exact size. Attempt to write a larger CAR file will fail. - */ - size: Schema.integer(), - /** - * Agent may optionally provide a link to a related CAR file using `origin` - * field. This is useful when storing large DAGs, agent could shard it - * across multiple CAR files and then link each shard with a previous one. - * - * Providing this relation tells service that given CAR is shard of the - * larger DAG as opposed to it being intentionally partial DAG. When DAG is - * not sharded, there will be only one `store/add` with `origin` left out. - */ - origin: Schema.Link.optional(), - }, - derives: (claim, from) => { - const result = equalLink(claim, from) - if (result.error) { - return result - } else if (claim.nb.size !== undefined && from.nb.size !== undefined) { - return claim.nb.size > from.nb.size - ? new Failure( - `Size constraint violation: ${claim.nb.size} > ${from.nb.size}` - ) - : true - } else { - return true - } - }, - }), - /** - * `store/add` can be derived from the `store/*` & `*` capability - * as long as the `with` fields match. - */ - derives: equalWith, +const CARReceiving = Schema.struct({ + status: 'upload', + with: Schema.DID.match({ method: 'key' }), + link: Schema.Link, + url: Schema.URI, }) -/** - * Capability can be used to remove the stored CAR file from the (memory) - * space identified by `with` field. - */ -export const remove = base.derive({ - to: capability({ - can: 'store/remove', - /** - * did:key identifier of the (memory) space where CAR is intended to - * be stored. - */ - with: URI.match({ protocol: 'did:' }), - nb: { - /** - * CID of the CAR file to be removed from the store. - */ - link: Link, - }, - derives: equalLink, +// roughly based on https://github.com/web3-storage/w3protocol/blob/6e83725072ee093dda16549675b9ac943ea096b7/packages/upload-client/src/types.ts#L30-L41 + +const store = { + add: Schema.task({ + in: Store.add, + ok: CARAdded.or(CARReceiving), }), - /** - * `store/remove` can be derived from the `store/*` & `*` capability - * as long as the `with` fields match. - */ - derives: equalWith, -}) + list: Schema.task({ + in: Store.list, + ok: list(car), + }), + remove: Schema.task({ + in: Store.remove, + ok: unit, + }), +} -/** - * Capability can be invoked to request a list of stored CAR files in the - * (memory) space identified by `with` field. - */ -export const list = base.derive({ - to: capability({ - can: 'store/list', - /** - * did:key identifier of the (memory) space where CAR is intended to - * be stored. - */ - with: URI.match({ protocol: 'did:' }), - nb: { - /** - * A pointer that can be moved back and forth on the list. - * It can be used to paginate a list for instance. - */ - cursor: Schema.string().optional(), - /** - * Maximum number of items per page. - */ - size: Schema.integer().optional(), - }, - derives: (claimed, delegated) => { - if (claimed.with !== delegated.with) { - return new Failure( - `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` - ) - } - return true - }, +const upload = { + add: Schema.task({ + in: Upload.add, + ok: up, + }), + list: Schema.task({ + in: Upload.list, + ok: list(up), + }), + remove: Schema.task({ + in: Upload.remove, + ok: up, }), - /** - * `store/list` can be derived from the `store/*` & `*` capability - * as long as the `with` fields match. - */ - derives: equalWith, +} + +test('task api', () => { + assert.deepInclude(store.add, { + can: 'store/add', + with: Schema.DID.match({ method: 'key' }), + in: Store.add, + out: Schema.result({ ok: CARAdded.or(CARReceiving) }), + }) + + assert.deepInclude(store.remove, { + can: 'store/remove', + with: Schema.URI.match({ protocol: 'did:' }), + in: Store.remove, + out: Schema.result({ ok: unit }), + }) }) -test('api', () => { - const api = Protocol.protocol([ - Protocol.task({ - in: add, - ok: Schema.struct({ link: Schema.Link }), - }), - Protocol.task({ - in: list, - ok: Schema.struct({ - cursor: Schema.string().optional(), - results: Schema.array( - Schema.struct({ - link: Schema.Link, - size: Schema.integer(), - }) - ), - }), - }), - Protocol.task({ - in: remove, - ok: Schema.struct({}), - }), +test('protocol derives capabilities', () => { + const w3Store = protocol([store.add, store.list, store.remove]) + + assert.deepEqual(w3Store.abilities, { store }) + + const w3 = protocol([ + store.add, + store.list, + store.remove, + upload.add, + upload.list, + upload.remove, ]) - const out = { ...api.out.read({}) } + assert.deepEqual(w3.abilities, { store, upload }) }) diff --git a/packages/interface/src/capability.ts b/packages/interface/src/capability.ts index bc16865d..943abc5f 100644 --- a/packages/interface/src/capability.ts +++ b/packages/interface/src/capability.ts @@ -165,9 +165,15 @@ export type InferCaveatParams = keyof T extends never [K in keyof T]: T[K] extends { toJSON(): infer U } ? U : T[K] } +export interface CapabilitySchema extends Reader { + readonly can: T['can'] + + readonly with: Reader +} + export interface TheCapabilityParser> extends CapabilityParser, - Reader { + CapabilitySchema { readonly can: M['value']['can'] create( diff --git a/packages/validator/src/capability.js b/packages/validator/src/capability.js index e0c539b6..dd54cd76 100644 --- a/packages/validator/src/capability.js +++ b/packages/validator/src/capability.js @@ -230,6 +230,14 @@ class Capability extends Unit { return this.descriptor.can } + /** + * @type {API.Reader} + */ + + get with() { + return this.descriptor.with + } + /** * @param {API.Source} source * @returns {API.MatchResult>>>} @@ -386,6 +394,9 @@ class Derive extends Unit { get can() { return this.to.can } + get with() { + return this.to.with + } /** * @param {API.Source} capability * @returns {API.MatchResult>} From 61d13a39779b22d023593cd68a7f7b44a6540d37 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Tue, 24 Jan 2023 21:57:48 -0800 Subject: [PATCH 10/17] chore: remove obsolete file --- packages/validator/src/schema/task.js | 45 --------------------------- 1 file changed, 45 deletions(-) delete mode 100644 packages/validator/src/schema/task.js diff --git a/packages/validator/src/schema/task.js b/packages/validator/src/schema/task.js deleted file mode 100644 index ee27c702..00000000 --- a/packages/validator/src/schema/task.js +++ /dev/null @@ -1,45 +0,0 @@ -import * as API from '@ucanto/interface' -import * as Schema from './schema.js' - -/** - * @template {API.URI} URI - * @template {API.Ability} Ability - * @template {{}} Caveats - * @param {{with: Schema.Reader, can: Ability, nb: Schema.StructSchema}} source - * @returns {{ can: Ability, schema: Schema.StructSchema<{with:Schema.Reader, can: Schema.Reader, nb:Schema.StructSchema}>}} - */ -const capability = source => ({ - can: source.can, - schema: Schema.struct({ - with: source.with, - can: Schema.literal(source.can), - nb: source.nb, - }), -}) - -/** - * @template {API.URI} URI - * @template {API.Ability} Ability - * @template {{}} Caveats - */ -class Capability { - /** - * @param {{with: Schema.Reader, can: Ability, nb: Schema.StructSchema}} source - */ - constructor(source) { - this.can = source.can - this.schema = Schema.struct({ - with: source.with, - can: Schema.literal(source.can), - nb: source.nb, - }) - } -} - -/** - * @template {{}} In - * @template {unknown} Out - * @template {{error: true}} Error - * @param {{in: API.Reader, out: API.Reader, unknown>}} options - */ -export const task = options => ({ ...options }) From e2649c9500a18d676f8e8a4a851aada848ce4e2b Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Tue, 24 Jan 2023 23:39:55 -0800 Subject: [PATCH 11/17] feat!: merge validator into core --- .github/release-please-config.json | 3 +- .github/release-please-manifest.json | 9 +- .github/workflows/server.yml | 2 - .github/workflows/validator.yml | 88 -- package.json | 3 +- packages/agent/package.json | 3 +- packages/agent/src/agent.js | 2 +- packages/agent/src/api.ts | 2 +- packages/agent/test/basic.spec.js | 4 +- packages/agent/test/infer.spec.js | 4 +- packages/agent/tsconfig.json | 3 +- packages/core/package.json | 16 + packages/core/src/capability.js | 2 +- packages/core/src/error.js | 144 +- packages/core/src/schema/did.js | 6 + packages/core/src/schema/schema.js | 64 +- packages/core/src/schema/type.ts | 13 + .../src/lib.js => core/src/validator.js} | 25 +- packages/core/test/capabilities/util.js | 36 + .../test/capabilities}/voucher.js | 4 +- .../test/capabilities/voucher}/types.js | 0 .../test/capabilities/voucher}/types.ts | 0 .../test/capability.spec.js | 121 +- .../{validator => core}/test/delegate.spec.js | 12 +- .../{validator => core}/test/error.spec.js | 2 +- .../test/extra-schema.spec.js | 38 + .../test/inference.spec.js | 16 +- .../{validator => core}/test/mailto.spec.js | 13 +- .../{validator => core}/test/schema.spec.js | 20 +- .../test/schema/fixtures.js | 69 +- .../{validator => core}/test/schema/util.js | 0 packages/core/test/test.js | 2 + .../test/validator.spec.js} | 19 +- packages/core/tsconfig.json | 2 +- packages/server/package.json | 3 +- packages/server/src/handler.js | 2 +- packages/server/src/server.js | 9 +- packages/server/test/handler.spec.js | 5 - packages/server/tsconfig.json | 3 +- packages/validator/CHANGELOG.md | 575 -------- packages/validator/package.json | 76 - packages/validator/src/capability.js | 837 ----------- packages/validator/src/error.js | 319 ---- packages/validator/src/schema.js | 5 - packages/validator/src/schema/did.js | 39 - packages/validator/src/schema/link.js | 79 - packages/validator/src/schema/schema.js | 1280 ----------------- packages/validator/src/schema/text.js | 34 - packages/validator/src/schema/type.js | 0 packages/validator/src/schema/type.ts | 142 -- packages/validator/src/schema/uri.js | 64 - packages/validator/src/util.js | 53 - packages/validator/test/fixtures.js | 18 - packages/validator/test/test.js | 6 - packages/validator/test/util.js | 60 - packages/validator/tsconfig.json | 108 -- 56 files changed, 520 insertions(+), 3944 deletions(-) delete mode 100644 .github/workflows/validator.yml rename packages/{validator/src/lib.js => core/src/validator.js} (98%) rename packages/{validator/test => core/test/capabilities}/voucher.js (88%) rename packages/{validator/test => core/test/capabilities/voucher}/types.js (100%) rename packages/{validator/test => core/test/capabilities/voucher}/types.ts (100%) rename packages/{validator => core}/test/capability.spec.js (95%) rename packages/{validator => core}/test/delegate.spec.js (97%) rename packages/{validator => core}/test/error.spec.js (98%) rename packages/{validator => core}/test/extra-schema.spec.js (92%) rename packages/{validator => core}/test/inference.spec.js (93%) rename packages/{validator => core}/test/mailto.spec.js (91%) rename packages/{validator => core}/test/schema.spec.js (96%) rename packages/{validator => core}/test/schema/fixtures.js (92%) rename packages/{validator => core}/test/schema/util.js (100%) rename packages/{validator/test/lib.spec.js => core/test/validator.spec.js} (97%) delete mode 100644 packages/validator/CHANGELOG.md delete mode 100644 packages/validator/package.json delete mode 100644 packages/validator/src/capability.js delete mode 100644 packages/validator/src/error.js delete mode 100644 packages/validator/src/schema.js delete mode 100644 packages/validator/src/schema/did.js delete mode 100644 packages/validator/src/schema/link.js delete mode 100644 packages/validator/src/schema/schema.js delete mode 100644 packages/validator/src/schema/text.js delete mode 100644 packages/validator/src/schema/type.js delete mode 100644 packages/validator/src/schema/type.ts delete mode 100644 packages/validator/src/schema/uri.js delete mode 100644 packages/validator/src/util.js delete mode 100644 packages/validator/test/fixtures.js delete mode 100644 packages/validator/test/test.js delete mode 100644 packages/validator/test/util.js delete mode 100644 packages/validator/tsconfig.json diff --git a/.github/release-please-config.json b/.github/release-please-config.json index b311e6ac..d2a2ce78 100644 --- a/.github/release-please-config.json +++ b/.github/release-please-config.json @@ -9,7 +9,6 @@ "packages/core": {}, "packages/interface": {}, "packages/server": {}, - "packages/transport": {}, - "packages/validator": {} + "packages/transport": {} } } diff --git a/.github/release-please-manifest.json b/.github/release-please-manifest.json index af61ac67..beba9b66 100644 --- a/.github/release-please-manifest.json +++ b/.github/release-please-manifest.json @@ -1 +1,8 @@ -{"packages/client":"4.0.3","packages/core":"4.0.3","packages/interface":"4.0.3","packages/principal":"4.0.3","packages/server":"4.0.3","packages/transport":"4.0.3","packages/validator":"4.0.3"} \ No newline at end of file +{ + "packages/client": "4.0.3", + "packages/core": "4.0.3", + "packages/interface": "4.0.3", + "packages/principal": "4.0.3", + "packages/server": "4.0.3", + "packages/transport": "4.0.3" +} diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index 6a423b1f..f708a9aa 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -10,7 +10,6 @@ on: - 'packages/core/**' - 'packages/transport/**' - 'packages/client/**' - - 'packages/validator/**' - 'packages/server/**' pull_request: branches: @@ -20,7 +19,6 @@ on: - 'packages/core/**' - 'packages/transport/**' - 'packages/client/**' - - 'packages/validator/**' - 'packages/server/**' - '.github/workflows/server.yml' jobs: diff --git a/.github/workflows/validator.yml b/.github/workflows/validator.yml deleted file mode 100644 index e238a85f..00000000 --- a/.github/workflows/validator.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: validator - -on: - workflow_dispatch: - push: - branches: - - main - paths: - - 'packages/interface/**' - - 'packages/core/**' - - 'packages/transport/**' - - 'packages/client/**' - - 'packages/validator/**' - pull_request: - branches: - - main - paths: - - 'packages/interface/**' - - 'packages/core/**' - - 'packages/transport/**' - - 'packages/client/**' - - 'packages/validator/**' - - '.github/workflows/validator.yml' -jobs: - check: - name: Typecheck - runs-on: ubuntu-latest - strategy: - matrix: - node-version: - - 16 - project: - - validator - steps: - - uses: actions/checkout@v2 - - - name: Setup node ${{ matrix.node-version }} - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - - - name: Install dependencies - uses: pnpm/action-setup@v2.0.1 - id: pnpm-install - with: - version: 7 - run_install: true - - - name: Typecheck - uses: gozala/typescript-error-reporter-action@v1.0.8 - with: - project: packages/${{matrix.project}}/tsconfig.json - test: - name: Test - runs-on: ${{ matrix.os }} - - strategy: - matrix: - node-version: - - 14 - - 16 - os: - - ubuntu-latest - - windows-latest - - macos-latest - project: - - validator - - steps: - - uses: actions/checkout@v2 - - - name: Setup Node - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - - - name: Install dependencies - uses: pnpm/action-setup@v2.0.1 - id: pnpm-install - with: - version: 7 - run_install: true - - - name: Test (Node) - run: pnpm run --if-present --dir packages/${{matrix.project}} test:node - - - name: Test (Web) - run: pnpm run --if-present --dir packages/${{matrix.project}} test:web diff --git a/package.json b/package.json index 4644a3d2..bae5c9c1 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,7 @@ "packages/client", "packages/server", "packages/transport", - "packages/principal", - "packages/validator" + "packages/principal" ], "scripts": { "format": "prettier --write '**/*.{js,ts,yml,json}' --ignore-path .gitignore", diff --git a/packages/agent/package.json b/packages/agent/package.json index 380bd899..07c8a27b 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -29,8 +29,7 @@ }, "dependencies": { "@ucanto/core": "^4.0.2", - "@ucanto/interface": "^4.0.2", - "@ucanto/validator": "^4.0.2" + "@ucanto/interface": "^4.0.2" }, "devDependencies": { "@types/chai": "^4.3.3", diff --git a/packages/agent/src/agent.js b/packages/agent/src/agent.js index a1176c22..178e8dd1 100644 --- a/packages/agent/src/agent.js +++ b/packages/agent/src/agent.js @@ -1,5 +1,5 @@ import * as API from '../src/api.js' -import { Schema, URI } from '@ucanto/validator' +import { Schema } from '@ucanto/core' export const fail = Schema.struct({ error: true }) diff --git a/packages/agent/src/api.ts b/packages/agent/src/api.ts index a686e1dd..dea3008d 100644 --- a/packages/agent/src/api.ts +++ b/packages/agent/src/api.ts @@ -26,7 +26,7 @@ import { IssuedInvocationView, } from '@ucanto/interface' -import { Schema } from '@ucanto/validator' +import { Schema } from '@ucanto/core' // This is the interface of the module we'll have export interface AgentModule { diff --git a/packages/agent/test/basic.spec.js b/packages/agent/test/basic.spec.js index 20fc3874..d204c982 100644 --- a/packages/agent/test/basic.spec.js +++ b/packages/agent/test/basic.spec.js @@ -1,6 +1,6 @@ import * as API from '../src/api.js' import { DID as Principal } from '@ucanto/core' -import { capability, Schema, DID, URI, Text, Link } from '@ucanto/validator' +import { capability, Schema } from '@ucanto/core' import { ed25519 } from '@ucanto/principal' import { CAR } from '@ucanto/transport' import { result, task } from '../src/agent.js' @@ -8,7 +8,7 @@ import * as Agent from '../src/agent.js' import { test, assert } from './test.js' test('create resource', () => { - const Space = DID.match({ method: 'key' }) + const Space = Schema.DID.match({ method: 'key' }) const Unit = Schema.struct({}) const Echo = Schema.struct({ message: Schema.string(), diff --git a/packages/agent/test/infer.spec.js b/packages/agent/test/infer.spec.js index c8745615..b478e473 100644 --- a/packages/agent/test/infer.spec.js +++ b/packages/agent/test/infer.spec.js @@ -1,6 +1,6 @@ import * as API from '../src/api.js' -import { DID as Principal } from '@ucanto/core' -import { capability, Schema, DID, URI, Text, Link } from '@ucanto/validator' +import { capability, Schema, DID as Principal } from '@ucanto/core' +import { DID, URI, Text, Link } from '@ucanto/core/schema' import { ed25519 } from '@ucanto/principal' import { CAR } from '@ucanto/transport' import { result, task } from '../src/agent.js' diff --git a/packages/agent/tsconfig.json b/packages/agent/tsconfig.json index 3e549c1a..88b09a3f 100644 --- a/packages/agent/tsconfig.json +++ b/packages/agent/tsconfig.json @@ -103,7 +103,6 @@ { "path": "../interface" }, { "path": "../core" }, { "path": "../transport" }, - { "path": "../client" }, - { "path": "../validator" } + { "path": "../client" } ] } diff --git a/packages/core/package.json b/packages/core/package.json index f2906524..be852c57 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -75,6 +75,22 @@ "./delegation": { "types": "./dist/src/delegation.d.ts", "import": "./src/delegation.js" + }, + "./schema": { + "types": "./dist/src/schema.d.ts", + "import": "./src/schema.js" + }, + "./capability": { + "types": "./dist/src/capability.d.ts", + "import": "./src/capability.js" + }, + "./validator": { + "types": "./dist/src/validator.d.ts", + "import": "./src/validator.js" + }, + "./error": { + "types": "./dist/src/error.d.ts", + "import": "./src/error.js" } }, "c8": { diff --git a/packages/core/src/capability.js b/packages/core/src/capability.js index 8a8ffbc3..450c1f29 100644 --- a/packages/core/src/capability.js +++ b/packages/core/src/capability.js @@ -132,7 +132,7 @@ class Capability extends Unit { result ) } catch (error) { - return /** @type {any} */ (error).cause + return /** @type {any} */ (error) } } diff --git a/packages/core/src/error.js b/packages/core/src/error.js index 82d2c000..1cadba69 100644 --- a/packages/core/src/error.js +++ b/packages/core/src/error.js @@ -1,5 +1,4 @@ import * as API from '@ucanto/interface' -import { the } from './util.js' import { isLink } from 'multiformats/link' /** @@ -35,7 +34,7 @@ export class EscalatedCapability extends Failure { this.claimed = claimed this.delegated = delegated this.cause = cause - this.name = the('EscalatedCapability') + this.name = 'EscalatedCapability' } describe() { return `Constraint violation: ${this.cause.message}` @@ -52,7 +51,8 @@ export class DelegationError extends Failure { */ constructor(causes, context) { super() - this.name = the('InvalidClaim') + /** @type {'InvalidClaim'} */ + this.name = 'InvalidClaim' this.causes = causes this.context = context } @@ -79,45 +79,6 @@ export class DelegationError extends Failure { } } -/** - * @implements {API.InvalidSignature} - */ -export class InvalidSignature extends Failure { - /** - * @param {API.Delegation} delegation - * @param {API.Verifier} verifier - */ - constructor(delegation, verifier) { - super() - this.name = the('InvalidSignature') - this.delegation = delegation - this.verifier = verifier - } - get issuer() { - return this.delegation.issuer - } - get audience() { - return this.delegation.audience - } - get key() { - return this.verifier.toDIDKey() - } - describe() { - const issuer = this.issuer.did() - const key = this.key - return ( - issuer.startsWith('did:key') - ? [ - `Proof ${this.delegation.cid} does not has a valid signature from ${key}`, - ] - : [ - `Proof ${this.delegation.cid} issued by ${issuer} does not has a valid signature from ${key}`, - ` ℹ️ Probably issuer signed with a different key, which got rotated, invalidating delegations that were issued with prior keys`, - ] - ).join('\n') - } -} - /** * @implements {API.UnavailableProof} */ @@ -128,7 +89,8 @@ export class UnavailableProof extends Failure { */ constructor(link, cause) { super() - this.name = the('UnavailableProof') + /** @type {'UnavailableProof'} */ + this.name = 'UnavailableProof' this.link = link this.cause = cause } @@ -149,7 +111,8 @@ export class DIDKeyResolutionError extends Failure { */ constructor(did, cause) { super() - this.name = the('DIDKeyResolutionError') + /** @type {'DIDKeyResolutionError'} */ + this.name = 'DIDKeyResolutionError' this.did = did this.cause = cause } @@ -171,7 +134,8 @@ export class InvalidAudience extends Failure { */ constructor(audience, delegation) { super() - this.name = the('InvalidAudience') + /** @type {'InvalidAudience'} */ + this.name = 'InvalidAudience' this.audience = audience this.delegation = delegation } @@ -201,7 +165,8 @@ export class MalformedCapability extends Failure { */ constructor(capability, cause) { super() - this.name = the('MalformedCapability') + /** @type {'MalformedCapability'} */ + this.name = 'MalformedCapability' this.capability = capability this.cause = cause } @@ -221,7 +186,8 @@ export class UnknownCapability extends Failure { */ constructor(capability) { super() - this.name = the('UnknownCapability') + /** @type {'UnknownCapability'} */ + this.name = 'UnknownCapability' this.capability = capability } /* c8 ignore next 3 */ @@ -236,7 +202,8 @@ export class Expired extends Failure { */ constructor(delegation) { super() - this.name = the('Expired') + /** @type {'Expired'} */ + this.name = 'Expired' this.delegation = delegation } describe() { @@ -259,13 +226,73 @@ export class Expired extends Failure { } } +/** + * @param {unknown} capability + * @param {string|number} [space] + */ + +const format = (capability, space) => + JSON.stringify( + capability, + (_key, value) => { + /* c8 ignore next 2 */ + if (isLink(value)) { + return value.toString() + } else { + return value + } + }, + space + ) + +/** + * @implements {API.InvalidSignature} + */ +export class InvalidSignature extends Failure { + /** + * @param {API.Delegation} delegation + * @param {API.Verifier} verifier + */ + constructor(delegation, verifier) { + super() + /** @type {'InvalidSignature'} */ + this.name = 'InvalidSignature' + this.delegation = delegation + this.verifier = verifier + } + get issuer() { + return this.delegation.issuer + } + get audience() { + return this.delegation.audience + } + get key() { + return this.verifier.toDIDKey() + } + describe() { + const issuer = this.issuer.did() + const key = this.key + return ( + issuer.startsWith('did:key') + ? [ + `Proof ${this.delegation.cid} does not has a valid signature from ${key}`, + ] + : [ + `Proof ${this.delegation.cid} issued by ${issuer} does not has a valid signature from ${key}`, + ` ℹ️ Probably issuer signed with a different key, which got rotated, invalidating delegations that were issued with prior keys`, + ] + ).join('\n') + } +} + export class NotValidBefore extends Failure { /** * @param {API.Delegation & { notBefore: number }} delegation */ constructor(delegation) { super() - this.name = the('NotValidBefore') + /** @type {'NotValidBefore'} */ + this.name = 'NotValidBefore' this.delegation = delegation } describe() { @@ -288,25 +315,6 @@ export class NotValidBefore extends Failure { } } -/** - * @param {unknown} capability - * @param {string|number} [space] - */ - -const format = (capability, space) => - JSON.stringify( - capability, - (_key, value) => { - /* c8 ignore next 2 */ - if (isLink(value)) { - return value.toString() - } else { - return value - } - }, - space - ) - /** * @param {string} message */ diff --git a/packages/core/src/schema/did.js b/packages/core/src/schema/did.js index 69277c58..42ee8bff 100644 --- a/packages/core/src/schema/did.js +++ b/packages/core/src/schema/did.js @@ -37,3 +37,9 @@ export const match = options => /** @type {Schema.Schema & API.URI<"did:">>} */ ( Schema.string().refine(new DIDSchema(options.method)) ) + +/** + * Create a DID string from any input (or throw) + * @param {unknown} input + */ +export const from = input => match({}).from(input) diff --git a/packages/core/src/schema/schema.js b/packages/core/src/schema/schema.js index c952fde8..8e4019bc 100644 --- a/packages/core/src/schema/schema.js +++ b/packages/core/src/schema/schema.js @@ -128,7 +128,7 @@ export class API { // we also check that fallback is not undefined because that is the point // of having a fallback if (fallback === undefined) { - throw new Error(`Value of type undefined is not a vaild default`) + throw new Error(`Value of type undefined is not a valid default`) } const schema = new Default({ @@ -400,6 +400,68 @@ class Tuple extends API { */ export const tuple = shape => new Tuple(shape) +/** + * @template V + * @template {string} K + * @template [I=unknown] + * @extends {API, I, { key: Schema.Reader, value: Schema.Reader }>} + * @implements {Schema.DictionarySchema} + */ +class Dictionary extends API { + /** + * @param {I} input + * @param {object} schema + * @param {Schema.Reader} schema.key + * @param {Schema.Reader} schema.value + */ + readWith(input, { key, value }) { + if (typeof input != 'object' || input === null || Array.isArray(input)) { + return typeError({ + expect: 'dictionary', + actual: input, + }) + } + + const dict = /** @type {Schema.Dictionary} */ ({}) + + for (const [k, v] of Object.entries(input)) { + const keyResult = key.read(k) + if (keyResult?.error) { + return memberError({ at: k, cause: keyResult }) + } + + const valueResult = value.read(v) + if (valueResult?.error) { + return memberError({ at: k, cause: valueResult }) + } + + dict[keyResult] = valueResult + } + + return dict + } + get key() { + return this.settings.key + } + get value() { + return this.settings.value + } + toString() { + return `dictionary(${this.settings})` + } +} + +/** + * @template {string} K + * @template {unknown} V + * @template [I=unknown] + * @param {object} shape + * @param {Schema.Reader} shape.value + * @param {Schema.Reader} [shape.key] + */ +export const dictionary = ({ value, key = string() }) => + new Dictionary({ value, key }) + /** * @template {[unknown, ...unknown[]]} T * @template [I=unknown] diff --git a/packages/core/src/schema/type.ts b/packages/core/src/schema/type.ts index a3d5a83b..e6fab8e2 100644 --- a/packages/core/src/schema/type.ts +++ b/packages/core/src/schema/type.ts @@ -49,6 +49,19 @@ export interface ArraySchema extends Schema { element: Reader } +export interface DictionarySchema + extends Schema, I> { + key: Reader + value: Reader +} + +export type Dictionary< + K extends string = string, + V extends unknown = unknown +> = { + [Key in K]: V +} + export interface LiteralSchema< T extends string | number | boolean | null, I = unknown diff --git a/packages/validator/src/lib.js b/packages/core/src/validator.js similarity index 98% rename from packages/validator/src/lib.js rename to packages/core/src/validator.js index 1c09eaab..709ee5ea 100644 --- a/packages/validator/src/lib.js +++ b/packages/core/src/validator.js @@ -1,32 +1,31 @@ import * as API from '@ucanto/interface' -import { isDelegation, UCAN } from '@ucanto/core' +// import { isDelegation, UCAN, Schema } from '@ucanto/core' +import { isDelegation } from './delegation.js' +import * as UCAN from '@ipld/dag-ucan' import { capability } from './capability.js' -import * as Schema from './schema.js' +import { DID, literal } from './schema.js' import { - UnavailableProof, - InvalidAudience, + InvalidSignature, Expired, NotValidBefore, - InvalidSignature, + UnavailableProof, + InvalidAudience, + DIDKeyResolutionError, DelegationError, Failure, MalformedCapability, - DIDKeyResolutionError, li, } from './error.js' +export { DID } export { Failure, UnavailableProof, MalformedCapability, + InvalidAudience, DIDKeyResolutionError as DIDResolutionError, } -export { capability } from './capability.js' -import { DID } from './schema.js' -export * from './schema.js' -export { Schema } - /** * @param {UCAN.Link} proof */ @@ -516,7 +515,7 @@ const resolveVerifier = async (did, delegation, config) => { const resolveDIDFromProofs = async (did, delegation, config) => { const update = Top.derive({ to: capability({ - with: Schema.literal(config.authority.did()), + with: literal(config.authority.did()), can: './update', nb: { key: DID.match({ method: 'key' }) }, }), @@ -542,5 +541,3 @@ const Top = capability({ const equalWith = (to, from) => to.with === from.with || new Failure(`Claimed ${to.with} can not be derived from ${from.with}`) - -export { InvalidAudience } diff --git a/packages/core/test/capabilities/util.js b/packages/core/test/capabilities/util.js index aeae1693..e4ba5cab 100644 --- a/packages/core/test/capabilities/util.js +++ b/packages/core/test/capabilities/util.js @@ -26,6 +26,42 @@ export const equalLink = (claimed, delegated) => { } } +/** + * Check URI can be delegated + * + * @param {string|undefined} child + * @param {string|undefined} parent + */ +export function canDelegateURI(child, parent) { + if (parent === undefined) { + return true + } + + if (child !== undefined && parent.endsWith('*')) { + return child.startsWith(parent.slice(0, -1)) + ? true + : new Failure(`${child} does not match ${parent}`) + } + + return child === parent + ? true + : new Failure(`${child} is different from ${parent}`) +} + +/** + * @param {API.Link|undefined} child + * @param {API.Link|undefined} parent + */ +export const canDelegateLink = (child, parent) => { + // if parent poses no restriction it's can be derived + if (parent === undefined) { + return true + } + + return String(child) === parent.toString() + ? true + : new Failure(`${child} is different from ${parent}`) +} /** * Checks that `with` on claimed capability is the same as `with` * in delegated capability. Note this will ignore `can` field. diff --git a/packages/validator/test/voucher.js b/packages/core/test/capabilities/voucher.js similarity index 88% rename from packages/validator/test/voucher.js rename to packages/core/test/capabilities/voucher.js index e0228b26..29f4d63c 100644 --- a/packages/validator/test/voucher.js +++ b/packages/core/test/capabilities/voucher.js @@ -1,5 +1,7 @@ import { equalWith, canDelegateURI, canDelegateLink, fail } from './util.js' -import { capability, URI, Text, Link, DID } from '../src/lib.js' +import { capability, Schema } from '../../src/lib.js' +const { URI, Text, Link, DID } = Schema +export * from './voucher/types.js' export const Voucher = capability({ can: 'voucher/*', diff --git a/packages/validator/test/types.js b/packages/core/test/capabilities/voucher/types.js similarity index 100% rename from packages/validator/test/types.js rename to packages/core/test/capabilities/voucher/types.js diff --git a/packages/validator/test/types.ts b/packages/core/test/capabilities/voucher/types.ts similarity index 100% rename from packages/validator/test/types.ts rename to packages/core/test/capabilities/voucher/types.ts diff --git a/packages/validator/test/capability.spec.js b/packages/core/test/capability.spec.js similarity index 95% rename from packages/validator/test/capability.spec.js rename to packages/core/test/capability.spec.js index d5e69cdc..32294a0a 100644 --- a/packages/validator/test/capability.spec.js +++ b/packages/core/test/capability.spec.js @@ -1,11 +1,13 @@ -import { capability, URI, Link, Schema } from '../src/lib.js' -import { invoke, parseLink } from '@ucanto/core' +import { capability, Schema } from '../src/lib.js' +import { invoke, parseLink } from '../src/lib.js' import * as API from '@ucanto/interface' import { Failure } from '../src/error.js' import { the } from '../src/util.js' import { test, assert } from './test.js' import { alice, bob, mallory, service as w3 } from './fixtures.js' +const { URI, Link } = Schema + /** * @template {API.Capabilities} C * @param {C} capabilities @@ -42,6 +44,8 @@ test('capability selects matches', () => { }, }) + assert.deepEqual(read.with, URI.match({ protocol: 'file:' })) + const d1 = delegate([ { can: 'file/read', with: 'space://zAlice' }, { can: 'file/write', with: 'file:///home/zAlice/' }, @@ -214,6 +218,8 @@ test('derived capability chain', () => { }, }) + assert.deepEqual(register.with, URI.match({ protocol: 'mailto:' })) + const d1 = delegate([ { can: 'account/register', @@ -2064,3 +2070,114 @@ test('default derive with nb', () => { } ) }) + +test('capability .read', () => { + const data = URI.match({ protocol: 'data:' }) + const echo = capability({ + can: 'test/echo', + with: URI.match({ protocol: 'did:' }), + nb: { + message: URI.match({ protocol: 'data:' }), + }, + }) + + assert.match( + echo + .read({ + with: 'file://gozala/path', + nb: { + message: 'data:hello', + }, + bar: 1, + }) + .toString(), + /Invalid 'with' - Expected did: URI/ + ) + assert.match( + echo + .read({ + with: 'file://gozala/path', + nb: { + message: 'data:hello', + }, + bar: 1, + }) + .toString(), + /Invalid 'with' - Expected did: URI/ + ) + + assert.match( + echo + .read({ + with: alice.did(), + }) + .toString(), + /Invalid 'nb.message' - Expected URI but got undefined/ + ) + + assert.match( + echo + .read({ + with: alice.did(), + nb: { + message: 'echo:foo', + }, + }) + .toString(), + /Invalid 'nb.message' - Expected data: URI instead got echo:foo/ + ) + + assert.deepEqual( + echo.create({ with: alice.did(), nb: { message: 'data:hello' } }), + { + can: 'test/echo', + with: alice.did(), + nb: { + message: 'data:hello', + }, + } + ) + + assert.deepEqual( + echo.read({ + with: new URL(alice.did()), + nb: { message: 'data:hello' }, + }), + { + can: 'test/echo', + with: alice.did(), + nb: { + message: 'data:hello', + }, + } + ) +}) + +test('derived capability .read', () => { + const A = capability({ + can: 'invoke/a', + with: Schema.URI, + }) + + const AA = A.derive({ + to: capability({ + can: 'derive/a', + with: Schema.URI, + }), + derives: (b, a) => + b.with === a.with ? true : new Failure(`with don't match`), + }) + + assert.equal(AA.can, 'derive/a') + assert.deepEqual(AA.with, Schema.URI) + + assert.deepEqual( + AA.read({ + with: 'data:a', + }), + { + can: 'derive/a', + with: 'data:a', + } + ) +}) diff --git a/packages/validator/test/delegate.spec.js b/packages/core/test/delegate.spec.js similarity index 97% rename from packages/validator/test/delegate.spec.js rename to packages/core/test/delegate.spec.js index 16ff551e..8adea1ae 100644 --- a/packages/validator/test/delegate.spec.js +++ b/packages/core/test/delegate.spec.js @@ -1,12 +1,18 @@ -import { capability, DID, URI, Link, unknown, Schema } from '../src/lib.js' -import { invoke, parseLink, delegate, UCAN } from '@ucanto/core' +import { + capability, + Schema, + invoke, + parseLink, + delegate, + UCAN, +} from '../src/lib.js' import * as API from '@ucanto/interface' import { Failure } from '../src/error.js' import { the } from '../src/util.js' import { CID } from 'multiformats' import { test, assert } from './test.js' import { alice, bob, mallory, service as w3 } from './fixtures.js' - +const { DID, URI, Link } = Schema const echo = capability({ can: 'test/echo', with: DID.match({ method: 'key' }), diff --git a/packages/validator/test/error.spec.js b/packages/core/test/error.spec.js similarity index 98% rename from packages/validator/test/error.spec.js rename to packages/core/test/error.spec.js index 32ac898b..281e95a6 100644 --- a/packages/validator/test/error.spec.js +++ b/packages/core/test/error.spec.js @@ -8,7 +8,7 @@ import { NotValidBefore, } from '../src/error.js' import { alice, bob, mallory, service as w3 } from './fixtures.js' -import { delegate, UCAN } from '@ucanto/core' +import { delegate, UCAN } from '../src/lib.js' test('Failure', () => { const error = new Failure('boom!') diff --git a/packages/validator/test/extra-schema.spec.js b/packages/core/test/extra-schema.spec.js similarity index 92% rename from packages/validator/test/extra-schema.spec.js rename to packages/core/test/extra-schema.spec.js index 292ed919..c722b05a 100644 --- a/packages/validator/test/extra-schema.spec.js +++ b/packages/core/test/extra-schema.spec.js @@ -381,3 +381,41 @@ test('URI.from', () => { }) } } + +{ + /** @type {Array<[unknown, null|{ name: string, message: string }]>} */ + const dataset = [ + ['did:key:foo', null], + ['did:web:example.com', null], + ['did:twosegments', null], + [ + 'notdid', + { + name: 'SchemaError', + message: 'Expected a did: but got "notdid" instead', + }, + ], + [ + undefined, + { + name: 'TypeError', + message: 'Expected value of type string instead got undefined', + }, + ], + ] + for (const [did, errorExpectation] of dataset) { + test(`DID.from("${did}")`, () => { + let error + try { + DID.from(did) + } catch (_error) { + error = _error + } + if (errorExpectation) { + assert.containSubset(error, errorExpectation) + } else { + assert.notOk(error, 'expected no error, but got an error') + } + }) + } +} diff --git a/packages/validator/test/inference.spec.js b/packages/core/test/inference.spec.js similarity index 93% rename from packages/validator/test/inference.spec.js rename to packages/core/test/inference.spec.js index 3825d84b..1bf57b96 100644 --- a/packages/validator/test/inference.spec.js +++ b/packages/core/test/inference.spec.js @@ -1,12 +1,14 @@ -import * as Voucher from './voucher.js' +import * as Voucher from './capabilities/voucher.js' import { test, assert } from './test.js' import { alice, bob, mallory, service as w3 } from './fixtures.js' -import { capability, URI, Link, DID } from '../src/lib.js' -import * as API from './types.js' +import { capability, Schema } from '../src/lib.js' +import * as API from '@ucanto/interface' -test('execute capabilty', () => +const { URI, Link, DID } = Schema + +test('execute capability', () => /** - * @param {API.ConnectionView} connection + * @param {API.ConnectionView} connection */ async connection => { const claim = Voucher.Claim.invoke({ @@ -55,7 +57,7 @@ test('execute capabilty', () => test('can access fields on the proof', () => /** - * @param {API.Delegation<[API.VoucherRedeem]>} proof + * @param {API.Delegation<[Voucher.VoucherRedeem]>} proof */ proof => { const redeem = Voucher.Redeem.invoke({ @@ -73,7 +75,7 @@ test('can access fields on the proof', () => test('use InferInvokedCapability', () => /** - * @param {API.ConnectionView} connection + * @param {API.ConnectionView} connection * @param {API.InferInvokedCapability} capability */ async (connection, capability) => { diff --git a/packages/validator/test/mailto.spec.js b/packages/core/test/mailto.spec.js similarity index 91% rename from packages/validator/test/mailto.spec.js rename to packages/core/test/mailto.spec.js index 2a9f22e9..dd16ac84 100644 --- a/packages/validator/test/mailto.spec.js +++ b/packages/core/test/mailto.spec.js @@ -1,10 +1,7 @@ import { test, assert } from './test.js' -import { access, DID } from '../src/lib.js' -import { capability, URI, Link, Schema } from '../src/lib.js' -import { Failure } from '../src/error.js' +import { access, DID } from '../src/validator.js' +import { capability, delegate } from '../src/lib.js' import { ed25519, Verifier } from '@ucanto/principal' -import * as Client from '@ucanto/client' -import * as Core from '@ucanto/core' import { alice, bob, mallory, service } from './fixtures.js' const w3 = service.withDID('did:web:web3.storage') @@ -63,7 +60,7 @@ test('delegated ./update', async () => { const manager = await ed25519.generate() const worker = await ed25519.generate() - const authority = await Core.delegate({ + const authority = await delegate({ issuer: manager, audience: worker, capabilities: [ @@ -74,7 +71,7 @@ test('delegated ./update', async () => { ], expiration: Infinity, proofs: [ - await Core.delegate({ + await delegate({ issuer: w3, audience: manager, capabilities: [ @@ -156,7 +153,7 @@ test('fail invalid ./update proof', async () => { with: w3.did(), nb: { key: alice.did() }, proofs: [ - await Core.delegate({ + await delegate({ issuer: w3, audience: service, capabilities: [ diff --git a/packages/validator/test/schema.spec.js b/packages/core/test/schema.spec.js similarity index 96% rename from packages/validator/test/schema.spec.js rename to packages/core/test/schema.spec.js index 669ab01e..9b264f22 100644 --- a/packages/validator/test/schema.spec.js +++ b/packages/core/test/schema.spec.js @@ -12,7 +12,7 @@ for (const { input, schema, expect, inputLabel, skip, only } of fixtures()) { } else { assert.deepEqual( result, - // if expcted value is set to undefined use input + // if expected value is set to undefined use input expect.value === undefined ? input : expect.value ) } @@ -24,7 +24,7 @@ for (const { input, schema, expect, inputLabel, skip, only } of fixtures()) { } else { assert.deepEqual( schema.from(input), - // if expcted value is set to undefined use input + // if expected value is set to undefined use input expect.value === undefined ? input : expect.value ) } @@ -62,7 +62,7 @@ test('string startsWith & endsWith', () => { assert.equal(hello.read('hello world'), 'hello world') }) -test('string startsWtih', () => { +test('string startsWith', () => { /** @type {Schema.StringSchema<`hello${string}`>} */ // @ts-expect-error - catches invalid type const bad = Schema.string() @@ -238,7 +238,7 @@ test('literal("foo").default("bar") throws', () => { ) }) -test('default on litral has default', () => { +test('default on literal has default', () => { const schema = Schema.literal('foo').default() assert.equal(schema.read(undefined), 'foo') }) @@ -262,6 +262,16 @@ test('.element of array', () => { assert.equal(Schema.array(schema).element, schema) }) +test('.key & .value of dictionary', () => { + const value = Schema.struct({}) + const key = Schema.enum(['x', 'y']) + const schema = Schema.dictionary({ value, key }) + + assert.deepEqual(schema.value, value) + assert.deepEqual(schema.key, key) + + assert.deepEqual(Schema.dictionary({ value }).key, Schema.string()) +}) test('struct', () => { const Point = Schema.struct({ type: 'Point', @@ -585,7 +595,7 @@ test('default throws on invalid default', () => { test('unknown with default', () => { assert.throws( () => Schema.unknown().default(undefined), - /undefined is not a vaild default/ + /undefined is not a valid default/ ) }) diff --git a/packages/validator/test/schema/fixtures.js b/packages/core/test/schema/fixtures.js similarity index 92% rename from packages/validator/test/schema/fixtures.js rename to packages/core/test/schema/fixtures.js index 9f3b911c..4794a81d 100644 --- a/packages/validator/test/schema/fixtures.js +++ b/packages/core/test/schema/fixtures.js @@ -1,5 +1,6 @@ import { pass, fail, display } from './util.js' import * as Schema from '../../src/schema.js' +import { string, unknown } from '../../src/schema.js' /** * @typedef {import('./util.js').Expect} Expect @@ -21,7 +22,7 @@ import * as Schema from '../../src/schema.js' * never: ExpectGroup * string: ExpectGroup, * boolean: ExpectGroup - * strartsWithHello: ExpectGroup + * startsWithHello: ExpectGroup * endsWithWorld: ExpectGroup * startsWithHelloEndsWithWorld: ExpectGroup * number: ExpectGroup @@ -52,6 +53,9 @@ import * as Schema from '../../src/schema.js' * point2d?: ExpectGroup * ['Red|Green|Blue']?: ExpectGroup * xyz?: ExpectGroup + * intDict?: ExpectGroup + * pointDict?: ExpectGroup + * dict: ExpectGroup * }} Fixture * * @param {Partial} source @@ -67,9 +71,9 @@ export const fixture = ({ in: input, got = input, array, ...expect }) => ({ never: { any: fail({ expect: 'never', got }), ...expect.never }, string: { any: fail({ expect: 'string', got }), ...expect.string }, boolean: { any: fail({ expect: 'boolean', got }), ...expect.boolean }, - strartsWithHello: { + startsWithHello: { any: fail({ expect: 'string', got }), - ...expect.strartsWithHello, + ...expect.startsWithHello, }, endsWithWorld: { any: fail({ expect: 'string', got }), @@ -128,6 +132,10 @@ export const fixture = ({ in: input, got = input, array, ...expect }) => ({ any: fail({ got }), ...expect.enum, }, + dict: { + any: fail({ expect: 'dictionary', got }), + ...expect.dict, + }, }) /** @type {Partial[]} */ @@ -139,7 +147,7 @@ export const source = [ unknown: { any: pass() }, literal: { hello: { any: pass() } }, stringOrNumber: { any: pass() }, - strartsWithHello: { any: fail.as(`expect .* "Hello" .* got "hello"`) }, + startsWithHello: { any: fail.as(`expect .* "Hello" .* got "hello"`) }, endsWithWorld: { any: fail.as(`expect .* "world" .* got "hello"`) }, startsWithHelloEndsWithWorld: { any: fail.as(`expect .* "Hello" .* got "hello"`), @@ -151,7 +159,7 @@ export const source = [ string: { any: pass() }, unknown: { any: pass() }, stringOrNumber: { any: pass() }, - strartsWithHello: { any: fail.as(`expect .* "Hello" .* got "Green"`) }, + startsWithHello: { any: fail.as(`expect .* "Hello" .* got "Green"`) }, endsWithWorld: { any: fail.as(`expect .* "world" .* got "Green"`) }, startsWithHelloEndsWithWorld: { any: fail.as(`expect .* "Hello" .* got "Green"`), @@ -166,7 +174,7 @@ export const source = [ string: { any: pass() }, unknown: { any: pass() }, stringOrNumber: { any: pass() }, - strartsWithHello: { any: pass() }, + startsWithHello: { any: pass() }, endsWithWorld: { any: pass() }, startsWithHelloEndsWithWorld: { any: pass(), @@ -182,6 +190,13 @@ export const source = [ xyz: { any: fail.at('"x"', { expect: 'number', got: 'undefined' }), }, + intDict: { + any: fail.at('"0"', { expect: 'number', got: '"h"' }), + }, + pointDict: { + any: fail.at('0', { expect: 'name|x|y', got: '"0"' }), + }, + dict: { any: pass({ 0: 'h', 1: 'e', 2: 'l', 3: 'l', 4: 'o' }) }, }, { in: null, @@ -331,6 +346,9 @@ export const source = [ xyz: { any: fail.at('"x"', { expect: 'number', got: 'undefined' }), }, + dict: { + any: pass(), + }, }, { in: [], @@ -536,6 +554,12 @@ export const source = [ xyz: { any: fail.at('"z"', { expect: 'number', got: 'undefined' }), }, + intDict: { + any: fail.at('"name"', { expect: 'number', got: '"Point2d"' }), + }, + dict: { + any: pass(), + }, }, { in: { name: 'Point2d', x: 0, z: 0 }, @@ -549,6 +573,15 @@ export const source = [ xyz: { any: fail.at('"y"', { expect: 'number', got: 'undefined' }), }, + intDict: { + any: fail.at('"name"', { expect: 'number', got: '"Point2d"' }), + }, + pointDict: { + any: fail.at('z', { expect: 'name|x|y', got: '"z"' }), + }, + dict: { + any: pass(), + }, }, { in: { name: 'Point2d', x: 0, y: 0.1 }, @@ -562,6 +595,12 @@ export const source = [ unknown: { any: pass(), }, + intDict: { + any: fail.at('"name"', { expect: 'number', got: '"Point2d"' }), + }, + dict: { + any: pass(), + }, }, ] @@ -825,7 +864,7 @@ export const scenarios = fixture => [ }, { schema: Schema.string().startsWith('Hello'), - expect: fixture.strartsWithHello.any || fixture.string.any || fixture.any, + expect: fixture.startsWithHello.any || fixture.string.any || fixture.any, }, { schema: Schema.string().endsWith('world'), @@ -860,6 +899,22 @@ export const scenarios = fixture => [ .and(Schema.struct({ z: Schema.integer() })), expect: fixture.xyz?.any || fixture.struct.any || fixture.any, }, + { + schema: Schema.dictionary({ value: Schema.integer() }), + + expect: fixture.intDict?.any || fixture.dict?.any || fixture.any, + }, + { + schema: Schema.dictionary({ value: unknown() }), + expect: fixture.dict?.any || fixture.any, + }, + { + schema: Schema.dictionary({ + value: unknown(), + key: Schema.enum(['name', 'x', 'y']), + }), + expect: fixture.pointDict?.any || fixture.dict.any || fixture.any, + }, ] export default function* () { diff --git a/packages/validator/test/schema/util.js b/packages/core/test/schema/util.js similarity index 100% rename from packages/validator/test/schema/util.js rename to packages/core/test/schema/util.js diff --git a/packages/core/test/test.js b/packages/core/test/test.js index d86c1160..dbe6d8ec 100644 --- a/packages/core/test/test.js +++ b/packages/core/test/test.js @@ -1,4 +1,6 @@ import { assert, use } from 'chai' +import subset from 'chai-subset' +use(subset) export const test = it export { assert } diff --git a/packages/validator/test/lib.spec.js b/packages/core/test/validator.spec.js similarity index 97% rename from packages/validator/test/lib.spec.js rename to packages/core/test/validator.spec.js index 224c184d..e4c0fda8 100644 --- a/packages/validator/test/lib.spec.js +++ b/packages/core/test/validator.spec.js @@ -1,13 +1,12 @@ import { test, assert } from './test.js' -import { access, claim } from '../src/lib.js' -import { capability, URI, Link } from '../src/lib.js' -import { Failure } from '../src/error.js' +import { access, claim } from '../src/validator.js' +import { capability, delegate } from '../src/lib.js' +import { URI, Link } from '../src/schema.js' +import { Failure, UnavailableProof } from '../src/error.js' import { Verifier } from '@ucanto/principal' -import * as Client from '@ucanto/client' import { alice, bob, mallory, service, service as w3 } from './fixtures.js' -import { UCAN, DID as Principal } from '@ucanto/core' -import { UnavailableProof } from '../src/error.js' +import { UCAN, DID as Principal } from '../src/lib.js' const storeAdd = capability({ can: 'store/add', @@ -156,7 +155,7 @@ test('unauthorized / invalid signature', async () => { }) test('unauthorized / unknown capability', async () => { - const invocation = await Client.delegate({ + const invocation = await delegate({ issuer: alice, audience: w3, capabilities: [ @@ -436,7 +435,7 @@ test('invalid claim / invalid signature', async () => { }) test('invalid claim / unknown capability', async () => { - const delegation = await Client.delegate({ + const delegation = await delegate({ issuer: alice, audience: bob, capabilities: [ @@ -477,7 +476,7 @@ test('invalid claim / unknown capability', async () => { test('invalid claim / malformed capability', async () => { const badDID = `bib:${alice.did().slice(4)}` - const delegation = await Client.delegate({ + const delegation = await delegate({ issuer: alice, audience: bob, capabilities: [ @@ -489,7 +488,7 @@ test('invalid claim / malformed capability', async () => { }) const nb = { link: Link.parse('bafkqaaa') } - const invocation = await Client.delegate({ + const invocation = await delegate({ issuer: bob, audience: w3, capabilities: [ diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 87d86b0d..052fe2a8 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -99,5 +99,5 @@ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": ["src", "test"], - "references": [{ "path": "../interface" }, { "path": "../principal" }, { "path": "../validator" } ] + "references": [{ "path": "../interface" }, { "path": "../principal" } ] } diff --git a/packages/server/package.json b/packages/server/package.json index b5acfe1e..66dc1a7c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -29,8 +29,7 @@ }, "dependencies": { "@ucanto/core": "^4.0.3", - "@ucanto/interface": "^4.0.3", - "@ucanto/validator": "^4.0.3" + "@ucanto/interface": "^4.0.3" }, "devDependencies": { "@types/chai": "^4.3.3", diff --git a/packages/server/src/handler.js b/packages/server/src/handler.js index 5f1bcbf3..74999be3 100644 --- a/packages/server/src/handler.js +++ b/packages/server/src/handler.js @@ -1,5 +1,5 @@ import * as API from './api.js' -import { access } from '@ucanto/validator' +import { access } from '@ucanto/core/validator' /** * @template {API.Ability} A diff --git a/packages/server/src/server.js b/packages/server/src/server.js index 6f5b3d6a..e36829d3 100644 --- a/packages/server/src/server.js +++ b/packages/server/src/server.js @@ -1,13 +1,12 @@ import * as API from '@ucanto/interface' -import { InvalidAudience } from '@ucanto/validator' +import { capability, Schema } from '@ucanto/core' import { Verifier } from '@ucanto/principal' +import { InvalidAudience } from '@ucanto/core/validator' export { - capability, - URI, - Link, Failure, MalformedCapability, -} from '@ucanto/validator' + InvalidAudience, +} from '@ucanto/core/validator' /** * Creates a connection to a service. diff --git a/packages/server/test/handler.spec.js b/packages/server/test/handler.spec.js index 62606a35..fb5255a1 100644 --- a/packages/server/test/handler.spec.js +++ b/packages/server/test/handler.spec.js @@ -7,7 +7,6 @@ import { alice, bob, mallory, service as w3 } from './fixtures.js' import { test, assert } from './test.js' import * as Access from './service/access.js' import { Verifier } from '@ucanto/principal/ed25519' -import { UnavailableProof } from '@ucanto/validator' const context = { id: w3, @@ -20,10 +19,6 @@ const context = { canIssue: (capability, issuer) => capability.with === issuer || issuer == w3.did(), principal: Verifier, - /** - * @param {API.UCANLink} link - */ - resolve: link => new UnavailableProof(link), } test('invocation', async () => { diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 3e549c1a..88b09a3f 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -103,7 +103,6 @@ { "path": "../interface" }, { "path": "../core" }, { "path": "../transport" }, - { "path": "../client" }, - { "path": "../validator" } + { "path": "../client" } ] } diff --git a/packages/validator/CHANGELOG.md b/packages/validator/CHANGELOG.md deleted file mode 100644 index edac134c..00000000 --- a/packages/validator/CHANGELOG.md +++ /dev/null @@ -1,575 +0,0 @@ -# Changelog - -### [4.0.3](https://www.github.com/web3-storage/ucanto/compare/validator-v4.0.2...validator-v4.0.3) (2022-12-14) - - -### Bug Fixes - -* trigger releases ([a0d9291](https://www.github.com/web3-storage/ucanto/commit/a0d9291f9e20456e115fa6c7890cafe937fa511e)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^4.0.2 to ^4.0.3 - * @ucanto/interface bumped from ^4.0.2 to ^4.0.3 - * devDependencies - * @ucanto/client bumped from ^4.0.2 to ^4.0.3 - * @ucanto/principal bumped from ^4.0.2 to ^4.0.3 - -## [4.0.2](https://github.com/web3-storage/ucanto/compare/validator-v4.1.0...validator-v4.0.2) (2022-12-14) - -### ⚠ BREAKING CHANGES - -* upgrades to multiformats@10 ([#117](https://github.com/web3-storage/ucanto/issues/117)) -* switch decoder API to zod like schema API ([#108](https://github.com/web3-storage/ucanto/issues/108)) -* upgrade to ucan 0.9 ([#95](https://github.com/web3-storage/ucanto/issues/95)) -* update dag-ucan, types and names ([#90](https://github.com/web3-storage/ucanto/issues/90)) - -### Features - -* alight link API with multiformats ([#36](https://github.com/web3-storage/ucanto/issues/36)) ([0ec460e](https://github.com/web3-storage/ucanto/commit/0ec460e43ddda0bb3a3fea8a7881da1463154f36)) -* capability provider API ([#34](https://github.com/web3-storage/ucanto/issues/34)) ([ea89f97](https://github.com/web3-storage/ucanto/commit/ea89f97125bb484a12ce3ca09a7884911a9fd4d6)) -* cherry pick changes from uploads-v2 demo ([#43](https://github.com/web3-storage/ucanto/issues/43)) ([4308fd2](https://github.com/web3-storage/ucanto/commit/4308fd2f392b9fcccc52af64432dcb04c8257e0b)) -* delgation iterate, more errors and types ([0606168](https://github.com/web3-storage/ucanto/commit/0606168313d17d66bcc1ad6091440765e1700a4f)) -* embedded key resolution ([#168](https://github.com/web3-storage/ucanto/issues/168)) ([5e650f3](https://github.com/web3-storage/ucanto/commit/5e650f376db79c690e4771695d1ff4e6deece40e)) -* Impelment InferInvokedCapability per [#99](https://github.com/web3-storage/ucanto/issues/99) ([#100](https://github.com/web3-storage/ucanto/issues/100)) ([fc5a2ac](https://github.com/web3-storage/ucanto/commit/fc5a2ace33f2a3599a654d8edd1641d111032074)) -* implement .delegate on capabilities ([#110](https://github.com/web3-storage/ucanto/issues/110)) ([fd0bb9d](https://github.com/web3-storage/ucanto/commit/fd0bb9da58836c05d6ee9f60cd6b1cb6b747e3b1)) -* refactor into monorepo ([#13](https://github.com/web3-storage/ucanto/issues/13)) ([1f99506](https://github.com/web3-storage/ucanto/commit/1f995064ec6e5953118c2dd1065ee6be959f25b9)) -* rip out special handling of my: and as: capabilities ([#109](https://github.com/web3-storage/ucanto/issues/109)) ([3ec8e64](https://github.com/web3-storage/ucanto/commit/3ec8e6434a096221bf72193e074810cc18dd5cd8)) -* setup pnpm & release-please ([84ac7f1](https://github.com/web3-storage/ucanto/commit/84ac7f12e5a66ee4919fa7527858dc916850e3e0)) -* switch decoder API to zod like schema API ([#108](https://github.com/web3-storage/ucanto/issues/108)) ([e2e03ff](https://github.com/web3-storage/ucanto/commit/e2e03ffeb35f00627335dbfd3e128e2cf9dcfdee)) -* **ucanto:** capability create / inovke methods ([#51](https://github.com/web3-storage/ucanto/issues/51)) ([ddf56b1](https://github.com/web3-storage/ucanto/commit/ddf56b1ec80ff6c0698255c531936d8eeab532fd)) -* **ucanto:** URI protocol type retention & capability constructors ([e291544](https://github.com/web3-storage/ucanto/commit/e2915447254990d6e2384ff79a1da38120426ed5)) -* update dag-ucan, types and names ([#90](https://github.com/web3-storage/ucanto/issues/90)) ([cd792c9](https://github.com/web3-storage/ucanto/commit/cd792c934fbd358d6ccfa5d02f205b14b5f2e14e)) -* upgrade to ucan 0.9 ([#95](https://github.com/web3-storage/ucanto/issues/95)) ([b752b39](https://github.com/web3-storage/ucanto/commit/b752b398950120d6121badcdbb639f4dc9ce8794)) -* upgrades to multiformats@10 ([#117](https://github.com/web3-storage/ucanto/issues/117)) ([61dc4ca](https://github.com/web3-storage/ucanto/commit/61dc4cafece3365bbf60f709265ea71180f226d7)) - - -### Bug Fixes - -* add return type to URI.uri() schema ([#127](https://github.com/web3-storage/ucanto/issues/127)) ([c302866](https://github.com/web3-storage/ucanto/commit/c3028667bc1094e6f6b16c43b3a396ef6207f75c)) -* build types before publishing ([#71](https://github.com/web3-storage/ucanto/issues/71)) ([04b7958](https://github.com/web3-storage/ucanto/commit/04b79588f77dba234aaf628f62f574b124bd540b)) -* downgrade versions ([#158](https://github.com/web3-storage/ucanto/issues/158)) ([f814e75](https://github.com/web3-storage/ucanto/commit/f814e75a89d3ed7c3488a8cb7af8d94f0cfba440)) -* intermittent test failures ([#166](https://github.com/web3-storage/ucanto/issues/166)) ([6cb0348](https://github.com/web3-storage/ucanto/commit/6cb03482bd257d2ea62b6558e1f6ee1a693b68fb)) -* optional caveats ([#106](https://github.com/web3-storage/ucanto/issues/106)) ([537a4c8](https://github.com/web3-storage/ucanto/commit/537a4c86fdd02c26c1442d6a205e8977afbad603)) -* optional field validation ([#124](https://github.com/web3-storage/ucanto/issues/124)) ([87b70d2](https://github.com/web3-storage/ucanto/commit/87b70d2d56c07f8257717fa5ef584a21eb0417c8)) -* package scripts to build types ([#84](https://github.com/web3-storage/ucanto/issues/84)) ([4d21132](https://github.com/web3-storage/ucanto/commit/4d2113246abdda215dd3fa882730ba71e68b5695)) -* update @ipld/car and @ipld/dag-cbor deps ([#120](https://github.com/web3-storage/ucanto/issues/120)) ([5dcd7a5](https://github.com/web3-storage/ucanto/commit/5dcd7a5788dfd126f332b126f7ad1215972f29c4)) -* versions ([#131](https://github.com/web3-storage/ucanto/issues/131)) ([88b87a7](https://github.com/web3-storage/ucanto/commit/88b87a7f3a32c02a22ddffcb8f38199445097133)) - - -### Miscellaneous Chores - -* release 0.0.1-beta ([d6c7e73](https://github.com/web3-storage/ucanto/commit/d6c7e73de56278e2f2c92c4a0e1a2709c92bcbf9)) -* release 4.0.2 ([e9e35df](https://github.com/web3-storage/ucanto/commit/e9e35dffeeb7e5b5e19627f791b66bbdd35d2d11)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^4.0.3 to ^4.0.2 - * @ucanto/interface bumped from ^4.1.0 to ^4.0.2 - * devDependencies - * @ucanto/client bumped from ^4.0.3 to ^4.0.2 - * @ucanto/principal bumped from ^4.1.0 to ^4.0.2 - -## [4.1.0](https://github.com/web3-storage/ucanto/compare/validator-v4.0.2...validator-v4.1.0) (2022-12-14) - - -### Features - -* embedded key resolution ([#168](https://github.com/web3-storage/ucanto/issues/168)) ([5e650f3](https://github.com/web3-storage/ucanto/commit/5e650f376db79c690e4771695d1ff4e6deece40e)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^4.0.2 to ^4.0.3 - * @ucanto/interface bumped from ^4.0.2 to ^4.1.0 - * devDependencies - * @ucanto/client bumped from ^4.0.2 to ^4.0.3 - * @ucanto/principal bumped from ^4.0.2 to ^4.1.0 - -## [4.0.2](https://github.com/web3-storage/ucanto/compare/validator-v4.1.0...validator-v4.0.2) (2022-12-14) - - -### ⚠ BREAKING CHANGES - -* upgrades to multiformats@10 ([#117](https://github.com/web3-storage/ucanto/issues/117)) -* switch decoder API to zod like schema API ([#108](https://github.com/web3-storage/ucanto/issues/108)) -* upgrade to ucan 0.9 ([#95](https://github.com/web3-storage/ucanto/issues/95)) -* update dag-ucan, types and names ([#90](https://github.com/web3-storage/ucanto/issues/90)) - -### Features - -* alight link API with multiformats ([#36](https://github.com/web3-storage/ucanto/issues/36)) ([0ec460e](https://github.com/web3-storage/ucanto/commit/0ec460e43ddda0bb3a3fea8a7881da1463154f36)) -* capability provider API ([#34](https://github.com/web3-storage/ucanto/issues/34)) ([ea89f97](https://github.com/web3-storage/ucanto/commit/ea89f97125bb484a12ce3ca09a7884911a9fd4d6)) -* cherry pick changes from uploads-v2 demo ([#43](https://github.com/web3-storage/ucanto/issues/43)) ([4308fd2](https://github.com/web3-storage/ucanto/commit/4308fd2f392b9fcccc52af64432dcb04c8257e0b)) -* delgation iterate, more errors and types ([0606168](https://github.com/web3-storage/ucanto/commit/0606168313d17d66bcc1ad6091440765e1700a4f)) -* embedded key resolution ([#168](https://github.com/web3-storage/ucanto/issues/168)) ([5e650f3](https://github.com/web3-storage/ucanto/commit/5e650f376db79c690e4771695d1ff4e6deece40e)) -* Impelment InferInvokedCapability per [#99](https://github.com/web3-storage/ucanto/issues/99) ([#100](https://github.com/web3-storage/ucanto/issues/100)) ([fc5a2ac](https://github.com/web3-storage/ucanto/commit/fc5a2ace33f2a3599a654d8edd1641d111032074)) -* implement .delegate on capabilities ([#110](https://github.com/web3-storage/ucanto/issues/110)) ([fd0bb9d](https://github.com/web3-storage/ucanto/commit/fd0bb9da58836c05d6ee9f60cd6b1cb6b747e3b1)) -* refactor into monorepo ([#13](https://github.com/web3-storage/ucanto/issues/13)) ([1f99506](https://github.com/web3-storage/ucanto/commit/1f995064ec6e5953118c2dd1065ee6be959f25b9)) -* rip out special handling of my: and as: capabilities ([#109](https://github.com/web3-storage/ucanto/issues/109)) ([3ec8e64](https://github.com/web3-storage/ucanto/commit/3ec8e6434a096221bf72193e074810cc18dd5cd8)) -* setup pnpm & release-please ([84ac7f1](https://github.com/web3-storage/ucanto/commit/84ac7f12e5a66ee4919fa7527858dc916850e3e0)) -* switch decoder API to zod like schema API ([#108](https://github.com/web3-storage/ucanto/issues/108)) ([e2e03ff](https://github.com/web3-storage/ucanto/commit/e2e03ffeb35f00627335dbfd3e128e2cf9dcfdee)) -* **ucanto:** capability create / inovke methods ([#51](https://github.com/web3-storage/ucanto/issues/51)) ([ddf56b1](https://github.com/web3-storage/ucanto/commit/ddf56b1ec80ff6c0698255c531936d8eeab532fd)) -* **ucanto:** URI protocol type retention & capability constructors ([e291544](https://github.com/web3-storage/ucanto/commit/e2915447254990d6e2384ff79a1da38120426ed5)) -* update dag-ucan, types and names ([#90](https://github.com/web3-storage/ucanto/issues/90)) ([cd792c9](https://github.com/web3-storage/ucanto/commit/cd792c934fbd358d6ccfa5d02f205b14b5f2e14e)) -* upgrade to ucan 0.9 ([#95](https://github.com/web3-storage/ucanto/issues/95)) ([b752b39](https://github.com/web3-storage/ucanto/commit/b752b398950120d6121badcdbb639f4dc9ce8794)) -* upgrades to multiformats@10 ([#117](https://github.com/web3-storage/ucanto/issues/117)) ([61dc4ca](https://github.com/web3-storage/ucanto/commit/61dc4cafece3365bbf60f709265ea71180f226d7)) - - -### Bug Fixes - -* add return type to URI.uri() schema ([#127](https://github.com/web3-storage/ucanto/issues/127)) ([c302866](https://github.com/web3-storage/ucanto/commit/c3028667bc1094e6f6b16c43b3a396ef6207f75c)) -* build types before publishing ([#71](https://github.com/web3-storage/ucanto/issues/71)) ([04b7958](https://github.com/web3-storage/ucanto/commit/04b79588f77dba234aaf628f62f574b124bd540b)) -* downgrade versions ([#158](https://github.com/web3-storage/ucanto/issues/158)) ([f814e75](https://github.com/web3-storage/ucanto/commit/f814e75a89d3ed7c3488a8cb7af8d94f0cfba440)) -* intermittent test failures ([#166](https://github.com/web3-storage/ucanto/issues/166)) ([6cb0348](https://github.com/web3-storage/ucanto/commit/6cb03482bd257d2ea62b6558e1f6ee1a693b68fb)) -* optional caveats ([#106](https://github.com/web3-storage/ucanto/issues/106)) ([537a4c8](https://github.com/web3-storage/ucanto/commit/537a4c86fdd02c26c1442d6a205e8977afbad603)) -* optional field validation ([#124](https://github.com/web3-storage/ucanto/issues/124)) ([87b70d2](https://github.com/web3-storage/ucanto/commit/87b70d2d56c07f8257717fa5ef584a21eb0417c8)) -* package scripts to build types ([#84](https://github.com/web3-storage/ucanto/issues/84)) ([4d21132](https://github.com/web3-storage/ucanto/commit/4d2113246abdda215dd3fa882730ba71e68b5695)) -* update @ipld/car and @ipld/dag-cbor deps ([#120](https://github.com/web3-storage/ucanto/issues/120)) ([5dcd7a5](https://github.com/web3-storage/ucanto/commit/5dcd7a5788dfd126f332b126f7ad1215972f29c4)) -* versions ([#131](https://github.com/web3-storage/ucanto/issues/131)) ([88b87a7](https://github.com/web3-storage/ucanto/commit/88b87a7f3a32c02a22ddffcb8f38199445097133)) - - -### Miscellaneous Chores - -* release 0.0.1-beta ([d6c7e73](https://github.com/web3-storage/ucanto/commit/d6c7e73de56278e2f2c92c4a0e1a2709c92bcbf9)) -* release 4.0.2 ([e9e35df](https://github.com/web3-storage/ucanto/commit/e9e35dffeeb7e5b5e19627f791b66bbdd35d2d11)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^4.0.3 to ^4.0.2 - * @ucanto/interface bumped from ^4.1.0 to ^4.0.2 - * devDependencies - * @ucanto/client bumped from ^4.0.3 to ^4.0.2 - * @ucanto/principal bumped from ^4.1.0 to ^4.0.2 - -## [4.1.0](https://github.com/web3-storage/ucanto/compare/validator-v4.0.2...validator-v4.1.0) (2022-12-14) - - -### Features - -* embedded key resolution ([#168](https://github.com/web3-storage/ucanto/issues/168)) ([5e650f3](https://github.com/web3-storage/ucanto/commit/5e650f376db79c690e4771695d1ff4e6deece40e)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^4.0.2 to ^4.0.3 - * @ucanto/interface bumped from ^4.0.2 to ^4.1.0 - * devDependencies - * @ucanto/client bumped from ^4.0.2 to ^4.0.3 - * @ucanto/principal bumped from ^4.0.2 to ^4.1.0 - -## [4.0.2](https://github.com/web3-storage/ucanto/compare/validator-v4.0.2...validator-v4.0.2) (2022-12-02) - - -### ⚠ BREAKING CHANGES - -* upgrades to multiformats@10 (#117) -* switch decoder API to zod like schema API (#108) -* upgrade to ucan 0.9 (#95) -* update dag-ucan, types and names (#90) - -### Features - -* alight link API with multiformats ([#36](https://github.com/web3-storage/ucanto/issues/36)) ([0ec460e](https://github.com/web3-storage/ucanto/commit/0ec460e43ddda0bb3a3fea8a7881da1463154f36)) -* capability provider API ([#34](https://github.com/web3-storage/ucanto/issues/34)) ([ea89f97](https://github.com/web3-storage/ucanto/commit/ea89f97125bb484a12ce3ca09a7884911a9fd4d6)) -* cherry pick changes from uploads-v2 demo ([#43](https://github.com/web3-storage/ucanto/issues/43)) ([4308fd2](https://github.com/web3-storage/ucanto/commit/4308fd2f392b9fcccc52af64432dcb04c8257e0b)) -* delgation iterate, more errors and types ([0606168](https://github.com/web3-storage/ucanto/commit/0606168313d17d66bcc1ad6091440765e1700a4f)) -* Impelment InferInvokedCapability per [#99](https://github.com/web3-storage/ucanto/issues/99) ([#100](https://github.com/web3-storage/ucanto/issues/100)) ([fc5a2ac](https://github.com/web3-storage/ucanto/commit/fc5a2ace33f2a3599a654d8edd1641d111032074)) -* implement .delegate on capabilities ([#110](https://github.com/web3-storage/ucanto/issues/110)) ([fd0bb9d](https://github.com/web3-storage/ucanto/commit/fd0bb9da58836c05d6ee9f60cd6b1cb6b747e3b1)) -* rip out special handling of my: and as: capabilities ([#109](https://github.com/web3-storage/ucanto/issues/109)) ([3ec8e64](https://github.com/web3-storage/ucanto/commit/3ec8e6434a096221bf72193e074810cc18dd5cd8)) -* setup pnpm & release-please ([84ac7f1](https://github.com/web3-storage/ucanto/commit/84ac7f12e5a66ee4919fa7527858dc916850e3e0)) -* switch decoder API to zod like schema API ([#108](https://github.com/web3-storage/ucanto/issues/108)) ([e2e03ff](https://github.com/web3-storage/ucanto/commit/e2e03ffeb35f00627335dbfd3e128e2cf9dcfdee)) -* **ucanto:** capability create / inovke methods ([#51](https://github.com/web3-storage/ucanto/issues/51)) ([ddf56b1](https://github.com/web3-storage/ucanto/commit/ddf56b1ec80ff6c0698255c531936d8eeab532fd)) -* **ucanto:** URI protocol type retention & capability constructors ([e291544](https://github.com/web3-storage/ucanto/commit/e2915447254990d6e2384ff79a1da38120426ed5)) -* update dag-ucan, types and names ([#90](https://github.com/web3-storage/ucanto/issues/90)) ([cd792c9](https://github.com/web3-storage/ucanto/commit/cd792c934fbd358d6ccfa5d02f205b14b5f2e14e)) -* upgrade to ucan 0.9 ([#95](https://github.com/web3-storage/ucanto/issues/95)) ([b752b39](https://github.com/web3-storage/ucanto/commit/b752b398950120d6121badcdbb639f4dc9ce8794)) -* upgrades to multiformats@10 ([#117](https://github.com/web3-storage/ucanto/issues/117)) ([61dc4ca](https://github.com/web3-storage/ucanto/commit/61dc4cafece3365bbf60f709265ea71180f226d7)) - - -### Bug Fixes - -* add return type to URI.uri() schema ([#127](https://github.com/web3-storage/ucanto/issues/127)) ([c302866](https://github.com/web3-storage/ucanto/commit/c3028667bc1094e6f6b16c43b3a396ef6207f75c)) -* build types before publishing ([#71](https://github.com/web3-storage/ucanto/issues/71)) ([04b7958](https://github.com/web3-storage/ucanto/commit/04b79588f77dba234aaf628f62f574b124bd540b)) -* downgrade versions ([#158](https://github.com/web3-storage/ucanto/issues/158)) ([f814e75](https://github.com/web3-storage/ucanto/commit/f814e75a89d3ed7c3488a8cb7af8d94f0cfba440)) -* intermittent test failures ([#166](https://github.com/web3-storage/ucanto/issues/166)) ([6cb0348](https://github.com/web3-storage/ucanto/commit/6cb03482bd257d2ea62b6558e1f6ee1a693b68fb)) -* optional caveats ([#106](https://github.com/web3-storage/ucanto/issues/106)) ([537a4c8](https://github.com/web3-storage/ucanto/commit/537a4c86fdd02c26c1442d6a205e8977afbad603)) -* optional field validation ([#124](https://github.com/web3-storage/ucanto/issues/124)) ([87b70d2](https://github.com/web3-storage/ucanto/commit/87b70d2d56c07f8257717fa5ef584a21eb0417c8)) -* package scripts to build types ([#84](https://github.com/web3-storage/ucanto/issues/84)) ([4d21132](https://github.com/web3-storage/ucanto/commit/4d2113246abdda215dd3fa882730ba71e68b5695)) -* update @ipld/car and @ipld/dag-cbor deps ([#120](https://github.com/web3-storage/ucanto/issues/120)) ([5dcd7a5](https://github.com/web3-storage/ucanto/commit/5dcd7a5788dfd126f332b126f7ad1215972f29c4)) -* versions ([#131](https://github.com/web3-storage/ucanto/issues/131)) ([88b87a7](https://github.com/web3-storage/ucanto/commit/88b87a7f3a32c02a22ddffcb8f38199445097133)) - - -### Miscellaneous Chores - -* release 4.0.2 ([e9e35df](https://github.com/web3-storage/ucanto/commit/e9e35dffeeb7e5b5e19627f791b66bbdd35d2d11)) - -## [4.0.2](https://github.com/web3-storage/ucanto/compare/validator-v3.0.7...validator-v4.0.2) (2022-12-02) - - -### Bug Fixes - -* intermittent test failures ([#166](https://github.com/web3-storage/ucanto/issues/166)) ([6cb0348](https://github.com/web3-storage/ucanto/commit/6cb03482bd257d2ea62b6558e1f6ee1a693b68fb)) - - -### Miscellaneous Chores - -* release 4.0.2 ([e9e35df](https://github.com/web3-storage/ucanto/commit/e9e35dffeeb7e5b5e19627f791b66bbdd35d2d11)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^4.0.0 to ^4.0.2 - * @ucanto/interface bumped from ^4.0.0 to ^4.0.2 - * devDependencies - * @ucanto/client bumped from ^4.0.0 to ^4.0.2 - * @ucanto/principal bumped from ^4.0.1 to ^4.0.2 - -### [3.0.7](https://www.github.com/web3-storage/ucanto/compare/validator-v3.0.6...validator-v3.0.7) (2022-12-02) - - -### Bug Fixes - -* downgrade versions ([#158](https://www.github.com/web3-storage/ucanto/issues/158)) ([f814e75](https://www.github.com/web3-storage/ucanto/commit/f814e75a89d3ed7c3488a8cb7af8d94f0cfba440)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^3.0.4 to ^3.0.5 - * devDependencies - * @ucanto/client bumped from ^3.0.4 to ^3.0.5 - -### [3.0.6](https://www.github.com/web3-storage/ucanto/compare/validator-v3.0.5...validator-v3.0.6) (2022-12-02) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^3.0.3 to ^3.0.4 - * devDependencies - * @ucanto/client bumped from ^3.0.3 to ^3.0.4 - * @ucanto/principal bumped from ^4.0.0 to ^4.0.1 - -### [3.0.5](https://www.github.com/web3-storage/ucanto/compare/validator-v3.0.4...validator-v3.0.5) (2022-12-01) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^3.0.2 to ^3.0.3 - * @ucanto/interface bumped from ^3.0.1 to ^4.0.0 - * devDependencies - * @ucanto/client bumped from ^3.0.2 to ^3.0.3 - * @ucanto/principal bumped from ^3.0.1 to ^4.0.0 - -### [3.0.4](https://www.github.com/web3-storage/ucanto/compare/validator-v3.0.3...validator-v3.0.4) (2022-11-12) - - -### Bug Fixes - -* versions ([#131](https://www.github.com/web3-storage/ucanto/issues/131)) ([88b87a7](https://www.github.com/web3-storage/ucanto/commit/88b87a7f3a32c02a22ddffcb8f38199445097133)) - -### [3.0.3](https://www.github.com/web3-storage/ucanto/compare/validator-v3.0.2...validator-v3.0.3) (2022-11-12) - - -### Bug Fixes - -* add return type to URI.uri() schema ([#127](https://www.github.com/web3-storage/ucanto/issues/127)) ([c302866](https://www.github.com/web3-storage/ucanto/commit/c3028667bc1094e6f6b16c43b3a396ef6207f75c)) - -### [3.0.2](https://www.github.com/web3-storage/ucanto/compare/validator-v3.0.1...validator-v3.0.2) (2022-11-11) - - -### Bug Fixes - -* optional field validation ([#124](https://www.github.com/web3-storage/ucanto/issues/124)) ([87b70d2](https://www.github.com/web3-storage/ucanto/commit/87b70d2d56c07f8257717fa5ef584a21eb0417c8)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^3.0.1 to ^3.0.2 - * @ucanto/interface bumped from ^3.0.0 to ^3.0.1 - * devDependencies - * @ucanto/client bumped from ^3.0.1 to ^3.0.2 - * @ucanto/principal bumped from ^3.0.0 to ^3.0.1 - -### [3.0.1](https://www.github.com/web3-storage/ucanto/compare/validator-v3.0.0...validator-v3.0.1) (2022-11-02) - - -### Bug Fixes - -* update @ipld/car and @ipld/dag-cbor deps ([#120](https://www.github.com/web3-storage/ucanto/issues/120)) ([5dcd7a5](https://www.github.com/web3-storage/ucanto/commit/5dcd7a5788dfd126f332b126f7ad1215972f29c4)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^3.0.0 to ^3.0.1 - * devDependencies - * @ucanto/client bumped from ^3.0.0 to ^3.0.1 - -## [3.0.0](https://www.github.com/web3-storage/ucanto/compare/validator-v2.0.0...validator-v3.0.0) (2022-10-20) - - -### ⚠ BREAKING CHANGES - -* upgrades to multiformats@10 (#117) - -### Features - -* upgrades to multiformats@10 ([#117](https://www.github.com/web3-storage/ucanto/issues/117)) ([61dc4ca](https://www.github.com/web3-storage/ucanto/commit/61dc4cafece3365bbf60f709265ea71180f226d7)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^2.0.0 to ^3.0.0 - * @ucanto/interface bumped from ^2.0.0 to ^3.0.0 - * devDependencies - * @ucanto/principal bumped from ^2.0.0 to ^3.0.0 - * @ucanto/client bumped from ^2.0.0 to ^3.0.0 - -## [2.0.0](https://www.github.com/web3-storage/ucanto/compare/validator-v1.0.1...validator-v2.0.0) (2022-10-16) - - -### ⚠ BREAKING CHANGES - -* switch decoder API to zod like schema API (#108) -* upgrade to ucan 0.9 (#95) - -### Features - -* Impelment InferInvokedCapability per [#99](https://www.github.com/web3-storage/ucanto/issues/99) ([#100](https://www.github.com/web3-storage/ucanto/issues/100)) ([fc5a2ac](https://www.github.com/web3-storage/ucanto/commit/fc5a2ace33f2a3599a654d8edd1641d111032074)) -* implement .delegate on capabilities ([#110](https://www.github.com/web3-storage/ucanto/issues/110)) ([fd0bb9d](https://www.github.com/web3-storage/ucanto/commit/fd0bb9da58836c05d6ee9f60cd6b1cb6b747e3b1)) -* rip out special handling of my: and as: capabilities ([#109](https://www.github.com/web3-storage/ucanto/issues/109)) ([3ec8e64](https://www.github.com/web3-storage/ucanto/commit/3ec8e6434a096221bf72193e074810cc18dd5cd8)) -* switch decoder API to zod like schema API ([#108](https://www.github.com/web3-storage/ucanto/issues/108)) ([e2e03ff](https://www.github.com/web3-storage/ucanto/commit/e2e03ffeb35f00627335dbfd3e128e2cf9dcfdee)) -* upgrade to ucan 0.9 ([#95](https://www.github.com/web3-storage/ucanto/issues/95)) ([b752b39](https://www.github.com/web3-storage/ucanto/commit/b752b398950120d6121badcdbb639f4dc9ce8794)) - - -### Bug Fixes - -* optional caveats ([#106](https://www.github.com/web3-storage/ucanto/issues/106)) ([537a4c8](https://www.github.com/web3-storage/ucanto/commit/537a4c86fdd02c26c1442d6a205e8977afbad603)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^1.0.1 to ^2.0.0 - * @ucanto/interface bumped from ^1.0.0 to ^2.0.0 - * devDependencies - * @ucanto/principal bumped from ^1.0.1 to ^2.0.0 - * @ucanto/client bumped from ^1.0.1 to ^2.0.0 - -### [1.0.1](https://www.github.com/web3-storage/ucanto/compare/validator-v1.0.0...validator-v1.0.1) (2022-09-21) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^1.0.0 to ^1.0.1 - * devDependencies - * @ucanto/principal bumped from ^1.0.0 to ^1.0.1 - * @ucanto/client bumped from ^1.0.0 to ^1.0.1 - -## [1.0.0](https://www.github.com/web3-storage/ucanto/compare/validator-v0.6.0...validator-v1.0.0) (2022-09-14) - - -### ⚠ BREAKING CHANGES - -* update dag-ucan, types and names (#90) - -### Features - -* update dag-ucan, types and names ([#90](https://www.github.com/web3-storage/ucanto/issues/90)) ([cd792c9](https://www.github.com/web3-storage/ucanto/commit/cd792c934fbd358d6ccfa5d02f205b14b5f2e14e)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^0.6.0 to ^1.0.0 - * @ucanto/interface bumped from ^0.7.0 to ^1.0.0 - * devDependencies - * @ucanto/principal bumped from ^0.5.0 to ^1.0.0 - * @ucanto/client bumped from ^0.6.0 to ^1.0.0 - -## [0.6.0](https://www.github.com/web3-storage/ucanto/compare/validator-v0.5.5...validator-v0.6.0) (2022-07-28) - - -### Features - -* delgation iterate, more errors and types ([0606168](https://www.github.com/web3-storage/ucanto/commit/0606168313d17d66bcc1ad6091440765e1700a4f)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^0.5.4 to ^0.6.0 - * @ucanto/interface bumped from ^0.6.2 to ^0.7.0 - * devDependencies - * @ucanto/authority bumped from ^0.4.5 to ^0.5.0 - * @ucanto/client bumped from ^0.5.4 to ^0.6.0 - -### [0.5.5](https://www.github.com/web3-storage/ucanto/compare/validator-v0.5.4...validator-v0.5.5) (2022-07-11) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^0.5.3 to ^0.5.4 - * devDependencies - * @ucanto/client bumped from ^0.5.3 to ^0.5.4 - * @ucanto/authority bumped from ^0.4.4 to ^0.4.5 - -### [0.5.4](https://www.github.com/web3-storage/ucanto/compare/validator-v0.5.3...validator-v0.5.4) (2022-07-01) - - -### Dependencies - -* The following workspace dependencies were updated - * devDependencies - * @ucanto/client bumped from ^0.5.2 to ^0.5.3 - -### [0.5.3](https://www.github.com/web3-storage/ucanto/compare/validator-v0.5.2...validator-v0.5.3) (2022-07-01) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^0.5.2 to ^0.5.3 - * @ucanto/interface bumped from ^0.6.1 to ^0.6.2 - * devDependencies - * @ucanto/client bumped from ^0.5.1 to ^0.5.2 - * @ucanto/authority bumped from ^0.4.3 to ^0.4.4 - -### [0.5.2](https://www.github.com/web3-storage/ucanto/compare/validator-v0.5.1...validator-v0.5.2) (2022-06-30) - - -### Bug Fixes - -* build types before publishing ([#71](https://www.github.com/web3-storage/ucanto/issues/71)) ([04b7958](https://www.github.com/web3-storage/ucanto/commit/04b79588f77dba234aaf628f62f574b124bd540b)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^0.5.1 to ^0.5.2 - * @ucanto/interface bumped from ^0.6.0 to ^0.6.1 - * devDependencies - * @ucanto/client bumped from ^0.5.0 to ^0.5.1 - * @ucanto/authority bumped from ^0.4.2 to ^0.4.3 - -### [0.5.1](https://www.github.com/web3-storage/ucanto/compare/validator-v0.5.0...validator-v0.5.1) (2022-06-24) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^0.5.0 to ^0.5.1 - * @ucanto/interface bumped from ^0.5.0 to ^0.6.0 - * devDependencies - * @ucanto/client bumped from ^0.4.0 to ^0.5.0 - * @ucanto/authority bumped from ^0.4.1 to ^0.4.2 - -## [0.5.0](https://www.github.com/web3-storage/ucanto/compare/validator-v0.4.0...validator-v0.5.0) (2022-06-23) - - -### Features - -* **ucanto:** capability create / inovke methods ([#51](https://www.github.com/web3-storage/ucanto/issues/51)) ([ddf56b1](https://www.github.com/web3-storage/ucanto/commit/ddf56b1ec80ff6c0698255c531936d8eeab532fd)) -* **ucanto:** URI protocol type retention & capability constructors ([e291544](https://www.github.com/web3-storage/ucanto/commit/e2915447254990d6e2384ff79a1da38120426ed5)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^0.4.0 to ^0.5.0 - * @ucanto/interface bumped from ^0.4.0 to ^0.5.0 - * devDependencies - * @ucanto/client bumped from ^0.3.0 to ^0.4.0 - * @ucanto/authority bumped from ^0.4.0 to ^0.4.1 - -## [0.4.0](https://www.github.com/web3-storage/ucanto/compare/validator-v0.3.0...validator-v0.4.0) (2022-06-20) - - -### Features - -* alight link API with multiformats ([#36](https://www.github.com/web3-storage/ucanto/issues/36)) ([0ec460e](https://www.github.com/web3-storage/ucanto/commit/0ec460e43ddda0bb3a3fea8a7881da1463154f36)) -* cherry pick changes from uploads-v2 demo ([#43](https://www.github.com/web3-storage/ucanto/issues/43)) ([4308fd2](https://www.github.com/web3-storage/ucanto/commit/4308fd2f392b9fcccc52af64432dcb04c8257e0b)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^0.3.0 to ^0.4.0 - * @ucanto/interface bumped from ^0.3.0 to ^0.4.0 - * devDependencies - * @ucanto/client bumped from ^0.2.2 to ^0.3.0 - * @ucanto/authority bumped from ^0.3.0 to ^0.4.0 - -## [0.3.0](https://www.github.com/web3-storage/ucanto/compare/validator-v0.2.0...validator-v0.3.0) (2022-06-15) - - -### Features - -* capability provider API ([#34](https://www.github.com/web3-storage/ucanto/issues/34)) ([ea89f97](https://www.github.com/web3-storage/ucanto/commit/ea89f97125bb484a12ce3ca09a7884911a9fd4d6)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^0.2.0 to ^0.3.0 - * @ucanto/interface bumped from ^0.2.0 to ^0.3.0 - * devDependencies - * @ucanto/client bumped from ^0.2.0 to ^0.2.2 - * @ucanto/authority bumped from ^0.2.0 to ^0.3.0 - -## [0.2.0](https://www.github.com/web3-storage/ucanto/compare/validator-v0.1.0...validator-v0.2.0) (2022-06-10) - - -### Features - -* setup pnpm & release-please ([84ac7f1](https://www.github.com/web3-storage/ucanto/commit/84ac7f12e5a66ee4919fa7527858dc916850e3e0)) - - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @ucanto/core bumped from ^0.0.1 to ^0.2.0 - * @ucanto/interface bumped from ^0.0.1 to ^0.2.0 - * devDependencies - * @ucanto/client bumped from ^0.0.1 to ^0.2.0 - * @ucanto/authority bumped from 0.0.1 to ^0.2.0 \ No newline at end of file diff --git a/packages/validator/package.json b/packages/validator/package.json deleted file mode 100644 index 9f83a25b..00000000 --- a/packages/validator/package.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "name": "@ucanto/validator", - "description": "UCAN RPC validators", - "version": "4.0.3", - "keywords": [ - "UCAN", - "ed25519", - "did", - "issuer", - "audience" - ], - "files": [ - "src", - "dist/src" - ], - "repository": { - "type": "git", - "url": "https://github.com/web3-storage/ucanto.git" - }, - "homepage": "https://github.com/web3-storage/ucanto", - "scripts": { - "test:web": "playwright-test test/**/*.spec.js --cov && nyc report", - "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha test/**/*.spec.js", - "test": "npm run test:node", - "coverage": "c8 --reporter=html mocha test/**/*.spec.js", - "check": "tsc --build", - "build": "tsc --build" - }, - "dependencies": { - "@ipld/car": "^5.0.0", - "@ipld/dag-cbor": "^8.0.0", - "@ucanto/core": "^4.0.3", - "@ucanto/interface": "^4.0.3", - "multiformats": "^10.0.2" - }, - "devDependencies": { - "@types/chai": "^4.3.3", - "@types/chai-subset": "^1.3.3", - "@types/mocha": "^9.1.0", - "@ucanto/client": "^4.0.3", - "@ucanto/principal": "^4.0.3", - "c8": "^7.11.0", - "chai": "^4.3.6", - "chai-subset": "^1.6.0", - "mocha": "^10.1.0", - "nyc": "^15.1.0", - "playwright-test": "^8.1.1", - "typescript": "^4.9.4" - }, - "type": "module", - "main": "src/lib.js", - "types": "./dist/src/lib.d.ts", - "typesVersions": { - "*": { - "*": [ - "dist/*" - ], - "dist/src/lib.d.ts": [ - "dist/src/lib.d.ts" - ] - } - }, - "exports": { - ".": { - "types": "./dist/src/lib.d.ts", - "import": "./src/lib.js" - } - }, - "c8": { - "exclude": [ - "test/**", - "dist/**" - ] - }, - "license": "(Apache-2.0 AND MIT)" -} diff --git a/packages/validator/src/capability.js b/packages/validator/src/capability.js deleted file mode 100644 index dd54cd76..00000000 --- a/packages/validator/src/capability.js +++ /dev/null @@ -1,837 +0,0 @@ -import * as API from '@ucanto/interface' -import { entries, combine, intersection } from './util.js' -import { - EscalatedCapability, - MalformedCapability, - UnknownCapability, - DelegationError as MatchError, - Failure, -} from './error.js' -import { invoke, delegate } from '@ucanto/core' - -/** - * @template {API.Ability} A - * @template {API.URI} R - * @template {API.Caveats} [C={}] - * @param {API.Descriptor} descriptor - * @returns {API.TheCapabilityParser>} - */ -export const capability = descriptor => new Capability(descriptor) - -/** - * @template {API.Match} M - * @template {API.Match} W - * @param {API.Matcher} left - * @param {API.Matcher} right - * @returns {API.CapabilityParser} - */ -export const or = (left, right) => new Or(left, right) - -/** - * @template {API.MatchSelector[]} Selectors - * @param {Selectors} selectors - * @returns {API.CapabilitiesParser>} - */ -export const and = (...selectors) => new And(selectors) - -/** - * @template {API.Match} M - * @template {API.ParsedCapability} T - * @param {API.DeriveSelector & { from: API.MatchSelector }} options - * @returns {API.TheCapabilityParser>} - */ -export const derive = ({ from, to, derives }) => new Derive(from, to, derives) - -/** - * @template {API.Match} M - * @implements {API.View} - */ -class View { - /** - * @param {API.Source} source - * @returns {API.MatchResult} - */ - /* c8 ignore next 3 */ - match(source) { - return new UnknownCapability(source.capability) - } - - /** - * @param {API.Source[]} capabilities - */ - select(capabilities) { - return select(this, capabilities) - } - - /** - * @template {API.ParsedCapability} U - * @param {API.DeriveSelector} options - * @returns {API.TheCapabilityParser>} - */ - derive({ derives, to }) { - return derive({ derives, to, from: this }) - } -} - -/** - * @template {API.Match} M - * @implements {API.CapabilityParser} - * @extends {View} - */ -class Unit extends View { - /** - * @template {API.Match} W - * @param {API.MatchSelector} other - * @returns {API.CapabilityParser} - */ - or(other) { - return or(this, other) - } - - /** - * @template {API.Match} W - * @param {API.CapabilityParser} other - * @returns {API.CapabilitiesParser<[M, W]>} - */ - and(other) { - return and(/** @type {API.CapabilityParser} */ (this), other) - } -} - -/** - * @template {API.Ability} A - * @template {API.URI} R - * @template {API.Caveats} C - * @implements {API.TheCapabilityParser>>>} - * @extends {Unit>>>} - */ -class Capability extends Unit { - /** - * @param {API.Descriptor} descriptor - */ - constructor(descriptor) { - super() - this.descriptor = { derives, ...descriptor } - } - - /** - * @param {unknown} source - */ - read(source) { - try { - const result = this.create(/** @type {any} */ (source)) - return /** @type {API.Result>, API.Failure>} */ ( - result - ) - } catch (error) { - return /** @type {any} */ (error).cause - } - } - - /** - * @param {API.InferCreateOptions>} options - */ - create(options) { - const { descriptor, can } = this - const decoders = descriptor.nb - const data = /** @type {API.InferCaveats} */ (options.nb || {}) - - const resource = descriptor.with.read(options.with) - if (resource.error) { - throw Object.assign(new Error(`Invalid 'with' - ${resource.message}`), { - cause: resource, - }) - } - - const capabality = - /** @type {API.ParsedCapability>} */ - ({ can, with: resource }) - - for (const [name, decoder] of Object.entries(decoders || {})) { - const key = /** @type {keyof data & string} */ (name) - const value = decoder.read(data[key]) - if (value?.error) { - throw Object.assign( - new Error(`Invalid 'nb.${key}' - ${value.message}`), - { cause: value } - ) - } else if (value !== undefined) { - const nb = - capabality.nb || - (capabality.nb = /** @type {API.InferCaveats} */ ({})) - - const key = /** @type {keyof nb} */ (name) - nb[key] = /** @type {typeof nb[key]} */ (value) - } - } - - return capabality - } - - /** - * @param {API.InferInvokeOptions>} options - */ - invoke({ with: with_, nb, ...options }) { - return invoke({ - ...options, - capability: this.create( - /** @type {API.InferCreateOptions>} */ - ({ with: with_, nb }) - ), - }) - } - - /** - * @param {API.InferDelegationOptions>} options - */ - async delegate({ with: with_, nb, ...options }) { - const { descriptor, can } = this - const readers = descriptor.nb - const data = /** @type {API.InferCaveats} */ (nb || {}) - - const resource = descriptor.with.read(with_) - if (resource.error) { - throw Object.assign(new Error(`Invalid 'with' - ${resource.message}`), { - cause: resource, - }) - } - - const capabality = - /** @type {API.ParsedCapability>} */ - ({ can, with: resource }) - - for (const [name, reader] of Object.entries(readers || {})) { - const key = /** @type {keyof data & string} */ (name) - const source = data[key] - // omit undefined fields in the delegation - const value = source === undefined ? source : reader.read(data[key]) - if (value?.error) { - throw Object.assign( - new Error(`Invalid 'nb.${key}' - ${value.message}`), - { cause: value } - ) - } else if (value !== undefined) { - const nb = - capabality.nb || - (capabality.nb = /** @type {API.InferCaveats} */ ({})) - - const key = /** @type {keyof nb} */ (name) - nb[key] = /** @type {typeof nb[key]} */ (value) - } - } - - return await delegate({ - capabilities: [capabality], - ...options, - }) - } - - get can() { - return this.descriptor.can - } - - /** - * @type {API.Reader} - */ - - get with() { - return this.descriptor.with - } - - /** - * @param {API.Source} source - * @returns {API.MatchResult>>>} - */ - match(source) { - const result = parse(this, source) - return result.error ? result : new Match(source, result, this.descriptor) - } - toString() { - return JSON.stringify({ can: this.descriptor.can }) - } -} - -/** - * @template {API.Match} M - * @template {API.Match} W - * @implements {API.CapabilityParser} - * @extends {Unit} - */ -class Or extends Unit { - /** - * @param {API.Matcher} left - * @param {API.Matcher} right - */ - constructor(left, right) { - super() - this.left = left - this.right = right - } - - /** - * @param {API.Source} capability - * @return {API.MatchResult} - */ - match(capability) { - const left = this.left.match(capability) - if (left.error) { - const right = this.right.match(capability) - if (right.error) { - return right.name === 'MalformedCapability' - ? // - right - : // - left - } else { - return right - } - } else { - return left - } - } - - toString() { - return `${this.left.toString()}|${this.right.toString()}` - } -} - -/** - * @template {API.MatchSelector[]} Selectors - * @implements {API.CapabilitiesParser>} - * @extends {View>>} - */ -class And extends View { - /** - * @param {Selectors} selectors - */ - constructor(selectors) { - super() - this.selectors = selectors - } - /** - * @param {API.Source} capability - * @returns {API.MatchResult>>} - */ - match(capability) { - const group = [] - for (const selector of this.selectors) { - const result = selector.match(capability) - if (result.error) { - return result - } else { - group.push(result) - } - } - - return new AndMatch(/** @type {API.InferMembers} */ (group)) - } - - /** - * @param {API.Source[]} capabilities - */ - select(capabilities) { - return selectGroup(this, capabilities) - } - /** - * @template E - * @template {API.Match} X - * @param {API.MatchSelector>} other - * @returns {API.CapabilitiesParser<[...API.InferMembers, API.Match]>} - */ - and(other) { - return new And([...this.selectors, other]) - } - toString() { - return `[${this.selectors.map(String).join(', ')}]` - } -} - -/** - * @template {API.ParsedCapability} T - * @template {API.Match} M - * @implements {API.TheCapabilityParser>} - * @extends {Unit>} - */ - -class Derive extends Unit { - /** - * @param {API.MatchSelector} from - * @param {API.TheCapabilityParser>} to - * @param {API.Derives, API.ToDeriveProof>} derives - */ - constructor(from, to, derives) { - super() - this.from = from - this.to = to - this.derives = derives - } - - /** - * @param {unknown} source - */ - read(source) { - return this.to.read(source) - } - - /** - * @type {typeof this.to['create']} - */ - create(options) { - return this.to.create(options) - } - /** - * @type {typeof this.to['invoke']} - */ - invoke(options) { - return this.to.invoke(options) - } - /** - * @type {typeof this.to['delegate']} - */ - delegate(options) { - return this.to.delegate(options) - } - get can() { - return this.to.can - } - get with() { - return this.to.with - } - /** - * @param {API.Source} capability - * @returns {API.MatchResult>} - */ - match(capability) { - const match = this.to.match(capability) - if (match.error) { - return match - } else { - return new DerivedMatch(match, this.from, this.derives) - } - } - toString() { - return this.to.toString() - } -} - -/** - * @template {API.Ability} A - * @template {API.URI} R - * @template {API.Caveats} C - * @implements {API.DirectMatch>>} - */ -class Match { - /** - * @param {API.Source} source - * @param {API.ParsedCapability>} value - * @param {API.Descriptor} descriptor - */ - constructor(source, value, descriptor) { - this.source = [source] - this.value = value - this.descriptor = { derives, ...descriptor } - } - get can() { - return this.value.can - } - - get proofs() { - const proofs = [this.source[0].delegation] - Object.defineProperties(this, { - proofs: { value: proofs }, - }) - return proofs - } - - /** - * @param {API.CanIssue} context - * @returns {API.DirectMatch>>|null} - */ - prune(context) { - if (context.canIssue(this.value, this.source[0].delegation.issuer.did())) { - return null - } else { - return this - } - } - - /** - * @param {API.Source[]} capabilities - * @returns {API.Select>>>} - */ - select(capabilities) { - const unknown = [] - const errors = [] - const matches = [] - for (const capability of capabilities) { - const result = parse(this, capability, true) - if (!result.error) { - const claim = this.descriptor.derives(this.value, result) - if (claim.error) { - errors.push( - new MatchError( - [new EscalatedCapability(this.value, result, claim)], - this - ) - ) - } else { - matches.push(new Match(capability, result, this.descriptor)) - } - } else { - switch (result.name) { - case 'UnknownCapability': - unknown.push(result.capability) - break - case 'MalformedCapability': - default: - errors.push(new MatchError([result], this)) - } - } - } - - return { matches, unknown, errors } - } - toString() { - const { nb } = this.value - return JSON.stringify({ - can: this.descriptor.can, - with: this.value.with, - nb: nb && Object.keys(nb).length > 0 ? nb : undefined, - }) - } -} - -/** - * @template {API.ParsedCapability} T - * @template {API.Match} M - * @implements {API.DerivedMatch} - */ - -class DerivedMatch { - /** - * @param {API.DirectMatch} selected - * @param {API.MatchSelector} from - * @param {API.Derives, API.ToDeriveProof>} derives - */ - constructor(selected, from, derives) { - this.selected = selected - this.from = from - this.derives = derives - } - get can() { - return this.value.can - } - get source() { - return this.selected.source - } - get proofs() { - const proofs = [] - for (const { delegation } of this.selected.source) { - proofs.push(delegation) - } - Object.defineProperties(this, { proofs: { value: proofs } }) - return proofs - } - get value() { - return this.selected.value - } - - /** - * @param {API.CanIssue} context - */ - prune(context) { - const selected = - /** @type {API.DirectMatch|null} */ - (this.selected.prune(context)) - return selected ? new DerivedMatch(selected, this.from, this.derives) : null - } - - /** - * @param {API.Source[]} capabilities - */ - select(capabilities) { - const { derives, selected, from } = this - const { value } = selected - - const direct = selected.select(capabilities) - - const derived = from.select(capabilities) - const matches = [] - const errors = [] - for (const match of derived.matches) { - // If capability can not be derived it escalates - const result = derives(value, match.value) - if (result.error) { - errors.push( - new MatchError( - [new EscalatedCapability(value, match.value, result)], - this - ) - ) - } else { - matches.push(match) - } - } - - return { - unknown: intersection(direct.unknown, derived.unknown), - errors: [ - ...errors, - ...direct.errors, - ...derived.errors.map(error => new MatchError([error], this)), - ], - matches: [ - ...direct.matches.map(match => new DerivedMatch(match, from, derives)), - ...matches, - ], - } - } - - toString() { - return this.selected.toString() - } -} - -/** - * @template {API.MatchSelector[]} Selectors - * @implements {API.Amplify>} - */ -class AndMatch { - /** - * @param {API.Match[]} matches - */ - constructor(matches) { - this.matches = matches - } - get selectors() { - return this.matches - } - /** - * @returns {API.Source[]} - */ - get source() { - const source = [] - - for (const match of this.matches) { - source.push(...match.source) - } - Object.defineProperties(this, { source: { value: source } }) - return source - } - - /** - * @param {API.CanIssue} context - */ - prune(context) { - const matches = [] - for (const match of this.matches) { - const pruned = match.prune(context) - if (pruned) { - matches.push(pruned) - } - } - return matches.length === 0 ? null : new AndMatch(matches) - } - - get proofs() { - const proofs = [] - - for (const { delegation } of this.source) { - proofs.push(delegation) - } - - Object.defineProperties(this, { proofs: { value: proofs } }) - return proofs - } - /** - * @type {API.InferValue>} - */ - get value() { - const value = [] - - for (const match of this.matches) { - value.push(match.value) - } - Object.defineProperties(this, { value: { value } }) - return /** @type {any} */ (value) - } - /** - * @param {API.Source[]} capabilities - */ - select(capabilities) { - return selectGroup(this, capabilities) - } - toString() { - return `[${this.matches.map(match => match.toString()).join(', ')}]` - } -} - -/** - * Parses capability `source` using a provided capability `parser`. By default - * invocation parsing occurs, which respects a capability schema, failing if - * any non-optional field is missing. If `optional` argument is `true` it will - * parse capability as delegation, in this case all `nb` fields are considered - * optional. - * - * @template {API.Ability} A - * @template {API.URI} R - * @template {API.Caveats} C - * @param {{descriptor: API.Descriptor}} parser - * @param {API.Source} source - * @param {boolean} [optional=false] - * @returns {API.Result>, API.InvalidCapability>} - */ - -const parse = (parser, source, optional = false) => { - const { can, with: withReader, nb: readers } = parser.descriptor - const { delegation } = source - const capability = /** @type {API.Capability>} */ ( - source.capability - ) - - if (capability.can !== can) { - return new UnknownCapability(capability) - } - - const uri = withReader.read(capability.with) - if (uri.error) { - return new MalformedCapability(capability, uri) - } - - const nb = /** @type {API.InferCaveats} */ ({}) - - if (readers) { - /** @type {Partial>} */ - const caveats = capability.nb || {} - for (const [name, reader] of entries(readers)) { - const key = /** @type {keyof caveats & keyof nb & string} */ (name) - if (key in caveats || !optional) { - const result = reader.read(caveats[key]) - if (result?.error) { - return new MalformedCapability(capability, result) - } else if (result != null) { - nb[key] = /** @type {any} */ (result) - } - } - } - } - - return new CapabilityView(can, capability.with, nb, delegation) -} - -/** - * @template {API.Ability} A - * @template {API.URI} R - * @template C - */ -class CapabilityView { - /** - * @param {A} can - * @param {R} with_ - * @param {API.InferCaveats} nb - * @param {API.Delegation} delegation - */ - constructor(can, with_, nb, delegation) { - this.can = can - this.with = with_ - this.delegation = delegation - this.nb = nb - } -} - -/** - * @template {API.Match} M - * @param {API.Matcher} matcher - * @param {API.Source[]} capabilities - */ - -const select = (matcher, capabilities) => { - const unknown = [] - const matches = [] - const errors = [] - for (const capability of capabilities) { - const result = matcher.match(capability) - if (result.error) { - switch (result.name) { - case 'UnknownCapability': - unknown.push(result.capability) - break - case 'MalformedCapability': - default: - errors.push(new MatchError([result], result.capability)) - } - } else { - matches.push(result) - } - } - - return { matches, errors, unknown } -} - -/** - * @template {API.Selector[]} S - * @param {{selectors:S}} self - * @param {API.Source[]} capabilities - */ - -const selectGroup = (self, capabilities) => { - let unknown - const data = [] - const errors = [] - for (const selector of self.selectors) { - const selected = selector.select(capabilities) - unknown = unknown - ? intersection(unknown, selected.unknown) - : selected.unknown - - for (const error of selected.errors) { - errors.push(new MatchError([error], self)) - } - - data.push(selected.matches) - } - - const matches = combine(data).map(group => new AndMatch(group)) - - return { - unknown: - /* c8 ignore next */ - unknown || [], - errors, - matches, - } -} - -/** - * @template {API.ParsedCapability} T - * @template {API.ParsedCapability} U - * @param {T} claimed - * @param {U} delegated - * @return {API.Result} - */ -const derives = (claimed, delegated) => { - if (delegated.with.endsWith('*')) { - if (!claimed.with.startsWith(delegated.with.slice(0, -1))) { - return new Failure( - `Resource ${claimed.with} does not match delegated ${delegated.with} ` - ) - } - } else if (delegated.with !== claimed.with) { - return new Failure( - `Resource ${claimed.with} is not contained by ${delegated.with}` - ) - } - - /* c8 ignore next 2 */ - const caveats = delegated.nb || {} - const nb = claimed.nb || {} - const kv = entries(caveats) - - for (const [name, value] of kv) { - if (nb[name] != value) { - return new Failure(`${String(name)}: ${nb[name]} violates ${value}`) - } - } - - return true -} diff --git a/packages/validator/src/error.js b/packages/validator/src/error.js deleted file mode 100644 index 82d2c000..00000000 --- a/packages/validator/src/error.js +++ /dev/null @@ -1,319 +0,0 @@ -import * as API from '@ucanto/interface' -import { the } from './util.js' -import { isLink } from 'multiformats/link' - -/** - * @implements {API.Failure} - */ -export class Failure extends Error { - /** @type {true} */ - get error() { - return true - } - /* c8 ignore next 3 */ - describe() { - return this.name - } - get message() { - return this.describe() - } - - toJSON() { - const { error, name, message, stack } = this - return { error, name, message, stack } - } -} - -export class EscalatedCapability extends Failure { - /** - * @param {API.ParsedCapability} claimed - * @param {object} delegated - * @param {API.Failure} cause - */ - constructor(claimed, delegated, cause) { - super() - this.claimed = claimed - this.delegated = delegated - this.cause = cause - this.name = the('EscalatedCapability') - } - describe() { - return `Constraint violation: ${this.cause.message}` - } -} - -/** - * @implements {API.DelegationError} - */ -export class DelegationError extends Failure { - /** - * @param {(API.InvalidCapability | API.EscalatedDelegation | API.DelegationError)[]} causes - * @param {object} context - */ - constructor(causes, context) { - super() - this.name = the('InvalidClaim') - this.causes = causes - this.context = context - } - describe() { - return [ - `Can not derive ${this.context} from delegated capabilities:`, - ...this.causes.map(cause => li(cause.message)), - ].join('\n') - } - - /** - * @type {API.InvalidCapability | API.EscalatedDelegation | API.DelegationError} - */ - get cause() { - /* c8 ignore next 9 */ - if (this.causes.length !== 1) { - return this - } else { - const [cause] = this.causes - const value = cause.name === 'InvalidClaim' ? cause.cause : cause - Object.defineProperties(this, { cause: { value } }) - return value - } - } -} - -/** - * @implements {API.InvalidSignature} - */ -export class InvalidSignature extends Failure { - /** - * @param {API.Delegation} delegation - * @param {API.Verifier} verifier - */ - constructor(delegation, verifier) { - super() - this.name = the('InvalidSignature') - this.delegation = delegation - this.verifier = verifier - } - get issuer() { - return this.delegation.issuer - } - get audience() { - return this.delegation.audience - } - get key() { - return this.verifier.toDIDKey() - } - describe() { - const issuer = this.issuer.did() - const key = this.key - return ( - issuer.startsWith('did:key') - ? [ - `Proof ${this.delegation.cid} does not has a valid signature from ${key}`, - ] - : [ - `Proof ${this.delegation.cid} issued by ${issuer} does not has a valid signature from ${key}`, - ` ℹ️ Probably issuer signed with a different key, which got rotated, invalidating delegations that were issued with prior keys`, - ] - ).join('\n') - } -} - -/** - * @implements {API.UnavailableProof} - */ -export class UnavailableProof extends Failure { - /** - * @param {API.UCAN.Link} link - * @param {Error} [cause] - */ - constructor(link, cause) { - super() - this.name = the('UnavailableProof') - this.link = link - this.cause = cause - } - describe() { - return [ - `Linked proof '${this.link}' is not included and could not be resolved`, - ...(this.cause - ? [li(`Proof resolution failed with: ${this.cause.message}`)] - : []), - ].join('\n') - } -} - -export class DIDKeyResolutionError extends Failure { - /** - * @param {API.UCAN.DID} did - * @param {API.Unauthorized} [cause] - */ - constructor(did, cause) { - super() - this.name = the('DIDKeyResolutionError') - this.did = did - this.cause = cause - } - describe() { - return [ - `Unable to resolve '${this.did}' key`, - ...(this.cause ? [li(`Resolution failed: ${this.cause.message}`)] : []), - ].join('\n') - } -} - -/** - * @implements {API.InvalidAudience} - */ -export class InvalidAudience extends Failure { - /** - * @param {API.UCAN.Principal} audience - * @param {API.Delegation} delegation - */ - constructor(audience, delegation) { - super() - this.name = the('InvalidAudience') - this.audience = audience - this.delegation = delegation - } - describe() { - return `Delegation audience is '${this.delegation.audience.did()}' instead of '${this.audience.did()}'` - } - toJSON() { - const { error, name, audience, message, stack } = this - return { - error, - name, - audience: audience.did(), - delegation: { audience: this.delegation.audience.did() }, - message, - stack, - } - } -} - -/** - * @implements {API.MalformedCapability} - */ -export class MalformedCapability extends Failure { - /** - * @param {API.Capability} capability - * @param {API.Failure} cause - */ - constructor(capability, cause) { - super() - this.name = the('MalformedCapability') - this.capability = capability - this.cause = cause - } - describe() { - return [ - `Encountered malformed '${this.capability.can}' capability: ${format( - this.capability - )}`, - li(this.cause.message), - ].join('\n') - } -} - -export class UnknownCapability extends Failure { - /** - * @param {API.Capability} capability - */ - constructor(capability) { - super() - this.name = the('UnknownCapability') - this.capability = capability - } - /* c8 ignore next 3 */ - describe() { - return `Encountered unknown capability: ${format(this.capability)}` - } -} - -export class Expired extends Failure { - /** - * @param {API.Delegation & { expiration: number }} delegation - */ - constructor(delegation) { - super() - this.name = the('Expired') - this.delegation = delegation - } - describe() { - return `Proof ${this.delegation.cid} has expired on ${new Date( - this.delegation.expiration * 1000 - )}` - } - get expiredAt() { - return this.delegation.expiration - } - toJSON() { - const { error, name, expiredAt, message, stack } = this - return { - error, - name, - message, - expiredAt, - stack, - } - } -} - -export class NotValidBefore extends Failure { - /** - * @param {API.Delegation & { notBefore: number }} delegation - */ - constructor(delegation) { - super() - this.name = the('NotValidBefore') - this.delegation = delegation - } - describe() { - return `Proof ${this.delegation.cid} is not valid before ${new Date( - this.delegation.notBefore * 1000 - )}` - } - get validAt() { - return this.delegation.notBefore - } - toJSON() { - const { error, name, validAt, message, stack } = this - return { - error, - name, - message, - validAt, - stack, - } - } -} - -/** - * @param {unknown} capability - * @param {string|number} [space] - */ - -const format = (capability, space) => - JSON.stringify( - capability, - (_key, value) => { - /* c8 ignore next 2 */ - if (isLink(value)) { - return value.toString() - } else { - return value - } - }, - space - ) - -/** - * @param {string} message - */ -export const indent = (message, indent = ' ') => - `${indent}${message.split('\n').join(`\n${indent}`)}` - -/** - * @param {string} message - */ -export const li = message => indent(`- ${message}`) diff --git a/packages/validator/src/schema.js b/packages/validator/src/schema.js deleted file mode 100644 index e2a5e547..00000000 --- a/packages/validator/src/schema.js +++ /dev/null @@ -1,5 +0,0 @@ -export * as URI from './schema/uri.js' -export * as Link from './schema/link.js' -export * as DID from './schema/did.js' -export * as Text from './schema/text.js' -export * from './schema/schema.js' diff --git a/packages/validator/src/schema/did.js b/packages/validator/src/schema/did.js deleted file mode 100644 index 69277c58..00000000 --- a/packages/validator/src/schema/did.js +++ /dev/null @@ -1,39 +0,0 @@ -import * as API from '@ucanto/interface' -import * as Schema from './schema.js' - -/** - * @template {string} Method - * @extends {Schema.API & API.URI<"did:">, string, void|Method>} - */ -class DIDSchema extends Schema.API { - /** - * @param {string} source - * @param {void|Method} method - */ - readWith(source, method) { - const prefix = method ? `did:${method}:` : `did:` - if (!source.startsWith(prefix)) { - return Schema.error(`Expected a ${prefix} but got "${source}" instead`) - } else { - return /** @type {API.DID} */ (source) - } - } -} - -const schema = Schema.string().refine(new DIDSchema()) - -export const did = () => schema -/** - * - * @param {unknown} input - */ -export const read = input => schema.read(input) - -/** - * @template {string} Method - * @param {{method?: Method}} options - */ -export const match = options => - /** @type {Schema.Schema & API.URI<"did:">>} */ ( - Schema.string().refine(new DIDSchema(options.method)) - ) diff --git a/packages/validator/src/schema/link.js b/packages/validator/src/schema/link.js deleted file mode 100644 index 80dd34c1..00000000 --- a/packages/validator/src/schema/link.js +++ /dev/null @@ -1,79 +0,0 @@ -import * as API from '@ucanto/interface' -import { create, createLegacy, isLink, parse } from '@ucanto/core/link' -import * as Schema from './schema.js' - -export { create, createLegacy, isLink, parse } - -/** - * @template {number} [Code=number] - * @template {number} [Alg=number] - * @template {1|0} [Version=0|1] - * @typedef {{code?:Code, algorithm?:Alg, version?:Version}} Settings - */ - -/** - * @template {number} Code - * @template {number} Alg - * @template {1|0} Version - * @extends {Schema.API, unknown, Settings>} - */ -class LinkSchema extends Schema.API { - /** - * - * @param {unknown} cid - * @param {Settings} settings - * @returns {Schema.ReadResult>} - */ - readWith(cid, { code, algorithm, version }) { - if (cid == null) { - return Schema.error(`Expected link but got ${cid} instead`) - } else { - if (!isLink(cid)) { - return Schema.error(`Expected link to be a CID instead of ${cid}`) - } else { - if (code != null && cid.code !== code) { - return Schema.error( - `Expected link to be CID with 0x${code.toString(16)} codec` - ) - } - if (algorithm != null && cid.multihash.code !== algorithm) { - return Schema.error( - `Expected link to be CID with 0x${algorithm.toString( - 16 - )} hashing algorithm` - ) - } - - if (version != null && cid.version !== version) { - return Schema.error( - `Expected link to be CID version ${version} instead of ${cid.version}` - ) - } - - // @ts-expect-error - can't infer version, code etc. - return cid - } - } - } -} - -/** @type {Schema.Schema, unknown>} */ -export const schema = new LinkSchema({}) - -export const link = () => schema - -/** - * @template {number} Code - * @template {number} Alg - * @template {1|0} Version - * @param {Settings} options - * @returns {Schema.Schema>} - */ -export const match = (options = {}) => new LinkSchema(options) - -/** - * @param {unknown} input - */ -export const read = input => schema.read(input) - -export const optional = () => schema.optional() diff --git a/packages/validator/src/schema/schema.js b/packages/validator/src/schema/schema.js deleted file mode 100644 index c952fde8..00000000 --- a/packages/validator/src/schema/schema.js +++ /dev/null @@ -1,1280 +0,0 @@ -import * as Schema from './type.js' - -export * from './type.js' - -/** - * @abstract - * @template [T=unknown] - * @template [I=unknown] - * @template [Settings=void] - * @extends {Schema.Base} - * @implements {Schema.Schema} - */ -export class API { - /** - * @param {Settings} settings - */ - constructor(settings) { - /** @protected */ - this.settings = settings - } - - toString() { - return `new ${this.constructor.name}()` - } - /** - * @abstract - * @param {I} input - * @param {Settings} settings - * @returns {Schema.ReadResult} - */ - /* c8 ignore next 3 */ - readWith(input, settings) { - throw new Error(`Abstract method readWith must be implemented by subclass`) - } - /** - * @param {I} input - * @returns {Schema.ReadResult} - */ - read(input) { - return this.readWith(input, this.settings) - } - - /** - * @param {unknown} value - * @returns {value is T} - */ - is(value) { - return !this.read(/** @type {I} */ (value))?.error - } - - /** - * @param {unknown} value - * @return {T} - */ - from(value) { - const result = this.read(/** @type {I} */ (value)) - if (result?.error) { - throw result - } else { - return result - } - } - - /** - * @returns {Schema.Schema} - */ - optional() { - return optional(this) - } - - /** - * @returns {Schema.Schema} - */ - nullable() { - return nullable(this) - } - - /** - * @returns {Schema.Schema} - */ - array() { - return array(this) - } - /** - * @template U - * @param {Schema.Reader} schema - * @returns {Schema.Schema} - */ - - or(schema) { - return or(this, schema) - } - - /** - * @template U - * @param {Schema.Reader} schema - * @returns {Schema.Schema} - */ - and(schema) { - return and(this, schema) - } - - /** - * @template {T} U - * @param {Schema.Reader} schema - * @returns {Schema.Schema} - */ - refine(schema) { - return refine(this, schema) - } - - /** - * @template {string} Kind - * @param {Kind} [kind] - * @returns {Schema.Schema, I>} - */ - brand(kind) { - return /** @type {Schema.Schema, I>} */ (this) - } - - /** - * @param {Schema.NotUndefined} value - * @returns {Schema.DefaultSchema, I>} - */ - default(value) { - // ⚠️ this.from will throw if wrong default is provided - const fallback = this.from(value) - // we also check that fallback is not undefined because that is the point - // of having a fallback - if (fallback === undefined) { - throw new Error(`Value of type undefined is not a vaild default`) - } - - const schema = new Default({ - reader: /** @type {Schema.Reader} */ (this), - value: /** @type {Schema.NotUndefined} */ (fallback), - }) - - return /** @type {Schema.DefaultSchema, I>} */ ( - schema - ) - } -} - -/** - * @template [I=unknown] - * @extends {API} - * @implements {Schema.Schema} - */ -class Never extends API { - toString() { - return 'never()' - } - /** - * @param {I} input - * @returns {Schema.ReadResult} - */ - read(input) { - return typeError({ expect: 'never', actual: input }) - } -} - -/** - * @template [I=unknown] - * @returns {Schema.Schema} - */ -export const never = () => new Never() - -/** - * @template [I=unknown] - * @extends API - * @implements {Schema.Schema} - */ -class Unknown extends API { - /** - * @param {I} input - */ - read(input) { - return /** @type {Schema.ReadResult}*/ (input) - } - toString() { - return 'unknown()' - } -} - -/** - * @template [I=unknown] - * @returns {Schema.Schema} - */ -export const unknown = () => new Unknown() - -/** - * @template O - * @template [I=unknown] - * @extends {API>} - * @implements {Schema.Schema} - */ -class Nullable extends API { - /** - * @param {I} input - * @param {Schema.Reader} reader - */ - readWith(input, reader) { - const result = reader.read(input) - if (result?.error) { - return input === null - ? null - : new UnionError({ - causes: [result, typeError({ expect: 'null', actual: input })], - }) - } else { - return result - } - } - toString() { - return `${this.settings}.nullable()` - } -} - -/** - * @template O - * @template [I=unknown] - * @param {Schema.Reader} schema - * @returns {Schema.Schema} - */ -export const nullable = schema => new Nullable(schema) - -/** - * @template O - * @template [I=unknown] - * @extends {API>} - * @implements {Schema.Schema} - */ -class Optional extends API { - optional() { - return this - } - /** - * @param {I} input - * @param {Schema.Reader} reader - * @returns {Schema.ReadResult} - */ - readWith(input, reader) { - const result = reader.read(input) - return result?.error && input === undefined ? undefined : result - } - toString() { - return `${this.settings}.optional()` - } -} - -/** - * @template {unknown} O - * @template [I=unknown] - * @extends {API, value:O & Schema.NotUndefined}>} - * @implements {Schema.DefaultSchema} - */ -class Default extends API { - /** - * @returns {Schema.DefaultSchema, I>} - */ - optional() { - // Short circuit here as we there is no point in wrapping this in optional. - return /** @type {Schema.DefaultSchema, I>} */ ( - this - ) - } - /** - * @param {I} input - * @param {object} options - * @param {Schema.Reader} options.reader - * @param {O} options.value - * @returns {Schema.ReadResult} - */ - readWith(input, { reader, value }) { - if (input === undefined) { - return /** @type {Schema.ReadResult} */ (value) - } else { - const result = reader.read(input) - - return /** @type {Schema.ReadResult} */ ( - result === undefined ? value : result - ) - } - } - toString() { - return `${this.settings.reader}.default(${JSON.stringify( - this.settings.value - )})` - } - - get value() { - return this.settings.value - } -} - -/** - * @template O - * @template [I=unknown] - * @param {Schema.Reader} schema - * @returns {Schema.Schema} - */ -export const optional = schema => new Optional(schema) - -/** - * @template O - * @template [I=unknown] - * @extends {API>} - * @implements {Schema.ArraySchema} - */ -class ArrayOf extends API { - /** - * @param {I} input - * @param {Schema.Reader} schema - */ - readWith(input, schema) { - if (!Array.isArray(input)) { - return typeError({ expect: 'array', actual: input }) - } - /** @type {O[]} */ - const results = [] - for (const [index, value] of input.entries()) { - const result = schema.read(value) - if (result?.error) { - return memberError({ at: index, cause: result }) - } else { - results.push(result) - } - } - return results - } - get element() { - return this.settings - } - toString() { - return `array(${this.element})` - } -} - -/** - * @template O - * @template [I=unknown] - * @param {Schema.Reader} schema - * @returns {Schema.ArraySchema} - */ -export const array = schema => new ArrayOf(schema) - -/** - * @template {Schema.Reader} T - * @template {[T, ...T[]]} U - * @template [I=unknown] - * @extends {API, I, U>} - * @implements {Schema.Schema, I>} - */ -class Tuple extends API { - /** - * @param {I} input - * @param {U} shape - * @returns {Schema.ReadResult>} - */ - readWith(input, shape) { - if (!Array.isArray(input)) { - return typeError({ expect: 'array', actual: input }) - } - if (input.length !== this.shape.length) { - return new SchemaError( - `Array must contain exactly ${this.shape.length} elements` - ) - } - - const results = [] - for (const [index, reader] of shape.entries()) { - const result = reader.read(input[index]) - if (result?.error) { - return memberError({ at: index, cause: result }) - } else { - results[index] = result - } - } - - return /** @type {Schema.InferTuple} */ (results) - } - - /** @type {U} */ - get shape() { - return this.settings - } - - toString() { - return `tuple([${this.shape.map(reader => reader.toString()).join(', ')}])` - } -} - -/** - * @template {Schema.Reader} T - * @template {[T, ...T[]]} U - * @template [I=unknown] - * @param {U} shape - * @returns {Schema.Schema, I>} - */ -export const tuple = shape => new Tuple(shape) - -/** - * @template {[unknown, ...unknown[]]} T - * @template [I=unknown] - * @extends {API}>} - * @implements {Schema.Schema} - */ -class Enum extends API { - /** - * @param {I} input - * @param {{type:string, variants:Set}} settings - * @returns {Schema.ReadResult} - */ - readWith(input, { variants, type }) { - if (variants.has(input)) { - return /** @type {Schema.ReadResult} */ (input) - } else { - return typeError({ expect: type, actual: input }) - } - } - toString() { - return this.settings.type - } -} - -/** - * @template {string} T - * @template {[T, ...T[]]} U - * @template [I=unknown] - * @param {U} variants - * @returns {Schema.Schema} - */ -const createEnum = variants => - new Enum({ - type: variants.join('|'), - variants: new Set(variants), - }) -export { createEnum as enum } - -/** - * @template {Schema.Reader} T - * @template {[T, ...T[]]} U - * @template [I=unknown] - * @extends {API, I, U>} - * @implements {Schema.Schema, I>} - */ -class Union extends API { - /** - * @param {I} input - * @param {U} variants - */ - readWith(input, variants) { - const causes = [] - for (const reader of variants) { - const result = reader.read(input) - if (result?.error) { - causes.push(result) - } else { - return result - } - } - return new UnionError({ causes }) - } - - get variants() { - return this.settings - } - toString() { - return `union([${this.variants.map(type => type.toString()).join(', ')}])` - } -} - -/** - * @template {Schema.Reader} T - * @template {[T, ...T[]]} U - * @template [I=unknown] - * @param {U} variants - * @returns {Schema.Schema, I>} - */ -const union = variants => new Union(variants) - -/** - * @template T, U - * @template [I=unknown] - * @param {Schema.Reader} left - * @param {Schema.Reader} right - * @returns {Schema.Schema} - */ -export const or = (left, right) => union([left, right]) - -/** - * @template {Schema.Reader} T - * @template {[T, ...T[]]} U - * @template [I=unknown] - * @extends {API, I, U>} - * @implements {Schema.Schema, I>} - */ -class Intersection extends API { - /** - * @param {I} input - * @param {U} schemas - * @returns {Schema.ReadResult>} - */ - readWith(input, schemas) { - const causes = [] - for (const schema of schemas) { - const result = schema.read(input) - if (result?.error) { - causes.push(result) - } - } - - return causes.length > 0 - ? new IntersectionError({ causes }) - : /** @type {Schema.ReadResult>} */ (input) - } - toString() { - return `intersection([${this.settings - .map(type => type.toString()) - .join(',')}])` - } -} - -/** - * @template {Schema.Reader} T - * @template {[T, ...T[]]} U - * @template [I=unknown] - * @param {U} variants - * @returns {Schema.Schema, I>} - */ -export const intersection = variants => new Intersection(variants) - -/** - * @template T, U - * @template [I=unknown] - * @param {Schema.Reader} left - * @param {Schema.Reader} right - * @returns {Schema.Schema} - */ -export const and = (left, right) => intersection([left, right]) - -/** - * @template [I=unknown] - * @extends {API} - */ -class Boolean extends API { - /** - * @param {I} input - */ - readWith(input) { - switch (input) { - case true: - case false: - return /** @type {boolean} */ (input) - default: - return typeError({ - expect: 'boolean', - actual: input, - }) - } - } - - toString() { - return `boolean()` - } -} - -/** @type {Schema.Schema} */ -const anyBoolean = new Boolean() - -export const boolean = () => anyBoolean - -/** - * @template {number} [O=number] - * @template [I=unknown] - * @template [Settings=void] - * @extends {API} - * @implements {Schema.NumberSchema} - */ -class UnknownNumber extends API { - /** - * @param {number} n - */ - greaterThan(n) { - return this.refine(greaterThan(n)) - } - /** - * @param {number} n - */ - lessThan(n) { - return this.refine(lessThan(n)) - } - - /** - * @template {O} U - * @param {Schema.Reader} schema - * @returns {Schema.NumberSchema} - */ - refine(schema) { - return new RefinedNumber({ base: this, schema }) - } -} - -/** - * @template [I=unknown] - * @extends {UnknownNumber} - * @implements {Schema.NumberSchema} - */ -class AnyNumber extends UnknownNumber { - /** - * @param {I} input - * @returns {Schema.ReadResult} - */ - readWith(input) { - return typeof input === 'number' - ? input - : typeError({ expect: 'number', actual: input }) - } - toString() { - return `number()` - } -} - -/** @type {Schema.NumberSchema} */ -const anyNumber = new AnyNumber() -export const number = () => anyNumber - -/** - * @template {number} [T=number] - * @template {T} [O=T] - * @template [I=unknown] - * @extends {UnknownNumber, schema:Schema.Reader}>} - * @implements {Schema.NumberSchema} - */ -class RefinedNumber extends UnknownNumber { - /** - * @param {I} input - * @param {{base:Schema.Reader, schema:Schema.Reader}} settings - * @returns {Schema.ReadResult} - */ - readWith(input, { base, schema }) { - const result = base.read(input) - return result?.error ? result : schema.read(result) - } - toString() { - return `${this.settings.base}.refine(${this.settings.schema})` - } -} - -/** - * @template {number} T - * @extends {API} - */ -class LessThan extends API { - /** - * @param {T} input - * @param {number} number - * @returns {Schema.ReadResult} - */ - readWith(input, number) { - if (input < number) { - return input - } else { - return error(`Expected ${input} < ${number}`) - } - } - toString() { - return `lessThan(${this.settings})` - } -} - -/** - * @template {number} T - * @param {number} n - * @returns {Schema.Schema} - */ -export const lessThan = n => new LessThan(n) - -/** - * @template {number} T - * @extends {API} - */ -class GreaterThan extends API { - /** - * @param {T} input - * @param {number} number - * @returns {Schema.ReadResult} - */ - readWith(input, number) { - if (input > number) { - return input - } else { - return error(`Expected ${input} > ${number}`) - } - } - toString() { - return `greaterThan(${this.settings})` - } -} - -/** - * @template {number} T - * @param {number} n - * @returns {Schema.Schema} - */ -export const greaterThan = n => new GreaterThan(n) - -const Integer = { - /** - * @param {number} input - * @returns {Schema.ReadResult} - */ - read(input) { - return Number.isInteger(input) - ? /** @type {Schema.Integer} */ (input) - : typeError({ - expect: 'integer', - actual: input, - }) - }, - toString() { - return `Integer` - }, -} - -const anyInteger = anyNumber.refine(Integer) -export const integer = () => anyInteger - -const Float = { - /** - * @param {number} number - * @returns {Schema.ReadResult} - */ - read(number) { - return Number.isFinite(number) - ? /** @type {Schema.Float} */ (number) - : typeError({ - expect: 'Float', - actual: number, - }) - }, - toString() { - return 'Float' - }, -} - -const anyFloat = anyNumber.refine(Float) -export const float = () => anyFloat - -/** - * @template {string} [O=string] - * @template [I=unknown] - * @template [Settings=void] - * @extends {API} - */ -class UnknownString extends API { - /** - * @template {O|unknown} U - * @param {Schema.Reader} schema - * @returns {Schema.StringSchema} - */ - refine(schema) { - const other = /** @type {Schema.Reader} */ (schema) - const rest = new RefinedString({ - base: this, - schema: other, - }) - - return /** @type {Schema.StringSchema} */ (rest) - } - /** - * @template {string} Prefix - * @param {Prefix} prefix - */ - startsWith(prefix) { - return this.refine(startsWith(prefix)) - } - /** - * @template {string} Suffix - * @param {Suffix} suffix - */ - endsWith(suffix) { - return this.refine(endsWith(suffix)) - } - toString() { - return `string()` - } -} - -/** - * @template O - * @template {string} [T=string] - * @template [I=unknown] - * @extends {UnknownString, schema:Schema.Reader}>} - * @implements {Schema.StringSchema} - */ -class RefinedString extends UnknownString { - /** - * @param {I} input - * @param {{base:Schema.Reader, schema:Schema.Reader}} settings - * @returns {Schema.ReadResult} - */ - readWith(input, { base, schema }) { - const result = base.read(input) - return result?.error - ? result - : /** @type {Schema.ReadResult} */ (schema.read(result)) - } - toString() { - return `${this.settings.base}.refine(${this.settings.schema})` - } -} - -/** - * @template [I=unknown] - * @extends {UnknownString} - * @implements {Schema.StringSchema} - */ -class AnyString extends UnknownString { - /** - * @param {I} input - * @returns {Schema.ReadResult} - */ - readWith(input) { - return typeof input === 'string' - ? input - : typeError({ expect: 'string', actual: input }) - } -} - -/** @type {Schema.StringSchema} */ -const anyString = new AnyString() -export const string = () => anyString - -/** - * @template {string} Prefix - * @template {string} Body - * @extends {API} - * @implements {Schema.Schema} - */ -class StartsWith extends API { - /** - * @param {Body} input - * @param {Prefix} prefix - */ - readWith(input, prefix) { - return input.startsWith(prefix) - ? /** @type {Schema.ReadResult} */ (input) - : error(`Expect string to start with "${prefix}" instead got "${input}"`) - } - get prefix() { - return this.settings - } - toString() { - return `startsWith("${this.prefix}")` - } -} - -/** - * @template {string} Prefix - * @template {string} Body - * @param {Prefix} prefix - * @returns {Schema.Schema<`${Prefix}${string}`, string>} - */ -export const startsWith = prefix => new StartsWith(prefix) - -/** - * @template {string} Suffix - * @template {string} Body - * @extends {API} - */ -class EndsWith extends API { - /** - * @param {Body} input - * @param {Suffix} suffix - */ - readWith(input, suffix) { - return input.endsWith(suffix) - ? /** @type {Schema.ReadResult} */ (input) - : error(`Expect string to end with "${suffix}" instead got "${input}"`) - } - get suffix() { - return this.settings - } - toString() { - return `endsWith("${this.suffix}")` - } -} - -/** - * @template {string} Suffix - * @param {Suffix} suffix - * @returns {Schema.Schema<`${string}${Suffix}`, string>} - */ -export const endsWith = suffix => new EndsWith(suffix) - -/** - * @template T - * @template {T} U - * @template [I=unknown] - * @extends {API, schema: Schema.Reader }>} - * @implements {Schema.Schema} - */ - -class Refine extends API { - /** - * @param {I} input - * @param {{ base: Schema.Reader, schema: Schema.Reader }} settings - */ - readWith(input, { base, schema }) { - const result = base.read(input) - return result?.error ? result : schema.read(result) - } - toString() { - return `${this.settings.base}.refine(${this.settings.schema})` - } -} - -/** - * @template T - * @template {T} U - * @template [I=unknown] - * @param {Schema.Reader} base - * @param {Schema.Reader} schema - * @returns {Schema.Schema} - */ -export const refine = (base, schema) => new Refine({ base, schema }) - -/** - * @template {null|boolean|string|number} T - * @template [I=unknown] - * @extends {API} - * @implements {Schema.LiteralSchema} - */ -class Literal extends API { - /** - * @param {I} input - * @param {T} expect - * @returns {Schema.ReadResult} - */ - readWith(input, expect) { - return input !== /** @type {unknown} */ (expect) - ? new LiteralError({ expect, actual: input }) - : expect - } - get value() { - return /** @type {Exclude} */ (this.settings) - } - /** - * @template {Schema.NotUndefined} U - * @param {U} value - */ - default(value = /** @type {U} */ (this.value)) { - return super.default(value) - } - toString() { - return `literal(${displayTypeName(this.value)})` - } -} - -/** - * @template {null|boolean|string|number} T - * @template [I=unknown] - * @param {T} value - * @returns {Schema.LiteralSchema} - */ -export const literal = value => new Literal(value) - -/** - * @template {{[key:string]: Schema.Reader}} U - * @template [I=unknown] - * @extends {API, I, U>} - */ -class Struct extends API { - /** - * @param {I} input - * @param {U} shape - * @returns {Schema.ReadResult>} - */ - readWith(input, shape) { - if (typeof input != 'object' || input === null || Array.isArray(input)) { - return typeError({ - expect: 'object', - actual: input, - }) - } - - const source = /** @type {{[K in keyof U]: unknown}} */ (input) - - const struct = /** @type {{[K in keyof U]: Schema.Infer}} */ ({}) - const entries = - /** @type {{[K in keyof U]: [K & string, U[K]]}[keyof U][]} */ ( - Object.entries(shape) - ) - - for (const [at, reader] of entries) { - const result = reader.read(source[at]) - if (result?.error) { - return memberError({ at, cause: result }) - } - // skip undefined because they mess up CBOR and are generally useless. - else if (result !== undefined) { - struct[at] = /** @type {Schema.Infer} */ (result) - } - } - - return struct - } - - /** @type {U} */ - get shape() { - // @ts-ignore - We declared `settings` private but we access it here - return this.settings - } - - toString() { - return [ - `struct({ `, - ...Object.entries(this.shape).map( - ([key, schema]) => `${key}: ${schema}, ` - ), - `})`, - ].join('') - } - - /** - * @param {Schema.InferStructSource} data - */ - create(data) { - return this.from(data || {}) - } - - /** - * @template {{[key:string]: Schema.Reader}} E - * @param {E} extension - * @returns {Schema.StructSchema} - */ - extend(extension) { - return new Struct({ ...this.shape, ...extension }) - } -} - -/** - * @template {null|boolean|string|number} T - * @template {{[key:string]: T|Schema.Reader}} U - * @template {{[K in keyof U]: U[K] extends Schema.Reader ? U[K] : Schema.LiteralSchema}} V - * @template [I=unknown] - * @param {U} fields - * @returns {Schema.StructSchema} - */ -export const struct = fields => { - const shape = - /** @type {{[K in keyof U]: Schema.Reader}} */ ({}) - /** @type {[keyof U & string, T|Schema.Reader][]} */ - const entries = Object.entries(fields) - - for (const [key, field] of entries) { - switch (typeof field) { - case 'number': - case 'string': - case 'boolean': - shape[key] = literal(field) - break - case 'object': - shape[key] = field === null ? literal(null) : field - break - default: - throw new Error( - `Invalid struct field "${key}", expected schema or literal, instead got ${typeof field}` - ) - } - } - - return new Struct(/** @type {V} */ (shape)) -} - -/** - * @param {string} message - * @returns {Schema.Error} - */ -export const error = message => new SchemaError(message) - -class SchemaError extends Error { - get name() { - return 'SchemaError' - } - /** @type {true} */ - get error() { - return true - } - /* c8 ignore next 3 */ - describe() { - return this.name - } - get message() { - return this.describe() - } - - toJSON() { - const { error, name, message, stack } = this - return { error, name, message, stack } - } -} - -class TypeError extends SchemaError { - /** - * @param {{expect:string, actual:unknown}} data - */ - constructor({ expect, actual }) { - super() - this.expect = expect - this.actual = actual - } - get name() { - return 'TypeError' - } - describe() { - return `Expected value of type ${this.expect} instead got ${displayTypeName( - this.actual - )}` - } -} - -/** - * @param {object} data - * @param {string} data.expect - * @param {unknown} data.actual - * @returns {Schema.Error} - */ -export const typeError = data => new TypeError(data) - -/** - * - * @param {unknown} value - */ -const displayTypeName = value => { - const type = typeof value - switch (type) { - case 'boolean': - case 'string': - return JSON.stringify(value) - // if these types we do not want JSON.stringify as it may mess things up - // eg turn NaN and Infinity to null - case 'bigint': - return `${value}n` - case 'number': - case 'symbol': - case 'undefined': - return String(value) - case 'object': - return value === null ? 'null' : Array.isArray(value) ? 'array' : 'object' - default: - return type - } -} - -class LiteralError extends SchemaError { - /** - * @param {{ - * expect:string|number|boolean|null - * actual:unknown - * }} data - */ - constructor({ expect, actual }) { - super() - this.expect = expect - this.actual = actual - } - get name() { - return 'LiteralError' - } - describe() { - return `Expected literal ${displayTypeName( - this.expect - )} instead got ${displayTypeName(this.actual)}` - } -} - -class ElementError extends SchemaError { - /** - * @param {{at:number, cause:Schema.Error}} data - */ - constructor({ at, cause }) { - super() - this.at = at - this.cause = cause - } - get name() { - return 'ElementError' - } - describe() { - return [ - `Array contains invalid element at ${this.at}:`, - li(this.cause.message), - ].join('\n') - } -} - -class FieldError extends SchemaError { - /** - * @param {{at:string, cause:Schema.Error}} data - */ - constructor({ at, cause }) { - super() - this.at = at - this.cause = cause - } - get name() { - return 'FieldError' - } - describe() { - return [ - `Object contains invalid field "${this.at}":`, - li(this.cause.message), - ].join('\n') - } -} - -/** - * @param {object} options - * @param {string|number} options.at - * @param {Schema.Error} options.cause - * @returns {Schema.Error} - */ -export const memberError = ({ at, cause }) => - typeof at === 'string' - ? new FieldError({ at, cause }) - : new ElementError({ at, cause }) - -class UnionError extends SchemaError { - /** - * @param {{causes: Schema.Error[]}} data - */ - constructor({ causes }) { - super() - this.causes = causes - } - get name() { - return 'UnionError' - } - describe() { - const { causes } = this - return [ - `Value does not match any type of the union:`, - ...causes.map(cause => li(cause.message)), - ].join('\n') - } -} - -class IntersectionError extends SchemaError { - /** - * @param {{causes: Schema.Error[]}} data - */ - constructor({ causes }) { - super() - this.causes = causes - } - get name() { - return 'IntersectionError' - } - describe() { - const { causes } = this - return [ - `Value does not match following types of the intersection:`, - ...causes.map(cause => li(cause.message)), - ].join('\n') - } -} - -/** - * @param {string} message - */ -const indent = (message, indent = ' ') => - `${indent}${message.split('\n').join(`\n${indent}`)}` - -/** - * @param {string} message - */ -const li = message => indent(`- ${message}`) diff --git a/packages/validator/src/schema/text.js b/packages/validator/src/schema/text.js deleted file mode 100644 index 546950bb..00000000 --- a/packages/validator/src/schema/text.js +++ /dev/null @@ -1,34 +0,0 @@ -import * as Schema from './schema.js' - -const schema = Schema.string() - -export const text = () => schema - -/** - * @param {{pattern: RegExp}} options - */ -export const match = ({ pattern }) => schema.refine(new Match(pattern)) - -/** - * @param {unknown} input - */ -export const read = input => schema.read(input) - -/** - * @extends {Schema.API} - */ -class Match extends Schema.API { - /** - * @param {string} source - * @param {RegExp} pattern - */ - readWith(source, pattern) { - if (!pattern.test(source)) { - return Schema.error( - `Expected to match ${pattern} but got "${source}" instead` - ) - } else { - return source - } - } -} diff --git a/packages/validator/src/schema/type.js b/packages/validator/src/schema/type.js deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/validator/src/schema/type.ts b/packages/validator/src/schema/type.ts deleted file mode 100644 index 0bce1e55..00000000 --- a/packages/validator/src/schema/type.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { Failure as Error, Result, Phantom } from '@ucanto/interface' - -export interface Reader< - O = unknown, - I = unknown, - X extends { error: true } = Error -> { - read(input: I): Result -} - -export type { Error } - -export type ReadResult = Result - -export interface Schema< - O extends unknown = unknown, - I extends unknown = unknown -> extends Reader { - optional(): Schema - nullable(): Schema - array(): Schema - default(value: NotUndefined): DefaultSchema, I> - or(other: Reader): Schema - and(other: Reader): Schema - refine(schema: Reader): Schema - - brand(kind?: K): Schema, I> - - is(value: unknown): value is O - from(value: I): O -} - -export interface DefaultSchema< - O extends unknown = unknown, - I extends unknown = unknown -> extends Schema { - readonly value: O & NotUndefined - optional(): DefaultSchema, I> -} - -export type NotUndefined = Exclude - -export interface ArraySchema extends Schema { - element: Reader -} - -export interface LiteralSchema< - T extends string | number | boolean | null, - I = unknown -> extends Schema { - default(value?: T): DefaultSchema, I> - readonly value: T -} - -export interface NumberSchema< - N extends number = number, - I extends unknown = unknown -> extends Schema { - greaterThan(n: number): NumberSchema - lessThan(n: number): NumberSchema - - refine(schema: Reader): NumberSchema -} - -export interface StructSchema< - U extends { [key: string]: Reader } = {}, - I extends unknown = unknown -> extends Schema, I> { - shape: U - - create(input: MarkEmptyOptional>): InferStruct - extend( - extension: E - ): StructSchema -} - -export interface StringSchema - extends Schema { - startsWith( - prefix: Prefix - ): StringSchema - endsWith( - suffix: Suffix - ): StringSchema - refine(schema: Reader): StringSchema -} - -declare const Marker: unique symbol -export type Branded = T & { - [Marker]: T -} - -export type Integer = number & Phantom<{ typeof: 'integer' }> -export type Float = number & Phantom<{ typeof: 'float' }> - -export type Infer = T extends Reader ? T : never - -export type InferIntesection = { - [K in keyof U]: (input: Infer) => void -}[number] extends (input: infer T) => void - ? T - : never - -export type InferUnion = Infer - -export type InferTuple = { - [K in keyof U]: Infer -} - -export type InferStruct = MarkOptionals<{ - [K in keyof U]: Infer -}> - -export type InferStructSource = - // MarkEmptyOptional< - MarkOptionals<{ - [K in keyof U]: InferSource - }> -// > - -export type InferSource = U extends DefaultSchema - ? T | undefined - : U extends StructSchema - ? InferStructSource - : U extends Reader - ? T - : never - -export type MarkEmptyOptional = RequiredKeys extends never - ? T | void - : T - -type MarkOptionals = Pick> & - Partial>> - -type RequiredKeys = { - [k in keyof T]: undefined extends T[k] ? never : k -}[keyof T] & {} - -type OptionalKeys = { - [k in keyof T]: undefined extends T[k] ? k : never -}[keyof T] & {} diff --git a/packages/validator/src/schema/uri.js b/packages/validator/src/schema/uri.js deleted file mode 100644 index 64796640..00000000 --- a/packages/validator/src/schema/uri.js +++ /dev/null @@ -1,64 +0,0 @@ -import * as API from '@ucanto/interface' -import * as Schema from './schema.js' - -/** - * @template {API.Protocol} [P=API.Protocol] - * @typedef {{protocol: P}} Options - */ - -/** - * @template {Options} O - * @extends {Schema.API, unknown, Partial>} - */ -class URISchema extends Schema.API { - /** - * @param {unknown} input - * @param {Partial} options - * @returns {Schema.ReadResult>} - */ - readWith(input, { protocol } = {}) { - if (typeof input !== 'string' && !(input instanceof URL)) { - return Schema.error( - `Expected URI but got ${input === null ? 'null' : typeof input}` - ) - } - - try { - const url = new URL(String(input)) - if (protocol != null && url.protocol !== protocol) { - return Schema.error(`Expected ${protocol} URI instead got ${url.href}`) - } else { - return /** @type {API.URI} */ (url.href) - } - } catch (_) { - return Schema.error(`Invalid URI`) - } - } -} - -const schema = new URISchema({}) - -/** - * @returns {Schema.Schema} - */ -export const uri = () => schema - -/** - * @param {unknown} input - */ -export const read = input => schema.read(input) - -/** - * @template {API.Protocol} P - * @template {Options

} O - * @param {O} options - * @returns {Schema.Schema, unknown>} - */ -export const match = options => new URISchema(options) - -/** - * @template {string} [Scheme=string] - * @param {`${Scheme}:${string}`} input - */ -export const from = input => - /** @type {API.URI<`${Scheme}:`>} */ (schema.from(input)) diff --git a/packages/validator/src/util.js b/packages/validator/src/util.js deleted file mode 100644 index efc740b4..00000000 --- a/packages/validator/src/util.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @template {string|boolean|number|[unknown, ...unknown[]]} T - * @param {T} value - * @returns {T} - */ -export const the = value => value - -/** - * @template {{}} O - * @param {O} object - * @returns {({ [K in keyof O]: [K, O[K]][] }[keyof O])|[[never, never]]} - */ - -export const entries = object => /** @type {any} */ (Object.entries(object)) - -/** - * @template T - * @param {T[][]} dataset - * @returns {T[][]} - */ -export const combine = ([first, ...rest]) => { - const results = first.map(value => [value]) - for (const values of rest) { - const tuples = results.splice(0) - for (const value of values) { - for (const tuple of tuples) { - results.push([...tuple, value]) - } - } - } - return results -} - -/** - * @template T - * @param {T[]} left - * @param {T[]} right - * @returns {T[]} - */ -export const intersection = (left, right) => { - const [result, other] = - left.length < right.length - ? [new Set(left), new Set(right)] - : [new Set(right), new Set(left)] - - for (const item of result) { - if (!other.has(item)) { - result.delete(item) - } - } - - return [...result] -} diff --git a/packages/validator/test/fixtures.js b/packages/validator/test/fixtures.js deleted file mode 100644 index 678e9516..00000000 --- a/packages/validator/test/fixtures.js +++ /dev/null @@ -1,18 +0,0 @@ -import * as ed25519 from '@ucanto/principal/ed25519' - -/** did:key:z6Mkqa4oY9Z5Pf5tUcjLHLUsDjKwMC95HGXdE1j22jkbhz6r */ -export const alice = ed25519.parse( - 'MgCZT5vOnYZoVAeyjnzuJIVY9J4LNtJ+f8Js0cTPuKUpFne0BVEDJjEu6quFIU8yp91/TY/+MYK8GvlKoTDnqOCovCVM=' -) -/** did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob */ -export const bob = ed25519.parse( - 'MgCYbj5AJfVvdrjkjNCxB3iAUwx7RQHVQ7H1sKyHy46Iose0BEevXgL1V73PD9snOCIoONgb+yQ9sycYchQC8kygR4qY=' -) -/** did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL */ -export const mallory = ed25519.parse( - 'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=' -) - -export const service = ed25519.parse( - 'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=' -) diff --git a/packages/validator/test/test.js b/packages/validator/test/test.js deleted file mode 100644 index dbe6d8ec..00000000 --- a/packages/validator/test/test.js +++ /dev/null @@ -1,6 +0,0 @@ -import { assert, use } from 'chai' -import subset from 'chai-subset' -use(subset) - -export const test = it -export { assert } diff --git a/packages/validator/test/util.js b/packages/validator/test/util.js deleted file mode 100644 index b7874a71..00000000 --- a/packages/validator/test/util.js +++ /dev/null @@ -1,60 +0,0 @@ -import { Failure } from '../src/error.js' -import * as API from '@ucanto/interface' - -/** - * @param {API.Failure|true} value - */ -export const fail = value => (value === true ? undefined : value) - -/** - * Check URI can be delegated - * - * @param {string|undefined} child - * @param {string|undefined} parent - */ -export function canDelegateURI(child, parent) { - if (parent === undefined) { - return true - } - - if (child !== undefined && parent.endsWith('*')) { - return child.startsWith(parent.slice(0, -1)) - ? true - : new Failure(`${child} does not match ${parent}`) - } - - return child === parent - ? true - : new Failure(`${child} is different from ${parent}`) -} - -/** - * @param {API.Link|undefined} child - * @param {API.Link|undefined} parent - */ -export const canDelegateLink = (child, parent) => { - // if parent poses no restriction it's can be derived - if (parent === undefined) { - return true - } - - return String(child) === parent.toString() - ? true - : new Failure(`${child} is different from ${parent}`) -} - -/** - * Checks that `with` on claimed capability is the same as `with` - * in delegated capability. Note this will ignore `can` field. - * - * @param {API.ParsedCapability} child - * @param {API.ParsedCapability} parent - */ -export function equalWith(child, parent) { - return ( - child.with === parent.with || - new Failure( - `Can not derive ${child.can} with ${child.with} from ${parent.with}` - ) - ) -} diff --git a/packages/validator/tsconfig.json b/packages/validator/tsconfig.json deleted file mode 100644 index 46d91b63..00000000 --- a/packages/validator/tsconfig.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ - - /* Projects */ - "incremental": true /* Enable incremental compilation */, - "composite": true /* Enable constraints that allow a TypeScript project to be used with project references. */, - // "tsBuildInfoFile": "./dist", /* Specify the folder for .tsbuildinfo incremental compilation files. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ - // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - - /* Modules */ - "module": "ES2020" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, - "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */, - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "resolveJsonModule": true, /* Enable importing .json files */ - // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, - "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - "declarationMap": true /* Create sourcemaps for d.ts files. */, - "emitDeclarationOnly": true /* Only output d.ts files and not JavaScript files. */, - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist/" /* Specify an output folder for all emitted files. */, - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ - // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ - // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, - "include": ["src", "test"], - "references": [ - { "path": "../interface" }, - { "path": "../client" }, - { "path": "../principal" }, - { "path": "../core" } - ] -} From be80aa55a8e8c97b62055e215ed779d6be0f3671 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 25 Jan 2023 00:12:14 -0800 Subject: [PATCH 12/17] fix: regression --- packages/core/src/error.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/error.js b/packages/core/src/error.js index 1cadba69..64b8a378 100644 --- a/packages/core/src/error.js +++ b/packages/core/src/error.js @@ -34,6 +34,7 @@ export class EscalatedCapability extends Failure { this.claimed = claimed this.delegated = delegated this.cause = cause + /** @type {'EscalatedCapability'} */ this.name = 'EscalatedCapability' } describe() { From 5f165fd15f57ee0344cdafb3cb9569cdfa6ec431 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 25 Jan 2023 00:12:21 -0800 Subject: [PATCH 13/17] chore: add missing tests --- packages/core/src/protocol/protocol.js | 17 ++++--- packages/core/src/protocol/type.ts | 10 +++-- packages/core/test/capabilities/access.js | 47 ++++++++++++++++++++ packages/core/test/protocol.spec.js | 54 +++++++++++++++++++++++ 4 files changed, 118 insertions(+), 10 deletions(-) create mode 100644 packages/core/test/capabilities/access.js diff --git a/packages/core/src/protocol/protocol.js b/packages/core/src/protocol/protocol.js index 45d95e68..aff6fdd3 100644 --- a/packages/core/src/protocol/protocol.js +++ b/packages/core/src/protocol/protocol.js @@ -17,19 +17,20 @@ const build = tasks => { for (const task of tasks) { const path = task.can.split('/') - const name = path.pop() - if (!name) { + if (path.length < 2) { throw new RangeError( `Expected task that has a valid 'can' field instead got '${task.can}'` ) } + const name = /** @type {string} */ (path.pop()) + const key = name === '*' ? '_' : name const namespace = buildNamespace(abilities, path) - if (namespace[name] && namespace[name] !== task) { + if (namespace[key] && namespace[key] !== task) { throw new RangeError( `All tasks must have unique 'can' fields, but multiple tasks with "can: '${task.can}'" had been provided` ) } - namespace[name] = task + namespace[key] = task } return abilities @@ -44,10 +45,12 @@ const buildNamespace = (source, path) => { /** @type {Record} */ let target = source for (const name of path) { - if (target[name] == null) { - target[name] = {} + if (name !== '.') { + if (target[name] == null) { + target[name] = {} + } + target = /** @type {Record} */ (target[name]) } - target = /** @type {Record} */ (target[name]) } return target } diff --git a/packages/core/src/protocol/type.ts b/packages/core/src/protocol/type.ts index f0188667..63e8628a 100644 --- a/packages/core/src/protocol/type.ts +++ b/packages/core/src/protocol/type.ts @@ -27,6 +27,10 @@ type InferAbility = T extends Task type InferNamespacedAbility< Path extends string, T extends Task -> = Path extends `${infer Key}/${infer Rest}` - ? { [K in Key]: InferNamespacedAbility } - : { [K in Path]: T } +> = Path extends `./${infer Rest}` + ? InferNamespacedAbility + : Path extends `${infer Key}/${infer Rest}` + ? { [K in EscapeKey]: InferNamespacedAbility } + : { [K in EscapeKey]: T } + +type EscapeKey = Key extends '*' ? '_' : Key diff --git a/packages/core/test/capabilities/access.js b/packages/core/test/capabilities/access.js new file mode 100644 index 00000000..a84e74ce --- /dev/null +++ b/packages/core/test/capabilities/access.js @@ -0,0 +1,47 @@ +import { _ } from './any.js' +import { capability, Schema } from '../../src/lib.js' +import { equalWith } from './util.js' + +/** + * Capability can only be delegated (but not invoked) allowing audience to + * derived any `access/` prefixed capability for the agent identified + * by did:key in the `with` field. + */ +export const access = _.derive({ + to: capability({ + can: 'access/*', + with: Schema.DID.match({ method: 'web' }), + }), + derives: equalWith, +}) + +/** + * Issued by trusted authority (usually the one handling invocation that contains this proof) + * to the account (aud) to update invocation local state of the document. + * + * @see https://github.com/web3-storage/specs/blob/main/w3-account.md#update + * + * @example + * ```js + * { + iss: "did:web:web3.storage", + aud: "did:mailto:alice@web.mail", + att: [{ + with: "did:web:web3.storage", + can: "./update", + nb: { key: "did:key:zAgent" } + }], + exp: null + sig: "..." + } + * ``` + */ +export const session = capability({ + can: './update', + // Should be web3.storage DID + with: Schema.DID.match({ method: 'web' }), + nb: { + // Agent DID so it can sign UCANs as did:mailto if it matches this delegation `aud` + key: Schema.DID.match({ method: 'key' }), + }, +}) diff --git a/packages/core/test/protocol.spec.js b/packages/core/test/protocol.spec.js index de08cd3e..e6fb4d2f 100644 --- a/packages/core/test/protocol.spec.js +++ b/packages/core/test/protocol.spec.js @@ -3,6 +3,8 @@ import { capability, Failure, Schema, protocol } from '../src/lib.js' import * as API from '@ucanto/interface' import * as Store from './capabilities/store.js' import * as Upload from './capabilities/upload.js' +import * as Access from './capabilities/access.js' +import { access } from '../src/validator.js' /** * @template T @@ -73,6 +75,13 @@ const upload = { }), } +const ucanto = { + update: Schema.task({ + in: Access.session, + ok: Schema.struct({}), + }), +} + test('task api', () => { assert.deepInclude(store.add, { can: 'store/add', @@ -105,3 +114,48 @@ test('protocol derives capabilities', () => { assert.deepEqual(w3.abilities, { store, upload }) }) + +test('protocol handles ./update', () => { + const w3 = protocol([store.add, store.list, store.remove, ucanto.update]) + + assert.deepEqual(w3.abilities, { store, ...ucanto }) +}) + +test('conflicting capabilities', () => { + assert.throws( + () => + protocol([ + store.add, + store.list, + Schema.task({ in: Store.add, ok: Schema.struct({}) }), + ]), + /multiple tasks with "can: 'store\/add'/ + ) +}) + +test('requires namespace', () => { + assert.throws( + () => + protocol([ + Schema.task({ + in: capability({ + // @ts-expect-error + can: 'boom', + with: Schema.DID, + nb: {}, + }), + ok: Schema.struct({}), + }), + ]), + /valid 'can' field instead got 'boom'/ + ) +}) + +test('maps * to _', () => { + const any = Schema.task({ in: Store._, ok: Store._ }) + const w3 = protocol([store.add, store.list, store.remove, any]) + + assert.deepEqual(w3.abilities, { + store: { ...store, _: any }, + }) +}) From 2c86aeeb81c0f969d7482cb0e755e703bf70a8a6 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 25 Jan 2023 00:17:12 -0800 Subject: [PATCH 14/17] chore: remove agent code --- package.json | 1 - packages/agent/package.json | 64 ----- packages/agent/src/agent.js | 249 ----------------- packages/agent/src/api.js | 0 packages/agent/src/api.ts | 431 ------------------------------ packages/agent/test/basic.spec.js | 69 ----- packages/agent/test/infer.spec.js | 250 ----------------- packages/agent/test/test.js | 6 - packages/agent/tsconfig.json | 108 -------- 9 files changed, 1178 deletions(-) delete mode 100644 packages/agent/package.json delete mode 100644 packages/agent/src/agent.js delete mode 100644 packages/agent/src/api.js delete mode 100644 packages/agent/src/api.ts delete mode 100644 packages/agent/test/basic.spec.js delete mode 100644 packages/agent/test/infer.spec.js delete mode 100644 packages/agent/test/test.js delete mode 100644 packages/agent/tsconfig.json diff --git a/package.json b/package.json index bae5c9c1..7a02c865 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "private": true, "type": "module", "workspaces": [ - "packages/agent", "packages/interface", "packages/core", "packages/client", diff --git a/packages/agent/package.json b/packages/agent/package.json deleted file mode 100644 index 07c8a27b..00000000 --- a/packages/agent/package.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "name": "@ucanto/agent", - "description": "UCAN Agent", - "version": "4.0.2", - "types": "./dist/src/lib.d.ts", - "main": "./src/lib.js", - "keywords": [ - "UCAN", - "RPC", - "JWT", - "server" - ], - "files": [ - "src", - "dist/src" - ], - "repository": { - "type": "git", - "url": "https://github.com/web3-storage/ucanto.git" - }, - "homepage": "https://github.com/web3-storage/ucanto", - "scripts": { - "test:web": "playwright-test test/**/*.spec.js --cov && nyc report", - "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha test/**/*.spec.js", - "test": "npm run test:node", - "coverage": "c8 --reporter=html mocha test/**/*.spec.js && npm_config_yes=true npx st -d coverage -p 8080", - "check": "tsc --build", - "build": "tsc --build" - }, - "dependencies": { - "@ucanto/core": "^4.0.2", - "@ucanto/interface": "^4.0.2" - }, - "devDependencies": { - "@types/chai": "^4.3.3", - "@types/chai-subset": "^1.3.3", - "@types/mocha": "^9.1.0", - "@ucanto/principal": "^4.0.2", - "@ucanto/client": "^4.0.2", - "@ucanto/transport": "^4.0.2", - "@web-std/fetch": "^4.1.0", - "@web-std/file": "^3.0.2", - "c8": "^7.11.0", - "chai": "^4.3.6", - "chai-subset": "^1.6.0", - "mocha": "^10.1.0", - "multiformats": "^10.0.2", - "nyc": "^15.1.0", - "playwright-test": "^8.1.1", - "typescript": "^4.9.4" - }, - "exports": { - ".": { - "types": "./dist/src/lib.d.ts", - "import": "./src/lib.js" - }, - "./server": { - "types": "./dist/src/server.d.ts", - "import": "./src/server.js" - } - }, - "type": "module", - "license": "(Apache-2.0 AND MIT)" -} diff --git a/packages/agent/src/agent.js b/packages/agent/src/agent.js deleted file mode 100644 index 178e8dd1..00000000 --- a/packages/agent/src/agent.js +++ /dev/null @@ -1,249 +0,0 @@ -import * as API from '../src/api.js' -import { Schema } from '@ucanto/core' - -export const fail = Schema.struct({ error: true }) - -/** - * Creates a schema for the task result by specifying ok and error types - * - * @template T - * @template {{}} [X={message:string}] - * @param {Schema.Reader} ok - * @param {Schema.Reader} error - * @returns {Schema.Schema>} - */ -export const result = ( - ok, - error = Schema.struct({ message: Schema.string() }) -) => - Schema.or( - /** @type {Schema.Reader} */ (ok), - fail.and(error) - ) - -/** - * @template {API.CreateTask} Create - * @param {Create} options - * @returns {API.Task ? T : void, Schema.Infer & { error?: never}, Schema.Infer & { error: true}>} - */ -export const task = options => - /** @type {any} */ ({ - in: options.in, - out: options.out, - }) - -/** - * @template {API.URI} URI - * @template {API.ResourceAbilities} Abilities - * @param {API.Reader} resource - * @param {Abilities} abilities - * @returns {API.Resource} - */ -export const resource = (resource, abilities) => { - return new Resource({ resource, abilities, context: {} }) -} - -/** - * @template {API.URI} URI - * @template {API.ResourceAbilities} Abilities - * @template {{}} Context - */ - -class Resource { - /** - * @type {{new (state:T): API.From }|undefined} - */ - #API - - /** - * @param {object} source - * @param {API.Reader} source.resource - * @param {Abilities} source.abilities - * @param {Context} source.context - */ - constructor(source) { - this.source = source - } - get abilities() { - return this.source.abilities - } - - /** - * @template {URI} At - * @param {At} at - * @returns {API.From}, - */ - from(at) { - const uri = this.source.resource.read(at) - if (uri.error) { - throw uri - } - - if (!this.#API) { - this.#API = gen('', this.source.abilities) - } - - return new this.#API({ uri: /** @type {At} */ (uri) }) - } - /** - * @template Q - * @param {Q} query - * @returns {API.Batch} - */ - query(query) { - throw 'query' - } - /** - * @template ContextExt - * @param {ContextExt} context - * @returns {Resource} - */ - with(context) { - return new Resource({ - ...this.source, - context: { ...this.source.context, ...context }, - }) - } - - /** - * @template {API.URI} URIExt - * @template {API.ResourceAbilities} AbilitiesExt - * @template {{}} ContextExt - * @param {Resource} other - * @returns {Resource} - */ - and(other) { - return new Resource({ - resource: Schema.or(this.source.resource, other.source.resource), - context: { ...this.source.context, ...other.source.context }, - // we need to actually merge these - abilities: { ...this.source.abilities, ...other.source.abilities }, - }) - } - - // /** - // * @template {API.ProviderOf} Provider - // * @param {Provider} provider - // */ - // provide(provider) { - - // } -} - -/** - * @template {API.ResourceAbilities} Abilities - * @param {string} at - * @param {Abilities} abilities - * @returns {{new (state:T): API.From }} - */ - -const gen = (at, abilities) => { - /** - * @template {{ uri: API.URI }} State - */ - class ResourceAPI { - /** - * @param {State} state - */ - constructor(state) { - this.state = state - } - } - - /** @type {PropertyDescriptorMap} */ - const descriptor = {} - - for (const [key, source] of Object.entries(abilities)) { - const path = `${at}/${key}` - if (isReader(source)) { - descriptor[key] = { - value: function () { - return new Selector(this.state, { path, schema: source }) - // Object.defineProperty(this, key, { value: selector }) - // return selector - }, - } - } else if (isTask(source)) { - descriptor[key] = { - value: function (input) { - return new TaskSelector({ - with: this.state.uri, - path, - schema: source, - input, - in: source.in.read(input), - }) - // Object.defineProperty(this, key, { value: selector }) - // return selector - }, - } - } else { - const SubAPI = gen(path, source) - descriptor[key] = { - get: function () { - const selector = new SubAPI(this.state) - Object.defineProperty(this, key, { value: selector }) - return selector - }, - } - } - } - - Object.defineProperties(ResourceAPI.prototype, descriptor) - - return /** @type {any} */ (ResourceAPI) -} - -class Selector { - /** - * @param {{ uri: API.URI }} state - */ - constructor(state, { path, source }) { - this.path = path - this.source = source - this.state = state - } -} - -class TaskSelector { - /** - * @param {object} state - */ - constructor(source) { - this.source = source - } - - get with() { - return this.source.with - } - get can() { - return this.source.path.slice(1) - } - get in() { - return this.source.in - } -} - -/** - * @template {API.Reader} Reader - * @param {Reader|unknown} value - * @returns {value is Reader} - */ -const isReader = value => - value != null && - typeof value === 'object' && - 'read' in value && - typeof value.read === 'function' - -/** - * @template {API.Task} Task - * @param {Task|unknown} value - * @returns {value is Task} - */ -const isTask = value => - value != null && - typeof value === 'object' && - 'in' in value && - isReader(value.in) && - 'out' in value && - isReader(value.out) diff --git a/packages/agent/src/api.js b/packages/agent/src/api.js deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/agent/src/api.ts b/packages/agent/src/api.ts deleted file mode 100644 index dea3008d..00000000 --- a/packages/agent/src/api.ts +++ /dev/null @@ -1,431 +0,0 @@ -import * as API from '@ucanto/interface' -export * from '@ucanto/interface' -import { - UCAN, - DID, - Link, - Signer, - Delegation, - CapabilityParser, - TheCapabilityParser, - Capability, - InferCaveats, - Match, - Await, - ParsedCapability, - Ability as Can, - InvocationError, - Failure, - Result, - URI, - Caveats, - Verifier, - Reader, - Invocation, - Proof, - IssuedInvocationView, -} from '@ucanto/interface' - -import { Schema } from '@ucanto/core' - -// This is the interface of the module we'll have -export interface AgentModule { - create(options: CreateAgent): Agent - - resource( - resource: Reader, - abilities: Abilities - ): Resource> // & ResourceCapabilities -} - -export interface Task< - In extends unknown = unknown, - Out extends unknown = unknown, - Fail extends { error: true } = { error: true }, - With extends URI = URI -> { - uri: With - - in: Reader - - ok: Reader - error: Reader - out: Reader> -} - -export type CreateTask< - In = unknown, - Out = unknown, - Fail extends { error: true } = { error: true } -> = TaskWithInput | TaskWithoutInput - -export interface TaskWithInput { - in: Reader - out: Reader> -} - -export interface TaskWithoutInput { - in?: undefined - out: Reader> -} - -export interface CapabilitySchema< - URI extends API.URI = API.URI, - Ability extends API.Ability = API.Ability, - Input extends { [key: string]: Reader } = {} -> extends Schema.StructSchema<{ - can: Reader - with: Reader - nb: Schema.StructSchema - }> {} - -export interface Resource< - ID extends URI = URI, - Abilities extends ResourceAbilities = ResourceAbilities, - Context extends {} = {} -> { - // id: ID - // abilities: Abilities - - capabilities: ResourceCapabilities - from(at: At): From - query(query: Q): Batch - - with(context: CTX): Resource - - // provide

>( - // provider: P - // ): Provider - - and< - ID2 extends URI, - Abilities2 extends ResourceAbilities, - Context2 extends {} - >( - other: Resource - ): Resource -} - -export type ResourceCapabilities< - At extends URI, - NS extends string, - Abilities extends ResourceAbilities -> = { - [K in keyof Abilities & string]: Abilities[K] extends Reader< - Result - > - ? ResourceCapability, In> - : Abilities[K] extends Task - ? ResourceCapability, In> - : Abilities[K] extends ResourceAbilities - ? ResourceCapabilities, Abilities[K]> - : Abilities[K] -} - -type ToCan = NS extends '' - ? T - : `${NS extends '_' ? '*' : NS}/${T extends '_' ? '*' : T}` - -export interface ResourceCapability< - At extends URI, - Can extends API.Ability, - Input extends unknown -> extends ResourceSchema { - can: Can - - delegate>( - uri: At, - input?: In - ): Promise]>> - - invoke( - uri: At, - input: In - ): IssuedInvocationView> -} - -export interface ResourceSchema< - At extends URI, - Can extends API.Ability, - Input extends unknown -> extends Schema.StructSchema<{ - with: Schema.Reader - can: Schema.Reader - input: Schema.Reader - }> {} - -export interface Provider< - ID extends URI = URI, - Abilities extends ResourceAbilities = ResourceAbilities, - Context extends {} = {} -> { - uri: ID - abilities: Abilities - context: Context -} - -export type ProviderOf< - Abilities extends ResourceAbilities = ResourceAbilities, - Context extends {} = {} -> = { - [K in keyof Abilities & string]: Abilities[K] extends Reader< - Result - > & { uri: infer ID } - ? (uri: ID, context: Context) => Await> - : Abilities[K] extends Task - ? (uri: URI, input: In, context: Context) => Await> - : Abilities[K] extends ResourceAbilities - ? ProviderOf - : never -} - -type With< - ID extends URI, - Abilities extends ResourceAbilities = ResourceAbilities -> = { - [K in keyof Abilities & string]: Abilities[K] extends Reader< - Result - > - ? Reader> & { uri: ID } - : Abilities[K] extends Task - ? Task - : Abilities[K] extends ResourceAbilities - ? With - : never -} - -type Query = - | { - [K: PropertyKey]: - | Selector - | Selection - } - | [ - Selector, - ...Selector[] - ] - -export interface Batch { - query: Q - - decode(): { - [K in keyof Q]: Q[K] extends Selector< - infer With, - infer Can, - infer In, - infer Out, - infer Fail - > - ? Result - : Q[K] extends Selection< - infer With, - infer Can, - infer In, - infer Out, - infer Fail, - infer Query - > - ? Result - : never - } -} - -export type From< - At extends URI, - Can extends string, - Abilities extends ResourceAbilities -> = { - [K in keyof Abilities & string]: Abilities[K] extends Reader< - Result - > - ? () => Selector - : Abilities[K] extends Task - ? (input: Input) => Selector - : Abilities[K] extends ResourceAbilities - ? From - : never -} - -export type Input = - | T - | Selector - | Selection - -/** - * Resource abilities is defined as a trees structure where leaves are query - * sources and paths leading to them define ability of the capability. - */ -export type ResourceAbilities = { - [K: string]: Source | ResourceAbilities -} - -export type Source = - // If query source takes no input it is defined as a reader - | Reader - // If query source takes an input and returns output it is defined - // as ability - | Task - -export interface Selector< - At extends URI, - Can extends API.Ability, - In extends unknown, - Out extends unknown, - Fail extends { error: true } -> { - with: At - can: Can - in: In - - encode(): Uint8Array - - decode(bytes: Uint8Array): Result - select>( - query: Q - ): Selection, Fail, Q> - - invoke(options: InvokeOptions): IssuedInvocationView<{ - with: At - can: Can - nb: In - }> -} - -export interface InvokeOptions { - issuer: Signer - audience: API.Principal - - lifetimeInSeconds?: number - expiration?: UCAN.UTCUnixTimestamp - notBefore?: UCAN.UTCUnixTimestamp - - nonce?: UCAN.Nonce - - facts?: UCAN.Fact[] - proofs?: Proof[] -} - -export interface Selection< - At extends URI, - Can extends API.Ability, - In extends unknown, - Out extends unknown, - Fail extends { error: true }, - Query -> extends Selector { - query: Query - - embed(): Promise<{ - link: Link<{ with: At; can: Can; in: In; query: Query }> - }> -} - -export type Select> = { - [K in keyof Query & keyof Out]: Query[K] extends true - ? Out[K] - : Query[K] extends object - ? Select - : { debug: Query[K] } -} - -type QueryFor = Partial<{ - [K in keyof Out]: true | QueryFor -}> - -export interface CreateAgent { - /** - * Signer will be used to sign all the invocation receipts and to check - * principal alignment on incoming delegations. - */ - signer: Signer - - authority?: API.Principal - /** - * Agent may act on behalf of some other authority e.g. in case of - * web3.storage we'd like to `root -> manager -> worker` chain of - * command if this agents acts as `worker` it will need a delegation - * chain it could use when signing receipts. - * - * @see https://github.com/web3-storage/w3protocol/issues/265 - */ - delegations?: Delegation[] -} - -export interface AgentConnect< - ID extends DID, - Abilities extends ResourceAbilities = {} -> { - principal: API.Principal - - delegations?: Delegation[] - - capabilities?: Resource -} - -/** - * @template ID - DID this agent has - * @template Context - Any additional context agent will hold - */ -export interface Agent< - ID extends DID = DID, - Context extends {} = {}, - Abilities extends ResourceAbilities = ResourceAbilities -> { - signer: Signer - context: Context - - connect( - options: AgentConnect - ): AgentConnection - - /** - * Attaches some context to the agent. - */ - with(context: Ext): Agent - /** - * Initialized agent with a given function which will extend the context with - * a result. - */ - init(start: () => API.Await): Agent - - provide( - capabilities: Resource, - provider: ProviderOf - ): Agent> - - resource(uri: At): From - - query: Resource['query'] -} - -export type QueryEndpoint< - Capabilities extends [CapabilityParser, ...CapabilityParser[]] -> = Capabilities extends [CapabilityParser] ? never : never - -export interface AgentConnection< - ID extends DID, - Abilities extends ResourceAbilities -> { - did(): ID - - resource(uri: At): From - - query: Resource['query'] -} - -export interface HandlerInput { - capability: T - context: Context - - invocation: API.ServiceInvocation - agent: Agent -} - -type Unit = Partial<{ [key: string]: never }> -export type Outcome = KeyedUnion<{ ok: T } | { error: X }> - -type Empty = { - [K in keyof T]?: never -} - -type KeyedUnion = Empty & T diff --git a/packages/agent/test/basic.spec.js b/packages/agent/test/basic.spec.js deleted file mode 100644 index d204c982..00000000 --- a/packages/agent/test/basic.spec.js +++ /dev/null @@ -1,69 +0,0 @@ -import * as API from '../src/api.js' -import { DID as Principal } from '@ucanto/core' -import { capability, Schema } from '@ucanto/core' -import { ed25519 } from '@ucanto/principal' -import { CAR } from '@ucanto/transport' -import { result, task } from '../src/agent.js' -import * as Agent from '../src/agent.js' -import { test, assert } from './test.js' - -test('create resource', () => { - const Space = Schema.DID.match({ method: 'key' }) - const Unit = Schema.struct({}) - const Echo = Schema.struct({ - message: Schema.string(), - }) - const EchoError = Schema.struct({ - name: 'EchoError', - }) - - const Add = Schema.struct({ - size: Schema.integer(), - link: Schema.Link.link(), - origin: Schema.Link.link().optional(), - }) - - const Allocate = Schema.struct({ - size: Schema.integer(), - link: Schema.Link.link(), - - proof: Schema.enum(['store/add']).optional(), - }) - - const out = Allocate.shape.proof.read({}) - if (!out?.error) { - const out2 = { ...out } - } - - const api = Agent.resource(Space, { - debug: { - _: Unit, - echo: task({ - in: Echo, - out: result(Echo, EchoError), - }), - }, - }) - - api.capabilities.debug.echo - - assert.equal(typeof api.from, 'function') - assert.throws( - () => - // @ts-expect-error - api.from('did:web:web3.storage'), - /Expected a did:key: but got "did:web/ - ) - - const space = api.from('did:key:zAlice') - assert.equal(typeof space.debug, 'object') - assert.equal(typeof space.debug.echo, 'function') - const echo = space.debug.echo({ message: 'hello world' }) - - assert.equal(echo.can, 'debug/echo') - assert.equal(echo.with, 'did:key:zAlice') - assert.deepEqual(echo.in, { - message: 'hello world', - }) - console.log(echo) -}) diff --git a/packages/agent/test/infer.spec.js b/packages/agent/test/infer.spec.js deleted file mode 100644 index b478e473..00000000 --- a/packages/agent/test/infer.spec.js +++ /dev/null @@ -1,250 +0,0 @@ -import * as API from '../src/api.js' -import { capability, Schema, DID as Principal } from '@ucanto/core' -import { DID, URI, Text, Link } from '@ucanto/core/schema' -import { ed25519 } from '@ucanto/principal' -import { CAR } from '@ucanto/transport' -import { result, task } from '../src/agent.js' - -/** - * @param {object} input - * @param {API.AgentModule} input.Agent - */ -const testW3protocol = async ({ Agent }) => { - const Space = DID.match({ method: 'key' }) - /** - * Schema representing a link (a.k.a CID) to a CAR file. Enforces CAR codec code and CID v1. - */ - const CARLink = Link.match({ code: CAR.codec.code, version: 1 }) - - const Add = Schema.struct({ - link: CARLink, - size: Schema.integer(), - origin: Schema.Link.optional(), - }) - - const AddDone = Schema.struct({ - status: 'done', - with: Space, - link: CARLink, - }) - - // Should be a dict instead, workaround for now - // https://github.com/web3-storage/ucanto/pull/192 - const headers = Schema.tuple([Schema.string(), Schema.string()]).array() - - const AddHandOff = Schema.struct({ - status: 'upload', - with: Space, - link: CARLink, - url: Schema.URI, - headers, - }) - - const SpaceHasNoStorageProvider = Schema.struct({ - name: 'SpaceHasNoStorageProvider', - }) - const ExceedsStorageCapacity = Schema.struct({ - name: 'ExceedsStorageCapacity', - }) - const MalformedCapability = Schema.struct({ - name: 'MalformedCapability', - }) - const InvocationError = Schema.struct({ - name: 'InvocationError', - }) - - const AddError = SpaceHasNoStorageProvider.or(ExceedsStorageCapacity) - .or(MalformedCapability) - .or(InvocationError) - - const Remove = Schema.struct({ - link: Link, - }) - - const Cursor = Schema.struct({ - cursor: Schema.string().optional(), - size: Schema.integer().optional(), - }) - - /** - * @template T - * @param {API.Reader} result - */ - const list = result => - Cursor.and( - Schema.struct({ - results: Schema.array(result), - }) - ) - - const UploadRoot = Schema.struct({ - root: Schema.Link, - }) - const UploadShards = Schema.struct({ - shards: CARLink.array().optional(), - }) - - const Upload = UploadRoot.and(UploadShards) - const Unit = Schema.struct({}) - - const store = Agent.resource(Space, { - store: { - _: Unit, - add: task({ - in: Add, - out: result(AddDone.or(AddHandOff), AddError), - }), - remove: task({ - in: Remove, - out: result(Remove, MalformedCapability.or(InvocationError)), - }), - list: task({ - in: Cursor, - out: result(list(Add), InvocationError), - }), - }, - }) - - const t1 = store.from('did:key:zAlice').store.add({}).select({ - with: true, - }) - - const t2 = { ...t1.decode(new Uint8Array()) } - if (!t2.error) { - t2.with - } - - const upload = Agent.resource(Space, { - upload: { - _: Unit, - add: task({ - in: Upload, - out: result(Upload), - }), - remove: task({ - in: UploadRoot, - out: result(Upload), - }), - list: task({ - in: Cursor, - out: result(list(Upload)), - }), - }, - }) - - upload.from('did:key:zAlice').upload.list({}) - - const Info = Schema.struct({ - did: DID, - }) - - const info = task({ out: result(Info) }) - - const debug = Agent.resource(Schema.URI, { - debug: { - info: task({ in: undefined, out: result(Info) }), - }, - }) - - const agent = Agent.create({ - authority: Principal.parse('did:web:web3.storage'), - signer: await ed25519.generate(), - delegations: [], - }) - .init(async () => { - return { - password: 'secret', - } - }) - .provide(store, { - store: { - add: async (uri, input, context) => { - return { - status: 'done', - link: input.link, - with: uri, - } - }, - list: async (uri, input, context) => { - return { - results: [], - } - }, - remove: async (uri, input, context) => { - return { link: input.link } - }, - _: () => { - throw new Error('Capability can not be invoked capability') - }, - }, - }) - - const { test } = agent - .query({ - test: agent.resource('did:key:zSpace').store.list({ - cursor: 'hello', - }), - }) - .decode() - - if (!test.error) { - test.results - } - - const worker = agent.connect({ - principal: Principal.parse('did:web:web3.storage'), - capabilities: upload.and(debug), - }) - - const space = worker.resource('did:key:zSpace') - - const listUploads = await space.upload - .list({ - cursor: 'last', - }) - .select({ - results: true, - }) - - const out = listUploads.decode(new Uint8Array()) - if (!out.error) { - out.results[0].root - } - - const { first, second } = worker - .query({ - first: space.upload - .list({ - cursor: 'last', - }) - .select({ - results: true, - cursor: true, - }), - second: space.debug.info(), - }) - .decode() - - if (!first.error) { - first.results[0].root - first.cursor - } - - if (!second.error) { - second.did - } -} - -/** - * @param {object} input - * @param {API.Outcome} input.out - */ -export const testResult = ({ out }) => { - const { ok, error } = out - if (!error) { - ok - } else { - error - ok - } -} diff --git a/packages/agent/test/test.js b/packages/agent/test/test.js deleted file mode 100644 index dbe6d8ec..00000000 --- a/packages/agent/test/test.js +++ /dev/null @@ -1,6 +0,0 @@ -import { assert, use } from 'chai' -import subset from 'chai-subset' -use(subset) - -export const test = it -export { assert } diff --git a/packages/agent/tsconfig.json b/packages/agent/tsconfig.json deleted file mode 100644 index 88b09a3f..00000000 --- a/packages/agent/tsconfig.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ - - /* Projects */ - "incremental": true /* Enable incremental compilation */, - "composite": true /* Enable constraints that allow a TypeScript project to be used with project references. */, - // "tsBuildInfoFile": "./dist", /* Specify the folder for .tsbuildinfo incremental compilation files. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ - // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - - /* Modules */ - "module": "ES2020" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, - "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */, - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "resolveJsonModule": true, /* Enable importing .json files */ - // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, - "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - "declarationMap": true /* Create sourcemaps for d.ts files. */, - "emitDeclarationOnly": true /* Only output d.ts files and not JavaScript files. */, - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist/" /* Specify an output folder for all emitted files. */, - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ - // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ - // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, - "include": ["src", "test"], - "references": [ - { "path": "../interface" }, - { "path": "../core" }, - { "path": "../transport" }, - { "path": "../client" } - ] -} From 0c9aba0c825ac95c46ae6833193cdb9d6e10f3db Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 25 Jan 2023 00:17:21 -0800 Subject: [PATCH 15/17] chore: fix server code --- packages/server/src/server.js | 2 +- packages/server/test/server.spec.js | 8 ++++---- packages/server/test/service/access.js | 6 +++--- packages/server/test/service/store.js | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/server/src/server.js b/packages/server/src/server.js index e36829d3..37ffbeda 100644 --- a/packages/server/src/server.js +++ b/packages/server/src/server.js @@ -1,7 +1,7 @@ import * as API from '@ucanto/interface' -import { capability, Schema } from '@ucanto/core' import { Verifier } from '@ucanto/principal' import { InvalidAudience } from '@ucanto/core/validator' +export { capability, Schema } from '@ucanto/core' export { Failure, MalformedCapability, diff --git a/packages/server/test/server.spec.js b/packages/server/test/server.spec.js index b2bd53cb..30e395aa 100644 --- a/packages/server/test/server.spec.js +++ b/packages/server/test/server.spec.js @@ -8,9 +8,9 @@ import { test, assert } from './test.js' const storeAdd = Server.capability({ can: 'store/add', - with: Server.URI.match({ protocol: 'did:' }), + with: Server.Schema.URI.match({ protocol: 'did:' }), nb: { - link: Server.Link.match().optional(), + link: Server.Schema.Link.match().optional(), }, derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { @@ -33,9 +33,9 @@ const storeAdd = Server.capability({ }) const storeRemove = Server.capability({ can: 'store/remove', - with: Server.URI.match({ protocol: 'did:' }), + with: Server.Schema.URI.match({ protocol: 'did:' }), nb: { - link: Server.Link.match().optional(), + link: Server.Schema.Link.match().optional(), }, derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { diff --git a/packages/server/test/service/access.js b/packages/server/test/service/access.js index a245127b..1afac82b 100644 --- a/packages/server/test/service/access.js +++ b/packages/server/test/service/access.js @@ -6,7 +6,7 @@ export const id = w3 const registerCapability = Server.capability({ can: 'identity/register', - with: Server.URI.match({ protocol: 'mailto:' }), + with: Server.Schema.URI.match({ protocol: 'mailto:' }), derives: (claimed, delegated) => claimed.with === delegated.with || new Server.Failure( @@ -16,7 +16,7 @@ const registerCapability = Server.capability({ const linkCapability = Server.capability({ can: 'identity/link', - with: Server.URI, + with: Server.Schema.URI, derives: (claimed, delegated) => claimed.with === delegated.with || new Server.Failure( @@ -26,7 +26,7 @@ const linkCapability = Server.capability({ const identifyCapability = Server.capability({ can: 'identity/identify', - with: Server.URI, + with: Server.Schema.URI, derives: (claimed, delegated) => claimed.with === delegated.with || delegated.with === 'ucan:*' || diff --git a/packages/server/test/service/store.js b/packages/server/test/service/store.js index 75f8c5d4..da78bb37 100644 --- a/packages/server/test/service/store.js +++ b/packages/server/test/service/store.js @@ -7,9 +7,9 @@ import { service as issuer } from '../fixtures.js' const addCapability = Server.capability({ can: 'store/add', - with: Server.URI.match({ protocol: 'did:' }), + with: Server.Schema.URI.match({ protocol: 'did:' }), nb: { - link: Server.Link.match().optional(), + link: Server.Schema.Link.match().optional(), }, derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { @@ -33,9 +33,9 @@ const addCapability = Server.capability({ const removeCapability = Server.capability({ can: 'store/remove', - with: Server.URI.match({ protocol: 'did:' }), + with: Server.Schema.URI.match({ protocol: 'did:' }), nb: { - link: Server.Link.match().optional(), + link: Server.Schema.Link.match().optional(), }, derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { From 43455c7426827f7559be8af74f881e3f0e111bf5 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 25 Jan 2023 00:34:06 -0800 Subject: [PATCH 16/17] chore: undo unnecessary change --- packages/core/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 052fe2a8..3ccd24b2 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -99,5 +99,5 @@ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": ["src", "test"], - "references": [{ "path": "../interface" }, { "path": "../principal" } ] + "references": [{ "path": "../interface" }, { "path": "../principal" }] } From 5ea415667a505cc57b3c2c11dd43a5b842682952 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 25 Jan 2023 00:34:13 -0800 Subject: [PATCH 17/17] chore: add chai-subset --- packages/core/package.json | 2 ++ pnpm-lock.yaml | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/packages/core/package.json b/packages/core/package.json index b6ab2e10..7faa16cc 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -38,9 +38,11 @@ "devDependencies": { "@types/chai": "^4.3.3", "@types/mocha": "^9.1.0", + "@types/chai-subset": "^1.3.3", "@ucanto/principal": "^4.1.0", "c8": "^7.11.0", "chai": "^4.3.6", + "chai-subset": "^1.6.0", "mocha": "^10.1.0", "nyc": "^15.1.0", "playwright-test": "^8.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e803de46..def013ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,11 +53,13 @@ importers: '@ipld/dag-cbor': ^9.0.0 '@ipld/dag-ucan': ^3.2.0 '@types/chai': ^4.3.3 + '@types/chai-subset': ^1.3.3 '@types/mocha': ^9.1.0 '@ucanto/interface': ^4.1.0 '@ucanto/principal': ^4.1.0 c8: ^7.11.0 chai: ^4.3.6 + chai-subset: ^1.6.0 mocha: ^10.1.0 multiformats: ^11.0.0 nyc: ^15.1.0 @@ -71,10 +73,12 @@ importers: multiformats: 11.0.1 devDependencies: '@types/chai': 4.3.4 + '@types/chai-subset': 1.3.3 '@types/mocha': 9.1.1 '@ucanto/principal': link:../principal c8: 7.12.0 chai: 4.3.7 + chai-subset: 1.6.0 mocha: 10.2.0 nyc: 15.1.0 playwright-test: 8.1.2