diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..150a05b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e59e82 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +.nyc_output +coverage diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0ec0709 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +## v1.0.1 - 2019-07-17 + +### Misc + +- Minor update in lodash to mitigate a snyk reported vulnerability +- Fixes bug in tests +- Minor updates in README + +## v1.0.0 - 2018-10-30 + +### Adds + +- Adds option for the samesite cookie flag +- Adds autoRenew option + +### Changes + +- Updates cryptographic algorithms. It's now using AES 256 in GCM mode + +### Removes + +- Removes the following exported functions: + - readSession + - readCookies + - checkLength + - headersToArray + - hmac_signature + +### Misc + +- Code refactor diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..0804cf3 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @auth0/product-security diff --git a/README.md b/README.md index 670bf98..bfb1e40 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,43 @@ # Cookie-Sessions -Secure cookie-based session middleware for -[Connect](http://github.com/senchalabs/connect). This is a new module and I -wouldn't recommend for production use just yet. +Secure cookie-based session middleware for Express. Session data is stored on the request object in the 'session' property: +```js + var app = require('express'); + var cookieParser = require('cookie-parser'); + var cookieSessions = require('cookie-sessions'); - var connect = require('connect'), - sessions = require('cookie-sessions'); + app.use( + cookieSessions({ + name: 'session_data', + secret: process.env.SECRET + }) + ); +``` - Connect.createServer( - sessions({secret: '123abc'}), - function(req, res, next){ - req.session = {'hello':'world'}; - res.writeHead(200, {'Content-Type':'text/plain'}); - res.end('session data updated'); - } - ).listen(8080); +The [cookie-parser](https://www.npmjs.com/package/cookie-parser) middleware +MUST also used. -The session data is JSON.stringified, encrypted and timestamped, then a HMAC -signature is attached to test for tampering. The main function accepts a -number of options: +The session data can be any JSON object. It's timestamped, encrypted and +authenticated automatically. The authenticated encryption uses `aes-256-gcm` +offered by the node `crypto` library. The httpOnly and secure cookie flags are +set by default. - * secret -- The secret to encrypt the session data with - * timeout -- The amount of time in miliseconds before the cookie expires - (default: 24 hours) - * session_key -- The cookie key name to store the session data in - (default: _node) - * path -- The path to use for the cookie (default: '/') - * domain -- (optional) Define a specific domain/subdomain scope for the cookie +The main function accepts a number of options: + +| Option | Required | Description | Default | +|---------------|----------|-------------------------------------------------------------------------------------------------------------------------|----------| +| secret | Yes | The secret to encrypt the session data. | | +| timeout | Yes | The amount of time in milliseconds before the cookie expires. | 24 hours | +| name | Yes | The cookie name in which to store the session data. | `\_node` | +| path | Yes | The path to use for the cookie. | `/` | +| domain | No | Define a specific domain/subdomain scope for the cookie. | | +| autoRenew | No | Boolean: if true, a new cookie will be set in each response with an updated expiration Date.now() + timeout | true | +| httpOnly | No | Boolean: if true, the httpOnly cookie flag will be set. | true | +| secure | No | Boolean: if true, the secure cookie flag will be set. | true | +| sameSite | No | If set to "lax" or "strict", the sameSite cookie flag with the corresponding mode will be set. | | +| sessionCookie | No | Boolean: if true, it's considered a session cookie and no "expires" is set. | | ## Why store session data in cookies? @@ -47,3 +56,16 @@ number of options: __In summary:__ don't use cookie storage if you keep a lot of data in your sessions! + +## Migrating to version 1.0.0 + +* Any cookie created with 0.0.2 version will be invalidated. +* The `options` object has two naming changes: + * `name` instead of `session_key` + * `sessionCookie` instead of `session_cookie` +* The following exported functions have been removed: + * readSession + * readCookies + * checkLength + * headersToArray + * hmac\_signature diff --git a/deps/nodeunit b/deps/nodeunit deleted file mode 160000 index 0e1afee..0000000 --- a/deps/nodeunit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0e1afee3a47a29c63d296de7dabc687122a301bd diff --git a/lib/cookie-sessions.js b/lib/cookie-sessions.js index 477a388..0cb319f 100644 --- a/lib/cookie-sessions.js +++ b/lib/cookie-sessions.js @@ -1,219 +1,169 @@ -var crypto = require('crypto'); -var url = require('url'); - -var exports = module.exports = function(settings){ - - var default_settings = { - // don't set a default cookie secret, must be explicitly defined - session_key: '_node', - timeout: 1000 * 60 * 60 * 24, // 24 hours - path: '/' - }; - var s = extend(default_settings, settings); - if(!s.secret) throw new Error('No secret set in cookie-session settings'); - - if(typeof s.path !== 'string' || s.path.indexOf('/') != 0) - throw new Error('invalid cookie path, must start with "/"'); - - return function(req, res, next){ - // if the request is not under the specified path, do nothing. - if (url.parse(req.url).pathname.indexOf(s.path) != 0) { - next(); - return; - } - - // Read session data from a request and store it in req.session - req.session = exports.readSession( - s.session_key, s.secret, s.timeout, req); - - // proxy writeHead to add cookie to response - var _writeHead = res.writeHead; - res.writeHead = function(statusCode){ - - var reasonPhrase, headers; - if (typeof arguments[1] === 'string') { - reasonPhrase = arguments[1]; - headers = arguments[2] || {}; - } - else { - headers = arguments[1] || {}; - } - - // Add a Set-Cookie header to all responses with the session data - // and the current timestamp. The cookie needs to be set on every - // response so that the timestamp is up to date, and the session - // does not expire unless the user is inactive. - - var cookiestr; - if (req.session === undefined) { - if ("cookie" in req.headers) { - cookiestr = escape(s.session_key) + '=' - + '; expires=' + exports.expires(0) - + '; path=' + s.path + '; HttpOnly'; - } - } else { - cookiestr = escape(s.session_key) + '=' - + escape(exports.serialize(s.secret, req.session)) - + '; expires=' + exports.expires(s.timeout) - + '; path=' + s.path + '; HttpOnly'; - } - if ( s.domain ) cookiestr +='; Domain=' + s.domain; - - if (cookiestr !== undefined) { - if(Array.isArray(headers)) headers.push(['Set-Cookie', cookiestr]); - else { - // if a Set-Cookie header already exists, convert headers to - // array so we can send multiple Set-Cookie headers. - if(headers['Set-Cookie'] !== undefined){ - headers = exports.headersToArray(headers); - headers.push(['Set-Cookie', cookiestr]); - } - // if no Set-Cookie header exists, leave the headers as an - // object, and add a Set-Cookie property - else { - headers['Set-Cookie'] = cookiestr; - } - } - } - - var args = [statusCode, reasonPhrase, headers]; - if (!args[1]) { - args.splice(1, 1); - } - // call the original writeHead on the request - return _writeHead.apply(res, args); - } - next(); - - }; +var _ = require('lodash') +var crypto = require('crypto') + +const MAX_LENGTH = 4096; + +// don't set a default cookie secret, must be explicitly defined +const DEFAULT_SETTINGS = { + name: '_node', + timeout: 1000 * 60 * 60 * 24, // 24 hours + path: '/', + autoRenew: true, + httpOnly: true, + secure: true }; -exports.headersToArray = function(headers){ - if(Array.isArray(headers)) return headers; - return Object.keys(headers).reduce(function(arr, k){ - arr.push([k, headers[k]]); - return arr; - }, []); -}; +var exports = module.exports = function(settings) { + var opts = Object.assign({}, DEFAULT_SETTINGS, settings) + if (!opts.secret) { + throw new Error('No secret set in cookie-session settings'); + } -// Extend a given object with all the properties in passed-in object(s). -// From underscore.js (http://documentcloud.github.com/underscore/) -function extend(obj) { - Array.prototype.slice.call(arguments).forEach(function(source) { - for (var prop in source) obj[prop] = source[prop]; - }); - return obj; -}; + if (typeof opts.path !== 'string' || opts.path.indexOf('/') !== 0) { + throw new Error('Invalid cookie path, must start with "/"'); + } + + if (opts.sameSite && (opts.sameSite !== 'lax' || opts.sameSite !== 'strict')) { + throw new Error('Possible values for sameSite option: "lax" or "strict"'); + } -exports.deserialize = function(secret, timeout, str){ - // Parses a secure cookie string, returning the object stored within it. - // Throws an exception if the secure cookie string does not validate. + return function(req, res, next) { - if(!exports.valid(secret, timeout, str)){ - throw new Error('invalid cookie'); + // if the request is not under the specified path, do nothing. + if (req.path.indexOf(opts.path) !== 0) { + next(); + return; } - var data = exports.decrypt(secret, exports.split(str).data_blob); - return JSON.parse(data); -}; + req.session = {} -exports.serialize = function(secret, data){ - // Turns a JSON-compatibile object literal into a secure cookie string + // Read session data from a request and store it in req.session + var session = { + data: {} + } + try { + session = exports.deserialize(opts.secret, req.cookies[opts.name], opts.timeout); + req.session = session.data + } catch (err) { + } + var oldSession = _.cloneDeep(req.session) + + var options = _.pick(opts, [ 'path', 'domain', 'httpOnly', 'secure', 'sameSite' ]) + + var _writeHead = res.writeHead; + res.writeHead = function() { + // only set cookie if autoRenew is on or session data changed + if (opts.autoRenew || !_.isEqual(oldSession, req.session)) { + /* + * If the session is empty, no cookie is sent (and the cookie is + * cleared if it is already set). If a session exists, we add a + * Set-Cookie header to all responses with the session data and the + * current timestamp. The cookie needs to be set on every response so + * that the timestamp is up to date, and the session does not expire + * unless the user is inactive. + */ + if (_.isEqual(req.session, {})) { + if (req.cookies[opts.name]) { + res.clearCookie(opts.name, options) + } + } else { + if ((!opts.sessionCookie || !opts.autoRenew) && opts.timeout) { + options.expires = new Date(Date.now() + opts.timeout); + } + try { + cookiestr = exports.serialize(opts.secret, req.session, session.time, opts.autoRenew) + } catch(err) { + cookiestr = '' + } + res.cookie(opts.name, cookiestr, options) + } + } - var data_str = JSON.stringify(data); - var data_enc = exports.encrypt(secret, data_str); - var timestamp = (new Date()).getTime(); - var hmac_sig = exports.hmac_signature(secret, timestamp, data_enc); - var result = hmac_sig + timestamp + data_enc; - if(!exports.checkLength(result)){ - throw new Error('data too long to store in a cookie'); + return _writeHead.apply(res, arguments); } - return result; + next() + }; }; exports.split = function(str){ - // Splits a cookie string into hmac signature, timestamp and data blob. - return { - hmac_signature: str.slice(0,40), - timestamp: parseInt(str.slice(40, 53), 10), - data_blob: str.slice(53) - }; + var arr = str.split('$') + return { + ciphertext: arr[0], + iv: arr[1], + authTag: arr[2] + }; }; -exports.hmac_signature = function(secret, timestamp, data){ - // Generates a HMAC for the timestamped data, returning the - // hex digest for the signature. - var hmac = crypto.createHmac('sha1', secret); - hmac.update(timestamp + data); - return hmac.digest('hex'); +exports.merge = function(obj) { + return [ obj.ciphertext, obj.iv, obj.authTag ].join('$') }; -exports.valid = function(secret, timeout, str){ - // Tests the validity of a cookie string. Returns true if the HMAC - // signature of the secret, timestamp and data blob matches the HMAC in the - // cookie string, and the cookie's age is less than the timeout value. - - var parts = exports.split(str); - var hmac_sig = exports.hmac_signature( - secret, parts.timestamp, parts.data_blob - ); - return ( - parts.hmac_signature === hmac_sig && - parts.timestamp + timeout > new Date().getTime() - ); -}; - -exports.decrypt = function(secret, str){ - // Decrypt the aes192 encoded str using secret. - var decipher = crypto.createDecipher("aes192", secret); - return decipher.update(str, 'hex', 'utf8') + decipher.final('utf8'); -}; +exports.valid = function(timestamp, timeout) { + return timestamp + timeout > Date.now(); +} -exports.encrypt = function(secret, str){ - // Encrypt the str with aes192 using secret. - var cipher = crypto.createCipher("aes192", secret); - return cipher.update(str, 'utf8', 'hex') + cipher.final('hex'); +exports.encrypt = function(secret, message) { + try { + var iv = crypto.randomBytes(16); + var cipher = crypto.createCipheriv('aes-256-gcm', secret, iv); + var ciphertext = cipher.update(message, 'utf8', 'hex'); + ciphertext += cipher.final('hex'); + return { + ciphertext: ciphertext, + iv: iv.toString('hex'), + authTag: cipher.getAuthTag().toString('hex') + }; + } catch(err) { + throw new Error('Failed to encrypt cookie') + } }; -exports.checkLength = function(str){ - // Test if a string is within the maximum length allowed for a cookie. - return str.length <= 4096; +exports.decrypt = function(secret, ciphertext, iv, authTag) { + try { + var decipher = crypto.createDecipheriv('aes-256-gcm', secret, Buffer.from(iv, 'hex')); + decipher.setAuthTag(Buffer.from(authTag, 'hex')); + var plaintext = decipher.update(ciphertext, 'hex', 'utf8'); + plaintext += decipher.final('utf8'); + return plaintext; + } catch(err) { + throw new Error('Failed to decrypt and authenticate cookie') + } }; -exports.readCookies = function(req){ - // if "cookieDecoder" is in use, then req.cookies - // will already contain the parsed cookies - if (req.cookies) { - return req.cookies; - } - else { - // Extracts the cookies from a request object. - var cookie = req.headers.cookie; - if(!cookie){ - return {}; - } - var parts = cookie.split(/\s*;\s*/g).map(function(x){ - return x.split('='); - }); - return parts.reduce(function(a, x){ - a[unescape(x[0])] = unescape(x[1]); - return a; - }, {}); - } -}; - -exports.readSession = function(key, secret, timeout, req){ - // Reads the session data stored in the cookie named 'key' if it validates, - // otherwise returns an empty object. - - var cookies = exports.readCookies(req); - if(cookies[key]){ - return exports.deserialize(secret, timeout, cookies[key]); +// Turns a JSON-compatibile object literal into a secure cookie string +exports.serialize = function(secret, data, sessionTime, autoRenew) { + if (autoRenew || typeof sessionTime === 'undefined') { + sessionTime = Date.now() + } + var cookieData = { + d: data, + t: sessionTime + } + var dataStr = JSON.stringify(cookieData); + var enc = exports.encrypt(secret, dataStr) + var result = exports.merge(enc) + if (result.length > MAX_LENGTH) { + throw Error('Data too long to store in a cookie') ; + } + return result +} + +// Parses a secure cookie string, returning the object stored within it. +exports.deserialize = function(secret, str, timeout) { + if (!str) { + return { + data: {} } - return undefined; -}; - - -exports.expires = function(timeout){ - return (new Date(new Date().getTime() + (timeout))).toUTCString(); + } + var splitted = exports.split(str) + cookieData = exports.decrypt(secret, splitted.ciphertext, splitted.iv, splitted.authTag) + var jsonData = JSON.parse(cookieData) + + if (!exports.valid(jsonData.t, timeout)) { + throw new Error('Cookie expired') + } + return { + data: jsonData.d, + time: jsonData.t + } }; diff --git a/opslevel.yml b/opslevel.yml new file mode 100644 index 0000000..6650937 --- /dev/null +++ b/opslevel.yml @@ -0,0 +1,6 @@ +--- +version: 1 +repository: + owner: product_security_engineering + tier: + tags: diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3dc01e4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2289 @@ +{ + "name": "cookie-sessions", + "version": "1.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/generator": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.1.3.tgz", + "integrity": "sha512-ZoCZGcfIJFJuZBqxcY9OjC1KW2lWK64qrX1o4UYL3yshVhwKFYgzpWZ0vvtGMNJdTlvkw0W+HR1VnYN8q3QPFQ==", + "dev": true, + "requires": { + "@babel/types": "^7.1.3", + "jsesc": "^2.5.1", + "lodash": "^4.17.10", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz", + "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.1.3.tgz", + "integrity": "sha512-gqmspPZOMW3MIRb9HlrnbZHXI1/KHTOroBwN1NcLL6pWxzqzEKGvRTq0W/PxS45OtQGbaFikSQpkS5zbnsQm2w==", + "dev": true + }, + "@babel/template": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.1.2.tgz", + "integrity": "sha512-SY1MmplssORfFiLDcOETrW7fCLl+PavlwMh92rrGcikQaRq4iWPVH0MpwPpY3etVMx6RnDjXtr6VZYr/IbP/Ag==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.1.2", + "@babel/types": "^7.1.2" + } + }, + "@babel/traverse": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.1.4.tgz", + "integrity": "sha512-my9mdrAIGdDiSVBuMjpn/oXYpva0/EZwWL3sm3Wcy/AVWO2eXnsoZruOT9jOGNRXU8KbCIu5zsKnXcAJ6PcV6Q==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.1.3", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.0.0", + "@babel/parser": "^7.1.3", + "@babel/types": "^7.1.3", + "debug": "^3.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.10" + } + }, + "@babel/types": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.1.3.tgz", + "integrity": "sha512-RpPOVfK+yatXyn8n4PB1NW6k9qjinrXrRR8ugBN8fD6hCy5RXI6PSbVqpOJBO9oSaY7Nom4ohj35feb0UR9hSA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.10", + "to-fast-properties": "^2.0.0" + } + }, + "@sinonjs/commons": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.0.2.tgz", + "integrity": "sha512-WR3dlgqJP4QNrLC4iXN/5/2WaLQQ0VijOOkmflqFGVJ6wLEpbSjo7c0ZeGIdtY8Crk7xBBp87sM6+Mkerz7alw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.0.0.tgz", + "integrity": "sha512-vdjoYLDptCgvtJs57ULshak3iJe4NW3sJ3g36xVDGff5AE8P30S6A093EIEPjdi2noGhfuNOEkbxt3J3awFW1w==", + "dev": true, + "requires": { + "@sinonjs/samsam": "2.1.0" + }, + "dependencies": { + "@sinonjs/samsam": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.0.tgz", + "integrity": "sha512-5x2kFgJYupaF1ns/RmharQ90lQkd2ELS8A9X0ymkAAdemYHGtI2KiUHG8nX2WU0T1qgnOU5YMqnBM2V7NUanNw==", + "dev": true, + "requires": { + "array-from": "^2.1.1" + } + } + } + }, + "@sinonjs/samsam": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.2.tgz", + "integrity": "sha512-ZwTHAlC9akprWDinwEPD4kOuwaYZlyMwVJIANsKNC3QVp0AHB04m7RnB4eqeWfgmxw8MGTzS9uMaw93Z3QcZbw==", + "dev": true + }, + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "dev": true, + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", + "dev": true + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true + }, + "cookie-parser": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.3.tgz", + "integrity": "sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU=", + "dev": true, + "requires": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6" + } + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "express": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", + "dev": true + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "globals": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.8.0.tgz", + "integrity": "sha512-io6LkyPVuzCHBSQV9fmOwxZkUk6nIaGmxheLDgmuFv89j0fm2aqDbIXKAGfzCMHqz3HLF2Zf8WSG6VqMh2qFmA==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ipaddr.js": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", + "integrity": "sha512-nPvSZsVlbG9aLhZYaC3Oi1gT/tpyo3Yt5fNyf6NmcKIayz4VV/txxJFFKAK/gU4dcNn8ehsanBbVHVl0+amOLA==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.0.0.tgz", + "integrity": "sha512-eQY9vN9elYjdgN9Iv6NS/00bptm02EBBk70lRMaVjeA6QYocQgenVrSgC28TJurdnZa80AGO3ASdFN+w/njGiQ==", + "dev": true, + "requires": { + "@babel/generator": "^7.0.0", + "@babel/parser": "^7.0.0", + "@babel/template": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "istanbul-lib-coverage": "^2.0.1", + "semver": "^5.5.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=", + "dev": true + }, + "just-extend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-3.0.0.tgz", + "integrity": "sha512-Fu3T6pKBuxjWT/p4DkqGHFRsysc8OauWr4ZRTY9dIx07Y9O0RkoR5jcv28aeD1vuAwhm3nLkDurwLXoALp4DpQ==", + "dev": true + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lolex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.0.0.tgz", + "integrity": "sha512-hcnW80h3j2lbUfFdMArd5UPA/vxZJ+G8vobd+wg3nVEQA0EigStbYcrG030FJxL6xiDDPEkoMatV9xIh5OecQQ==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", + "dev": true + }, + "mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "dev": true, + "requires": { + "mime-db": "~1.37.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "dev": true + }, + "nise": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.6.tgz", + "integrity": "sha512-1GedetLKzmqmgwabuMSqPsT7oumdR77SBpDfNNJhADRIeA3LN/2RVqR4fFqwvzhAqcTef6PPCzQwITE/YQ8S8A==", + "dev": true, + "requires": { + "@sinonjs/formatio": "3.0.0", + "just-extend": "^3.0.0", + "lolex": "^2.3.2", + "path-to-regexp": "^1.7.0", + "text-encoding": "^0.6.4" + }, + "dependencies": { + "lolex": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", + "dev": true + } + } + }, + "nyc": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-13.1.0.tgz", + "integrity": "sha512-3GyY6TpQ58z9Frpv4GMExE1SV2tAgYqC7HSy2omEhNiCT3mhT9NyiOvIE8zkbuJVFzmvvNTnE4h/7/wQae7xLg==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "arrify": "^1.0.1", + "caching-transform": "^2.0.0", + "convert-source-map": "^1.6.0", + "debug-log": "^1.0.1", + "find-cache-dir": "^2.0.0", + "find-up": "^3.0.0", + "foreground-child": "^1.5.6", + "glob": "^7.1.3", + "istanbul-lib-coverage": "^2.0.1", + "istanbul-lib-hook": "^2.0.1", + "istanbul-lib-instrument": "^3.0.0", + "istanbul-lib-report": "^2.0.2", + "istanbul-lib-source-maps": "^2.0.1", + "istanbul-reports": "^2.0.1", + "make-dir": "^1.3.0", + "merge-source-map": "^1.1.0", + "resolve-from": "^4.0.0", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "spawn-wrap": "^1.4.2", + "test-exclude": "^5.0.0", + "uuid": "^3.3.2", + "yargs": "11.1.0", + "yargs-parser": "^9.0.2" + }, + "dependencies": { + "align-text": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "amdefine": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "append-transform": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, + "archy": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "arrify": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "async": { + "version": "1.5.2", + "bundled": true, + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "builtin-modules": { + "version": "1.1.1", + "bundled": true, + "dev": true + }, + "caching-transform": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "make-dir": "^1.0.0", + "md5-hex": "^2.0.0", + "package-hash": "^2.0.0", + "write-file-atomic": "^2.0.0" + } + }, + "camelcase": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true + }, + "center-align": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "cliui": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "commondir": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "convert-source-map": { + "version": "1.6.0", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cross-spawn": { + "version": "4.0.2", + "bundled": true, + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "debug": { + "version": "3.1.0", + "bundled": true, + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "debug-log": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "default-require-extensions": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + } + }, + "error-ex": { + "version": "1.3.2", + "bundled": true, + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es6-error": { + "version": "4.1.1", + "bundled": true, + "dev": true + }, + "execa": { + "version": "0.7.0", + "bundled": true, + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "bundled": true, + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } + } + }, + "find-cache-dir": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^1.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "foreground-child": { + "version": "1.5.6", + "bundled": true, + "dev": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "get-caller-file": { + "version": "1.0.3", + "bundled": true, + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "bundled": true, + "dev": true, + "requires": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "bundled": true, + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "has-flag": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "hosted-git-info": { + "version": "2.7.1", + "bundled": true, + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "bundled": true, + "dev": true + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "invert-kv": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "bundled": true, + "dev": true, + "optional": true + }, + "is-builtin-module": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "isexe": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "istanbul-lib-coverage": { + "version": "2.0.1", + "bundled": true, + "dev": true + }, + "istanbul-lib-hook": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "append-transform": "^1.0.0" + } + }, + "istanbul-lib-report": { + "version": "2.0.2", + "bundled": true, + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.1", + "make-dir": "^1.3.0", + "supports-color": "^5.4.0" + } + }, + "istanbul-lib-source-maps": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "debug": "^3.1.0", + "istanbul-lib-coverage": "^2.0.1", + "make-dir": "^1.3.0", + "rimraf": "^2.6.2", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "bundled": true, + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "handlebars": "^4.0.11" + } + }, + "json-parse-better-errors": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "bundled": true, + "dev": true, + "optional": true + }, + "lcid": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash.flattendeep": { + "version": "4.4.0", + "bundled": true, + "dev": true + }, + "longest": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "lru-cache": { + "version": "4.1.3", + "bundled": true, + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "make-dir": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "md5-hex": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "md5-o-matic": "^0.1.1" + } + }, + "md5-o-matic": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "mem": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "merge-source-map": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "bundled": true, + "dev": true + } + } + }, + "mimic-fn": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.10", + "bundled": true, + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + } + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "normalize-package-data": { + "version": "2.4.0", + "bundled": true, + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "bundled": true, + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optimist": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "os-locale": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + } + }, + "p-finally": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "p-limit": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "package-hash": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "lodash.flattendeep": "^4.4.0", + "md5-hex": "^2.0.0", + "release-zalgo": "^1.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "path-key": { + "version": "2.0.1", + "bundled": true, + "dev": true + }, + "path-type": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "pseudomap": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + }, + "release-zalgo": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "repeat-string": { + "version": "1.6.1", + "bundled": true, + "dev": true, + "optional": true + }, + "require-directory": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "bundled": true, + "dev": true + }, + "right-align": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "align-text": "^0.1.1" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "dev": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true + }, + "source-map": { + "version": "0.5.7", + "bundled": true, + "dev": true, + "optional": true + }, + "spawn-wrap": { + "version": "1.4.2", + "bundled": true, + "dev": true, + "requires": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + } + }, + "spdx-correct": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "bundled": true, + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "string-width": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "bundled": true, + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "test-exclude": { + "version": "5.0.0", + "bundled": true, + "dev": true, + "requires": { + "arrify": "^1.0.1", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^1.0.1" + } + }, + "uglify-js": { + "version": "2.8.29", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "yargs": { + "version": "3.10.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "uuid": { + "version": "3.3.2", + "bundled": true, + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "window-size": { + "version": "0.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "0.0.3", + "bundled": true, + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "write-file-atomic": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "y18n": { + "version": "3.2.1", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "2.1.2", + "bundled": true, + "dev": true + }, + "yargs": { + "version": "11.1.0", + "bundled": true, + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.1.1", + "find-up": "^2.1.0", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^9.0.2" + }, + "dependencies": { + "cliui": { + "version": "4.1.0", + "bundled": true, + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "bundled": true, + "dev": true + } + } + }, + "yargs-parser": { + "version": "9.0.2", + "bundled": true, + "dev": true, + "requires": { + "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "bundled": true, + "dev": true + } + } + } + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "proxy-addr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", + "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.8.0" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "dev": true + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "dev": true, + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "sinon": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.0.0.tgz", + "integrity": "sha512-8OrSYFPZ9xaECfi1ayVMd0ihYCW2OZYgX3rBczrB990gHZMM+aftvhNTJazGz/luS0Us9NWgD5P3KGQ7kYZvGg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.0.2", + "@sinonjs/formatio": "^3.0.0", + "@sinonjs/samsam": "^2.1.2", + "diff": "^3.5.0", + "lodash.get": "^4.4.2", + "lolex": "^3.0.0", + "nise": "^1.4.5", + "supports-color": "^5.5.0", + "type-detect": "^4.0.8" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + } + }, + "supertest": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.3.0.tgz", + "integrity": "sha512-dMQSzYdaZRSANH5LL8kX3UpgK9G1LRh/jnggs/TI0W2Sz7rkMx9Y48uia3K9NgcaWEV28tYkBnXE4tiFC77ygQ==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^3.8.3" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "text-encoding": { + "version": "0.6.4", + "resolved": "http://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/package.json b/package.json index daddbdb..b3411be 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,33 @@ -{ "name": "cookie-sessions" -, "description": "Secure cookie-based session middleware for Connect" -, "main": "./index" -, "author": "Caolan McMahon" -, "version": "0.0.2" -, "repository" : - { "type" : "git" - , "url" : "http://github.com/caolan/cookie-sessions.git" - } -, "bugs" : { "url" : "http://github.com/caolan/cookie-sessions/issues" } -, "licenses" : - [ { "type" : "MIT" - , "url" : "http://github.com/caolan/cookie-sessions/raw/master/LICENSE" +{ + "name": "cookie-sessions", + "description": "Secure cookie-based session middleware for Express", + "main": "./lib/cookie-sessions", + "version": "1.0.1", + "scripts": { + "test": "NODE_PATH=. nyc mocha ./test" + }, + "repository": { + "type": "git", + "url": "http://github.com/auth0/cookie-sessions.git" + }, + "bugs": { + "url": "http://github.com/auth0/cookie-sessions/issues" + }, + "devDependencies": { + "cookie-parser": "^1.4.3", + "express": "^4.16.4", + "mocha": "^5.2.0", + "nyc": "^13.1.0", + "sinon": "^7.0.0", + "supertest": "^3.3.0" + }, + "licenses": [ + { + "type": "MIT", + "url": "http://github.com/caolan/cookie-sessions/raw/master/LICENSE" } - ] + ], + "dependencies": { + "lodash": "^4.17.21" + } } diff --git a/test.js b/test.js deleted file mode 100755 index f41d154..0000000 --- a/test.js +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/local/bin/node - -require.paths.push(__dirname); -require.paths.push(__dirname + '/deps'); -require.paths.push(__dirname + '/lib'); - -try { - var testrunner = require('nodeunit').testrunner; -} -catch(e) { - var sys = require('sys'); - sys.puts("Cannot find nodeunit module."); - sys.puts("You can download submodules for this project by doing:"); - sys.puts(""); - sys.puts(" git submodule init"); - sys.puts(" git submodule update"); - sys.puts(""); - process.exit(); -} - -process.chdir(__dirname); -testrunner.run(['test']); diff --git a/test/cookie-sessions.test.js b/test/cookie-sessions.test.js new file mode 100644 index 0000000..af1e211 --- /dev/null +++ b/test/cookie-sessions.test.js @@ -0,0 +1,264 @@ +var sessions = require('../lib/cookie-sessions'); +var assert = require('assert') +var sinon = require('sinon') +var crypto = require('crypto') +var request = require('supertest'); +var express = require('express'); +var cookieParser = require('cookie-parser') + +describe('cookie sessions tests', function() { + var secret = '8c06bdc84bf095d76b18c1e3a485dfe6' + var message = 'the answer to the ultimate question of life, the universe, and everything' + var encr = '017cb25516cf63f676e680abf99e2797da7f0765b3f19e53adbdadfaf2cd8f9a9378a35b1e52fb00e2a5aa577eb277d98096dbb75afd9dd57b0498e54ead8fc66ed980bf1e60a1b2b8' + var iv = '55df03d8dd90145b7f23211613d46b2d' + var authTag = 'c9e0fe9af2412ce941d18e7eae667402' + var user_data = { + id: 123, + username: 'foobar', + email: 'foobar@gmail.com', + role: 'user' + } + + beforeEach(function () { + this.sandbox = sinon.createSandbox(); + }); + + afterEach(function () { + this.sandbox.restore(); + }); + + describe('encrypt', function() { + it('should sucessfully return encrypted data', function() { + enc = sessions.encrypt(secret, message) + assert.ok(enc.ciphertext) + assert.ok(enc.iv) + assert.ok(enc.authTag) + }) + }) + + describe('decrypt', function() { + it('should sucessfully return decrypted data', function() { + plaintext = sessions.decrypt(secret, encr, iv, authTag) + assert.equal(message, plaintext) + }) + }) + + describe('serialize', function() { + it('should return a string', function() { + var result = sessions.serialize(secret, user_data, 1540678757323, false) + var arr = result.split('$') + var cipher = arr[0] + var iv = arr[1] + var authTag = arr[2] + assert.ok(cipher) + assert.ok(iv) + assert.ok(authTag) + }) + + it('should return error on long data', function() { + var long = ''; + for(var i = 0; i < 1500; i++){ + long += 'a'; + }; + this.sandbox.stub(sessions, 'encrypt').returns({ciphertext: long, iv: long, authTag: long}); + try { + var cookiestr = sessions.serialize(secret, user_data) + } + catch(err) { + assert.equal(err.message, 'Data too long to store in a cookie') + }; + }) + }) + + describe('deserialize', function() { + it('should return user data in JSON if cookie has not expired', function() { + var cookiestr = sessions.serialize(secret, user_data, Date.now(), false) + var jsonData = sessions.deserialize(secret, cookiestr, 1000 * 60 * 60 * 12) + assert.deepEqual(jsonData.data, user_data) + }) + + it('should return validation error if cookie has expired', function() { + var str = '3d11d4581d2359247a4183fc3c84aceaa5835d38075117b10c1f8bcff4a63f0d3740f6ff30f60942b9851af38fd8f1d5bcf6b2376c6803f001a844dc71b5d50276cbb735f501c47d320ef6c6d07cd2047b7a6b26521c23687718ebf248bd9b$69d2b05d79b47c3e5a94b0f32f2fbe3b$ed4c06a57df5a2b646f7e5f82bffb17f' + try { + var jsonData = sessions.deserialize(secret, str, 1000 * 60 * 60 * 10) + } + catch(err) { + assert.ok(err) + assert.equal(err.message, 'Cookie expired') + } + }) + }) + + describe('onInit', function() { + it('throw error if no secret set in server settings', function() { + try { + sessions({}) + } catch(err) { + assert.ok(err) + assert.equal(err.message, 'No secret set in cookie-session settings') + } + }); + it('throw error for invalid path', function() { + try { + sessions({secret: secret, path: 'foo/bar'}) + } catch(err) { + assert.ok(err) + assert.equal(err.message, 'Invalid cookie path, must start with "/"') + } + }); + it('should do nothing for a request not under the specified path', (done) => { + const session = sessions({path: '/foo/bar', secret: secret }) + req = { path: '/foo/baz' } + res = {} + next = function() { + assert.ok(!req.session) + done() + } + session(req, res, next) + }); + it('throw error for invalid value in sameSite option', function() { + try { + sessions({secret: secret, sameSite: true}) + } catch(err) { + assert.ok(err) + assert.equal(err.message, 'Possible values for sameSite option: "lax" or "strict"') + } + }); + }) + + describe('the middleware', function() { + beforeEach(function () { + this.app = express(); + }); + + describe('when session has already data', function() { + beforeEach(function() { + this.cookieTime = Date.now() + this.sandbox.stub(sessions, 'deserialize').returns({data: {username: 'foobar'}, time: this.cookieTime}); + }); + + describe('and new data are added to req.session', function() { + describe('and autoRenew is set to true', function() { + it('should set a new cookie with the combined data and with new timestamp', function () { + this.app.use(cookieParser()) + this.app.use(sessions({secret: secret, autoRenew: true})) + this.app.get('/', (req, res) => { + req.session.koko = 'bar' + res.send('Hello World!') + }) + return request(this.app) + .get('/') + .then(res => { + this.sandbox.restore(); + assert(res.headers['set-cookie']) + _node = res.headers['set-cookie'][0].split(';')[0].split('=')[1] + var jsonData = sessions.deserialize(secret, decodeURIComponent(_node), 1000 * 60 * 60 * 24) + assert.notEqual(jsonData.time, this.cookieTime) + assert.deepEqual(jsonData.data, { username: 'foobar', koko: 'bar' }) + }) + }); + }); + describe('and autoRenew is set to false', function() { + it('should set a new cookie with the combined data and the initial timestamp', function () { + this.app.use(cookieParser()) + this.app.use(sessions({secret: secret, autoRenew: false})) + this.app.get('/', (req, res) => { + req.session.koko = 'bar' + res.send('Hello World!') + }) + return request(this.app) + .get('/') + .then(res => { + this.sandbox.restore(); + assert(res.headers['set-cookie']) + var _node = res.headers['set-cookie'][0].split(';')[0].split('=')[1] + var jsonData = sessions.deserialize(secret, decodeURIComponent(_node), 1000 * 60 * 60 * 24) + assert.equal(jsonData.time, this.cookieTime) + assert.deepEqual(jsonData.data, { username: 'foobar', koko: 'bar' }) + }) + }); + }); + }); + describe('and no data are added to req.session', function() { + describe('and autoRenew is set to true', function() { + it('should set a new cookie with new timestamp', function () { + this.app.use(cookieParser()) + this.app.use(sessions({secret: secret, autoRenew: true})) + this.app.get('/', (req, res) => { + res.send('Hello World!') + }) + return request(this.app) + .get('/') + .then(res => { + this.sandbox.restore(); + assert(res.headers['set-cookie']) + var _node = res.headers['set-cookie'][0].split(';')[0].split('=')[1] + var jsonData = sessions.deserialize(secret, decodeURIComponent(_node), 1000 * 60 * 60 * 24) + assert.notEqual(jsonData.time, this.cookieTime) + assert.deepEqual(jsonData.data, { username: 'foobar'}) + }) + }); + }); + describe('and autoRenew is set to false', function() { + it('should not set a new cookie', function () { + this.app.use(cookieParser()) + this.app.use(sessions({secret: secret, autoRenew: false})) + this.app.get('/', (req, res) => { + res.send('Hello World!') + }) + return request(this.app) + .get('/') + .then(res => { + this.sandbox.restore(); + assert(!res.headers['set-cookie']) + }) + }); + }); + }); + describe('data get deleted from req.session', function() { + it('should set a new cookie with empty data', function () { + this.app.use(cookieParser()) + this.app.use(sessions({secret: secret})) + this.app.get('/', (req, res) => { + req.session = {} + res.send('Hello World!') + }) + this.sandbox.restore(); + var cookiestr = sessions.serialize(secret, user_data, Date.now(), false) + return request(this.app) + .get('/') + .set('Cookie', '_node=' + encodeURIComponent(cookiestr)) + .then(res => { + assert(res.headers['set-cookie']) + var _node = res.headers['set-cookie'][0].split(';')[0].split('=')[1] + var jsonData = sessions.deserialize(secret, decodeURIComponent(_node), 1000 * 60 * 60 * 24) + assert.deepEqual(jsonData.data, {}) + }) + }); + }); + }); + describe('when session is empty', function() { + describe('and new data are added to req.session', function() { + it('should set a new cookie with the new data and with a timestamp', function () { + this.sandbox.stub(sessions, 'deserialize').returns({data: {}}); + this.app.use(cookieParser()) + this.app.use(sessions({secret: secret})) + this.app.get('/', (req, res) => { + req.session.koko = 'bar' + res.send('Hello World!') + }) + return request(this.app) + .get('/') + .then(res => { + this.sandbox.restore() + assert(res.headers['set-cookie']) + var _node = res.headers['set-cookie'][0].split(';')[0].split('=')[1] + var jsonData = sessions.deserialize(secret, decodeURIComponent(_node), 1000 * 60 * 60 * 24) + assert(jsonData.time) + assert.deepEqual(jsonData.data, { koko: 'bar' }) + }) + }); + }); + }); + }) +}) diff --git a/test/test-cookie-sessions.js b/test/test-cookie-sessions.js deleted file mode 100644 index e05026c..0000000 --- a/test/test-cookie-sessions.js +++ /dev/null @@ -1,667 +0,0 @@ -var sessions = require('cookie-sessions'); - - -exports['split'] = function(test){ - var hmac_sig = 'c82d1eacb4adb15a3250a6df7c8f190b586ab6b9', - timestamp = 1264710287440, - data_blob = 'somedata'; - - var serialized_cookie = hmac_sig + timestamp + data_blob; - test.same( - sessions.split(serialized_cookie), - {hmac_signature: hmac_sig, timestamp: timestamp, data_blob: data_blob}, - 'split correctly seperates sig, timestamp and data blob' - ); - test.done(); -}; - -exports['valid'] = function(test){ - var secret = 'secret'; - current_valid_sig = '5eaaa22480acefd8b18d67bb194573dc1b75d9db', - expired_valid_sig = '9c7ad86913ceeced1f6f249ba52868006c8dfdab', - invalid_sig = '51a2a32485a6e7d8b9810711112513d14b15d16b', - expired_timestamp = 1264700000000, - current_timestamp = 1264710287440, - session_timeout = 54000, - data_blob = 'somedata'; - - var Date_copy = global.Date; - global.Date = function(){this.getTime = function(){return 1264710287000}}; - - test.ok( - sessions.valid( - secret, session_timeout, - current_valid_sig + current_timestamp + data_blob - ) === true, - 'returns true for valid hmac sig within timeout' - ); - test.ok( - sessions.valid( - secret, session_timeout, - expired_valid_sig + expired_timestamp + data_blob - ) === false, - 'returns false for valid hmac sig past timeout' - ); - test.ok( - sessions.valid( - secret, session_timeout, - invalid_sig + current_timestamp + data_blob - ) === false, - 'returns false for invalid hmac sig within timeout' - ); - test.ok( - sessions.valid( - secret, session_timeout, - invalid_sig + expired_timestamp + data_blob - ) === false, - 'returns false for invalid hmac sig past timeout' - ); - - // restore Date - global.Date = Date_copy; - test.done(); -}; - -exports['decrypt'] = function(test){ - var r = sessions.decrypt( - 'secret', '686734eb9e0fff9adea53983210825ef' - ); - test.same(r, 'somedata', 'decrypt sucessfully returns decrypted data'); - test.done(); -}; - -exports['encrypt'] = function(test){ - var r = sessions.encrypt('secret', 'somedata'); - test.same( - r, '686734eb9e0fff9adea53983210825ef', - 'encrypt sucessfully returns encrypted data' - ); - test.done(); -}; - -exports['deserialize valid cookie'] = function(test){ - test.expect(8); - // copy some functions - var valid = sessions.valid; - var decrypt = sessions.decrypt; - var parse = JSON.parse; - - sessions.valid = function(secret, timeout, str){ - test.equals(secret, 'secret', 'valid called with secret'); - test.equals(timeout, 123, 'valid called with timeout'); - test.equals(str, 'cookiestring', 'valid called with cookie string'); - return true; - }; - sessions.split = function(str){ - test.equals(str, 'cookiestring', 'split called with cookie string'); - return {data_blob: 'datastr'}; - }; - sessions.decrypt = function(secret, str){ - test.equals(secret, 'secret', 'decrypt called with secret'); - test.equals(str, 'datastr', 'decrypt called with data string'); - return 'decrypted_data'; - }; - JSON.parse = function(str){ - test.equals( - str, 'decrypted_data', 'JSON.parse called with decrypted data' - ); - return {test:'test'}; - }; - var r = sessions.deserialize('secret', 123, 'cookiestring'); - test.same(r, {test:'test'}, 'deserialize returns parsed json data'); - - // restore copied functions: - sessions.valid = valid; - sessions.decrypt = decrypt; - JSON.parse = parse; - test.done(); -}; - -exports['deserialize invalid cookie'] = function(test){ - test.expect(1); - // copy some functions - var valid = sessions.valid; - var decrypt = sessions.decrypt; - var parse = JSON.parse; - - sessions.valid = function(secret, timeout, str){ - return false; - }; - sessions.decrypt = function(secret, str){ - test.ok(false, 'should not attempt to decrypt invalid cookie'); - }; - JSON.parse = function(str){ - test.ok(false, 'should not attempt to parse invalid cookie'); - }; - try { - sessions.deserialize('secret', 123, 'cookiestring'); - } - catch(e){ - test.ok(true, 'throw exception on invalid cookie'); - } - - // restore copied functions: - sessions.valid = valid; - sessions.decrypt = decrypt; - JSON.parse = parse; - test.done(); -}; - -exports['serialize'] = function(test){ - test.expect(7); - // copy some functions - var encrypt = sessions.encrypt; - var hmac_signature = sessions.hmac_signature; - var stringify= JSON.stringify; - var Date_copy = global.Date; - - global.Date = function(){this.getTime = function(){return 1234;}}; - JSON.stringify = function(obj){ - test.same( - obj, {test:'test'}, 'JSON.stringify called with cookie data obj' - ); - return 'data'; - }; - sessions.encrypt = function(secret, str){ - test.equals(secret, 'secret', 'encrypt called with secret'); - test.equals(str, 'data', 'encrypt called with stringified data'); - return 'encrypted_data'; - }; - sessions.hmac_signature = function(secret, timestamp, data_str){ - test.equals(secret, 'secret', 'hmac_signature called with secret'); - test.equals(timestamp, 1234, 'hmac_signature called with timestamp'); - test.equals( - data_str, 'encrypted_data', - 'hmac_signature called with encrypted data string' - ); - return 'hmac'; - }; - var r = sessions.serialize('secret', {test:'test'}); - test.equals( - r, 'hmac1234encrypted_data', 'serialize returns correct string' - ); - - // restore copied functions: - sessions.encrypt = encrypt; - sessions.hmac_signature = hmac_signature; - JSON.stringify = stringify; - global.Date = Date_copy; - test.done(); -}; - -exports['serialize data over 4096 chars'] = function(test){ - test.expect(1); - // copy some functions - var encrypt = sessions.encrypt; - var hmac_signature = sessions.hmac_signature; - var stringify= JSON.stringify; - var Date_copy = global.Date; - - global.Date = function(){this.getTime = function(){return 1234;}}; - JSON.stringify = function(obj){ - return 'data'; - }; - sessions.encrypt = function(secret, str){ - // lets make this too long! - var r = ''; - for(var i=0; i<4089; i++){ - r = r + 'x'; - }; - return r; - }; - sessions.hmac_signature = function(secret, timestamp, data_str){ - return 'hmac'; - }; - try { - var r = sessions.serialize('secret', {test:'test'}); - } - catch(e){ - test.ok( - true, 'serializing a cookie over 4096 chars throws an exception' - ); - } - - // restore copied functions: - sessions.encrypt = encrypt; - sessions.hmac_signature = hmac_signature; - JSON.stringify = stringify; - global.Date = Date_copy; - test.done(); -}; - -exports['readCookies'] = function(test){ - var req = { - headers: {cookie: "name1=data1; test=\"abcXYZ%20123\""}, - url: '/' - }; - var r = sessions.readCookies(req); - test.same(r, {name1: 'data1', test: '"abcXYZ 123"'}, 'test header read ok'); - test.done(); -}; - -exports['readCookies alternate format'] = function(test){ - var req = { - headers: {cookie: "name1=data1;test=\"abcXYZ%20123\""}, - url: '/' - }; - var r = sessions.readCookies(req); - test.same(r, {name1: 'data1', test: '"abcXYZ 123"'}, 'test header read ok'); - test.done(); -}; - -exports['readCookies no cookie in headers'] = function(test){ - var req = {headers: {}, url: '/'}; - var r = sessions.readCookies(req); - test.same(r, {}, 'returns empty object'); - test.done(); -}; - -exports['readCookies from Connect cookieDecoder'] = function(test){ - var req = {headers: {}, cookies: {'test':'cookie'}, url: '/'}; - test.same(sessions.readCookies(req), {'test': 'cookie'}); - test.done(); -}; - -exports['readSession'] = function(test){ - test.expect(5); - var readCookies = sessions.readCookies; - var deserialize = sessions.deserialize; - - sessions.readCookies = function(r){ - test.equals(r, 'request_obj', 'readCookies called with request object'); - return {'node_session': 'cookie_data'}; - }; - sessions.deserialize = function(secret, timeout, str){ - test.equals(secret, 'secret', 'readCookies called with secret'); - test.equals(timeout, 12, 'readCookies called with timeout'); - test.equals(str, 'cookie_data', 'readCookies called with cookie data'); - return {test: 'test'}; - }; - - var r = sessions.readSession( - 'node_session', 'secret', 12, 'request_obj' - ); - test.same(r, {test: 'test'}, 'session with key node_session read ok'); - - // restore copied functions - sessions.readCookies = readCookies; - sessions.deserialize = deserialize; - test.done(); -}; - -exports['readSession no cookie'] = function(test){ - test.expect(2); - var readCookies = sessions.readCookies; - var deserialize = sessions.deserialize; - - sessions.readCookies = function(r){ - test.equals(r, 'request_obj', 'readCookies called with request object'); - return {}; - }; - sessions.deserialize = function(secret, timeout, str){ - test.ok(false, 'should not call deserialize'); - }; - - var r = sessions.readSession( - 'node_session', 'secret', 12, 'request_obj' - ); - test.same(r, undefined, 'return empty session'); - - // restore copied functions - sessions.readCookies = readCookies; - sessions.deserialize = deserialize; - test.done(); -}; - -exports['onRequest'] = function(test){ - test.expect(5); - var readSession = sessions.readSession; - var s = { - session_key:'_node', - secret: 'secret', - timeout: 86400 - }; - var req = {url: '/'}; - - sessions.readSession = function(key, secret, timeout, req){ - test.equals(key, '_node', 'readSession called with session key'); - test.equals(secret, 'secret', 'readSession called with secret'); - test.equals(timeout, 86400, 'readSession called with timeout'); - return 'testsession'; - }; - var next = function(){ - test.ok(true, 'chain.next called'); - test.equals( - req.session, 'testsession', 'req.session equals session data' - ); - }; - sessions(s)(req, 'res', next); - - // restore copied functions - sessions.readSession = readSession; - test.done(); -}; - -exports['writeHead'] = function(test){ - test.expect(6); - - var s = { - session_key:'_node', - secret: 'secret', - timeout: 86400, - domain:'.domain.com' - }; - var req = {headers: {cookie: "_node="}, url: '/'}; - var res = { - writeHead: function(code, headers){ - test.equals( - headers['Set-Cookie'], - '_node=serialized_session; ' + - 'expires=expiry_date; ' + - 'path=/; HttpOnly; Domain=.domain.com' - ); - test.equals(headers['original'], 'header'); - } - }; - - var serialize = sessions.serialize; - sessions.serialize = function(secret, data){ - test.equals(secret, 'secret', 'serialize called with secret'); - test.same(data, {test:'test'}, 'serialize called with session data'); - return 'serialized_session'; - }; - - var expires = sessions.expires; - sessions.expires = function(timeout){ - test.equals(timeout, s.timeout); - return 'expiry_date'; - }; - - var next = function(){ - test.ok(true, 'chain.next called'); - req.session = {test:'test'}; - res.writeHead(200, {'original':'header'}); - // restore copied functions - sessions.serialize = serialize; - sessions.expires = expires; - test.done(); - }; - sessions(s)(req, res, next); -}; - -exports['writeHead doesnt write cookie if none exists and session is undefined'] = function(test){ - test.expect(3); - - var s = { - session_key:'_node', - secret: 'secret', - timeout: 86400 - }; - var req = {headers: {}, url: '/'}; - var res = { - writeHead: function(code, headers){ - test.ok(!("Set-Cookie" in headers)); - test.equals(headers['original'], 'header'); - } - }; - - var next = function(){ - test.ok(true, 'chain.next called'); - req.session = undefined; - res.writeHead(200, {'original':'header'}); - test.done(); - }; - sessions(s)(req, res, next); -}; - -exports['writeHead writes empty cookie with immediate expiration if session is undefined'] = function(test){ - test.expect(4); - - var s = { - session_key:'_node', - secret: 'secret', - timeout: 86400 - }; - var req = {headers: {cookie: "_node=Blah"}, url: '/'}; - var res = { - writeHead: function(code, headers){ - test.equals( - headers['Set-Cookie'], - '_node=; ' + - 'expires=now; ' + - 'path=/; HttpOnly' - ); - test.equals(headers['original'], 'header'); - } - }; - - var expires = sessions.expires; - sessions.expires = function(timeout){ - test.equals(timeout, 0); - return 'now'; - }; - var readSession = sessions.readSession; - sessions.readSession = function(key, secret, timeout, req) { - return {"username": "Bob"}; - }; - - var next = function(){ - test.ok(true, 'chain.next called'); - req.session = undefined; - res.writeHead(200, {'original':'header'}); - // restore copied functions - sessions.expires = expires; - sessions.readSession = readSession; - test.done(); - }; - sessions(s)(req, res, next); -}; - -exports['onInit secret set'] = function(test){ - test.expect(0); - var s = {secret: 'secret'}; - try { - sessions({secret: 'secret'}); - } - catch(e){ - test.ok(false, 'do nothing if secret set in server settings'); - } - test.done(); -}; - -exports['onInit no secret set'] = function(test){ - test.expect(1); - try { - sessions({}); - } - catch(e){ - test.ok(true, 'throw exception if no secret set in server settings'); - } - test.done(); -}; - -exports['set multiple cookies'] = function(test){ - test.expect(3); - var _serialize = sessions.serialize; - sessions.serialize = function(){ - return 'session_data'; - }; - - var _expires = sessions.expires; - sessions.expires = function(timeout){ - test.equals(timeout, 12345); - return 'expiry_date'; - }; - - var req = {headers: {cookie:''}, url: '/'}; - var res = {writeHead: function(statusCode, headers){ - test.equals(statusCode, 200); - test.same(headers, [ - ['other_header', 'val'], - ['Set-Cookie', 'testcookie=testvalue'], - ['Set-Cookie', '_node=session_data; ' + - 'expires=expiry_date; ' + - 'path=/; HttpOnly'] - ]); - sessions.serialize = _serialize; - sessions.expires = _expires; - test.done(); - }}; - - sessions({secret: 'secret', timeout: 12345})(req, res, function(){ - req.session = {test: 'test'}; - res.writeHead(200, { - 'other_header': 'val', - 'Set-Cookie':'testcookie=testvalue' - }); - }); -}; - -exports['set single cookie'] = function(test){ - test.expect(3); - var _serialize = sessions.serialize; - sessions.serialize = function(){ - return 'session_data'; - }; - - var _expires = sessions.expires; - sessions.expires = function(timeout){ - test.equals(timeout, 12345); - return 'expiry_date'; - }; - - var req = {headers: {cookie:''}, url: '/'}; - var res = {writeHead: function(statusCode, headers){ - test.equals(statusCode, 200); - test.same(headers, { - 'other_header': 'val', - 'Set-Cookie': '_node=session_data; ' + - 'expires=expiry_date; ' + - 'path=/; HttpOnly' - }); - sessions.serialize = _serialize; - sessions.expires = _expires; - test.done(); - }}; - sessions({secret: 'secret', timeout: 12345})(req, res, function(){ - req.session = {test: 'test'}; - res.writeHead(200, {'other_header': 'val'}); - }); -}; - -exports['handle headers as array'] = function(test){ - test.expect(3); - var _serialize = sessions.serialize; - sessions.serialize = function(){ - return 'session_data'; - }; - - var _expires = sessions.expires; - sessions.expires = function(timeout){ - test.equals(timeout, 12345); - return 'expiry_date'; - }; - - var req = {headers: {cookie:''}, url: '/'}; - var res = {writeHead: function(statusCode, headers){ - test.equals(statusCode, 200); - test.same(headers, [ - ['header1', 'val1'], - ['header2', 'val2'], - ['Set-Cookie', '_node=session_data; ' + - 'expires=expiry_date; ' + - 'path=/; HttpOnly'] - ]); - sessions.serialize = _serialize; - test.done(); - }}; - sessions({secret: 'secret', timeout: 12345})(req, res, function(){ - req.session = {test: 'test'}; - res.writeHead(200, [['header1', 'val1'],['header2', 'val2']]); - }); -}; - -exports['convert headers to array'] = function(test){ - test.same( - sessions.headersToArray({'key1':'val1','key2':'val2'}), - [['key1','val1'],['key2','val2']] - ); - test.same( - sessions.headersToArray([['key1','val1'],['key2','val2']]), - [['key1','val1'],['key2','val2']] - ); - test.done(); -}; - -exports['send cookies even if there are no headers'] = function (test) { - test.expect(2); - var req = {headers: {cookie:''}, url: '/'}; - var res = { - writeHead: function (code, headers) { - test.equal(code, 200); - test.ok(headers['Set-Cookie']); - test.done(); - } - }; - sessions({secret: 'secret', timeout: 12345})(req, res, function () { - req.session = {test: 'test'}; - res.writeHead(200); - }); -}; - -exports['send cookies when no headers but reason_phrase'] = function (test) { - test.expect(3); - var req = {headers: {cookie:''}, url: '/'}; - var res = { - writeHead: function (code, reason_phrase, headers) { - test.equal(code, 200); - test.equal(reason_phrase, 'reason'); - test.ok(headers['Set-Cookie']); - test.done(); - } - }; - sessions({secret: 'secret', timeout: 12345})(req, res, function () { - req.session = {test: 'test'}; - res.writeHead(200, 'reason'); - }); -}; - -exports['custom path'] = function (test) { - test.expect(2); - var req = {headers: {cookie:''}, url: '/test/path'}; - var res = { - writeHead: function (code, headers) { - test.equal(code, 200); - test.ok(/path=\/test\/path/.test(headers['Set-Cookie'])); - test.done(); - } - }; - sessions({ - secret: 'secret', - timeout: 12345, - path: '/test/path' - })(req, res, function () { - req.session = {test: 'test'}; - res.writeHead(200, {'other_header': 'val'}); - }); -}; - -exports['don\'t set cookie if incorrect path'] = function (test) { - test.expect(2); - var req = {headers: {cookie:''}, url: '/other/path'}; - var res = { - writeHead: function (code, headers) { - test.equal(code, 200); - test.equal(headers['Set-Cookie'], undefined); - test.done(); - } - }; - sessions({ - secret: 'secret', - timeout: 12345, - path: '/test/path' - })(req, res, function () { - req.session = {test: 'test'}; - res.writeHead(200, {'other_header': 'val'}); - }); -};