├── README.md ├── test.js └── app.js /README.md: -------------------------------------------------------------------------------- 1 | # Install 2 | 3 | You'll need CouchDB version 1.0.0 or higher. We're using some newish features. 4 | I recommend getting one from http://couchone.com/ 5 | 6 | Add a vhost config: 7 | 8 | [vhosts] 9 | packages:5984 = /jsregistry/_design/app/_rewrite 10 | 11 | Where `packages` is the hostname where you'll be running the thing, and `5984` is the port that CouchDB is running on. If you're running on port 80, then omit the port altogether. 12 | 13 | Now install couchapp: 14 | 15 | npm install couchapp 16 | 17 | Now run the sync app.js from this repository. 18 | 19 | couchapp --design app.js --sync --couch http://localhost:5984/jsregistry 20 | 21 | You may need to put a username and password in the URL: 22 | 23 | couchapp --design app.js --sync --couch http://user:pass@localhost:5984/jsregistry 24 | 25 | # API 26 | 27 | ### GET /packagename 28 | 29 | Returns the JSON document for this package. Includes all known dists and metadata. Example: 30 | 31 | { 32 | "name": "foo", 33 | "dist-tags": { "stable": "0.1" }, 34 | "_id": "foo", 35 | "versions": { 36 | "0.1": { 37 | "name": "foo", 38 | "_id": "foo", 39 | "version": "0.1", 40 | "dist": { "tarball": "http:\/\/domain.com\/0.1.tgz" }, 41 | "description": "A fake package" 42 | } 43 | }, 44 | "description": "A fake package." 45 | } 46 | 47 | ### GET /packagename/0.1.2 48 | 49 | Returns the JSON object for a specified release. Example: 50 | 51 | { 52 | "name": "foo", 53 | "_id": "foo", 54 | "version": "0.1.2", 55 | "dist": { "tarball": "http:\/\/domain.com\/0.1.tgz" }, 56 | "description": "A fake package" 57 | } 58 | 59 | ### GET /packagename/stable 60 | 61 | Returns the JSON object for the specified tag. 62 | 63 | { 64 | "name": "foo", 65 | "_id": "foo", 66 | "version": "0.1", 67 | "dist": { "tarball": "http:\/\/domain.com\/0.1.tgz" }, 68 | "description": "A fake package" 69 | } 70 | 71 | ### PUT /packagename 72 | 73 | Create or update the entire package info. 74 | 75 | MUST include the JSON body of the entire document. Must have `content-type:application/json`. 76 | 77 | If updating this must include the latest _rev. 78 | 79 | This method can also remove previous versions and distributions if necessary. 80 | 81 | ### PUT /packagename/0.1.2 82 | 83 | Create a new release version. 84 | 85 | MUST include all the metadata from package.json along with dist information as the JSON body of the request. MUST have `content-type:application/json` 86 | 87 | ### PUT /pacakgename/stable 88 | 89 | Link a distribution tag (ie. "stable") to a specific version string. 90 | 91 | MUST but a JSON string as the body. Example: 92 | 93 | "0.1.2" 94 | 95 | Must have `content-type:application/json`. 96 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var http = require("http"), 2 | url = require("url"), 3 | sys = require("sys"); 4 | 5 | function request (path, method, headers, body, callback) { 6 | if (!headers) { 7 | headers = {'Content-Type':'application/json', "Accept":'application/json', "Host":"jsregistry:5984"}; 8 | } 9 | if (!method) { 10 | method = "GET" 11 | } 12 | if (body) { 13 | if (typeof body !== "string") { 14 | body = JSON.stringify(body) 15 | } 16 | } 17 | 18 | var client = http.createClient(5984, "jsregistry"); 19 | var request = client.request(method, path, headers); 20 | request.addListener("response", function (response) { 21 | var buffer = '' 22 | response.addListener("data", function (chunk) { 23 | buffer += chunk; 24 | }) 25 | response.addListener("end", function () { 26 | callback(response, buffer); 27 | }) 28 | }) 29 | request.write(body) 30 | request.close() 31 | 32 | } 33 | 34 | function requestQueue (args, doneCallback) { 35 | var runner = function (args, i) { 36 | var validation = args[i].pop(); 37 | args[i].push(function (request, response) { 38 | validation(request, response); 39 | if (i<(args.length - 1)) { 40 | runner(args, i + 1) 41 | } else if (doneCallback) { 42 | doneCallback(); 43 | } 44 | }) 45 | request.apply(request, args[i]) 46 | } 47 | runner(args, 0); 48 | } 49 | 50 | // Test new module creation 51 | 52 | function assertStatus (code) { 53 | var c = code; 54 | return function (response, body) { 55 | if (response.statusCode != c) { 56 | sys.puts("Status is not "+c+" it is "+response.statusCode+'. '+body) 57 | throw "Status is not "+c+" it is "+response.statusCode+'. '+body; 58 | } else { 59 | sys.puts(body); 60 | } 61 | } 62 | } 63 | 64 | // request('/foo', "PUT", undefined, {id:"foo", description:"new module"}, function () {sys.puts('done')}) 65 | 66 | var sha = require('./deps/sha1'), 67 | base64 = require('./deps/base64'); 68 | 69 | var userDoc = {_id:"org.couchdb.user:testuser", 70 | name:"testuser", 71 | password_sha: sha.hex_sha1('testing' + 'pepper'), 72 | salt:"pepper", type:"user", roles:[]} 73 | var auth = {'Content-Type':'application/json', 74 | accept:'application/json', 75 | authorization:'Basic ' + base64.encode('testuser:testing'), 76 | host:"jsregistry:5984"} 77 | 78 | requestQueue([ 79 | ["/adduser/org.couchdb.user:testuser", "PUT", undefined, userDoc, assertStatus(201)], 80 | // ["/session", "POST", undefined, "user=testuser&password=testing", function (response, body) { 81 | // sys.puts('s', body, 'd') 82 | // auth.cookie = response.headers['set-cookie']; 83 | // }], 84 | ["/foo", "PUT", auth, {_id:"foo", description:"new module"}, assertStatus(201)], 85 | ["/foo/0.1.0", "PUT", auth, 86 | {_id:"foo", description:"new module", dist:{tarball:"http://path/to/tarball"}}, assertStatus(201)], 87 | ["/foo/stable", "PUT", auth, "0.1.0", assertStatus(201)], 88 | ["/foo", "GET", undefined, "0.1.0", assertStatus(200)], 89 | ["/foo/0.1.0", "GET", undefined, "0.1.0", assertStatus(200)], 90 | ["/foo/stable", "GET", undefined, "0.1.0", assertStatus(200)], 91 | ], function () {sys.puts('done')}) 92 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var couchapp = require('couchapp') 2 | , ddoc = {_id:'_design/app', shows:{}, updates:{}, views:{}, lists:{}} 3 | , fs = require("fs") 4 | 5 | exports.app = ddoc 6 | 7 | ddoc.language = "javascript" 8 | // there has GOT to be a better way than this... 9 | ddoc.semver = [ 'var expr = exports.expression = ' 10 | + require("npm/utils/semver").expressions.parse.toString() 11 | , 'function valid (v) { return v && typeof v === "string" && v.match(expr) }' 12 | , 'function clean (v) {' 13 | , 'v = valid(v)' 14 | , 'if (!v) return v' 15 | , "return [v[1]||'0', v[2]||'0', v[3]||'0'].join('.') + (v[4]||'') + (v[5]||'')" 16 | ,'}' 17 | , 'exports.valid = valid' 18 | , 'exports.clean = clean' 19 | ].join("\n") 20 | ddoc.valid = [ 'function validName (name) {' 21 | , 'if (!name) return false' 22 | , 'if (name === "favicon.ico") return false' 23 | , 'var n = name.replace("%20", " ")' 24 | , 'n = n.replace(/^\\s+|\\s+$/g, "")' 25 | , 'if (!n || n.charAt(0) === "." || n.match(/[\\/@\\s]/) || n !== name) {' 26 | , 'return false' 27 | , '}' 28 | , 'return n' 29 | , '}' 30 | , 'function validPackage (pkg) {' 31 | , 'return validName(pkg.name) && semver.valid(pkg.version)' 32 | , '}' 33 | , 'exports.name = validName' 34 | , 'exports.package = validPackage' 35 | ].join("\n") 36 | 37 | 38 | 39 | ddoc.shows.requirey = function () { 40 | return { code : 200 41 | , body : toJSON([require("semver").expression.toString(), typeof ("asdf".match), 42 | require("semver").clean("0.2.4-1"), 43 | require("semver").valid("0.2.4-1"), 44 | new Date().toISOString(),"hi" 45 | ]) 46 | , headers : {} 47 | } 48 | } 49 | 50 | ddoc.rewrites = 51 | [ { from: "/", to:"_list/index/listAll", method: "GET" } 52 | , { from : "/favicon.ico", to:"../../npm/favicon.ico", method:"GET" } 53 | , { from: "/all", to:"_list/index/listAll", method: "GET" } 54 | , { from: "/all/-/jsonp/:jsonp", to:"_list/index/listAll", method: "GET" } 55 | , { from: "/-/jsonp/:jsonp", to:"_list/index/listAll", method: "GET" } 56 | 57 | , { from: "/adduser/:user", to:"../../../_users/:user", method: "PUT" } 58 | , { from: "/adduser/:user/-rev/:rev", to:"../../../_users/:user", method: "PUT" } 59 | , { from: "/getuser/:user", to:"../../../_users/:user", method: "GET" } 60 | 61 | , { from: "/:pkg", to: "/_show/package/:pkg", method: "GET" } 62 | , { from: "/:pkg/-/jsonp/:jsonp", to: "/_show/package/:pkg", method: "GET" } 63 | , { from: "/:pkg/:version", to: "_show/package/:pkg", method: "GET" } 64 | , { from: "/:pkg/:version/-/jsonp/:jsonp", to: "_show/package/:pkg", method: "GET" } 65 | 66 | , { from: "/:pkg/-/:att", to: "../../:pkg/:att", method: "GET" } 67 | , { from: "/:pkg/-/:att/:rev", to: "../../:pkg/:att", method: "PUT" } 68 | , { from: "/:pkg/-/:att/-rev/:rev", to: "../../:pkg/:att", method: "PUT" } 69 | , { from: "/:pkg/-/:att/:rev", to: "../../:pkg/:att", method: "DELETE" } 70 | , { from: "/:pkg/-/:att/-rev/:rev", to: "../../:pkg/:att", method: "DELETE" } 71 | 72 | , { from: "/:pkg", to: "/_update/package/:pkg", method: "PUT" } 73 | , { from: "/:pkg/-rev/:rev", to: "/_update/package/:pkg", method: "PUT" } 74 | , { from: "/:pkg/:version", to: "_update/package/:pkg", method: "PUT" } 75 | 76 | , { from: "/:pkg/-rev/:rev", to: "../../:pkg", method: "DELETE" } 77 | ] 78 | 79 | ddoc.lists.index = function (head, req) { 80 | var row 81 | , out = {} 82 | , semver = require("semver") 83 | 84 | while (row = getRow()) { 85 | var p = out[row.id] = {} 86 | var doc = row.value 87 | // legacy kludge 88 | for (var v in doc.versions) { 89 | var clean = semver.clean(v) 90 | if (clean !== v) { 91 | var x = doc.versions[v] 92 | delete doc.versions[v] 93 | x.version = v = clean 94 | doc.versions[clean] = x 95 | } 96 | } 97 | for (var tag in doc["dist-tags"]) { 98 | var clean = semver.clean(doc["dist-tags"][tag]) 99 | if (!clean) delete doc["dist-tags"][tag] 100 | else doc["dist-tags"][tag] = clean 101 | } 102 | // end kludge 103 | 104 | for (var i in doc) { 105 | if (i === "versions" || i.charAt(0) === "_") continue 106 | p[i] = doc[i] 107 | } 108 | p.versions = {} 109 | if (row.repository) p.repository = row.repository 110 | if (row.description) p.description = row.description 111 | for (var i in doc.versions) { 112 | if (doc.versions[i].repository && !row.repository) { 113 | p.repository = doc.versions[i].repository 114 | } 115 | if (doc.versions[i].description && !row.description) { 116 | p.description = doc.versions[i].description 117 | } 118 | p.versions[i] = "http://"+req.headers.Host+"/"+doc.name+"/"+i 119 | } 120 | if (!p.url) p.url = "http://"+req.headers.Host+"/"+encodeURIComponent(doc.name)+"/" 121 | } 122 | out = req.query.jsonp 123 | ? req.query.jsonp + "(" + JSON.stringify(out) + ")" 124 | : toJSON(out) 125 | 126 | send(out) 127 | } 128 | ddoc.views.listAll = { 129 | map : function (doc) { return emit(doc._id, doc) } 130 | } 131 | 132 | ddoc.shows.package = function (doc, req) { 133 | var semver = require("semver") 134 | , code = 200 135 | , headers = {"Content-Type":"application/json"} 136 | , body = null 137 | if (doc.url) { 138 | // the package specifies a URL, redirect to it 139 | code = 301 140 | var url = doc.url 141 | if (req.query.version) { 142 | url += '/' + req.query.version // add the version to the URL if necessary 143 | delete req.query.version // stay out of the version branch below 144 | } 145 | headers.Location = url 146 | doc = { 147 | location: url 148 | } 149 | } 150 | // legacy kludge 151 | for (var v in doc.versions) { 152 | var clean = semver.clean(v) 153 | if (clean !== v) { 154 | var p = doc.versions[v] 155 | delete doc.versions[v] 156 | p.version = v = clean 157 | doc.versions[clean] = p 158 | } 159 | if (doc.versions[v].dist.tarball) { 160 | var t = doc.versions[v].dist.tarball 161 | t = t.replace(/^https?:\/\/[^\/:]+(:[0-9]+)?/, '') 162 | doc.versions[v].dist.tarball = t 163 | var h 164 | for (var i in req.headers) { 165 | if (i.toLowerCase() === 'host') { 166 | h = req.headers[i] 167 | break 168 | } 169 | } 170 | h = h ? 'http://' + h : h 171 | doc.versions[v].dist.tarball = h + t 172 | } 173 | } 174 | for (var tag in doc["dist-tags"]) { 175 | var clean = semver.clean(doc["dist-tags"][tag]) 176 | if (!clean) delete doc["dist-tags"][tag] 177 | else doc["dist-tags"][tag] = clean 178 | } 179 | // end kludge 180 | if (req.query.version) { 181 | var ver = req.query.version 182 | // if not a valid version, then treat as a tag. 183 | if (!semver.valid(ver)) { 184 | ver = doc["dist-tags"][ver] 185 | } 186 | body = doc.versions[ver] 187 | if (!body) { 188 | code = 404 189 | body = {"error" : "version not found: "+req.query.version} 190 | } 191 | } else { 192 | body = doc 193 | delete body._revisions 194 | delete body._attachments 195 | } 196 | body = req.query.jsonp 197 | ? req.query.jsonp + "(" + JSON.stringify(body) + ")" 198 | : toJSON(body) 199 | return { 200 | code : code, 201 | body : body, 202 | headers : headers, 203 | } 204 | } 205 | 206 | ddoc.updates.package = function (doc, req) { 207 | var semver = require("semver") 208 | var valid = require("valid") 209 | function error (reason) { 210 | return [{forbidden:reason}, JSON.stringify({forbidden:reason})] 211 | } 212 | 213 | if (doc) { 214 | if (req.query.version) { 215 | var parsed = semver.valid(req.query.version) 216 | if (!parsed) { 217 | // it's a tag. 218 | var tag = req.query.version 219 | , ver = JSON.parse(req.body) 220 | if (!semver.valid(ver)) { 221 | return error("setting tag "+tag+" to invalid version: "+req.body) 222 | } 223 | doc["dist-tags"][tag] = semver.clean(ver) 224 | return [doc, JSON.stringify({ok:"updated tag"})] 225 | } 226 | // adding a new version. 227 | var ver = req.query.version 228 | if (!semver.valid(ver)) { 229 | return error("invalid version: "+ver) 230 | } 231 | if ((ver in doc.versions) || (semver.clean(ver) in doc.versions)) { 232 | // attempting to overwrite an existing version. 233 | // not supported at this time. 234 | return error("cannot modify existing version") 235 | } 236 | var body = JSON.parse(req.body) 237 | if (!valid.name(body.name)) { 238 | return error( "Invalid name: "+JSON.stringify(body.name)) 239 | } 240 | body.version = semver.clean(body.version) 241 | ver = semver.clean(ver) 242 | if (body.version !== ver) { 243 | return error( "version in doc doesn't match version in request: " 244 | + JSON.stringify(body.version) + " !== " + JSON.stringify(ver)) 245 | } 246 | if (body.description) doc.description = body.description 247 | if (body.author) doc.author = body.author 248 | if (body.repository) doc.repository = body.repository 249 | doc["dist-tags"].latest = body.version 250 | doc.versions[ver] = body 251 | return [doc, JSON.stringify({ok:"added version"})] 252 | } 253 | 254 | // update the package info 255 | var newdoc = JSON.parse(req.body) 256 | , changed = false 257 | if (doc._rev && doc._rev !== newdoc._rev) { 258 | return error( "must supply latest _rev to update existing package" ) 259 | } 260 | for (var i in newdoc) if (typeof newdoc[i] === "string" || i === "maintainers") { 261 | doc[i] = newdoc[i] 262 | } 263 | if (newdoc.versions) { 264 | doc.versions = newdoc.versions 265 | doc["dist-tags"] = newdoc["dist-tags"] 266 | } 267 | return [doc, JSON.stringify({ok:"updated package metadata"})] 268 | } else { 269 | // Create new package doc 270 | doc = JSON.parse(req.body) 271 | if (!doc.versions) doc.versions = {} 272 | var latest 273 | for (var v in doc.versions) { 274 | if (!semver.valid(v)) return error("Invalid version: "+JSON.stringify(v)) 275 | var p = doc.versions[p] 276 | if (p.version !== v) return error("Version mismatch: "+JSON.stringify(v) 277 | +" !== "+JSON.stringify(p.version)) 278 | if (!valid.name(p.name)) return error("Invalid name: "+JSON.stringify(p.name)) 279 | latest = semver.clean(v) 280 | } 281 | if (latest) doc["dist-tags"].latest = latest 282 | if (!doc['dist-tags']) doc['dist-tags'] = {} 283 | return [doc, JSON.stringify({ok:"created new entry"})] 284 | } 285 | } 286 | 287 | ddoc.validate_doc_update = function (newDoc, oldDoc, user) { 288 | var semver = require("semver") 289 | var valid = require("valid") 290 | 291 | function assert (ok, message) { 292 | if (!ok) throw {forbidden:message} 293 | } 294 | 295 | // if the newDoc is an {error:"blerg"}, then throw that right out. 296 | // something detected in the _updates/package script. 297 | assert(!newDoc.forbidden || newDoc._deleted, newDoc.forbidden) 298 | 299 | function validUser () { 300 | if ( !oldDoc || !oldDoc.maintainers ) return true 301 | if (isAdmin()) return true 302 | if (typeof oldDoc.maintainers !== "object") return true 303 | for (var i = 0, l = oldDoc.maintainers.length; i < l; i ++) { 304 | if (oldDoc.maintainers[i].name === user.name) return true 305 | } 306 | return false 307 | } 308 | function isAdmin () { return user.roles.indexOf("_admin") >= 0 } 309 | 310 | assert(validUser(), "user: " + user.name + " not authorized to modify " 311 | + newDoc.name ) 312 | if (newDoc._deleted) return 313 | 314 | assert(newDoc.maintainers, "Please upgrade your package manager program") 315 | var n = valid.name(newDoc.name) 316 | assert(valid.name(n) && n === newDoc.name && n 317 | , "Invalid name: " 318 | + JSON.stringify(newDoc.name) 319 | + " may not start with '.' or contain '/' or '@' or whitespace") 320 | 321 | // make sure all the dist-tags and versions are valid semver 322 | assert(newDoc["dist-tags"], "must have dist-tags") 323 | assert(newDoc.versions, "must have versions") 324 | 325 | for (var i in newDoc["dist-tags"]) { 326 | assert(semver.valid(newDoc["dist-tags"][i]), 327 | "dist-tag "+i+" is not a valid version: "+newDoc["dist-tags"][i]) 328 | assert(newDoc["dist-tags"][i] in newDoc.versions, 329 | "dist-tag "+i+" refers to non-existent version: "+newDoc["dist-tags"][i]) 330 | } 331 | for (var i in newDoc.versions) { 332 | assert(semver.valid(i), "version "+i+" is not a valid version") 333 | } 334 | } 335 | --------------------------------------------------------------------------------