doipjs/src/claim.js

411 lines
11 KiB
JavaScript
Raw Normal View History

2021-04-16 05:11:27 -06:00
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
2023-07-08 00:17:13 -06:00
import isAlphanumeric from 'validator/lib/isAlphanumeric.js'
import { isUri } from 'valid-url'
import mergeOptions from 'merge-options'
import { fetch } from './proofs.js'
import { run } from './verifications.js'
import { list, data as _data } from './serviceProviders/index.js'
2023-07-08 00:17:13 -06:00
import { opts as _opts } from './defaults.js'
import { ClaimStatus } from './enums.js'
2021-04-16 05:11:27 -06:00
/**
* @class
* @classdesc Identity claim
2021-04-22 07:14:21 -06:00
* @property {string} uri - The claim's URI
* @property {string} fingerprint - The fingerprint to verify the claim against
* @property {number} status - The current status code of the claim
2021-04-22 07:14:21 -06:00
* @property {Array<object>} matches - The claim definitions matched against the URI
2021-04-16 05:11:27 -06:00
*/
2023-07-08 00:17:13 -06:00
export class Claim {
2021-04-16 05:11:27 -06:00
/**
* Initialize a Claim object
* @constructor
* @param {string} [uri] - The URI of the identity claim
2021-04-22 07:14:21 -06:00
* @param {string} [fingerprint] - The fingerprint of the OpenPGP key
* @example
* const claim = doip.Claim();
* const claim = doip.Claim('dns:domain.tld?type=TXT');
* const claim = doip.Claim('dns:domain.tld?type=TXT', '123abc123abc');
2021-04-16 05:11:27 -06:00
*/
2021-07-09 15:44:52 -06:00
constructor (uri, fingerprint) {
2021-04-16 05:11:27 -06:00
// Verify validity of URI
2023-07-08 00:17:13 -06:00
if (uri && !isUri(uri)) {
2021-04-16 05:11:27 -06:00
throw new Error('Invalid URI')
}
2021-04-22 08:00:37 -06:00
2021-04-16 05:11:27 -06:00
// Verify validity of fingerprint
if (fingerprint) {
try {
2023-07-08 00:17:13 -06:00
// @ts-ignore
isAlphanumeric.default(fingerprint)
2021-04-16 05:11:27 -06:00
} catch (err) {
2021-07-09 15:44:52 -06:00
throw new Error('Invalid fingerprint')
2021-04-16 05:11:27 -06:00
}
}
/**
* @type {string}
*/
2022-11-17 13:09:42 -07:00
this._uri = uri || ''
/**
* @type {string}
*/
2022-11-17 13:09:42 -07:00
this._fingerprint = fingerprint || ''
/**
* @type {number}
*/
2023-07-08 00:17:13 -06:00
this._status = ClaimStatus.INIT
/**
* @type {import('./serviceProvider.js').ServiceProvider[]}
*/
2022-11-17 13:09:42 -07:00
this._matches = []
}
/**
* @function
* @param {object} claimObject
* @example
* const claimAlt = doip.Claim(JSON.stringify(claim));
*/
static fromJson (claimObject) {
/** @type {Claim} */
let claim
let result
if (typeof claimObject === 'object' && 'claimVersion' in claimObject) {
switch (claimObject.claimVersion) {
case 1:
result = importJsonClaimVersion1(claimObject)
if (result instanceof Error) {
throw result
}
claim = result
break
case 2:
result = importJsonClaimVersion2(claimObject)
if (result instanceof Error) {
throw result
}
claim = result
break
default:
throw new Error('Invalid claim version')
}
}
return claim
2021-04-16 05:11:27 -06:00
}
2021-07-09 15:44:52 -06:00
get uri () {
2021-04-19 05:38:00 -06:00
return this._uri
2021-04-16 05:11:27 -06:00
}
2021-07-09 15:44:52 -06:00
get fingerprint () {
2021-04-19 05:38:00 -06:00
return this._fingerprint
2021-04-16 05:11:27 -06:00
}
2021-07-09 15:44:52 -06:00
get status () {
2021-04-22 07:14:21 -06:00
return this._status
2021-04-16 05:11:27 -06:00
}
2021-07-09 15:44:52 -06:00
get matches () {
2023-07-08 00:17:13 -06:00
if (this._status === ClaimStatus.INIT) {
2021-04-16 05:11:27 -06:00
throw new Error('This claim has not yet been matched')
}
2021-04-30 04:28:01 -06:00
return this._matches
2021-04-16 05:11:27 -06:00
}
2021-07-09 15:44:52 -06:00
set uri (uri) {
2023-07-08 00:17:13 -06:00
if (this._status !== ClaimStatus.INIT) {
2021-04-19 03:44:30 -06:00
throw new Error(
'Cannot change the URI, this claim has already been matched'
)
2021-04-16 05:11:27 -06:00
}
// Verify validity of URI
2023-07-08 00:17:13 -06:00
if (uri.length > 0 && !isUri(uri)) {
2021-04-16 05:11:27 -06:00
throw new Error('The URI was invalid')
}
// Remove leading and trailing spaces
uri = uri.replace(/^\s+|\s+$/g, '')
2021-04-19 05:38:00 -06:00
this._uri = uri
2021-04-16 05:11:27 -06:00
}
2021-07-09 15:44:52 -06:00
set fingerprint (fingerprint) {
2023-07-08 00:17:13 -06:00
if (this._status === ClaimStatus.VERIFIED) {
2021-04-19 03:44:30 -06:00
throw new Error(
'Cannot change the fingerprint, this claim has already been verified'
)
2021-04-16 05:11:27 -06:00
}
2021-04-19 05:38:00 -06:00
this._fingerprint = fingerprint
2021-04-19 03:41:40 -06:00
}
2021-07-09 15:44:52 -06:00
set status (anything) {
2021-04-22 07:14:21 -06:00
throw new Error("Cannot change a claim's status")
2021-04-19 03:41:40 -06:00
}
2021-07-09 15:44:52 -06:00
set matches (anything) {
2021-04-30 04:28:01 -06:00
throw new Error("Cannot change a claim's matches")
2021-04-19 03:41:40 -06:00
}
2021-04-16 05:11:27 -06:00
/**
* Match the claim's URI to candidate definitions
* @function
*/
2021-07-09 15:44:52 -06:00
match () {
2023-07-08 00:17:13 -06:00
if (this._status !== ClaimStatus.INIT) {
2021-04-16 05:11:27 -06:00
throw new Error('This claim was already matched')
}
2023-07-08 00:17:13 -06:00
if (this._uri.length === 0 || !isUri(this._uri)) {
2021-04-16 05:11:27 -06:00
throw new Error('This claim has no URI')
}
2021-04-30 04:28:01 -06:00
this._matches = []
2021-04-16 05:11:27 -06:00
2023-07-08 00:17:13 -06:00
list.every((name, i) => {
const def = _data[name]
2021-04-16 05:11:27 -06:00
// If the candidate is invalid, continue matching
2021-04-19 05:38:00 -06:00
if (!def.reURI.test(this._uri)) {
2021-04-16 05:11:27 -06:00
return true
}
2021-04-19 05:38:00 -06:00
const candidate = def.processURI(this._uri)
2022-10-07 02:18:52 -06:00
// If the candidate could not be processed, continue matching
if (!candidate) {
return true
}
if (candidate.claim.uriIsAmbiguous) {
2021-04-16 05:11:27 -06:00
// Add to the possible candidates
2021-04-30 04:28:01 -06:00
this._matches.push(candidate)
2021-04-16 05:11:27 -06:00
} else {
// Set a single candidate and stop
2021-04-30 04:28:01 -06:00
this._matches = [candidate]
2021-04-16 05:11:27 -06:00
return false
}
// Continue matching
return true
})
this._status = this._matches.length === 0 ? ClaimStatus.NO_MATCHES : ClaimStatus.MATCHED
2021-04-16 05:11:27 -06:00
}
/**
* Verify the claim. The proof for each candidate is sequentially fetched and
* checked for the fingerprint. The verification stops when either a positive
* result was obtained, or an unambiguous claim definition was processed
* regardless of the result.
* @async
* @function
* @param {object} [opts] - Options for proxy, fetchers
*/
2021-07-09 15:44:52 -06:00
async verify (opts) {
2023-07-08 00:17:13 -06:00
if (this._status === ClaimStatus.INIT) {
2021-04-16 05:11:27 -06:00
throw new Error('This claim has not yet been matched')
}
if (this._status >= 200) {
2021-04-16 05:11:27 -06:00
throw new Error('This claim has already been verified')
}
2022-11-17 13:09:42 -07:00
if (this._fingerprint.length === 0) {
2021-04-16 05:11:27 -06:00
throw new Error('This claim has no fingerprint')
}
// Handle options
2023-07-08 00:17:13 -06:00
opts = mergeOptions(_opts, opts || {})
2021-04-16 05:11:27 -06:00
2021-06-03 07:03:54 -06:00
// If there are no matches
if (this._matches.length === 0) {
this.status = ClaimStatus.NO_MATCHES
2021-06-03 07:03:54 -06:00
}
2021-04-16 05:11:27 -06:00
// For each match
2021-04-30 04:28:01 -06:00
for (let index = 0; index < this._matches.length; index++) {
// Continue if a result was already obtained
if (this._status >= 200) { continue }
2022-10-03 14:32:46 -06:00
let claimData = this._matches[index]
2021-04-19 03:44:30 -06:00
2021-07-09 15:44:52 -06:00
let verificationResult = null
let proofData = null
let proofFetchError
2021-04-16 05:11:27 -06:00
try {
2023-07-08 00:17:13 -06:00
proofData = await fetch(claimData, opts)
2021-04-16 05:11:27 -06:00
} catch (err) {
proofFetchError = err
}
if (proofData) {
// Run the verification process
2023-07-08 00:17:13 -06:00
verificationResult = await run(
2021-04-19 03:44:30 -06:00
proofData.result,
claimData,
2021-04-19 05:38:00 -06:00
this._fingerprint
2021-04-19 03:44:30 -06:00
)
2021-04-16 05:11:27 -06:00
verificationResult.proof = {
fetcher: proofData.fetcher,
2021-07-09 15:44:52 -06:00
viaProxy: proofData.viaProxy
2021-04-16 05:11:27 -06:00
}
2022-10-03 14:32:46 -06:00
// Post process the data
const def = _data[claimData.about.id]
2023-03-06 13:59:00 -07:00
if (def.functions?.postprocess) {
2022-10-25 01:22:25 -06:00
try {
({ claimData, proofData } = def.functions.postprocess(claimData, proofData))
2022-10-25 01:22:25 -06:00
} catch (_) {}
2022-10-03 14:32:46 -06:00
}
2021-04-16 05:11:27 -06:00
} else {
// Consider the proof completed but with a negative result
2021-07-09 15:44:52 -06:00
verificationResult = verificationResult || {
2021-04-16 05:11:27 -06:00
result: false,
completed: true,
proof: {},
2021-07-09 15:44:52 -06:00
errors: [proofFetchError]
2021-04-16 05:11:27 -06:00
}
2022-10-25 01:22:25 -06:00
}
2022-10-25 01:22:25 -06:00
if (this.isAmbiguous() && !verificationResult.result) {
// Assume a wrong match and continue
continue
2021-04-16 05:11:27 -06:00
}
if (verificationResult.result) {
this._status = verificationResult.proof.viaProxy ? ClaimStatus.VERIFIED_VIA_PROXY : ClaimStatus.VERIFIED
2021-04-30 04:28:01 -06:00
this._matches = [claimData]
2021-04-16 05:11:27 -06:00
}
}
this._status = this._status >= 200 ? this._status : ClaimStatus.NO_PROOF_FOUND
2021-04-16 05:11:27 -06:00
}
/**
2021-04-22 07:14:21 -06:00
* Determine the ambiguity of the claim. A claim is only unambiguous if any
2021-04-16 05:11:27 -06:00
* of the candidates is unambiguous. An ambiguous claim should never be
* displayed in an user interface when its result is negative.
* @function
* @returns {boolean}
*/
2021-07-09 15:44:52 -06:00
isAmbiguous () {
2023-07-08 00:17:13 -06:00
if (this._status === ClaimStatus.INIT) {
2021-04-16 05:11:27 -06:00
throw new Error('The claim has not been matched yet')
}
2021-04-30 04:28:01 -06:00
if (this._matches.length === 0) {
2021-04-16 05:11:27 -06:00
throw new Error('The claim has no matches')
2021-04-19 03:44:30 -06:00
}
return this._matches.length > 1 || this._matches[0].claim.uriIsAmbiguous
2021-04-16 05:11:27 -06:00
}
/**
2021-04-19 03:44:30 -06:00
* Get a JSON representation of the Claim object. Useful when transferring
2021-04-16 05:11:27 -06:00
* data between instances/machines.
* @function
* @returns {object}
*/
2021-07-09 15:44:52 -06:00
toJSON () {
let displayName = this._uri
let displayUrl = null
let displayServiceProviderName = null
if (this._status >= 200 && this._status < 300) {
displayName = this._matches[0].profile.display
displayUrl = this._matches[0].profile.uri
displayServiceProviderName = this._matches[0].about.name
2023-07-10 02:39:00 -06:00
} else if (this._status === ClaimStatus.MATCHED && !this.isAmbiguous()) {
displayName = this._matches[0].profile.display
displayUrl = this._matches[0].profile.uri
displayServiceProviderName = this._matches[0].about.name
}
2021-04-16 05:11:27 -06:00
return {
claimVersion: 2,
2021-04-19 05:38:00 -06:00
uri: this._uri,
proofs: [this._fingerprint],
matches: this._matches.map(x => x.toJSON()),
status: this._status,
display: {
name: displayName,
url: displayUrl,
serviceProviderName: displayServiceProviderName
}
2021-04-16 05:11:27 -06:00
}
}
}
/**
* @param {object} claimObject
* @returns {Claim | Error}
*/
function importJsonClaimVersion1 (claimObject) {
if (!('claimVersion' in claimObject && claimObject.claimVersion === 1)) {
return new Error('Invalid claim')
}
const claim = new Claim()
claim._uri = claimObject.uri
claim._fingerprint = claimObject.fingerprint
claim._matches = claimObject.matches
if (claimObject.status === 'init') {
claim._status = 100
}
if (claimObject.status === 'matched') {
if (claimObject.matches.length === 0) {
claim._status = 301
}
claim._status = 101
}
if (!('result' in claimObject.verification && 'errors' in claimObject.verification)) {
claim._status = 400
}
if (claimObject.verification.errors.length > 0) {
claim._status = 400
}
if (claimObject.verification.result && claimObject.verification.proof.viaProxy) {
claim._status = 201
}
if (claimObject.verification.result && !claimObject.verification.proof.viaProxy) {
claim._status = 200
}
return claim
}
/**
* @param {object} claimObject
* @returns {Claim | Error}
*/
function importJsonClaimVersion2 (claimObject) {
if (!('claimVersion' in claimObject && claimObject.claimVersion === 2)) {
return new Error('Invalid claim')
}
const claim = new Claim()
claim._uri = claimObject.uri
claim._fingerprint = claimObject.proofs[0]
claim._matches = claimObject.matches
claim._status = claimObject.status
return claim
}