├── .dockerignore ├── .env.development ├── .gitignore ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── app.js ├── bin └── registry ├── lib ├── gitlab.js └── shasum_url.js ├── middleware ├── curry.js └── request_hostname.js ├── package.json └── routes └── index.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | .env.local 4 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | DEBUG=registry* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:0.10.32 2 | MAINTAINER Roman Shtylman 3 | 4 | RUN mkdir -p /usr/src/app 5 | WORKDIR /usr/src/app 6 | 7 | EXPOSE 8000 8 | ENTRYPOINT ["bin/registry"] 9 | 10 | ADD package.json /usr/src/app/ 11 | RUN npm install --production 12 | 13 | ADD . /usr/src/app 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Roman Shtylman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node bin/registry 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # npm-gitlab (fork of defunctzombie/npm-github) 2 | 3 | Example and demo registry to allow for npm to install package directly from gitlab. Works for public and private repositories. 4 | 5 | # why 6 | 7 | Private npm registries are cool, they are often cumbersome to setup and are a bit of an overkill right now. Since your sourcecode already lives on gitlab, why not just install directly from gitlab using npm. 8 | 9 | The problem with the current support for gitlab in npm is that it doesn't work well for private repos. To download private repos you have to use the less elegant git+ssh uri syntax and make your package.json files look terrible. 10 | 11 | To solve these issues and provide a "cheap" (both in cost and complexity) solution to installing from gitlab via npm, an npm registry proxy can be used. Previously, you would need to proxy all requests through this fake registry but with the addition of the [scopes](http://blog.nodejitsu.com/a-summary-of-scoped-modules-in-npm/) this can actually be done on a per organization or user level and also makes installing private repos as modules much simpler. 12 | 13 | # setup 14 | 15 | You need to use npm v1.5+ for the scopes features. The easiest way to do this without messing up your existing install is to use the `npm-next` module. Install it globally (as shown below) and run it whenever you would normally run the `npm` cli command. 16 | 17 | ``` 18 | $ npm i -g npm-next 19 | $ npm-next -v 20 | 2.0.0-beta.2 21 | ``` 22 | 23 | ## configure npm 24 | 25 | npm needs to be told where to find our scoped modules. In our example, we are going to download a module from my personal gitlab account. To educate npm about how to handle `@defunctzombie` scope, add the following to your `$HOME/.npmrc` file 26 | 27 | ``` 28 | @defunctzombie:registry=http://npm-gitlab.herokuapp.com 29 | ``` 30 | 31 | This tells npm that whenever it sees a module that starts with `@defunctzombie/` to download it from the npm-gitlab.herokuapp.com registry and not the default main npm registry. 32 | 33 | The same process is used for organizations. 34 | 35 | ## install a scoped module 36 | 37 | I have created a demo scoped module to test your setup. Run the command below. Notice that this module [scope-test-02](https://gitlab.com/defunctzombie/scope-test-02) and its dependency do not live on the npm registry but are downloaded via the demo proxy and installed as regular modules. 38 | 39 | ``` 40 | $ npm-next install @defunctzombie/scope-test-02 41 | ``` 42 | 43 | If everything is configured correctly you should see the following output 44 | 45 | ``` 46 | @defunctzombie/scope-test-02@0.0.2 node_modules/@defunctzombie/scope-test-02 47 | └── @defunctzombie/scope-test-01@0.0.2 48 | ``` 49 | 50 | If you wish to add the scoped module to your app's package.json, simply use the full scoped name as the module name. 51 | 52 | ```json 53 | { 54 | "dependencies": { 55 | "@defunctzombie/scope-test-01": "0.0.2" 56 | } 57 | } 58 | ``` 59 | 60 | # private repositories 61 | 62 | If you have a private gitlab repository and wish to install that as an npm module the registry proxy supports that too. One benefit to the approach used here is that you can continue using gitlab to manage user access to your repositories without configuring another permission system. 63 | 64 | First, you need to create an oauth token using whatever gitlab account has access to the private repo you wish to install. Follow [these](https://help.gitlab.com/articles/creating-an-access-token-for-command-line-use) instructions from gitlab. 65 | 66 | Then, once again edit your `.npmrc` file and add the following lines after the existing `@defunctzombie/` line. Note, you will need these lines under every `@user` or `@org` that has private repos you wish to install. 67 | 68 | ``` 69 | @defunctzombie:registry=http://npm-gitlab.herokuapp.com 70 | //npm-gitlab.herokuapp.com/:always-auth=true 71 | //npm-gitlab.herokuapp.com/:_authToken= 72 | ``` 73 | 74 | These credentials WILL BE SENT to the heroku demo app which will use them to grant access to the private repository. 75 | 76 | That's it! You can now install any private repo your user account has access to via npm. For CI systems, simply make a CI gitlab user and token with only the repo access you need. 77 | 78 | **Note**: while I am not storing any gitlab auth tokens, PLEASE run your own copy of the registry code for any serious use. The heroku app is only a demo. 79 | 80 | # gotchas and tips 81 | 82 | * You must tag the version in gitlab before you can install it. 83 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var debug = require('debug')('registry'); 3 | 4 | var curry = require('./middleware/curry'); 5 | var req_hostname = require('./middleware/request_hostname'); 6 | var router = require('./routes'); 7 | 8 | var app = express(); 9 | 10 | // enables use of req.curry(fn) to handle error first callbacks 11 | app.use(curry); 12 | app.use(req_hostname); 13 | 14 | app.use(function(req, res, next) { 15 | var auth_header = req.headers.authorization; 16 | if (!auth_header) { 17 | return next(); 18 | } 19 | 20 | if (auth_header.indexOf('Basic') >= 0) { 21 | return next(); 22 | } 23 | 24 | debug('Authorization header found'); 25 | var token = auth_header.replace('Bearer ', ''); 26 | 27 | debug('oauth token %s', token); 28 | req.oauth_token = token; 29 | next(); 30 | }); 31 | 32 | app.use(router); 33 | 34 | // if not already handled, it is a 404 35 | app.use(function(req, res, next) { 36 | res.status(404).end(); 37 | }); 38 | 39 | app.use(function(err, req, res, next) { 40 | var status = err.status || err.statusCode || 500; 41 | console.error(err.stack); 42 | res.status(status).json({ 43 | message: err.message || 'oops!' 44 | }); 45 | }); 46 | 47 | module.exports = app; 48 | -------------------------------------------------------------------------------- /bin/registry: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var dotenv = require('dotenv').load(); 3 | var debug = require('debug')('registry'); 4 | 5 | var app = require('../app'); 6 | 7 | var port = process.env.PORT || 8000; 8 | var server = app.listen(port, function() { 9 | debug('listening on %d', port); 10 | }); 11 | -------------------------------------------------------------------------------- /lib/gitlab.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | 3 | var uri = process.env.GITLAB_URL || 'https://gitlab.com/api/v3'; 4 | 5 | module.exports.tags = function tags(user, repo, opt, cb) { 6 | // TODO: Change this to gitlab url 7 | // http://gitlab.eirenerx.com/projects?private_token= 8 | // get project id 9 | // then get tags /projects/:id/repository/tags 10 | 11 | var options = { 12 | url: uri + '/repos/' + user + '/' + repo + '/tags', 13 | json: true, 14 | headers: { 15 | 'User-Agent': 'npm-gitlab-proxy', 16 | 'Accepts': 'application/json' 17 | } 18 | }; 19 | 20 | if (opt.token) { 21 | options.headers['Authorization'] = 'token ' + opt.token 22 | } 23 | 24 | request(options, function(err, res, body) { 25 | if (err) { 26 | return cb(err); 27 | } 28 | 29 | if (res.statusCode !== 200) { 30 | return cb(new Error('unable to get tags for repo ' + user + '/' + repo)); 31 | } 32 | 33 | cb(null, body); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /lib/shasum_url.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | var request = require('request'); 3 | var debug = require('debug')('registry:shasum'); 4 | 5 | // TODO database cache for restart persistence 6 | var cache = Object.create(null); 7 | 8 | function shasum_url(url, opt, cb) { 9 | debug('shasum %s', url); 10 | var shasum = crypto.createHash('sha1'); 11 | 12 | if (cache[url]) { 13 | return setImmediate(cb, null, cache[url]); 14 | } 15 | 16 | var options = { 17 | url: url, 18 | headers: { 19 | 'User-Agent': 'npm-github-proxy' 20 | } 21 | }; 22 | 23 | if (opt.token) { 24 | options.headers['Authorization'] = 'token ' + opt.token 25 | } 26 | 27 | request(options) 28 | .on('data', function(chunk) { 29 | shasum.update(chunk); 30 | }) 31 | .on('error', cb) 32 | .on('end', function() { 33 | var hash = cache[url] = shasum.digest('hex'); 34 | cb(null, hash); 35 | }); 36 | } 37 | 38 | module.exports = shasum_url; 39 | -------------------------------------------------------------------------------- /middleware/curry.js: -------------------------------------------------------------------------------- 1 | module.exports = function(req, res, next) { 2 | req.curry = function(fn) { 3 | return function(err, arg1) { 4 | if (err) { 5 | return next(err); 6 | } 7 | 8 | fn.apply(null, Array.prototype.slice.call(arguments, 1)); 9 | } 10 | }; 11 | 12 | next(); 13 | }; 14 | -------------------------------------------------------------------------------- /middleware/request_hostname.js: -------------------------------------------------------------------------------- 1 | var url = require('url'); 2 | 3 | // return the hostname with protocol for the request 4 | // @param {Request} req is an http server client request 5 | function hostname(req) { 6 | var host_header = req.headers.host; 7 | return url.format({ 8 | protocol: req.protocol, 9 | slashes: true, 10 | host: host_header, 11 | }); 12 | }; 13 | 14 | module.exports = function(req, res, next) { 15 | req.href = hostname(req); 16 | next(); 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-github-registery", 3 | "version": "0.1.0", 4 | "author": "Roman Shtylman ", 5 | "license": "MIT", 6 | "dependencies": { 7 | "async": "0.9.0", 8 | "debug": "1.0.4", 9 | "express": "4.8.7", 10 | "httperrors": "0.5.0", 11 | "request": "2.40.0", 12 | "dotenv": "defunctzombie/dotenv#84d8f45" 13 | }, 14 | "engines": { 15 | "node": "0.10.31" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var NotFound = require('httperrors').NotFound; 3 | var debug = require('debug')('registry:router'); 4 | var request = require('request'); 5 | var async = require('async'); 6 | 7 | var shasum_url = require('../lib/shasum_url'); 8 | var gitlab = require('../lib/gitlab'); 9 | 10 | var gitlab_uri = process.env.GITLAB_URL || 'https://gitlab.com/api/v3'; 11 | 12 | var router = express.Router(); 13 | 14 | // parse the module name into a the separate user/repo parts 15 | // put that into req.module_spec for future routes 16 | router.param('module', function(req, res, next, name) { 17 | debug('module name', name); 18 | 19 | // we only support '@' scoped modules currently 20 | if (name[0] !== '@') { 21 | return next(NotFound); 22 | } 23 | 24 | var parts = name.replace('@', '').split('/'); 25 | 26 | var spec = req.module_spec = { 27 | name: name, 28 | user: parts[0], 29 | repo: parts[1] 30 | }; 31 | 32 | debug(spec); 33 | next(); 34 | }); 35 | 36 | // get details for module from github 37 | // assemble the details by querying for the tags 38 | // then shasum of each tag tarball to create the distribution list 39 | // we have to do this for all tags because npm is stupid and doesn't tell us what 40 | // version the person wants to install 41 | // ... have not yet found a hack to make existing npm do that... complain to npm authors more 42 | router.get('/:module', function(req, res, next) { 43 | var spec = req.module_spec; 44 | 45 | var user = spec.user; 46 | var repo = spec.repo; 47 | debug('details for %s/%s', user, repo); 48 | 49 | var opt = { 50 | token: req.oauth_token 51 | }; 52 | 53 | function shasum_tag_map(tag, cb) { 54 | var name = tag.name; 55 | var version = name.replace(/^v/, ''); 56 | 57 | shasum_url(tag.tarball_url, opt, function(err, shasum) { 58 | if (err) { 59 | return cb(err) 60 | } 61 | 62 | cb(null, { 63 | name: name, 64 | version: version, 65 | shasum: shasum, 66 | tarball: tag.tarball_url 67 | }); 68 | }); 69 | }; 70 | 71 | gitlab.tags(user, repo, opt, req.curry(function(tags) { 72 | if (tags.length === 0) { 73 | return next(NotFound()); 74 | } 75 | 76 | async.mapSeries(tags, shasum_tag_map, req.curry(function(sha_tags) { 77 | var latest_version = sha_tags[0].version; 78 | 79 | var map = Object.create(null); 80 | sha_tags.forEach(function(tag) { 81 | map[tag.version] = { 82 | name: spec.name, 83 | version: tag.version, 84 | dist: { 85 | shasum: tag.shasum, 86 | tarball: req.href + '/' + encodeURIComponent(spec.name) + '/' + tag.name + '/tarball' 87 | } 88 | }; 89 | }); 90 | 91 | res.json({ 92 | name: spec.name, 93 | description: spec.name, 94 | versions: map, 95 | // need to have latest match the single version 'tag' 96 | // so that '*' in package.json works 97 | 'dist-tags': { 98 | latest: latest_version 99 | } 100 | }); 101 | })); 102 | })); 103 | }); 104 | 105 | // serve up the tarball 106 | // use this proxy versus direct github for authroization 107 | router.get('/:module/:version/tarball', function(req, res, next) { 108 | var spec = req.module_spec; 109 | var user = spec.user; 110 | var repo = spec.repo; 111 | var tag = req.param('version'); 112 | 113 | //https://api.github.com/repos///tarball/' 114 | 115 | // TODO: Change this to gitlab url 116 | // http://gitlab.eirenerx.com///repository/archive.tar.gz?ref= 117 | 118 | var tarball_url = gitlab_uri + '/repos/' + user + '/' + repo + '/tarball/' + tag; 119 | 120 | debug('proxy tarball', tarball_url); 121 | 122 | var options = { 123 | url: tarball_url, 124 | headers: { 125 | 'User-Agent': 'npm-github-proxy', 126 | } 127 | }; 128 | 129 | if (req.oauth_token) { 130 | options.headers['Authorization'] = 'token ' + req.oauth_token 131 | } 132 | 133 | request(options).pipe(res); 134 | }); 135 | 136 | module.exports = router; 137 | --------------------------------------------------------------------------------