[Pkg-javascript-commits] [node-cors] 01/02: Imported Upstream version 2.7.1
Thorsten Alteholz
alteholz at moszumanska.debian.org
Sat Feb 27 14:01:49 UTC 2016
This is an automated email from the git hooks/post-receive script.
alteholz pushed a commit to branch master
in repository node-cors.
commit 9d12d04dcf94e834c16b7d3d521c0e5bcaa72172
Author: Thorsten Alteholz <debian at alteholz.de>
Date: Sat Feb 27 15:01:41 2016 +0100
Imported Upstream version 2.7.1
---
.eslintrc | 9 +
.npmignore | 3 +
.travis.yml | 3 +
CONTRIBUTING.md | 35 +++
LICENSE | 9 +
README.md | 192 +++++++++++++++
lib/index.js | 242 ++++++++++++++++++
package.json | 41 ++++
test/basic-auth.js | 40 +++
test/body-events.js | 81 ++++++
test/cors.js | 652 +++++++++++++++++++++++++++++++++++++++++++++++++
test/error-response.js | 77 ++++++
test/example-app.js | 98 ++++++++
test/issue-2.js | 56 +++++
test/issue-31.js | 58 +++++
test/mocha.opts | 3 +
16 files changed, 1599 insertions(+)
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..51a7893
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,9 @@
+{
+ "env": {
+ "node": true
+ },
+ "rules": {
+ "indent": [2, 2],
+ "quotes": "single"
+ }
+}
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..7595163
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,3 @@
+.DS_Store
+node_modules
+npm-debug.log
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..20fd86b
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,3 @@
+language: node_js
+node_js:
+ - 0.10
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..90ea7b1
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,35 @@
+# contributing to `cors`
+
+CORS is a node.js package for providing a [connect](http://www.senchalabs.org/connect/)/[express](http://expressjs.com/) middleware that can be used to enable [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) with various options. Learn more about the project in [the README](README.md).
+
+[![build status](https://secure.travis-ci.org/TroyGoode/node-cors.png)](http://travis-ci.org/TroyGoode/node-cors)
+
+## The CORS Spec
+
+[http://www.w3.org/TR/cors/](http://www.w3.org/TR/cors/)
+
+## Pull Requests Welcome
+
+* Include `'use strict';` in every javascript file.
+* 2 space indentation.
+* Please run the testing steps below before submitting.
+
+## Testing
+
+```bash
+$ npm install
+$ npm test
+```
+
+## Interactive Testing Harness
+
+[http://node-cors-client.herokuapp.com](http://node-cors-client.herokuapp.com)
+
+Related git repositories:
+
+* [https://github.com/TroyGoode/node-cors-server](https://github.com/TroyGoode/node-cors-server)
+* [https://github.com/TroyGoode/node-cors-client](https://github.com/TroyGoode/node-cors-client)
+
+## License
+
+[MIT License](http://www.opensource.org/licenses/mit-license.php)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a01c3f3
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,9 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 Troy Goode <troygoode at gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bc963eb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,192 @@
+# `cors`
+
+CORS is a node.js package for providing a [Connect](http://www.senchalabs.org/connect/)/[Express](http://expressjs.com/) middleware that can be used to enable [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) with various options.
+
+**[Follow me (@troygoode) on Twitter!](https://twitter.com/intent/user?screen_name=troygoode)**
+
+[![NPM](https://nodei.co/npm/cors.png?downloads=true&stars=true)](https://nodei.co/npm/cors/)
+
+[![build status](https://secure.travis-ci.org/expressjs/cors.png)](http://travis-ci.org/expressjs/cors)
+* [Installation](#installation)
+* [Usage](#usage)
+ * [Simple Usage](#simple-usage-enable-all-cors-requests)
+ * [Enable CORS for a Single Route](#enable-cors-for-a-single-route)
+ * [Configuring CORS](#configuring-cors)
+ * [Configuring CORS Asynchronously](#configuring-cors-asynchronously)
+ * [Enabling CORS Pre-Flight](#enabling-cors-pre-flight)
+* [Configuration Options](#configuration-options)
+* [Demo](#demo)
+* [License](#license)
+* [Author](#author)
+
+## Installation (via [npm](https://npmjs.org/package/cors))
+
+```bash
+$ npm install cors
+```
+
+## Usage
+
+### Simple Usage (Enable *All* CORS Requests)
+
+```javascript
+var express = require('express')
+ , cors = require('cors')
+ , app = express();
+
+app.use(cors());
+
+app.get('/products/:id', function(req, res, next){
+ res.json({msg: 'This is CORS-enabled for all origins!'});
+});
+
+app.listen(80, function(){
+ console.log('CORS-enabled web server listening on port 80');
+});
+```
+
+### Enable CORS for a Single Route
+
+```javascript
+var express = require('express')
+ , cors = require('cors')
+ , app = express();
+
+app.get('/products/:id', cors(), function(req, res, next){
+ res.json({msg: 'This is CORS-enabled for all origins!'});
+});
+
+app.listen(80, function(){
+ console.log('CORS-enabled web server listening on port 80');
+});
+```
+
+### Configuring CORS
+
+```javascript
+var express = require('express')
+ , cors = require('cors')
+ , app = express();
+
+var corsOptions = {
+ origin: 'http://example.com'
+};
+
+app.get('/products/:id', cors(corsOptions), function(req, res, next){
+ res.json({msg: 'This is CORS-enabled for only example.com.'});
+});
+
+app.listen(80, function(){
+ console.log('CORS-enabled web server listening on port 80');
+});
+```
+
+### Configuring CORS w/ Dynamic Origin
+
+```javascript
+var express = require('express')
+ , cors = require('cors')
+ , app = express();
+
+var whitelist = ['http://example1.com', 'http://example2.com'];
+var corsOptions = {
+ origin: function(origin, callback){
+ var originIsWhitelisted = whitelist.indexOf(origin) !== -1;
+ callback(null, originIsWhitelisted);
+ }
+};
+
+app.get('/products/:id', cors(corsOptions), function(req, res, next){
+ res.json({msg: 'This is CORS-enabled for a whitelisted domain.'});
+});
+
+app.listen(80, function(){
+ console.log('CORS-enabled web server listening on port 80');
+});
+```
+
+### Enabling CORS Pre-Flight
+
+Certain CORS requests are considered 'complex' and require an initial
+`OPTIONS` request (called the "pre-flight request"). An example of a
+'complex' CORS request is one that uses an HTTP verb other than
+GET/HEAD/POST (such as DELETE) or that uses custom headers. To enable
+pre-flighting, you must add a new OPTIONS handler for the route you want
+to support:
+
+```javascript
+var express = require('express')
+ , cors = require('cors')
+ , app = express();
+
+app.options('/products/:id', cors()); // enable pre-flight request for DELETE request
+app.del('/products/:id', cors(), function(req, res, next){
+ res.json({msg: 'This is CORS-enabled for all origins!'});
+});
+
+app.listen(80, function(){
+ console.log('CORS-enabled web server listening on port 80');
+});
+```
+
+You can also enable pre-flight across-the-board like so:
+
+```
+app.options('*', cors()); // include before other routes
+```
+
+### Configuring CORS Asynchronously
+
+```javascript
+var express = require('express')
+ , cors = require('cors')
+ , app = express();
+
+var whitelist = ['http://example1.com', 'http://example2.com'];
+var corsOptionsDelegate = function(req, callback){
+ var corsOptions;
+ if(whitelist.indexOf(req.header('Origin')) !== -1){
+ corsOptions = { origin: true }; // reflect (enable) the requested origin in the CORS response
+ }else{
+ corsOptions = { origin: false }; // disable CORS for this request
+ }
+ callback(null, corsOptions); // callback expects two parameters: error and options
+};
+
+app.get('/products/:id', cors(corsOptionsDelegate), function(req, res, next){
+ res.json({msg: 'This is CORS-enabled for a whitelisted domain.'});
+});
+
+app.listen(80, function(){
+ console.log('CORS-enabled web server listening on port 80');
+});
+```
+
+## Configuration Options
+
+* `origin`: Configures the **Access-Control-Allow-Origin** CORS header. Expects a string (ex: "http://example.com"). Set to `true` to reflect the [request origin](http://tools.ietf.org/html/draft-abarth-origin-09), as defined by `req.header('Origin')`. Set to `false` to disable CORS. Can also be set to a function, which takes the request origin as the first parameter and a callback (which expects the signature `err [object], allow [bool]`) as the second. Finally, it can also be a regular [...]
+* `methods`: Configures the **Access-Control-Allow-Methods** CORS header. Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: `['GET', 'PUT', 'POST']`).
+* `allowedHeaders`: Configures the **Access-Control-Allow-Headers** CORS header. Expects a comma-delimited string (ex: 'Content-Type,Authorization') or an array (ex: `['Content-Type', 'Authorization']`). If not specified, defaults to reflecting the headers specified in the request's **Access-Control-Request-Headers** header.
+* `exposedHeaders`: Configures the **Access-Control-Expose-Headers** CORS header. Expects a comma-delimited string (ex: 'Content-Range,X-Content-Range') or an array (ex: `['Content-Range', 'X-Content-Range']`). If not specified, no custom headers are exposed.
+* `credentials`: Configures the **Access-Control-Allow-Credentials** CORS header. Set to `true` to pass the header, otherwise it is omitted.
+* `maxAge`: Configures the **Access-Control-Allow-Max-Age** CORS header. Set to an integer to pass the header, otherwise it is omitted.
+* `preflightContinue`: Pass the CORS preflight response to the next handler.
+
+For details on the effect of each CORS header, [read this article on HTML5 Rocks](http://www.html5rocks.com/en/tutorials/cors/).
+
+## Demo
+
+A demo that illustrates CORS working (and not working) using jQuery is available here: [http://node-cors-client.herokuapp.com/](http://node-cors-client.herokuapp.com/)
+
+Code for that demo can be found here:
+
+* Client: [https://github.com/TroyGoode/node-cors-client](https://github.com/TroyGoode/node-cors-client)
+* Server: [https://github.com/TroyGoode/node-cors-server](https://github.com/TroyGoode/node-cors-server)
+
+## License
+
+[MIT License](http://www.opensource.org/licenses/mit-license.php)
+
+## Author
+
+[Troy Goode](https://github.com/TroyGoode) ([troygoode at gmail.com](mailto:troygoode at gmail.com))
diff --git a/lib/index.js b/lib/index.js
new file mode 100644
index 0000000..d520998
--- /dev/null
+++ b/lib/index.js
@@ -0,0 +1,242 @@
+(function () {
+
+ 'use strict';
+
+ var vary = require('vary');
+
+ var defaults = {
+ origin: '*',
+ methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
+ preflightContinue: false
+ };
+
+ function isString(s) {
+ return typeof s === 'string' || s instanceof String;
+ }
+
+ function isOriginAllowed(origin, allowedOrigin) {
+ if (Array.isArray(allowedOrigin)) {
+ for (var i = 0; i < allowedOrigin.length; ++i) {
+ if (isOriginAllowed(origin, allowedOrigin[i])) {
+ return true;
+ }
+ }
+ return false;
+ } else if (isString(allowedOrigin)) {
+ return origin === allowedOrigin;
+ } else if (allowedOrigin instanceof RegExp) {
+ return allowedOrigin.test(origin);
+ } else {
+ return !!allowedOrigin;
+ }
+ }
+
+ function configureOrigin(options, req) {
+ var requestOrigin = req.headers.origin,
+ headers = [],
+ isAllowed;
+
+ if (!options.origin || options.origin === '*') {
+ // allow any origin
+ headers.push([{
+ key: 'Access-Control-Allow-Origin',
+ value: '*'
+ }]);
+ } else if (isString(options.origin)) {
+ // fixed origin
+ headers.push([{
+ key: 'Access-Control-Allow-Origin',
+ value: options.origin
+ }]);
+ headers.push([{
+ key: 'Vary',
+ value: 'Origin'
+ }]);
+ } else {
+ isAllowed = isOriginAllowed(requestOrigin, options.origin);
+ // reflect origin
+ headers.push([{
+ key: 'Access-Control-Allow-Origin',
+ value: isAllowed ? requestOrigin : false
+ }]);
+ if (isAllowed) {
+ headers.push([{
+ key: 'Vary',
+ value: 'Origin'
+ }]);
+ }
+ }
+
+ return headers;
+ }
+
+ function configureMethods(options) {
+ var methods = options.methods || defaults.methods;
+ if (methods.join) {
+ methods = options.methods.join(','); // .methods is an array, so turn it into a string
+ }
+ return {
+ key: 'Access-Control-Allow-Methods',
+ value: methods
+ };
+ }
+
+ function configureCredentials(options) {
+ if (options.credentials === true) {
+ return {
+ key: 'Access-Control-Allow-Credentials',
+ value: 'true'
+ };
+ }
+ return null;
+ }
+
+ function configureAllowedHeaders(options, req) {
+ var headers = options.allowedHeaders || options.headers;
+ if (!headers) {
+ headers = req.headers['access-control-request-headers']; // .headers wasn't specified, so reflect the request headers
+ } else if (headers.join) {
+ headers = headers.join(','); // .headers is an array, so turn it into a string
+ }
+ if (headers && headers.length) {
+ return {
+ key: 'Access-Control-Allow-Headers',
+ value: headers
+ };
+ }
+ return null;
+ }
+
+ function configureExposedHeaders(options) {
+ var headers = options.exposedHeaders;
+ if (!headers) {
+ return null;
+ } else if (headers.join) {
+ headers = headers.join(','); // .headers is an array, so turn it into a string
+ }
+ if (headers && headers.length) {
+ return {
+ key: 'Access-Control-Expose-Headers',
+ value: headers
+ };
+ }
+ return null;
+ }
+
+ function configureMaxAge(options) {
+ var maxAge = options.maxAge && options.maxAge.toString();
+ if (maxAge && maxAge.length) {
+ return {
+ key: 'Access-Control-Max-Age',
+ value: maxAge
+ };
+ }
+ return null;
+ }
+
+ function applyHeaders(headers, res) {
+ for (var i = 0, n = headers.length; i < n; i++) {
+ var header = headers[i];
+ if (header) {
+ if (Array.isArray(header)) {
+ applyHeaders(header, res);
+ } else if (header.key === 'Vary' && header.value) {
+ vary(res, header.value);
+ } else if (header.value) {
+ res.setHeader(header.key, header.value);
+ }
+ }
+ }
+ }
+
+ function cors(options, req, res, next) {
+ var headers = [],
+ method = req.method && req.method.toUpperCase && req.method.toUpperCase();
+
+ if (method === 'OPTIONS') {
+ // preflight
+ headers.push(configureOrigin(options, req));
+ headers.push(configureCredentials(options, req));
+ headers.push(configureMethods(options, req));
+ headers.push(configureAllowedHeaders(options, req));
+ headers.push(configureMaxAge(options, req));
+ headers.push(configureExposedHeaders(options, req));
+ applyHeaders(headers, res);
+
+ if (options.preflightContinue ) {
+ next();
+ } else {
+ res.statusCode = 204;
+ res.end();
+ }
+ } else {
+ // actual response
+ headers.push(configureOrigin(options, req));
+ headers.push(configureCredentials(options, req));
+ headers.push(configureExposedHeaders(options, req));
+ applyHeaders(headers, res);
+ next();
+ }
+ }
+
+ function middlewareWrapper(o) {
+ // if no options were passed in, use the defaults
+ if (!o) {
+ o = {};
+ }
+ if (o.origin === undefined) {
+ o.origin = defaults.origin;
+ }
+ if (o.methods === undefined) {
+ o.methods = defaults.methods;
+ }
+ if (o.preflightContinue === undefined) {
+ o.preflightContinue = defaults.preflightContinue;
+ }
+
+ // if options are static (either via defaults or custom options passed in), wrap in a function
+ var optionsCallback = null;
+ if (typeof o === 'function') {
+ optionsCallback = o;
+ } else {
+ optionsCallback = function (req, cb) {
+ cb(null, o);
+ };
+ }
+
+ return function (req, res, next) {
+ optionsCallback(req, function (err, options) {
+ if (err) {
+ next(err);
+ } else {
+ var originCallback = null;
+ if (options.origin && typeof options.origin === 'function') {
+ originCallback = options.origin;
+ } else if (options.origin) {
+ originCallback = function (origin, cb) {
+ cb(null, options.origin);
+ };
+ }
+
+ if (originCallback) {
+ originCallback(req.headers.origin, function (err2, origin) {
+ if (err2 || !origin) {
+ next(err2);
+ } else {
+ var corsOptions = Object.create(options);
+ corsOptions.origin = origin;
+ cors(corsOptions, req, res, next);
+ }
+ });
+ } else {
+ next();
+ }
+ }
+ });
+ };
+ }
+
+ // can pass either an options hash, an options delegate, or nothing
+ module.exports = middlewareWrapper;
+
+}());
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..87eca86
--- /dev/null
+++ b/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "cors",
+ "version": "2.7.1",
+ "author": "Troy Goode <troygoode at gmail.com> (https://github.com/troygoode/)",
+ "description": "middleware for dynamically or statically enabling CORS in express/connect applications",
+ "keywords": ["cors", "express", "connect", "middleware"],
+ "homepage": "https://github.com/expressjs/cors/",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/expressjs/cors.git"
+ },
+ "contributors": [
+ {
+ "name": "Troy Goode",
+ "email": "troygoode at gmail.com",
+ "web": "https://github.com/troygoode/"
+ }
+ ],
+ "license": "MIT",
+ "bugs": {"url": "https://github.com/expressjs/cors/issues"},
+ "main": "./lib/index.js",
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "dependencies": {
+ "vary": "^1"
+ },
+ "devDependencies": {
+ "basic-auth-connect": "^1.0.0",
+ "body-parser": "^1.12.4",
+ "eslint": "^0.21.2",
+ "express": "^4.12.4",
+ "mocha": "^2.2.5",
+ "should": "^6.0.3",
+ "supertest": "^1.0.1"
+ },
+ "scripts": {
+ "test": "npm run lint && ./node_modules/mocha/bin/mocha",
+ "lint": "./node_modules/eslint/bin/eslint.js lib test"
+ }
+}
diff --git a/test/basic-auth.js b/test/basic-auth.js
new file mode 100644
index 0000000..1323f42
--- /dev/null
+++ b/test/basic-auth.js
@@ -0,0 +1,40 @@
+(function () {
+ /*global describe, it*/
+
+ 'use strict';
+
+ var should = require('should'),
+ express = require('express'),
+ supertest = require('supertest'),
+ basicAuth = require('basic-auth-connect'),
+ cors = require('../lib');
+
+ var app;
+
+ /* -------------------------------------------------------------------------- */
+
+ app = express();
+ app.use(basicAuth('username', 'password'));
+ app.use(cors());
+ app.post('/', function (req, res) {
+ res.send('hello world');
+ });
+
+ /* -------------------------------------------------------------------------- */
+
+ describe('basic auth', function () {
+ it('POST works', function (done) {
+ supertest(app)
+ .post('/')
+ .auth('username', 'password')
+ .expect(200)
+ .end(function (err, res) {
+ should.not.exist(err);
+ res.headers['access-control-allow-origin'].should.eql('*');
+ res.text.should.eql('hello world');
+ done();
+ });
+ });
+ });
+
+}());
diff --git a/test/body-events.js b/test/body-events.js
new file mode 100644
index 0000000..99d582b
--- /dev/null
+++ b/test/body-events.js
@@ -0,0 +1,81 @@
+(function () {
+ /*global describe, it*/
+
+ 'use strict';
+
+ var should = require('should'),
+ express = require('express'),
+ supertest = require('supertest'),
+ bodyParser = require('body-parser'),
+ cors = require('../lib');
+
+ var dynamicOrigin,
+ app1,
+ app2,
+ text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed justo turpis, tempor id sem fringilla, cursus tristique purus. Mauris a sollicitudin magna. Etiam dui lacus, vehicula non dictum at, cursus vitae libero. Curabitur lorem nulla, sollicitudin id enim ut, vehicula rhoncus felis. Ut nec iaculis velit. Vivamus at augue nulla. Fusce at molestie arcu. Duis at dui at tellus mattis tincidunt. Vestibulum sit amet dictum metus. Curabitur nec pretium ante. Proin vulputate elit [...]
+
+ /* -------------------------------------------------------------------------- */
+
+ dynamicOrigin = function (origin, cb) {
+ setTimeout(function () {
+ cb(null, true);
+ }, 200);
+ };
+
+ /* -------------------------------------------------------------------------- */
+
+ app1 = express();
+ app1.use(cors({origin: dynamicOrigin}));
+ app1.use(bodyParser.json());
+ app1.post('/', function (req, res) {
+ res.send(req.body);
+ });
+
+ /* -------------------------------------------------------------------------- */
+
+ app2 = express();
+ app2.use(bodyParser.json());
+ app2.use(cors({origin: dynamicOrigin}));
+ app2.post('/', function (req, res) {
+ res.send(req.body);
+ });
+
+ /* -------------------------------------------------------------------------- */
+
+ describe('body-parser-events', function () {
+ describe('app1 (cors before bodyparser)', function () {
+ it('POST works', function (done) {
+ var body = {
+ example: text
+ };
+ supertest(app1)
+ .post('/')
+ .send(body)
+ .expect(200)
+ .end(function (err, res) {
+ should.not.exist(err);
+ res.body.should.eql(body);
+ done();
+ });
+ });
+ });
+
+ describe('app2 (bodyparser before cors)', function () {
+ it('POST works', function (done) {
+ var body = {
+ example: text
+ };
+ supertest(app2)
+ .post('/')
+ .send(body)
+ .expect(200)
+ .end(function (err, res) {
+ should.not.exist(err);
+ res.body.should.eql(body);
+ done();
+ });
+ });
+ });
+ });
+
+}());
diff --git a/test/cors.js b/test/cors.js
new file mode 100644
index 0000000..b27e4ef
--- /dev/null
+++ b/test/cors.js
@@ -0,0 +1,652 @@
+(function () {
+ /*global describe, it*/
+
+ 'use strict';
+
+ var should = require('should'),
+ cors = require('../lib');
+
+ var fakeRequest = function (headers) {
+ return {
+ headers: headers || {
+ 'origin': 'request.com',
+ 'access-control-request-headers': 'requestedHeader1,requestedHeader2'
+ },
+ pause: function () {
+ // do nothing
+ return;
+ },
+ resume: function () {
+ // do nothing
+ return;
+ }
+ };
+ },
+ fakeResponse = function () {
+ var headers = {};
+ return {
+ allHeaders: function () {
+ return headers;
+ },
+ getHeader: function (key) {
+ return headers[key];
+ },
+ setHeader: function (key, value) {
+ headers[key] = value;
+ return;
+ },
+ get: function (key) {
+ return headers[key];
+ }
+ };
+ };
+
+ describe('cors', function () {
+ it('passes control to next middleware', function (done) {
+ // arrange
+ var req, res, next;
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function () {
+ done();
+ };
+
+ // act
+ cors()(req, res, next);
+ });
+
+ it('shortcircuits preflight requests', function (done) {
+ // arrange
+ var req, res, next;
+ req = fakeRequest();
+ req.method = 'OPTIONS';
+ res = fakeResponse();
+ res.end = function () {
+ // assert
+ res.statusCode.should.equal(204);
+ done();
+ };
+ next = function () {
+ // assert
+ done('should not be called');
+ };
+
+ // act
+ cors()(req, res, next);
+ });
+
+ it('doesn\'t shortcircuit preflight requests with preflightContinue option', function (done) {
+ // arrange
+ var req, res, next;
+ req = fakeRequest();
+ req.method = 'OPTIONS';
+ res = fakeResponse();
+ res.end = function () {
+ // assert
+ done('should not be called');
+ };
+ next = function () {
+ // assert
+ done();
+ };
+
+ // act
+ cors({preflightContinue: true})(req, res, next);
+ });
+
+ it('normalizes method names', function (done) {
+ // arrange
+ var req, res, next;
+ req = fakeRequest();
+ req.method = 'options';
+ res = fakeResponse();
+ res.end = function () {
+ // assert
+ res.statusCode.should.equal(204);
+ done();
+ };
+ next = function () {
+ // assert
+ done('should not be called');
+ };
+
+ // act
+ cors()(req, res, next);
+ });
+
+ it('no options enables default CORS to all origins', function (done) {
+ // arrange
+ var req, res, next;
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function () {
+ // assert
+ res.getHeader('Access-Control-Allow-Origin').should.equal('*');
+ should.not.exist(res.getHeader('Access-Control-Allow-Methods'));
+ done();
+ };
+
+ // act
+ cors()(req, res, next);
+ });
+
+ it('OPTION call with no options enables default CORS to all origins and methods', function (done) {
+ // arrange
+ var req, res, next;
+ req = fakeRequest();
+ req.method = 'OPTIONS';
+ res = fakeResponse();
+ res.end = function () {
+ // assert
+ res.statusCode.should.equal(204);
+ done();
+ };
+ next = function () {
+ // assert
+ res.getHeader('Access-Control-Allow-Origin').should.equal('*');
+ res.getHeader('Access-Control-Allow-Methods').should.equal('GET,PUT,PATCH,POST,DELETE');
+ done();
+ };
+
+ // act
+ cors()(req, res, next);
+ });
+
+ describe('passing static options', function () {
+ it('overrides defaults', function (done) {
+ // arrange
+ var req, res, next, options;
+ options = {
+ origin: 'example.com',
+ methods: ['FOO', 'bar'],
+ headers: ['FIZZ', 'buzz'],
+ credentials: true,
+ maxAge: 123
+ };
+ req = fakeRequest();
+ req.method = 'OPTIONS';
+ res = fakeResponse();
+ res.end = function () {
+ // assert
+ res.statusCode.should.equal(204);
+ done();
+ };
+ next = function () {
+ // assert
+ res.getHeader('Access-Control-Allow-Origin').should.equal('example.com');
+ res.getHeader('Access-Control-Allow-Methods').should.equal('FOO,bar');
+ res.getHeader('Access-Control-Allow-Headers').should.equal('FIZZ,buzz');
+ res.getHeader('Access-Control-Allow-Credentials').should.equal('true');
+ res.getHeader('Access-Control-Allow-Max-Age').should.equal('123');
+ done();
+ };
+
+ // act
+ cors(options)(req, res, next);
+ });
+
+ it('matches request origin against regexp', function(done) {
+ var req = fakeRequest();
+ var res = fakeResponse();
+ var options = { origin: /^(.+\.)?request.com$/ };
+ cors(options)(req, res, function(err) {
+ should.not.exist(err);
+ res.getHeader('Access-Control-Allow-Origin').should.equal(req.headers.origin);
+ should.exist(res.getHeader('Vary'));
+ res.getHeader('Vary').should.equal('Origin');
+ return done();
+ });
+ });
+
+ it('matches request origin against array of origin checks', function(done) {
+ var req = fakeRequest();
+ var res = fakeResponse();
+ var options = { origin: [ /foo\.com$/, 'request.com' ] };
+ cors(options)(req, res, function(err) {
+ should.not.exist(err);
+ res.getHeader('Access-Control-Allow-Origin').should.equal(req.headers.origin);
+ should.exist(res.getHeader('Vary'));
+ res.getHeader('Vary').should.equal('Origin');
+ return done();
+ });
+ });
+
+ it('doesn\'t match request origin against array of invalid origin checks', function(done) {
+ var req = fakeRequest();
+ var res = fakeResponse();
+ var options = { origin: [ /foo\.com$/, 'bar.com' ] };
+ cors(options)(req, res, function(err) {
+ should.not.exist(err);
+ should.not.exist(res.getHeader('Access-Control-Allow-Origin'));
+ should.not.exist(res.getHeader('Vary'));
+ return done();
+ });
+ });
+
+ it('origin of false disables cors', function (done) {
+ // arrange
+ var req, res, next, options;
+ options = {
+ origin: false,
+ methods: ['FOO', 'bar'],
+ headers: ['FIZZ', 'buzz'],
+ credentials: true,
+ maxAge: 123
+ };
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function () {
+ // assert
+ should.not.exist(res.getHeader('Access-Control-Allow-Origin'));
+ should.not.exist(res.getHeader('Access-Control-Allow-Methods'));
+ should.not.exist(res.getHeader('Access-Control-Allow-Headers'));
+ should.not.exist(res.getHeader('Access-Control-Allow-Credentials'));
+ should.not.exist(res.getHeader('Access-Control-Allow-Max-Age'));
+ done();
+ };
+
+ // act
+ cors(options)(req, res, next);
+ });
+
+ it('can override origin', function (done) {
+ // arrange
+ var req, res, next, options;
+ options = {
+ origin: 'example.com'
+ };
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function () {
+ // assert
+ res.getHeader('Access-Control-Allow-Origin').should.equal('example.com');
+ done();
+ };
+
+ // act
+ cors(options)(req, res, next);
+ });
+
+ it('includes Vary header for specific origins', function (done) {
+ // arrange
+ var req, res, next, options;
+ options = {
+ origin: 'example.com'
+ };
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function () {
+ // assert
+ should.exist(res.getHeader('Vary'));
+ res.getHeader('Vary').should.equal('Origin');
+ done();
+ };
+
+ // act
+ cors(options)(req, res, next);
+ });
+
+ it('appends to an existing Vary header', function (done) {
+ // arrange
+ var req, res, next, options;
+ options = {
+ origin: 'example.com'
+ };
+ req = fakeRequest();
+ res = fakeResponse();
+ res.setHeader('Vary', 'Foo');
+ next = function () {
+ // assert
+ res.getHeader('Vary').should.equal('Foo, Origin');
+ done();
+ };
+
+ // act
+ cors(options)(req, res, next);
+ });
+
+ it('origin defaults to *', function (done) {
+ // arrange
+ var req, res, next, options;
+ options = {
+ };
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function () {
+ // assert
+ res.getHeader('Access-Control-Allow-Origin').should.equal('*');
+ done();
+ };
+
+ // act
+ cors(options)(req, res, next);
+ });
+
+ it('specifying true for origin reflects requesting origin', function (done) {
+ // arrange
+ var req, res, next, options;
+ options = {
+ origin: true
+ };
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function () {
+ // assert
+ res.getHeader('Access-Control-Allow-Origin').should.equal('request.com');
+ done();
+ };
+
+ // act
+ cors(options)(req, res, next);
+ });
+
+ it('should allow origin when callback returns true', function (done) {
+ var req, res, next, options;
+ options = {
+ origin: function (sentOrigin, cb) {
+ sentOrigin.should.equal('request.com');
+ cb(null, true);
+ }
+ };
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function () {
+ res.getHeader('Access-Control-Allow-Origin').should.equal('request.com');
+ done();
+ };
+
+ cors(options)(req, res, next);
+ });
+
+ it('should not allow origin when callback returns false', function (done) {
+ var req, res, next, options;
+ options = {
+ origin: function (sentOrigin, cb) {
+ sentOrigin.should.equal('request.com');
+ cb(null, false);
+ }
+ };
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function () {
+ should.not.exist(res.getHeader('Access-Control-Allow-Origin'));
+ should.not.exist(res.getHeader('Access-Control-Allow-Methods'));
+ should.not.exist(res.getHeader('Access-Control-Allow-Headers'));
+ should.not.exist(res.getHeader('Access-Control-Allow-Credentials'));
+ should.not.exist(res.getHeader('Access-Control-Allow-Max-Age'));
+ done();
+ };
+
+ cors(options)(req, res, next);
+ });
+
+ it('should not override options.origin callback', function (done) {
+ var req, res, next, options;
+ options = {
+ origin: function (sentOrigin, cb) {
+ var isValid = sentOrigin === 'request.com';
+ cb(null, isValid);
+ }
+ };
+
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function () {
+ res.getHeader('Access-Control-Allow-Origin').should.equal('request.com');
+ };
+
+ cors(options)(req, res, next);
+
+ req = fakeRequest({
+ 'origin': 'invalid-request.com'
+ });
+ res = fakeResponse();
+
+ next = function () {
+ should.not.exist(res.getHeader('Access-Control-Allow-Origin'));
+ should.not.exist(res.getHeader('Access-Control-Allow-Methods'));
+ should.not.exist(res.getHeader('Access-Control-Allow-Headers'));
+ should.not.exist(res.getHeader('Access-Control-Allow-Credentials'));
+ should.not.exist(res.getHeader('Access-Control-Allow-Max-Age'));
+ done();
+ };
+
+ cors(options)(req, res, next);
+ });
+
+
+ it('can override methods', function (done) {
+ // arrange
+ var req, res, next, options;
+ options = {
+ methods: ['method1', 'method2']
+ };
+ req = fakeRequest();
+ req.method = 'OPTIONS';
+ res = fakeResponse();
+ res.end = function () {
+ // assert
+ res.statusCode.should.equal(204);
+ done();
+ };
+ next = function () {
+ // assert
+ res.getHeader('Access-Control-Allow-Methods').should.equal('method1,method2');
+ done();
+ };
+
+ // act
+ cors(options)(req, res, next);
+ });
+
+ it('methods defaults to GET, PUT, PATCH, POST, DELETE', function (done) {
+ // arrange
+ var req, res, next, options;
+ options = {
+ };
+ req = fakeRequest();
+ req.method = 'OPTIONS';
+ res = fakeResponse();
+ res.end = function () {
+ // assert
+ res.statusCode.should.equal(204);
+ done();
+ };
+ next = function () {
+ // assert
+ res.getHeader('Access-Control-Allow-Methods').should.equal('GET,PUT,PATCH,POST,DELETE');
+ done();
+ };
+
+ // act
+ cors(options)(req, res, next);
+ });
+
+ it('can specify allowed headers', function (done) {
+ // arrange
+ var req, res, options;
+ options = {
+ allowedHeaders: ['header1', 'header2']
+ };
+ req = fakeRequest();
+ req.method = 'OPTIONS';
+ res = fakeResponse();
+ res.end = function () {
+ // assert
+ res.getHeader('Access-Control-Allow-Headers').should.equal('header1,header2');
+ done();
+ };
+
+ // act
+ cors(options)(req, res, null);
+ });
+
+ it('specifying an empty list or string of allowed headers will result in no response header for allowed headers', function (done) {
+ // arrange
+ var req, res, next, options;
+ options = {
+ allowedHeaders: []
+ };
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function () {
+ // assert
+ should.not.exist(res.getHeader('Access-Control-Allow-Headers'));
+ done();
+ };
+
+ // act
+ cors(options)(req, res, next);
+ });
+
+ it('if no allowed headers are specified, defaults to requested allowed headers', function (done) {
+ // arrange
+ var req, res, options;
+ options = {
+ };
+ req = fakeRequest();
+ req.method = 'OPTIONS';
+ res = fakeResponse();
+ res.end = function () {
+ // assert
+ res.getHeader('Access-Control-Allow-Headers').should.equal('requestedHeader1,requestedHeader2');
+ done();
+ };
+
+ // act
+ cors(options)(req, res, null);
+ });
+
+ it('can specify exposed headers', function (done) {
+ // arrange
+ var req, res, options, next;
+ options = {
+ exposedHeaders: ['custom-header1', 'custom-header2']
+ };
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function () {
+ // assert
+ res.getHeader('Access-Control-Expose-Headers').should.equal('custom-header1,custom-header2');
+ done();
+ };
+
+ // act
+ cors(options)(req, res, next);
+ });
+
+ it('includes credentials if explicitly enabled', function (done) {
+ // arrange
+ var req, res, options;
+ options = {
+ credentials: true
+ };
+ req = fakeRequest();
+ req.method = 'OPTIONS';
+ res = fakeResponse();
+ res.end = function () {
+ // assert
+ res.getHeader('Access-Control-Allow-Credentials').should.equal('true');
+ done();
+ };
+
+ // act
+ cors(options)(req, res, null);
+ });
+
+ it('does not includes credentials unless explicitly enabled', function (done) {
+ // arrange
+ var req, res, next, options;
+ options = {
+ };
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function () {
+ // assert
+ should.not.exist(res.getHeader('Access-Control-Allow-Credentials'));
+ done();
+ };
+
+ // act
+ cors(options)(req, res, next);
+ });
+
+ it('includes maxAge when specified', function (done) {
+ // arrange
+ var req, res, options;
+ options = {
+ maxAge: 456
+ };
+ req = fakeRequest();
+ req.method = 'OPTIONS';
+ res = fakeResponse();
+ res.end = function () {
+ // assert
+ res.getHeader('Access-Control-Max-Age').should.equal('456');
+ done();
+ };
+
+ // act
+ cors(options)(req, res, null);
+ });
+
+ it('does not includes maxAge unless specified', function (done) {
+ // arrange
+ var req, res, next, options;
+ options = {
+ };
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function () {
+ // assert
+ should.not.exist(res.getHeader('Access-Control-Allow-Max-Age'));
+ done();
+ };
+
+ // act
+ cors(options)(req, res, next);
+ });
+ });
+
+ describe('passing a function to build options', function () {
+ it('handles options specified via callback', function (done) {
+ // arrange
+ var req, res, next, delegate;
+ delegate = function (req2, cb) {
+ cb(null, {
+ origin: 'delegate.com'
+ });
+ };
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function () {
+ // assert
+ res.getHeader('Access-Control-Allow-Origin').should.equal('delegate.com');
+ done();
+ };
+
+ // act
+ cors(delegate)(req, res, next);
+ });
+
+ it('handles error specified via callback', function (done) {
+ // arrange
+ var req, res, next, delegate;
+ delegate = function (req2, cb) {
+ cb('some error');
+ };
+ req = fakeRequest();
+ res = fakeResponse();
+ next = function (err) {
+ // assert
+ err.should.equal('some error');
+ done();
+ };
+
+ // act
+ cors(delegate)(req, res, next);
+ });
+ });
+ });
+
+}());
diff --git a/test/error-response.js b/test/error-response.js
new file mode 100644
index 0000000..e751b96
--- /dev/null
+++ b/test/error-response.js
@@ -0,0 +1,77 @@
+(function () {
+ /*global describe, it*/
+
+ 'use strict';
+
+ var should = require('should'),
+ express = require('express'),
+ supertest = require('supertest'),
+ cors = require('../lib');
+
+ var app;
+
+ /* -------------------------------------------------------------------------- */
+
+ app = express();
+ app.use(cors());
+
+ app.post('/five-hundred', function (req, res, next) {
+ next(new Error('nope'));
+ });
+
+ app.post('/four-oh-one', function (req, res, next) {
+ next(new Error('401'));
+ });
+
+ app.post('/four-oh-four', function (req, res, next) {
+ next();
+ });
+
+ app.use(function (err, req, res, next) {
+ if (err.message === '401') {
+ res.status(401).send('unauthorized');
+ } else {
+ next(err);
+ }
+ });
+
+ /* -------------------------------------------------------------------------- */
+
+ describe('error response', function () {
+ it('500', function (done) {
+ supertest(app)
+ .post('/five-hundred')
+ .expect(500)
+ .end(function (err, res) {
+ should.not.exist(err);
+ res.headers['access-control-allow-origin'].should.eql('*');
+ res.text.should.startWith('Error: nope');
+ done();
+ });
+ });
+
+ it('401', function (done) {
+ supertest(app)
+ .post('/four-oh-one')
+ .expect(401)
+ .end(function (err, res) {
+ should.not.exist(err);
+ res.headers['access-control-allow-origin'].should.eql('*');
+ res.text.should.eql('unauthorized');
+ done();
+ });
+ });
+
+ it('404', function (done) {
+ supertest(app)
+ .post('/four-oh-four')
+ .expect(404)
+ .end(function (err, res) {
+ should.not.exist(err);
+ res.headers['access-control-allow-origin'].should.eql('*');
+ done();
+ });
+ });
+ });
+
+}());
diff --git a/test/example-app.js b/test/example-app.js
new file mode 100644
index 0000000..590cb31
--- /dev/null
+++ b/test/example-app.js
@@ -0,0 +1,98 @@
+(function () {
+ /*global describe, it*/
+
+ 'use strict';
+
+ var should = require('should'),
+ express = require('express'),
+ supertest = require('supertest'),
+ cors = require('../lib');
+
+ var simpleApp,
+ complexApp;
+
+ /* -------------------------------------------------------------------------- */
+
+ simpleApp = express();
+ simpleApp.head('/', cors(), function (req, res) {
+ res.status(204).send();
+ });
+ simpleApp.get('/', cors(), function (req, res) {
+ res.send('Hello World (Get)');
+ });
+ simpleApp.post('/', cors(), function (req, res) {
+ res.send('Hello World (Post)');
+ });
+
+ /* -------------------------------------------------------------------------- */
+
+ complexApp = express();
+ complexApp.options('/', cors());
+ complexApp.delete('/', cors(), function (req, res) {
+ res.send('Hello World (Delete)');
+ });
+
+ /* -------------------------------------------------------------------------- */
+
+ describe('example app(s)', function () {
+ describe('simple methods', function () {
+ it('GET works', function (done) {
+ supertest(simpleApp)
+ .get('/')
+ .expect(200)
+ .end(function (err, res) {
+ should.not.exist(err);
+ res.headers['access-control-allow-origin'].should.eql('*');
+ res.text.should.eql('Hello World (Get)');
+ done();
+ });
+ });
+ it('HEAD works', function (done) {
+ supertest(simpleApp)
+ .head('/')
+ .expect(204)
+ .end(function (err, res) {
+ should.not.exist(err);
+ res.headers['access-control-allow-origin'].should.eql('*');
+ done();
+ });
+ });
+ it('POST works', function (done) {
+ supertest(simpleApp)
+ .post('/')
+ .expect(200)
+ .end(function (err, res) {
+ should.not.exist(err);
+ res.headers['access-control-allow-origin'].should.eql('*');
+ res.text.should.eql('Hello World (Post)');
+ done();
+ });
+ });
+ });
+
+ describe('complex methods', function () {
+ it('OPTIONS works', function (done) {
+ supertest(complexApp)
+ .options('/')
+ .expect(204)
+ .end(function (err, res) {
+ should.not.exist(err);
+ res.headers['access-control-allow-origin'].should.eql('*');
+ done();
+ });
+ });
+ it('DELETE works', function (done) {
+ supertest(complexApp)
+ .del('/')
+ .expect(200)
+ .end(function (err, res) {
+ should.not.exist(err);
+ res.headers['access-control-allow-origin'].should.eql('*');
+ res.text.should.eql('Hello World (Delete)');
+ done();
+ });
+ });
+ });
+ });
+
+}());
diff --git a/test/issue-2.js b/test/issue-2.js
new file mode 100644
index 0000000..0784bcd
--- /dev/null
+++ b/test/issue-2.js
@@ -0,0 +1,56 @@
+(function () {
+ /*global describe, it*/
+
+ 'use strict';
+
+ var should = require('should'),
+ express = require('express'),
+ supertest = require('supertest'),
+ cors = require('../lib');
+
+ var app,
+ corsOptions;
+
+ /* -------------------------------------------------------------------------- */
+
+ app = express();
+ corsOptions = {
+ origin: true,
+ methods: ['POST'],
+ credentials: true,
+ maxAge: 3600
+ };
+ app.options('/api/login', cors(corsOptions));
+ app.post('/api/login', cors(corsOptions), function (req, res) {
+ res.send('LOGIN');
+ });
+
+ /* -------------------------------------------------------------------------- */
+
+ describe('issue #2', function () {
+ it('OPTIONS works', function (done) {
+ supertest(app)
+ .options('/api/login')
+ .expect(204)
+ .set('Origin', 'http://example.com')
+ .end(function (err, res) {
+ should.not.exist(err);
+ res.headers['access-control-allow-origin'].should.eql('http://example.com');
+ done();
+ });
+ });
+ it('POST works', function (done) {
+ supertest(app)
+ .post('/api/login')
+ .expect(200)
+ .set('Origin', 'http://example.com')
+ .end(function (err, res) {
+ should.not.exist(err);
+ res.headers['access-control-allow-origin'].should.eql('http://example.com');
+ res.text.should.eql('LOGIN');
+ done();
+ });
+ });
+ });
+
+}());
diff --git a/test/issue-31.js b/test/issue-31.js
new file mode 100644
index 0000000..4f678fe
--- /dev/null
+++ b/test/issue-31.js
@@ -0,0 +1,58 @@
+(function () {
+ /*global describe, it*/
+
+ 'use strict';
+
+ var should = require('should'),
+ express = require('express'),
+ supertest = require('supertest'),
+ cors = require('../lib');
+
+ var app,
+ mainRouter,
+ itemsRouter;
+
+ /* -------------------------------------------------------------------------- */
+
+ itemsRouter = new express.Router();
+ itemsRouter.get('/', function (req, res) {
+ res.send('hello world');
+ });
+
+ mainRouter = new express.Router();
+ mainRouter.use('/items', itemsRouter);
+
+ app = express();
+ app.use(cors());
+ app.use(mainRouter);
+
+ /* -------------------------------------------------------------------------- */
+
+ describe('issue #31', function () {
+ it('OPTIONS works', function (done) {
+ supertest(app)
+ .options('/items')
+ .expect(204)
+ .set('Origin', 'http://example.com')
+ .end(function (err, res) {
+ should.not.exist(err);
+ res.headers['access-control-allow-origin'].should.eql('*');
+ done();
+ });
+ });
+
+ it('GET works', function (done) {
+ supertest(app)
+ .get('/items')
+ .expect(200)
+ .set('Origin', 'http://example.com')
+ .end(function (err, res) {
+ should.not.exist(err);
+ res.headers['access-control-allow-origin'].should.eql('*');
+ res.text.should.eql('hello world');
+ done();
+ });
+ });
+ });
+
+}());
diff --git a/test/mocha.opts b/test/mocha.opts
new file mode 100644
index 0000000..780b796
--- /dev/null
+++ b/test/mocha.opts
@@ -0,0 +1,3 @@
+--ui bdd
+--reporter spec
+--require should
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-cors.git
More information about the Pkg-javascript-commits
mailing list