UNPKG

identifi-lib

Version:

Basic tools for reading and writing Identifi messages and identities.

540 lines (436 loc) 17.4 kB
'use strict'; exports.__esModule = true; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var _util = require('./util'); var _util2 = _interopRequireDefault(_util); var _attribute = require('./attribute'); var _attribute2 = _interopRequireDefault(_attribute); var _key = require('./key'); var _key2 = _interopRequireDefault(_key); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /*jshint unused: false */ 'use strict'; var errorMsg = 'Invalid Identifi message:'; var ValidationError = function (_Error) { _inherits(ValidationError, _Error); function ValidationError() { _classCallCheck(this, ValidationError); return _possibleConstructorReturn(this, _Error.apply(this, arguments)); } return ValidationError; }(Error); /** * Identifi message: an object that has an author, recipient, signer, type, timestamp, context and optionally other fields. * * On Identifi, signer and author can be different entities. This enables the crawling of content * from existing datasets. That makes Identifi an useful search tool even with no initial userbase. * * Messages are serialized as JSON Web Signatures. */ var Message = function () { /** * Creates a message from the param object that must contain at least the mandatory fields: author, recipient, type, context and timestamp. You can use createRating() and createVerification() to automatically populate some of these fields and optionally sign the message. * @param signedData */ function Message(obj) { _classCallCheck(this, Message); if (obj.signedData) { this.signedData = obj.signedData; } if (obj.pubKey) { this.pubKey = obj.pubKey; } if (obj.ipfsUri) { this.ipfsUri = obj.ipfsUri; } if (obj.sig) { if (typeof obj.sig !== 'string') { throw new ValidationError('Message signature must be a string'); } this.sig = obj.sig; this.getHash(); } this._validate(); } Message._getArray = function _getArray(authorOrRecipient) { var arr = []; var keys = Object.keys(authorOrRecipient); for (var i = 0; i < keys.length; i++) { var type = keys[i]; var value = authorOrRecipient[keys[i]]; if (typeof value === 'string') { arr.push(new _attribute2.default(type, value)); } else { // array for (var j = 0; j < value.length; j++) { var elementValue = value[j]; arr.push(new _attribute2.default(type, elementValue)); } } } return arr; }; Message._getIterable = function _getIterable(authorOrRecipient) { var _ref; return _ref = {}, _ref[Symbol.iterator] = /*#__PURE__*/regeneratorRuntime.mark(function _callee() { var keys, i, type, value, j, elementValue; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: keys = Object.keys(authorOrRecipient); i = 0; case 2: if (!(i < keys.length)) { _context.next = 21; break; } type = keys[i]; value = authorOrRecipient[keys[i]]; if (!(typeof value === 'string')) { _context.next = 10; break; } _context.next = 8; return new _attribute2.default(type, value); case 8: _context.next = 18; break; case 10: j = 0; case 11: if (!(j < value.length)) { _context.next = 18; break; } elementValue = value[j]; _context.next = 15; return new _attribute2.default(type, elementValue); case 15: j++; _context.next = 11; break; case 18: i++; _context.next = 2; break; case 21: case 'end': return _context.stop(); } } }, _callee, this); }), _ref; }; /** * @returns {object} Javascript iterator over author attributes */ Message.prototype.getAuthorIterable = function getAuthorIterable() { return Message._getIterable(this.signedData.author); }; /** * @returns {object} Javascript iterator over recipient attributes */ Message.prototype.getRecipientIterable = function getRecipientIterable() { return Message._getIterable(this.signedData.recipient); }; /** * @returns {array} Array containing author attributes */ Message.prototype.getAuthorArray = function getAuthorArray() { return Message._getArray(this.signedData.author); }; /** * @returns {array} Array containing recipient attributes */ Message.prototype.getRecipientArray = function getRecipientArray() { return Message._getArray(this.signedData.recipient); }; /** * @returns {string} Message signer keyID, i.e. base64 hash of public key */ Message.prototype.getSignerKeyID = function getSignerKeyID() { return this.pubKey; // hack until gun supports keyID lookups //return util.getHash(this.pubKey); }; Message.prototype._validate = function _validate() { if (!this.signedData) { throw new ValidationError(errorMsg + ' Missing signedData'); } if (typeof this.signedData !== 'object') { throw new ValidationError(errorMsg + ' signedData must be an object'); } var d = this.signedData; if (!d.type) { throw new ValidationError(errorMsg + ' Missing type definition'); } if (!d.author) { throw new ValidationError(errorMsg + ' Missing author'); } if (typeof d.author !== 'object') { throw new ValidationError(errorMsg + ' Author must be object'); } if (Array.isArray(d.author)) { throw new ValidationError(errorMsg + ' Author must not be an array'); } if (Object.keys(d.author).length === 0) { throw new ValidationError(errorMsg + ' Author empty'); } if (this.pubKey) { this.signerKeyHash = this.getSignerKeyID(); } for (var attr in d.author) { var t = _typeof(d.author[attr]); if (t !== 'string') { if (Array.isArray(d.author[attr])) { for (var i = 0; i < d.author[attr].length; i++) { if (typeof d.author[attr][i] !== 'string') { throw new ValidationError(errorMsg + ' Author attribute must be string, got ' + attr + ': [' + d.author[attr][i] + ']'); } if (d.author[attr][i].length === 0) { throw new ValidationError(errorMsg + ' author ' + attr + ' in array[' + i + '] is empty'); } } } else { throw new ValidationError(errorMsg + ' Author attribute must be string or array, got ' + attr + ': ' + d.author[attr]); } } if (attr === 'keyID') { if (t !== 'string') { throw new ValidationError(errorMsg + ' Author keyID must be string, got ' + t); } if (this.signerKeyHash && d.author[attr] !== this.signerKeyHash) { throw new ValidationError(errorMsg + ' If message has a keyID author, it must be signed by the same key'); } } } if (!d.recipient) { throw new ValidationError(errorMsg + ' Missing recipient'); } if (typeof d.recipient !== 'object') { throw new ValidationError(errorMsg + ' Recipient must be object'); } if (Array.isArray(d.recipient)) { throw new ValidationError(errorMsg + ' Recipient must not be an array'); } if (Object.keys(d.recipient).length === 0) { throw new ValidationError(errorMsg + ' Recipient empty'); } for (var _attr in d.recipient) { var _t = _typeof(d.recipient[_attr]); if (_t !== 'string') { if (Array.isArray(d.recipient[_attr])) { for (var _i = 0; _i < d.recipient[_attr].length; _i++) { if (typeof d.recipient[_attr][_i] !== 'string') { throw new ValidationError(errorMsg + ' Recipient attribute must be string, got ' + _attr + ': [' + d.recipient[_attr][_i] + ']'); } if (d.recipient[_attr][_i].length === 0) { throw new ValidationError(errorMsg + ' recipient ' + _attr + ' in array[' + _i + '] is empty'); } } } else { throw new ValidationError(errorMsg + ' Recipient attribute must be string or array, got ' + _attr + ': ' + d.recipient[_attr]); } } } if (!d.timestamp) { throw new ValidationError(errorMsg + ' Missing timestamp'); } if (!Date.parse(d.timestamp)) { throw new ValidationError(errorMsg + ' Invalid timestamp'); } if (d.type === 'rating') { if (isNaN(d.rating)) { throw new ValidationError(errorMsg + ' Invalid rating'); } if (isNaN(d.maxRating)) { throw new ValidationError(errorMsg + ' Invalid maxRating'); } if (isNaN(d.minRating)) { throw new ValidationError(errorMsg + ' Invalid minRating'); } if (d.rating > d.maxRating) { throw new ValidationError(errorMsg + ' Rating is above maxRating'); } if (d.rating < d.minRating) { throw new ValidationError(errorMsg + ' Rating is below minRating'); } if (typeof d.context !== 'string' || !d.context.length) { throw new ValidationError(errorMsg + ' Rating messages must have a context field'); } } if (d.type === 'verification' || d.type === 'unverification') { if (d.recipient.length < 2) { throw new ValidationError(errorMsg + ' At least 2 recipient attributes are needed for a connection / disconnection. Got: ' + d.recipient); } } return true; }; /** * @returns {boolean} true if message has a positive rating */ Message.prototype.isPositive = function isPositive() { return this.signedData.type === 'rating' && this.signedData.rating > (this.signedData.maxRating + this.signedData.minRating) / 2; }; /** * @returns {boolean} true if message has a negative rating */ Message.prototype.isNegative = function isNegative() { return this.signedData.type === 'rating' && this.signedData.rating < (this.signedData.maxRating + this.signedData.minRating) / 2; }; /** * @returns {boolean} true if message has a neutral rating */ Message.prototype.isNeutral = function isNeutral() { return this.signedData.type === 'rating' && this.signedData.rating === (this.signedData.maxRating + this.signedData.minRating) / 2; }; /** * @param {Object} key Gun.SEA keypair to sign the message with */ Message.prototype.sign = async function sign(key) { this.sig = await _key2.default.sign(this.signedData, key); this.pubKey = key.pub; this.getHash(); return true; }; /** * Create an identifi message. Message timestamp and context (identifi) are automatically set. If signingKey is specified and author omitted, signingKey will be used as author. * @param {Object} signedData message data object including author, recipient and other possible attributes * @param {Object} signingKey optionally, you can set the key to sign the message with * @returns Promise{Message} Identifi message */ Message.create = async function create(signedData, signingKey) { if (!signedData.author && signingKey) { signedData.author = { keyID: _key2.default.getId(signingKey) }; } signedData.timestamp = signedData.timestamp || new Date().toISOString(); signedData.context = signedData.context || 'identifi'; var m = new Message({ signedData: signedData }); if (signingKey) { await m.sign(signingKey); } return m; }; /** * Create an Identifi verification message. Message signedData's type, timestamp and context (identifi) are automatically set. Recipient must be set. If signingKey is specified and author omitted, signingKey will be used as author. * @returns Promise{Object} message object promise */ Message.createVerification = function createVerification(signedData, signingKey) { signedData.type = 'verification'; return Message.create(signedData, signingKey); }; /** * Create an Identifi rating message. Message signedData's type, maxRating, minRating, timestamp and context are set automatically. Recipient and rating must be set. If signingKey is specified and author omitted, signingKey will be used as author. * @returns Promise{Object} message object promise */ Message.createRating = function createRating(signedData, signingKey) { signedData.type = 'rating'; signedData.maxRating = signedData.maxRating || 10; signedData.minRating = signedData.minRating || -10; return Message.create(signedData, signingKey); }; /** * @param {Index} index index to look up the message author from * @returns {Identity} message author identity */ Message.prototype.getAuthor = function getAuthor(index) { for (var _iterator = this.getAuthorIterable(), _isArray = Array.isArray(_iterator), _i2 = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { var _ref2; if (_isArray) { if (_i2 >= _iterator.length) break; _ref2 = _iterator[_i2++]; } else { _i2 = _iterator.next(); if (_i2.done) break; _ref2 = _i2.value; } var a = _ref2; if (a.isUniqueType()) { return index.get(a); } } }; /** * @param {Index} index index to look up the message recipient from * @returns {Identity} message recipient identity */ Message.prototype.getRecipient = function getRecipient(index) { for (var _iterator2 = this.getRecipientIterable(), _isArray2 = Array.isArray(_iterator2), _i3 = 0, _iterator2 = _isArray2 ? _iterator2 : _iterator2[Symbol.iterator]();;) { var _ref3; if (_isArray2) { if (_i3 >= _iterator2.length) break; _ref3 = _iterator2[_i3++]; } else { _i3 = _iterator2.next(); if (_i3.done) break; _ref3 = _i3.value; } var a = _ref3; if (a.isUniqueType()) { return index.get(a); } } }; /** * @returns {string} base64 hash of message */ Message.prototype.getHash = function getHash() { if (this.sig && !this.hash) { this.hash = _util2.default.getHash(this.sig); } return this.hash; }; Message.fromSig = async function fromSig(obj) { var signedData = await _key2.default.verify(obj.sig, obj.pubKey); var o = { signedData: signedData, sig: obj.sig, pubKey: obj.pubKey }; return new Message(o); }; /** * @return {boolean} true if message signature is valid. Otherwise throws ValidationError. */ Message.prototype.verify = async function verify() { if (!this.pubKey) { throw new ValidationError(errorMsg + ' Message has no .pubKey'); } if (!this.sig) { throw new ValidationError(errorMsg + ' Message has no .sig'); } this.signedData = await _key2.default.verify(this.sig, this.pubKey); if (!this.signedData) { throw new ValidationError(errorMsg + ' Invalid signature'); } if (this.hash) { if (this.hash !== _util2.default.getHash(this.sig)) { throw new ValidationError(errorMsg + ' Invalid message hash'); } } else { this.getHash(); } return true; }; Message.prototype.saveToIpfs = async function saveToIpfs(ipfs) { var s = this.toString(); var r = await ipfs.add(ipfs.types.Buffer.from(s)); if (r.length) { this.ipfsUri = r[0].hash; } return this.ipfsUri; }; Message.loadFromIpfs = async function loadFromIpfs(ipfs, uri) { var f = await ipfs.cat(uri); var s = ipfs.types.Buffer.from(f).toString('utf8'); return Message.fromString(s); }; Message.prototype.toString = function toString() { return JSON.stringify({ sig: this.sig, pubKey: this.pubKey }); }; Message.fromString = function fromString(s) { return Message.fromSig(JSON.parse(s)); }; return Message; }(); exports.default = Message; module.exports = exports['default'];