UNPKG

@coboxcoop/space

Version:

a peer-to-peer private and encrypted space

240 lines (205 loc) 6.61 kB
const path = require('path') const through = require('through2') const debug = require('@coboxcoop/logger')('@coboxcoop/space') const assert = require('assert') const maybe = require('call-me-maybe') const crypto = require('@coboxcoop/crypto') const { Replicator } = require('@coboxcoop/replicator') const constants = require('@coboxcoop/constants') const { loadKey, saveKey } = require('@coboxcoop/keys') const { keyIds } = constants const { log: LOG_ID } = keyIds const { encodings } = require('@coboxcoop/schemas') const Kappa = require('kappa-core') const Log = require('@coboxcoop/log') const Drive = require('@coboxcoop/drive') const State = require('./lib/handlers/state') const { setupLevel } = require('@coboxcoop/replicator/lib/level') const PeerAbout = encodings.peer.about const SpaceAbout = encodings.space.about const ENC_KEY = 'encryption_key' class Space extends Replicator { /** * Create a cobox space * @constructor */ constructor (storage, address, identity, opts = {}) { var encryptionKey = opts.encryptionKey if (opts.encryptionKey) delete opts.encryptionKey super(storage, address, identity, opts) var key = loadKey(this.storage, ENC_KEY) || encryptionKey encryptionKey = crypto.encryptionKey(key) assert(crypto.isKey(encryptionKey), `invalid: ${ENC_KEY}`) saveKey(this.storage, ENC_KEY, encryptionKey) this._deriveKeyPair = opts.deriveKeyPair || randomKeyPair this.core = new Kappa() this.identity = identity var encryptionEncoder = crypto.encoder(encryptionKey, { valueEncoding: 'binary', nonce: encryptionKey.slice(0, crypto.encoder.NONCEBYTES) }) this._initFeeds({ valueEncoding: encryptionEncoder }) } /** * You should probably call this when you create a space ;) * @param {Space} space * @param {Object} { identity: publicKey<Buffer|String>, name<String> } */ static async onCreate (space, { identity }) { if (identity) { const peerAbout = { type: 'peer/about', version: '1.0.0', timestamp: Date.now(), author: hex(identity.publicKey), content: { name: identity.name } } debug({ msg: `publishing identity message to space log`, name: space.name, peerAbout }) await space.log.publish(peerAbout, { valueEncoding: PeerAbout }) } const spaceAbout = { type: 'space/about', version: '1.0.0', timestamp: Date.now(), author: hex(identity.publicKey), content: { name: space.name } } debug({ msg: `publishing space name message to space log`, spaceAbout }) await space.log.publish(spaceAbout, { valueEncoding: SpaceAbout }) return true } ready (callback) { return maybe(callback, new Promise((resolve, reject) => { super.ready((err) => { if (err) return reject(err) this.open((err) => { if (err) return reject(err) this.log.ready((err) => { if (err) return reject(err) this.drive.ready((err) => { if (err) return reject(err) this.state.ready((err) => { if (err) return reject(err) this.core.ready((err) => { if (err) return reject(err) return resolve() }) }) }) }) }) }) })) } destroy (callback) { return maybe(callback, new Promise((resolve, reject) => { this.drive._gracefulUnmount((err) => { if (err) return reject(err) super.destroy().then(resolve).catch(reject) }) })) } createLastSyncStream () { const address = hex(this.address) return this.lastSyncDb .createLiveStream() .pipe(through.obj(function (msg, _, next) { if (msg.sync) return next() next(null, { type: 'space/last-sync', address: address, data: { peerId: msg.key, lastSyncAt: msg.value } }) })) } createLogStream () { var self = this let query = [{ $filter: { value: { timestamp: { $gt: 0 } } } }] return this.log .read({ query, live: true, old: true }) .pipe(through.obj(function (msg, _, next) { if (msg.sync) return next() this.push({ resourceType: 'SPACE', feedId: msg.key, address: hex(self.address), data: msg.value }) next() })) } // ---------------------------------------------------------------- // _open (callback) { super._open((err) => { if (err) return callback(err) this.logDb = setupLevel(path.join(this.storage, 'views', 'log')) this.driveDb = setupLevel(path.join(this.storage, 'views', 'drive')) this.stateDb = setupLevel(path.join(this.storage, 'views', 'state')) this.log = Log({ _id: this._id, core: this.core, feeds: this.feeds, keyPair: this._deriveKeyPair(LOG_ID, this.address), db: this.logDb }) this.drive = Drive({ _id: this._id, corestore: this.corestore, address: this.address, feeds: this.feeds, muxer: this.muxer, core: this.core, db: this.driveDb, deriveKeyPair: this._deriveKeyPair }) this.state = State({ _id: this._id, drive: this.drive, core: this.core, feeds: this.feeds, db: this.stateDb }) return callback() }) } _closeIndexes (callback) { this.core.close((err) => { if (err) return callback(err) this.logDb.close((err) => { if (err) return callback(err) this.stateDb.close((err) => { if (err) return callback(err) this.driveDb.close((err) => { if (err) return callback(err) callback() }) }) }) }) } _close (callback) { assert(this.drive, 'cannot call close before open') this.drive._gracefulUnmount((err) => { if (err) return callback(err) this.log.close((err) => { if (err) return callback(err) this.drive._closeDrives((err) => { if (err) return callback(err) super._close(callback) }) }) }) } } function randomKeyPair (id, context) { return crypto.keyPair(crypto.masterKey(), id, context) } function hex (buf) { if (Buffer.isBuffer(buf)) return buf.toString('hex') return buf } module.exports = (storage, address, identity, opts) => new Space(storage, address, identity, opts) module.exports.Space = Space