diff --git a/Readme.md b/Readme.md index 9ac98cc3..a3ad0c55 100644 --- a/Readme.md +++ b/Readme.md @@ -118,11 +118,10 @@ export const server = (context { store = new Map() } : { store: Map { const fileServer = server(context) - HTTP.createServer(async (request, response), => { + HTTP.createServer(async (request, response) => { const chunks = [] for await (const chunk of request) { chunks.push(chunk) @@ -130,7 +129,7 @@ export const listen = ({ port = 8080, context = new Map() }) => { const { headers, body } = await fileServer.request({ headers: request.headers, - body: Buffer.concat(chunks) + body: Buffer.concat(chunks), }) response.writeHead(200, headers) diff --git a/packages/interface/src/capability.ts b/packages/interface/src/capability.ts index de8d10f6..1bddeadd 100644 --- a/packages/interface/src/capability.ts +++ b/packages/interface/src/capability.ts @@ -6,6 +6,7 @@ import { Identity, Resource, Ability, + URI, Capability, DID, LinkedProof, @@ -54,18 +55,12 @@ export interface MatchSelector export interface DirectMatch extends Match> {} -export type Parser< - I extends unknown, - O extends unknown, - X extends { error: true } = Failure -> = (input: I) => Result - export interface Decoder< I extends unknown, O extends unknown, X extends { error: true } = Failure > { - decode: Parser //(input: I): Result + decode: (input: I) => Result } export interface Caveats @@ -128,9 +123,19 @@ export interface View extends Matcher, Selector { ): TheCapabilityParser> } +export type InferCaveatParams = { + [K in keyof T]: T[K] extends { toJSON(): infer U } ? U : T[K] +} + export interface TheCapabilityParser> extends CapabilityParser { readonly can: M['value']['can'] + + create: ( + resource: M['value']['uri']['href'], + caveats: InferCaveatParams + ) => Capability & + M['value']['caveats'] } export interface CapabilityParser extends View { @@ -225,43 +230,49 @@ export type InferMatch = Members extends [] export interface ParsedCapability< Can extends Ability = Ability, + Resource extends URI = URI, C extends object = {} > { can: Can - with: Resource - uri: URL + with: Resource['href'] + uri: Resource caveats: C } export type InferCaveats = InferRequiredCaveats & InferOptionalCaveats export type InferOptionalCaveats = { - [Key in keyof C as C[Key] extends Decoder - ? Key - : never]?: C[Key] extends Decoder - ? T - : never + [K in keyof C as C[K] extends Decoder + ? K + : never]?: C[K] extends Decoder ? T : never } export type InferRequiredCaveats = { - [Key in keyof C as C[Key] extends Decoder + [K in keyof C as C[K] extends Decoder ? never - : Key]: C[Key] extends Decoder ? T : never + : K]: C[K] extends Decoder ? T : never } -export interface Descriptor { +export interface Descriptor< + A extends Ability, + R extends URI, + C extends Caveats +> { can: A - with: Decoder + with: Decoder caveats?: C derives: Derives< - ParsedCapability>, - ParsedCapability> + ParsedCapability>, + ParsedCapability> > } -export interface CapabilityMatch - extends DirectMatch>> {} +export interface CapabilityMatch< + A extends Ability, + R extends URI, + C extends Caveats +> extends DirectMatch>> {} export interface CanIssue { /** @@ -271,9 +282,11 @@ export interface CanIssue { canIssue(capability: ParsedCapability, issuer: DID): boolean } -export interface ValidationOptions< - C extends ParsedCapability = ParsedCapability -> extends CanIssue { +export interface AuthorityOptions { + authority: AuthorityParser +} + +export interface IssuingOptions { /** * You can provide default set of capabilities per did, which is used to * validate whether claim is satisfied by `{ with: my:*, can: "*" }`. If @@ -281,16 +294,23 @@ export interface ValidationOptions< */ my?: (issuer: DID) => Capability[] +} +export interface ProofResolver extends AuthorityOptions, IssuingOptions { /** * You can provide a proof resolver that validator will call when UCAN * links to external proof. If resolver is not provided validator may not * be able to explore correesponding path within a proof chain. */ resolve?: (proof: LinkedProof) => Await> +} - authority: AuthorityParser - capability: CapabilityParser> +export interface ValidationOptions + extends CanIssue, + IssuingOptions, + AuthorityOptions, + ProofResolver { + capability: CapabilityParser> } export interface DelegationError extends Failure { diff --git a/packages/interface/src/lib.ts b/packages/interface/src/lib.ts index fc7b8a69..e8210325 100644 --- a/packages/interface/src/lib.ts +++ b/packages/interface/src/lib.ts @@ -362,3 +362,13 @@ export type Service = Record< > export type Await = T | PromiseLike | Promise + +export type Protocol = `${Scheme}:` +export interface URI

extends URL { + protocol: P + href: `${P}${string}` +} + +export type URIString

= `${URI['protocol']}${string}` & { + protocol?: Protocol +} diff --git a/packages/server/src/api.ts b/packages/server/src/api.ts index 661e9714..9f50b390 100644 --- a/packages/server/src/api.ts +++ b/packages/server/src/api.ts @@ -20,11 +20,11 @@ export interface ProviderOptions extends CanIssue { export interface ProviderContext< A extends API.Ability = API.Ability, - R extends API.Resource = API.Resource, + R extends API.URI = API.URI, C extends API.Caveats = API.Caveats > { - capability: API.ParsedCapability> - invocation: API.Invocation & API.InferCaveats> + capability: API.ParsedCapability> + invocation: API.Invocation & API.InferCaveats> context: API.InvocationContext } diff --git a/packages/server/src/handler.js b/packages/server/src/handler.js index d445c41e..09685e32 100644 --- a/packages/server/src/handler.js +++ b/packages/server/src/handler.js @@ -4,22 +4,26 @@ import { access } from '@ucanto/validator' /** * @template {API.Ability} A * @template {API.Caveats} C - * @template {API.Resource} R + * @template {API.URI} R * @template {unknown} U - * @param {API.TheCapabilityParser>} capability + * @template {API.Match} Z + * @param {API.CapabilityParser>, Z>>} capability * @param {(input:API.ProviderContext) => API.Await} handler - * @returns {API.ServiceMethod & API.InferCaveats, Exclude, Exclude>>} + * @returns {API.ServiceMethod & API.InferCaveats, Exclude, Exclude>>} */ export const provide = (capability, handler) => /** - * @param {API.Invocation & API.InferCaveats>} invocation + * @param {API.Invocation & API.InferCaveats>} invocation * @param {API.InvocationContext} options * @return {Promise, Exclude>|API.InvocationError>>} */ async (invocation, options) => { - const authorization = await access(invocation, { ...options, capability }) + const authorization = await access(invocation, { + ...options, + capability, + }) if (authorization.error) { return authorization } else { diff --git a/packages/server/src/lib.js b/packages/server/src/lib.js index bfa4a475..1299b4b1 100644 --- a/packages/server/src/lib.js +++ b/packages/server/src/lib.js @@ -1,4 +1,7 @@ export * from './server.js' +export * from '@ucanto/authority' +export * from '@ucanto/core' // @ts-ignore export * from './api.js' export * from './handler.js' +export * as API from './api.js' diff --git a/packages/server/src/server.js b/packages/server/src/server.js index 05f7f77c..bf351426 100644 --- a/packages/server/src/server.js +++ b/packages/server/src/server.js @@ -1,5 +1,4 @@ import * as API from '@ucanto/interface' -export * from '@ucanto/interface' import { InvalidAudience } from '@ucanto/validator' import { Authority } from '@ucanto/authority' export { diff --git a/packages/server/test/fixtures.js b/packages/server/test/fixtures.js index 78ee5f67..5be5bd3a 100644 --- a/packages/server/test/fixtures.js +++ b/packages/server/test/fixtures.js @@ -13,6 +13,7 @@ export const mallory = SigningAuthority.parse( 'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=' ) +/** did:key:z6MkrZ1r5XBFZjBU34qyD8fueMbMRkKw17BZaq2ivKFjnz2z */ export const service = SigningAuthority.parse( 'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=' ) diff --git a/packages/server/test/server.spec.js b/packages/server/test/server.spec.js index 0559f053..daadbfe2 100644 --- a/packages/server/test/server.spec.js +++ b/packages/server/test/server.spec.js @@ -1,5 +1,5 @@ import * as Client from '@ucanto/client' -import * as Server from '../src/server.js' +import * as Server from '../src/lib.js' import * as CAR from '@ucanto/transport/car' import * as CBOR from '@ucanto/transport/cbor' import { alice, bob, mallory, service as w3 } from './fixtures.js' diff --git a/packages/transport/src/cbor/codec.js b/packages/transport/src/cbor/codec.js index ed110bbd..38066be4 100644 --- a/packages/transport/src/cbor/codec.js +++ b/packages/transport/src/cbor/codec.js @@ -30,6 +30,10 @@ const prepare = (data, seen) => { return cid } + if (ArrayBuffer.isView(data)) { + return data + } + if (Array.isArray(data)) { seen.add(data) const items = [] diff --git a/packages/transport/test/cbor.spec.js b/packages/transport/test/cbor.spec.js index ce044ea5..cde42c69 100644 --- a/packages/transport/test/cbor.spec.js +++ b/packages/transport/test/cbor.spec.js @@ -1,6 +1,7 @@ import { test, assert } from './test.js' import * as CBOR from '../src/cbor.js' import { decode, encode } from '@ipld/dag-cbor' +import * as UTF8 from '../src/utf8.js' test('encode / decode', async () => { // @ts-ignore const response = CBOR.encode([{ ok: true, value: 1 }]) @@ -70,6 +71,11 @@ test('content-type case', async () => { }) } + test(`encode / decode bytes`, async () => { + const actual = transcode({ bytes: UTF8.encode('hello') }) + assert.deepEqual(actual, { bytes: UTF8.encode('hello') }) + }) + test('circular objects throw', () => { const circular = { a: 1, circle: {} } circular.circle = circular diff --git a/packages/validator/src/capability.js b/packages/validator/src/capability.js index 1b240832..3db0e54b 100644 --- a/packages/validator/src/capability.js +++ b/packages/validator/src/capability.js @@ -9,9 +9,10 @@ import { /** * @template {API.Ability} A + * @template {API.URI} R * @template {API.Caveats} [C={}] - * @param {API.Descriptor} descriptor - * @returns {API.TheCapabilityParser>} + * @param {API.Descriptor} descriptor + * @returns {API.TheCapabilityParser>} */ export const capability = (descriptor) => new Capability(descriptor) @@ -96,26 +97,50 @@ class Unit extends View { /** * @template {API.Ability} A + * @template {API.URI} R * @template {API.Caveats} C - * @implements {API.TheCapabilityParser>} - * @extends {Unit>} + * @implements {API.TheCapabilityParser>>>} + * @extends {Unit>>>} */ class Capability extends Unit { /** - * @param {API.Descriptor} descriptor + * @param {API.Descriptor} descriptor */ constructor(descriptor) { super() this.descriptor = descriptor } + /** + * @param {R['href']} resource + * @param {API.InferCaveatParams>} data + * @return {API.Capability & API.InferCaveats} + */ + create(resource, data) { + const { descriptor, can } = this + const decoders = descriptor.caveats + + const caveats = /** @type {API.InferCaveats} */ ({}) + for (const [name, decoder] of Object.entries(decoders || {})) { + const key = /** @type {keyof caveats} */ (name) + const value = decoder.decode(data[key]) + if (value?.error) { + throw value + } else { + caveats[key] = /** @type {typeof caveats[key]} */ (value) + } + } + + return { ...caveats, can, with: resource } + } + get can() { return this.descriptor.can } /** * @param {API.Source} source - * @returns {API.MatchResult>} + * @returns {API.MatchResult>>>} */ match(source) { const result = parse(this, source) @@ -236,6 +261,13 @@ class Derive extends Unit { this.to = to this.derives = derives } + + /** + * @type {typeof this.to['create']} + */ + create(resource, caveats) { + return this.to.create(resource, caveats) + } get can() { return this.to.can } @@ -258,14 +290,15 @@ class Derive extends Unit { /** * @template {API.Ability} A + * @template {API.URI} R * @template {API.Caveats} C - * @implements {API.CapabilityMatch} + * @implements {API.DirectMatch>>} */ class Match { /** * @param {API.Source} source - * @param {API.ParsedCapability>} value - * @param {API.Descriptor} descriptor + * @param {API.ParsedCapability>} value + * @param {API.Descriptor} descriptor */ constructor(source, value, descriptor) { this.source = [source] @@ -286,7 +319,7 @@ class Match { /** * @param {API.CanIssue} context - * @returns {API.CapabilityMatch|null} + * @returns {API.DirectMatch>>|null} */ prune(context) { if (context.canIssue(this.value, this.source[0].delegation.issuer.did())) { @@ -298,7 +331,7 @@ class Match { /** * @param {API.Source[]} capabilities - * @returns {API.Select>} + * @returns {API.Select>>>} */ select(capabilities) { const unknown = [] @@ -513,11 +546,11 @@ class AndMatch { /** * @template {API.Ability} A + * @template {API.URI} R * @template {API.Caveats} C - * @template {API.ParsedCapability} T - * @param {{descriptor: API.Descriptor}} self + * @param {{descriptor: API.Descriptor}} self * @param {API.Source} source - * @returns {API.Result>, API.InvalidCapability>} + * @returns {API.Result>, API.InvalidCapability>} */ const parse = (self, source) => { @@ -536,33 +569,41 @@ const parse = (self, source) => { return new MalformedCapability(capability, uri) } - const caveats = /** @type {T['caveats']} */ ({}) + const caveats = /** @type {API.InferCaveats} */ ({}) if (decoders) { for (const [name, decoder] of entries(decoders)) { - const value = capability[/** @type {string} */ (name)] + const key = /** @type {keyof capability & keyof caveats} */ (name) + const value = capability[key] const result = decoder.decode(value) if (result?.error) { return new MalformedCapability(capability, result) } else if (result != null) { - caveats[name] = result + caveats[key] = /** @type {any} */ (result) } } } - return new CapabilityView(can, capability.with, uri, caveats, delegation) + return new CapabilityView( + can, + /** @type {R['href']} */ (capability.with), + uri, + caveats, + delegation + ) } /** * @template {API.Ability} A + * @template {API.URI} R * @template C - * @implements {API.ParsedCapability>} + * @implements {API.ParsedCapability>} */ class CapabilityView { /** * @param {A} can - * @param {API.Resource} with_ - * @param {URL} uri + * @param {R['href']} with_ + * @param {R} uri * @param {API.InferCaveats} caveats * @param {API.Delegation} delegation */ diff --git a/packages/validator/src/decoder/uri.js b/packages/validator/src/decoder/uri.js index dd7e7204..98323555 100644 --- a/packages/validator/src/decoder/uri.js +++ b/packages/validator/src/decoder/uri.js @@ -3,16 +3,17 @@ import { Failure } from '../error.js' /** * @template {`${string}:`} Protocol - * @param {string} input + * @param {unknown} input * @param {{protocol?: Protocol}} [options] + * @return {API.Result, API.Failure>} */ export const decode = (input, { protocol } = {}) => { try { - const url = new URL(input) + const url = new URL(String(input)) if (protocol != null && url.protocol !== protocol) { return new Failure(`Expected ${protocol} URI instead got ${url.href}`) } else { - return /** @type {URL & {protocol:Protocol}} */ (url) + return /** @type {API.URI} */ (url) } } catch (_) { return new Failure(`Invalid URI`) @@ -22,8 +23,25 @@ export const decode = (input, { protocol } = {}) => { /** * @template {`${string}:`} Protocol * @param {{protocol: Protocol}} options - * @returns {API.Decoder} + * @returns {API.Decoder, API.Failure>} */ export const match = (options) => ({ decode: (input) => decode(input, options), }) + +/** + * @template {`${string}:`} Protocol + * @typedef {`${Protocol}${string}`} URIString + */ + +/** + * @template {string} Schema + * @param {{protocol?: API.Protocol}} [options] + * @returns {API.Decoder} + */ +export const string = (options) => ({ + decode: (input) => { + const result = decode(input, options) + return result.error ? result : result.href + }, +}) diff --git a/packages/validator/src/lib.js b/packages/validator/src/lib.js index 368cc0fe..91f2eeb0 100644 --- a/packages/validator/src/lib.js +++ b/packages/validator/src/lib.js @@ -27,8 +27,7 @@ const empty = () => [] const unavailable = (proof) => new UnavailableProof(proof) /** - * @template {API.ParsedCapability} C - * @param {Required>} config + * @param {Required} config * @param {API.Match} match */ @@ -53,9 +52,8 @@ const resolveMatch = async (match, config) => { } /** - * @template {API.ParsedCapability} C * @param {API.Delegation} delegation - * @param {Required>} config + * @param {Required} config */ const resolveProofs = async (delegation, config) => { /** @type {API.Result[]} */ @@ -86,9 +84,8 @@ const resolveProofs = async (delegation, config) => { } /** - * @template {API.ParsedCapability} C * @param {API.Source} from - * @param {Required>} config + * @param {Required} config * @return {Promise<{sources:API.Source[], errors:ProofError[]}>} */ const resolveSources = async ({ delegation }, config) => { @@ -135,10 +132,12 @@ const resolveSources = async ({ delegation }, config) => { } /** - * @template {API.ParsedCapability} C - * @param {API.Invocation} invocation - * @param {API.ValidationOptions} config - * @returns {Promise, API.Unauthorized>>} + * @template {API.Ability} A + * @template {API.Caveats} C + * @template {API.URI} R + * @param {API.Invocation & API.InferCaveats>} invocation + * @param {API.ValidationOptions>>} config + * @returns {Promise>>, API.Unauthorized>>} */ export const access = async ( invocation, @@ -340,7 +339,7 @@ const ALL = '*' /** * @template {API.ParsedCapability} C * @param {API.Delegation} delegation - * @param {Required>} options + * @param {Required} options */ function* iterateCapabilities({ issuer, capabilities }, { my }) { const did = issuer.did() @@ -391,9 +390,10 @@ const parseMyURI = (uri, did) => { } /** - * @param {API.Delegation} delegation - * @param {API.ValidationOptions} config - * @returns {Promise>} + * @template {API.Delegation} T + * @param {T} delegation + * @param {API.AuthorityOptions} config + * @returns {Promise>} */ const validate = async (delegation, config) => { if (UCAN.isExpired(delegation.data)) { @@ -412,9 +412,10 @@ const validate = async (delegation, config) => { } /** - * @param {API.Delegation} delegation - * @param {API.ValidationOptions} config - * @returns {Promise>} + * @template {API.Delegation} T + * @param {T} delegation + * @param {API.AuthorityOptions} config + * @returns {Promise>} */ const verifySignature = async (delegation, { authority }) => { const issuer = authority.parse(delegation.issuer.did()) diff --git a/packages/validator/test/lib.spec.js b/packages/validator/test/lib.spec.js index 70a93813..ec359eea 100644 --- a/packages/validator/test/lib.spec.js +++ b/packages/validator/test/lib.spec.js @@ -178,6 +178,7 @@ test('unknown capability', async () => { }) const result = await access(invocation, { + // @ts-expect-error capability: storeAdd, authority: Authority, canIssue: (claim, issuer) => {