├── .gitignore ├── README.md ├── bin └── hypercorn ├── feed.md ├── lib ├── hypercorn.js └── hypercorn │ ├── api.js │ ├── feed.js │ ├── hypercorn.js │ ├── meta.js │ ├── schema.js │ ├── utils.js │ └── values │ └── feed-key.js ├── package.json ├── scripts ├── common.js ├── follow.js ├── message.js ├── package.json ├── post.js ├── timeline.js └── trust.js └── test └── corn-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | test/tmp/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hypercorn 2 | 3 | **WORK IN PROGRESS/EXPERIMENTAL** 4 | 5 | ## LICENSE 6 | 7 | This software is licensed under the MIT License. 8 | 9 | Copyright Fedor Indutny, 2017. 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a 12 | copy of this software and associated documentation files (the 13 | "Software"), to deal in the Software without restriction, including 14 | without limitation the rights to use, copy, modify, merge, publish, 15 | distribute, sublicense, and/or sell copies of the Software, and to permit 16 | persons to whom the Software is furnished to do so, subject to the 17 | following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included 20 | in all copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 23 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 24 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 25 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 26 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 27 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 28 | USE OR OTHER DEALINGS IN THE SOFTWARE. 29 | -------------------------------------------------------------------------------- /bin/hypercorn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const argv = require('yargs') 3 | .usage('Usage: $0 [options] --dir ./directory') 4 | .alias('p', 'port') 5 | .default('port', 8367) 6 | .alias('h', 'host') 7 | .default('host', '127.0.0.1') 8 | .describe('dir', 'hypercorn directory') 9 | .describe('port', 'HTTP control server port') 10 | .describe('host', 'HTTP control server host') 11 | .demand('dir') 12 | .argv; 13 | 14 | const hypercorn = require('../'); 15 | 16 | const HyperCorn = hypercorn.HyperCorn; 17 | const API = hypercorn.API; 18 | 19 | const h = new HyperCorn({ 20 | storage: argv.dir 21 | }); 22 | 23 | const api = new API(h); 24 | 25 | h.listen(() => { 26 | api.listen(argv.port, argv.host, () => { 27 | const info = api.address(); 28 | 29 | console.log('Listening on [%s]:%d, feedKey=%s', 30 | info.address, 31 | info.port, 32 | h.getFeedKey().toString('base64')); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /feed.md: -------------------------------------------------------------------------------- 1 | # Feed 2 | 3 | All messages MUST have following structure: 4 | 5 | ```js 6 | { 7 | "type": "message-type", 8 | "timestamp": 123, // seconds since `1970-01-01T00:00:00.000Z` 9 | "payload": { 10 | // custom payload 11 | } 12 | } 13 | ``` 14 | 15 | Some standard messages (skipping common fields): 16 | 17 | ## Open 18 | 19 | ```js 20 | { 21 | "type": "open", 22 | "payload": { 23 | "protocol": "hypercorn", 24 | "version": 1 25 | } 26 | } 27 | ``` 28 | 29 | First message! 30 | 31 | ## Post 32 | 33 | ```js 34 | { 35 | "type": "post", 36 | "payload": { 37 | "content": "text content", 38 | "reply_to": /* optional */ { 39 | "feed_key": "...", 40 | "index": 0 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | ## Trust 47 | 48 | ```js 49 | { 50 | "type": "trust", 51 | "payload": { 52 | "expires_at": 123, // seconds since `1970-01-01T00:00:00.000Z` 53 | "description": "optional description", 54 | "feed_key": "base64-encoded Trustee's key", 55 | "link": "base64-encoded Trust Link" 56 | } 57 | } 58 | ``` 59 | 60 | See [Trust Link][0] for details. 61 | 62 | ## Follow 63 | 64 | ```js 65 | { 66 | "type": "follow", 67 | "payload": { 68 | "feed_key": "base64-encoded feed key" 69 | } 70 | } 71 | ``` 72 | 73 | ## Unfollow 74 | 75 | ```js 76 | { 77 | "type": "follow", 78 | "payload": { 79 | "feed_key": "base64-encoded feed key" 80 | } 81 | } 82 | ``` 83 | 84 | # Meta 85 | 86 | HyperBloom is used for storing replies and other public editable information. 87 | The values are encoded this way: 88 | 89 | - `key_len` - 1 byte key length 90 | - `key` - bytes of key 91 | - `value_len` - 1 byte value length 92 | - `value` - bytes of value 93 | 94 | `key_len` must be between 0 and 127 (both inclusive). 95 | 96 | ## Keys 97 | 98 | ### Messages 99 | 100 | For the messages key MUST have following structure: 101 | 102 | - `0` - 1 byte 103 | - `index` - 4 byte big endian integer 104 | 105 | ## Values 106 | 107 | ### Reply 108 | 109 | - `0` - 1 byte 110 | - `feed_key` - 32 byte feed key 111 | - `index` - 4 byte big endian integer 112 | 113 | [0]: https://github.com/hyperbloom/hyperbloom-protocol/blob/master/spec.md#signature-chain 114 | -------------------------------------------------------------------------------- /lib/hypercorn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.utils = require('./hypercorn/utils'); 4 | 5 | exports.values = {}; 6 | exports.values.FeedKey = require('./hypercorn/values/feed-key'); 7 | 8 | exports.schema = require('./hypercorn/schema'); 9 | 10 | exports.API = require('./hypercorn/api'); 11 | exports.Meta = require('./hypercorn/meta'); 12 | exports.Feed = require('./hypercorn/feed'); 13 | exports.HyperCorn = require('./hypercorn/hypercorn'); 14 | -------------------------------------------------------------------------------- /lib/hypercorn/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | const bodyParser = require('body-parser'); 5 | const util = require('util'); 6 | const express = require('express'); 7 | const Buffer = require('buffer').Buffer; 8 | const Joi = require('joi'); 9 | const Celebrate = require('celebrate'); 10 | 11 | const hypercorn = require('../hypercorn'); 12 | const schema = hypercorn.schema; 13 | 14 | const DEFAULT_TIMELINE_LIMIT = 64; 15 | 16 | function API(backend) { 17 | this.app = express(); 18 | 19 | this.app.use(bodyParser.json()); 20 | 21 | http.Server.call(this, this.app); 22 | 23 | this.backend = backend; 24 | 25 | this._routes(); 26 | } 27 | util.inherits(API, http.Server); 28 | module.exports = API; 29 | 30 | API.prototype._routes = function _routes() { 31 | const app = this.app; 32 | 33 | // GET 34 | 35 | app.get('/api/info', (req, res) => { 36 | res.json({ feedKey: this.backend.getFeedKey().toString('base64') }); 37 | }); 38 | 39 | app.get('/api/timeline', Celebrate({ 40 | query: { 41 | feed_key: Joi.string().base64().optional(), 42 | offset: Joi.number().min(0).optional(), 43 | limit: Joi.number().min(1).optional() 44 | } 45 | }), (req, res) => this._handleTimeline(req, res)); 46 | 47 | app.get('/api/message', Celebrate({ 48 | query: { 49 | feed_key: Joi.string().base64().optional(), 50 | index: Joi.number().min(0).required() 51 | } 52 | }), (req, res) => this._handleMessage(req, res)); 53 | 54 | // POST 55 | 56 | app.post('/api/post', Celebrate({ 57 | body: schema.Post 58 | }), (req, res) => this._handlePost(req, res)); 59 | 60 | app.post('/api/trust', Celebrate({ 61 | body: schema.TrustBody 62 | }), (req, res) => this._handleTrust(req, res)); 63 | 64 | app.post('/api/follow', Celebrate({ 65 | body: schema.Follow 66 | }), (req, res) => this._handleFollow(req, res)); 67 | 68 | app.post('/api/unfollow', Celebrate({ 69 | body: schema.Unfollow 70 | }), (req, res) => this._handleUnfollow(req, res)); 71 | 72 | this.app.use(Celebrate.errors()); 73 | }; 74 | 75 | API.prototype._feedKey = function _feedKey(raw) { 76 | if (raw) 77 | return Buffer.from(raw, 'base64'); 78 | else 79 | return this.backend.getFeedKey(); 80 | }; 81 | 82 | API.prototype._handleTimeline = function _handleTimeline(req, res) { 83 | const feedKey = this._feedKey(req.query.feed_key); 84 | const offset = req.query.offset; 85 | const limit = req.query.limit; 86 | 87 | const options = { 88 | offset, 89 | limit, 90 | feedKey 91 | }; 92 | 93 | if (!options.offset) 94 | options.offset = 0; 95 | if (!options.limit) 96 | options.limit = DEFAULT_TIMELINE_LIMIT; 97 | 98 | this.backend.getTimeline(options, (err, messages) => { 99 | if (err) 100 | return res.json(500, { error: err.message }); 101 | 102 | res.json({ ok: true, messages }); 103 | }); 104 | }; 105 | 106 | API.prototype._handleMessage = function _handleMessage(req, res) { 107 | const feedKey = this._feedKey(req.query.feed_key); 108 | const index = req.query.index; 109 | 110 | const options = { 111 | feedKey, 112 | index 113 | }; 114 | 115 | if (!options.index) 116 | options.index = 0; 117 | 118 | this.backend.getMessage(options, (err, message) => { 119 | if (err) 120 | return res.json(500, { error: err.message }); 121 | 122 | res.json({ ok: true, message }); 123 | }); 124 | }; 125 | 126 | API.prototype._handleTrust = function _handleTrust(req, res) { 127 | const body = req.body; 128 | 129 | const options = {}; 130 | if (body.expires_in) 131 | options.expiresIn = body.expires_in; 132 | if (body.description) 133 | options.description = body.description; 134 | 135 | const trusteeKey = Buffer.from(body.feed_key, 'base64'); 136 | const link = this.backend.trust(trusteeKey, options); 137 | 138 | res.json({ link: link.toString('hex') }); 139 | }; 140 | 141 | API.prototype._handleFollow = function _handleFollow(req, res) { 142 | const body = req.body; 143 | const feedKey = Buffer.from(body.feed_key, 'base64'); 144 | this.backend.follow(feedKey); 145 | res.json({ ok: true }); 146 | }; 147 | 148 | API.prototype._handleUnfollow = function _handleUnfollow(req, res) { 149 | const body = req.body; 150 | const feedKey = Buffer.from(body.feed_key, 'base64'); 151 | this.backend.unfollow(feedKey); 152 | res.json({ ok: true }); 153 | }; 154 | 155 | API.prototype._handlePost = function _handlePost(req, res) { 156 | const body = req.body; 157 | this.backend.post({ 158 | content: body.content, 159 | replyTo: body.reply_to && { 160 | feedKey: Buffer.from(body.reply_to.feed_key, 'base64'), 161 | index: body.reply_to.index 162 | } 163 | }); 164 | res.json({ ok: true }); 165 | }; 166 | -------------------------------------------------------------------------------- /lib/hypercorn/feed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const async = require('async'); 5 | const debug = require('debug')('hypercorn:feed'); 6 | const util = require('util'); 7 | const ram = require('random-access-memory'); 8 | const hypercore = require('hypercore'); 9 | const hyperdiscovery = require('hyperdiscovery'); 10 | 11 | const Buffer = require('buffer').Buffer; 12 | const EventEmitter = require('events').EventEmitter; 13 | 14 | const hypercorn = require('../hypercorn'); 15 | const Meta = hypercorn.Meta; 16 | 17 | const DEFAULT_TIMEOUT = 30000; 18 | 19 | const SCRAPE_LIMIT = 100; 20 | const MIN_SPARSE_PEERS = 1; 21 | 22 | function Feed(options) { 23 | EventEmitter.call(this); 24 | 25 | this.writable = !!options.privateKey; 26 | this.sparse = options.full === false; 27 | this.feedKey = options.feedKey; 28 | this.hyperbloom = options.hyperbloom; 29 | 30 | this.meta = new Meta(); 31 | 32 | const joinHyperBloom = (callback) => { 33 | const bloom = { 34 | full: !this.sparse 35 | }; 36 | this.hyperbloom.join(this.feedKey, bloom, (err, node) => { 37 | // Most likely trust chain can't be built yet 38 | // TODO(indutny): check this exactly 39 | if (err) { 40 | assert(!this.writable); 41 | return callback(null, null); 42 | } 43 | 44 | this.node = node; 45 | return callback(null, node); 46 | }); 47 | }; 48 | 49 | const onHyperCore = (err) => { 50 | if (err) 51 | return this.emit('error', err); 52 | 53 | debug('hypercore updated'); 54 | if (this.node) 55 | return this._emitReady(); 56 | 57 | // Scrape trust and retry 58 | this._scrapeTrust(() => { 59 | joinHyperBloom(() => { 60 | if (this.node) 61 | debug('scrape successful'); 62 | else 63 | debug('scrape not successful'); 64 | this._emitReady(); 65 | }); 66 | }); 67 | }; 68 | 69 | const onHyperBloom = () => { 70 | const feedStorage = this.sparse ? () => ram() : options.feedDir; 71 | 72 | this.hypercore = hypercore(feedStorage, this.feedKey, { 73 | secretKey: options.privateKey, 74 | sparse: this.sparse, 75 | storeSecretKey: false, 76 | valueEncoding: 'json' 77 | }); 78 | 79 | this.hypercore.ready(() => { 80 | debug('hypercore ready'); 81 | this.swarm = hyperdiscovery(this.hypercore, { live: true }); 82 | 83 | // TODO(indutny): this sounds hacky 84 | // We need this event to do the first post 85 | this.emit('hypercore'); 86 | 87 | if (this.hypercore.length === 0) { 88 | debug('hypercore empty, updating'); 89 | this.hypercore.update(onHyperCore); 90 | } else { 91 | onHyperCore(); 92 | } 93 | }); 94 | }; 95 | 96 | joinHyperBloom(onHyperBloom); 97 | 98 | this._ready = false; 99 | this._queue = []; 100 | 101 | this.swarm = null; 102 | this.node = null; 103 | } 104 | util.inherits(Feed, EventEmitter); 105 | module.exports = Feed; 106 | 107 | Feed.prototype.getLength = function getLength() { 108 | return this.hypercore.length; 109 | }; 110 | 111 | Feed.prototype._onReady = function _onReady(callback) { 112 | if (this._ready) 113 | return process.nextTick(callback); 114 | 115 | this._queue.push(callback); 116 | }; 117 | 118 | Feed.prototype.watch = function watch(range, callback) { 119 | assert.equal(typeof range, 'object', '`range` must be an Object'); 120 | assert.equal(typeof range.start, 'number', '`range.start` must be a Number'); 121 | assert(typeof range.end === 'number' || !range.end, 122 | '`range.end` must be a Number or not be present'); 123 | 124 | const options = { 125 | wait: true, 126 | timeout: DEFAULT_TIMEOUT 127 | }; 128 | 129 | const start = { type: 'message', payload: { index: range.start } }; 130 | const end = range.end && { type: 'message', payload: { index: range.end } }; 131 | 132 | this._onReady(() => { 133 | const watcher = this.node && this.node.watch({ 134 | start: this.meta.generate(start), 135 | end: range.end && this.meta.generate(end) 136 | }); 137 | 138 | process.nextTick(callback, null, { 139 | message: this.hypercore.createReadStream({ 140 | start: range.start, 141 | end: range.end, 142 | live: !range.end, 143 | wait: true 144 | }), 145 | meta: watcher 146 | }); 147 | }); 148 | }; 149 | 150 | Feed.prototype.unwatch = function unwatch(obj) { 151 | if (this.node) 152 | this.node.unwatch(obj.meta); 153 | }; 154 | 155 | Feed.prototype.append = function append(type, payload, callback) { 156 | assert(this.writable, 'Feed not writable'); 157 | assert.equal(typeof type, 'string', '`type` must be a String'); 158 | assert.equal(typeof payload, 'object', '`payload` must be an Object'); 159 | 160 | const now = Date.now() / 1000; 161 | this.hypercore.append({ 162 | type: type, 163 | created_at: now, 164 | payload: payload 165 | }, (err) => { 166 | // See: https://github.com/mafintosh/hypercore/issues/94 167 | if (callback) 168 | callback(err, this.hypercore.length - 1); 169 | }); 170 | }; 171 | 172 | Feed.prototype.addMeta = function addMeta(index, value, callback) { 173 | assert.equal(typeof index, 'number', '`index` must be a Number'); 174 | assert.equal(typeof value, 'object', '`value` must be an Object'); 175 | 176 | if (!this.node) { 177 | debug('can\'t add meta'); 178 | process.nextTick(callback, null); 179 | return; 180 | } 181 | 182 | debug('adding meta'); 183 | const meta = this.meta.generate({ 184 | type: 'message', 185 | payload: { index } 186 | }, value); 187 | 188 | if (this.sparse) { 189 | debug('sparse hyperbloom, wait for peers'); 190 | this.node.onPeers((err, count) => { 191 | debug('got hyperbloom peers=%d', count); 192 | this.node.insert(meta, { minPeers: MIN_SPARSE_PEERS }, callback); 193 | }); 194 | } else { 195 | this.node.insert(meta, callback); 196 | } 197 | }; 198 | 199 | Feed.prototype.addReply = function addReply(index, reply, callback) { 200 | assert.equal(typeof index, 'number', '`index` must be a Number'); 201 | assert.equal(typeof reply, 'object', '`reply` must be an Object'); 202 | assert(Buffer.isBuffer(reply.feedKey), '`reply.feedKey` must be a Buffer'); 203 | assert.equal(typeof reply.index, 'number', 204 | '`reply.index` must be a Number'); 205 | 206 | this.addMeta(index, { 207 | type: 'reply', 208 | payload: { 209 | feedKey: reply.feedKey, 210 | index: reply.index 211 | } 212 | }, callback); 213 | }; 214 | 215 | Feed.prototype.close = function close(callback) { 216 | debug('close'); 217 | this._onReady(() => { 218 | debug('closing hypercore'); 219 | this.hypercore.close(); 220 | if (this.node) { 221 | debug('closing hyperbloom'); 222 | this.hyperbloom.leave(this.feedKey); 223 | } 224 | debug('destroying hypercore swarm'); 225 | this.swarm.destroy(); 226 | 227 | callback(null); 228 | }); 229 | }; 230 | 231 | Feed.prototype.getTimeline = function getTimeline(options, callback) { 232 | assert.equal(typeof options, 'object', '`options` must be an Object'); 233 | assert.equal(typeof options.offset, 'number', 234 | '`options.offset` must be a Number'); 235 | assert.equal(typeof options.limit, 'number', 236 | '`options.limit` must be a Number'); 237 | 238 | const onLength = () => { 239 | const end = Math.max(0, this.hypercore.length - options.offset); 240 | const start = Math.max(0, end - options.limit); 241 | 242 | debug('downloading start=%d end=%d', start, end); 243 | 244 | async.parallel({ 245 | messages: (callback) => { 246 | const messages = []; 247 | this.hypercore.createReadStream({ 248 | start, 249 | end 250 | }).on('data', (message) => { 251 | messages.push(message); 252 | }).once('end', () => { 253 | callback(null, messages); 254 | }).on('error', (err) => { 255 | callback(err); 256 | }); 257 | }, 258 | meta: (callback) => { 259 | if (!this.node) 260 | return callback(null, []); 261 | 262 | const keyStart = { type: 'message', payload: { index: start } }; 263 | const keyEnd = { type: 'message', payload: { index: end } }; 264 | 265 | const values = this.node.request({ 266 | start: this.meta.generate(keyStart), 267 | end: this.meta.generate(keyEnd) 268 | }).map(value => this.meta.parse(value)); 269 | 270 | callback(null, values.filter(({ key }) => { 271 | return key.type === 'message'; 272 | }).map(({ key, value }) => { 273 | return { 274 | index: key.payload.index, 275 | value 276 | }; 277 | })); 278 | } 279 | }, (err, result) => { 280 | if (err) 281 | return callback(err); 282 | 283 | debug('downloaded start=%d end=%d', start, end); 284 | 285 | const messages = result.messages.map((message, index) => { 286 | return { message, index, meta: [] }; 287 | }); 288 | 289 | result.meta.forEach(({ index, value }) => { 290 | index -= start; 291 | if (0 <= index && index < messages.length) 292 | messages[index].meta.push(value); 293 | }); 294 | 295 | callback(null, messages); 296 | }); 297 | }; 298 | 299 | if (this.hypercore.length === 0) 300 | this.hypercore.update(onLength); 301 | else 302 | onLength(); 303 | }; 304 | 305 | Feed.prototype.getMessage = function getMessage(options, callback) { 306 | assert.equal(typeof options, 'object', '`options` must be an Object'); 307 | assert.equal(typeof options.index, 'number', 308 | '`options.index` must be a Number'); 309 | 310 | const index = options.index; 311 | debug('get message index=%d', index); 312 | 313 | async.parallel({ 314 | message: callback => this.hypercore.get(index, callback), 315 | meta: (callback) => { 316 | if (!this.node) 317 | return callback(null, []); 318 | 319 | const keyStart = { type: 'message', payload: { index } }; 320 | const keyEnd = { type: 'message', payload: { index: index + 1 } }; 321 | 322 | const values = this.node.request({ 323 | start: this.meta.generate(keyStart), 324 | end: this.meta.generate(keyEnd) 325 | }).map(value => this.meta.parse(value)); 326 | 327 | callback(null, values.filter(({ key }) => { 328 | return key.type === 'message' && key.payload.index === index; 329 | }).map(({ key, value }) => { 330 | return value; 331 | })); 332 | } 333 | }, (err, result) => { 334 | if (err) 335 | return callback(err); 336 | 337 | debug('get message done index=%d', index); 338 | 339 | callback(null, { 340 | message: result.message, 341 | index, 342 | meta: result.meta 343 | }); 344 | }); 345 | }; 346 | 347 | // Private 348 | 349 | Feed.prototype._scrapeTrust = function _scrapeTrust(callback) { 350 | assert(!this.node); 351 | 352 | debug('scraping for trust'); 353 | this.hypercore.createReadStream({ 354 | start: Math.max(0, this.hypercore.length - SCRAPE_LIMIT), 355 | end: this.hypercore.length 356 | }).on('data', (message) => { 357 | if (message.type !== 'trust') 358 | return; 359 | 360 | this.emit('trust', message); 361 | }).once('end', () => { 362 | debug('scraping for trust done'); 363 | callback(null); 364 | }).on('error', (err) => { 365 | callback(err); 366 | }); 367 | }; 368 | 369 | Feed.prototype._emitReady = function _emitReady() { 370 | this._ready = true; 371 | const queue = this._queue; 372 | this._queue = []; 373 | for (let i = 0; i < queue.length; i++) 374 | queue[i](); 375 | 376 | this.emit('ready'); 377 | }; 378 | -------------------------------------------------------------------------------- /lib/hypercorn/hypercorn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const async = require('async'); 4 | const assert = require('assert'); 5 | const debug = require('debug')('hypercorn'); 6 | const path = require('path'); 7 | const mkdirp = require('mkdirp'); 8 | const datDefaults = require('datland-swarm-defaults'); 9 | const HyperChain = require('hyperbloom-chain'); 10 | const HyperBloom = require('hyperbloom'); 11 | const Buffer = require('buffer').Buffer; 12 | const Joi = require('joi'); 13 | 14 | const hypercorn = require('../hypercorn'); 15 | const schema = hypercorn.schema; 16 | const utils = hypercorn.utils; 17 | const Feed = hypercorn.Feed; 18 | 19 | const FEED_DIR = 'hypercore'; 20 | const BLOOM_DIR = 'hyperbloom'; 21 | const TRUST_DIR = 'trust.db'; 22 | 23 | const DEFAULT_EXPIRATION = 3600 * 24 * 365; // 1 year 24 | 25 | const HYPERCORN_VERSION = 1; 26 | 27 | function HyperCorn(options) { 28 | this.options = Object.assign({}, options); 29 | assert.equal(typeof this.options.storage, 'string', 30 | '`options.storage` must be a path to directory'); 31 | 32 | this.feedDir = path.join(this.options.storage, FEED_DIR); 33 | const bloomDir = path.join(this.options.storage, BLOOM_DIR); 34 | 35 | mkdirp.sync(this.feedDir); 36 | mkdirp.sync(bloomDir); 37 | 38 | let pair = { 39 | publicKey: this.options.publicKey, 40 | privateKey: this.options.privateKey, 41 | justCreated: false 42 | }; 43 | 44 | if (!pair.publicKey || !pair.privateKey) 45 | pair = utils.loadKey(this.options.storage); 46 | 47 | this.pair = pair; 48 | 49 | this.hyperbloom = new HyperBloom({ 50 | storage: bloomDir, 51 | publicKey: this.pair.publicKey, 52 | privateKey: this.pair.privateKey, 53 | trust: { 54 | db: path.join(bloomDir, TRUST_DIR) 55 | }, 56 | discovery: datDefaults() 57 | }); 58 | 59 | this._chain = new HyperChain({ root: this.pair.publicKey }); 60 | this._feeds = new Map(); 61 | 62 | this._main = { 63 | feed: null, 64 | watcher: null 65 | }; 66 | } 67 | module.exports = HyperCorn; 68 | 69 | // Public 70 | 71 | HyperCorn.prototype.listen = function listen(callback) { 72 | const feed = new Feed({ 73 | hyperbloom: this.hyperbloom, 74 | feedDir: path.join(this.feedDir, this.pair.publicKey.toString('hex')), 75 | full: true, 76 | 77 | feedKey: this.pair.publicKey, 78 | privateKey: this.pair.privateKey 79 | }); 80 | 81 | this._main.feed = feed; 82 | this._feeds.set(this.pair.publicKey.toString('base64'), feed); 83 | 84 | feed.once('ready', () => { 85 | feed.watch({ 86 | start: 0 87 | }, (err, watcher) => { 88 | if (err) 89 | return debug('watch err=%s', err.message); 90 | 91 | watcher.message.on('data', msg => this._onSelfMessage(msg)); 92 | 93 | this._main.watcher = watcher; 94 | 95 | callback(null); 96 | }); 97 | }); 98 | 99 | if (this.pair.justCreated) { 100 | feed.once('hypercore', () => { 101 | this._postOpen(); 102 | }); 103 | } 104 | }; 105 | 106 | HyperCorn.prototype.close = function close(callback) { 107 | let waiting = 1; 108 | const done = () => { 109 | if (--waiting === 0 && callback) 110 | return callback(); 111 | }; 112 | this.hyperbloom.close(done); 113 | 114 | const main = this._main; 115 | this._main = null; 116 | if (main.feed && main.watcher) { 117 | main.feed.unwatch(main.watcher); 118 | waiting++; 119 | main.feed.close(done); 120 | } 121 | }; 122 | 123 | HyperCorn.prototype.getFeedKey = function getFeedKey() { 124 | return this.pair.publicKey; 125 | }; 126 | 127 | // Timeline 128 | 129 | HyperCorn.prototype.getTimeline = function getTimeline(options, callback) { 130 | assert(Buffer.isBuffer(options.feedKey), 131 | '`options.feedKey` must be a Buffer'); 132 | assert.equal(typeof options.offset, 'number', 133 | '`options.offset` must be a Number'); 134 | assert.equal(typeof options.limit, 'number', 135 | '`options.limit` must be a Number'); 136 | 137 | const base64Key = options.feedKey.toString('base64'); 138 | debug('timeline request key=%s offset=%d limit=%d', base64Key, 139 | options.offset, options.limit); 140 | 141 | this._withFeed(options.feedKey, (feed, done) => { 142 | feed.getTimeline(options, (err, data) => { 143 | done(); 144 | callback(err, data); 145 | }); 146 | }); 147 | }; 148 | 149 | HyperCorn.prototype.getMessage = function getMessage(options, callback) { 150 | assert(Buffer.isBuffer(options.feedKey), 151 | '`options.feedKey` must be a Buffer'); 152 | assert.equal(typeof options.index, 'number', 153 | '`options.index` must be a Number'); 154 | 155 | const base64Key = options.feedKey.toString('base64'); 156 | debug('message request key=%s index=%d', base64Key, options.index); 157 | 158 | this._withFeed(options.feedKey, (feed, done) => { 159 | feed.getMessage(options, (err, data) => { 160 | done(); 161 | callback(err, data); 162 | }); 163 | }); 164 | }; 165 | 166 | // Messages 167 | 168 | HyperCorn.prototype.trust = function trust(feedKey, options, callback) { 169 | const base64PublicKey = feedKey.toString('base64'); 170 | debug('trust key=%s', base64PublicKey); 171 | 172 | options = Object.assign({ 173 | expiresIn: DEFAULT_EXPIRATION 174 | }, options); 175 | 176 | const expiresAt = Date.now() / 1000 + options.expiresIn; 177 | 178 | const link = this._chain.issueLink({ 179 | publicKey: feedKey, 180 | expiration: expiresAt 181 | }, this.pair.privateKey); 182 | 183 | this._main.feed.append('trust', { 184 | expires_at: expiresAt, 185 | feed_key: base64PublicKey, 186 | link: link.toString('base64'), 187 | description: options.description 188 | }, callback); 189 | 190 | return link; 191 | }; 192 | 193 | HyperCorn.prototype.follow = function follow(feedKey, callback) { 194 | const base64FeedKey = feedKey.toString('base64'); 195 | debug('follow feed=%s', base64FeedKey); 196 | this._main.feed.append('follow', { 197 | feed_key: base64FeedKey 198 | }, callback); 199 | }; 200 | 201 | HyperCorn.prototype.unfollow = function unfollow(feedKey, callback) { 202 | const base64FeedKey = feedKey.toString('base64'); 203 | debug('unfollow feed=%s', base64FeedKey); 204 | this._main.feed.append('unfollow', { 205 | feed_key: base64FeedKey 206 | }, callback); 207 | }; 208 | 209 | HyperCorn.prototype.post = function post(object, callback) { 210 | debug('new post'); 211 | this._main.feed.append('post', { 212 | content: object.content, 213 | reply_to: object.replyTo && { 214 | feed_key: object.replyTo.feedKey.toString('base64'), 215 | index: object.replyTo.index 216 | } 217 | }, (err, index) => { 218 | if (err) { 219 | debug('post append error=%s', err.message); 220 | if (callback) 221 | callback(err); 222 | return; 223 | } 224 | 225 | if (object.replyTo) 226 | this._addReply(index, object.replyTo, callback); 227 | else if (callback) 228 | callback(null); 229 | }); 230 | }; 231 | 232 | // Private 233 | 234 | HyperCorn.prototype._postOpen = function _postOpen() { 235 | debug('posting open'); 236 | this._main.feed.append('open', { 237 | protocol: 'hypercorn', 238 | version: HYPERCORN_VERSION 239 | }); 240 | }; 241 | 242 | HyperCorn.prototype._withFeed = function _withFeed(feedKey, body) { 243 | const base64Key = feedKey.toString('base64'); 244 | if (this._feeds.has(base64Key)) { 245 | process.nextTick(body, this._feeds.get(base64Key), () => {}); 246 | return; 247 | } 248 | 249 | // No existing feeds, try to get sparse feed 250 | const feed = new Feed({ 251 | hyperbloom: this.hyperbloom, 252 | feedDir: path.join(this.feedDir, feedKey.toString('hex')), 253 | full: false, 254 | 255 | feedKey 256 | }); 257 | 258 | feed.on('trust', (trust) => { 259 | this._onExternalTrust(trust.payload, feedKey); 260 | }); 261 | 262 | feed.once('ready', () => { 263 | body(feed, () => { 264 | feed.close(() => {}); 265 | }); 266 | }); 267 | }; 268 | 269 | HyperCorn.prototype._addReply = function _addReply(index, options, callback) { 270 | debug('adding reply'); 271 | 272 | this._withFeed(options.feedKey, (feed, done) => { 273 | feed.addReply(options.index, { 274 | feedKey: this.getFeedKey(), 275 | index 276 | }, () => { 277 | if (callback) 278 | callback(null); 279 | done(); 280 | }); 281 | }); 282 | }; 283 | 284 | HyperCorn.prototype._onSelfMessage = function _onSelfMessage(message) { 285 | if (message.type === 'follow') 286 | this._onFollow(message.payload); 287 | else if (message.type === 'unfollow') 288 | this._onUnfollow(message.payload); 289 | else if (message.type === 'trust') 290 | this._onTrust(message.payload); 291 | else if (message.type === 'open') 292 | debug('has open'); 293 | }; 294 | 295 | HyperCorn.prototype._onFollow = function _onFollow(payload) { 296 | const { error, value } = Joi.validate(payload, schema.Follow); 297 | if (error) 298 | return debug('validation err=%s', error.message); 299 | 300 | if (this._feeds.has(value.feed_key)) 301 | return; 302 | 303 | debug('on follow feed=%s', value.feed_key); 304 | 305 | const feedKey = Buffer.from(value.feed_key, 'base64'); 306 | 307 | const feed = new Feed({ 308 | hyperbloom: this.hyperbloom, 309 | feedDir: path.join(this.feedDir, feedKey.toString('hex')), 310 | full: true, 311 | 312 | feedKey: feedKey 313 | }); 314 | 315 | feed.watch({ 316 | start: 0 317 | }, (err, watcher) => { 318 | if (err) 319 | return debug('watch err=%s', err.message); 320 | 321 | watcher.message.on('data', msg => this._onExternalMessage(msg, feedKey)); 322 | 323 | this._feeds.set(value.feed_key, feed); 324 | }); 325 | }; 326 | 327 | HyperCorn.prototype._onUnfollow = function _onUnfollow(payload) { 328 | const { error, value } = Joi.validate(payload, schema.Unfollow); 329 | if (error) 330 | return debug('validation err=%s', error.message); 331 | 332 | if (!this._feeds.has(value.feed_key)) 333 | return; 334 | 335 | debug('on unfollow feed=%s', value.feed_key); 336 | 337 | const feed = this._feeds.get(payload_feed_key); 338 | feed.close(() => {}); 339 | }; 340 | 341 | HyperCorn.prototype._onTrust = function _onTrust(payload) { 342 | const { error, value } = Joi.validate(payload, schema.TrustMessage); 343 | if (error) 344 | return debug('validation err=%s', error.message); 345 | 346 | debug('on trust key=%s', value.feed_key); 347 | 348 | const link = Buffer.from(value.link, 'base64'); 349 | this.hyperbloom.addLink(this.getFeedKey(), link); 350 | }; 351 | 352 | HyperCorn.prototype._onExternalMessage = function _onExternalMessage(message, 353 | feedKey) { 354 | if (message.type === 'trust') 355 | this._onExternalTrust(message.payload, feedKey); 356 | }; 357 | 358 | HyperCorn.prototype._onExternalTrust = function _onExternalTrust(payload, 359 | feedKey) { 360 | const { error, value } = Joi.validate(payload, schema.TrustMessage); 361 | if (error) 362 | return debug('validation err=%s', error.message); 363 | 364 | const link = Buffer.from(value.link, 'base64'); 365 | debug('on external trust key=%s by=%s', value.feed_key, 366 | feedKey.toString('base64')); 367 | 368 | // TODO(indutny): refresh feeds to load hyperbloom nodes 369 | this.hyperbloom.addLink(feedKey, link); 370 | }; 371 | -------------------------------------------------------------------------------- /lib/hypercorn/meta.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const Buffer = require('buffer').Buffer; 5 | 6 | const hypercorn = require('../hypercorn'); 7 | const FeedKey = hypercorn.values.FeedKey; 8 | 9 | const FEED_KEY_SIZE = 32; 10 | 11 | const KEY_TYPE = { 12 | MESSAGE: 0, 13 | }; 14 | 15 | const KEY_SIZE = { 16 | MESSAGE: 5 17 | }; 18 | 19 | const VALUE_TYPE = { 20 | REPLY: 0 21 | }; 22 | 23 | const VALUE_SIZE = { 24 | REPLY: 1 + FEED_KEY_SIZE + 4 25 | }; 26 | 27 | function Meta() { 28 | } 29 | module.exports = Meta; 30 | 31 | // General 32 | 33 | Meta.prototype.generate = function generate(key, value) { 34 | key = this.generateKey(key.type, key.payload); 35 | value = value ? this.generateValue(value.type, value.payload) : 36 | Buffer.alloc(0); 37 | 38 | const meta = Buffer.alloc(1 + key.length + 1 + value.length); 39 | 40 | let offset = 0; 41 | meta[offset] = key.length; 42 | offset++; 43 | key.copy(meta, offset); 44 | offset += key.length; 45 | meta[offset] = value.length; 46 | offset++; 47 | value.copy(meta, offset); 48 | offset += value.length; 49 | assert.equal(offset, meta.length); 50 | 51 | return meta; 52 | }; 53 | 54 | Meta.prototype.parse = function parse(raw) { 55 | let offset = 0; 56 | let len = raw.length; 57 | 58 | if (len < 1) 59 | return false; 60 | 61 | const keyLen = raw[offset]; 62 | offset++; 63 | len--; 64 | if (len < keyLen) 65 | return false; 66 | 67 | const key = raw.slice(offset, offset + keyLen); 68 | offset += keyLen; 69 | len -= keyLen; 70 | if (len < 1) 71 | return false; 72 | 73 | const valueLen = raw[offset]; 74 | offset++; 75 | len--; 76 | if (len < valueLen) 77 | return false; 78 | 79 | const value = raw.slice(offset, offset + valueLen); 80 | offset += valueLen; 81 | len -= valueLen; 82 | if (len !== 0) 83 | return false; 84 | 85 | return { key: this.parseKey(key), value: this.parseValue(value) }; 86 | }; 87 | 88 | // Keys/Values 89 | 90 | Meta.prototype.generateKey = function generateKey(type, payload) { 91 | if (type === 'message') 92 | return this._generateKeyMessage(payload); 93 | else 94 | throw new Error(`Unknown key type: ${type}`); 95 | }; 96 | 97 | Meta.prototype.parseKey = function parseKey(key) { 98 | if (this.isKey('message', key)) 99 | return { type: 'message', payload: this._parseKeyMessage(key) }; 100 | else 101 | return { type: 'unknown', payload: key }; 102 | }; 103 | 104 | Meta.prototype.isKey = function isKey(type, key) { 105 | if (type === 'message') 106 | return this._isKeyMessage(key); 107 | else 108 | return false; 109 | }; 110 | 111 | Meta.prototype.generateValue = function generateValue(type, payload) { 112 | if (type === 'reply') 113 | return this._generateValueReply(payload); 114 | else 115 | throw new Error(`Unknown key type: ${type}`); 116 | }; 117 | 118 | Meta.prototype.parseValue = function parseValue(value) { 119 | if (this.isValue('reply', value)) 120 | return { type: 'reply', payload: this._parseValueReply(value) }; 121 | else 122 | return { type: 'unknown', payload: value }; 123 | }; 124 | 125 | Meta.prototype.isValue = function isValue(type, value) { 126 | if (type === 'reply') 127 | return this._isValueReply(value); 128 | else 129 | return false; 130 | }; 131 | 132 | // Private 133 | 134 | Meta.prototype._generateKeyMessage = function _generateKeyMessage({ index }) { 135 | assert(isFinite(index), FEED_KEY_SIZE, '`index` must be integer'); 136 | 137 | const key = Buffer.alloc(KEY_SIZE.MESSAGE); 138 | 139 | key[0] = KEY_TYPE.MESSAGE; 140 | key.writeUInt32BE(index, 1); 141 | 142 | return key; 143 | }; 144 | 145 | Meta.prototype._isKeyMessage = function _isKeyMessage(key) { 146 | return key.length === KEY_SIZE.MESSAGE && key[0] === KEY_TYPE.MESSAGE; 147 | }; 148 | 149 | Meta.prototype._parseKeyMessage = function _parseKeyMessage(key) { 150 | return { index: key.readUInt32BE(1) }; 151 | }; 152 | 153 | Meta.prototype._generateValueReply = function _generateValueReply(options) { 154 | const feedKey = FeedKey.from(options.feedKey).toBuffer(); 155 | 156 | assert.equal(feedKey.length, FEED_KEY_SIZE, 'Invalid `feedKey` size'); 157 | assert(isFinite(options.index), FEED_KEY_SIZE, '`index` must be integer'); 158 | 159 | const value = Buffer.alloc(VALUE_SIZE.REPLY); 160 | 161 | let offset = 0; 162 | value[offset] = VALUE_TYPE.REPLY; 163 | offset++; 164 | 165 | feedKey.copy(value, offset); 166 | offset += feedKey.length; 167 | 168 | value.writeUInt32BE(options.index, offset); 169 | 170 | return value; 171 | }; 172 | 173 | Meta.prototype._isValueReply = function _isValueReply(value) { 174 | return value.length === VALUE_SIZE.REPLY && value[0] === VALUE_TYPE.REPLY; 175 | }; 176 | 177 | Meta.prototype._parseValueReply = function _parseValueReply(value) { 178 | return { 179 | feedKey: FeedKey.from(value.slice(1, 1 + FEED_KEY_SIZE)), 180 | index: value.readUInt32BE(1 + FEED_KEY_SIZE) 181 | }; 182 | }; 183 | -------------------------------------------------------------------------------- /lib/hypercorn/schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Joi = require('joi'); 4 | 5 | exports.Message = Joi.object().keys({ 6 | type: Joi.string().required(), 7 | timestamp: Joi.number().min(0).required(), 8 | payload: Joi.object().required() 9 | }); 10 | 11 | exports.Open = Joi.object().keys({ 12 | protocol: Joi.string().valid('hypercorn').required(), 13 | version: Joi.number().valid(1).required() 14 | }); 15 | 16 | exports.Post = Joi.object().keys({ 17 | content: Joi.string().required(), 18 | reply_to: Joi.object().keys({ 19 | feed_key: Joi.string().base64().required(), 20 | index: Joi.number().min(0).required() 21 | }).optional() 22 | }); 23 | 24 | // The message in the feed 25 | exports.TrustMessage = Joi.object().keys({ 26 | feed_key: Joi.string().base64().required(), 27 | expires_at: Joi.number().min(0).required(), 28 | description: Joi.string().optional(), 29 | link: Joi.string().base64().required() 30 | }); 31 | 32 | // POST body 33 | exports.TrustBody = Joi.object().keys({ 34 | feed_key: Joi.string().base64().required(), 35 | expires_at: Joi.number().min(0).optional(), 36 | description: Joi.string().optional() 37 | }); 38 | 39 | exports.Follow = Joi.object().keys({ 40 | feed_key: Joi.string().base64().required() 41 | }); 42 | 43 | exports.Unfollow = Joi.object().keys({ 44 | feed_key: Joi.string().base64().required() 45 | }); 46 | -------------------------------------------------------------------------------- /lib/hypercorn/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | const signatures = require('sodium-signatures'); 7 | const Buffer = require('buffer').Buffer; 8 | 9 | const KEY_FILE = 'key.json'; 10 | 11 | exports.loadKey = function loadKey(dir) { 12 | const file = path.join(dir, KEY_FILE); 13 | 14 | if (fs.existsSync(file)) { 15 | const data = fs.readFileSync(file).toString(); 16 | const json = JSON.parse( 17 | data.split(/\r\n|\n/g).filter(line => !/^\s*#/.test(line)).join('\n')); 18 | 19 | assert.equal(typeof json['public'], 'string', 20 | `missing \`public\` in key file ${file}`); 21 | assert.equal(typeof json['private'], 'string', 22 | `missing \`private\` in key file ${file}`); 23 | 24 | return { 25 | justCreated: false, 26 | publicKey: Buffer.from(json['public'], 'base64'), 27 | privateKey: Buffer.from(json['private'], 'base64') 28 | }; 29 | } 30 | 31 | const pair = signatures.keyPair(); 32 | 33 | const lines = [ 34 | '#', 35 | '#', 36 | '# WARNING', 37 | '# DO NOT SHARE THIS WITH ANYONE', 38 | '# THIS IS YOUR HYPERCORN IDENTITY', 39 | '#', 40 | '#', 41 | JSON.stringify({ 42 | 'public': pair.publicKey.toString('base64'), 43 | 'private': pair.secretKey.toString('base64') 44 | }, null, 2), 45 | '#', 46 | '#', 47 | '# END OF SENSITIVE DATA', 48 | '#', 49 | '#' 50 | ]; 51 | fs.writeFileSync(file, lines.join('\n')); 52 | 53 | return { 54 | justCreated: true, 55 | publicKey: pair.publicKey, 56 | privateKey: pair.secretKey 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /lib/hypercorn/values/feed-key.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Buffer = require('buffer').Buffer; 4 | 5 | function FeedKey(value) { 6 | this.value = value; 7 | } 8 | module.exports = FeedKey; 9 | 10 | FeedKey.from = function from(value) { 11 | if (value instanceof FeedKey) 12 | return value; 13 | else if (typeof value === 'string') 14 | return new FeedKey(Buffer.from(value, 'base64')); 15 | else 16 | return new FeedKey(value); 17 | }; 18 | 19 | FeedKey.prototype.toJSON = function toJSON() { 20 | return this.value.toString('base64'); 21 | }; 22 | 23 | FeedKey.prototype.toBuffer = function toBuffer() { 24 | return this.value; 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hypercorn", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "lib/hypercorn.js", 6 | "bin": { 7 | "hypercorn": "bin/hypercorn" 8 | }, 9 | "scripts": { 10 | "test": "tape test/*-test.js" 11 | }, 12 | "keywords": [], 13 | "author": "Fedor Indutny (http://darksi.de/)", 14 | "license": "MIT", 15 | "dependencies": { 16 | "async": "^2.3.0", 17 | "body-parser": "^1.17.1", 18 | "celebrate": "^4.0.1", 19 | "datland-swarm-defaults": "^1.0.2", 20 | "debug": "^2.6.4", 21 | "express": "^4.15.2", 22 | "hyperbloom": "^2.0.0-0", 23 | "hyperbloom-chain": "^1.0.0", 24 | "hypercore": "^5.11.2", 25 | "hyperdiscovery": "^6.0.1", 26 | "joi": "^10.4.1", 27 | "mkdirp": "^0.5.1", 28 | "random-access-memory": "^2.4.0", 29 | "sodium-signatures": "^2.0.0", 30 | "yargs": "^7.1.0" 31 | }, 32 | "devDependencies": { 33 | "rimraf": "^2.6.1", 34 | "tape": "^4.6.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scripts/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | const qs = require('querystring'); 5 | const prompt = require('prompt'); 6 | 7 | function REST(port, host) { 8 | this.port = port; 9 | this.host = host; 10 | } 11 | module.exports = REST; 12 | 13 | REST.prototype._params = function _params(params, callback) { 14 | prompt.get(params, (err, result) => { 15 | const obj = {}; 16 | Object.keys(result).forEach((key) => { 17 | let value = result[key]; 18 | const parts = key.split('.'); 19 | 20 | // Skip 21 | if (value === '') 22 | return; 23 | 24 | let last = parts.pop(); 25 | 26 | const match = last.match(/:(\w)$/); 27 | last = last.replace(/:\w$/, ''); 28 | 29 | if (match && match[1] === 'i') 30 | value = parseInt(value, 10); 31 | 32 | let dig = obj; 33 | for (let i = 0; i < parts.length; i++) { 34 | if (!dig[parts[i]]) 35 | dig[parts[i]] = {}; 36 | dig = dig[parts[i]]; 37 | } 38 | dig[last] = value; 39 | }); 40 | 41 | console.log(obj); 42 | prompt.get([ 'looks good?' ], (err, result) => { 43 | if (err) 44 | return callback(err); 45 | 46 | callback(null, obj); 47 | }); 48 | }); 49 | }; 50 | 51 | REST.prototype._getJSON = function _getJSON(res, callback) { 52 | let chunks = ''; 53 | res.on('data', chunk => chunks += chunk); 54 | res.once('end', () => { 55 | let value; 56 | 57 | try { 58 | value = JSON.parse(chunks); 59 | } catch (e) { 60 | return callback(e); 61 | } 62 | 63 | return callback(null, value); 64 | }); 65 | }; 66 | 67 | REST.prototype.get = function get(path, params, callback) { 68 | this._params(params, (err, query) => { 69 | if (err) 70 | return callback(err); 71 | 72 | http.request({ 73 | method: 'GET', 74 | host: this.host, 75 | port: this.port, 76 | path: path + '?' + qs.encode(query) 77 | }, (res) => { 78 | this._getJSON(res, callback); 79 | }).end(); 80 | }); 81 | }; 82 | 83 | REST.prototype.post = function post(path, params, callback) { 84 | this._params(params, (err, body) => { 85 | if (err) 86 | return callback(err); 87 | 88 | http.request({ 89 | method: 'POST', 90 | host: this.host, 91 | port: this.port, 92 | path: path, 93 | headers: { 94 | 'content-type': 'application/json' 95 | } 96 | }, (res) => { 97 | this._getJSON(res, callback); 98 | }).end(JSON.stringify(body)); 99 | }); 100 | }; 101 | 102 | module.exports = () => { 103 | const PORT = parseInt(process.argv[2], 10); 104 | const HOST = process.argv[3]; 105 | 106 | console.log('Request to %s:%d', HOST, PORT); 107 | return new REST(PORT, HOST); 108 | }; 109 | -------------------------------------------------------------------------------- /scripts/follow.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./common')().post('/api/follow', [ 4 | 'feed_key' 5 | ], (err, data) => { 6 | if (err) 7 | throw err; 8 | 9 | console.log(JSON.stringify(data, null, 2)); 10 | }); 11 | -------------------------------------------------------------------------------- /scripts/message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./common')().get('/api/message', [ 4 | 'feed_key', 'index:i' 5 | ], (err, data) => { 6 | if (err) 7 | throw err; 8 | 9 | console.log(JSON.stringify(data, null, 2)); 10 | }); 11 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scripts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "Fedor Indutny (http://darksi.de/)", 11 | "license": "MIT", 12 | "dependencies": { 13 | "prompt": "^1.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /scripts/post.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./common')().post('/api/post', [ 4 | 'content', 'reply_to.feed_key', 'reply_to.index:i' 5 | ], (err, data) => { 6 | if (err) 7 | throw err; 8 | 9 | console.log(JSON.stringify(data, null, 2)); 10 | }); 11 | -------------------------------------------------------------------------------- /scripts/timeline.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./common')().get('/api/timeline', [ 4 | 'feed_key', 'offset:i', 'limit:i' 5 | ], (err, data) => { 6 | if (err) 7 | throw err; 8 | 9 | console.log(JSON.stringify(data, null, 2)); 10 | }); 11 | -------------------------------------------------------------------------------- /scripts/trust.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./common')().post('/api/trust', [ 4 | 'feed_key', 'expires_in:i', 'description' 5 | ], (err, data) => { 6 | if (err) 7 | throw err; 8 | 9 | console.log(JSON.stringify(data, null, 2)); 10 | }); 11 | -------------------------------------------------------------------------------- /test/corn-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const tape = require('tape'); 5 | const rimraf = require('rimraf'); 6 | 7 | const TMP_DIR = path.join(__dirname, 'tmp'); 8 | const A_DIR = path.join(TMP_DIR, 'a'); 9 | const B_DIR = path.join(TMP_DIR, 'b'); 10 | 11 | const HyperCorn = require('../').HyperCorn; 12 | 13 | tape('HyperCorn test', (t) => { 14 | t.timeoutAfter(50000); 15 | 16 | rimraf.sync(TMP_DIR); 17 | 18 | const a = new HyperCorn({ storage: A_DIR }); 19 | const b = new HyperCorn({ storage: B_DIR }); 20 | 21 | a.listen(() => { 22 | b.listen(() => { 23 | a.getTimeline({ 24 | feedKey: b.getFeedKey(), 25 | offset: 0, 26 | limit: 10 27 | }, onBTimeline); 28 | }); 29 | }); 30 | 31 | function onBTimeline(err, timeline) { 32 | t.error(err, '`.getTimeline()` should not error'); 33 | 34 | t.equal(timeline.length, 1, 'one post expected'); 35 | t.equal(timeline[0].message.type, 'open', 'it should be `open`'); 36 | 37 | b.trust(a.getFeedKey(), { 38 | description: 'some info' 39 | }, onTrust); 40 | } 41 | 42 | function onTrust(err) { 43 | t.error(err, '`.trust()` should not error'); 44 | 45 | a.post({ 46 | content: 'reply', 47 | replyTo: { 48 | feedKey: b.getFeedKey(), 49 | index: 0 50 | } 51 | }, onPost); 52 | } 53 | 54 | function onPost(err) { 55 | t.error(err, '`.post()` should not error'); 56 | 57 | // The time to get the message is not deterministic 58 | setTimeout(() => { 59 | b.getMessage({ feedKey: b.getFeedKey(), index: 0 }, onMessage); 60 | }, 1000); 61 | } 62 | 63 | function onMessage(err, message) { 64 | t.error(err, '`.getMessage()` should not error'); 65 | t.equal(message.meta.length, 1, 'reply should get through'); 66 | t.equal(message.meta[0].type, 'reply', 'reply should have `reply` type'); 67 | 68 | const reply = message.meta[0].payload; 69 | t.deepEqual(reply.feedKey.toBuffer(), a.getFeedKey(), 'reply link feed'); 70 | t.equal(reply.index, 1, 'reply link index'); 71 | 72 | a.follow(b.getFeedKey(), onFollow); 73 | } 74 | 75 | function onFollow(err) { 76 | t.error(err, '`.follow()` should not error'); 77 | 78 | // Same thing, hyperbloom is eventually consistent 79 | setTimeout(() => { 80 | a.getMessage({ feedKey: b.getFeedKey(), index: 0 }, onRemoteMessage); 81 | }, 1000); 82 | } 83 | 84 | function onRemoteMessage(err, message) { 85 | t.error(err, 'remote `.getMessage()` should not error'); 86 | t.equal(message.meta.length, 1, 'reply should get through to remote too'); 87 | end(); 88 | } 89 | 90 | function end() { 91 | // TODO(indutny): investigate why teardown doesn't work 92 | t.end(); 93 | process.exit(0); 94 | } 95 | }); 96 | --------------------------------------------------------------------------------