@coboxcoop/space
Version:
a peer-to-peer private and encrypted space
240 lines (205 loc) • 6.61 kB
JavaScript
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