@ucanto/validator
Version:
UCAN RPC validators
395 lines (371 loc) • 9.12 kB
JavaScript
import * as API from '@ucanto/interface'
import { the } from './util.js'
import { isLink } from '@ucanto/core/link'
import { fail, Failure } from '@ucanto/core/result'
export { Failure, fail }
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.SessionEscalation}
*/
export class SessionEscalation extends Failure {
/**
* @param {object} source
* @param {API.Delegation} source.delegation
* @param {API.Failure} source.cause
*/
constructor({ delegation, cause }) {
super()
this.name = the('SessionEscalation')
this.delegation = delegation
this.cause = cause
}
describe() {
const issuer = this.delegation.issuer.did()
return [
`Delegation ${this.delegation.cid} issued by ${issuer} has an invalid session`,
li(this.cause.message),
].join('\n')
}
}
/**
* @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.Failure} [cause]
*/
constructor(did, cause) {
super()
this.name = the('DIDKeyResolutionError')
this.did = did
this.cause = cause
}
describe() {
return `Unable to resolve '${this.did}' key`
}
}
/**
* @implements {API.InvalidAudience}
*/
export class PrincipalAlignmentError 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 { name, audience, message, stack } = this
return {
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 { name, expiredAt, message, stack } = this
return {
name,
message,
expiredAt,
stack,
}
}
}
/**
* @implements {API.Revoked}
*/
export class Revoked extends Failure {
/**
* @param {API.Delegation} delegation
*/
constructor(delegation) {
super()
this.name = the('Revoked')
this.delegation = delegation
}
describe() {
return `Proof ${this.delegation.cid} has been revoked`
}
toJSON() {
const { name, message, stack } = this
return {
name,
message,
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 { name, validAt, message, stack } = this
return {
name,
message,
validAt,
stack,
}
}
}
/**
* @implements {API.Unauthorized}
*/
export class Unauthorized extends Failure {
/**
* @param {{
* capability: API.CapabilityParser
* delegationErrors: API.DelegationError[]
* unknownCapabilities: API.Capability[]
* invalidProofs: API.InvalidProof[]
* failedProofs: API.InvalidClaim[]
* }} cause
*/
constructor({
capability,
delegationErrors,
unknownCapabilities,
invalidProofs,
failedProofs,
}) {
super()
this.name = /** @type {const} */ ('Unauthorized')
this.capability = capability
this.delegationErrors = delegationErrors
this.unknownCapabilities = unknownCapabilities
this.invalidProofs = invalidProofs
this.failedProofs = failedProofs
}
describe() {
const errors = [
...this.failedProofs.map(error => li(error.message)),
...this.delegationErrors.map(error => li(error.message)),
...this.invalidProofs.map(error => li(error.message)),
]
const unknown = this.unknownCapabilities.map(c => li(JSON.stringify(c)))
return [
`Claim ${this.capability} is not authorized`,
...(errors.length > 0
? errors
: [li(`No matching delegated capability found`)]),
...(unknown.length > 0
? [li(`Encountered unknown capabilities\n${unknown.join('\n')}`)]
: []),
].join('\n')
}
}
/**
* @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}`)