From 1a463594dcefd8ca5ec34a96c28b66b4121da44f Mon Sep 17 00:00:00 2001 From: Yarmo Mackenbach Date: Thu, 22 Apr 2021 15:14:21 +0200 Subject: [PATCH] Add jsdoc documentation --- jsdoc-lib.json | 25 +++++++++ package.json | 2 +- src/claim.js | 105 ++++++++++++-------------------------- src/defaults.js | 24 +++++++++ src/enums.js | 80 ++++++++++++++++++++++++----- src/fetcher/dns.js | 16 ++++++ src/fetcher/gitlab.js | 19 ++++++- src/fetcher/http.js | 17 +++++++ src/fetcher/index.js | 3 +- src/fetcher/irc.js | 19 +++++++ src/fetcher/matrix.js | 20 ++++++++ src/fetcher/twitter.js | 18 +++++++ src/fetcher/xmpp.js | 21 ++++++++ src/index.js | 2 - src/keys.js | 112 ++++++++++++++++++++++++++++------------- src/proofs.js | 15 ++++++ src/signatures.js | 10 ++++ src/utils.js | 18 +++++++ src/verifications.js | 12 +++++ static/doip.png | Bin 0 -> 12464 bytes 20 files changed, 412 insertions(+), 126 deletions(-) create mode 100644 jsdoc-lib.json create mode 100644 static/doip.png diff --git a/jsdoc-lib.json b/jsdoc-lib.json new file mode 100644 index 0000000..5ea6458 --- /dev/null +++ b/jsdoc-lib.json @@ -0,0 +1,25 @@ +{ + "plugins": ["plugins/markdown"], + "source": { + "include": [ + "./src", + "./README.md" + ] + }, + "recurseDepth": 2, + "templates": { + "default": { + "staticFiles": { + "include": [ + "./static" + ] + } + } + }, + "opts": { + "template": "node_modules/docdash" + }, + "docdash": { + "search": true + } +} \ No newline at end of file diff --git a/package.json b/package.json index 556fbaf..6a9be73 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "license:check": "./node_modules/.bin/license-check-and-add check", "license:add": "./node_modules/.bin/license-check-and-add add", "license:remove": "./node_modules/.bin/license-check-and-add remove", - "docs:lib": "./node_modules/.bin/jsdoc -c jsdoc-lib.json -d ./docs", + "docs:lib": "./node_modules/.bin/jsdoc -c jsdoc-lib.json -r -d ./docs", "test": "./node_modules/.bin/mocha", "proxy": "NODE_ENV=production node ./src/proxy/", "proxy:dev": "NODE_ENV=development ./node_modules/.bin/nodemon ./src/proxy/" diff --git a/src/claim.js b/src/claim.js index d730372..cb26755 100644 --- a/src/claim.js +++ b/src/claim.js @@ -23,20 +23,24 @@ const defaults = require('./defaults') const E = require('./enums') /** - * OpenPGP-based identity claim * @class - * @property {String} uri - The claim's URI - * @property {String} uri - The claim's URI - * @property {String} uri - The claim's URI - * @property {String} uri - The claim's URI - * @property {String} uri - The claim's URI + * @classdesc OpenPGP-based 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 {Array} matches - The claim definitions matched against the URI + * @property {object} result - The result of the verification process */ class Claim { /** * Initialize a Claim object * @constructor - * @param {string} [uri] - The URI of the identity claim - * @param {string} [fingerprint] - The fingerprint of the OpenPGP key + * @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'); */ constructor(uri, fingerprint) { // Import JSON @@ -45,7 +49,7 @@ class Claim { case 1: this._uri = data.uri this._fingerprint = data.fingerprint - this._state = data.state + this._status = data.status this._dataMatches = data.dataMatches this._verification = data.verification break @@ -61,6 +65,7 @@ class Claim { if (uri && !validUrl.isUri(uri)) { throw new Error('Invalid URI') } + // Verify validity of fingerprint if (fingerprint) { try { @@ -72,67 +77,39 @@ class Claim { this._uri = uri ? uri : null this._fingerprint = fingerprint ? fingerprint : null - this._state = E.ClaimState.INIT + this._status = E.ClaimStatus.INIT this._dataMatches = null this._verification = null } - /** - * Get the claim's URI - */ get uri() { return this._uri } - /** - * Get the fingerprint the claim is supposed to acknowledge - * @function - * @returns {string} - */ get fingerprint() { return this._fingerprint } - /** - * Get the current state of the claim's verification process - * @function - * @returns {string} - */ - get state() { - return this._state + get status() { + return this._status } - /** - * Get the candidate claim definitions the URI matched against - * @function - * @returns {object} - */ get matches() { - if (this._state === E.ClaimState.INIT) { + if (this._status === E.ClaimStatus.INIT) { throw new Error('This claim has not yet been matched') } return this._dataMatches } - /** - * Get the result of the verification process - * @function - * @returns {object} - */ get result() { - if (this._state !== E.ClaimState.VERIFIED) { + if (this._status !== E.ClaimStatus.VERIFIED) { throw new Error('This claim has not yet been verified') } return this._verification } - /** - * Set the claim's URI - * @function - * @param {string} uri - The new claim URI - */ set uri(uri) { - if (this._state !== E.ClaimState.INIT) { + if (this._status !== E.ClaimStatus.INIT) { throw new Error( 'Cannot change the URI, this claim has already been matched' ) @@ -147,13 +124,8 @@ class Claim { this._uri = uri } - /** - * Set the claim's fingerprint to verify against - * @function - * @param {string} fingerprint - The new fingerprint - */ set fingerprint(fingerprint) { - if (this._state === E.ClaimState.VERIFIED) { + if (this._status === E.ClaimStatus.VERIFIED) { throw new Error( 'Cannot change the fingerprint, this claim has already been verified' ) @@ -161,29 +133,14 @@ class Claim { this._fingerprint = fingerprint } - /** - * Throw error when attempting to alter the state - * @function - * @param {any} anything - Anything will throw an error - */ - set state(anything) { - throw new Error("Cannot change a claim's state") + set status(anything) { + throw new Error("Cannot change a claim's status") } - /** - * Throw error when attempting to alter the dataMatches - * @function - * @param {any} anything - Anything will throw an error - */ set dataMatches(anything) { throw new Error("Cannot change a claim's dataMatches") } - /** - * Throw error when attempting to alter the verification data - * @function - * @param {any} anything - Anything will throw an error - */ set verification(anything) { throw new Error("Cannot change a claim's verification data") } @@ -193,7 +150,7 @@ class Claim { * @function */ match() { - if (this._state !== E.ClaimState.INIT) { + if (this._status !== E.ClaimStatus.INIT) { throw new Error('This claim was already matched') } if (this._uri === null) { @@ -224,7 +181,7 @@ class Claim { return true }) - this._state = E.ClaimState.MATCHED + this._status = E.ClaimStatus.MATCHED } /** @@ -237,10 +194,10 @@ class Claim { * @param {object} [opts] - Options for proxy, fetchers */ async verify(opts) { - if (this._state === E.ClaimState.INIT) { + if (this._status === E.ClaimStatus.INIT) { throw new Error('This claim has not yet been matched') } - if (this._state === E.ClaimState.VERIFIED) { + if (this._status === E.ClaimStatus.VERIFIED) { throw new Error('This claim has already been verified') } if (this._fingerprint === null) { @@ -298,18 +255,18 @@ class Claim { } } - this._state = E.ClaimState.VERIFIED + this._status = E.ClaimStatus.VERIFIED } /** - * Get the ambiguity of the claim. A claim is only unambiguous if any + * Determine the ambiguity of the claim. A claim is only unambiguous if any * of the candidates is unambiguous. An ambiguous claim should never be * displayed in an user interface when its result is negative. * @function * @returns {boolean} */ isAmbiguous() { - if (this._state === E.ClaimState.INIT) { + if (this._status === E.ClaimStatus.INIT) { throw new Error('The claim has not been matched yet') } if (this._dataMatches.length === 0) { @@ -331,7 +288,7 @@ class Claim { claimVersion: 1, uri: this._uri, fingerprint: this._fingerprint, - state: this._state, + status: this._status, dataMatches: this._dataMatches, verification: this._verification, } diff --git a/src/defaults.js b/src/defaults.js index 8bb4e07..014741a 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -15,6 +15,30 @@ limitations under the License. */ const E = require('./enums') +/** + * Contains default values + * @module defaults + */ + +/** + * The default options used throughout the library + * @constant {object} + * @property {object} proxy - Options related to the proxy + * @property {string|null} proxy.hostname - The hostname of the proxy + * @property {string} proxy.policy - The policy that defines when to use a proxy ({@link module:enums~ProxyPolicy|here}) + * @property {object} claims - Options related to claim verification + * @property {object} claims.irc - Options related to the verification of IRC claims + * @property {string|null} claims.irc.nick - The nick that the library uses to connect to the IRC server + * @property {object} claims.matrix - Options related to the verification of Matrix claims + * @property {string|null} claims.matrix.instance - The server hostname on which the library can log in + * @property {string|null} claims.matrix.accessToken - The access token required to identify the library ({@link https://www.matrix.org/docs/guides/client-server-api|Matrix docs}) + * @property {object} claims.xmpp - Options related to the verification of XMPP claims + * @property {string|null} claims.xmpp.service - The server hostname on which the library can log in + * @property {string|null} claims.xmpp.username - The username used to log in + * @property {string|null} claims.xmpp.password - The password used to log in + * @property {object} claims.twitter - Options related to the verification of Twitter claims + * @property {string|null} claims.twitter.bearerToken - The Twitter API's bearer token ({@link https://developer.twitter.com/en/docs/authentication/oauth-2-0/bearer-tokens|Twitter docs}) + */ const opts = { proxy: { hostname: null, diff --git a/src/enums.js b/src/enums.js index 86fd609..95da108 100644 --- a/src/enums.js +++ b/src/enums.js @@ -13,66 +13,123 @@ 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. */ +/** + * Contains enums + * @module enums + */ + +/** + * The proxy policy that decides how to fetch a proof + * @readonly + * @enum {string} + */ const ProxyPolicy = { + /** Proxy usage decision depends on environment and service provider */ ADAPTIVE: 'adaptive', + /** Always use a proxy */ ALWAYS: 'always', + /** Never use a proxy, skip a verification if a proxy is inevitable */ NEVER: 'never', } Object.freeze(ProxyPolicy) +/** + * Methods for fetching proofs + * @readonly + * @enum {string} + */ const Fetcher = { + /** Basic HTTP requests */ HTTP: 'http', + /** DNS module from Node.js */ DNS: 'dns', + /** IRC module from Node.js */ IRC: 'irc', + /** XMPP module from Node.js */ XMPP: 'xmpp', + /** HTTP request to Matrix API */ MATRIX: 'matrix', + /** HTTP request to Gitlab API */ GITLAB: 'gitlab', + /** HTTP request to Twitter API */ TWITTER: 'twitter', } Object.freeze(Fetcher) +/** + * Levels of access restriction for proof fetching + * @readonly + * @enum {number} + */ const ProofAccess = { + /** Any HTTP request will work */ GENERIC: 0, + /** CORS requests are denied */ NOCORS: 1, + /** HTTP requests must contain API or access tokens */ GRANTED: 2, + /** Not accessible by HTTP request, needs server software */ SERVER: 3, } Object.freeze(ProofAccess) +/** + * Format of proof + * @readonly + * @enum {string} + */ const ProofFormat = { + /** JSON format */ JSON: 'json', + /** Plaintext format */ TEXT: 'text', } Object.freeze(ProofFormat) +/** + * Format of claim + * @readonly + * @enum {number} + */ const ClaimFormat = { + /** `openpgp4fpr:123123123` */ URI: 0, + /** `123123123` */ FINGERPRINT: 1, + /** `[Verifying my OpenPGP key: openpgp4fpr:123123123]` */ MESSAGE: 2, } Object.freeze(ClaimFormat) +/** + * How to find the claim inside the proof's JSON data + * @readonly + * @enum {number} + */ const ClaimRelation = { + /** Claim is somewhere in the JSON field's textual content */ CONTAINS: 0, + /** Claim is equal to the JSON field's textual content */ EQUALS: 1, + /** Claim is equal to an element of the JSON field's array of strings */ ONEOF: 2, } Object.freeze(ClaimRelation) -const VerificationStatus = { - INIT: 0, - INPROGRESS: 1, - FAILED: 2, - COMPLETED: 3, -} -Object.freeze(VerificationStatus) - -const ClaimState = { +/** + * Status of the Claim instance + * @readonly + * @enum {string} + */ +const ClaimStatus = { + /** Claim has been initialized */ INIT: 'init', + /** Claim has matched its URI to candidate claim definitions */ MATCHED: 'matched', + /** Claim has verified one or multiple candidate claim definitions */ VERIFIED: 'verified', } -Object.freeze(ClaimState) +Object.freeze(ClaimStatus) exports.ProxyPolicy = ProxyPolicy exports.Fetcher = Fetcher @@ -80,5 +137,4 @@ exports.ProofAccess = ProofAccess exports.ProofFormat = ProofFormat exports.ClaimFormat = ClaimFormat exports.ClaimRelation = ClaimRelation -exports.VerificationStatus = VerificationStatus -exports.ClaimState = ClaimState +exports.ClaimStatus = ClaimStatus diff --git a/src/fetcher/dns.js b/src/fetcher/dns.js index 8e4cd35..967f7b8 100644 --- a/src/fetcher/dns.js +++ b/src/fetcher/dns.js @@ -15,8 +15,24 @@ limitations under the License. */ const dns = require('dns') +/** + * @module fetcher/dns + */ + +/** + * The request's timeout value in milliseconds + * @constant {number} timeout + */ module.exports.timeout = 5000 +/** + * Execute a fetch request + * @function + * @async + * @param {object} data - Data used in the request + * @param {string} data.domain - The targeted domain + * @returns {object} + */ module.exports.fn = async (data, opts) => { let timeoutHandle const timeoutPromise = new Promise((resolve, reject) => { diff --git a/src/fetcher/gitlab.js b/src/fetcher/gitlab.js index 948a2c3..d4bbcfb 100644 --- a/src/fetcher/gitlab.js +++ b/src/fetcher/gitlab.js @@ -16,8 +16,25 @@ limitations under the License. const bent = require('bent') const req = bent('GET') +/** + * @module fetcher/gitlab + */ + +/** + * The request's timeout value in milliseconds + * @constant {number} timeout + */ module.exports.timeout = 5000 +/** + * Execute a fetch request + * @function + * @async + * @param {object} data - Data used in the request + * @param {string} data.username - The username of the targeted account + * @param {string} data.domain - The domain on which the targeted account is registered + * @returns {object} + */ module.exports.fn = async (data, opts) => { let timeoutHandle const timeoutPromise = new Promise((resolve, reject) => { @@ -45,7 +62,7 @@ module.exports.fn = async (data, opts) => { const project = jsonProject.find((proj) => proj.path === 'gitlab_proof') if (!project) { - reject(`No project at ${spData.proof.uri}`) + reject(`No project found`) } resolve(project) diff --git a/src/fetcher/http.js b/src/fetcher/http.js index 985765f..ca3d125 100644 --- a/src/fetcher/http.js +++ b/src/fetcher/http.js @@ -17,8 +17,25 @@ const bent = require('bent') const req = bent('GET') const E = require('../enums') +/** + * @module fetcher/http + */ + +/** + * The request's timeout value in milliseconds + * @constant {number} timeout + */ module.exports.timeout = 5000 +/** + * Execute a fetch request + * @function + * @async + * @param {object} data - Data used in the request + * @param {string} data.url - The URL pointing at targeted content + * @param {string} data.format - The format of the targeted content + * @returns {object|string} + */ module.exports.fn = async (data, opts) => { let timeoutHandle const timeoutPromise = new Promise((resolve, reject) => { diff --git a/src/fetcher/index.js b/src/fetcher/index.js index ae41e0d..be3604a 100644 --- a/src/fetcher/index.js +++ b/src/fetcher/index.js @@ -13,10 +13,11 @@ 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. */ + exports.dns = require('./dns') exports.gitlab = require('./gitlab') exports.http = require('./http') exports.irc = require('./irc') exports.matrix = require('./matrix') exports.twitter = require('./twitter') -exports.xmpp = require('./xmpp') +exports.xmpp = require('./xmpp') \ No newline at end of file diff --git a/src/fetcher/irc.js b/src/fetcher/irc.js index 7a05834..19bb39c 100644 --- a/src/fetcher/irc.js +++ b/src/fetcher/irc.js @@ -16,8 +16,27 @@ limitations under the License. const irc = require('irc-upd') const validator = require('validator') +/** + * @module fetcher/irc + */ + +/** + * The request's timeout value in milliseconds + * @constant {number} timeout + */ module.exports.timeout = 20000 +/** + * Execute a fetch request + * @function + * @async + * @param {object} data - Data used in the request + * @param {string} data.nick - The nick of the targeted account + * @param {string} data.domain - The domain on which the targeted account is registered + * @param {object} opts - Options used to enable the request + * @param {string} opts.claims.irc.nick - The nick to be used by the library to log in + * @returns {object} + */ module.exports.fn = async (data, opts) => { let timeoutHandle const timeoutPromise = new Promise((resolve, reject) => { diff --git a/src/fetcher/matrix.js b/src/fetcher/matrix.js index c20e681..0295ee8 100644 --- a/src/fetcher/matrix.js +++ b/src/fetcher/matrix.js @@ -17,8 +17,28 @@ const bent = require('bent') const bentReq = bent('GET') const validator = require('validator') +/** + * @module fetcher/matrix + */ + +/** + * The request's timeout value in milliseconds + * @constant {number} timeout + */ module.exports.timeout = 5000 +/** + * Execute a fetch request + * @function + * @async + * @param {object} data - Data used in the request + * @param {string} data.eventId - The identifier of the targeted post + * @param {string} data.roomId - The identifier of the room containing the targeted post + * @param {object} opts - Options used to enable the request + * @param {string} opts.claims.matrix.instance - The server hostname on which the library can log in + * @param {string} opts.claims.matrix.accessToken - The access token required to identify the library ({@link https://www.matrix.org/docs/guides/client-server-api|Matrix docs}) + * @returns {object} + */ module.exports.fn = async (data, opts) => { let timeoutHandle const timeoutPromise = new Promise((resolve, reject) => { diff --git a/src/fetcher/twitter.js b/src/fetcher/twitter.js index 4f16ffa..dc19172 100644 --- a/src/fetcher/twitter.js +++ b/src/fetcher/twitter.js @@ -17,8 +17,26 @@ const bent = require('bent') const bentReq = bent('GET') const validator = require('validator') +/** + * @module fetcher/twitter + */ + +/** + * The request's timeout value in milliseconds + * @constant {number} timeout + */ module.exports.timeout = 5000 +/** + * Execute a fetch request + * @function + * @async + * @param {object} data - Data used in the request + * @param {number|string} data.tweetId - Identifier of the tweet + * @param {object} opts - Options used to enable the request + * @param {string} opts.claims.twitter.bearerToken - The Twitter API's bearer token + * @returns {object} + */ module.exports.fn = async (data, opts) => { let timeoutHandle const timeoutPromise = new Promise((resolve, reject) => { diff --git a/src/fetcher/xmpp.js b/src/fetcher/xmpp.js index 1895044..b0655dc 100644 --- a/src/fetcher/xmpp.js +++ b/src/fetcher/xmpp.js @@ -18,6 +18,14 @@ const { client, xml } = require('@xmpp/client') const debug = require('@xmpp/debug') const validator = require('validator') +/** + * @module fetcher/xmpp + */ + +/** + * The request's timeout value in milliseconds + * @constant {number} timeout + */ module.exports.timeout = 5000 let xmpp = null, @@ -44,6 +52,19 @@ const xmppStart = async (service, username, password) => { }) } +/** + * Execute a fetch request + * @function + * @async + * @param {object} data - Data used in the request + * @param {string} data.id - The identifier of the targeted account + * @param {string} data.field - The vCard field to return (should be "note") + * @param {object} opts - Options used to enable the request + * @param {string} opts.claims.xmpp.service - The server hostname on which the library can log in + * @param {string} opts.claims.xmpp.username - The username used to log in + * @param {string} opts.claims.xmpp.password - The password used to log in + * @returns {object} + */ module.exports.fn = async (data, opts) => { let timeoutHandle const timeoutPromise = new Promise((resolve, reject) => { diff --git a/src/index.js b/src/index.js index a46a564..e2e71ca 100644 --- a/src/index.js +++ b/src/index.js @@ -16,7 +16,6 @@ limitations under the License. const Claim = require('./claim') const claimDefinitions = require('./claimDefinitions') const proofs = require('./proofs') -const verifications = require('./verifications') const keys = require('./keys') const signatures = require('./signatures') const enums = require('./enums') @@ -26,7 +25,6 @@ const utils = require('./utils') exports.Claim = Claim exports.claimDefinitions = claimDefinitions exports.proofs = proofs -exports.verifications = verifications exports.keys = keys exports.signatures = signatures exports.enums = enums diff --git a/src/keys.js b/src/keys.js index aa941a6..b3fb0b4 100644 --- a/src/keys.js +++ b/src/keys.js @@ -19,10 +19,25 @@ const validUrl = require('valid-url') const openpgp = require('openpgp') const Claim = require('./claim') -const fetchHKP = (identifier, keyserverBaseUrl) => { +/** + * Functions related to the fetching and handling of keys + * @module keys + */ + +/** + * Fetch a public key using keyservers + * @function + * @param {string} identifier - Fingerprint or email address + * @param {string} [keyserverDomain=keys.openpgp.org] - Domain of the keyserver + * @returns {openpgp.key.Key} + * @example + * const key1 = doip.keys.fetchHKP('alice@domain.tld'); + * const key2 = doip.keys.fetchHKP('123abc123abc'); + */ + exports.fetchHKP = (identifier, keyserverDomain) => { return new Promise(async (resolve, reject) => { - keyserverBaseUrl = keyserverBaseUrl - ? `https://${keyserverBaseUrl}` + const keyserverBaseUrl = keyserverDomain + ? `https://${keyserverDomain}` : 'https://keys.openpgp.org' const hkp = new openpgp.HKP(keyserverBaseUrl) @@ -51,7 +66,15 @@ const fetchHKP = (identifier, keyserverBaseUrl) => { }) } -const fetchWKD = (identifier) => { +/** + * Fetch a public key using Web Key Directory + * @function + * @param {string} identifier - Identifier of format 'username@domain.tld` + * @returns {openpgp.key.Key} + * @example + * const key = doip.keys.fetchWKD('alice@domain.tld'); + */ +exports.fetchWKD = (identifier) => { return new Promise(async (resolve, reject) => { const wkd = new openpgp.WKD() const lookupOpts = { @@ -75,7 +98,16 @@ const fetchWKD = (identifier) => { }) } -const fetchKeybase = (username, fingerprint) => { +/** + * Fetch a public key from Keybase + * @function + * @param {string} username - Keybase username + * @param {string} fingerprint - Fingerprint of key + * @returns {openpgp.key.Key} + * @example + * const key = doip.keys.fetchKeybase('alice', '123abc123abc'); + */ +exports.fetchKeybase = (username, fingerprint) => { return new Promise(async (resolve, reject) => { const keyLink = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}` try { @@ -107,7 +139,21 @@ const fetchKeybase = (username, fingerprint) => { }) } -const fetchPlaintext = (rawKeyContent) => { +/** + * Get a public key from plaintext data + * @function + * @param {string} rawKeyContent - Plaintext ASCII-formatted public key data + * @returns {openpgp.key.Key} + * @example + * const plainkey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + * + * mQINBF0mIsIBEADacleiyiV+z6FIunvLWrO6ZETxGNVpqM+WbBQKdW1BVrJBBolg + * [...] + * =6lib + * -----END PGP PUBLIC KEY BLOCK-----` + * const key = doip.keys.fetchPlaintext(plainkey); + */ +exports.fetchPlaintext = (rawKeyContent) => { return new Promise(async (resolve, reject) => { const publicKey = (await openpgp.key.readArmored(rawKeyContent)).keys[0] @@ -115,23 +161,17 @@ const fetchPlaintext = (rawKeyContent) => { }) } -const fetchSignature = (rawSignatureContent, keyserverBaseUrl) => { - return new Promise(async (resolve, reject) => { - let sig = await openpgp.signature.readArmored(rawSignatureContent) - if ('compressed' in sig.packets[0]) { - sig = sig.packets[0] - let sigContent = await openpgp.stream.readToEnd( - await sig.packets[1].getText() - ) - } - const sigUserId = sig.packets[0].signersUserId - const sigKeyId = await sig.packets[0].issuerKeyId.toHex() - - resolve(fetchHKP(sigUserId ? sigUserId : sigKeyId, keyserverBaseUrl)) - }) -} - -const fetchURI = (uri) => { +/** + * Fetch a public key using an URI + * @function + * @param {string} uri - URI that defines the location of the key + * @returns {openpgp.key.Key} + * @example + * const key1 = doip.keys.fetchURI('hkp:alice@domain.tld'); + * const key2 = doip.keys.fetchURI('hkp:123abc123abc'); + * const key3 = doip.keys.fetchURI('wkd:alice@domain.tld'); + */ +exports.fetchURI = (uri) => { return new Promise(async (resolve, reject) => { if (!validUrl.isUri(uri)) { reject('Invalid URI') @@ -163,7 +203,19 @@ const fetchURI = (uri) => { }) } -const process = (publicKey) => { +/** + * Process a public key to get user data and claims + * @function + * @param {openpgp.key.Key} publicKey - The public key to process + * @returns {object} + * @example + * const key = doip.keys.fetchURI('hkp:alice@domain.tld'); + * const data = doip.keys.process(key); + * data.users[0].claims.forEach(claim => { + * console.log(claim.uri); + * }); + */ +exports.process = (publicKey) => { return new Promise(async (resolve, reject) => { if (!publicKey || !(publicKey instanceof openpgp.key.Key)) { reject('Invalid public key') @@ -210,14 +262,4 @@ const process = (publicKey) => { }, }) }) -} - -exports.fetch = { - uri: fetchURI, - hkp: fetchHKP, - wkd: fetchWKD, - keybase: fetchKeybase, - plaintext: fetchPlaintext, - signature: fetchSignature, -} -exports.process = process +} \ No newline at end of file diff --git a/src/proofs.js b/src/proofs.js index 524db5e..04c5b05 100644 --- a/src/proofs.js +++ b/src/proofs.js @@ -18,6 +18,21 @@ const fetcher = require('./fetcher') const utils = require('./utils') const E = require('./enums') +/** + * @module proofs + */ + +/** + * Delegate the proof request to the correct fetcher. + * This method uses the current environment (browser/node), certain values from + * the `data` parameter and the proxy policy set in the `opts` parameter to + * choose the right approach to fetch the proof. An error will be thrown if no + * approach is possible. + * @async + * @param {object} data - Data from a claim definition + * @param {object} opts - Options to enable the request + * @returns {Promise} + */ const fetch = (data, opts) => { switch (data.proof.request.fetcher) { case E.Fetcher.HTTP: diff --git a/src/signatures.js b/src/signatures.js index a2ccba1..987c432 100644 --- a/src/signatures.js +++ b/src/signatures.js @@ -17,6 +17,16 @@ const openpgp = require('openpgp') const Claim = require('./claim') const keys = require('./keys') +/** + * @module signatures + */ + +/** + * Extract data from a signature and fetch the associated key + * @async + * @param {string} signature - The plaintext signature to process + * @returns {Promise} + */ const process = (signature) => { return new Promise(async (resolve, reject) => { let sigData, diff --git a/src/utils.js b/src/utils.js index 6374bcf..276a79a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -16,6 +16,18 @@ limitations under the License. const validator = require('validator') const E = require('./enums') +/** + * @module utils + */ + +/** + * Generate an URL to request data from a proxy server + * @param {string} type - The name of the fetcher the proxy must use + * @param {object} data - The data the proxy must provide to the fetcher + * @param {object} opts - Options to enable the request + * @param {object} opts.proxy.hostname - The hostname of the proxy server + * @returns {string} + */ const generateProxyURL = (type, data, opts) => { try { validator.isFQDN(opts.proxy.hostname) @@ -34,6 +46,12 @@ const generateProxyURL = (type, data, opts) => { )}` } +/** + * Generate the string that must be found in the proof to verify a claim + * @param {string} fingerprint - The fingerprint of the claim + * @param {number} format - The claim's format (see {@link module:enums~ClaimFormat|enums.ClaimFormat}) + * @returns {string} + */ const generateClaim = (fingerprint, format) => { switch (format) { case E.ClaimFormat.URI: diff --git a/src/verifications.js b/src/verifications.js index eec4625..812db1a 100644 --- a/src/verifications.js +++ b/src/verifications.js @@ -16,6 +16,11 @@ limitations under the License. const utils = require('./utils') const E = require('./enums') +/** + * @module verifications + * @ignore + */ + const runJSON = (proofData, checkPath, checkClaim, checkRelation) => { let re @@ -70,6 +75,13 @@ const runJSON = (proofData, checkPath, checkClaim, checkRelation) => { ) } +/** + * Run the verification by finding the formatted fingerprint in the proof + * @param {object} proofData - The proof data + * @param {object} claimData - The claim data + * @param {string} fingerprint - The fingerprint + * @returns {object} + */ const run = (proofData, claimData, fingerprint) => { let res = { result: false, diff --git a/static/doip.png b/static/doip.png new file mode 100644 index 0000000000000000000000000000000000000000..18be00590d5d4011de63e75732239d40cc2f3bb7 GIT binary patch literal 12464 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 literal 0 HcmV?d00001