UNPKG

tim

Version:

Tradle's P2P OTR-based chat with plaintext and structured messages, and the ability to externalize messages onto a blockchain (currently bitcoin's)

297 lines (242 loc) 7.4 kB
var debug = require('debug')('identityDB') var typeforce = require('typeforce') var extend = require('xtend') var map = require('map-stream') var levelup = require('levelup') var Q = require('q') var constants = require('@tradle/constants') var TYPE = constants.TYPE var IDENTITY_TYPE = constants.TYPES.IDENTITY var ROOT_HASH = constants.ROOT_HASH var CUR_HASH = constants.CUR_HASH var Simple = require('logbase').Simple var rebuf = require('./rebufEncoding') var EventType = require('./eventType') var DEBUG = require('./debug') var ENTRY_TIMEOUT = DEBUG ? false : 5000 var prefixes = { fingerprint: 'f' } prefixes[CUR_HASH] = CUR_HASH prefixes[ROOT_HASH] = ROOT_HASH module.exports = function mkIdentityDB (path, options) { typeforce('String', path) typeforce({ leveldown: 'Function', log: 'Log', keeper: 'Object' }, options) var log = options.log var db = levelup(path, { db: options.leveldown, valueEncoding: rebuf }) db = Simple({ db: db, log: log, process: processEntry, timeout: typeof options.timeout === 'undefined' ? ENTRY_TIMEOUT : options.timeout, autostart: options.autostart, }) var topics = [ EventType.chain.readSuccess, EventType.misc.addIdentity ] var keeper = options.keeper var createReadStream = db.createReadStream db.createReadStream = function (options) { options = options || {} return createReadStream.call(db, extend(options, { start: prefixes[ROOT_HASH], end: prefixes[ROOT_HASH] + '\xff' })) .pipe(map(function (data, cb) { if (options.keys !== false) { var key = options.values === false ? data : data.key key = unprefixKey(ROOT_HASH, key) if (options.values === false) data = key else data.key = key } if (options.values === false) return cb(null, data) var id = options.keys === false ? data : data.value getContents(id, function (err, contents) { if (err) return cb(err) var ret = options.keys === false ? contents : { key: data.key, value: contents } cb(null, ret) }) })) } // db.createKeyStream = function (options) { // return db.createReadStream(extend({ values: false }, options)) // } // db.createValueStream = function (options) { // return db.createReadStream(extend({ keys: false }, options)) // } db.query = function (query, cb) { // by one and only one prop if (Object.keys(query).length !== 1 || !(query.fingerprint || query[ROOT_HASH] || query[CUR_HASH])) { return process.nextTick(function () { cb(new Error('specify "fingerprint", ' + ROOT_HASH + ' OR ' + CUR_HASH)) }) } if (query.fingerprint) { db.byFingerprint(query.fingerprint, cb) } else if (query[ROOT_HASH]) { db.byRootHash(query[ROOT_HASH], cb) } else if (query[CUR_HASH]) { db.byCurHash(query[CUR_HASH], cb) } } db.byFingerprint = function (fingerprint, cb) { db.rootHashByFingerprint(fingerprint, function (err, rootHash) { if (err) return cb(err) return db.byRootHash(rootHash, cb) }) } db.rootHashByFingerprint = db.liveOnly(function (fingerprint, cb) { db.get(prefixKey('fingerprint', fingerprint), cb) }) db.byRootHash = db.liveOnly(function (rootHash, cb) { db.get(prefixKey(ROOT_HASH, rootHash), function (err, id) { if (err) return cb(err) getContents(id, cb) }) }) db.byCurHash = db.liveOnly(function (curHash, cb) { db.get(prefixKey(CUR_HASH, curHash), function (err, id) { if (err) return cb(err) getContents(id, cb) }) }) return db function getContents (id, cb) { log.get(id, function (err, entry) { if (err) return cb(err) var curHash = entry.get(CUR_HASH) keeper.getOne(curHash) .then(function (buf) { var ret = { identity: toJSON(buf) } ret[ROOT_HASH] = entry.get(ROOT_HASH) ret[CUR_HASH] = curHash cb(null, ret) }) .catch(cb) .done() }) } function processEntry (entry, cb) { if (topics.indexOf(entry.get('type')) === -1) return cb() if (entry.get(TYPE) !== IDENTITY_TYPE) { return cb() } var rootHash = entry.get(ROOT_HASH) if (!rootHash) throw new Error('missing root hash ("' + ROOT_HASH + '")') var curHash = entry.get(CUR_HASH) if (!curHash) throw new Error('missing current hash ("' + CUR_HASH + '")') db.get(prefixKey(CUR_HASH, curHash), function (err, val) { if (err){ // TODO: handle this better if (!err.notFound) { debug('database choked on get: ' + JSON.stringify(err)) } save() } else { cb() } }) function save () { return keeper.getOne(curHash) .then(doSave, function (err) { if (err instanceof TypeError || err instanceof ReferenceError) { throw err } debug('unable to get identity from keeper', curHash, err) cb() }) } function doSave (buf) { var identityJSON = toJSON(buf) var batch = identityJSON.pubkeys.map(function (k) { return { type: 'put', key: prefixKey('fingerprint', k.fingerprint), value: rootHash } }) var results = batch.map(function (item) { return null }) batch.forEach(function (item, i) { db.get(item.key, function (err, result) { if (result) results[i] = result next() }) }) var togo = batch.length function next () { if (--togo) return var conflict = results.some(function (r) { // only allow iterations of identity // by original identity publisher return r && r !== rootHash }) if (conflict) { debug('ignoring identity that presents keys already presented by another identity') return cb() } batch.push({ type: 'put', key: prefixKey(ROOT_HASH, rootHash), value: entry.id() }, { type: 'put', key: prefixKey(CUR_HASH, curHash), value: entry.id() }) debug(db.name, 'stored identity', curHash) cb(batch) } } } } function toJSON (buf) { if (Buffer.isBuffer(buf)) buf = buf.toString() if (typeof buf === 'string') buf = JSON.parse(buf) return buf } function prefixKey (type, key) { if (!(type in prefixes)) throw new Error('no such type: ' + type) return prefixes[type] + '!' + key } function unprefixKey (type, key) { if (!(type in prefixes)) throw new Error('no such type: ' + type) if (key.indexOf(type) !== 0) throw new Error('key has wrong prefix') return key.slice(type.length + 1) } // function logify (cb, errsOnly) { // return function (err) { // if (errsOnly) { // if (err) debug(err) // } else { // debug(arguments) // } // cb(arguments[0], arguments[1], arguments[2], arguments[3]) // } // } // function liveOnly (db, fn, ctx) { // return function () { // var args = arguments // db.onLive(function () { // return fn.apply(ctx, args) // }) // } // }