Skip to content

Commit

Permalink
fix: optional field validation (#124)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gozala authored Nov 11, 2022
1 parent 62c4206 commit 87b70d2
Show file tree
Hide file tree
Showing 17 changed files with 462 additions and 311 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
},
"pnpm": {
"overrides": {
"multiformats": "^10.0.0"
"multiformats": "^10.0.2"
}
}
}
2 changes: 1 addition & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
},
"dependencies": {
"@ucanto/interface": "^3.0.0",
"multiformats": "^10.0.0"
"multiformats": "^10.0.2"
},
"devDependencies": {
"@types/chai": "^4.3.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@ipld/dag-cbor": "^8.0.0",
"@ipld/dag-ucan": "^2.0.0",
"@ucanto/interface": "^3.0.0",
"multiformats": "^10.0.0"
"multiformats": "^10.0.2"
},
"devDependencies": {
"@types/chai": "^4.3.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
},
"dependencies": {
"@ipld/dag-ucan": "^2.0.0",
"multiformats": "^10.0.0"
"multiformats": "^10.0.2"
},
"devDependencies": {
"typescript": "^4.8.4"
Expand Down
44 changes: 38 additions & 6 deletions packages/interface/src/capability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,43 @@ export interface DerivedMatch<T, M extends Match>

export interface DeriveSelector<M extends Match, T extends ParsedCapability> {
to: TheCapabilityParser<DirectMatch<T>>
derives: Derives<T, M['value']>
derives: Derives<ToDeriveClaim<T>, ToDeriveProof<M['value']>>
}

export interface Derives<T, U = T> {
(self: T, from: U): Result<true, Failure>
/**
* Utility type is used to infer the type of the capability passed into
* `derives` handler. It simply makes all `nb` fileds optional because
* in delegation all `nb` fields could be left out implying no restrictions.
*/
export type ToDeriveClaim<T extends ParsedCapability> =
| T
| ParsedCapability<T['can'], T['with'], Partial<T['nb']>>

/**
* Utility type is used to infer type of the second argument of `derives`
* handler (in the `cap.derive({ to, derives: (claim, proof) => true })`)
* which could be either capability or set of capabilities. It simply makes
* all `nb` fields optional, because in delegation all `nb` fields could be
* left out implying no restrictions.
*/
export type ToDeriveProof<T> = T extends ParsedCapability
? // If it a capability we just make `nb` partial
ToDeriveClaim<T>
: // otherwise we need to map tuple
ToDeriveProofs<T>

/**
* Another helper type which is equivalent of `ToDeriveClaim` except it works
* on tuple of capabilities.
*/
type ToDeriveProofs<T> = T extends [infer U, ...infer E]
? [ToDeriveClaim<U & ParsedCapability>, ...ToDeriveProofs<E>]
: T extends never[]
? []
: never

export interface Derives<T extends ParsedCapability, U = T> {
(claim: T, proof: U): Result<true, Failure>
}

export interface View<M extends Match> extends Matcher<M>, Selector<M> {
Expand Down Expand Up @@ -153,7 +185,7 @@ export interface TheCapabilityParser<M extends Match<ParsedCapability>>
*/
delegate(
options: InferDelegationOptions<M['value']['with'], M['value']['nb']>
): Promise<Delegation<[M['value']]>>
): Promise<Delegation<[ToDeriveClaim<M['value']>]>>
}

export type InferCreateOptions<R extends Resource, C extends {} | undefined> =
Expand Down Expand Up @@ -299,8 +331,8 @@ export interface Descriptor<
nb?: C

derives?: Derives<
ParsedCapability<A, R, InferCaveats<C>>,
ParsedCapability<A, R, InferCaveats<C>>
ToDeriveClaim<ParsedCapability<A, R, InferCaveats<C>>>,
ToDeriveClaim<ParsedCapability<A, R, InferCaveats<C>>>
>
}

Expand Down
2 changes: 1 addition & 1 deletion packages/principal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@ipld/dag-ucan": "^2.0.0",
"@noble/ed25519": "^1.7.1",
"@ucanto/interface": "^3.0.0",
"multiformats": "^10.0.0",
"multiformats": "^10.0.2",
"one-webcrypto": "^1.0.3"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"chai": "^4.3.6",
"chai-subset": "^1.6.0",
"mocha": "^10.1.0",
"multiformats": "^10.0.0",
"multiformats": "^10.0.2",
"nyc": "^15.1.0",
"playwright-test": "^8.1.1",
"typescript": "^4.8.4"
Expand Down
2 changes: 1 addition & 1 deletion packages/transport/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@ipld/dag-cbor": "^8.0.0",
"@ucanto/core": "^3.0.1",
"@ucanto/interface": "^3.0.0",
"multiformats": "^10.0.0"
"multiformats": "^10.0.2"
},
"devDependencies": {
"@types/chai": "^4.3.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/validator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@ipld/dag-cbor": "^8.0.0",
"@ucanto/core": "^3.0.1",
"@ucanto/interface": "^3.0.0",
"multiformats": "^10.0.0"
"multiformats": "^10.0.2"
},
"devDependencies": {
"@types/chai": "^4.3.3",
Expand Down
42 changes: 25 additions & 17 deletions packages/validator/src/capability.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ class Derive extends Unit {
/**
* @param {API.MatchSelector<M>} from
* @param {API.TheCapabilityParser<API.DirectMatch<T>>} to
* @param {API.Derives<T, M['value']>} derives
* @param {API.Derives<API.ToDeriveClaim<T>, API.ToDeriveProof<M['value']>>} derives
*/
constructor(from, to, derives) {
super()
Expand Down Expand Up @@ -432,7 +432,7 @@ class Match {
const errors = []
const matches = []
for (const capability of capabilities) {
const result = parse(this, capability)
const result = parse(this, capability, true)
if (!result.error) {
const claim = this.descriptor.derives(this.value, result)
if (claim.error) {
Expand Down Expand Up @@ -479,7 +479,7 @@ class DerivedMatch {
/**
* @param {API.DirectMatch<T>} selected
* @param {API.MatchSelector<M>} from
* @param {API.Derives<T, M['value']>} derives
* @param {API.Derives<API.ToDeriveClaim<T>, API.ToDeriveProof<M['value']>>} derives
*/
constructor(selected, from, derives) {
this.selected = selected
Expand Down Expand Up @@ -635,16 +635,23 @@ class AndMatch {
}

/**
* 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<A, R, C>}} self
* @param {{descriptor: API.Descriptor<A, R, C>}} parser
* @param {API.Source} source
* @param {boolean} [optional=false]
* @returns {API.Result<API.ParsedCapability<A, R, API.InferCaveats<C>>, API.InvalidCapability>}
*/

const parse = (self, source) => {
const { can, with: withReader, nb: readers } = self.descriptor
const parse = (parser, source, optional = false) => {
const { can, with: withReader, nb: readers } = parser.descriptor
const { delegation } = source
const capability = /** @type {API.Capability<A, R, API.InferCaveats<C>>} */ (
source.capability
Expand All @@ -665,12 +672,14 @@ const parse = (self, source) => {
/** @type {Partial<API.InferCaveats<C>>} */
const caveats = capability.nb || {}
for (const [name, reader] of entries(readers)) {
const key = /** @type {keyof caveats & keyof nb} */ (name)
const result = reader.read(caveats[key])
if (result?.error) {
return new MalformedCapability(capability, result)
} else if (result != null) {
nb[key] = /** @type {any} */ (result)
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)
}
}
}
}
Expand Down Expand Up @@ -762,11 +771,10 @@ const selectGroup = (self, capabilities) => {
}

/**
* @template {API.Ability} A
* @template {API.URI} R
* @template {API.Caveats} C
* @param {API.ParsedCapability<A, R, API.InferCaveats<C>>} claimed
* @param {API.ParsedCapability<A, R, API.InferCaveats<C>>} delegated
* @template {API.ParsedCapability} T
* @template {API.ParsedCapability} U
* @param {T} claimed
* @param {U} delegated
* @return {API.Result<true, API.Failure>}
*/
const derives = (claimed, delegated) => {
Expand Down
10 changes: 0 additions & 10 deletions packages/validator/test/capability.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1669,16 +1669,6 @@ test('.and(...).match', () => {
message: 'Constraint violation: a: a violates A',
},
},
{
name: 'InvalidClaim',
cause: {
name: 'MalformedCapability',
cause: {
name: 'TypeError',
message: 'Expected value of type string instead got undefined',
},
},
},
],
matches: [
{
Expand Down
19 changes: 11 additions & 8 deletions packages/validator/test/delegate.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@ const echo = capability({

test('delegate can omit constraints', async () => {
assert.deepEqual(
await echo.delegate({
issuer: alice,
audience: w3,
with: alice.did(),
nb: {
message: 'data:1',
},
}),
/** @type {API.Delegation} */
(
await echo.delegate({
issuer: alice,
audience: w3,
with: alice.did(),
nb: {
message: 'data:1',
},
})
),
await delegate({
issuer: alice,
audience: w3,
Expand Down
Loading

0 comments on commit 87b70d2

Please sign in to comment.