forked from Mirrors/doipjs
feat: update claim, persona, profile classes
This commit is contained in:
parent
fd8c760689
commit
a30339272a
3 changed files with 391 additions and 102 deletions
211
src/claim.js
211
src/claim.js
|
@ -18,50 +18,30 @@ 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 './claimDefinitions/index.js'
|
||||
import { list, data as _data } from './serviceProviders/index.js'
|
||||
import { opts as _opts } from './defaults.js'
|
||||
import { ClaimStatus } from './enums.js'
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @classdesc OpenPGP-based identity claim
|
||||
* @classdesc Identity claim
|
||||
* @property {string} uri - The claim's URI
|
||||
* @property {string} fingerprint - The fingerprint to verify the claim against
|
||||
* @property {string} status - The current status of the claim
|
||||
* @property {number} status - The current status code of the claim
|
||||
* @property {Array<object>} matches - The claim definitions matched against the URI
|
||||
* @property {object} verification - The result of the verification process
|
||||
*/
|
||||
export class Claim {
|
||||
/**
|
||||
* Initialize a Claim object
|
||||
* @constructor
|
||||
* @param {string|object} [uri] - The URI of the identity claim or a JSONified Claim instance
|
||||
* @param {string} [uri] - The URI of the identity claim
|
||||
* @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');
|
||||
* const claimAlt = doip.Claim(JSON.stringify(claim));
|
||||
*/
|
||||
constructor (uri, fingerprint) {
|
||||
// Import JSON
|
||||
if (typeof uri === 'object' && 'claimVersion' in uri) {
|
||||
const data = uri
|
||||
switch (data.claimVersion) {
|
||||
case 1:
|
||||
this._uri = data.uri
|
||||
this._fingerprint = data.fingerprint
|
||||
this._status = data.status
|
||||
this._matches = data.matches
|
||||
this._verification = data.verification
|
||||
break
|
||||
|
||||
default:
|
||||
throw new Error('Invalid claim version')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Verify validity of URI
|
||||
if (uri && !isUri(uri)) {
|
||||
throw new Error('Invalid URI')
|
||||
|
@ -77,11 +57,59 @@ export class Claim {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
this._uri = uri || ''
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
this._fingerprint = fingerprint || ''
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
this._status = ClaimStatus.INIT
|
||||
/**
|
||||
* @type {import('./serviceProvider.js').ServiceProvider[]}
|
||||
*/
|
||||
this._matches = []
|
||||
this._verification = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
}
|
||||
|
||||
get uri () {
|
||||
|
@ -103,13 +131,6 @@ export class Claim {
|
|||
return this._matches
|
||||
}
|
||||
|
||||
get verification () {
|
||||
if (this._status !== ClaimStatus.VERIFIED) {
|
||||
throw new Error('This claim has not yet been verified')
|
||||
}
|
||||
return this._verification
|
||||
}
|
||||
|
||||
set uri (uri) {
|
||||
if (this._status !== ClaimStatus.INIT) {
|
||||
throw new Error(
|
||||
|
@ -143,10 +164,6 @@ export class Claim {
|
|||
throw new Error("Cannot change a claim's matches")
|
||||
}
|
||||
|
||||
set verification (anything) {
|
||||
throw new Error("Cannot change a claim's verification result")
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the claim's URI to candidate definitions
|
||||
* @function
|
||||
|
@ -175,7 +192,7 @@ export class Claim {
|
|||
return true
|
||||
}
|
||||
|
||||
if (candidate.match.isAmbiguous) {
|
||||
if (candidate.claim.uriIsAmbiguous) {
|
||||
// Add to the possible candidates
|
||||
this._matches.push(candidate)
|
||||
} else {
|
||||
|
@ -188,7 +205,7 @@ export class Claim {
|
|||
return true
|
||||
})
|
||||
|
||||
this._status = ClaimStatus.MATCHED
|
||||
this._status = this._matches.length === 0 ? ClaimStatus.NO_MATCHES : ClaimStatus.MATCHED
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -204,7 +221,7 @@ export class Claim {
|
|||
if (this._status === ClaimStatus.INIT) {
|
||||
throw new Error('This claim has not yet been matched')
|
||||
}
|
||||
if (this._status === ClaimStatus.VERIFIED) {
|
||||
if (this._status >= 200) {
|
||||
throw new Error('This claim has already been verified')
|
||||
}
|
||||
if (this._fingerprint.length === 0) {
|
||||
|
@ -216,16 +233,14 @@ export class Claim {
|
|||
|
||||
// If there are no matches
|
||||
if (this._matches.length === 0) {
|
||||
this._verification = {
|
||||
result: false,
|
||||
completed: true,
|
||||
proof: {},
|
||||
errors: ['No matches for claim']
|
||||
}
|
||||
this.status = ClaimStatus.NO_MATCHES
|
||||
}
|
||||
|
||||
// For each match
|
||||
for (let index = 0; index < this._matches.length; index++) {
|
||||
// Continue if a result was already obtained
|
||||
if (this._status >= 200) { continue }
|
||||
|
||||
let claimData = this._matches[index]
|
||||
|
||||
let verificationResult = null
|
||||
|
@ -246,12 +261,12 @@ export class Claim {
|
|||
this._fingerprint
|
||||
)
|
||||
verificationResult.proof = {
|
||||
fetcher: proofData.fetcher,
|
||||
protocol: proofData.fetcher,
|
||||
viaProxy: proofData.viaProxy
|
||||
}
|
||||
|
||||
// Post process the data
|
||||
const def = _data[claimData.serviceprovider.name]
|
||||
const def = _data[claimData.about.id]
|
||||
if (def.functions?.postprocess) {
|
||||
try {
|
||||
({ claimData, proofData } = def.functions.postprocess(claimData, proofData))
|
||||
|
@ -272,25 +287,13 @@ export class Claim {
|
|||
continue
|
||||
}
|
||||
|
||||
if (verificationResult.completed) {
|
||||
// Store the result, keep a single match and stop verifying
|
||||
this._verification = verificationResult
|
||||
if (verificationResult.result) {
|
||||
this._status = verificationResult.proof.viaProxy ? ClaimStatus.VERIFIED_VIA_PROXY : ClaimStatus.VERIFIED
|
||||
this._matches = [claimData]
|
||||
index = this._matches.length
|
||||
}
|
||||
}
|
||||
|
||||
// Fail safe verification result
|
||||
this._verification = Object.keys(this._verification).length > 0
|
||||
? this._verification
|
||||
: {
|
||||
result: false,
|
||||
completed: true,
|
||||
proof: {},
|
||||
errors: []
|
||||
}
|
||||
|
||||
this._status = ClaimStatus.VERIFIED
|
||||
this._status = this._status >= 200 ? this._status : ClaimStatus.NO_PROOF_FOUND
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -307,7 +310,7 @@ export class Claim {
|
|||
if (this._matches.length === 0) {
|
||||
throw new Error('The claim has no matches')
|
||||
}
|
||||
return this._matches.length > 1 || this._matches[0].match.isAmbiguous
|
||||
return this._matches.length > 1 || this._matches[0].claim.uriIsAmbiguous
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -317,13 +320,87 @@ export class Claim {
|
|||
* @returns {object}
|
||||
*/
|
||||
toJSON () {
|
||||
let displayName = this._uri
|
||||
let displayUrl = ''
|
||||
let displayServiceProviderName = ''
|
||||
|
||||
if (this._status >= 200 && this._status < 300) {
|
||||
displayName = this._matches[0].profile.display
|
||||
displayUrl = this._matches[0].profile.uri
|
||||
displayServiceProviderName = this._matches[0].about.id
|
||||
}
|
||||
|
||||
return {
|
||||
claimVersion: 1,
|
||||
claimVersion: 2,
|
||||
uri: this._uri,
|
||||
fingerprint: this._fingerprint,
|
||||
status: this._status,
|
||||
proofs: [this._fingerprint],
|
||||
matches: this._matches,
|
||||
verification: this._verification
|
||||
status: this._status,
|
||||
display: {
|
||||
name: displayName,
|
||||
url: displayUrl,
|
||||
serviceProviderName: displayServiceProviderName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
}
|
||||
|
|
126
src/persona.js
126
src/persona.js
|
@ -13,9 +13,6 @@ 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.
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
import { Claim } from './claim.js'
|
||||
|
||||
/**
|
||||
* A persona with identity claims
|
||||
* @class
|
||||
|
@ -27,28 +24,115 @@ import { Claim } from './claim.js'
|
|||
*/
|
||||
export class Persona {
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string} [description]
|
||||
* @param {Claim[]} [claims]
|
||||
*/
|
||||
constructor (name, description, claims) {
|
||||
* @param {string} name
|
||||
* @param {import('./claim.js').Claim[]} claims
|
||||
*/
|
||||
constructor (name, claims) {
|
||||
/**
|
||||
* Name to be displayed on the profile page
|
||||
* @type {string}
|
||||
* @public
|
||||
*/
|
||||
* Identifier of the persona
|
||||
* @type {string | null}
|
||||
* @public
|
||||
*/
|
||||
this.identifier = null
|
||||
/**
|
||||
* Name to be displayed on the profile page
|
||||
* @type {string}
|
||||
* @public
|
||||
*/
|
||||
this.name = name
|
||||
/**
|
||||
* Description to be displayed on the profile page
|
||||
* @type {string}
|
||||
* @public
|
||||
*/
|
||||
this.description = description
|
||||
* Email address of the persona
|
||||
* @type {string | null}
|
||||
* @public
|
||||
*/
|
||||
this.email = null
|
||||
/**
|
||||
* List of identity claims
|
||||
* @type {Array<Claim>}
|
||||
* @public
|
||||
*/
|
||||
* Description to be displayed on the profile page
|
||||
* @type {string | null}
|
||||
* @public
|
||||
*/
|
||||
this.description = null
|
||||
/**
|
||||
* URL to an avatar image
|
||||
* @type {string | null}
|
||||
* @public
|
||||
*/
|
||||
this.avatarUrl = null
|
||||
/**
|
||||
* List of identity claims
|
||||
* @type {import('./claim.js').Claim[]}
|
||||
* @public
|
||||
*/
|
||||
this.claims = claims
|
||||
/**
|
||||
* Has the persona been revoked
|
||||
* @type {boolean}
|
||||
* @public
|
||||
*/
|
||||
this.isRevoked = false
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {string} identifier
|
||||
*/
|
||||
setIdentifier (identifier) {
|
||||
this.identifier = identifier
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {string} description
|
||||
*/
|
||||
setDescription (description) {
|
||||
this.description = description
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {string} email
|
||||
*/
|
||||
setEmailAddress (email) {
|
||||
this.email = email
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {string} avatarUrl
|
||||
*/
|
||||
setAvatarUrl (avatarUrl) {
|
||||
this.avatarUrl = avatarUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {import('./claim.js').Claim} claim
|
||||
*/
|
||||
addClaim (claim) {
|
||||
this.claims.push(claim)
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
*/
|
||||
revoke () {
|
||||
this.isRevoked = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a JSON representation of the Profile object
|
||||
* @function
|
||||
* @returns {object}
|
||||
*/
|
||||
toJSON () {
|
||||
return {
|
||||
identifier: this.identifier,
|
||||
name: this.name,
|
||||
email: this.email,
|
||||
description: this.description,
|
||||
avatarUrl: this.avatarUrl,
|
||||
isRevoked: this.isRevoked,
|
||||
claims: this.claims.map(x => x.toJSON())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
156
src/profile.js
156
src/profile.js
|
@ -13,13 +13,12 @@ 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.
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
import { Persona } from './persona.js'
|
||||
import { PublicKeyFetchMethod, PublicKeyEncoding, PublicKeyType } from './enums.js'
|
||||
|
||||
/**
|
||||
* A profile of personas with identity claims
|
||||
* @function
|
||||
* @param {Array<Persona>} personas
|
||||
* @param {Array<import('./persona.js').Persona>} personas
|
||||
* @public
|
||||
* @example
|
||||
* const claim = Claim('https://alice.tld', '123');
|
||||
|
@ -28,23 +27,152 @@ import { Persona } from './persona.js'
|
|||
*/
|
||||
export class Profile {
|
||||
/**
|
||||
* Create a new profile
|
||||
* @function
|
||||
* @param {Array<Persona>} personas
|
||||
* Create a new profile
|
||||
* @function
|
||||
* @param {import('./enums.js').ProfileType} profileType
|
||||
* @param {string} identifier
|
||||
* @param {Array<import('./persona.js').Persona>} personas
|
||||
* @public
|
||||
*/
|
||||
constructor (profileType, identifier, personas) {
|
||||
/**
|
||||
* Profile version
|
||||
* @type {number}
|
||||
* @public
|
||||
*/
|
||||
constructor (personas) {
|
||||
this.profileVersion = 2
|
||||
/**
|
||||
* List of personas
|
||||
* @type {Array<Persona>}
|
||||
* @public
|
||||
*/
|
||||
* Profile version
|
||||
* @type {import('./enums.js').ProfileType}
|
||||
* @public
|
||||
*/
|
||||
this.profileType = profileType
|
||||
/**
|
||||
* Identifier of the profile (fingerprint, email address, uri...)
|
||||
* @type {string}
|
||||
* @public
|
||||
*/
|
||||
this.identifier = identifier
|
||||
/**
|
||||
* List of personas
|
||||
* @type {Array<import('./persona.js').Persona>}
|
||||
* @public
|
||||
*/
|
||||
this.personas = personas || []
|
||||
/**
|
||||
* Index of primary persona (to be displayed first or prominently)
|
||||
* @type {Number}
|
||||
* Index of primary persona (to be displayed first or prominently)
|
||||
* @type {number}
|
||||
* @public
|
||||
*/
|
||||
this.primaryPersonaIndex = personas.length > 0 ? 0 : -1
|
||||
/**
|
||||
* The cryptographic key associated with the profile
|
||||
* @property {object}
|
||||
* @public
|
||||
*/
|
||||
this.publicKey = {
|
||||
/**
|
||||
* The type of cryptographic key
|
||||
* @type {PublicKeyType}
|
||||
* @public
|
||||
*/
|
||||
keyType: PublicKeyType.NONE,
|
||||
/**
|
||||
* The encoding of the cryptographic key
|
||||
* @type {PublicKeyEncoding}
|
||||
* @public
|
||||
*/
|
||||
encoding: PublicKeyEncoding.NONE,
|
||||
/**
|
||||
* The raw cryptographic key
|
||||
* @type {string | null}
|
||||
* @public
|
||||
*/
|
||||
encodedKey: null,
|
||||
/**
|
||||
* The raw cryptographic key as object (to be removed during toJSON())
|
||||
* @type {import('openpgp').PublicKey | import('jose').KeyLike | null}
|
||||
* @public
|
||||
*/
|
||||
key: null,
|
||||
/**
|
||||
* Details on how to fetch the public key
|
||||
* @property {object}
|
||||
* @public
|
||||
*/
|
||||
fetch: {
|
||||
/**
|
||||
* The method to fetch the key
|
||||
* @type {PublicKeyFetchMethod}
|
||||
* @public
|
||||
*/
|
||||
this.primaryPersona = -1
|
||||
method: PublicKeyFetchMethod.NONE,
|
||||
/**
|
||||
* The query to fetch the key
|
||||
* @type {string | null}
|
||||
* @public
|
||||
*/
|
||||
query: null,
|
||||
/**
|
||||
* The URL the method eventually resolved to
|
||||
* @type {string | null}
|
||||
* @public
|
||||
*/
|
||||
resolvedUrl: null
|
||||
}
|
||||
}
|
||||
/**
|
||||
* List of verifier URLs
|
||||
* @type {{name: string, url: string}[]}
|
||||
* @public
|
||||
*/
|
||||
this.verifiers = []
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {string} name
|
||||
* @param {string} url
|
||||
*/
|
||||
addVerifier (name, url) {
|
||||
this.verifiers.push({ name, url })
|
||||
}
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {import('openpgp').PublicKey} publicKey
|
||||
*/
|
||||
setOpenPgpPublicKey (publicKey) {}
|
||||
|
||||
/**
|
||||
* @function
|
||||
* @param {import('jose').KeyLike} publicKey
|
||||
*/
|
||||
setJwkPublicKey (publicKey) {}
|
||||
|
||||
/**
|
||||
* Get a JSON representation of the Profile object
|
||||
* @function
|
||||
* @returns {object}
|
||||
*/
|
||||
toJSON () {
|
||||
return {
|
||||
profileVersion: this.profileVersion,
|
||||
profileType: this.profileType,
|
||||
identifier: this.identifier,
|
||||
personas: this.personas.map(x => x.toJSON()),
|
||||
primaryPersonaIndex: this.primaryPersonaIndex,
|
||||
publicKey: {
|
||||
keyType: this.publicKey.keyType,
|
||||
format: this.publicKey.format,
|
||||
keyData: this.publicKey.keyData,
|
||||
fetch: {
|
||||
method: this.publicKey.fetch.method,
|
||||
query: this.publicKey.fetch.query,
|
||||
resolvedUrl: this.publicKey.fetch.resolvedUrl
|
||||
}
|
||||
},
|
||||
verifiers: this.verifiers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue