├── .gitignore ├── server.js ├── bin ├── usage.txt └── cli.js ├── swarm-addr.js ├── files.js ├── package.json ├── rewrite-packagejson.js ├── registry.js ├── README.md ├── hyperdrive.js └── design.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var hyperdrive = require('./hyperdrive') 2 | var registry = require('./registry') 3 | 4 | module.exports = function (done) { 5 | var driver = hyperdrive() 6 | registry(driver, done) 7 | } 8 | -------------------------------------------------------------------------------- /bin/usage.txt: -------------------------------------------------------------------------------- 1 | USAGE: 2 | 3 | peer-npm i, install [-S] [-D] 4 | 5 | Works like `npm install`. Accepts a peer-npm package name to install from 6 | the swarm. 7 | 8 | peer-npm publish 9 | 10 | Works like `npm publish`. Publish the current package to the swarm. 11 | 12 | -------------------------------------------------------------------------------- /swarm-addr.js: -------------------------------------------------------------------------------- 1 | var SEP = '_' 2 | 3 | function parse (str) { 4 | var key = str.substring(str.lastIndexOf(SEP) + 1) 5 | str = str.substring(0, str.lastIndexOf(SEP)) 6 | var net = str.substring(str.lastIndexOf(SEP) + 1) 7 | var pkg = str.substring(0, str.lastIndexOf(SEP)) 8 | return { 9 | pkg: pkg, 10 | net: net, 11 | key: key 12 | } 13 | } 14 | 15 | function build (pkg, net, key) { 16 | return pkg + SEP + net + SEP + key 17 | } 18 | 19 | // TODO: this is wrong; what about packages with SEP in them? 20 | function is (str) { 21 | var p = str.split(SEP) 22 | return p.length === 3 23 | } 24 | 25 | module.exports = { 26 | parse: parse, 27 | build: build, 28 | is: is, 29 | SEP: SEP 30 | } 31 | -------------------------------------------------------------------------------- /files.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var mkdirp = require('mkdirp') 3 | var path = require('path') 4 | 5 | module.exports = function (root) { 6 | mkdirp.sync(root) 7 | 8 | this.isPeerPackage = function (pkg) { 9 | return fs.existsSync(path.join(root, pkg + '.json')) 10 | } 11 | 12 | this.writeTarball = function (filename, buffer, done) { 13 | fs.writeFile(path.join(root, filename), buffer, done) 14 | } 15 | 16 | this.writeMetadata = function (data, done) { 17 | fs.writeFile(path.join(root, data.name + '.json'), JSON.stringify(data), done) 18 | } 19 | 20 | this.fetchTarball = function (pkg, done) { 21 | var fn = path.join(root, pkg + '.json') 22 | if (fs.existsSync(fn)) { 23 | done(null, fs.createReadStream(fn)) 24 | } else { 25 | done(null, null) 26 | } 27 | } 28 | 29 | return this 30 | } 31 | 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peer-npm", 3 | "description": "A peer-to-peer substitute for npm.", 4 | "author": "Stephen Whitmore ", 5 | "version": "0.1.2", 6 | "bin": { 7 | "peer-npm": "bin/cli.js" 8 | }, 9 | "repository": { 10 | "url": "git://github.com/noffle/peer-npm.git" 11 | }, 12 | "homepage": "https://github.com/noffle/peer-npm", 13 | "bugs": "https://github.com/noffle/peer-npm/issues", 14 | "main": "index.js", 15 | "scripts": { 16 | "test": "tape test/*.js", 17 | "lint": "standard" 18 | }, 19 | "keywords": [ 20 | "peer", 21 | "npm", 22 | "distributed", 23 | "offline", 24 | "decentralized", 25 | "node", 26 | "package", 27 | "manager", 28 | "swarm" 29 | ], 30 | "dependencies": { 31 | "application-config-path": "^0.1.0", 32 | "body": "^5.1.0", 33 | "collect-stream": "^1.2.1", 34 | "comandante": "0.0.1", 35 | "discovery-swarm": "^4.2.0", 36 | "hyperdrive": "^7.14.3", 37 | "level": "^1.6.0", 38 | "mkdirp": "^0.5.1", 39 | "request": "^2.79.0", 40 | "routes": "^2.1.0", 41 | "traverse": "^0.6.6", 42 | "uniq": "^1.0.1" 43 | }, 44 | "devDependencies": { 45 | "tape": "~4.6.2", 46 | "standard": "~8.3.0" 47 | }, 48 | "license": "ISC" 49 | } 50 | -------------------------------------------------------------------------------- /rewrite-packagejson.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var swarmAddr = require('./swarm-addr') 3 | 4 | // TODO: async + error checking + graceful atomic failure 5 | function rewrite (filename, rewriteDoneCb, cleanupDoneCb) { 6 | cleanupDoneCb = cleanupDoneCb || function () {} 7 | 8 | // 1. Make a copy of the file in the same dir with '.orig' ext 9 | var contents = fs.readFileSync(filename, 'utf-8') 10 | fs.writeFileSync(filename + '.orig', contents) 11 | 12 | // 2. open + JSON parse package.json 13 | var pkgJson = JSON.parse(contents) 14 | 15 | // 3. pull out all regular deps that are also swarmdeps; hold on to them 16 | var dupes = extractNpmDupes(pkgJson) 17 | 18 | // 4. move all swarm deps into regular deps 19 | Object.keys(pkgJson['swarmDependencies'] || {}).forEach(function (key) { 20 | pkgJson['dependencies'][key] = pkgJson['swarmDependencies'][key] 21 | }) 22 | delete pkgJson['swarmDependencies'] 23 | 24 | // 5. shell out to npm 25 | rewriteDoneCb(cleanup) 26 | 27 | function cleanup () { 28 | // 6. reread package.json 29 | pkgJson = JSON.parse(fs.readFileSync(filename, 'utf-8')) 30 | 31 | // 7. move all swarm deps in regular deps back into swarm deps 32 | pkgJson['swarmDependencies'] = pkgJson['swarmDependencies'] || {} 33 | for (var key in pkgJson['dependencies']) { 34 | if (swarmAddr.is(key)) { 35 | pkgJson['swarmDependencies'][key] = pkgJson['dependencies'][key] 36 | delete pkgJson['dependencies'][key] 37 | } 38 | } 39 | 40 | // 8. move all held npm dupes back into deps 41 | for (var key in dupes) { 42 | pkgJson['dependencies'][key] = dupes[key] 43 | } 44 | 45 | // 9. write new package.json 46 | fs.writeFileSync(filename, JSON.stringify(pkgJson, null, 2)) 47 | 48 | // 9. remove '.orig'backup file 49 | fs.unlinkSync(filename + '.orig') 50 | 51 | // 10. done! 52 | cleanupDoneCb() 53 | } 54 | } 55 | 56 | function extractNpmDupes (pkgJson) { 57 | var dupes = {} 58 | for (var key in pkgJson['swarmDependencies']) { 59 | var pkg = swarmAddr.parse(key).pkg 60 | if (pkgJson['dependencies'][pkg]) { 61 | dupes[pkg] = pkgJson['dependencies'][pkg] 62 | delete pkgJson['dependencies'][pkg] 63 | } 64 | } 65 | return dupes 66 | } 67 | 68 | module.exports = rewrite 69 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var comandante = require('comandante') 4 | var fs = require('fs') 5 | var path = require('path') 6 | var spawn = require('child_process').spawn 7 | var config = require('application-config-path') 8 | var createServer = require('../server') 9 | var homedir = require('os').homedir 10 | var swarmAddr = require('../swarm-addr') 11 | var rewrite = require('../rewrite-packagejson') 12 | 13 | if (process.argv.length === 2) { 14 | printUsage() 15 | return 16 | } 17 | 18 | switch (process.argv[2]) { 19 | case 'install': 20 | case 'i': 21 | case 'remove': 22 | rewrite('package.json', function (done) { 23 | var args = ['--registry', 'http://localhost:9000'] 24 | args = args.concat(process.argv.slice(2)) 25 | var p = spawn('npm', args, {stdio:'inherit'}) 26 | p.on('close', done) 27 | }) 28 | break 29 | case 'publish': // TODO: output the package name /w public key 30 | if (!isNpmrcReady()) { 31 | process.stdout.write('Creating a new keypair..') 32 | initNpmrc() 33 | console.log('..done!\n') 34 | } 35 | 36 | var args = ['--registry', 'http://localhost:9000'] 37 | args = args.concat(process.argv.slice(2)) 38 | var p = spawn('npm', args) 39 | p.stderr.pipe(process.stderr) 40 | var version = '' 41 | p.stdout.on('data', function (line) { 42 | line = line.toString() 43 | version = line.substring(line.indexOf('@')+1) 44 | version = version.replace('\n', '') 45 | }) 46 | p.on('close', function (code) { 47 | if (code === 0) { 48 | var root = config('peer-npm') 49 | var pub = JSON.parse(fs.readFileSync(path.join(root, 'keys.json'), 'utf-8')).pub 50 | var name = JSON.parse(fs.readFileSync('package.json')).name 51 | // TODO: hack! not network agnostic! 52 | console.log('+ ' + name + swarmAddr.SEP + 'hyperdrive' + swarmAddr.SEP + pub) 53 | console.log('Published ' + version) 54 | } 55 | }) 56 | break 57 | case 'daemon': 58 | createServer(function (err, server) { 59 | console.log('listening on http://0.0.0.0:9000') 60 | }) 61 | break 62 | default: 63 | printUsage() 64 | break 65 | } 66 | 67 | function printUsage () { 68 | require('fs').createReadStream(__dirname + '/usage.txt').pipe(process.stdout) 69 | } 70 | 71 | function isNpmrcReady () { 72 | var npmrc = fs.readFileSync(path.join(homedir(), '.npmrc')) 73 | return npmrc.indexOf('//localhost:9000/:_authToken=baz') !== -1 74 | } 75 | 76 | function initNpmrc () { 77 | fs.appendFileSync(path.join(homedir(), '.npmrc'), '//localhost:9000/:_authToken=baz') 78 | } 79 | -------------------------------------------------------------------------------- /registry.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var fs = require('fs') 3 | var routes = require('routes') 4 | var url = require('url') 5 | var body = require('body') 6 | var request = require('request') 7 | 8 | module.exports = function (driver, done) { 9 | var router = routes() 10 | router.addRoute('/:pkg', onPackage) 11 | router.addRoute('/-/user/org.couchdb.user\::user', onAddUser) 12 | router.addRoute('/:pkg/-/:tarball', onTarball) 13 | 14 | var server = http.createServer(function (req, res) { 15 | console.log(req.method.toUpperCase() + ' ' + req.url) 16 | 17 | var path = url.parse(req.url).pathname 18 | var match = router.match(path) 19 | if (match) { 20 | match.fn(req, res, match) 21 | } else { 22 | res.statusCode = 404 23 | res.end() 24 | } 25 | }) 26 | 27 | server.listen(9000, function () { 28 | done(null, server) 29 | }) 30 | 31 | function onPackage (req, res, match) { 32 | if (req.method === 'GET') { 33 | var pkg = match.params.pkg 34 | if (driver.isPeerPackage(pkg)) { 35 | console.log(pkg + ' is a peer network package') 36 | // use peer network 37 | driver.fetchMetadata(pkg, function (err, meta) { 38 | if (err) { 39 | res.statusCode = 404 40 | } else { 41 | res.statusCode = 201 42 | res.write(JSON.stringify(meta)) 43 | } 44 | res.end() 45 | }) 46 | } else { 47 | // use npm 48 | console.log(pkg + ' is an npm package') 49 | req.pipe(request('http://registry.npmjs.org/'+pkg)).pipe(res) 50 | } 51 | } else if (req.method === 'PUT') { 52 | console.log('wants to publish', match.params.pkg) 53 | body(req, function (err, data) { 54 | if (err) { 55 | res.statusCode = 500 56 | res.end() 57 | return 58 | } 59 | data = JSON.parse(data) 60 | publishPackage(data, function (err) { 61 | if (err) { 62 | res.statusCode = 500 63 | } else { 64 | res.statusCode = 201 65 | } 66 | res.end() 67 | }) 68 | 69 | }) 70 | } else { 71 | res.statusCode = 404 72 | res.end() 73 | } 74 | } 75 | 76 | function publishPackage (data, done) { 77 | var attachments = data._attachments 78 | delete data._attachments 79 | 80 | var pending = 2 81 | var pkg = data.name 82 | driver.writeMetadata(pkg, data, function (err) { 83 | console.log('wrote meta') 84 | if (--pending === 0) done(err) 85 | }) 86 | writeAttachments(pkg, attachments, function (err) { 87 | console.log('wrote tarball') 88 | if (--pending === 0) done(err) 89 | }) 90 | } 91 | 92 | function writeAttachments (pkg, attachments, done) { 93 | var pending = Object.keys(attachments).length 94 | 95 | Object.keys(attachments).forEach(function (filename) { 96 | var data = new Buffer(attachments[filename].data, 'base64') 97 | driver.writeTarball(pkg, filename, data, function (err) { 98 | if (--pending === 0) done() 99 | }) 100 | }) 101 | } 102 | 103 | function fetchPackage (pkg, out, done) { 104 | console.log('fetch pkg', pkg) 105 | driver.fetchMetadata(pkg, done) 106 | } 107 | 108 | function onAddUser (req, res, match) { 109 | body(req, function (err, data) { 110 | driver.addUser({ 111 | name: data.name, 112 | email: data.email 113 | }, function (err) { 114 | if (err) { 115 | res.statusCode = 404 116 | } else { 117 | res.statusCode = 201 118 | } 119 | res.end() 120 | }) 121 | }) 122 | } 123 | 124 | function onTarball (req, res, match) { 125 | var tarball = match.params.tarball 126 | console.log('wants tarball:', tarball) 127 | driver.fetchTarball(tarball, function (err, stream) { 128 | if (err) { 129 | res.statusCode = 404 130 | res.end() 131 | } else { 132 | stream.pipe(res) 133 | } 134 | }) 135 | } 136 | 137 | return server 138 | } 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # peer-npm 2 | 3 | > an npm-compatible registry backed by peer-to-peer networks 4 | 5 | **NOTE**: Very unstable and mad science-y. Use at your own discretion. 6 | 7 | ## WHY would someone want something like this? 8 | 9 | - I want an easy way to use/publish/install packages when I'm offline 10 | - I want to be able to install/share packages /w my friends over LAN 11 | - I want my packages to be available & resistant to censorship & network failure 12 | - I want a fail-safe in case npm Inc ever goes away or is seized by the 13 | government 14 | - I want a package manager whose backend is 100% permissively open source 15 | 16 | ## Usage 17 | 18 | To be used just like vanilla `npm`, but with a subset of commands: `install`, 19 | `remove`, and `publish`. 20 | 21 | ``` 22 | USAGE: 23 | 24 | peer-npm i, install [-S] [-D] 25 | 26 | Works like `npm install`. Accepts a peer-npm package name to install from 27 | the swarm. 28 | 29 | peer-npm publish 30 | 31 | Works like `npm publish`. Publish the current package to the swarm. 32 | Generates a new keypair if one is not already present. 33 | 34 | ``` 35 | 36 | ## Getting started 37 | 38 | ### Install 39 | 40 | With [npm](https://npmjs.org/) installed, run 41 | 42 | ``` 43 | $ npm install --global peer-npm 44 | ``` 45 | 46 | ### Join the swarm 47 | 48 | In another window run 49 | 50 | ``` 51 | $ peer-npm daemon 52 | ``` 53 | 54 | so that you can download packages from others and share the ones you publish. 55 | 56 | ### Publish a module to the swarm 57 | 58 | Let's grab a package from github and try to publish it: 59 | 60 | ``` 61 | $ cd /tmp 62 | 63 | $ git clone https://github.com/noffle/resync-srt 64 | 65 | $ cd resync-srt 66 | 67 | $ npm install 68 | 69 | $ peer-npm publish 70 | + resync-srt_hyperdrive_c5abee5fd496620499c3d203f15c95d24a51d16ec05dea4a8ab2c88368c296b9 71 | Published 3.1.0 72 | ``` 73 | 74 | `resync-srt` is now in the swarm! The name of the package is made of three 75 | parts, concatenated by underscores: the package name, the peer network its 76 | shared on, and the public key of the publisher. 77 | 78 | ### Install a swarm dependency 79 | 80 | Let's make a new package that depends on `resync-srt`: 81 | 82 | ``` 83 | $ cd /tmp 84 | $ mkdir foobar 85 | $ cd foobar 86 | 87 | $ npm init 88 | 89 | # you'll want to use the package name generated from the last step 90 | $ peer-npm install --save resync-srt_hyperdrive_c5abee5fd496620499c3d203f15c95d24a51d16ec05dea4a8ab2c88368c296b9 91 | ``` 92 | 93 | If you look in your `package.json` you'll see a new section called 94 | `swarmDependencies`. This lets `peer-npm` know what packages you depend on in 95 | the swarm, but in a way that keeps vanilla `npm` working. 96 | 97 | In fact, you can have a package in both `swarmDependencies` *and* regular 98 | `dependencies`. Using `peer-npm` won't break your package for non-`peer-npm` 99 | users. 100 | 101 | 102 | ## How does it work? 103 | 104 | `peer-npm` pretends to be an npm registry, but running on your local machine. 105 | When you run `peer-npm daemon` it runs this registry (and also does the peering 106 | logic). 107 | 108 | `peer-npm install` is mostly a wrapper for something like `npm install 109 | --registry=http://localhost:9000`. 110 | 111 | When you publish or try to install a package, `peer-npm` looks at its name to 112 | decide whether it is a package from the central npm registry, or from the swarm. 113 | 114 | npm packages have a name like `field-trip`, whereas swarm packages have a name 115 | like `field-trip_hyperdrive_79cf7ecc9baf627642099542b3714bbef`. The part after 116 | the name is the public key of the author. This makes packages resiliant against 117 | impersonation or malicious peers. 118 | 119 | `peer-npm` can work with different peer networks; right now there is only a 120 | [hyperdrive](https://github.com/mafintosh/hyperdrive) driver, which is the 121 | default. 122 | 123 | When you run `peer-npm install` it will find other peers with the packages you 124 | want and download them, recursively down the dependency tree. Similarly, when 125 | you run `peer-npm publish`, the new package's key is shared amongst other 126 | `peer-npm` peers for future discovery. 127 | 128 | ## IRC 129 | 130 | Come hang out in `#peer-npm` on Freenode to help test and develop! 131 | 132 | ## License 133 | 134 | ISC 135 | 136 | -------------------------------------------------------------------------------- /hyperdrive.js: -------------------------------------------------------------------------------- 1 | var level = require('level') 2 | var hyperdrive = require('hyperdrive') 3 | var config = require('application-config-path') 4 | var fs = require('fs') 5 | var path = require('path') 6 | var mkdirp = require('mkdirp') 7 | var collect = require('collect-stream') 8 | var Swarm = require('discovery-swarm') 9 | var swarmAddr = require('./swarm-addr') 10 | var uniq = require('uniq') 11 | 12 | var NETWORK = 'hyperdrive' 13 | 14 | module.exports = function () { 15 | var root = config('peer-npm') 16 | mkdirp.sync(root) 17 | var drive = hyperdrive(level(path.join(root, 'packages.db'))) 18 | 19 | var keys 20 | var archive 21 | if (fs.existsSync(path.join(root, 'keys.json'))) { 22 | keys = JSON.parse(fs.readFileSync(path.join(root, 'keys.json'), 'utf-8')) 23 | archive = drive.createArchive(keys.pub, { live: true }) 24 | console.log('found existing keypair + archive: ' + keys.pub) 25 | 26 | // TODO: use base58 encoding for keys 27 | // var k = new Buffer(keys.pub, 'hex') 28 | // console.log(k) 29 | // var out = require('bs58').encode(k) 30 | // console.log(out) 31 | // console.log(keys.pub) 32 | } else { 33 | archive = drive.createArchive({live: true}) 34 | keys = { 35 | pub: archive.key.toString('hex'), 36 | prv: archive.metadata.secretKey.toString('hex') 37 | } 38 | fs.writeFileSync(path.join(root, 'keys.json'), JSON.stringify(keys)) 39 | console.log('created brand new keypair + archive: ' + keys.pub) 40 | } 41 | 42 | archive.list(function (err, entries) { 43 | console.log('--- current entries ---') 44 | entries = entries.filter(function (e) { 45 | return e.name.endsWith('.json') 46 | }) 47 | entries = entries.map(function (e) { 48 | return e.name.substring(0, e.name.length - 5) 49 | }) 50 | entries.sort() 51 | uniq(entries) 52 | entries.forEach(function (e) { 53 | console.log(e) 54 | }) 55 | console.log('---') 56 | }) 57 | 58 | function host () { 59 | var link = archive.key.toString('hex') 60 | 61 | var swarm = Swarm() 62 | swarm.listen() 63 | swarm.join(link) 64 | swarm.on('connection', function (connection, info) { 65 | console.log('[HOST] found a peer: ', info.id.toString('hex')) 66 | var r = archive.replicate() 67 | connection.pipe(r).pipe(connection) 68 | r.on('end', function () { 69 | console.log('replicated with peer to share', link) 70 | }) 71 | r.on('error', function (err) { 72 | console.log('ERROR REPLICATION:', err) 73 | }) 74 | }) 75 | return swarm 76 | } 77 | 78 | // TODO: clean up archive when done 79 | function getArchive (key, done) { 80 | console.log('getting archive', key) 81 | var archive = drive.createArchive(key) 82 | done(null, archive) 83 | 84 | var swarm = Swarm() 85 | swarm.listen() 86 | swarm.join(key) 87 | swarm.on('connection', function (connection, info) { 88 | console.log('[PEER] found a peer: ', info.id.toString('hex')) 89 | var r = archive.replicate() 90 | connection.pipe(r).pipe(connection) 91 | r.on('end', function () { 92 | console.log('replicated with peer to share', key) 93 | }) 94 | r.on('error', function (err) { 95 | console.log('ERROR REPLICATION:', err) 96 | }) 97 | }) 98 | } 99 | 100 | var swarm = host() 101 | 102 | this.isPeerPackage = function (pkg) { 103 | return swarmAddr.is(pkg) 104 | } 105 | 106 | this.writeTarball = function (pkg, filename, buffer, done) { 107 | filename = filename.replace(pkg, swarmAddr.build(pkg, NETWORK, keys.pub)) 108 | var ws = archive.createFileWriteStream(filename) 109 | ws.on('end', done) 110 | ws.on('finish', done) 111 | ws.on('close', done) 112 | ws.write(buffer) 113 | ws.end() 114 | console.log('writing', filename) 115 | } 116 | 117 | this.writeMetadata = function (pkg, data, done) { 118 | var outname = swarmAddr.build(pkg, NETWORK, keys.pub) 119 | 120 | // rewrite FOO to FOO_hyperdrive_publickey 121 | data._id = outname 122 | data.name = outname 123 | Object.keys(data.versions).forEach(function (version) { 124 | var v = data.versions[version] 125 | v.name = outname 126 | var r = new RegExp(pkg, 'g') 127 | v.dist.tarball = v.dist.tarball.replace(r, outname) 128 | }) 129 | 130 | // move swarmDependencies into dependencies 131 | moveSwarmDepsIntoRegularDeps(data) 132 | 133 | var ws = archive.createFileWriteStream(outname + '.json') 134 | ws.on('finish', done) 135 | ws.on('error', done) 136 | ws.on('close', done) 137 | ws.write(JSON.stringify(data)) 138 | ws.end() 139 | console.log('writing', outname + '.json') 140 | } 141 | 142 | this.fetchMetadata = function (addr, done) { 143 | var key = swarmAddr.parse(addr).key 144 | getArchive(key, function (err, archive) { 145 | if (err) return done(err) 146 | var filename = addr + '.json' 147 | collect(archive.createFileReadStream(filename), function (err, data) { 148 | if (err) return done(err) 149 | var json = JSON.parse(data.toString()) 150 | done(null, json) 151 | }) 152 | }) 153 | } 154 | 155 | this.fetchTarball = function (filename, done) { 156 | var idx = filename.lastIndexOf(swarmAddr.SEP) 157 | var pkg = filename.substring(idx+1, idx+64+1) 158 | 159 | getArchive(pkg, function (err, archive) { 160 | if (err) return done(err) 161 | var rs = archive.createFileReadStream(filename) 162 | done(null, rs) 163 | }) 164 | } 165 | 166 | this.addUser = function (user, done) { 167 | // TODO: generate + write keypair 168 | done() 169 | } 170 | 171 | return this 172 | } 173 | 174 | function moveSwarmDepsIntoRegularDeps (data) { 175 | Object.keys(data.versions).forEach(function (version) { 176 | var v = data.versions[version] 177 | 178 | for (var key in v['swarmDependencies']) { 179 | v['dependencies'][key] = v['swarmDependencies'][key] 180 | } 181 | }) 182 | } 183 | 184 | -------------------------------------------------------------------------------- /design.md: -------------------------------------------------------------------------------- 1 | # WHY would someone want something like this? 2 | 3 | - I want to publish my package to npm AND "the swarm" so that my package is more 4 | reliably available 5 | - I want an easy way to use/publish/install packages when I'm offline 6 | - I want to be able to install/share packages /w my friends over LAN 7 | - I want my packages to be available & resistant to censorship 8 | - I want a package manager whose backend is 100% permissively open source 9 | - I think the future of the node community doesn't belong in the hands of a 10 | for-profit company 11 | - I want a fail-safe in case npm Inc ever goes belly-up or is seized by the 12 | government 13 | 14 | 15 | # How could developers use peer-npm? 16 | 17 | 1. Exclusively: `peer-npm publish --recursive` publishes both my package and all 18 | of its dependencies to the swarm. I and users can use my package and any of 19 | its (pinned?) deps without npm. 20 | 2. Both; npm preferred: I publish my package and its deps to the swarm, but for 21 | development and everyday use, I use `npm` and centralized dependencies. 22 | 3. Both; peer-npm preferred: ??? 23 | 24 | 25 | # Interoperation of peer-npm and npm 26 | 27 | 1. Does running `peer-npm install` in a directory install JUST swarm packages, 28 | or also npm ones? 29 | 2. Can I use `peer-npm SUBCOMMAND` on any package, or just swarm ones? 30 | 3. Can I define clear boundaries between `peer-npm` and `npm`, or do I need to 31 | make `peer-npm` layer nicely over top of `npm`? 32 | 4. Does using `peer-npm` "poison the well"? Can I not go back to using vanilla 33 | `npm` now that I have swarm deps in my `package.json`? 34 | 35 | ## Interop FAQ 36 | 37 | ### How do I get started? 38 | 39 | Install `peer-npm` and keep the peering node running in the background. 40 | 41 | ``` 42 | $ npm install -g peer-npm 43 | 44 | $ peer-npm daemon 45 | ``` 46 | 47 | Now you can publish an existing package of yours using `peer-npm publish`, or 48 | install swarm dependencies using `peer-npm install`. 49 | 50 | Swarm dependencies look different than regular npm names. They are a 51 | concatenation of a human-readable name and a public key, like 52 | 53 | ``` 54 | field-trip_79cf7ecc9baf627642099542b3714bbef 55 | ``` 56 | 57 | This name is both readable by humans, and also uniquely and verifiably 58 | identifies a package. Simply run 59 | 60 | ``` 61 | $ peer-npm install field-trip_79cf7ecc9baf627642099542b3714bbef 62 | ``` 63 | 64 | and use the module just as you would any regular npm dependency. 65 | 66 | Publishing is just as easy as vanilla `npm`: 67 | 68 | ``` 69 | $ peer-npm publish 70 | + field-trip_79cf7ecc9baf627642099542b3714bbef@1.2.1 71 | ``` 72 | 73 | By default, all of the package's dependencies also published to the swarm, and 74 | the dependencies are rewritten to reflect this, not unlike `npm shrinkwrap` 75 | (TODO: check this). If you don't want this behaviour and only want the top-level 76 | package published to the swarm, use `peer-npm publish --shallow`. 77 | 78 | 79 | ### Can I publish to the swarm, but keep using npm otherwise? 80 | 81 | Yes. When ready to publish, run 82 | 83 | ``` 84 | peer-npm publish && npm publish 85 | ``` 86 | 87 | to publish, and share the resulting package name with friends/family/felines. 88 | This will publish your package AND all of its dependencies to the swarm. 89 | 90 | Your `package.json` will *not* be modified. 91 | 92 | ### Can I publish my package to the swarm but keep npm dependencies? 93 | 94 | Yes. Use 95 | 96 | ``` 97 | peer-npm publish --shallow 98 | ``` 99 | 100 | to keep npm dependencies intact. 101 | 102 | ### I cloned a repo that has swarm and npm dependencies. How do I proceed? 103 | 104 | `peer-npm` understands both types, and can tell which is which. Simply run 105 | 106 | ``` 107 | peer-npm install 108 | ``` 109 | 110 | And both types of dependencies will be installed. 111 | 112 | ### Someone gave me the name of their swarm package. How do I depend on it? 113 | 114 | Install it just like you would with `npm`, but using `peer-npm`: 115 | 116 | ``` 117 | peer-npm install -S field-trip_79cf7ecc9baf627642099542b3714bbef 118 | ``` 119 | 120 | ### Do I have to use the peer-npm command for everything now? 121 | 122 | If you wish to install a package that depends on swarm dependencies, you MUST 123 | use `peer-npm install`. 124 | 125 | Otherwise, for npm subcommands that operate on a specific package, use 126 | `peer-npm` for swarm dependencies, and `npm` for the rest. 127 | 128 | Things like `peer-npm info FOO` will work, while others, like `peer-npm star 129 | FOO` will not. 130 | 131 | ### How do I use peer-npm and the swarm exclusively? 132 | 133 | 1. create a brand new directory & package (I like `pkginit`) 134 | 2. install dependencies using `peer-npm install -S ` 135 | 3. when you're ready to share, run `peer-npm publish` and share the resulting 136 | identifier with friends/family/felines. 137 | 4. everyone else using peer-npm can now install your package with `peer-npm 138 | install `! 139 | 140 | ## How do I find packages? Is there a search feature? 141 | 142 | TODO: this 143 | 144 | ## How can I have a hybrid package that others can use npm with but peer-npm users can rely on the swarm? 145 | 146 | `peer-npm` stores all p2p network dependencies under a special key of 147 | `package.json` that stock `npm` does not look at: `swarmDependencies`. 148 | 149 | This means you can continue to use `npm` as per normal, but rely on peer network 150 | for fetching dependencies if you choose to use `peer-npm`. peer-npm won't 151 | "poison" your `package.json` in such a way that `npm` ceases to function. 152 | 153 | ## How will interop work with other p2p networks? 154 | 155 | A swarm package is identified by its name, the network it resides on, and a 156 | unique identifier. A package on hyperdrive, for example, might be 157 | 158 | ``` 159 | airfile_hdrive_ac06be2400c40f2c90f5f5282d57877b2de1674f5a736d3d9ae7c29e491d1a5c 160 | ``` 161 | 162 | Daemons will be spun up as required. 163 | 164 | ## How will swarmDependencies work when a dep has its own swarmDependencies? 165 | 166 | So, I do a `peer-npm install 167 | airfile_hdrive_ac06be2400c40f2c90f5f5282d57877b2de1674f5a736d3d9ae7c29e491d1a5c`, 168 | but that module has, say, `xhr_hdrive_publickey` in its swarmDependencies. 169 | 170 | Currently: it won't work. The current logic only pulls swarm deps out of the top 171 | level package you're currently working with. 172 | 173 | Is this even possible without digging into the npm client program? 174 | 175 | What if, at `peer-npm publish`-time, we published the module with its 176 | swarmDependencies in the dependencies slot? 177 | 178 | Ah ha! So then if you do a `git clone` or want to work /w the source code it'll 179 | work just fine (proper partitioning), but if you `peer-npm install` it, it'll 180 | recursively use the swarm deps available. Smart! <3 181 | 182 | TODO: 183 | - rewrite swarmDependencies -> dependencies in driver.writeMetadata 184 | - means discarding npm dupes 185 | 186 | 187 | 188 | # npm adduser 189 | 190 | This generates a new public key and places it in 191 | `$HOME/.peer-npm/key.{pub,prv}`. This is used for future publishing. Users ought 192 | not lose this. 193 | 194 | 195 | # Installing a package 196 | 197 | Behave like `npm`, except package names are their name concatenated with the 198 | hex-string of the publisher's public key. 199 | 200 | The package is placed in `node_modules` like any other module, and is structured 201 | identically. 202 | 203 | ``` 204 | $ nps i -S field-trip_79cf7ecc9baf627642099542b3714bbef 205 | 206 | $ cat package.json 207 | ... 208 | "dependencies": { 209 | "field-trip_79cf7ecc9baf627642099542b3714bbef": "^1.2.0" 210 | }, 211 | ... 212 | ``` 213 | 214 | # Publishing a package 215 | 216 | Same syntax as `npm`, except the package and its tarball goes to the swarm. 217 | 218 | By default, dependencies are *not* also published. However, the user can provide 219 | `--recursive` to also published pinned dependencies to the swarm. 220 | 221 | ``` 222 | $ nps publish 223 | + field-trip_79cf7ecc9baf627642099542b3714bbef@1.2.1 224 | ``` 225 | 226 | 227 | # Security concerns 228 | 229 | ## Name phishing via npm 230 | 231 | Someone could publish a package on npm with the same name as a package on 232 | peer-npm that is compromised. If a user got confused and used npm where they 233 | should have used peer-npm, they'd install the compromised package. 234 | 235 | 236 | # See Also 237 | 238 | This is hardly a new idea! 239 | 240 | - http://everythingstays.com 241 | - https://github.com/ipmjs/ipmjs 242 | - https://github.com/elsehow/gx-js 243 | - https://github.com/sterpe/gx-js 244 | - https://github.com/dominictarr/npmd 245 | - https://github.com/ipfs/ipfs-npm/ 246 | - https://github.com/diasdavid/npm-on-ipfs 247 | - https://github.com/watson/npm-to-hypercore 248 | 249 | --------------------------------------------------------------------------------