From 17b32801145af597f29c0bc367d460129c80d233 Mon Sep 17 00:00:00 2001 From: Dominic Egginton Date: Sat, 18 Apr 2020 14:11:18 +0100 Subject: [PATCH 01/25] add secure flag to readme exmaple (#196) --- Readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Readme.md b/Readme.md index 844a32b..80b60ba 100644 --- a/Readme.md +++ b/Readme.md @@ -59,6 +59,7 @@ const CONFIG = { signed: true, /** (boolean) signed or not (default true) */ rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. (default is false) */ renew: false, /** (boolean) renew session when session is nearly expired, so we can always keep user logged in. (default is false)*/ + secure: true, /** (boolean) secure cookie*/ sameSite: null, /** (string) session cookie sameSite options (default null, don't set it) */ }; From 23f7f52a9bd1e21094d7cfa42fc7b9178be546d4 Mon Sep 17 00:00:00 2001 From: Dominic Egginton Date: Fri, 24 Apr 2020 05:11:33 +0100 Subject: [PATCH 02/25] chore: disabled package-lock.json (#198) --- .npmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9cf9495 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file From 7fca3deda6cb78524992adfd1f520a6e664d52b0 Mon Sep 17 00:00:00 2001 From: Zac Anger Date: Tue, 28 Apr 2020 16:15:54 +0000 Subject: [PATCH 03/25] [BREAKING CHANGE] fix: RFC6265 default cookie name (#197) --- History.md | 6 + Readme.md | 4 +- index.js | 4 +- package-lock.json | 2076 +++++++++++++++++++++++++++++++++++++ test/contextstore.test.js | 40 +- test/cookie.test.js | 40 +- test/store.test.js | 32 +- 7 files changed, 2142 insertions(+), 60 deletions(-) create mode 100644 package-lock.json diff --git a/History.md b/History.md index 37f76a6..40a77ce 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,9 @@ +6.0.0 / 2020-04-26 +================== + +**fixes** + * [[`d34fc8e`](https://github.com/koajs/session/commit/d34fc8e0395bd3dc0c8cceda4374039a4d414060)] - fix: RFC6265 compliant default cookie name (#197) (zacanger <>) + * [BREAKING CHANGE]: Default cookie is now `koa.sess` rather than `koa:sess` 5.13.1 / 2020-02-01 ================== diff --git a/Readme.md b/Readme.md index 80b60ba..fb5664f 100644 --- a/Readme.md +++ b/Readme.md @@ -48,7 +48,7 @@ const app = new Koa(); app.keys = ['some secret hurr']; const CONFIG = { - key: 'koa:sess', /** (string) cookie key (default is koa:sess) */ + key: 'koa.sess', /** (string) cookie key (default is koa.sess) */ /** (number || 'session') maxAge in ms (default is 1 days) */ /** 'session' will result in a cookie that expires when session/browser is closed */ /** Warning: If a session cookie is stolen, this cookie will never expire */ @@ -84,7 +84,7 @@ console.log('listening on port 3000'); ### Options The cookie name is controlled by the `key` option, which defaults - to "koa:sess". All other options are passed to `ctx.cookies.get()` and + to "koa.sess". All other options are passed to `ctx.cookies.get()` and `ctx.cookies.set()` allowing you to control security, domain, path, and signing among other settings. diff --git a/index.js b/index.js index e4f3052..816a15a 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,7 @@ const _CONTEXT_SESSION = Symbol('context#_contextSession'); /** * Initialize session middleware with `opts`: * - * - `key` session cookie name ["koa:sess"] + * - `key` session cookie name ["koa.sess"] * - all other options are passed as cookie options * * @param {Object} [opts] @@ -60,7 +60,7 @@ module.exports = function(opts, app) { function formatOpts(opts) { opts = opts || {}; // key - opts.key = opts.key || 'koa:sess'; + opts.key = opts.key || 'koa.sess'; // back-compat maxage if (!('maxAge' in opts)) opts.maxAge = opts.maxage; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d60aff9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2076 @@ +{ + "name": "koa-session", + "version": "5.13.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "requires": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true, + "optional": true + }, + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "benchmark": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", + "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", + "dev": true, + "requires": { + "lodash": "^4.17.4", + "platform": "^1.3.3" + } + }, + "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 + }, + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "dev": true, + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "^0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "optional": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "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 + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "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 + }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, + "cookies": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", + "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "dev": true, + "requires": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "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=" + }, + "crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "requires": { + "buffer": "^5.1.0" + } + }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dev": true, + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "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 + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "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 + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "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 + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "error-inject": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", + "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=", + "dev": true + }, + "es5-ext": { + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "dev": true, + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "~0.3.5" + }, + "dependencies": { + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + } + } + }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dev": true, + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "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 + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + }, + "dependencies": { + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + } + } + }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true, + "requires": { + "es6-map": "^0.1.3", + "es6-weak-map": "^2.0.1", + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", + "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", + "dev": true, + "requires": { + "babel-code-frame": "^6.16.0", + "chalk": "^1.1.3", + "concat-stream": "^1.5.2", + "debug": "^2.1.1", + "doctrine": "^2.0.0", + "escope": "^3.6.0", + "espree": "^3.4.0", + "esquery": "^1.0.0", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "glob": "^7.0.3", + "globals": "^9.14.0", + "ignore": "^3.2.0", + "imurmurhash": "^0.1.4", + "inquirer": "^0.12.0", + "is-my-json-valid": "^2.10.0", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.5.1", + "json-stable-stringify": "^1.0.0", + "levn": "^0.3.0", + "lodash": "^4.0.0", + "mkdirp": "^0.5.0", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.1", + "pluralize": "^1.2.1", + "progress": "^1.1.8", + "require-uncached": "^1.0.2", + "shelljs": "^0.7.5", + "strip-bom": "^3.0.0", + "strip-json-comments": "~2.0.1", + "table": "^3.7.8", + "text-table": "~0.2.0", + "user-home": "^2.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" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-config-egg": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-config-egg/-/eslint-config-egg-3.2.0.tgz", + "integrity": "sha1-eb/dndaNh21oIHQSuQUymgfUL4g=", + "dev": true + }, + "espree": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "dev": true, + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", + "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "dev": true, + "requires": { + "type": "^2.0.0" + }, + "dependencies": { + "type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz", + "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==", + "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 + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "flat-cache": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", + "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "graceful-fs": "^4.1.2", + "rimraf": "~2.6.2", + "write": "^0.2.1" + } + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", + "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 + }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "requires": { + "is-property": "^1.0.2" + } + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "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" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "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 + }, + "handlebars": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "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-assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz", + "integrity": "sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==", + "dev": true, + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.7.2" + } + }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "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.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "inquirer": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", + "dev": true, + "requires": { + "ansi-escapes": "^1.1.0", + "ansi-regex": "^2.0.0", + "chalk": "^1.0.0", + "cli-cursor": "^1.0.1", + "cli-width": "^2.0.0", + "figures": "^1.3.5", + "lodash": "^4.3.0", + "readline2": "^1.0.1", + "run-async": "^0.1.0", + "rx-lite": "^3.1.2", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" + } + }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "dev": true + }, + "is-class-hotfix": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/is-class-hotfix/-/is-class-hotfix-0.0.6.tgz", + "integrity": "sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ==" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==", + "dev": true + }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", + "dev": true + }, + "is-my-json-valid": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.0.tgz", + "integrity": "sha512-XTHBZSIIxNsIsZXg7XB5l8z/OBFosl1Wao4tXLpeC7eKU4Vm/kdop2azkPqULwnfGQjmeDIyey9g7afMMtdWAA==", + "dev": true, + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^4.0.0", + "xtend": "^4.0.0" + } + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-type-of": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-type-of/-/is-type-of-1.2.1.tgz", + "integrity": "sha512-uK0kyX9LZYhSDS7H2sVJQJop1UnWPWmo5RvR3q2kFH6AUHYs7sOrVg0b4nyBHw29kRRNFofYN/JbHZDlHiItTA==", + "requires": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dev": true, + "requires": { + "tsscmp": "1.0.6" + } + }, + "ko-sleep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ko-sleep/-/ko-sleep-1.0.3.tgz", + "integrity": "sha1-KKKgoUhei39BX/SI3uF9JHiKsII=", + "dev": true, + "requires": { + "ms": "^2.0.0" + } + }, + "koa": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.11.0.tgz", + "integrity": "sha512-EpR9dElBTDlaDgyhDMiLkXrPwp6ZqgAIBvhhmxQ9XN4TFgW+gEz6tkcsNI6BnUbUftrKDjVFj4lW2/J2aNBMMA==", + "dev": true, + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.8.0", + "debug": "~3.1.0", + "delegates": "^1.0.0", + "depd": "^1.1.2", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "error-inject": "^1.0.0", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^1.2.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "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" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "dev": true + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "dev": true, + "requires": { + "co": "^4.6.0", + "koa-compose": "^3.0.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "dev": true, + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "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.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "dev": true, + "requires": { + "mime-db": "1.43.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" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "mm": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/mm/-/mm-2.5.0.tgz", + "integrity": "sha512-ilm+lGEBNm7Cw45um9ax0tbApiNwQV3PY6Yk1ol+wtA8c98hHuJqTgmdKB6rYQJTUC2QrhBfoWwN+/766ZlrYA==", + "dev": true, + "requires": { + "is-type-of": "^1.0.0", + "ko-sleep": "^1.0.2", + "muk-prop": "^1.0.0", + "thenify": "^3.2.1" + } + }, + "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": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "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" + } + }, + "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" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "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" + } + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "muk-prop": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/muk-prop/-/muk-prop-1.2.1.tgz", + "integrity": "sha512-NdkOVav3GoIkBZqMUneU435HW0a90zitpuO1erPRhOQdPtl65dXD3G9/1k46G6/0ZMau4CJFFUHkMKVsyNZT+w==", + "dev": true + }, + "mute-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", + "dev": true + }, + "mz-modules": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mz-modules/-/mz-modules-2.1.0.tgz", + "integrity": "sha512-sjk8lcRW3vrVYnZ+W+67L/2rL+jbO5K/N6PFGIcLWTiYytNr22Ah9FDXFs+AQntTM1boZcoHi5qS+CV1seuPog==", + "dev": true, + "requires": { + "glob": "^7.1.2", + "ko-sleep": "^1.0.3", + "mkdirp": "^0.5.1", + "pump": "^3.0.0", + "rimraf": "^2.6.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "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" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=", + "dev": true + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "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-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "pedding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pedding/-/pedding-1.1.0.tgz", + "integrity": "sha1-97E4wojUvVhOraEhX1vZJPHh5mc=", + "dev": true + }, + "platform": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", + "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==", + "dev": true + }, + "pluralize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "qs": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", + "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", + "dev": true + }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "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" + } + }, + "readline2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "mute-stream": "0.0.5" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + } + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-async": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "dev": true, + "requires": { + "once": "^1.3.0" + } + }, + "rx-lite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", + "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 + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "shelljs": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", + "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "should": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/should/-/should-8.4.0.tgz", + "integrity": "sha1-XmCInT5kS73Tl6MM00+tKPz5C8A=", + "dev": true, + "requires": { + "should-equal": "0.8.0", + "should-format": "0.3.2", + "should-type": "0.2.0" + } + }, + "should-equal": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-0.8.0.tgz", + "integrity": "sha1-o/BXMv9FusG3ukEvhAiFaBlkEpk=", + "dev": true, + "requires": { + "should-type": "0.2.0" + } + }, + "should-format": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-0.3.2.tgz", + "integrity": "sha1-pZgx4Bot3uFJkRvHFIvlyAMZ4f8=", + "dev": true, + "requires": { + "should-type": "0.2.0" + } + }, + "should-type": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-0.2.0.tgz", + "integrity": "sha1-ZwfvlVKdmJ3MCY/gdTqx+RNrt/Y=", + "dev": true + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "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" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "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.4.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.4.2.tgz", + "integrity": "sha512-WZWbwceHUo2P36RoEIdXvmqfs47idNNZjCuJOqDz6rvtkk8ym56aU5oglORCpPeXGxT7l9rkJ41+O1lffQXYSA==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^3.8.3" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "table": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "dev": true, + "requires": { + "ajv": "^4.7.0", + "ajv-keywords": "^1.0.0", + "chalk": "^1.1.1", + "lodash": "^4.0.0", + "slice-ansi": "0.0.4", + "string-width": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "thenify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", + "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", + "dev": true, + "requires": { + "any-promise": "^1.0.0" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "dev": true + }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uglify-js": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.1.tgz", + "integrity": "sha512-JUPoL1jHsc9fOjVFHdQIhqEEJsQvfKDjlubcCilu8U26uZ73qOg8VsN8O1jbuei44ZPlwL7kmbAdM4tzaUvqnA==", + "dev": true, + "optional": true, + "requires": { + "commander": "~2.20.3" + } + }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dev": true, + "requires": { + "random-bytes": "~1.0.0" + } + }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "ylru": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", + "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==", + "dev": true + } + } +} diff --git a/test/contextstore.test.js b/test/contextstore.test.js index 57cfb44..cf71eb5 100644 --- a/test/contextstore.test.js +++ b/test/contextstore.test.js @@ -87,7 +87,7 @@ describe('Koa Session External Context Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(200, (err, res) => { if (err) return done(err); cookie = res.header['set-cookie'].join(';'); @@ -107,7 +107,7 @@ describe('Koa Session External Context Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(200, (err, res) => { if (err) return done(err); cookie = res.header['set-cookie'].join(';'); @@ -200,7 +200,7 @@ describe('Koa Session External Context Store', () => { request(app.listen()) .get('/') .set('Cookie', cookie) - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(200, done); }); }); @@ -218,7 +218,7 @@ describe('Koa Session External Context Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(200, done); }); }); @@ -253,7 +253,7 @@ describe('Koa Session External Context Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(200, done); }); }); @@ -285,7 +285,7 @@ describe('Koa Session External Context Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess=.+;/) + .expect('Set-Cookie', /koa\.sess=.+;/) .expect({ foo: 'bar' }) .expect(200, done); }); @@ -302,7 +302,7 @@ describe('Koa Session External Context Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess=.+;/) + .expect('Set-Cookie', /koa\.sess=.+;/) .expect('1') .expect(200, done); }); @@ -319,7 +319,7 @@ describe('Koa Session External Context Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess=.+;/) + .expect('Set-Cookie', /koa\.sess=.+;/) .expect('true') .expect(200, done); }); @@ -336,7 +336,7 @@ describe('Koa Session External Context Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess=.+;/) + .expect('Set-Cookie', /koa\.sess=.+;/) .expect('hello') .expect(200, done); }); @@ -371,7 +371,7 @@ describe('Koa Session External Context Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(401, done); }); }); @@ -415,7 +415,7 @@ describe('Koa Session External Context Store', () => { request(server) .post('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .end(err => { if (err) return done(err); }) @@ -441,7 +441,7 @@ describe('Koa Session External Context Store', () => { request(server) .post('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .end((err, res) => { if (err) return done(err); const cookie = res.headers['set-cookie'].join(';'); @@ -463,7 +463,7 @@ describe('Koa Session External Context Store', () => { request(server) .get('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect('hi 1') .end((err, res) => { if (err) return done(err); @@ -473,7 +473,7 @@ describe('Koa Session External Context Store', () => { request(server) .get('/') .set('cookie', cookie) - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect('hi 2') .end((err, res) => { if (err) return done(err); @@ -483,7 +483,7 @@ describe('Koa Session External Context Store', () => { request(server) .get('/') .set('cookie', cookie) - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect('hi 3') .end((err, res) => { if (err) return done(err); @@ -510,7 +510,7 @@ describe('Koa Session External Context Store', () => { request(server) .post('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .end((err, res) => { if (err) return done(err); const cookie = res.headers['set-cookie'].join(';'); @@ -539,7 +539,7 @@ describe('Koa Session External Context Store', () => { request(server) .post('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .end((err, res) => { if (err) return done(err); const cookie = res.headers['set-cookie'].join(';'); @@ -570,7 +570,7 @@ describe('Koa Session External Context Store', () => { request(server) .post('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .end((err, res) => { if (err) return done(err); const cookie = res.headers['set-cookie'].join(';'); @@ -655,7 +655,7 @@ describe('Koa Session External Context Store', () => { request(app.listen()) .get('/') - .set('cookie', 'koa:sess=invalid-key') + .set('cookie', 'koa.sess=invalid-key') .expect('true') .expect(200, done); }); @@ -693,7 +693,7 @@ describe('Koa Session External Context Store', () => { should.not.exist(err); const data = res.body; const cookies = res.headers['set-cookie'].join(';'); - cookies.should.containEql('koa:sess='); + cookies.should.containEql('koa.sess='); request(app.callback()) .get('/') diff --git a/test/cookie.test.js b/test/cookie.test.js index cd71825..568d2b1 100644 --- a/test/cookie.test.js +++ b/test/cookie.test.js @@ -153,7 +153,7 @@ describe('Koa Session Cookie', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(200, (err, res) => { if (err) return done(err); cookie = res.header['set-cookie'].join(';'); @@ -245,7 +245,7 @@ describe('Koa Session Cookie', () => { request(app.listen()) .get('/') .set('Cookie', cookie) - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(res => { const cookie = res.headers['set-cookie']; // samesite is not set @@ -270,8 +270,8 @@ describe('Koa Session Cookie', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess=; path=\/; expires=Thu, 01 Jan 1970 00:00:00 GMT/) - .expect('Set-Cookie', /koa:sess.sig=(.*); path=\/; expires=Thu, 01 Jan 1970 00:00:00 GMT/) + .expect('Set-Cookie', /koa\.sess=; path=\/; expires=Thu, 01 Jan 1970 00:00:00 GMT/) + .expect('Set-Cookie', /koa\.sess.sig=(.*); path=\/; expires=Thu, 01 Jan 1970 00:00:00 GMT/) .expect('true') .expect(200, done); }); @@ -291,7 +291,7 @@ describe('Koa Session Cookie', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess=; path=\/; expires=Thu, 01 Jan 1970 00:00:00 GMT/) + .expect('Set-Cookie', /koa\.sess=; path=\/; expires=Thu, 01 Jan 1970 00:00:00 GMT/) .expect('true') .expect(200, done); }); @@ -309,7 +309,7 @@ describe('Koa Session Cookie', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess=;/) + .expect('Set-Cookie', /koa\.sess=;/) .expect('true') .expect(200, done); }); @@ -326,7 +326,7 @@ describe('Koa Session Cookie', () => { request(app.listen()) .get('/') - .set('cookie', 'koa:sess=invalid-session;') + .set('cookie', 'koa.sess=invalid-session;') .expect('true') .expect(200, done); }); @@ -347,8 +347,8 @@ describe('Koa Session Cookie', () => { request(app.listen()) .get('/') - .set('cookie', 'koa:sess=invalid-session;') - .expect('Set-Cookie', /koa:sess=;/) + .set('cookie', 'koa.sess=invalid-session;') + .expect('Set-Cookie', /koa\.sess=;/) .expect(500, done); }); }); @@ -390,7 +390,7 @@ describe('Koa Session Cookie', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess=.+;/) + .expect('Set-Cookie', /koa\.sess=.+;/) .expect({ foo: 'bar' }) .expect(200, done); }); @@ -407,7 +407,7 @@ describe('Koa Session Cookie', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess=.+;/) + .expect('Set-Cookie', /koa\.sess=.+;/) .expect('1') .expect(200, done); }); @@ -424,7 +424,7 @@ describe('Koa Session Cookie', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess=.+;/) + .expect('Set-Cookie', /koa\.sess=.+;/) .expect('true') .expect(200, done); }); @@ -441,7 +441,7 @@ describe('Koa Session Cookie', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess=.+;/) + .expect('Set-Cookie', /koa\.sess=.+;/) .expect('hello') .expect(200, done); }); @@ -460,7 +460,7 @@ describe('Koa Session Cookie', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(200, done); }); }); @@ -495,7 +495,7 @@ describe('Koa Session Cookie', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(200, done); }); }); @@ -543,7 +543,7 @@ describe('Koa Session Cookie', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(401, done); }); }); @@ -566,7 +566,7 @@ describe('Koa Session Cookie', () => { request(server) .post('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .end((err, res) => { if (err) return done(err); const cookie = res.headers['set-cookie'].join(';'); @@ -597,7 +597,7 @@ describe('Koa Session Cookie', () => { request(server) .post('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .end((err, res) => { if (err) return done(err); const cookie = res.headers['set-cookie'].join(';'); @@ -691,7 +691,7 @@ describe('Koa Session Cookie', () => { .expect(200, (err, res) => { should.not.exist(err); const cookies = res.headers['set-cookie'].join(';'); - cookies.should.containEql('koa:sess='); + cookies.should.containEql('koa.sess='); request(app.callback()) .get('/') @@ -770,7 +770,7 @@ describe('Koa Session Cookie', () => { should.not.exist(err); const data = res.body; const cookies = res.headers['set-cookie'].join(';'); - cookies.should.containEql('koa:sess='); + cookies.should.containEql('koa.sess='); request(app.callback()) .get('/') diff --git a/test/store.test.js b/test/store.test.js index f2f0765..d1bbf93 100644 --- a/test/store.test.js +++ b/test/store.test.js @@ -90,7 +90,7 @@ describe('Koa Session External Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(200, (err, res) => { if (err) return done(err); cookie = res.header['set-cookie'].join(';'); @@ -182,7 +182,7 @@ describe('Koa Session External Store', () => { request(app.listen()) .get('/') .set('Cookie', cookie) - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(200, done); }); }); @@ -200,7 +200,7 @@ describe('Koa Session External Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(200, done); }); }); @@ -235,7 +235,7 @@ describe('Koa Session External Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(200, done); }); }); @@ -267,7 +267,7 @@ describe('Koa Session External Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess=.+;/) + .expect('Set-Cookie', /koa\.sess=.+;/) .expect({ foo: 'bar' }) .expect(200, done); }); @@ -284,7 +284,7 @@ describe('Koa Session External Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess=.+;/) + .expect('Set-Cookie', /koa\.sess=.+;/) .expect('1') .expect(200, done); }); @@ -301,7 +301,7 @@ describe('Koa Session External Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess=.+;/) + .expect('Set-Cookie', /koa\.sess=.+;/) .expect('true') .expect(200, done); }); @@ -318,7 +318,7 @@ describe('Koa Session External Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess=.+;/) + .expect('Set-Cookie', /koa\.sess=.+;/) .expect('hello') .expect(200, done); }); @@ -353,7 +353,7 @@ describe('Koa Session External Store', () => { request(app.listen()) .get('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .expect(401, done); }); }); @@ -375,7 +375,7 @@ describe('Koa Session External Store', () => { request(server) .post('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .end((err, res) => { if (err) return done(err); const cookie = res.headers['set-cookie'].join(';'); @@ -401,7 +401,7 @@ describe('Koa Session External Store', () => { request(server) .post('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .end((err, res) => { if (err) return done(err); const cookie = res.headers['set-cookie'].join(';'); @@ -430,7 +430,7 @@ describe('Koa Session External Store', () => { request(server) .post('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .end((err, res) => { if (err) return done(err); const cookie = res.headers['set-cookie'].join(';'); @@ -467,7 +467,7 @@ describe('Koa Session External Store', () => { request(server) .post('/') - .expect('Set-Cookie', /koa:sess/) + .expect('Set-Cookie', /koa\.sess/) .end((err, res) => { if (err) return done(err); const cookie = res.headers['set-cookie'].join(';'); @@ -559,7 +559,7 @@ describe('Koa Session External Store', () => { request(app.listen()) .get('/') - .set('cookie', 'koa:sess=invalid-key') + .set('cookie', 'koa.sess=invalid-key') .expect('true') .expect(200, done); }); @@ -604,7 +604,7 @@ describe('Koa Session External Store', () => { should.not.exist(err); const data = res.body; const cookies = res.headers['set-cookie'].join(';'); - cookies.should.containEql('koa:sess='); + cookies.should.containEql('koa.sess='); request(app.callback()) .get('/') @@ -710,7 +710,7 @@ describe('Koa Session External Store', () => { .expect(204, (err, res) => { if (err) return done(err); const cookie = res.headers['set-cookie']; - cookie.join().should.match(/koa:sess=sess:/); + cookie.join().should.match(/koa\.sess=sess:/); request(server) .get('/') .set('Cookie', cookie.join(';')) From 6c9df069b0ee9da5410e0ff1e652c8c1a274f07f Mon Sep 17 00:00:00 2001 From: dead-horse Date: Wed, 29 Apr 2020 00:16:52 +0800 Subject: [PATCH 04/25] chore: remove package-lock --- .npmrc | 2 +- package-lock.json | 2076 --------------------------------------------- 2 files changed, 1 insertion(+), 2077 deletions(-) delete mode 100644 package-lock.json diff --git a/.npmrc b/.npmrc index 9cf9495..43c97e7 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -package-lock=false \ No newline at end of file +package-lock=false diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index d60aff9..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2076 +0,0 @@ -{ - "name": "koa-session", - "version": "5.13.1", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "abbrev": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", - "dev": true - }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "dev": true, - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "acorn": { - "version": "5.7.4", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", - "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", - "dev": true - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "^3.0.4" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } - }, - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "dev": true, - "requires": { - "co": "^4.6.0", - "json-stable-stringify": "^1.0.1" - } - }, - "ajv-keywords": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", - "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", - "dev": true - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true, - "optional": true - }, - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" - }, - "benchmark": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", - "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", - "dev": true, - "requires": { - "lodash": "^4.17.4", - "platform": "^1.3.3" - } - }, - "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 - }, - "buffer": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "cache-content-type": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", - "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", - "dev": true, - "requires": { - "mime-types": "^2.1.18", - "ylru": "^1.2.0" - } - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "^0.2.0" - } - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "dev": true, - "requires": { - "restore-cursor": "^1.0.1" - } - }, - "cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", - "dev": true - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "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 - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "dev": true, - "requires": { - "safe-buffer": "5.1.2" - } - }, - "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 - }, - "cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", - "dev": true - }, - "cookies": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", - "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", - "dev": true, - "requires": { - "depd": "~2.0.0", - "keygrip": "~1.1.0" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "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=" - }, - "crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "requires": { - "buffer": "^5.1.0" - } - }, - "d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "dev": true, - "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "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 - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "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 - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "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 - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "error-inject": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", - "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=", - "dev": true - }, - "es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", - "dev": true, - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-set": "~0.1.5", - "es6-symbol": "~3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-symbol": "3.1.1", - "event-emitter": "~0.3.5" - }, - "dependencies": { - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - } - } - }, - "es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "dev": true, - "requires": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } - }, - "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 - }, - "escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", - "dev": true, - "requires": { - "esprima": "^2.7.1", - "estraverse": "^1.9.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.2.0" - }, - "dependencies": { - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - }, - "estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", - "dev": true - } - } - }, - "escope": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", - "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", - "dev": true, - "requires": { - "es6-map": "^0.1.3", - "es6-weak-map": "^2.0.1", - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", - "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", - "dev": true, - "requires": { - "babel-code-frame": "^6.16.0", - "chalk": "^1.1.3", - "concat-stream": "^1.5.2", - "debug": "^2.1.1", - "doctrine": "^2.0.0", - "escope": "^3.6.0", - "espree": "^3.4.0", - "esquery": "^1.0.0", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "file-entry-cache": "^2.0.0", - "glob": "^7.0.3", - "globals": "^9.14.0", - "ignore": "^3.2.0", - "imurmurhash": "^0.1.4", - "inquirer": "^0.12.0", - "is-my-json-valid": "^2.10.0", - "is-resolvable": "^1.0.0", - "js-yaml": "^3.5.1", - "json-stable-stringify": "^1.0.0", - "levn": "^0.3.0", - "lodash": "^4.0.0", - "mkdirp": "^0.5.0", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.1", - "pluralize": "^1.2.1", - "progress": "^1.1.8", - "require-uncached": "^1.0.2", - "shelljs": "^0.7.5", - "strip-bom": "^3.0.0", - "strip-json-comments": "~2.0.1", - "table": "^3.7.8", - "text-table": "~0.2.0", - "user-home": "^2.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" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-config-egg": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-config-egg/-/eslint-config-egg-3.2.0.tgz", - "integrity": "sha1-eb/dndaNh21oIHQSuQUymgfUL4g=", - "dev": true - }, - "espree": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", - "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", - "dev": true, - "requires": { - "acorn": "^5.5.0", - "acorn-jsx": "^3.0.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", - "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "dev": true, - "requires": { - "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "dev": true - }, - "ext": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", - "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", - "dev": true, - "requires": { - "type": "^2.0.0" - }, - "dependencies": { - "type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz", - "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==", - "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 - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", - "dev": true, - "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" - } - }, - "flat-cache": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", - "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", - "dev": true, - "requires": { - "circular-json": "^0.3.1", - "graceful-fs": "^4.1.2", - "rimraf": "~2.6.2", - "write": "^0.2.1" - } - }, - "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "formidable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", - "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", - "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 - }, - "generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "dev": true, - "requires": { - "is-property": "^1.0.2" - } - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, - "requires": { - "is-property": "^1.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "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" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "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 - }, - "handlebars": { - "version": "4.7.6", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", - "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", - "dev": true, - "requires": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "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-assert": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz", - "integrity": "sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==", - "dev": true, - "requires": { - "deep-equal": "~1.0.1", - "http-errors": "~1.7.2" - } - }, - "http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "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.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "inquirer": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", - "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", - "dev": true, - "requires": { - "ansi-escapes": "^1.1.0", - "ansi-regex": "^2.0.0", - "chalk": "^1.0.0", - "cli-cursor": "^1.0.1", - "cli-width": "^2.0.0", - "figures": "^1.3.5", - "lodash": "^4.3.0", - "readline2": "^1.0.1", - "run-async": "^0.1.0", - "rx-lite": "^3.1.2", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.0", - "through": "^2.3.6" - } - }, - "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", - "dev": true - }, - "is-class-hotfix": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/is-class-hotfix/-/is-class-hotfix-0.0.6.tgz", - "integrity": "sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ==" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-generator-function": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", - "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==", - "dev": true - }, - "is-my-ip-valid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", - "dev": true - }, - "is-my-json-valid": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.0.tgz", - "integrity": "sha512-XTHBZSIIxNsIsZXg7XB5l8z/OBFosl1Wao4tXLpeC7eKU4Vm/kdop2azkPqULwnfGQjmeDIyey9g7afMMtdWAA==", - "dev": true, - "requires": { - "generate-function": "^2.0.0", - "generate-object-property": "^1.1.0", - "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^4.0.0", - "xtend": "^4.0.0" - } - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "is-type-of": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-type-of/-/is-type-of-1.2.1.tgz", - "integrity": "sha512-uK0kyX9LZYhSDS7H2sVJQJop1UnWPWmo5RvR3q2kFH6AUHYs7sOrVg0b4nyBHw29kRRNFofYN/JbHZDlHiItTA==", - "requires": { - "core-util-is": "^1.0.2", - "is-class-hotfix": "~0.0.6", - "isstream": "~0.1.2" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "istanbul": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", - "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", - "dev": true, - "requires": { - "abbrev": "1.0.x", - "async": "1.x", - "escodegen": "1.8.x", - "esprima": "2.7.x", - "glob": "^5.0.15", - "handlebars": "^4.0.1", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "once": "1.x", - "resolve": "1.1.x", - "supports-color": "^3.1.0", - "which": "^1.1.1", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, - "keygrip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", - "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", - "dev": true, - "requires": { - "tsscmp": "1.0.6" - } - }, - "ko-sleep": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ko-sleep/-/ko-sleep-1.0.3.tgz", - "integrity": "sha1-KKKgoUhei39BX/SI3uF9JHiKsII=", - "dev": true, - "requires": { - "ms": "^2.0.0" - } - }, - "koa": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.11.0.tgz", - "integrity": "sha512-EpR9dElBTDlaDgyhDMiLkXrPwp6ZqgAIBvhhmxQ9XN4TFgW+gEz6tkcsNI6BnUbUftrKDjVFj4lW2/J2aNBMMA==", - "dev": true, - "requires": { - "accepts": "^1.3.5", - "cache-content-type": "^1.0.0", - "content-disposition": "~0.5.2", - "content-type": "^1.0.4", - "cookies": "~0.8.0", - "debug": "~3.1.0", - "delegates": "^1.0.0", - "depd": "^1.1.2", - "destroy": "^1.0.4", - "encodeurl": "^1.0.2", - "error-inject": "^1.0.0", - "escape-html": "^1.0.3", - "fresh": "~0.5.2", - "http-assert": "^1.3.0", - "http-errors": "^1.6.3", - "is-generator-function": "^1.0.7", - "koa-compose": "^4.1.0", - "koa-convert": "^1.2.0", - "on-finished": "^2.3.0", - "only": "~0.0.2", - "parseurl": "^1.3.2", - "statuses": "^1.5.0", - "type-is": "^1.6.16", - "vary": "^1.1.2" - }, - "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" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "koa-compose": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", - "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", - "dev": true - }, - "koa-convert": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", - "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", - "dev": true, - "requires": { - "co": "^4.6.0", - "koa-compose": "^3.0.0" - }, - "dependencies": { - "koa-compose": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", - "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", - "dev": true, - "requires": { - "any-promise": "^1.1.0" - } - } - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "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.43.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", - "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", - "dev": true - }, - "mime-types": { - "version": "2.1.26", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", - "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", - "dev": true, - "requires": { - "mime-db": "1.43.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" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "mm": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/mm/-/mm-2.5.0.tgz", - "integrity": "sha512-ilm+lGEBNm7Cw45um9ax0tbApiNwQV3PY6Yk1ol+wtA8c98hHuJqTgmdKB6rYQJTUC2QrhBfoWwN+/766ZlrYA==", - "dev": true, - "requires": { - "is-type-of": "^1.0.0", - "ko-sleep": "^1.0.2", - "muk-prop": "^1.0.0", - "thenify": "^3.2.1" - } - }, - "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": { - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "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" - } - } - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "muk-prop": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/muk-prop/-/muk-prop-1.2.1.tgz", - "integrity": "sha512-NdkOVav3GoIkBZqMUneU435HW0a90zitpuO1erPRhOQdPtl65dXD3G9/1k46G6/0ZMau4CJFFUHkMKVsyNZT+w==", - "dev": true - }, - "mute-stream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", - "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", - "dev": true - }, - "mz-modules": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mz-modules/-/mz-modules-2.1.0.tgz", - "integrity": "sha512-sjk8lcRW3vrVYnZ+W+67L/2rL+jbO5K/N6PFGIcLWTiYytNr22Ah9FDXFs+AQntTM1boZcoHi5qS+CV1seuPog==", - "dev": true, - "requires": { - "glob": "^7.1.2", - "ko-sleep": "^1.0.3", - "mkdirp": "^0.5.1", - "pump": "^3.0.0", - "rimraf": "^2.6.1" - } - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", - "dev": true - }, - "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", - "dev": true - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "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" - } - }, - "onetime": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - }, - "only": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", - "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=", - "dev": true - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "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-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "pedding": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pedding/-/pedding-1.1.0.tgz", - "integrity": "sha1-97E4wojUvVhOraEhX1vZJPHh5mc=", - "dev": true - }, - "platform": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", - "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==", - "dev": true - }, - "pluralize": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", - "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", - "dev": true - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", - "dev": true - }, - "random-bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "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" - } - }, - "readline2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", - "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "mute-stream": "0.0.5" - } - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true, - "requires": { - "resolve": "^1.1.6" - } - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" - } - }, - "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "dev": true, - "requires": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" - } - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-async": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", - "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", - "dev": true, - "requires": { - "once": "^1.3.0" - } - }, - "rx-lite": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", - "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", - "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 - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "dev": true - }, - "shelljs": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", - "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", - "dev": true, - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - } - }, - "should": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/should/-/should-8.4.0.tgz", - "integrity": "sha1-XmCInT5kS73Tl6MM00+tKPz5C8A=", - "dev": true, - "requires": { - "should-equal": "0.8.0", - "should-format": "0.3.2", - "should-type": "0.2.0" - } - }, - "should-equal": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-0.8.0.tgz", - "integrity": "sha1-o/BXMv9FusG3ukEvhAiFaBlkEpk=", - "dev": true, - "requires": { - "should-type": "0.2.0" - } - }, - "should-format": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/should-format/-/should-format-0.3.2.tgz", - "integrity": "sha1-pZgx4Bot3uFJkRvHFIvlyAMZ4f8=", - "dev": true, - "requires": { - "should-type": "0.2.0" - } - }, - "should-type": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/should-type/-/should-type-0.2.0.tgz", - "integrity": "sha1-ZwfvlVKdmJ3MCY/gdTqx+RNrt/Y=", - "dev": true - }, - "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true - }, - "source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "dev": true, - "optional": true, - "requires": { - "amdefine": ">=0.0.4" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "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" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "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.4.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.4.2.tgz", - "integrity": "sha512-WZWbwceHUo2P36RoEIdXvmqfs47idNNZjCuJOqDz6rvtkk8ym56aU5oglORCpPeXGxT7l9rkJ41+O1lffQXYSA==", - "dev": true, - "requires": { - "methods": "^1.1.2", - "superagent": "^3.8.3" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "table": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", - "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", - "dev": true, - "requires": { - "ajv": "^4.7.0", - "ajv-keywords": "^1.0.0", - "chalk": "^1.1.1", - "lodash": "^4.0.0", - "slice-ansi": "0.0.4", - "string-width": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "thenify": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", - "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", - "dev": true, - "requires": { - "any-promise": "^1.0.0" - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "dev": true - }, - "tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "dev": true - }, - "type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "uglify-js": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.1.tgz", - "integrity": "sha512-JUPoL1jHsc9fOjVFHdQIhqEEJsQvfKDjlubcCilu8U26uZ73qOg8VsN8O1jbuei44ZPlwL7kmbAdM4tzaUvqnA==", - "dev": true, - "optional": true, - "requires": { - "commander": "~2.20.3" - } - }, - "uid-safe": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", - "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", - "dev": true, - "requires": { - "random-bytes": "~1.0.0" - } - }, - "user-home": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true - }, - "ylru": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", - "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==", - "dev": true - } - } -} From e630f7386f269d233f64e270c0eecd1eda91312b Mon Sep 17 00:00:00 2001 From: dead-horse Date: Wed, 29 Apr 2020 00:23:20 +0800 Subject: [PATCH 05/25] docs: add migrate from 5.x to 6.x --- Readme.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Readme.md b/Readme.md index fb5664f..35bae2d 100644 --- a/Readme.md +++ b/Readme.md @@ -36,6 +36,10 @@ $ npm install koa-session ``` +## Notice + +6.x changed the default cookie key from `koa:sess` to `koa.sess` to ensure `set-cookie` value valid with HTTP spec.[see issue](https://github.com/koajs/session/issues/28). If you want to be compatible with the previous version, you can manually set `config.key` to `koa:sess`. + ## Example View counter example: From 136b9034c374b9a51f05973d79e1a8d9a2c83b90 Mon Sep 17 00:00:00 2001 From: dead-horse Date: Wed, 29 Apr 2020 00:25:28 +0800 Subject: [PATCH 06/25] Release 6.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3528393..f05b363 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware with external store support", "repository": "koajs/session", - "version": "5.13.1", + "version": "6.0.0", "keywords": [ "koa", "middleware", From f76559568bb7f6321cab3f44ae759521deca3dd1 Mon Sep 17 00:00:00 2001 From: Dominic Egginton Date: Tue, 28 Apr 2020 17:26:12 +0100 Subject: [PATCH 07/25] chore: Create LICENSE File (#195) --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..eaf38ab --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2014-present Koajs contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 32e352665f2adbcda34d1d990bb6c5d875c0b625 Mon Sep 17 00:00:00 2001 From: Ngorror Date: Thu, 8 Oct 2020 11:35:20 +0200 Subject: [PATCH 08/25] feat: add context to external store .get() and .set() options params (#201) --- Readme.md | 6 +-- lib/context.js | 5 ++- test/store_with_ctx.js | 23 +++++++++++ test/store_with_ctx.test.js | 81 +++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 test/store_with_ctx.js create mode 100644 test/store_with_ctx.test.js diff --git a/Readme.md b/Readme.md index 35bae2d..ee59469 100644 --- a/Readme.md +++ b/Readme.md @@ -111,9 +111,9 @@ console.log('listening on port 3000'); You can store the session content in external stores (Redis, MongoDB or other DBs) by passing `options.store` with three methods (these need to be async functions): - - `get(key, maxAge, { rolling })`: get session object by key - - `set(key, sess, maxAge, { rolling, changed })`: set session object for key, with a `maxAge` (in ms) - - `destroy(key)`: destroy session for key + - `get(key, maxAge, { rolling, ctx })`: get session object by key + - `set(key, sess, maxAge, { rolling, changed, ctx })`: set session object for key, with a `maxAge` (in ms) + - `destroy(key, {ctx})`: destroy session for key Once you pass `options.store`, session storage is dependent on your external store -- you can't access the session if your external store is down. **Use external session stores only if necessary, avoid using session as a cache, keep the session lean, and store it in a cookie if possible!** diff --git a/lib/context.js b/lib/context.js index 1bb41a8..ccb8d5c 100644 --- a/lib/context.js +++ b/lib/context.js @@ -87,7 +87,7 @@ class ContextSession { return; } - const json = await this.store.get(externalKey, opts.maxAge, { rolling: opts.rolling }); + const json = await this.store.get(externalKey, opts.maxAge, { ctx, rolling: opts.rolling }); if (!this.valid(json, externalKey)) { // create a new `externalKey` this.create(); @@ -284,7 +284,7 @@ class ContextSession { const key = opts.key; const externalKey = this.externalKey; - if (externalKey) await this.store.destroy(externalKey); + if (externalKey) await this.store.destroy(externalKey, { ctx }); ctx.cookies.set(key, '', opts); } @@ -320,6 +320,7 @@ class ContextSession { } await this.store.set(externalKey, json, maxAge, { changed, + ctx: this.ctx, rolling: opts.rolling, }); if (opts.externalKey) { diff --git a/test/store_with_ctx.js b/test/store_with_ctx.js new file mode 100644 index 0000000..2ebd234 --- /dev/null +++ b/test/store_with_ctx.js @@ -0,0 +1,23 @@ +'use strict'; + +const sessions = {}; + +module.exports = { + async get(key, maxAge, options) { + // check access to options.ctx + options.ctx.state.test = 'get'; + return sessions[key]; + }, + + async set(key, sess, maxAge, options) { + // check access to options.ctx + options.ctx.state.test = 'set'; + sessions[key] = sess; + }, + + async destroy(key, options) { + // check access to options.ctx + options.ctx.state.test = 'destroyed'; + sessions[key] = undefined; + }, +}; diff --git a/test/store_with_ctx.test.js b/test/store_with_ctx.test.js new file mode 100644 index 0000000..21fe43a --- /dev/null +++ b/test/store_with_ctx.test.js @@ -0,0 +1,81 @@ +'use strict'; + +const Koa = require('koa'); +const request = require('supertest'); +const session = require('..'); +const store = require('./store_with_ctx'); + +describe('Koa Session External Store methods can acceess Koa context', () => { + let cookie; + + describe('new session', () => { + describe('when not accessed', () => { + it('should not set ctx.state.test variable', async () => { + const app = App(); + + request(app.callback()) + .get('/') + .expect('undefined'); + }); + }); + + describe('when populated', () => { + it('should set ctx.state.test variable', done => { + const app = App(); + + app.use(async ctx => { + if (ctx.path === '/set') ctx.session = { foo: 'bar' }; + }); + + request(app.listen()) + .get('/set') + .expect(200, (err, res) => { + if (err) return done(err); + cookie = res.header['set-cookie'].join(';'); + res.text.should.equal('set'); + done(); + }); + }); + }); + + describe('when accessed', () => { + it('should access ctx.state.test variable', async () => { + const app = App(); + + request(app.callback()) + .get('/') + .set('Cookie', cookie) + .expect('get'); + }); + }); + + describe('session destroyed', () => { + it('should access ctx.state.test variable', async () => { + const app = App(); + + app.use(async ctx => { + if (ctx.path === '/destroy') ctx.session = null; + }); + + request(app.callback()) + .get('/destroy') + .set('Cookie', cookie) + .expect('destroyed'); + }); + }); + }); +}); + +function App(options) { + const app = new Koa(); + app.keys = [ 'a', 'b' ]; + options = options || {}; + options.store = store; + app.use(async (ctx, next) => { + await next(); + ctx.body = ctx.state.test === undefined ? 'undefined' : ctx.state.test; + }); + + app.use(session(options, app)); + return app; +} From bbd9d238778e4bb8f686f20cd3454a6a9e8115bb Mon Sep 17 00:00:00 2001 From: dead-horse Date: Thu, 8 Oct 2020 17:40:06 +0800 Subject: [PATCH 09/25] Release 6.1.0 --- History.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 40a77ce..1744063 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,12 @@ + +6.1.0 / 2020-10-08 +================== + +**features** + * [[`32e3526`](http://github.com/koajs/session/commit/32e352665f2adbcda34d1d990bb6c5d875c0b625)] - feat: add context to external store .get() and .set() options params (#201) (Ngorror <>) + +**others** + * [[`f765595`](http://github.com/koajs/session/commit/f76559568bb7f6321cab3f44ae759521deca3dd1)] - chore: Create LICENSE File (#195) (Dominic Egginton <>) 6.0.0 / 2020-04-26 ================== diff --git a/package.json b/package.json index f05b363..81a3fa6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware with external store support", "repository": "koajs/session", - "version": "6.0.0", + "version": "6.1.0", "keywords": [ "koa", "middleware", From 7cde341db1691ee6885eb1b11ff3a3a3632fd5ce Mon Sep 17 00:00:00 2001 From: Yiyu He Date: Tue, 30 Mar 2021 18:27:14 +0800 Subject: [PATCH 10/25] feat: add session.externalKey (#207) --- Readme.md | 4 ++++ lib/context.js | 2 +- lib/session.js | 15 +++++++++++++-- test/contextstore.test.js | 4 +++- test/cookie.test.js | 4 +++- test/externalkey.test.js | 3 +++ test/store.test.js | 4 +++- 7 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Readme.md b/Readme.md index ee59469..b5f0561 100644 --- a/Readme.md +++ b/Readme.md @@ -160,6 +160,10 @@ if (this.session.isNew) { Set cookie's maxAge. +### Session#externalKey + + Get session external key, only exist when external session store present. + ### Session#save() Save this session no matter whether it is populated. diff --git a/lib/context.js b/lib/context.js index ccb8d5c..95681a0 100644 --- a/lib/context.js +++ b/lib/context.js @@ -204,7 +204,7 @@ class ContextSession { create(val, externalKey) { debug('create session with val: %j externalKey: %s', val, externalKey); if (this.store) this.externalKey = externalKey || this.opts.genid && this.opts.genid(this.ctx); - this.session = new Session(this, val); + this.session = new Session(this, val, this.externalKey); } /** diff --git a/lib/session.js b/lib/session.js index 6961e3b..c9019af 100644 --- a/lib/session.js +++ b/lib/session.js @@ -4,6 +4,8 @@ * Session model. */ +const inspect = Symbol.for('nodejs.util.inspect.custom'); + class Session { /** * Session constructor @@ -12,9 +14,10 @@ class Session { * @api private */ - constructor(sessionContext, obj) { + constructor(sessionContext, obj, externalKey) { this._sessCtx = sessionContext; this._ctx = sessionContext.ctx; + this._externalKey = externalKey; if (!obj) { this.isNew = true; } else { @@ -52,7 +55,7 @@ class Session { * @api public */ - inspect() { + [inspect]() { return this.toJSON(); } @@ -103,6 +106,14 @@ class Session { this._requireSave = true; } + /** + * get session external key + * only exist if opts.store present + */ + get externalKey() { + return this._externalKey; + } + /** * save this session no matter whether it is populated * diff --git a/test/contextstore.test.js b/test/contextstore.test.js index cf71eb5..b31fada 100644 --- a/test/contextstore.test.js +++ b/test/contextstore.test.js @@ -7,6 +7,8 @@ const mm = require('mm'); const session = require('..'); const ContextStore = require('./context_store'); +const inspect = Symbol.for('nodejs.util.inspect.custom'); + describe('Koa Session External Context Store', () => { let cookie; @@ -280,7 +282,7 @@ describe('Koa Session External Context Store', () => { app.use(async function(ctx) { ctx.session.foo = 'bar'; - ctx.body = ctx.session.inspect(); + ctx.body = ctx.session[inspect](); }); request(app.listen()) diff --git a/test/cookie.test.js b/test/cookie.test.js index 568d2b1..f3fe914 100644 --- a/test/cookie.test.js +++ b/test/cookie.test.js @@ -6,6 +6,8 @@ const request = require('supertest'); const should = require('should'); const session = require('..'); +const inspect = Symbol.for('nodejs.util.inspect.custom'); + describe('Koa Session Cookie', () => { let cookie; @@ -385,7 +387,7 @@ describe('Koa Session Cookie', () => { app.use(async function(ctx) { ctx.session.foo = 'bar'; - ctx.body = ctx.session.inspect(); + ctx.body = ctx.session[inspect](); }); request(app.listen()) diff --git a/test/externalkey.test.js b/test/externalkey.test.js index c3625cb..3dfd8c1 100644 --- a/test/externalkey.test.js +++ b/test/externalkey.test.js @@ -5,6 +5,7 @@ const assert = require('assert'); const session = require('..'); const store = require('./store'); const TOKEN_KEY = 'User-Token'; + describe('Koa Session External Key', () => { describe('when the external key set/get is invalid', () => { it('should throw a error', () => { @@ -24,8 +25,10 @@ describe('Koa Session External Key', () => { if (ctx.method === 'POST') { ctx.session.string = ';'; ctx.status = 204; + assert(ctx.session.externalKey); } else { ctx.body = ctx.session.string; + assert(ctx.session.externalKey === ctx.get(TOKEN_KEY)); } }); const server = app.listen(); diff --git a/test/store.test.js b/test/store.test.js index d1bbf93..caf5375 100644 --- a/test/store.test.js +++ b/test/store.test.js @@ -10,6 +10,8 @@ const pedding = require('pedding'); const assert = require('assert'); const sleep = require('mz-modules/sleep'); +const inspect = Symbol.for('nodejs.util.inspect.custom'); + describe('Koa Session External Store', () => { let cookie; @@ -262,7 +264,7 @@ describe('Koa Session External Store', () => { app.use(async function(ctx) { ctx.session.foo = 'bar'; - ctx.body = ctx.session.inspect(); + ctx.body = ctx.session[inspect](); }); request(app.listen()) From 07ca4d7653e864841326117c90e435a90633a0d7 Mon Sep 17 00:00:00 2001 From: dead-horse Date: Tue, 30 Mar 2021 18:27:33 +0800 Subject: [PATCH 11/25] Release 6.2.0 --- History.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 1744063..a5b4e51 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,10 @@ +6.2.0 / 2021-03-30 +================== + +**features** + * [[`7cde341`](http://github.com/koajs/session/commit/7cde341db1691ee6885eb1b11ff3a3a3632fd5ce)] - feat: add session.externalKey (#207) (Yiyu He <>) + 6.1.0 / 2020-10-08 ================== diff --git a/package.json b/package.json index 81a3fa6..6feecb9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware with external store support", "repository": "koajs/session", - "version": "6.1.0", + "version": "6.2.0", "keywords": [ "koa", "middleware", From 878669ee2c734e3d9902d83f57e21d2113178b79 Mon Sep 17 00:00:00 2001 From: zhennann Date: Tue, 3 Jan 2023 18:00:54 +0800 Subject: [PATCH 12/25] feat: update uuid to v8 (#218) --- index.js | 6 +++--- package.json | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 816a15a..4d149aa 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,7 @@ const debug = require('debug')('koa-session'); const ContextSession = require('./lib/context'); const util = require('./lib/util'); const assert = require('assert'); -const uuid = require('uuid/v4'); +const uuid = require('uuid'); const is = require('is-type-of'); const CONTEXT_SESSION = Symbol('context#contextSession'); @@ -105,8 +105,8 @@ function formatOpts(opts) { } if (!opts.genid) { - if (opts.prefix) opts.genid = () => `${opts.prefix}${uuid()}`; - else opts.genid = uuid; + if (opts.prefix) opts.genid = () => `${opts.prefix}${uuid.v4()}`; + else opts.genid = uuid.v4; } return opts; diff --git a/package.json b/package.json index 6feecb9..87249f9 100644 --- a/package.json +++ b/package.json @@ -23,16 +23,16 @@ "mocha": "^5.2.0", "mz-modules": "^2.0.0", "pedding": "^1.1.0", - "uid-safe": "^2.1.3", "should": "8", - "supertest": "^3.3.0" + "supertest": "^3.3.0", + "uid-safe": "^2.1.3" }, "license": "MIT", "dependencies": { - "crc": "^3.4.4", - "debug": "^3.1.0", - "is-type-of": "^1.0.0", - "uuid": "^3.3.2" + "crc": "^4.0.0", + "debug": "^4.3.3", + "is-type-of": "^1.2.1", + "uuid": "^8.3.2" }, "engines": { "node": ">=7.6" From df2d28ffb177272739964eb5a503f93db870aa28 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 3 Jan 2023 18:14:21 +0800 Subject: [PATCH 13/25] test: run ci on GitHub Action (#222) --- .github/workflows/nodejs.yml | 27 +++++++++++++++++++++++++++ .travis.yml | 9 --------- Readme.md | 21 ++------------------- package.json | 4 ++-- 4 files changed, 31 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/nodejs.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml new file mode 100644 index 0000000..a46f3e4 --- /dev/null +++ b/.github/workflows/nodejs.yml @@ -0,0 +1,27 @@ +name: Node.js CI + +on: + push: + branch: master + pull_request: + branch: master + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [8, 10, 12, 14, 16, 18] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm run lint + - run: npm run ci + - run: npx codecov diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 80b9e4a..0000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -sudo: false -language: node_js -node_js: - - '7' - - '8' - - '10' - - '12' -script: 'npm run test-travis' -after_script: 'npm install coveralls@2 && cat ./coverage/lcov.info | coveralls' diff --git a/Readme.md b/Readme.md index b5f0561..03275e1 100644 --- a/Readme.md +++ b/Readme.md @@ -1,34 +1,17 @@ # koa-session [![NPM version][npm-image]][npm-url] -[![build status][travis-image]][travis-url] -[![Test coverage][coveralls-image]][coveralls-url] -[![Gittip][gittip-image]][gittip-url] -[![David deps][david-image]][david-url] -[![iojs version][iojs-image]][iojs-url] -[![node version][node-image]][node-url] +[![Node.js CI](https://github.com/koajs/session/actions/workflows/nodejs.yml/badge.svg)](https://github.com/koajs/session/actions/workflows/nodejs.yml) [![npm download][download-image]][download-url] [npm-image]: https://img.shields.io/npm/v/koa-session.svg?style=flat-square [npm-url]: https://npmjs.org/package/koa-session -[travis-image]: https://img.shields.io/travis/koajs/session.svg?style=flat-square -[travis-url]: https://travis-ci.org/koajs/session -[coveralls-image]: https://img.shields.io/coveralls/koajs/session.svg?style=flat-square -[coveralls-url]: https://coveralls.io/r/koajs/session?branch=master -[gittip-image]: https://img.shields.io/gittip/fengmk2.svg?style=flat-square -[gittip-url]: https://www.gittip.com/fengmk2/ -[david-image]: https://img.shields.io/david/koajs/session.svg?style=flat-square -[david-url]: https://david-dm.org/koajs/session -[iojs-image]: https://img.shields.io/badge/io.js-%3E=_1.0-yellow.svg?style=flat-square -[iojs-url]: http://iojs.org/ -[node-image]: https://img.shields.io/badge/node.js-%3E=_7.6-green.svg?style=flat-square -[node-url]: http://nodejs.org/download/ [download-image]: https://img.shields.io/npm/dm/koa-session.svg?style=flat-square [download-url]: https://npmjs.org/package/koa-session Simple session middleware for Koa. Defaults to cookie-based sessions and supports external stores. - *Requires Node 7.6 or greater for async/await support* + *Requires Node 8.0.0 or greater for async/await support* ## Installation diff --git a/package.json b/package.json index 87249f9..3797976 100644 --- a/package.json +++ b/package.json @@ -35,12 +35,12 @@ "uuid": "^8.3.2" }, "engines": { - "node": ">=7.6" + "node": ">=8.0.0" }, "scripts": { "test": "npm run lint && NODE_ENV=test mocha --exit --require should --reporter spec test/*.test.js", "test-cov": "NODE_ENV=test node ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --exit --require should test/*.test.js", - "test-travis": "npm run lint && NODE_ENV=test node ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- --exit --require should test/*.test.js", + "ci": "npm run lint && NODE_ENV=test node ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- --exit --require should test/*.test.js", "lint": "eslint lib test index.js" } } From 75f5706cc540e6573707ad1fb5e104dac98f44a4 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 3 Jan 2023 18:15:23 +0800 Subject: [PATCH 14/25] Release 6.3.0 --- History.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index a5b4e51..c06a9c0 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,13 @@ +6.3.0 / 2023-01-03 +================== + +**features** + * [[`878669e`](http://github.com/koajs/session/commit/878669ee2c734e3d9902d83f57e21d2113178b79)] - feat: update uuid to v8 (#218) (zhennann <>) + +**others** + * [[`df2d28f`](http://github.com/koajs/session/commit/df2d28ffb177272739964eb5a503f93db870aa28)] - test: run ci on GitHub Action (#222) (fengmk2 <>) + 6.2.0 / 2021-03-30 ================== diff --git a/package.json b/package.json index 3797976..abf0ef6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware with external store support", "repository": "koajs/session", - "version": "6.2.0", + "version": "6.3.0", "keywords": [ "koa", "middleware", From e2de39e6acaf5eef9c8660cbd864ecccaa2b60d0 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 3 Jan 2023 18:32:35 +0800 Subject: [PATCH 15/25] fix: keep crc v3 (#223) avoid peerDependencies WARNING https://github.com/koajs/session/pull/218/files#r1060445443 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index abf0ef6..fe91994 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ }, "license": "MIT", "dependencies": { - "crc": "^4.0.0", + "crc": "^3.8.0", "debug": "^4.3.3", "is-type-of": "^1.2.1", "uuid": "^8.3.2" From 6b7043156471d0a7fe91666fe909baec21743590 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 3 Jan 2023 18:33:05 +0800 Subject: [PATCH 16/25] Release 6.3.1 --- History.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index c06a9c0..00f139c 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,10 @@ +6.3.1 / 2023-01-03 +================== + +**fixes** + * [[`e2de39e`](http://github.com/koajs/session/commit/e2de39e6acaf5eef9c8660cbd864ecccaa2b60d0)] - fix: keep crc v3 (#223) (fengmk2 <>) + 6.3.0 / 2023-01-03 ================== diff --git a/package.json b/package.json index fe91994..747493e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware with external store support", "repository": "koajs/session", - "version": "6.3.0", + "version": "6.3.1", "keywords": [ "koa", "middleware", From 4cd3bef4fc1900b847d2e133e3b2599a711f1aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 4 Feb 2023 15:53:32 +0100 Subject: [PATCH 17/25] feat: Add Session.regenerate() method (#221) And make both save() and regenerate() compatible with koa-passport closes https://github.com/koajs/session/pull/74 --- lib/context.js | 12 +++++++----- lib/session.js | 32 ++++++++++++++++++++++++++++---- test/cookie.test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ test/store.test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 9 deletions(-) diff --git a/lib/context.js b/lib/context.js index 95681a0..d3efbec 100644 --- a/lib/context.js +++ b/lib/context.js @@ -213,7 +213,7 @@ class ContextSession { * @api public */ - async commit() { + async commit({ save = false, regenerate = false } = {}) { const session = this.session; const opts = this.opts; const ctx = this.ctx; @@ -226,8 +226,13 @@ class ContextSession { await this.remove(); return; } + if (regenerate) { + await this.remove(); + if (this.store) this.externalKey = opts.genid && opts.genid(ctx); + } - const reason = this._shouldSaveSession(); + // force save session when `session._requireSave` set + const reason = save || regenerate || session._requireSave ? 'force' : this._shouldSaveSession(); debug('should save session: %s', reason); if (!reason) return; @@ -243,9 +248,6 @@ class ContextSession { const prevHash = this.prevHash; const session = this.session; - // force save session when `session._requireSave` set - if (session._requireSave) return 'force'; - // do nothing if new and not populated const json = session.toJSON(); if (!prevHash && !Object.keys(json).length) return ''; diff --git a/lib/session.js b/lib/session.js index c9019af..202acc1 100644 --- a/lib/session.js +++ b/lib/session.js @@ -117,11 +117,23 @@ class Session { /** * save this session no matter whether it is populated * + * @param {Function} callback the optional function to call after saving the session * @api public */ - save() { - this._requireSave = true; + save(callback) { + return this.commit({ save: true }, callback); + } + + /** + * regenerate this session + * + * @param {Function} callback the optional function to call after regenerating the session + * @api public + */ + + regenerate(callback) { + return this.commit({ regenerate: true }, callback); } /** @@ -130,10 +142,22 @@ class Session { * @api public */ - async manuallyCommit() { - await this._sessCtx.commit(); + manuallyCommit() { + return this.commit(); } + commit(options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + const promise = this._sessCtx.commit(options); + if (callback) { + promise.then(() => callback(), callback); + } else { + return promise; + } + } } module.exports = Session; diff --git a/test/cookie.test.js b/test/cookie.test.js index f3fe914..69b0683 100644 --- a/test/cookie.test.js +++ b/test/cookie.test.js @@ -674,6 +674,48 @@ describe('Koa Session Cookie', () => { }); }); + describe('ctx.session.regenerate', () => { + it('should change the session key, but not content', done => { + const app = new App(); + const message = 'hi'; + app.use(async function(ctx, next) { + ctx.session = { message: 'hi' }; + await next(); + }); + + app.use(async function(ctx, next) { + const sessionKey = ctx.cookies.get('koa.sess'); + if (sessionKey) { + await ctx.session.regenerate(); + } + await next(); + }); + + app.use(async function(ctx) { + ctx.session.message.should.equal(message); + ctx.body = ''; + }); + let koaSession = null; + request(app.callback()) + .get('/') + .expect(200, (err, res) => { + should.not.exist(err); + koaSession = res.headers['set-cookie'][0]; + koaSession.should.containEql('koa.sess='); + request(app.callback()) + .get('/') + .set('Cookie', koaSession) + .expect(200, (err, res) => { + should.not.exist(err); + const cookies = res.headers['set-cookie'][0]; + cookies.should.containEql('koa.sess='); + cookies.should.not.equal(koaSession); + done(); + }); + }); + }); + }); + describe('when get session before enter session middleware', () => { it('should work', done => { const app = new Koa(); diff --git a/test/store.test.js b/test/store.test.js index caf5375..be3a5fe 100644 --- a/test/store.test.js +++ b/test/store.test.js @@ -544,6 +544,48 @@ describe('Koa Session External Store', () => { }); }); + describe('ctx.session.regenerate', () => { + it('should change the session key, but not content', done => { + const app = new App(); + const message = 'hi'; + app.use(async function(ctx, next) { + ctx.session = { message: 'hi' }; + await next(); + }); + + app.use(async function(ctx, next) { + const sessionKey = ctx.cookies.get('koa.sess'); + if (sessionKey) { + await ctx.session.regenerate(); + } + await next(); + }); + + app.use(async function(ctx) { + ctx.session.message.should.equal(message); + ctx.body = ''; + }); + let koaSession = null; + request(app.callback()) + .get('/') + .expect(200, (err, res) => { + should.not.exist(err); + koaSession = res.headers['set-cookie'][0]; + koaSession.should.containEql('koa.sess='); + request(app.callback()) + .get('/') + .set('Cookie', koaSession) + .expect(200, (err, res) => { + should.not.exist(err); + const cookies = res.headers['set-cookie'][0]; + cookies.should.containEql('koa.sess='); + cookies.should.not.equal(koaSession); + done(); + }); + }); + }); + }); + describe('when store return empty', () => { it('should create new Session', done => { done = pedding(done, 2); From 98630aaf6587b199969490f8503641554aeee917 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sat, 4 Feb 2023 22:56:54 +0800 Subject: [PATCH 18/25] Release 6.4.0 --- History.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 00f139c..1e86323 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,10 @@ +6.4.0 / 2023-02-04 +================== + +**features** + * [[`4cd3bef`](http://github.com/koajs/session/commit/4cd3bef4fc1900b847d2e133e3b2599a711f1aea)] - feat: Add Session.regenerate() method (#221) (Jürg Lehni <>) + 6.3.1 / 2023-01-03 ================== diff --git a/package.json b/package.json index 747493e..a61dc55 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware with external store support", "repository": "koajs/session", - "version": "6.3.1", + "version": "6.4.0", "keywords": [ "koa", "middleware", From 575864c6ae7504da15e72e8112a99757f3eee188 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sun, 19 Jan 2025 15:31:25 +0800 Subject: [PATCH 19/25] feat: support cjs and esm both by tshy (#228) BREAKING CHANGE: drop Node.js < 18.19.0 support --- .eslintrc | 8 +- .github/workflows/nodejs.yml | 32 +- .github/workflows/release.yml | 13 + .gitignore | 3 + History.md => CHANGELOG.md | 0 Readme.md | 80 +- example.cjs | 20 + example.js | 18 - index.js | 151 --- lib/session.js | 163 --- lib/util.js | 39 - package.json | 83 +- lib/context.js => src/context.ts | 212 ++-- src/index.ts | 305 ++++++ src/session.ts | 139 +++ src/util.ts | 26 + test/context_store.js | 25 - test/context_store.ts | 22 + test/contextstore.test.js | 758 -------------- test/contextstore.test.ts | 677 +++++++++++++ test/cookie.test.js | 948 ------------------ test/cookie.test.ts | 900 +++++++++++++++++ test/externalkey.test.js | 59 -- test/externalkey.test.ts | 60 ++ test/genid_bench.js | 45 - test/store.js | 17 - test/store.test.js | 861 ---------------- test/store.test.ts | 800 +++++++++++++++ test/store.ts | 15 + ...ith_ctx.test.js => store_with_ctx.test.ts} | 65 +- test/{store_with_ctx.js => store_with_ctx.ts} | 12 +- tsconfig.json | 10 + 32 files changed, 3258 insertions(+), 3308 deletions(-) create mode 100644 .github/workflows/release.yml rename History.md => CHANGELOG.md (100%) create mode 100644 example.cjs delete mode 100644 example.js delete mode 100644 index.js delete mode 100644 lib/session.js delete mode 100644 lib/util.js rename lib/context.js => src/context.ts (56%) create mode 100644 src/index.ts create mode 100644 src/session.ts create mode 100644 src/util.ts delete mode 100644 test/context_store.js create mode 100644 test/context_store.ts delete mode 100644 test/contextstore.test.js create mode 100644 test/contextstore.test.ts delete mode 100644 test/cookie.test.js create mode 100644 test/cookie.test.ts delete mode 100644 test/externalkey.test.js create mode 100644 test/externalkey.test.ts delete mode 100644 test/genid_bench.js delete mode 100644 test/store.js delete mode 100644 test/store.test.js create mode 100644 test/store.test.ts create mode 100644 test/store.ts rename test/{store_with_ctx.test.js => store_with_ctx.test.ts} (59%) rename test/{store_with_ctx.js => store_with_ctx.ts} (55%) create mode 100644 tsconfig.json diff --git a/.eslintrc b/.eslintrc index 7364c13..9bcdb46 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,6 @@ { - "extends": "eslint-config-egg", - "parserOptions": { - "ecmaVersion": 2017 - } + "extends": [ + "eslint-config-egg/typescript", + "eslint-config-egg/lib/rules/enforce-node-prefix" + ] } diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index a46f3e4..c70132d 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,27 +1,17 @@ -name: Node.js CI +name: CI on: push: - branch: master + branches: [ master ] pull_request: - branch: master + branches: [ master ] jobs: - build: - - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [8, 10, 12, 14, 16, 18] - - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - run: npm run lint - - run: npm run ci - - run: npx codecov + Job: + name: Node.js + uses: node-modules/github-actions/.github/workflows/node-test.yml@master + with: + os: 'ubuntu-latest, macos-latest, windows-latest' + version: '18.19.0, 18, 20, 22' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..035a626 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,13 @@ +name: Release + +on: + push: + branches: [ master ] + +jobs: + release: + name: Node.js + uses: koajs/github-actions/.github/workflows/node-release.yml@master + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GIT_TOKEN: ${{ secrets.GIT_TOKEN }} diff --git a/.gitignore b/.gitignore index 659fedb..388e6c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ node_modules coverage *.log +.tshy* +.eslintcache +dist diff --git a/History.md b/CHANGELOG.md similarity index 100% rename from History.md rename to CHANGELOG.md diff --git a/Readme.md b/Readme.md index 03275e1..34ee099 100644 --- a/Readme.md +++ b/Readme.md @@ -2,34 +2,45 @@ [![NPM version][npm-image]][npm-url] [![Node.js CI](https://github.com/koajs/session/actions/workflows/nodejs.yml/badge.svg)](https://github.com/koajs/session/actions/workflows/nodejs.yml) +[![Test coverage][codecov-image]][codecov-url] +[![Known Vulnerabilities][snyk-image]][snyk-url] [![npm download][download-image]][download-url] +[![Node.js Version](https://img.shields.io/node/v/koajs/session.svg?style=flat)](https://nodejs.org/en/download/) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) [npm-image]: https://img.shields.io/npm/v/koa-session.svg?style=flat-square [npm-url]: https://npmjs.org/package/koa-session +[codecov-image]: https://codecov.io/gh/koajs/session/branch/master/graph/badge.svg +[codecov-url]: https://codecov.io/gh/koajs/session +[snyk-image]: https://snyk.io/test/npm/koa-session/badge.svg?style=flat-square +[snyk-url]: https://snyk.io/test/npm/koa-session [download-image]: https://img.shields.io/npm/dm/koa-session.svg?style=flat-square [download-url]: https://npmjs.org/package/koa-session - Simple session middleware for Koa. Defaults to cookie-based sessions and supports external stores. - - *Requires Node 8.0.0 or greater for async/await support* +Simple session middleware for Koa. Defaults to cookie-based sessions and supports external stores. ## Installation -```js -$ npm install koa-session +```bash +npm install koa-session ``` ## Notice -6.x changed the default cookie key from `koa:sess` to `koa.sess` to ensure `set-cookie` value valid with HTTP spec.[see issue](https://github.com/koajs/session/issues/28). If you want to be compatible with the previous version, you can manually set `config.key` to `koa:sess`. +7.x has a breaking change: drop Node.js < 18.19.0 support. And it support CommonJS and ESM both. + +6.x changed the default cookie key from `koa:sess` to `koa.sess` to ensure `set-cookie` value valid with HTTP spec. +[See issue](https://github.com/koajs/session/issues/28). +If you want to be compatible with the previous version, you can manually set `config.key` to `koa:sess`. ## Example - View counter example: +View counter example: ```js -const session = require('koa-session'); -const Koa = require('koa'); +import Koa from 'koa'; +import session from 'koa-session'; + const app = new Koa(); app.keys = ['some secret hurr']; @@ -70,38 +81,35 @@ console.log('listening on port 3000'); ### Options - The cookie name is controlled by the `key` option, which defaults - to "koa.sess". All other options are passed to `ctx.cookies.get()` and - `ctx.cookies.set()` allowing you to control security, domain, path, - and signing among other settings. +The cookie name is controlled by the `key` option, which defaults +to "koa.sess". All other options are passed to `ctx.cookies.get()` and +`ctx.cookies.set()` allowing you to control security, domain, path, +and signing among other settings. #### Custom `encode/decode` Support - Use `options.encode` and `options.decode` to customize your own encode/decode methods. +Use `options.encode` and `options.decode` to customize your own encode/decode methods. ### Hooks - - `valid()`: valid session value before use it - - `beforeSave()`: hook before save session +- `valid()`: valid session value before use it +- `beforeSave()`: hook before save session ### External Session Stores - The session is stored in a cookie by default, but it has some disadvantages: - - - Session is stored on client side unencrypted - - [Browser cookies always have length limits](http://browsercookielimits.squawky.net/) +The session is stored in a cookie by default, but it has some disadvantages: +- Session is stored on client side unencrypted +- [Browser cookies always have length limits](http://browsercookielimits.squawky.net/) You can store the session content in external stores (Redis, MongoDB or other DBs) by passing `options.store` with three methods (these need to be async functions): - - `get(key, maxAge, { rolling, ctx })`: get session object by key - - `set(key, sess, maxAge, { rolling, changed, ctx })`: set session object for key, with a `maxAge` (in ms) - - `destroy(key, {ctx})`: destroy session for key - +- `get(key, maxAge, { rolling, ctx })`: get session object by key +- `set(key, sess, maxAge, { rolling, changed, ctx })`: set session object for key, with a `maxAge` (in ms) +- `destroy(key, {ctx})`: destroy session for key Once you pass `options.store`, session storage is dependent on your external store -- you can't access the session if your external store is down. **Use external session stores only if necessary, avoid using session as a cache, keep the session lean, and store it in a cookie if possible!** - The way of generating external session id is controlled by the `options.genid(ctx)`, which defaults to `uuid.v4()`. If you want to add prefix for all external session id, you can use `options.prefix`, it will not work if `options.genid(ctx)` present. @@ -125,7 +133,7 @@ External key is used the cookie by default, but you can use `options.externalKey ### Session#isNew - Returns __true__ if the session is new. +Returns **true** if the session is new. ```js if (this.session.isNew) { @@ -137,27 +145,27 @@ if (this.session.isNew) { ### Session#maxAge - Get cookie's maxAge. +Get cookie's maxAge. ### Session#maxAge= - Set cookie's maxAge. +Set cookie's maxAge. ### Session#externalKey - Get session external key, only exist when external session store present. +Get session external key, only exist when external session store present. ### Session#save() - Save this session no matter whether it is populated. +Save this session no matter whether it is populated. ### Session#manuallyCommit() - Session headers are auto committed by default. Use this if `autoCommit` is set to `false`. +Session headers are auto committed by default. Use this if `autoCommit` is set to `false`. ### Destroying a session - To destroy a session simply set it to `null`: +To destroy a session simply set it to `null`: ```js this.session = null; @@ -165,4 +173,10 @@ this.session = null; ## License - MIT +[MIT](LICENSE) + +## Contributors + +[![Contributors](https://contrib.rocks/image?repo=koajs/session)](https://github.com/koajs/session/graphs/contributors) + +Made with [contributors-img](https://contrib.rocks). diff --git a/example.cjs b/example.cjs new file mode 100644 index 0000000..f238ff5 --- /dev/null +++ b/example.cjs @@ -0,0 +1,20 @@ + +const Koa = require('koa'); +const { createSession } = require('./'); + +const app = new Koa(); + +app.keys = [ 'some secret hurr' ]; + +app.use(createSession(app)); + +app.use(async (ctx, next) => { + if (ctx.path === '/favicon.ico') return next(); + + let n = ctx.session.views || 0; + ctx.session.views = ++n; + ctx.body = n + ' views'; +}); + +app.listen(3000); +console.log('listening on port http://localhost:3000'); diff --git a/example.js b/example.js deleted file mode 100644 index ea47cce..0000000 --- a/example.js +++ /dev/null @@ -1,18 +0,0 @@ - -var session = require('./'); -var Koa = require('koa'); -var app = new Koa(); - -app.keys = ['some secret hurr']; - -app.use(session(app)); - -app.use(function* (next){ - if ('/favicon.ico' == this.path) return; - var n = this.session.views || 0; - this.session.views = ++n; - this.body = n + ' views'; -}); - -app.listen(3000); -console.log('listening on port 3000'); diff --git a/index.js b/index.js deleted file mode 100644 index 4d149aa..0000000 --- a/index.js +++ /dev/null @@ -1,151 +0,0 @@ -'use strict'; - -const debug = require('debug')('koa-session'); -const ContextSession = require('./lib/context'); -const util = require('./lib/util'); -const assert = require('assert'); -const uuid = require('uuid'); -const is = require('is-type-of'); - -const CONTEXT_SESSION = Symbol('context#contextSession'); -const _CONTEXT_SESSION = Symbol('context#_contextSession'); - -/** - * Initialize session middleware with `opts`: - * - * - `key` session cookie name ["koa.sess"] - * - all other options are passed as cookie options - * - * @param {Object} [opts] - * @param {Application} app, koa application instance - * @api public - */ - -module.exports = function(opts, app) { - // session(app[, opts]) - if (opts && typeof opts.use === 'function') { - [ app, opts ] = [ opts, app ]; - } - // app required - if (!app || typeof app.use !== 'function') { - throw new TypeError('app instance required: `session(opts, app)`'); - } - - opts = formatOpts(opts); - extendContext(app.context, opts); - - return async function session(ctx, next) { - const sess = ctx[CONTEXT_SESSION]; - if (sess.store) await sess.initFromExternal(); - try { - await next(); - } catch (err) { - throw err; - } finally { - if (opts.autoCommit) { - await sess.commit(); - } - } - }; -}; - -/** - * format and check session options - * @param {Object} opts session options - * @return {Object} new session options - * - * @api private - */ - -function formatOpts(opts) { - opts = opts || {}; - // key - opts.key = opts.key || 'koa.sess'; - - // back-compat maxage - if (!('maxAge' in opts)) opts.maxAge = opts.maxage; - - // defaults - if (opts.overwrite == null) opts.overwrite = true; - if (opts.httpOnly == null) opts.httpOnly = true; - // delete null sameSite config - if (opts.sameSite == null) delete opts.sameSite; - if (opts.signed == null) opts.signed = true; - if (opts.autoCommit == null) opts.autoCommit = true; - - debug('session options %j', opts); - - // setup encoding/decoding - if (typeof opts.encode !== 'function') { - opts.encode = util.encode; - } - if (typeof opts.decode !== 'function') { - opts.decode = util.decode; - } - - const store = opts.store; - if (store) { - assert(is.function(store.get), 'store.get must be function'); - assert(is.function(store.set), 'store.set must be function'); - assert(is.function(store.destroy), 'store.destroy must be function'); - } - - const externalKey = opts.externalKey; - if (externalKey) { - assert(is.function(externalKey.get), 'externalKey.get must be function'); - assert(is.function(externalKey.set), 'externalKey.set must be function'); - } - - const ContextStore = opts.ContextStore; - if (ContextStore) { - assert(is.class(ContextStore), 'ContextStore must be a class'); - assert(is.function(ContextStore.prototype.get), 'ContextStore.prototype.get must be function'); - assert(is.function(ContextStore.prototype.set), 'ContextStore.prototype.set must be function'); - assert(is.function(ContextStore.prototype.destroy), 'ContextStore.prototype.destroy must be function'); - } - - if (!opts.genid) { - if (opts.prefix) opts.genid = () => `${opts.prefix}${uuid.v4()}`; - else opts.genid = uuid.v4; - } - - return opts; -} - -/** - * extend context prototype, add session properties - * - * @param {Object} context koa's context prototype - * @param {Object} opts session options - * - * @api private - */ - -function extendContext(context, opts) { - if (context.hasOwnProperty(CONTEXT_SESSION)) { - return; - } - Object.defineProperties(context, { - [CONTEXT_SESSION]: { - get() { - if (this[_CONTEXT_SESSION]) return this[_CONTEXT_SESSION]; - this[_CONTEXT_SESSION] = new ContextSession(this, opts); - return this[_CONTEXT_SESSION]; - }, - }, - session: { - get() { - return this[CONTEXT_SESSION].get(); - }, - set(val) { - this[CONTEXT_SESSION].set(val); - }, - configurable: true, - }, - sessionOptions: { - get() { - return this[CONTEXT_SESSION].opts; - }, - }, - }); -} diff --git a/lib/session.js b/lib/session.js deleted file mode 100644 index 202acc1..0000000 --- a/lib/session.js +++ /dev/null @@ -1,163 +0,0 @@ -'use strict'; - -/** - * Session model. - */ - -const inspect = Symbol.for('nodejs.util.inspect.custom'); - -class Session { - /** - * Session constructor - * @param {Context} ctx - * @param {Object} obj - * @api private - */ - - constructor(sessionContext, obj, externalKey) { - this._sessCtx = sessionContext; - this._ctx = sessionContext.ctx; - this._externalKey = externalKey; - if (!obj) { - this.isNew = true; - } else { - for (const k in obj) { - // restore maxAge from store - if (k === '_maxAge') this._ctx.sessionOptions.maxAge = obj._maxAge; - else if (k === '_session') this._ctx.sessionOptions.maxAge = 'session'; - else this[k] = obj[k]; - } - } - } - - /** - * JSON representation of the session. - * - * @return {Object} - * @api public - */ - - toJSON() { - const obj = {}; - - Object.keys(this).forEach(key => { - if (key === 'isNew') return; - if (key[0] === '_') return; - obj[key] = this[key]; - }); - - return obj; - } - - /** - * - * alias to `toJSON` - * @api public - */ - - [inspect]() { - return this.toJSON(); - } - - /** - * Return how many values there are in the session object. - * Used to see if it's "populated". - * - * @return {Number} - * @api public - */ - - get length() { - return Object.keys(this.toJSON()).length; - } - - /** - * populated flag, which is just a boolean alias of .length. - * - * @return {Boolean} - * @api public - */ - - get populated() { - return !!this.length; - } - - /** - * get session maxAge - * - * @return {Number} - * @api public - */ - - get maxAge() { - return this._ctx.sessionOptions.maxAge; - } - - /** - * set session maxAge - * - * @param {Number} - * @api public - */ - - set maxAge(val) { - this._ctx.sessionOptions.maxAge = val; - // maxAge changed, must save to cookie and store - this._requireSave = true; - } - - /** - * get session external key - * only exist if opts.store present - */ - get externalKey() { - return this._externalKey; - } - - /** - * save this session no matter whether it is populated - * - * @param {Function} callback the optional function to call after saving the session - * @api public - */ - - save(callback) { - return this.commit({ save: true }, callback); - } - - /** - * regenerate this session - * - * @param {Function} callback the optional function to call after regenerating the session - * @api public - */ - - regenerate(callback) { - return this.commit({ regenerate: true }, callback); - } - - /** - * commit this session's headers if autoCommit is set to false - * - * @api public - */ - - manuallyCommit() { - return this.commit(); - } - - commit(options, callback) { - if (typeof options === 'function') { - callback = options; - options = {}; - } - const promise = this._sessCtx.commit(options); - if (callback) { - promise.then(() => callback(), callback); - } else { - return promise; - } - } -} - -module.exports = Session; diff --git a/lib/util.js b/lib/util.js deleted file mode 100644 index b3a1a28..0000000 --- a/lib/util.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -const crc = require('crc').crc32; - -module.exports = { - - /** - * Decode the base64 cookie value to an object. - * - * @param {String} string - * @return {Object} - * @api private - */ - - decode(string) { - const body = Buffer.from(string, 'base64').toString('utf8'); - const json = JSON.parse(body); - return json; - }, - - /** - * Encode an object into a base64-encoded JSON string. - * - * @param {Object} body - * @return {String} - * @api private - */ - - encode(body) { - body = JSON.stringify(body); - return Buffer.from(body).toString('base64'); - }, - - hash(sess) { - return crc(JSON.stringify(sess)); - }, - - CookieDateEpoch: 'Thu, 01 Jan 1970 00:00:00 GMT', -}; diff --git a/package.json b/package.json index a61dc55..725d6c3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,10 @@ { "name": "koa-session", "description": "Koa cookie session middleware with external store support", - "repository": "koajs/session", + "repository": { + "type": "git", + "url": "git@github.com:koajs/session.git" + }, "version": "6.4.0", "keywords": [ "koa", @@ -9,38 +12,70 @@ "session", "cookie" ], - "files": [ - "index.js", - "lib" - ], "devDependencies": { - "benchmark": "^2.1.4", - "eslint": "3", - "eslint-config-egg": "3", - "istanbul": "0", + "@arethetypeswrong/cli": "^0.17.1", + "@eggjs/bin": "7", + "@eggjs/supertest": "8", + "@eggjs/tsconfig": "1", + "@types/crc": "^3.8.3", + "@types/koa": "^2.15.0", + "@types/mocha": "10", + "@types/node": "22", + "eslint": "8", + "eslint-config-egg": "14", "koa": "2", - "mm": "^2.1.0", - "mocha": "^5.2.0", - "mz-modules": "^2.0.0", - "pedding": "^1.1.0", - "should": "8", - "supertest": "^3.3.0", - "uid-safe": "^2.1.3" + "mm": "4", + "rimraf": "6", + "tshy": "3", + "tshy-after": "1", + "typescript": "5" }, "license": "MIT", "dependencies": { "crc": "^3.8.0", "debug": "^4.3.3", - "is-type-of": "^1.2.1", - "uuid": "^8.3.2" + "is-type-of": "^2.2.0", + "uuid": "^8.3.2", + "zod": "^3.24.1" }, "engines": { - "node": ">=8.0.0" + "node": ">= 18.19.0" }, "scripts": { - "test": "npm run lint && NODE_ENV=test mocha --exit --require should --reporter spec test/*.test.js", - "test-cov": "NODE_ENV=test node ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --exit --require should test/*.test.js", - "ci": "npm run lint && NODE_ENV=test node ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- --exit --require should test/*.test.js", - "lint": "eslint lib test index.js" - } + "lint": "eslint --cache src test --ext .ts", + "pretest": "npm run clean && npm run lint -- --fix", + "test": "egg-bin test", + "preci": "npm run clean && npm run lint", + "ci": "egg-bin cov", + "postci": "npm run prepublishOnly && npm run clean", + "clean": "rimraf dist", + "prepublishOnly": "tshy && tshy-after && attw --pack" + }, + "type": "module", + "tshy": { + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + } + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/commonjs/index.d.ts", + "default": "./dist/commonjs/index.js" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "src" + ], + "types": "./dist/commonjs/index.d.ts", + "main": "./dist/commonjs/index.js", + "module": "./dist/esm/index.js" } diff --git a/lib/context.js b/src/context.ts similarity index 56% rename from lib/context.js rename to src/context.ts index d3efbec..c31a751 100644 --- a/lib/context.js +++ b/src/context.ts @@ -1,52 +1,52 @@ -'use strict'; +import { debuglog } from 'node:util'; +import { Session } from './session.js'; +import util from './util.js'; +import type { SessionOptions } from './index.js'; -const debug = require('debug')('koa-session:context'); -const Session = require('./session'); -const util = require('./util'); +const debug = debuglog('koa-session:context'); const COOKIE_EXP_DATE = new Date(util.CookieDateEpoch); const ONE_DAY = 24 * 60 * 60 * 1000; -class ContextSession { +export class ContextSession { + ctx: any; + app: any; + opts: SessionOptions; + store: SessionOptions['store']; + session: Session | false; + externalKey?: string; + prevHash?: number; + /** * context session constructor - * @api public */ - - constructor(ctx, opts) { + constructor(ctx: any, opts: SessionOptions) { this.ctx = ctx; this.app = ctx.app; - this.opts = Object.assign({}, opts); + this.opts = { ...opts }; this.store = this.opts.ContextStore ? new this.opts.ContextStore(ctx) : this.opts.store; } /** * internal logic of `ctx.session` * @return {Session} session object - * - * @api public */ - - get() { - const session = this.session; + get(): Session | null { // already retrieved - if (session) return session; + if (this.session) return this.session; // unset - if (session === false) return null; + if (this.session === false) return null; // create an empty session or init from cookie this.store ? this.create() : this.initFromCookie(); - return this.session; + return this.session as Session; } /** * internal logic of `ctx.session=` * @param {Object} val session object - * - * @api public */ - - set(val) { + set(val: Record | null) { if (val === null) { this.session = false; return; @@ -63,7 +63,7 @@ class ContextSession { * init session from external store * will be called in the front of session middleware * - * @api public + * @public */ async initFromExternal() { @@ -87,23 +87,23 @@ class ContextSession { return; } - const json = await this.store.get(externalKey, opts.maxAge, { ctx, rolling: opts.rolling }); - if (!this.valid(json, externalKey)) { + const sessionData = await this.store!.get(externalKey, opts.maxAge as number, { ctx, rolling: opts.rolling }); + if (!this.valid(sessionData, externalKey)) { + debug('invalid session data, create a new session'); // create a new `externalKey` this.create(); return; } // create with original `externalKey` - this.create(json, externalKey); - this.prevHash = util.hash(this.session.toJSON()); + this.create(sessionData, externalKey); + this.prevHash = util.hash((this.session as Session).toJSON()); } /** * init session from cookie - * @api private + * @private */ - initFromCookie() { debug('init from cookie'); const ctx = this.ctx; @@ -115,68 +115,67 @@ class ContextSession { return; } - let json; - debug('parse %s', cookie); + let sessionData: Record; + debug('parse cookie: %j', cookie); try { - json = opts.decode(cookie); - } catch (err) { + sessionData = opts.decode(cookie); + } catch (err: unknown) { // backwards compatibility: // create a new session if parsing fails. - // new Buffer(string, 'base64') does not seem to crash + // `Buffer.from(string, 'base64')` does not seem to crash // when `string` is not base64-encoded. // but `JSON.parse(string)` will crash. debug('decode %j error: %s', cookie, err); - if (!(err instanceof SyntaxError)) { + if (err instanceof Error && !(err instanceof SyntaxError)) { // clean this cookie to ensure next request won't throw again ctx.cookies.set(opts.key, '', opts); - // ctx.onerror will unset all headers, and set those specified in err - err.headers = { + // `ctx.onerror` will unset all headers, and set those specified in err + Reflect.set(err, 'headers', { 'set-cookie': ctx.response.get('set-cookie'), - }; + }); throw err; } this.create(); return; } - debug('parsed %j', json); - - if (!this.valid(json)) { + debug('parsed session data: %j', sessionData); + if (!this.valid(sessionData)) { + // create a new session if the session data is invalid this.create(); + debug('invalid session data, create a new session'); return; } // support access `ctx.session` before session middleware - this.create(json); - this.prevHash = util.hash(this.session.toJSON()); + this.create(sessionData); + this.prevHash = util.hash((this.session as Session).toJSON()); } /** - * verify session(expired or ) - * @param {Object} value session object - * @param {Object} key session externalKey(optional) - * @return {Boolean} valid - * @api private + * verify session(expired or custom verification) + * @param {Object} sessionData session data + * @param {Object} [key] session externalKey(optional) + * @private */ - - valid(value, key) { + protected valid(sessionData: Record, key?: string) { const ctx = this.ctx; - if (!value) { - this.emit('missed', { key, value, ctx }); + if (!sessionData) { + this.emit('missed', { key, value: sessionData, ctx }); return false; } - if (value._expire && value._expire < Date.now()) { + if (typeof sessionData._expire === 'number' && sessionData._expire < Date.now()) { debug('expired session'); - this.emit('expired', { key, value, ctx }); + this.emit('expired', { key, value: sessionData, ctx }); return false; } const valid = this.opts.valid; - if (typeof valid === 'function' && !valid(ctx, value)) { + if (typeof valid === 'function' && !valid(ctx, sessionData)) { // valid session value fail, ignore this session debug('invalid session'); - this.emit('invalid', { key, value, ctx }); + this.emit('invalid', { key, value: sessionData, ctx }); return false; } return true; @@ -185,9 +184,9 @@ class ContextSession { /** * @param {String} event event name * @param {Object} data event data - * @api private + * @private */ - emit(event, data) { + emit(event: string, data: unknown) { setImmediate(() => { this.app.emit(`session:${event}`, data); }); @@ -196,45 +195,49 @@ class ContextSession { /** * create a new session and attach to ctx.sess * - * @param {Object} [val] session data + * @param {Object} [sessionData] session data * @param {String} [externalKey] session external key - * @api private */ - - create(val, externalKey) { - debug('create session with val: %j externalKey: %s', val, externalKey); - if (this.store) this.externalKey = externalKey || this.opts.genid && this.opts.genid(this.ctx); - this.session = new Session(this, val, this.externalKey); + protected create(sessionData?: Record, externalKey?: string) { + debug('create session with data: %j, externalKey: %s', sessionData, externalKey); + if (this.store) { + this.externalKey = externalKey ?? this.opts.genid?.(this.ctx); + } + this.session = new Session(this, sessionData, this.externalKey); } /** * Commit the session changes or removal. - * - * @api public */ - async commit({ save = false, regenerate = false } = {}) { const session = this.session; const opts = this.opts; const ctx = this.ctx; // not accessed - if (undefined === session) return; + if (session === undefined) { + return; + } // removed if (session === false) { await this.remove(); return; } + if (regenerate) { await this.remove(); - if (this.store) this.externalKey = opts.genid && opts.genid(ctx); + if (this.store) { + this.externalKey = opts.genid?.(ctx); + } } // force save session when `session._requireSave` set const reason = save || regenerate || session._requireSave ? 'force' : this._shouldSaveSession(); - debug('should save session: %s', reason); - if (!reason) return; + debug('should save session: %j', reason); + if (!reason) { + return; + } if (typeof opts.beforeSave === 'function') { debug('before save'); @@ -246,81 +249,90 @@ class ContextSession { _shouldSaveSession() { const prevHash = this.prevHash; - const session = this.session; + const session = this.session as Session; // do nothing if new and not populated - const json = session.toJSON(); - if (!prevHash && !Object.keys(json).length) return ''; + const sessionData = session.toJSON(); + if (!prevHash && !Object.keys(sessionData).length) { + return ''; + } // save if session changed - const changed = prevHash !== util.hash(json); - if (changed) return 'changed'; + const changed = prevHash !== util.hash(sessionData); + if (changed) { + return 'changed'; + } // save if opts.rolling set - if (this.opts.rolling) return 'rolling'; + if (this.opts.rolling) { + return 'rolling'; + } // save if opts.renew and session will expired if (this.opts.renew) { const expire = session._expire; const maxAge = session.maxAge; // renew when session will expired in maxAge / 2 - if (expire && maxAge && expire - Date.now() < maxAge / 2) return 'renew'; + if (expire && maxAge && expire - Date.now() < maxAge / 2) { + return 'renew'; + } } + // don't save return ''; } /** * remove session - * @api private + * @private */ - async remove() { // Override the default options so that we can properly expire the session cookies - const opts = Object.assign({}, this.opts, { + const opts = { + ...this.opts, expires: COOKIE_EXP_DATE, maxAge: false, - }); - + }; const ctx = this.ctx; const key = opts.key; const externalKey = this.externalKey; - if (externalKey) await this.store.destroy(externalKey, { ctx }); + if (externalKey) { + await this.store!.destroy(externalKey, { ctx }); + } ctx.cookies.set(key, '', opts); } /** * save session - * @api private + * @private */ - - async save(changed) { + async save(changed: boolean) { const opts = this.opts; const key = opts.key; const externalKey = this.externalKey; - let json = this.session.toJSON(); + const sessionData = (this.session as Session).toJSON(); // set expire for check let maxAge = opts.maxAge ? opts.maxAge : ONE_DAY; if (maxAge === 'session') { // do not set _expire in json if maxAge is set to 'session' // also delete maxAge from options opts.maxAge = undefined; - json._session = true; + sessionData._session = true; } else { // set expire for check - json._expire = maxAge + Date.now(); - json._maxAge = maxAge; + sessionData._expire = maxAge + Date.now(); + sessionData._maxAge = maxAge; } // save to external store if (externalKey) { - debug('save %j to external key %s', json, externalKey); + debug('save %j to external key %s', sessionData, externalKey); if (typeof maxAge === 'number') { // ensure store expired after cookie maxAge += 10000; } - await this.store.set(externalKey, json, maxAge, { + await this.store!.set(externalKey, sessionData, maxAge as number, { changed, ctx: this.ctx, rolling: opts.rolling, @@ -333,13 +345,11 @@ class ContextSession { return; } - // save to cookie - debug('save %j to cookie', json); - json = opts.encode(json); - debug('save %s', json); - - this.ctx.cookies.set(key, json, opts); + // save to cookie with base64 encode string + debug('save session data %j to cookie', sessionData); + const base64String = opts.encode(sessionData); + debug('save session data json base64 format: %s to cookie key: %s with options: %j', + base64String, key, opts); + this.ctx.cookies.set(key, base64String, opts); } } - -module.exports = ContextSession; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..0652ffe --- /dev/null +++ b/src/index.ts @@ -0,0 +1,305 @@ +import assert from 'node:assert'; +import { debuglog } from 'node:util'; +import { randomUUID } from 'node:crypto'; +import { isClass } from 'is-type-of'; +import z from 'zod'; +import { ContextSession } from './context.js'; +import util from './util.js'; + +const debug = debuglog('koa-session'); + +const GET_CONTEXT_SESSION = Symbol('get contextSession'); +const CONTEXT_SESSION_INSTANCE = Symbol('contextSession instance'); + +const SessionOptionsSchema = z.object({ + /** + * cookie key + * Default is `koa.sess` + */ + key: z.string().default('koa.sess'), + /** + * maxAge in ms + * Default is `86400000`, one day + * If set to 'session' will result in a cookie that expires when session/browser is closed + * + * Warning: If a session cookie is stolen, this cookie will never expire + */ + maxAge: z.union([ z.number(), z.literal('session') ]).optional(), + /** + * automatically commit headers + * Default is `true` + */ + autoCommit: z.boolean().default(true), + /** + * cookie value can overwrite or not + * Default is `true` + */ + overwrite: z.boolean().default(true), + /** + * httpOnly or not + * Default is `true` + */ + httpOnly: z.boolean().default(true), + /** + * signed or not + * Default is `true` + */ + signed: z.boolean().default(true), + /** + * Force a session identifier cookie to be set on every response. + * The expiration is reset to the original `maxAge`, resetting the expiration countdown. + * Default is `false` + */ + rolling: z.boolean().default(false), + /** + * renew session when session is nearly expired, so we can always keep user logged in. + * Default is `false` + */ + renew: z.boolean().default(false), + /** + * secure cookie + * Default is `undefined`, will be set to `true` if the connection is over HTTPS, otherwise `false`. + */ + secure: z.boolean().optional(), + /** + * session cookie sameSite options + * Default is `undefined`, meaning don't set it + */ + sameSite: z.string().optional(), + /** + * External key is used the cookie by default, + * but you can use `options.externalKey` to customize your own external key methods. + */ + externalKey: z.object({ + /** + * get the external key + * `(ctx) => string` + */ + get: z.function() + .args(z.any()) + .returns(z.string()), + /** + * set the external key + * `(ctx, key) => void` + */ + set: z.function() + .args(z.any(), z.string()) + .returns(z.void()), + }).optional(), + /** + * session storage is dependent on your external store + */ + store: z.object({ + /** + * get session data by key + * `(key, maxAge, { rolling, ctx }) => sessionData | Promise` + */ + get: z.function() + .args(z.string(), z.number(), z.object({ rolling: z.boolean(), ctx: z.any() })) + .returns(z.promise(z.any())), + /** + * set session data for key, with a `maxAge` (in ms) + * `(key, sess, maxAge, { rolling, changed, ctx }) => void | Promise` + */ + set: z.function() + .args(z.string(), z.any(), z.number(), z.object({ rolling: z.boolean(), changed: z.boolean(), ctx: z.any() })) + .returns(z.promise(z.void())), + /** + * destroy session data for key + * `(key, { ctx })=> void | Promise` + */ + destroy: z.function() + .args(z.string(), z.object({ ctx: z.any() })) + .returns(z.promise(z.void())), + }).optional(), + /** + * If your session store requires data or utilities from context, `opts.ContextStore` is also supported. + * `ContextStore` must be a class which claims three instance methods demonstrated above. + * `new ContextStore(ctx)` will be executed on every request. + */ + ContextStore: z.any().optional(), + encode: z.function() + .args(z.any()) + .returns(z.string()) + .optional() + .default(() => util.encode), + decode: z.function() + .args(z.string()) + .returns(z.any()) + .default(() => util.decode), + /** + * If you want to generate a new session id, you can use `genid` option to customize it. + * Default is a function that uses `randomUUID()`. + * `(ctx) => string` + */ + genid: z.function() + .args(z.any()) + .returns(z.string()) + .optional(), + /** + * If you want to prefix the session id, you can use `prefix` option to customize it. + * It will not work if `options.genid(ctx)` present. + */ + prefix: z.string().optional(), + /** + * valid session value before use it + * `(ctx, sessionData) => boolean` + */ + valid: z.function() + .args(z.any(), z.any()) + .returns(z.any()) + .optional(), + /** + * hook before save session + * `(ctx, sessionModel) => void` + */ + beforeSave: z.function() + .args(z.any(), z.any()) + .returns(z.void()) + .optional(), +}); + +const DEFAULT_SESSION_OPTIONS = SessionOptionsSchema.parse({}); + +export type SessionOptions = z.infer; +export type CreateSessionOptions = Partial; + +type Middleware = (ctx: any, next: any) => Promise; + +/** + * Initialize session middleware with `opts`: + * + * - `key` session cookie name ["koa.sess"] + * - all other options are passed as cookie options + * + * @param {Object} [opts] session options + * @param {Application} app koa application instance + * @public + */ +export function createSession(opts: CreateSessionOptions, app: any): Middleware; +export function createSession(app: any, opts?: CreateSessionOptions): Middleware; +export function createSession(opts: CreateSessionOptions | any, app: any): Middleware { + // session(app[, opts]) + if (opts && 'use' in opts && typeof opts.use === 'function') { + [ app, opts ] = [ opts, app ]; + } + // app required + if (typeof app?.use !== 'function') { + throw new TypeError('app instance required: `session(opts, app)`'); + } + + // back-compat maxage + if (opts && !('maxAge' in opts) && 'maxage' in opts) { + Reflect.set(opts, 'maxAge', Reflect.get(opts, 'maxage')); + if (process.env.NODE_ENV !== 'production') { + console.warn('DeprecationWarning: `maxage` option has been renamed to `maxAge`'); + } + } + let options = { + ...DEFAULT_SESSION_OPTIONS, + ...opts, + }; + SessionOptionsSchema.parse(options); + options = formatOptions(options); + extendContext(app.context, options); + + return async function session(ctx: any, next: any) { + const sess = ctx[GET_CONTEXT_SESSION]; + if (sess.store) { + await sess.initFromExternal(); + } + try { + await next(); + } catch (err) { + throw err; + } finally { + if (options.autoCommit) { + await sess.commit(); + } + } + }; +} + +// Usage: `import session from 'koa-session'` +export default createSession; + +/** + * format and check session options + */ +function formatOptions(opts: SessionOptions) { + // defaults + if (opts.overwrite == null) opts.overwrite = true; + if (opts.httpOnly == null) opts.httpOnly = true; + // delete null sameSite config + if (opts.sameSite == null) delete opts.sameSite; + if (opts.signed == null) opts.signed = true; + if (opts.autoCommit == null) opts.autoCommit = true; + + debug('session options %j', opts); + const store = opts.store; + if (store) { + assert(typeof store.get === 'function', 'store.get must be function'); + assert(typeof store.set === 'function', 'store.set must be function'); + assert(typeof store.destroy === 'function', 'store.destroy must be function'); + } + + const externalKey = opts.externalKey; + if (externalKey) { + assert(typeof externalKey.get === 'function', 'externalKey.get must be function'); + assert(typeof externalKey.set === 'function', 'externalKey.set must be function'); + } + + const ContextStore = opts.ContextStore; + if (ContextStore) { + assert(isClass(ContextStore), 'ContextStore must be a class'); + assert(typeof ContextStore.prototype.get === 'function', 'ContextStore.prototype.get must be function'); + assert(typeof ContextStore.prototype.set === 'function', 'ContextStore.prototype.set must be function'); + assert(typeof ContextStore.prototype.destroy === 'function', 'ContextStore.prototype.destroy must be function'); + } + + if (!opts.genid) { + if (opts.prefix) { + opts.genid = () => `${opts.prefix}${randomUUID()}`; + } else { + opts.genid = () => randomUUID(); + } + } + return opts; +} + +/** + * extend context prototype, add session properties + * + * @param {Object} context koa's context prototype + * @param {Object} opts session options + */ +function extendContext(context: object, opts: SessionOptions) { + if (context.hasOwnProperty(GET_CONTEXT_SESSION)) { + return; + } + Object.defineProperties(context, { + [GET_CONTEXT_SESSION]: { + get() { + if (this[CONTEXT_SESSION_INSTANCE]) { + return this[CONTEXT_SESSION_INSTANCE]; + } + this[CONTEXT_SESSION_INSTANCE] = new ContextSession(this, opts); + return this[CONTEXT_SESSION_INSTANCE]; + }, + }, + session: { + get() { + return this[GET_CONTEXT_SESSION].get(); + }, + set(val) { + this[GET_CONTEXT_SESSION].set(val); + }, + configurable: true, + }, + sessionOptions: { + get() { + return this[GET_CONTEXT_SESSION].opts; + }, + }, + }); +} diff --git a/src/session.ts b/src/session.ts new file mode 100644 index 0000000..024b553 --- /dev/null +++ b/src/session.ts @@ -0,0 +1,139 @@ +import { inspect } from 'node:util'; +import type { ContextSession } from './context.js'; + +type Callback = (err?: Error) => void; + +/** + * Session model + */ +export class Session { + #sessCtx: ContextSession; + #ctx: any; + #externalKey?: string; + isNew = false; + _requireSave = false; + // session expire time, will be set from sessionData + _expire?: number; + + constructor(sessionContext: ContextSession, sessionData?: Record, externalKey?: string) { + this.#sessCtx = sessionContext; + this.#ctx = sessionContext.ctx; + this.#externalKey = externalKey; + if (!sessionData) { + this.isNew = true; + } else { + for (const k in sessionData) { + // restore maxAge from store + if (k === '_maxAge') { + this.#ctx.sessionOptions.maxAge = sessionData._maxAge; + } else if (k === '_session') { + // set maxAge to 'session' if it's a session lifetime + this.#ctx.sessionOptions.maxAge = 'session'; + } else { + Reflect.set(this, k, sessionData[k]); + } + } + } + } + + /** + * JSON representation of the session. + */ + toJSON() { + const obj: Record = {}; + for (const key in this) { + if (key === 'isNew') continue; + // skip private stuff + if (key[0] === '_') continue; + const value = this[key]; + // skip functions + if (typeof value === 'function') continue; + obj[key] = value; + } + return obj; + } + + /** + * alias to `toJSON` + */ + [inspect.custom]() { + return this.toJSON(); + } + + /** + * Return how many values there are in the session object. + * Used to see if it's "populated". + */ + get length() { + return Object.keys(this.toJSON()).length; + } + + /** + * populated flag, which is just a boolean alias of .length. + */ + get populated() { + return !!this.length; + } + + /** + * get session maxAge + */ + get maxAge(): number { + return this.#ctx.sessionOptions.maxAge; + } + + /** + * set session maxAge + */ + set maxAge(val: number) { + this.#ctx.sessionOptions.maxAge = val; + // maxAge changed, must save to cookie and store + this._requireSave = true; + } + + /** + * get session external key + * only exist if opts.store present + */ + get externalKey() { + return this.#externalKey; + } + + /** + * save this session no matter whether it is populated + * + * @param {Function} [callback] the optional function to call after saving the session + */ + save(callback?: Callback) { + return this.commit({ save: true }, callback); + } + + /** + * regenerate this session + * + * @param {Function} [callback] the optional function to call after regenerating the session + */ + regenerate(callback?: Callback) { + return this.commit({ regenerate: true }, callback); + } + + /** + * commit this session's headers if autoCommit is set to false + */ + manuallyCommit() { + return this.commit(); + } + + commit(options?: any, callback?: Callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + const promise = this.#sessCtx.commit(options); + if (callback) { + promise.then(() => callback(), callback); + } else { + return promise; + } + } +} diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..39f0806 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,26 @@ +import crc from 'crc'; + +export default { + /** + * Decode the base64 cookie value to an object + * @private + */ + decode(base64String: string): Record { + const body = Buffer.from(base64String, 'base64').toString('utf8'); + const json = JSON.parse(body); + return json; + }, + + /** + * Encode an object into a base64-encoded JSON string + */ + encode(data: Record) { + return Buffer.from(JSON.stringify(data)).toString('base64'); + }, + + hash(data: Record) { + return crc.crc32(JSON.stringify(data)); + }, + + CookieDateEpoch: 'Thu, 01 Jan 1970 00:00:00 GMT', +}; diff --git a/test/context_store.js b/test/context_store.js deleted file mode 100644 index 8a053eb..0000000 --- a/test/context_store.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -// this is a stupid nonsense example just to test - -const sessions = {}; - -class ContextStore { - constructor(ctx) { - this.ctx = ctx; - } - - async get(key) { - return sessions[key]; - } - - async set(key, value) { - sessions[key] = value; - } - - async destroy(key) { - sessions[key] = undefined; - } -} - -module.exports = ContextStore; diff --git a/test/context_store.ts b/test/context_store.ts new file mode 100644 index 0000000..fb15761 --- /dev/null +++ b/test/context_store.ts @@ -0,0 +1,22 @@ +// this is a stupid nonsense example just to test + +const sessions: Record = {}; + +export default class ContextStore { + ctx: any; + constructor(ctx: any) { + this.ctx = ctx; + } + + async get(key: string) { + return sessions[key]; + } + + async set(key: string, value: unknown) { + sessions[key] = value; + } + + async destroy(key: string) { + sessions[key] = undefined; + } +} diff --git a/test/contextstore.test.js b/test/contextstore.test.js deleted file mode 100644 index b31fada..0000000 --- a/test/contextstore.test.js +++ /dev/null @@ -1,758 +0,0 @@ -'use strict'; - -const Koa = require('koa'); -const request = require('supertest'); -const should = require('should'); -const mm = require('mm'); -const session = require('..'); -const ContextStore = require('./context_store'); - -const inspect = Symbol.for('nodejs.util.inspect.custom'); - -describe('Koa Session External Context Store', () => { - let cookie; - - describe('when the session contains a ;', () => { - it('should still work', done => { - const app = App(); - - app.use(async function(ctx) { - if (ctx.method === 'POST') { - ctx.session.string = ';'; - ctx.status = 204; - } else { - ctx.body = ctx.session.string; - } - }); - - const server = app.listen(); - - request(server) - .post('/') - .expect(204, (err, res) => { - if (err) return done(err); - const cookie = res.headers['set-cookie']; - request(server) - .get('/') - .set('Cookie', cookie.join(';')) - .expect(';', done); - }); - }); - }); - - describe('new session', () => { - describe('when not accessed', () => { - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.body = 'greetings'; - }); - - request(app.listen()) - .get('/') - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - - describe('when accessed and not populated', () => { - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session; - ctx.body = 'greetings'; - }); - - request(app.listen()) - .get('/') - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - - describe('when populated', () => { - it('should Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.message = 'hello'; - ctx.body = ''; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess/) - .expect(200, (err, res) => { - if (err) return done(err); - cookie = res.header['set-cookie'].join(';'); - cookie.indexOf('_suffix').should.greaterThan(1); - done(); - }); - }); - - it('should pass sid to middleware', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.message = 'hello'; - ctx.state.sid.indexOf('_suffix').should.greaterThan(1); - ctx.body = ''; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess/) - .expect(200, (err, res) => { - if (err) return done(err); - cookie = res.header['set-cookie'].join(';'); - cookie.indexOf('_suffix').should.greaterThan(1); - done(); - }); - }); - - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.body = ctx.session; - }); - - request(app.listen()) - .get('/') - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - }); - - describe('saved session', () => { - describe('when not accessed', () => { - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.body = 'aklsdjflasdjf'; - }); - - request(app.listen()) - .get('/') - .set('Cookie', cookie) - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - - describe('when accessed but not changed', () => { - it('should be the same session', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.message.should.equal('hello'); - ctx.body = 'aklsdjflasdjf'; - }); - - request(app.listen()) - .get('/') - .set('Cookie', cookie) - .expect(200, done); - }); - - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.message.should.equal('hello'); - ctx.body = 'aklsdjflasdjf'; - }); - - request(app.listen()) - .get('/') - .set('Cookie', cookie) - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - - describe('when accessed and changed', () => { - it('should Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.money = '$$$'; - ctx.body = 'aklsdjflasdjf'; - }); - - request(app.listen()) - .get('/') - .set('Cookie', cookie) - .expect('Set-Cookie', /koa\.sess/) - .expect(200, done); - }); - }); - }); - - describe('when session is', () => { - describe('null', () => { - it('should expire the session', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session = null; - ctx.body = 'asdf'; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess/) - .expect(200, done); - }); - }); - - describe('an empty object', () => { - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session = {}; - ctx.body = 'asdf'; - }); - - request(app.listen()) - .get('/') - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - - describe('an object', () => { - it('should create a session', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session = { message: 'hello' }; - ctx.body = 'asdf'; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess/) - .expect(200, done); - }); - }); - - describe('anything else', () => { - it('should throw', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session = 'asdf'; - }); - - request(app.listen()) - .get('/') - .expect(500, done); - }); - }); - }); - - describe('session', () => { - describe('.inspect()', () => { - it('should return session content', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.foo = 'bar'; - ctx.body = ctx.session[inspect](); - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess=.+;/) - .expect({ foo: 'bar' }) - .expect(200, done); - }); - }); - - describe('.length', () => { - it('should return session length', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.foo = 'bar'; - ctx.body = String(ctx.session.length); - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess=.+;/) - .expect('1') - .expect(200, done); - }); - }); - - describe('.populated', () => { - it('should return session populated', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.foo = 'bar'; - ctx.body = String(ctx.session.populated); - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess=.+;/) - .expect('true') - .expect(200, done); - }); - }); - - describe('.save()', () => { - it('should save session', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.save(); - ctx.body = 'hello'; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess=.+;/) - .expect('hello') - .expect(200, done); - }); - }); - }); - - describe('when an error is thrown downstream and caught upstream', () => { - it('should still save the session', done => { - const app = new Koa(); - - app.keys = [ 'a', 'b' ]; - - app.use(async function(ctx, next) { - try { - await next(); - } catch (err) { - ctx.status = err.status; - ctx.body = err.message; - } - }); - - app.use(session({ ContextStore }, app)); - - app.use(async function(ctx, next) { - ctx.session.name = 'funny'; - await next(); - }); - - app.use(async function(ctx) { - ctx.throw(401); - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess/) - .expect(401, done); - }); - }); - - describe('when autoCommit is present', () => { - describe('and set to false', () => { - it('should not set headers if manuallyCommit() isn\'t called', done => { - const app = App({ autoCommit: false }); - app.use(async function(ctx) { - if (ctx.method === 'POST') { - ctx.session.message = 'hi'; - ctx.body = 200; - return; - } - ctx.body = ctx.session.message; - }); - const server = app.listen(); - - request(server) - .post('/') - .end((err, res) => { - if (err) return done(err); - const cookie = res.headers['set-cookie']; - should.not.exist(cookie); - }) - .expect(200, done); - }); - it('should set headers if manuallyCommit() is called', done => { - const app = App({ autoCommit: false }); - app.use(async function(ctx, next) { - if (ctx.method === 'POST') { - ctx.session.message = 'dummy'; - } - await next(); - }); - app.use(async function(ctx) { - ctx.body = 200; - await ctx.session.manuallyCommit(); - }); - const server = app.listen(); - - request(server) - .post('/') - .expect('Set-Cookie', /koa\.sess/) - .end(err => { - if (err) return done(err); - }) - .expect(200, done); - }); - }); - }); - - describe('when maxAge present', () => { - describe('and set to be a session cookie', () => { - it('should not expire the session', done => { - const app = App({ maxAge: 'session' }); - - app.use(async function(ctx) { - if (ctx.method === 'POST') { - ctx.session.message = 'hi'; - ctx.body = 200; - return; - } - ctx.body = ctx.session.message; - }); - const server = app.listen(); - - request(server) - .post('/') - .expect('Set-Cookie', /koa\.sess/) - .end((err, res) => { - if (err) return done(err); - const cookie = res.headers['set-cookie'].join(';'); - cookie.should.not.containEql('expires='); - request(server) - .get('/') - .set('cookie', cookie) - .expect('hi', done); - }); - }); - it('should not expire the session after multiple session changes', done => { - const app = App({ maxAge: 'session' }); - - app.use(async function(ctx) { - ctx.session.count = (ctx.session.count || 0) + 1; - ctx.body = `hi ${ctx.session.count}`; - }); - const server = app.listen(); - - request(server) - .get('/') - .expect('Set-Cookie', /koa\.sess/) - .expect('hi 1') - .end((err, res) => { - if (err) return done(err); - let cookie = res.headers['set-cookie'].join(';'); - cookie.should.not.containEql('expires='); - - request(server) - .get('/') - .set('cookie', cookie) - .expect('Set-Cookie', /koa\.sess/) - .expect('hi 2') - .end((err, res) => { - if (err) return done(err); - cookie = res.headers['set-cookie'].join(';'); - cookie.should.not.containEql('expires='); - - request(server) - .get('/') - .set('cookie', cookie) - .expect('Set-Cookie', /koa\.sess/) - .expect('hi 3') - .end((err, res) => { - if (err) return done(err); - cookie = res.headers['set-cookie'].join(';'); - cookie.should.not.containEql('expires='); - - done(); - }); - }); - }); - }); - it('should use the default maxAge when improper string given', done => { - const app = App({ maxAge: 'not the right string' }); - - app.use(async function(ctx) { - if (ctx.method === 'POST') { - ctx.session.message = 'hi'; - ctx.body = 200; - return; - } - ctx.body = ctx.session.message; - }); - const server = app.listen(); - - request(server) - .post('/') - .expect('Set-Cookie', /koa\.sess/) - .end((err, res) => { - if (err) return done(err); - const cookie = res.headers['set-cookie'].join(';'); - cookie.should.containEql('expires='); - request(server) - .get('/') - .set('cookie', cookie) - .expect('hi', done); - }); - }); - }); - describe('and not expire', () => { - it('should not expire the session', done => { - const app = App({ maxAge: 100 }); - - app.use(async function(ctx) { - if (ctx.method === 'POST') { - ctx.session.message = 'hi'; - ctx.body = 200; - return; - } - ctx.body = ctx.session.message; - }); - - const server = app.listen(); - - request(server) - .post('/') - .expect('Set-Cookie', /koa\.sess/) - .end((err, res) => { - if (err) return done(err); - const cookie = res.headers['set-cookie'].join(';'); - - request(server) - .get('/') - .set('cookie', cookie) - .expect('hi', done); - }); - }); - }); - - describe('and expired', () => { - it('should expire the sess', done => { - const app = App({ maxAge: 100 }); - - app.use(async function(ctx) { - if (ctx.method === 'POST') { - ctx.session.message = 'hi'; - ctx.status = 200; - return; - } - - ctx.body = ctx.session.message || ''; - }); - - const server = app.listen(); - - request(server) - .post('/') - .expect('Set-Cookie', /koa\.sess/) - .end((err, res) => { - if (err) return done(err); - const cookie = res.headers['set-cookie'].join(';'); - - setTimeout(() => { - request(server) - .get('/') - .set('cookie', cookie) - .expect('', done); - }, 200); - }); - }); - }); - }); - - describe('ctx.session.maxAge', () => { - it('should return opt.maxAge', done => { - const app = App({ maxAge: 100 }); - - app.use(async function(ctx) { - ctx.body = ctx.session.maxAge; - }); - - request(app.listen()) - .get('/') - .expect('100', done); - }); - }); - - describe('ctx.session.maxAge=', () => { - it('should set sessionOptions.maxAge', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.foo = 'bar'; - ctx.session.maxAge = 100; - ctx.body = ctx.session.foo; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /expires=/) - .expect(200, done); - }); - - it('should save even session not change', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.maxAge = 100; - ctx.body = ctx.session; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /expires=/) - .expect(200, done); - }); - - it('should save when create session only with maxAge', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session = { maxAge: 100 }; - ctx.body = ctx.session; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /expires=/) - .expect(200, done); - }); - }); - - describe('when store return empty', () => { - it('should create new Session', done => { - const app = App({ signed: false }); - - app.use(async function(ctx) { - ctx.body = String(ctx.session.isNew); - }); - - request(app.listen()) - .get('/') - .set('cookie', 'koa.sess=invalid-key') - .expect('true') - .expect(200, done); - }); - }); - - describe('when valid and beforeSave set', () => { - it('should ignore session when uid changed', done => { - const app = new Koa(); - - app.keys = [ 'a', 'b' ]; - app.use(session({ - valid(ctx, sess) { - return ctx.cookies.get('uid') === sess.uid; - }, - beforeSave(ctx, sess) { - sess.uid = ctx.cookies.get('uid'); - }, - ContextStore, - }, app)); - app.use(async function(ctx) { - if (!ctx.session.foo) { - ctx.session.foo = Date.now() + '|uid:' + ctx.cookies.get('uid'); - } - - ctx.body = { - foo: ctx.session.foo, - uid: ctx.cookies.get('uid'), - }; - }); - - request(app.callback()) - .get('/') - .set('Cookie', 'uid=123') - .expect(200, (err, res) => { - should.not.exist(err); - const data = res.body; - const cookies = res.headers['set-cookie'].join(';'); - cookies.should.containEql('koa.sess='); - - request(app.callback()) - .get('/') - .set('Cookie', cookies + ';uid=123') - .expect(200) - .expect(data, err => { - should.not.exist(err); - - // should ignore uid:123 session and create a new session for uid:456 - request(app.callback()) - .get('/') - .set('Cookie', cookies + ';uid=456') - .expect(200, (err, res) => { - should.not.exist(err); - res.body.uid.should.equal('456'); - res.body.foo.should.not.equal(data.foo); - done(); - }); - }); - }); - }); - }); - - describe('ctx.session', () => { - after(mm.restore); - - it('can be mocked', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.body = ctx.session; - }); - - mm(app.context, 'session', { - foo: 'bar', - }); - - request(app.listen()) - .get('/') - .expect({ - foo: 'bar', - }) - .expect(200, done); - }); - }); -}); - -function App(options) { - const app = new Koa(); - app.keys = [ 'a', 'b' ]; - options = options || {}; - options.ContextStore = ContextStore; - options.genid = ctx => { - const sid = Date.now() + '_suffix'; - ctx.state.sid = sid; - return sid; - }; - app.use(session(options, app)); - return app; -} diff --git a/test/contextstore.test.ts b/test/contextstore.test.ts new file mode 100644 index 0000000..e77f295 --- /dev/null +++ b/test/contextstore.test.ts @@ -0,0 +1,677 @@ +import { strict as assert } from 'node:assert'; +import { scheduler } from 'node:timers/promises'; +import Koa from 'koa'; +import { request } from '@eggjs/supertest'; +import { mm } from 'mm'; +import session, { type CreateSessionOptions } from '../src/index.js'; +import ContextStore from './context_store.js'; + +const inspect = Symbol.for('nodejs.util.inspect.custom'); + +function App(options: CreateSessionOptions = {}) { + const app = new Koa(); + app.keys = [ 'a', 'b' ]; + options.ContextStore = ContextStore; + options.genid = ctx => { + const sid = Date.now() + '_suffix'; + ctx.state.sid = sid; + return sid; + }; + app.use(session(options, app)); + return app; +} + +describe('Koa Session External Context Store', () => { + let cookie: string; + + describe('when the session contains a ;', () => { + it('should still work', async () => { + const app = App(); + + app.use(async function(ctx) { + if (ctx.method === 'POST') { + ctx.session.string = ';'; + ctx.status = 204; + } else { + ctx.body = ctx.session.string; + } + }); + + const res = await request(app.callback()) + .post('/') + .expect(204); + const cookie = res.get('Set-Cookie')!; + assert(cookie, 'should have set cookie'); + await request(app.callback()) + .get('/') + .set('Cookie', cookie.join(';')) + .expect(';'); + }); + }); + + describe('new session', () => { + describe('when not accessed', () => { + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.body = 'greetings'; + }); + + const res = await request(app.callback()) + .get('/') + .expect(200); + assert.equal(res.get('Set-Cookie'), undefined, 'should not have set cookie'); + }); + }); + + describe('when accessed and not populated', () => { + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.session; + ctx.body = 'greetings'; + }); + + const res = await request(app.callback()) + .get('/') + .expect(200); + assert.equal(res.get('Set-Cookie'), undefined, 'should not have set cookie'); + }); + }); + + describe('when populated', () => { + it('should Set-Cookie', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.session.message = 'hello'; + ctx.body = ''; + }); + + const res = await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + cookie = res.get('Set-Cookie')!.join(';'); + assert.match(cookie, /\d+_suffix/); + }); + + it('should pass sid to middleware', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.session.message = 'hello'; + assert.match(ctx.state.sid, /\d+_suffix/); + ctx.body = ''; + }); + + const res = await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + + cookie = res.get('Set-Cookie')!.join(';'); + assert.match(cookie, /\d+_suffix/); + }); + + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.body = ctx.session; + }); + + const res = await request(app.callback()) + .get('/') + .expect(200); + assert.equal(res.get('Set-Cookie'), undefined, 'should not have set cookie'); + }); + }); + }); + + describe('saved session', () => { + describe('when not accessed', () => { + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.body = 'aklsdjflasdjf'; + }); + + const res = await request(app.callback()) + .get('/') + .set('Cookie', cookie) + .expect(200); + assert.equal(res.get('Set-Cookie'), undefined, 'should not have set cookie'); + }); + }); + + describe('when accessed but not changed', () => { + it('should be the same session', async () => { + const app = App(); + + app.use(async function(ctx) { + assert.equal(ctx.session.message, 'hello'); + ctx.body = 'aklsdjflasdjf'; + }); + + await request(app.callback()) + .get('/') + .set('Cookie', cookie) + .expect(200); + }); + + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async function(ctx) { + assert.equal(ctx.session.message, 'hello'); + ctx.body = 'aklsdjflasdjf'; + }); + + const res = await request(app.callback()) + .get('/') + .set('Cookie', cookie) + .expect(200); + assert.equal(res.get('Set-Cookie'), undefined, 'should not have set cookie'); + }); + }); + + describe('when accessed and changed', () => { + it('should Set-Cookie', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.session.money = '$$$'; + ctx.body = 'aklsdjflasdjf'; + }); + + await request(app.callback()) + .get('/') + .set('Cookie', cookie) + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + }); + }); + }); + + describe('when session is', () => { + describe('null', () => { + it('should expire the session', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.session = null; + ctx.body = 'asdf'; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess/); + }); + }); + + describe('an empty object', () => { + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.session = {}; + ctx.body = 'asdf'; + }); + + const res = await request(app.callback()) + .get('/') + .expect(200); + assert.equal(res.get('Set-Cookie'), undefined, 'should not have set cookie'); + }); + }); + + describe('an object', () => { + it('should create a session', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.session = { message: 'hello' }; + ctx.body = 'asdf'; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + }); + }); + + describe('anything else', () => { + it('should throw', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.session = 'asdf'; + }); + + await request(app.callback()) + .get('/') + .expect(500); + }); + }); + }); + + describe('session', () => { + describe('.inspect()', () => { + it('should return session content', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.session.foo = 'bar'; + ctx.body = ctx.session[inspect](); + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess=.+;/) + .expect({ foo: 'bar' }) + .expect(200); + }); + }); + + describe('.length', () => { + it('should return session length', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.session.foo = 'bar'; + ctx.body = String(ctx.session.length); + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess=.+;/) + .expect('1') + .expect(200); + }); + }); + + describe('.populated', () => { + it('should return session populated', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.session.foo = 'bar'; + ctx.body = String(ctx.session.populated); + }); + + await request(app.listen()) + .get('/') + .expect('Set-Cookie', /koa\.sess=.+;/) + .expect('true') + .expect(200); + }); + }); + + describe('.save()', () => { + it('should save session', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.session.save(); + ctx.body = 'hello'; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess=.+;/) + .expect('hello') + .expect(200); + }); + }); + }); + + describe('when an error is thrown downstream and caught upstream', () => { + it('should still save the session', async () => { + const app = new Koa(); + + app.keys = [ 'a', 'b' ]; + + app.use(async function(ctx, next) { + try { + await next(); + } catch (err: any) { + ctx.status = err.status; + ctx.body = err.message; + } + }); + + app.use(session({ ContextStore }, app)); + + app.use(async function(ctx, next) { + ctx.session.name = 'funny'; + await next(); + }); + + app.use(async function(ctx) { + ctx.throw(401); + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(401); + }); + }); + + describe('when autoCommit is present', () => { + describe('and set to false', () => { + it('should not set headers if manuallyCommit() isn\'t called', async () => { + const app = App({ autoCommit: false }); + + app.use(async function(ctx) { + if (ctx.method === 'POST') { + ctx.session.message = 'hi'; + ctx.body = 200; + return; + } + ctx.body = ctx.session.message; + }); + + const res = await request(app.callback()) + .post('/') + .expect(200); + assert.equal(res.get('Set-Cookie'), undefined, 'should not have set cookie'); + }); + + it('should set headers if manuallyCommit() is called', async () => { + const app = App({ autoCommit: false }); + app.use(async function(ctx, next) { + if (ctx.method === 'POST') { + ctx.session.message = 'dummy'; + } + await next(); + }); + app.use(async function(ctx) { + ctx.body = 200; + await ctx.session.manuallyCommit(); + }); + + await request(app.callback()) + .post('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + }); + }); + }); + + describe('when maxAge present', () => { + describe('and set to be a session cookie', () => { + it('should not expire the session', async () => { + const app = App({ maxAge: 'session' }); + + app.use(async function(ctx) { + if (ctx.method === 'POST') { + ctx.session.message = 'hi'; + ctx.body = 200; + return; + } + ctx.body = ctx.session.message; + }); + + const res = await request(app.callback()) + .post('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + + const cookie = res.get('Set-Cookie')!.join(';'); + assert.doesNotMatch(cookie, /expires=/); + await request(app.callback()) + .get('/') + .set('cookie', cookie) + .expect('hi'); + }); + + it('should not expire the session after multiple session changes', async () => { + const app = App({ maxAge: 'session' }); + + app.use(async function(ctx) { + ctx.session.count = (ctx.session.count || 0) + 1; + ctx.body = `hi ${ctx.session.count}`; + }); + + let res = await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess/) + .expect('hi 1') + .expect(200); + let cookie = res.get('Set-Cookie')!.join(';'); + assert.doesNotMatch(cookie, /expires=/); + + res = await request(app.callback()) + .get('/') + .set('cookie', cookie) + .expect('Set-Cookie', /koa\.sess/) + .expect('hi 2') + .expect(200); + cookie = res.get('Set-Cookie')!.join(';'); + assert.doesNotMatch(cookie, /expires=/); + + res = await request(app.callback()) + .get('/') + .set('cookie', cookie) + .expect('Set-Cookie', /koa\.sess/) + .expect('hi 3') + .expect(200); + cookie = res.get('Set-Cookie')!.join(';'); + assert.doesNotMatch(cookie, /expires=/); + }); + + it('should throw error when the maxAge improper string given', () => { + assert.throws(() => { + App({ maxAge: 'not the right string' } as any); + }, /Invalid input/); + }); + }); + + describe('and not expire', () => { + it('should not expire the session', async () => { + const app = App({ maxAge: 100 }); + + app.use(async function(ctx) { + if (ctx.method === 'POST') { + ctx.session.message = 'hi'; + ctx.body = 200; + return; + } + ctx.body = ctx.session.message; + }); + + const res = await request(app.callback()) + .post('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + const cookie = res.get('Set-Cookie')!.join(';'); + await request(app.callback()) + .get('/') + .set('cookie', cookie) + .expect('hi'); + }); + }); + + describe('and expired', () => { + it('should expire the sess', async () => { + const app = App({ maxAge: 100 }); + + app.use(async function(ctx) { + if (ctx.method === 'POST') { + ctx.session.message = 'hi'; + ctx.status = 200; + return; + } + + ctx.body = ctx.session.message || ''; + }); + + const res = await request(app.callback()) + .post('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + const cookie = res.get('Set-Cookie')!.join(';'); + await scheduler.wait(200); + await request(app.callback()) + .get('/') + .set('cookie', cookie) + .expect(''); + }); + }); + }); + + describe('ctx.session.maxAge', () => { + it('should return opt.maxAge', async () => { + const app = App({ maxAge: 100 }); + + app.use(async function(ctx) { + ctx.body = ctx.session.maxAge; + }); + + await request(app.callback()) + .get('/') + .expect('100'); + }); + }); + + describe('ctx.session.maxAge=', () => { + it('should set sessionOptions.maxAge', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.session.foo = 'bar'; + ctx.session.maxAge = 100; + ctx.body = ctx.session.foo; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /expires=/) + .expect(200); + }); + + it('should save even session not change', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.session.maxAge = 100; + ctx.body = ctx.session; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /expires=/) + .expect(200); + }); + + it('should save when create session only with maxAge', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.session = { maxAge: 100 }; + ctx.body = ctx.session; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /expires=/) + .expect(200); + }); + }); + + describe('when store return empty', () => { + it('should create new Session', async () => { + const app = App({ signed: false }); + + app.use(async function(ctx) { + ctx.body = String(ctx.session.isNew); + }); + + await request(app.callback()) + .get('/') + .set('cookie', 'koa.sess=invalid-key') + .expect('true') + .expect(200); + }); + }); + + describe('when valid and beforeSave set', () => { + it('should ignore session when uid changed', async () => { + const app = new Koa(); + + app.keys = [ 'a', 'b' ]; + app.use(session({ + valid(ctx, sess) { + return ctx.cookies.get('uid') === sess.uid; + }, + beforeSave(ctx, sess) { + sess.uid = ctx.cookies.get('uid'); + }, + ContextStore, + }, app)); + + app.use(async function(ctx) { + if (!ctx.session.foo) { + ctx.session.foo = Date.now() + '|uid:' + ctx.cookies.get('uid'); + } + + ctx.body = { + foo: ctx.session.foo, + uid: ctx.cookies.get('uid'), + }; + }); + + let res = await request(app.callback()) + .get('/') + .set('Cookie', 'uid=123') + .expect(200); + + const data = res.body; + const cookies = res.get('Set-Cookie')!.join(';'); + assert.match(cookies, /koa\.sess=/); + + res = await request(app.callback()) + .get('/') + .set('Cookie', cookies + ';uid=123') + .expect(200) + .expect(data); + + // should ignore uid:123 session and create a new session for uid:456 + res = await request(app.callback()) + .get('/') + .set('Cookie', cookies + ';uid=456') + .expect(200); + assert.equal(res.body.uid, '456'); + assert.notEqual(res.body.foo, data.foo); + }); + }); + + describe('ctx.session', () => { + after(mm.restore); + + it('can be mocked', async () => { + const app = App(); + + app.use(async function(ctx) { + ctx.body = ctx.session; + }); + + mm(app.context, 'session', { + foo: 'bar', + }); + + await request(app.callback()) + .get('/') + .expect({ + foo: 'bar', + }) + .expect(200); + }); + }); +}); diff --git a/test/cookie.test.js b/test/cookie.test.js deleted file mode 100644 index 69b0683..0000000 --- a/test/cookie.test.js +++ /dev/null @@ -1,948 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const Koa = require('koa'); -const request = require('supertest'); -const should = require('should'); -const session = require('..'); - -const inspect = Symbol.for('nodejs.util.inspect.custom'); - -describe('Koa Session Cookie', () => { - let cookie; - - describe('when options.signed = true', () => { - describe('when app.keys are set', () => { - it('should work', done => { - const app = new Koa(); - - app.keys = [ 'a', 'b' ]; - app.use(session({}, app)); - - app.use(async function(ctx) { - ctx.session.message = 'hi'; - ctx.body = ctx.session; - }); - - request(app.listen()) - .get('/') - .expect(200, done); - }); - }); - - describe('when app.keys are not set', () => { - it('should throw and clean this cookie', done => { - const app = new Koa(); - - app.use(session(app)); - - app.use(async function(ctx) { - ctx.session.message = 'hi'; - ctx.body = ctx.session; - }); - - request(app.listen()) - .get('/') - .expect(500, done); - }); - }); - - describe('when app not set', () => { - it('should throw', () => { - const app = new Koa(); - (function() { - app.use(session()); - }).should.throw('app instance required: `session(opts, app)`'); - }); - }); - }); - - describe('when options.signed = false', () => { - describe('when app.keys are not set', () => { - it('should work', done => { - const app = new Koa(); - - app.use(session({ signed: false }, app)); - - app.use(async function(ctx) { - ctx.session.message = 'hi'; - ctx.body = ctx.session; - }); - - request(app.listen()) - .get('/') - .expect(200, done); - }); - }); - }); - - describe('when the session contains a ;', () => { - it('should still work', done => { - const app = App(); - - app.use(async function(ctx) { - if (ctx.method === 'POST') { - ctx.session.string = ';'; - ctx.status = 204; - } else { - ctx.body = ctx.session.string; - } - }); - - const server = app.listen(); - - request(server) - .post('/') - .expect(204, (err, res) => { - if (err) return done(err); - const cookie = res.headers['set-cookie']; - // samesite is not set - assert(!cookie.join(';').includes('samesite')); - request(server) - .get('/') - .set('Cookie', cookie.join(';')) - .expect(';', done); - }); - }); - }); - - describe('new session', () => { - describe('when not accessed', () => { - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.body = 'greetings'; - }); - - request(app.listen()) - .get('/') - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - - describe('when accessed and not populated', () => { - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session; - ctx.body = 'greetings'; - }); - - request(app.listen()) - .get('/') - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - - describe('when populated', () => { - it('should Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.message = 'hello'; - ctx.body = ''; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess/) - .expect(200, (err, res) => { - if (err) return done(err); - cookie = res.header['set-cookie'].join(';'); - done(); - }); - }); - - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.body = ctx.session; - }); - - request(app.listen()) - .get('/') - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - }); - - describe('saved session', () => { - describe('when not accessed', () => { - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.body = 'aklsdjflasdjf'; - }); - - request(app.listen()) - .get('/') - .set('Cookie', cookie) - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - - describe('when accessed but not changed', () => { - it('should be the same session', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.message.should.equal('hello'); - ctx.body = 'aklsdjflasdjf'; - }); - - request(app.listen()) - .get('/') - .set('Cookie', cookie) - .expect(200, done); - }); - - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.message.should.equal('hello'); - ctx.body = 'aklsdjflasdjf'; - }); - - request(app.listen()) - .get('/') - .set('Cookie', cookie) - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - - describe('when accessed and changed', () => { - it('should Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.money = '$$$'; - ctx.body = 'aklsdjflasdjf'; - }); - - request(app.listen()) - .get('/') - .set('Cookie', cookie) - .expect('Set-Cookie', /koa\.sess/) - .expect(res => { - const cookie = res.headers['set-cookie']; - // samesite is not set - assert(!cookie.join(';').includes('samesite')); - }) - .expect(200, done); - }); - }); - }); - - describe('after session set to null with signed cookie', () => { - it('should return expired cookies', done => { - const app = App({ - signed: true, - }); - - app.use(async function(ctx) { - ctx.session.hello = {}; - ctx.session = null; - ctx.body = String(ctx.session === null); - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess=; path=\/; expires=Thu, 01 Jan 1970 00:00:00 GMT/) - .expect('Set-Cookie', /koa\.sess.sig=(.*); path=\/; expires=Thu, 01 Jan 1970 00:00:00 GMT/) - .expect('true') - .expect(200, done); - }); - }); - - describe('after session set to null without signed cookie', () => { - it('should return expired cookies', done => { - const app = App({ - signed: false, - }); - - app.use(async function(ctx) { - ctx.session.hello = {}; - ctx.session = null; - ctx.body = String(ctx.session === null); - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess=; path=\/; expires=Thu, 01 Jan 1970 00:00:00 GMT/) - .expect('true') - .expect(200, done); - }); - }); - - describe('when get session after set to null', () => { - it('should return null', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.hello = {}; - ctx.session = null; - ctx.body = String(ctx.session === null); - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess=;/) - .expect('true') - .expect(200, done); - }); - }); - - describe('when decode session', () => { - describe('SyntaxError', () => { - it('should create new session', done => { - const app = App({ signed: false }); - - app.use(async function(ctx) { - ctx.body = String(ctx.session.isNew); - }); - - request(app.listen()) - .get('/') - .set('cookie', 'koa.sess=invalid-session;') - .expect('true') - .expect(200, done); - }); - }); - - describe('Other Error', () => { - it('should throw', done => { - const app = App({ - signed: false, - decode() { - throw new Error('decode error'); - }, - }); - - app.use(async function(ctx) { - ctx.body = String(ctx.session.isNew); - }); - - request(app.listen()) - .get('/') - .set('cookie', 'koa.sess=invalid-session;') - .expect('Set-Cookie', /koa\.sess=;/) - .expect(500, done); - }); - }); - }); - - describe('when encode session error', () => { - it('should throw', done => { - const app = App({ - encode() { - throw new Error('encode error'); - }, - }); - - app.use(async function(ctx) { - ctx.session.foo = 'bar'; - ctx.body = 'hello'; - }); - - app.once('error', (err, ctx) => { - err.message.should.equal('encode error'); - should.exists(ctx); - }); - - request(app.listen()) - .get('/') - .expect(500, done); - }); - }); - - describe('session', () => { - describe('.inspect()', () => { - it('should return session content', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.foo = 'bar'; - ctx.body = ctx.session[inspect](); - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess=.+;/) - .expect({ foo: 'bar' }) - .expect(200, done); - }); - }); - - describe('.length', () => { - it('should return session length', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.foo = 'bar'; - ctx.body = String(ctx.session.length); - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess=.+;/) - .expect('1') - .expect(200, done); - }); - }); - - describe('.populated', () => { - it('should return session populated', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.foo = 'bar'; - ctx.body = String(ctx.session.populated); - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess=.+;/) - .expect('true') - .expect(200, done); - }); - }); - - describe('.save()', () => { - it('should save session', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.save(); - ctx.body = 'hello'; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess=.+;/) - .expect('hello') - .expect(200, done); - }); - }); - }); - - describe('when session is', () => { - describe('null', () => { - it('should expire the session', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session = null; - ctx.body = 'asdf'; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess/) - .expect(200, done); - }); - }); - - describe('an empty object', () => { - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session = {}; - ctx.body = 'asdf'; - }); - - request(app.listen()) - .get('/') - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - - describe('an object', () => { - it('should create a session', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session = { message: 'hello' }; - ctx.body = 'asdf'; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess/) - .expect(200, done); - }); - }); - - describe('anything else', () => { - it('should throw', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session = 'asdf'; - }); - - request(app.listen()) - .get('/') - .expect(500, done); - }); - }); - }); - - describe('when an error is thrown downstream and caught upstream', () => { - it('should still save the session', done => { - const app = new Koa(); - - app.keys = [ 'a', 'b' ]; - - app.use(async function(ctx, next) { - try { - await next(); - } catch (err) { - ctx.status = err.status; - ctx.body = err.message; - } - }); - - app.use(session(app)); - - app.use(async function(ctx, next) { - ctx.session.name = 'funny'; - await next(); - }); - - app.use(async function(ctx) { - ctx.throw(401); - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess/) - .expect(401, done); - }); - }); - - describe('when maxAge present', () => { - describe('and not expire', () => { - it('should not expire the session', done => { - const app = App({ maxAge: 100 }); - - app.use(async function(ctx) { - if (ctx.method === 'POST') { - ctx.session.message = 'hi'; - ctx.body = 200; - return; - } - ctx.body = ctx.session.message; - }); - - const server = app.listen(); - - request(server) - .post('/') - .expect('Set-Cookie', /koa\.sess/) - .end((err, res) => { - if (err) return done(err); - const cookie = res.headers['set-cookie'].join(';'); - - request(server) - .get('/') - .set('cookie', cookie) - .expect('hi', done); - }); - }); - }); - - describe('and expired', () => { - it('should expire the sess', done => { - const app = App({ maxAge: 100 }); - - app.use(async function(ctx) { - if (ctx.method === 'POST') { - ctx.session.message = 'hi'; - ctx.status = 200; - return; - } - - ctx.body = ctx.session.message || ''; - }); - - const server = app.listen(); - - request(server) - .post('/') - .expect('Set-Cookie', /koa\.sess/) - .end((err, res) => { - if (err) return done(err); - const cookie = res.headers['set-cookie'].join(';'); - - setTimeout(() => { - request(server) - .get('/') - .set('cookie', cookie) - .expect('', done); - }, 200); - }); - }); - }); - }); - - describe('ctx.session.maxAge', () => { - it('should return opt.maxAge', done => { - const app = App({ maxAge: 100 }); - - app.use(async function(ctx) { - ctx.body = ctx.session.maxAge; - }); - - request(app.listen()) - .get('/') - .expect('100', done); - }); - }); - - describe('ctx.session.maxAge=', () => { - it('should set sessionOptions.maxAge', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.foo = 'bar'; - ctx.session.maxAge = 100; - ctx.body = ctx.session.foo; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /expires=/) - .expect(200, done); - }); - - it('should save even session not change', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.maxAge = 100; - ctx.body = ctx.session; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /expires=/) - .expect(200, done); - }); - - it('should save when create session only with maxAge', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session = { maxAge: 100 }; - ctx.body = ctx.session; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /expires=/) - .expect(200, done); - }); - }); - - describe('ctx.session.regenerate', () => { - it('should change the session key, but not content', done => { - const app = new App(); - const message = 'hi'; - app.use(async function(ctx, next) { - ctx.session = { message: 'hi' }; - await next(); - }); - - app.use(async function(ctx, next) { - const sessionKey = ctx.cookies.get('koa.sess'); - if (sessionKey) { - await ctx.session.regenerate(); - } - await next(); - }); - - app.use(async function(ctx) { - ctx.session.message.should.equal(message); - ctx.body = ''; - }); - let koaSession = null; - request(app.callback()) - .get('/') - .expect(200, (err, res) => { - should.not.exist(err); - koaSession = res.headers['set-cookie'][0]; - koaSession.should.containEql('koa.sess='); - request(app.callback()) - .get('/') - .set('Cookie', koaSession) - .expect(200, (err, res) => { - should.not.exist(err); - const cookies = res.headers['set-cookie'][0]; - cookies.should.containEql('koa.sess='); - cookies.should.not.equal(koaSession); - done(); - }); - }); - }); - }); - - describe('when get session before enter session middleware', () => { - it('should work', done => { - const app = new Koa(); - - app.keys = [ 'a', 'b' ]; - app.use(async function(ctx, next) { - ctx.session.foo = 'hi'; - await next(); - }); - app.use(session({}, app)); - app.use(async function(ctx) { - ctx.body = ctx.session; - }); - - request(app.callback()) - .get('/') - .expect(200, (err, res) => { - should.not.exist(err); - const cookies = res.headers['set-cookie'].join(';'); - cookies.should.containEql('koa.sess='); - - request(app.callback()) - .get('/') - .set('Cookie', cookies) - .expect(200, done); - }); - }); - }); - - describe('options.sameSite', () => { - it('should return opt.sameSite=none', done => { - const app = App({ sameSite: 'none' }); - - app.use(async function(ctx) { - ctx.session = { foo: 'bar' }; - ctx.body = ctx.session.foo; - }); - - request(app.listen()) - .get('/') - .expect(res => { - const cookie = res.headers['set-cookie'].join('|'); - assert(cookie.includes('path=/; samesite=none; httponly')); - }) - .expect('bar') - .expect(200, done); - }); - - it('should return opt.sameSite=lax', done => { - const app = App({ sameSite: 'lax' }); - - app.use(async function(ctx) { - ctx.session = { foo: 'bar' }; - ctx.body = ctx.session.foo; - }); - - request(app.listen()) - .get('/') - .expect(res => { - const cookie = res.headers['set-cookie'].join('|'); - assert(cookie.includes('path=/; samesite=lax; httponly')); - }) - .expect('bar') - .expect(200, done); - }); - }); - - describe('when valid and beforeSave set', () => { - it('should ignore session when uid changed', done => { - const app = new Koa(); - - app.keys = [ 'a', 'b' ]; - app.use(session({ - valid(ctx, sess) { - return ctx.cookies.get('uid') === sess.uid; - }, - beforeSave(ctx, sess) { - sess.uid = ctx.cookies.get('uid'); - }, - }, app)); - app.use(async function(ctx) { - if (!ctx.session.foo) { - ctx.session.foo = Date.now() + '|uid:' + ctx.cookies.get('uid'); - } - - ctx.body = { - foo: ctx.session.foo, - uid: ctx.cookies.get('uid'), - }; - }); - - request(app.callback()) - .get('/') - .set('Cookie', 'uid=123') - .expect(200, (err, res) => { - should.not.exist(err); - const data = res.body; - const cookies = res.headers['set-cookie'].join(';'); - cookies.should.containEql('koa.sess='); - - request(app.callback()) - .get('/') - .set('Cookie', cookies + ';uid=123') - .expect(200) - .expect(data, err => { - should.not.exist(err); - - // should ignore uid:123 session and create a new session for uid:456 - request(app.callback()) - .get('/') - .set('Cookie', cookies + ';uid=456') - .expect(200, (err, res) => { - should.not.exist(err); - res.body.uid.should.equal('456'); - res.body.foo.should.not.equal(data.foo); - done(); - }); - }); - }); - }); - }); - - describe('when options.encode and options.decode are functions', () => { - describe('they are used to encode/decode stored cookie values', () => { - it('should work', done => { - let encodeCallCount = 0; - let decodeCallCount = 0; - - function encode(data) { - ++encodeCallCount; - return JSON.stringify({ enveloped: data }); - } - function decode(data) { - ++decodeCallCount; - return JSON.parse(data).enveloped; - } - - const app = new Koa(); - app.keys = [ 'a', 'b' ]; - app.use(session({ - encode, - decode, - }, app)); - - app.use(async function(ctx) { - ctx.session.counter = (ctx.session.counter || 0) + 1; - ctx.body = ctx.session; - return; - }); - - request(app.callback()) - .get('/') - .expect(() => { encodeCallCount.should.above(0, 'encode was not called'); }) - .expect(200, (err, res) => { - should.not.exist(err); - res.body.counter.should.equal(1, 'expected body to be equal to session.counter'); - const cookies = res.headers['set-cookie'].join(';'); - request(app.callback()) - .get('/') - .set('Cookie', cookies) - .expect(() => { decodeCallCount.should.be.above(0, 'decode was not called'); }) - .expect(200, (err, res) => { - should.not.exist(err); - res.body.counter.should.equal(2); - done(); - }); - }); - }); - }); - }); - - describe('when rolling set to true', () => { - let app; - before(() => { - app = App({ rolling: true }); - - app.use(function* () { - console.log(this.path); - if (this.path === '/set') this.session = { foo: 'bar' }; - this.body = this.session; - }); - }); - - it('should not send set-cookie when session not exists', () => { - return request(app.callback()) - .get('/') - .expect({}) - .expect(res => { - should.not.exist(res.headers['set-cookie']); - }); - }); - - it('should send set-cookie when session exists and not change', done => { - request(app.callback()) - .get('/set') - .expect({ foo: 'bar' }) - .end((err, res) => { - should.not.exist(err); - res.headers['set-cookie'].should.have.length(2); - const cookie = res.headers['set-cookie'].join(';'); - request(app.callback()) - .get('/') - .set('cookie', cookie) - .expect(res => { - res.headers['set-cookie'].should.have.length(2); - }) - .expect({ foo: 'bar' }, done); - }); - }); - }); - - describe('init multi session middleware', () => { - it('should work', () => { - const app = new Koa(); - - app.keys = [ 'a', 'b' ]; - const s1 = session({}, app); - const s2 = session({}, app); - assert(s1); - assert(s2); - }); - }); -}); - -function App(options) { - const app = new Koa(); - app.keys = [ 'a', 'b' ]; - app.use(session(options, app)); - return app; -} diff --git a/test/cookie.test.ts b/test/cookie.test.ts new file mode 100644 index 0000000..add102e --- /dev/null +++ b/test/cookie.test.ts @@ -0,0 +1,900 @@ +import { strict as assert } from 'node:assert'; +import Koa from 'koa'; +import { request } from '@eggjs/supertest'; +import session, { type CreateSessionOptions } from '../src/index.js'; + +function App(options: CreateSessionOptions = {}) { + const app = new Koa(); + app.keys = [ 'a', 'b' ]; + app.use(session(options, app)); + return app; +} + +const inspect = Symbol.for('nodejs.util.inspect.custom'); + +describe('Koa Session Cookie', () => { + let cookie: string; + + describe('when options.signed = true', () => { + describe('when app.keys are set', () => { + it('should work', async () => { + const app = new Koa(); + + app.keys = [ 'a', 'b' ]; + app.use(session({}, app)); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.message = 'hi'; + ctx.body = ctx.session; + }); + + await request(app.callback()) + .get('/') + .expect(200); + }); + }); + + describe('when app.keys are not set', () => { + it('should throw and clean this cookie', async () => { + const app = new Koa(); + + app.use(session(app)); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.message = 'hi'; + ctx.body = ctx.session; + }); + + await request(app.callback()) + .get('/') + .expect(500); + }); + }); + + describe('when app not set', () => { + it('should throw', () => { + const app = new Koa(); + assert.throws(() => { + app.use((session as any)()); + }, /app instance required: `session\(opts, app\)`/); + assert.throws(() => { + app.use(session({})); + }, /app instance required: `session\(opts, app\)`/); + }); + }); + }); + + describe('when options.signed = false', () => { + describe('when app.keys are not set', () => { + it('should work', async () => { + const app = new Koa(); + + app.use(session({ signed: false }, app)); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.message = 'hi'; + ctx.body = ctx.session; + }); + + await request(app.callback()) + .get('/') + .expect(200); + }); + }); + }); + + describe('when the session contains a ;', () => { + it('should still work', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + if (ctx.method === 'POST') { + ctx.session!.string = ';'; + ctx.status = 204; + } else { + ctx.body = ctx.session!.string; + } + }); + + const server = app.callback(); + + const res = await request(server) + .post('/') + .expect(204); + + const cookie = res.get('Set-Cookie')!; + // samesite is not set + assert(!cookie.join(';').includes('samesite')); + await request(server) + .get('/') + .set('Cookie', cookie.join(';')) + .expect(';'); + }); + }); + + describe('new session', () => { + describe('when not accessed', () => { + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.body = 'greetings'; + }); + + const res = await request(app.callback()) + .get('/') + .expect(200); + assert.equal(res.header['set-cookie'], undefined); + }); + }); + + describe('when accessed and not populated', () => { + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session; + ctx.body = 'greetings'; + }); + + const res = await request(app.callback()) + .get('/') + .expect(200); + assert.equal(res.header['set-cookie'], undefined); + }); + }); + + describe('when populated', () => { + it('should Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.message = 'hello'; + ctx.body = ''; + }); + + const res = await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + cookie = res.get('Set-Cookie')!.join(';'); + }); + + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.body = ctx.session; + }); + + const res = await request(app.callback()) + .get('/') + .expect(200); + assert.equal(res.header['set-cookie'], undefined); + }); + }); + }); + + describe('saved session', () => { + describe('when not accessed', () => { + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.body = 'aklsdjflasdjf'; + }); + + const res = await request(app.callback()) + .get('/') + .set('Cookie', cookie) + .expect(200); + assert.equal(res.header['set-cookie'], undefined); + }); + }); + + describe('when accessed but not changed', () => { + it('should be the same session', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + assert.equal(ctx.session!.message, 'hello'); + ctx.body = 'aklsdjflasdjf'; + }); + + await request(app.callback()) + .get('/') + .set('Cookie', cookie) + .expect(200); + }); + + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + assert.equal(ctx.session!.message, 'hello'); + ctx.body = 'aklsdjflasdjf'; + }); + + const res = await request(app.callback()) + .get('/') + .set('Cookie', cookie) + .expect(200); + assert.equal(res.header['set-cookie'], undefined); + }); + }); + + describe('when accessed and changed', () => { + it('should Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.money = '$$$'; + ctx.body = 'aklsdjflasdjf'; + }); + + const res = await request(app.callback()) + .get('/') + .set('Cookie', cookie) + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + const newCookie = res.get('Set-Cookie')!; + // samesite is not set + assert(!newCookie.join(';').includes('samesite')); + }); + }); + }); + + describe('after session set to null with signed cookie', () => { + it('should return expired cookies', async () => { + const app = App({ + signed: true, + }); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.hello = {}; + ctx.session = null; + ctx.body = String(ctx.session === null); + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', + /koa\.sess=; path=\/; expires=Thu, 01 Jan 1970 00:00:00 GMT/) + .expect('Set-Cookie', + /koa\.sess.sig=(.*); path=\/; expires=Thu, 01 Jan 1970 00:00:00 GMT/) + .expect('true') + .expect(200); + }); + }); + + describe('after session set to null without signed cookie', () => { + it('should return expired cookies', async () => { + const app = App({ + signed: false, + }); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.hello = {}; + ctx.session = null; + ctx.body = String(ctx.session === null); + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess=; path=\/; expires=Thu, 01 Jan 1970 00:00:00 GMT/) + .expect('true') + .expect(200); + }); + }); + + describe('when get session after set to null', () => { + it('should return null', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.hello = {}; + ctx.session = null; + ctx.body = String(ctx.session === null); + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess=;/) + .expect('true') + .expect(200); + }); + }); + + describe('when decode session', () => { + describe('SyntaxError', () => { + it('should create new session', async () => { + const app = App({ signed: false }); + + app.use(async (ctx: Koa.Context) => { + ctx.body = String(ctx.session.isNew); + }); + + await request(app.callback()) + .get('/') + .set('cookie', 'koa.sess=invalid-session;') + .expect('true') + .expect(200); + }); + }); + + describe('Other Error', () => { + it('should throw', async () => { + const app = App({ + signed: false, + decode() { + throw new Error('decode error'); + }, + }); + + app.use(async (ctx: Koa.Context) => { + ctx.body = String(ctx.session!.isNew); + }); + + await request(app.callback()) + .get('/') + .set('cookie', 'koa.sess=invalid-session;') + .expect('Set-Cookie', /koa\.sess=;/) + .expect(500); + }); + }); + }); + + describe('when encode session error', () => { + it('should throw', async () => { + const app = App({ + encode() { + throw new Error('encode error'); + }, + }); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.foo = 'bar'; + ctx.body = 'hello'; + }); + + app.once('error', (err, ctx) => { + assert.equal(err.message, 'encode error'); + assert(ctx); + }); + + await request(app.callback()) + .get('/') + .expect(500); + }); + }); + + describe('session', () => { + describe('.inspect()', () => { + it('should return session content', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.foo = 'bar'; + ctx.body = ctx.session![inspect](); + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess=.+;/) + .expect({ foo: 'bar' }) + .expect(200); + }); + }); + + describe('.length', () => { + it('should return session length', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.foo = 'bar'; + ctx.body = String(ctx.session!.length); + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess=.+;/) + .expect('1') + .expect(200); + }); + }); + + describe('.populated', () => { + it('should return session populated', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.foo = 'bar'; + ctx.body = String(ctx.session!.populated); + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess=.+;/) + .expect('true') + .expect(200); + }); + }); + + describe('.save()', () => { + it('should save session', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.save(); + ctx.body = 'hello'; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess=.+;/) + .expect('hello') + .expect(200); + }); + }); + }); + + describe('when session is', () => { + describe('null', () => { + it('should expire the session', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session = null; + ctx.body = 'asdf'; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + }); + }); + + describe('an empty object', () => { + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session = {}; + ctx.body = 'asdf'; + }); + + const res = await request(app.callback()) + .get('/') + .expect(200); + assert.equal(res.header['set-cookie'], undefined); + }); + }); + + describe('an object', () => { + it('should create a session', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session = { message: 'hello' }; + ctx.body = 'asdf'; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + }); + }); + + describe('anything else', () => { + it('should throw', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session = 'asdf'; + }); + + await request(app.callback()) + .get('/') + .expect(/Internal Server Error/) + .expect(500); + }); + }); + }); + + describe('when an error is thrown downstream and caught upstream', () => { + it('should still save the session', async () => { + const app = new Koa(); + + app.keys = [ 'a', 'b' ]; + + app.use(async (ctx: Koa.Context, next: Koa.Next) => { + try { + await next(); + } catch (err: any) { + ctx.status = err.status; + ctx.body = err.message; + } + }); + + app.use(session(app)); + + app.use(async (ctx: Koa.Context, next: Koa.Next) => { + ctx.session.name = 'funny'; + await next(); + }); + + app.use(async (ctx: Koa.Context) => { + ctx.throw(401); + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(401); + }); + }); + + describe('when maxAge present', () => { + describe('and not expire', () => { + it('should not expire the session', async () => { + const app = App({ maxAge: 100 }); + + app.use(async (ctx: Koa.Context) => { + if (ctx.method === 'POST') { + ctx.session!.message = 'hi'; + ctx.body = 200; + return; + } + ctx.body = ctx.session!.message; + }); + + const server = app.callback(); + + const res = await request(server) + .post('/') + .expect('Set-Cookie', /koa\.sess/); + + const cookie = res.get('Set-Cookie')!.join(';'); + + await request(server) + .get('/') + .set('cookie', cookie) + .expect('hi'); + }); + }); + + describe('and expired', () => { + it('should expire the sess', async () => { + const app = App({ maxAge: 100 }); + + app.use(async (ctx: Koa.Context) => { + if (ctx.method === 'POST') { + ctx.session!.message = 'hi'; + ctx.status = 200; + return; + } + + ctx.body = ctx.session.message || ''; + }); + + const server = app.callback(); + + const res = await request(server) + .post('/') + .expect('Set-Cookie', /koa\.sess/); + + const cookie = res.get('Set-Cookie')!.join(';'); + + await new Promise(resolve => setTimeout(resolve, 200)); + + await request(server) + .get('/') + .set('cookie', cookie) + .expect(''); + }); + }); + }); + + describe('ctx.session.maxAge', () => { + it('should return opt.maxAge', async () => { + const app = App({ maxAge: 100 }); + + app.use(async (ctx: Koa.Context) => { + ctx.body = ctx.session!.maxAge; + }); + + await request(app.callback()) + .get('/') + .expect('100'); + }); + }); + + describe('ctx.session.maxAge=', () => { + it('should set sessionOptions.maxAge', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.foo = 'bar'; + ctx.session!.maxAge = 100; + ctx.body = ctx.session!.foo; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /expires=/) + .expect(200); + }); + + it('should save even session not change', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.maxAge = 100; + ctx.body = ctx.session; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /expires=/) + .expect(200); + }); + + it('should save when create session only with maxAge', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session = { maxAge: 100 }; + ctx.body = ctx.session; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /expires=/) + .expect(200); + }); + }); + + describe('ctx.session.regenerate', () => { + it('should change the session key, but not content', async () => { + const app = App(); + const message = 'hi'; + app.use(async (ctx: Koa.Context, next: Koa.Next) => { + ctx.session = { message: 'hi' }; + await next(); + }); + + app.use(async (ctx: Koa.Context, next: Koa.Next) => { + const sessionKey = ctx.cookies.get('koa.sess'); + if (sessionKey) { + await ctx.session!.regenerate(); + } + await next(); + }); + + app.use(async (ctx: Koa.Context) => { + assert.equal(ctx.session!.message, message); + ctx.body = ''; + }); + let res = await request(app.callback()) + .get('/') + .expect(200); + + const koaSession = res.get('Set-Cookie')!.join(';'); + assert.match(koaSession, /koa\.sess=/); + res = await request(app.callback()) + .get('/') + .set('Cookie', koaSession) + .expect(200); + + const cookies = res.get('Set-Cookie')!.join(';'); + assert.match(cookies, /koa\.sess=/); + assert.notEqual(cookies, koaSession); + }); + }); + + describe('when get session before enter session middleware', () => { + it('should work', async () => { + const app = new Koa(); + + app.keys = [ 'a', 'b' ]; + app.use(async (ctx: Koa.Context, next: Koa.Next) => { + ctx.session!.foo = 'hi'; + await next(); + }); + app.use(session({}, app)); + app.use(async (ctx: Koa.Context) => { + ctx.body = ctx.session; + }); + + const res = await request(app.callback()) + .get('/') + .expect(200); + + const cookies = res.get('Set-Cookie')!.join(';'); + assert(cookies.includes('koa.sess=')); + + await request(app.callback()) + .get('/') + .set('Cookie', cookies) + .expect(200); + }); + }); + + describe('options.sameSite', () => { + it('should return opt.sameSite=none', async () => { + const app = App({ sameSite: 'none' }); + + app.use(async (ctx: Koa.Context) => { + ctx.session = { foo: 'bar' }; + ctx.body = ctx.session.foo; + }); + + const res = await request(app.callback()) + .get('/') + .expect('bar') + .expect(200); + const cookie = res.get('Set-Cookie')!.join('|'); + assert(cookie.includes('path=/; samesite=none; httponly')); + }); + + it('should return opt.sameSite=lax', async () => { + const app = App({ sameSite: 'lax' }); + + app.use(async (ctx: Koa.Context) => { + ctx.session = { foo: 'bar' }; + ctx.body = ctx.session.foo; + }); + + const res = await request(app.callback()) + .get('/') + .expect('bar') + .expect(200); + const cookie = res.get('Set-Cookie')!.join('|'); + assert(cookie.includes('path=/; samesite=lax; httponly')); + }); + }); + + describe('when valid and beforeSave set', () => { + it('should ignore session when uid changed', async () => { + const app = new Koa(); + + app.keys = [ 'a', 'b' ]; + app.use(session({ + valid(ctx, sess) { + return ctx.cookies.get('uid') === sess.uid; + }, + beforeSave(ctx, sess) { + sess.uid = ctx.cookies.get('uid'); + }, + }, app)); + app.use(async (ctx: Koa.Context) => { + if (!ctx.session!.foo) { + ctx.session!.foo = Date.now() + '|uid:' + ctx.cookies.get('uid'); + } + + ctx.body = { + foo: ctx.session!.foo, + uid: ctx.cookies.get('uid'), + }; + }); + + const res = await request(app.callback()) + .get('/') + .set('Cookie', 'uid=123') + .expect(200); + + const data = res.body; + const cookies = res.get('Set-Cookie')!.join(';'); + assert(cookies.includes('koa.sess=')); + + await request(app.callback()) + .get('/') + .set('Cookie', cookies + ';uid=123') + .expect(200) + .expect(data); + + // should ignore uid:123 session and create a new session for uid:456 + const res2 = await request(app.callback()) + .get('/') + .set('Cookie', cookies + ';uid=456') + .expect(200); + + assert.equal(res2.body.uid, '456'); + assert.notEqual(res2.body.foo, data.foo); + }); + }); + + describe('when options.encode and options.decode are functions', () => { + describe('they are used to encode/decode stored cookie values', () => { + it('should work', async () => { + let encodeCallCount = 0; + let decodeCallCount = 0; + + function encode(data: any) { + ++encodeCallCount; + return JSON.stringify({ enveloped: data }); + } + function decode(data: string) { + ++decodeCallCount; + return JSON.parse(data).enveloped; + } + + const app = new Koa(); + app.keys = [ 'a', 'b' ]; + app.use(session({ + encode, + decode, + }, app)); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.counter = (ctx.session!.counter || 0) + 1; + ctx.body = ctx.session; + return; + }); + + const res = await request(app.callback()) + .get('/') + .expect(() => { assert(encodeCallCount > 0, 'encode was not called'); }) + .expect(200); + + assert.equal(res.body.counter, 1, 'expected body to be equal to session.counter'); + const cookies = res.get('Set-Cookie')!.join(';'); + const res2 = await request(app.callback()) + .get('/') + .set('Cookie', cookies) + .expect(() => { assert(decodeCallCount > 0, 'decode was not called'); }) + .expect(200); + + assert.equal(res2.body.counter, 2); + }); + }); + }); + + describe('when rolling set to true', () => { + let app: Koa; + before(() => { + app = App({ rolling: true }); + + app.use(async (ctx: Koa.Context) => { + if (ctx.path === '/set') ctx.session = { foo: 'bar' }; + ctx.body = ctx.session; + }); + }); + + it('should not send set-cookie when session not exists', async () => { + const res = await request(app.callback()) + .get('/') + .expect({}); + + assert.equal(res.headers['set-cookie'], undefined); + }); + + it('should send set-cookie when session exists and not change', async () => { + const res = await request(app.callback()) + .get('/set') + .expect({ foo: 'bar' }); + + assert.equal(res.get('Set-Cookie')!.length, 2); + const cookie = res.get('Set-Cookie')!.join(';'); + const res2 = await request(app.callback()) + .get('/') + .set('cookie', cookie) + .expect({ foo: 'bar' }); + assert.equal(res2.headers['set-cookie'].length, 2); + }); + }); + + describe('init multi session middleware', () => { + it('should work', () => { + const app = new Koa(); + + app.keys = [ 'a', 'b' ]; + const s1 = session({}, app); + const s2 = session({}, app); + assert(s1); + assert(s2); + }); + }); +}); diff --git a/test/externalkey.test.js b/test/externalkey.test.js deleted file mode 100644 index 3dfd8c1..0000000 --- a/test/externalkey.test.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; -const Koa = require('koa'); -const request = require('supertest'); -const assert = require('assert'); -const session = require('..'); -const store = require('./store'); -const TOKEN_KEY = 'User-Token'; - -describe('Koa Session External Key', () => { - describe('when the external key set/get is invalid', () => { - it('should throw a error', () => { - try { - new App({ - externalKey: {}, - }); - } catch (err) { - assert.equal(err.message, 'externalKey.get must be function'); - } - }); - }); - describe('custom get/set external key', () => { - it('should still work', done => { - const app = App(); - app.use(async function(ctx) { - if (ctx.method === 'POST') { - ctx.session.string = ';'; - ctx.status = 204; - assert(ctx.session.externalKey); - } else { - ctx.body = ctx.session.string; - assert(ctx.session.externalKey === ctx.get(TOKEN_KEY)); - } - }); - const server = app.listen(); - request(server) - .post('/') - .expect(204, (err, res) => { - if (err) return done(err); - const token = res.get(TOKEN_KEY); - request(server) - .get('/') - .set(TOKEN_KEY, token) - .expect(';', done); - }); - }); - }); -}); -function App(options) { - const app = new Koa(); - app.keys = [ 'a', 'b' ]; - options = options || {}; - options.store = store; - options.externalKey = options.externalKey || { - get: ctx => ctx.get(TOKEN_KEY), - set: (ctx, value) => ctx.set(TOKEN_KEY, value), - }; - app.use(session(options, app)); - return app; -} diff --git a/test/externalkey.test.ts b/test/externalkey.test.ts new file mode 100644 index 0000000..ef69845 --- /dev/null +++ b/test/externalkey.test.ts @@ -0,0 +1,60 @@ +import { strict as assert } from 'node:assert'; +import Koa from 'koa'; +import { ZodError } from 'zod'; +import { request } from '@eggjs/supertest'; +import session, { type CreateSessionOptions } from '../src/index.js'; +import store from './store.js'; + +const TOKEN_KEY = 'User-Token'; + +function App(options: CreateSessionOptions = {}) { + const app = new Koa(); + app.keys = [ 'a', 'b' ]; + options.store = store; + options.externalKey = options.externalKey ?? { + get: ctx => ctx.get(TOKEN_KEY), + set: (ctx, value) => ctx.set(TOKEN_KEY, value), + }; + app.use(session(options, app)); + return app; +} + +describe('Koa Session External Key', () => { + describe('when the external key set/get is invalid', () => { + it('should throw a error', () => { + assert.throws(() => { + App({ + externalKey: {} as any, + }); + }, err => { + assert(err instanceof ZodError); + assert.match(err.message, /externalKey/); + return true; + }); + }); + }); + + describe('custom get/set external key', () => { + it('should still work', async () => { + const app = App(); + app.use(async function(ctx) { + if (ctx.method === 'POST') { + ctx.session.string = ';'; + ctx.status = 204; + assert(ctx.session.externalKey); + } else { + ctx.body = ctx.session.string; + assert.equal(ctx.session.externalKey, ctx.get(TOKEN_KEY)); + } + }); + const res = await request(app.callback()) + .post('/') + .expect(204); + const token = res.get(TOKEN_KEY)!; + await request(app.callback()) + .get('/') + .set(TOKEN_KEY as any, token) + .expect(';'); + }); + }); +}); diff --git a/test/genid_bench.js b/test/genid_bench.js deleted file mode 100644 index 8ff2900..0000000 --- a/test/genid_bench.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -const Benchmark = require('benchmark'); -const uuid = require('uuid'); -const uid = require('uid-safe'); - -const suite = new Benchmark.Suite(); - -const genidByUid = () => `${Date.now()}-${uid.sync(24)}`; -const genidByUuidV1 = () => uuid.v1(); -const genidByUuidV4 = () => uuid.v4(); - -console.log('genidByUid() => %s', genidByUid()); -console.log('genidByUuidV1() => %s', genidByUuidV1()); -console.log('genidByUuidV4() => %s', genidByUuidV4()); - -// add tests -suite -.add('uid()', function() { - genidByUid(); -}) -.add('genidByUuidV1()', function() { - genidByUuidV1(); -}) -.add('genidByUuidV4()', function() { - genidByUuidV4(); -}) -// add listeners -.on('cycle', function(event) { - console.log(String(event.target)); -}) -.on('complete', function() { - console.log('Fastest is ' + this.filter('fastest').map('name')); -}) -// run async -.run({ async: true }); - -// genidByUid() => 1556529339180-DRnQyEqlYjGr_Zq_42fHpdFMlBfVlAoG -// genidByUuidV1() => 5af3b830-6a5f-11e9-91fb-abc918efca3d -// genidByUuidV4() => 27088fa8-8436-4c8b-aae7-76ba316db9e3 -// -// uid() x 260,850 ops/sec ±1.50% (84 runs sampled) -// genidByUuidV1() x 1,181,483 ops/sec ±0.93% (86 runs sampled) -// genidByUuidV4() x 301,840 ops/sec ±1.40% (83 runs sampled) -// Fastest is genidByUuidV1() diff --git a/test/store.js b/test/store.js deleted file mode 100644 index e344c72..0000000 --- a/test/store.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -const sessions = {}; - -module.exports = { - async get(key) { - return sessions[key]; - }, - - async set(key, value) { - sessions[key] = value; - }, - - async destroy(key) { - sessions[key] = undefined; - }, -}; diff --git a/test/store.test.js b/test/store.test.js deleted file mode 100644 index be3a5fe..0000000 --- a/test/store.test.js +++ /dev/null @@ -1,861 +0,0 @@ -'use strict'; - -const Koa = require('koa'); -const request = require('supertest'); -const should = require('should'); -const mm = require('mm'); -const session = require('..'); -const store = require('./store'); -const pedding = require('pedding'); -const assert = require('assert'); -const sleep = require('mz-modules/sleep'); - -const inspect = Symbol.for('nodejs.util.inspect.custom'); - -describe('Koa Session External Store', () => { - let cookie; - - describe('when the session contains a ;', () => { - it('should still work', done => { - const app = App(); - - app.use(async function(ctx) { - if (ctx.method === 'POST') { - ctx.session.string = ';'; - ctx.status = 204; - } else { - ctx.body = ctx.session.string; - } - }); - - const server = app.listen(); - - request(server) - .post('/') - .expect(204, (err, res) => { - if (err) return done(err); - const cookie = res.headers['set-cookie']; - request(server) - .get('/') - .set('Cookie', cookie.join(';')) - .expect(';', done); - }); - }); - }); - - describe('new session', () => { - describe('when not accessed', () => { - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.body = 'greetings'; - }); - - request(app.listen()) - .get('/') - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - - describe('when accessed and not populated', () => { - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session; - ctx.body = 'greetings'; - }); - - request(app.listen()) - .get('/') - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - - describe('when populated', () => { - it('should Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.message = 'hello'; - ctx.body = ''; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess/) - .expect(200, (err, res) => { - if (err) return done(err); - cookie = res.header['set-cookie'].join(';'); - done(); - }); - }); - - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.body = ctx.session; - }); - - request(app.listen()) - .get('/') - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - }); - - describe('saved session', () => { - describe('when not accessed', () => { - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.body = 'aklsdjflasdjf'; - }); - - request(app.listen()) - .get('/') - .set('Cookie', cookie) - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - - describe('when accessed but not changed', () => { - it('should be the same session', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.message.should.equal('hello'); - ctx.body = 'aklsdjflasdjf'; - }); - - request(app.listen()) - .get('/') - .set('Cookie', cookie) - .expect(200, done); - }); - - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.message.should.equal('hello'); - ctx.body = 'aklsdjflasdjf'; - }); - - request(app.listen()) - .get('/') - .set('Cookie', cookie) - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - - describe('when accessed and changed', () => { - it('should Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.money = '$$$'; - ctx.body = 'aklsdjflasdjf'; - }); - - request(app.listen()) - .get('/') - .set('Cookie', cookie) - .expect('Set-Cookie', /koa\.sess/) - .expect(200, done); - }); - }); - }); - - describe('when session is', () => { - describe('null', () => { - it('should expire the session', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session = null; - ctx.body = 'asdf'; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess/) - .expect(200, done); - }); - }); - - describe('an empty object', () => { - it('should not Set-Cookie', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session = {}; - ctx.body = 'asdf'; - }); - - request(app.listen()) - .get('/') - .expect(200, (err, res) => { - if (err) return done(err); - res.header.should.not.have.property('set-cookie'); - done(); - }); - }); - }); - - describe('an object', () => { - it('should create a session', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session = { message: 'hello' }; - ctx.body = 'asdf'; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess/) - .expect(200, done); - }); - }); - - describe('anything else', () => { - it('should throw', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session = 'asdf'; - }); - - request(app.listen()) - .get('/') - .expect(500, done); - }); - }); - }); - - describe('session', () => { - describe('.inspect()', () => { - it('should return session content', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.foo = 'bar'; - ctx.body = ctx.session[inspect](); - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess=.+;/) - .expect({ foo: 'bar' }) - .expect(200, done); - }); - }); - - describe('.length', () => { - it('should return session length', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.foo = 'bar'; - ctx.body = String(ctx.session.length); - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess=.+;/) - .expect('1') - .expect(200, done); - }); - }); - - describe('.populated', () => { - it('should return session populated', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.foo = 'bar'; - ctx.body = String(ctx.session.populated); - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess=.+;/) - .expect('true') - .expect(200, done); - }); - }); - - describe('.save()', () => { - it('should save session', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.save(); - ctx.body = 'hello'; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess=.+;/) - .expect('hello') - .expect(200, done); - }); - }); - }); - - describe('when an error is thrown downstream and caught upstream', () => { - it('should still save the session', done => { - const app = new Koa(); - - app.keys = [ 'a', 'b' ]; - - app.use(async function(ctx, next) { - try { - await next(); - } catch (err) { - ctx.status = err.status; - ctx.body = err.message; - } - }); - - app.use(session({ store }, app)); - - app.use(async function(ctx, next) { - ctx.session.name = 'funny'; - await next(); - }); - - app.use(async function(ctx) { - ctx.throw(401); - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /koa\.sess/) - .expect(401, done); - }); - }); - - describe('when maxAge present', () => { - describe('and set to be a session cookie', () => { - it('should not expire the session', done => { - const app = App({ maxAge: 'session' }); - - app.use(async function(ctx) { - if (ctx.method === 'POST') { - ctx.session.message = 'hi'; - ctx.body = 200; - return; - } - ctx.body = ctx.session.message; - }); - const server = app.listen(); - - request(server) - .post('/') - .expect('Set-Cookie', /koa\.sess/) - .end((err, res) => { - if (err) return done(err); - const cookie = res.headers['set-cookie'].join(';'); - cookie.should.not.containEql('expires='); - request(server) - .get('/') - .set('cookie', cookie) - .expect('hi', done); - }); - }); - it('should use the default maxAge when improper string given', done => { - const app = App({ maxAge: 'not the right string' }); - - app.use(async function(ctx) { - if (ctx.method === 'POST') { - ctx.session.message = 'hi'; - ctx.body = 200; - return; - } - ctx.body = ctx.session.message; - }); - const server = app.listen(); - - request(server) - .post('/') - .expect('Set-Cookie', /koa\.sess/) - .end((err, res) => { - if (err) return done(err); - const cookie = res.headers['set-cookie'].join(';'); - cookie.should.containEql('expires='); - request(server) - .get('/') - .set('cookie', cookie) - .expect('hi', done); - }); - }); - }); - describe('and not expire', () => { - it('should not expire the session', done => { - const app = App({ maxAge: 100 }); - - app.use(async function(ctx) { - if (ctx.method === 'POST') { - ctx.session.message = 'hi'; - ctx.body = 200; - return; - } - ctx.body = ctx.session.message; - }); - - const server = app.listen(); - - request(server) - .post('/') - .expect('Set-Cookie', /koa\.sess/) - .end((err, res) => { - if (err) return done(err); - const cookie = res.headers['set-cookie'].join(';'); - - request(server) - .get('/') - .set('cookie', cookie) - .expect('hi', done); - }); - }); - }); - - describe('and expired', () => { - it('should expire the sess', done => { - done = pedding(done, 2); - const app = App({ maxAge: 100 }); - app.on('session:expired', args => { - assert(args.key.match(/^\w+-/)); - assert(args.value); - assert(args.ctx); - done(); - }); - app.use(async function(ctx) { - if (ctx.method === 'POST') { - ctx.session.message = 'hi'; - ctx.status = 200; - return; - } - - ctx.body = ctx.session.message || ''; - }); - - const server = app.listen(); - - request(server) - .post('/') - .expect('Set-Cookie', /koa\.sess/) - .end((err, res) => { - if (err) return done(err); - const cookie = res.headers['set-cookie'].join(';'); - - setTimeout(() => { - request(server) - .get('/') - .set('cookie', cookie) - .expect('', done); - }, 200); - }); - }); - }); - }); - - describe('ctx.session.maxAge', () => { - it('should return opt.maxAge', done => { - const app = App({ maxAge: 100 }); - - app.use(async function(ctx) { - ctx.body = ctx.session.maxAge; - }); - - request(app.listen()) - .get('/') - .expect('100', done); - }); - }); - - describe('ctx.session.maxAge=', () => { - it('should set sessionOptions.maxAge', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.foo = 'bar'; - ctx.session.maxAge = 100; - ctx.body = ctx.session.foo; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /expires=/) - .expect(200, done); - }); - - it('should save even session not change', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session.maxAge = 100; - ctx.body = ctx.session; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /expires=/) - .expect(200, done); - }); - - it('should save when create session only with maxAge', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.session = { maxAge: 100 }; - ctx.body = ctx.session; - }); - - request(app.listen()) - .get('/') - .expect('Set-Cookie', /expires=/) - .expect(200, done); - }); - }); - - describe('ctx.session.regenerate', () => { - it('should change the session key, but not content', done => { - const app = new App(); - const message = 'hi'; - app.use(async function(ctx, next) { - ctx.session = { message: 'hi' }; - await next(); - }); - - app.use(async function(ctx, next) { - const sessionKey = ctx.cookies.get('koa.sess'); - if (sessionKey) { - await ctx.session.regenerate(); - } - await next(); - }); - - app.use(async function(ctx) { - ctx.session.message.should.equal(message); - ctx.body = ''; - }); - let koaSession = null; - request(app.callback()) - .get('/') - .expect(200, (err, res) => { - should.not.exist(err); - koaSession = res.headers['set-cookie'][0]; - koaSession.should.containEql('koa.sess='); - request(app.callback()) - .get('/') - .set('Cookie', koaSession) - .expect(200, (err, res) => { - should.not.exist(err); - const cookies = res.headers['set-cookie'][0]; - cookies.should.containEql('koa.sess='); - cookies.should.not.equal(koaSession); - done(); - }); - }); - }); - }); - - describe('when store return empty', () => { - it('should create new Session', done => { - done = pedding(done, 2); - const app = App({ signed: false }); - - app.use(async function(ctx) { - ctx.body = String(ctx.session.isNew); - }); - - app.on('session:missed', args => { - assert(args.key === 'invalid-key'); - assert(args.ctx); - done(); - }); - - request(app.listen()) - .get('/') - .set('cookie', 'koa.sess=invalid-key') - .expect('true') - .expect(200, done); - }); - }); - - describe('when valid and beforeSave set', () => { - it('should ignore session when uid changed', done => { - done = pedding(done, 2); - const app = new Koa(); - - app.keys = [ 'a', 'b' ]; - app.use(session({ - valid(ctx, sess) { - return ctx.cookies.get('uid') === sess.uid; - }, - beforeSave(ctx, sess) { - sess.uid = ctx.cookies.get('uid'); - }, - store, - }, app)); - app.use(async function(ctx) { - if (!ctx.session.foo) { - ctx.session.foo = Date.now() + '|uid:' + ctx.cookies.get('uid'); - } - - ctx.body = { - foo: ctx.session.foo, - uid: ctx.cookies.get('uid'), - }; - }); - app.on('session:invalid', args => { - assert(args.key); - assert(args.value); - assert(args.ctx); - done(); - }); - - request(app.callback()) - .get('/') - .set('Cookie', 'uid=123') - .expect(200, (err, res) => { - should.not.exist(err); - const data = res.body; - const cookies = res.headers['set-cookie'].join(';'); - cookies.should.containEql('koa.sess='); - - request(app.callback()) - .get('/') - .set('Cookie', cookies + ';uid=123') - .expect(200) - .expect(data, err => { - should.not.exist(err); - - // should ignore uid:123 session and create a new session for uid:456 - request(app.callback()) - .get('/') - .set('Cookie', cookies + ';uid=456') - .expect(200, (err, res) => { - should.not.exist(err); - res.body.uid.should.equal('456'); - res.body.foo.should.not.equal(data.foo); - done(); - }); - }); - }); - }); - }); - - describe('ctx.session', () => { - after(mm.restore); - - it('can be mocked', done => { - const app = App(); - - app.use(async function(ctx) { - ctx.body = ctx.session; - }); - - mm(app.context, 'session', { - foo: 'bar', - }); - - request(app.listen()) - .get('/') - .expect({ - foo: 'bar', - }) - .expect(200, done); - }); - }); - - describe('when rolling set to true', () => { - let app; - before(() => { - app = App({ rolling: true }); - - app.use(async ctx => { - if (ctx.path === '/set') ctx.session = { foo: 'bar' }; - ctx.body = ctx.session; - }); - }); - - it('should not send set-cookie when session not exists', () => { - return request(app.callback()) - .get('/') - .expect({}) - .expect(res => { - should.not.exist(res.headers['set-cookie']); - }); - }); - - it('should send set-cookie when session exists and not change', done => { - request(app.callback()) - .get('/set') - .expect({ foo: 'bar' }) - .end((err, res) => { - should.not.exist(err); - res.headers['set-cookie'].should.have.length(2); - const cookie = res.headers['set-cookie'].join(';'); - request(app.callback()) - .get('/') - .set('cookie', cookie) - .expect(res => { - res.headers['set-cookie'].should.have.length(2); - }) - .expect({ foo: 'bar' }, done); - }); - }); - }); - - describe('when prefix present', () => { - it('should still work', done => { - const app = App({ prefix: 'sess:' }); - - app.use(async ctx => { - if (ctx.method === 'POST') { - ctx.session.string = ';'; - ctx.status = 204; - } else { - ctx.body = ctx.session.string; - } - }); - - const server = app.listen(); - - request(server) - .post('/') - .expect(204, (err, res) => { - if (err) return done(err); - const cookie = res.headers['set-cookie']; - cookie.join().should.match(/koa\.sess=sess:/); - request(server) - .get('/') - .set('Cookie', cookie.join(';')) - .expect(';', done); - }); - }); - }); - - describe('when renew set to true', () => { - let app; - before(() => { - app = App({ renew: true, maxAge: 2000 }); - - app.use(async ctx => { - if (ctx.path === '/set') ctx.session = { foo: 'bar' }; - ctx.body = ctx.session; - }); - }); - - it('should not send set-cookie when session not exists', () => { - return request(app.callback()) - .get('/') - .expect({}) - .expect(res => { - should.not.exist(res.headers['set-cookie']); - }); - }); - - it('should send set-cookie when session near expire and not change', async () => { - let res = await request(app.callback()) - .get('/set') - .expect({ foo: 'bar' }); - - res.headers['set-cookie'].should.have.length(2); - const cookie = res.headers['set-cookie'].join(';'); - await sleep(1200); - res = await request(app.callback()) - .get('/') - .set('cookie', cookie) - .expect({ foo: 'bar' }); - res.headers['set-cookie'].should.have.length(2); - }); - - it('should not send set-cookie when session not near expire and not change', async () => { - let res = await request(app.callback()) - .get('/set') - .expect({ foo: 'bar' }); - - res.headers['set-cookie'].should.have.length(2); - const cookie = res.headers['set-cookie'].join(';'); - await sleep(500); - res = await request(app.callback()) - .get('/') - .set('cookie', cookie) - .expect({ foo: 'bar' }); - should.not.exist(res.headers['set-cookie']); - }); - }); - - describe('when get session before middleware', () => { - it('should return empty session', async () => { - const app = new Koa(); - app.keys = [ 'a', 'b' ]; - const options = {}; - options.store = store; - app.use(async (ctx, next) => { - // will not take effect - ctx.session.should.be.ok(); - ctx.session.foo = '123'; - await next(); - }); - app.use(session(options, app)); - app.use(async ctx => { - if (ctx.path === '/set') ctx.session = { foo: 'bar' }; - ctx.body = ctx.session; - }); - - let res = await request(app.callback()) - .get('/') - .expect({}); - - res = await request(app.callback()) - .get('/set') - .expect({ foo: 'bar' }); - - res.headers['set-cookie'].should.have.length(2); - const cookie = res.headers['set-cookie'].join(';'); - await sleep(1200); - res = await request(app.callback()) - .get('/') - .set('cookie', cookie) - .expect({ foo: 'bar' }); - }); - }); -}); - -function App(options) { - const app = new Koa(); - app.keys = [ 'a', 'b' ]; - options = options || {}; - options.store = store; - app.use(session(options, app)); - return app; -} diff --git a/test/store.test.ts b/test/store.test.ts new file mode 100644 index 0000000..39ba1b8 --- /dev/null +++ b/test/store.test.ts @@ -0,0 +1,800 @@ +import { strict as assert } from 'node:assert'; +import { scheduler } from 'node:timers/promises'; +import Koa from 'koa'; +import { request } from '@eggjs/supertest'; +import { mm } from 'mm'; +import session, { type CreateSessionOptions } from '../src/index.js'; +import store from './store.js'; + +const inspect = Symbol.for('nodejs.util.inspect.custom'); + +function App(options: CreateSessionOptions = {}) { + const app = new Koa(); + app.keys = [ 'a', 'b' ]; + options.store = store; + app.use(session(options, app)); + return app; +} + +describe('Koa Session External Store', () => { + let cookie: string; + + describe('when the session contains a ;', () => { + it('should still work', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + if (ctx.method === 'POST') { + ctx.session!.string = ';'; + ctx.status = 204; + } else { + ctx.body = ctx.session!.string; + } + }); + + const server = app.callback(); + const res = await request(server) + .post('/') + .expect(204); + + const cookie = res.get('Set-Cookie')!; + await request(server) + .get('/') + .set('Cookie', cookie.join(';')) + .expect(';'); + }); + }); + + describe('new session', () => { + describe('when not accessed', () => { + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.body = 'greetings'; + }); + + const res = await request(app.callback()) + .get('/') + .expect(200); + + assert(!res.headers['set-cookie']); + }); + }); + + describe('when accessed and not populated', () => { + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session; + ctx.body = 'greetings'; + }); + + const res = await request(app.callback()) + .get('/') + .expect(200); + + assert(!res.headers['set-cookie']); + }); + }); + + describe('when populated', () => { + it('should Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.message = 'hello'; + ctx.body = ''; + }); + + const res = await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + + cookie = res.get('Set-Cookie')!.join(';'); + }); + + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.body = ctx.session; + }); + + const res = await request(app.callback()) + .get('/') + .expect(200); + + assert(!res.headers['set-cookie']); + }); + }); + }); + + describe('saved session', () => { + describe('when not accessed', () => { + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.body = 'aklsdjflasdjf'; + }); + + const res = await request(app.callback()) + .get('/') + .set('Cookie', cookie) + .expect(200); + + assert(!res.headers['set-cookie']); + }); + }); + + describe('when accessed but not changed', () => { + it('should be the same session', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + assert.equal(ctx.session!.message, 'hello'); + ctx.body = 'aklsdjflasdjf'; + }); + + await request(app.callback()) + .get('/') + .set('Cookie', cookie) + .expect(200); + }); + + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + assert.equal(ctx.session!.message, 'hello'); + ctx.body = 'aklsdjflasdjf'; + }); + + const res = await request(app.callback()) + .get('/') + .set('Cookie', cookie) + .expect(200); + + assert(!res.headers['set-cookie']); + }); + }); + + describe('when accessed and changed', () => { + it('should Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.money = '$$$'; + ctx.body = 'aklsdjflasdjf'; + }); + + await request(app.callback()) + .get('/') + .set('Cookie', cookie) + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + }); + }); + }); + + describe('when session is', () => { + describe('null', () => { + it('should expire the session', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session = null; + ctx.body = 'asdf'; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + }); + }); + + describe('an empty object', () => { + it('should not Set-Cookie', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session = {}; + ctx.body = 'asdf'; + }); + + const res = await request(app.callback()) + .get('/') + .expect(200); + + assert(!res.headers['set-cookie']); + }); + }); + + describe('an object', () => { + it('should create a session', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session = { message: 'hello' }; + ctx.body = 'asdf'; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(200); + }); + }); + + describe('anything else', () => { + it('should throw', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session = 'asdf'; + }); + + await request(app.callback()) + .get('/') + .expect(500); + }); + }); + }); + + describe('session', () => { + describe('.inspect()', () => { + it('should return session content', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.foo = 'bar'; + ctx.body = ctx.session![inspect](); + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess=.+;/) + .expect({ foo: 'bar' }) + .expect(200); + }); + }); + + describe('.length', () => { + it('should return session length', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.foo = 'bar'; + ctx.body = String(ctx.session!.length); + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess=.+;/) + .expect('1') + .expect(200); + }); + }); + + describe('.populated', () => { + it('should return session populated', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.foo = 'bar'; + ctx.body = String(ctx.session!.populated); + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess=.+;/) + .expect('true') + .expect(200); + }); + }); + + describe('.save()', () => { + it('should save session', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.save(); + ctx.body = 'hello'; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess=.+;/) + .expect('hello') + .expect(200); + }); + }); + }); + + describe('when an error is thrown downstream and caught upstream', () => { + it('should still save the session', async () => { + const app = new Koa(); + + app.keys = [ 'a', 'b' ]; + + app.use(async (ctx: Koa.Context, next: Koa.Next) => { + try { + await next(); + } catch (err: any) { + ctx.status = err.status; + ctx.body = err.message; + } + }); + + app.use(session({ store }, app)); + + app.use(async (ctx: Koa.Context, next: Koa.Next) => { + ctx.session!.name = 'funny'; + await next(); + }); + + app.use(async (ctx: Koa.Context) => { + ctx.throw(401); + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /koa\.sess/) + .expect(401); + }); + }); + + describe('when maxAge present', () => { + describe('and set to be a session cookie', () => { + it('should not expire the session', async () => { + const app = App({ maxAge: 'session' }); + + app.use(async (ctx: Koa.Context) => { + if (ctx.method === 'POST') { + ctx.session!.message = 'hi'; + ctx.body = 200; + return; + } + ctx.body = ctx.session!.message; + }); + + const server = app.callback(); + const res = await request(server) + .post('/') + .expect('Set-Cookie', /koa\.sess/); + + const cookie = res.get('Set-Cookie')!.join(';'); + assert(!cookie.includes('expires=')); + + await request(server) + .get('/') + .set('cookie', cookie) + .expect('hi'); + }); + }); + + describe('and not expire', () => { + it('should not expire the session', async () => { + const app = App({ maxAge: 100 }); + + app.use(async (ctx: Koa.Context) => { + if (ctx.method === 'POST') { + ctx.session!.message = 'hi'; + ctx.body = 200; + return; + } + ctx.body = ctx.session!.message; + }); + + const server = app.callback(); + const res = await request(server) + .post('/') + .expect('Set-Cookie', /koa\.sess/); + + const cookie = res.get('Set-Cookie')!.join(';'); + + await request(server) + .get('/') + .set('cookie', cookie) + .expect('hi'); + }); + }); + + describe('and expired', () => { + it('should expire the sess', async () => { + const app = App({ maxAge: 100 }); + app.on('session:expired', args => { + assert(args.key.match(/^\w+-/)); + assert(args.value); + assert(args.ctx); + }); + + app.use(async (ctx: Koa.Context) => { + if (ctx.method === 'POST') { + ctx.session!.message = 'hi'; + ctx.status = 200; + return; + } + + ctx.body = ctx.session!.message || ''; + }); + + const server = app.callback(); + const res = await request(server) + .post('/') + .expect('Set-Cookie', /koa\.sess/); + + const cookie = res.get('Set-Cookie')!.join(';'); + + await new Promise(resolve => setTimeout(resolve, 200)); + + await request(server) + .get('/') + .set('cookie', cookie) + .expect(''); + }); + }); + }); + + describe('ctx.session.maxAge', () => { + it('should return opt.maxAge', async () => { + const app = App({ maxAge: 100 }); + + app.use(async (ctx: Koa.Context) => { + ctx.body = ctx.session!.maxAge; + }); + + await request(app.callback()) + .get('/') + .expect('100'); + }); + }); + + describe('ctx.session.maxAge=', () => { + it('should set sessionOptions.maxAge', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.foo = 'bar'; + ctx.session!.maxAge = 100; + ctx.body = ctx.session!.foo; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /expires=/) + .expect(200); + }); + + it('should save even session not change', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session!.maxAge = 100; + ctx.body = ctx.session; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /expires=/) + .expect(200); + }); + + it('should save when create session only with maxAge', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.session = { maxAge: 100 }; + ctx.body = ctx.session; + }); + + await request(app.callback()) + .get('/') + .expect('Set-Cookie', /expires=/) + .expect(200); + }); + }); + + describe('ctx.session.regenerate', () => { + it('should change the session key, but not content', async () => { + const app = App(); + const message = 'hi'; + + app.use(async (ctx: Koa.Context, next: Koa.Next) => { + ctx.session = { message: 'hi' }; + await next(); + }); + + app.use(async (ctx: Koa.Context, next: Koa.Next) => { + const sessionKey = ctx.cookies.get('koa.sess'); + if (sessionKey) { + await ctx.session!.regenerate(); + } + await next(); + }); + + app.use(async (ctx: Koa.Context) => { + assert.equal(ctx.session!.message, message); + ctx.body = ''; + }); + + let res = await request(app.callback()) + .get('/') + .expect(200); + + const koaSession = res.get('Set-Cookie')!.join(';'); + assert.match(koaSession, /koa\.sess=/); + res = await request(app.callback()) + .get('/') + .set('Cookie', koaSession) + .expect(200); + + const cookies = res.get('Set-Cookie')!.join(';'); + assert.match(cookies, /koa\.sess=/); + assert.notEqual(cookies, koaSession); + }); + }); + + describe('when store return empty', () => { + it('should create new Session', async () => { + const app = App({ signed: false }); + + app.use(async (ctx: Koa.Context) => { + ctx.body = String(ctx.session!.isNew); + }); + + app.on('session:missed', args => { + assert.equal(args.key, 'invalid-key'); + assert(args.ctx); + }); + + await request(app.callback()) + .get('/') + .set('cookie', 'koa.sess=invalid-key') + .expect('true') + .expect(200); + }); + }); + + describe('when valid and beforeSave set', () => { + it('should ignore session when uid changed', async () => { + const app = new Koa(); + + app.keys = [ 'a', 'b' ]; + app.use(session({ + valid(ctx, sess) { + return ctx.cookies.get('uid') === sess.uid; + }, + beforeSave(ctx, sess) { + sess.uid = ctx.cookies.get('uid'); + }, + store, + }, app)); + + app.use(async (ctx: Koa.Context) => { + if (!ctx.session!.foo) { + ctx.session!.foo = Date.now() + '|uid:' + ctx.cookies.get('uid'); + } + + ctx.body = { + foo: ctx.session!.foo, + uid: ctx.cookies.get('uid'), + }; + }); + + app.on('session:invalid', args => { + assert(args.key); + assert(args.value); + assert(args.ctx); + }); + + let res = await request(app.callback()) + .get('/') + .set('Cookie', 'uid=123') + .expect(200); + + const data = res.body; + const cookies = res.get('Set-Cookie')!.join(';'); + assert(cookies.includes('koa.sess=')); + + res = await request(app.callback()) + .get('/') + .set('Cookie', cookies + ';uid=123') + .expect(200); + + assert.deepEqual(res.body, data); + + res = await request(app.callback()) + .get('/') + .set('Cookie', cookies + ';uid=456') + .expect(200); + + assert.equal(res.body.uid, '456'); + assert.notEqual(res.body.foo, data.foo); + }); + }); + + describe('ctx.session', () => { + after(mm.restore); + + it('can be mocked', async () => { + const app = App(); + + app.use(async (ctx: Koa.Context) => { + ctx.body = ctx.session; + }); + + mm(app.context, 'session', { + foo: 'bar', + }); + + await request(app.callback()) + .get('/') + .expect({ + foo: 'bar', + }) + .expect(200); + }); + }); + + describe('when rolling set to true', () => { + let app: Koa; + before(() => { + app = App({ rolling: true }); + + app.use(async (ctx: Koa.Context) => { + if (ctx.path === '/set') ctx.session = { foo: 'bar' }; + ctx.body = ctx.session; + }); + }); + + it('should not send set-cookie when session not exists', async () => { + const res = await request(app.callback()) + .get('/') + .expect({}); + + assert(!res.headers['set-cookie']); + }); + + it('should send set-cookie when session exists and not change', async () => { + let res = await request(app.callback()) + .get('/set') + .expect({ foo: 'bar' }); + + assert.equal(res.headers['set-cookie'].length, 2); + const cookie = res.get('Set-Cookie')!.join(';'); + + res = await request(app.callback()) + .get('/') + .set('cookie', cookie) + .expect({ foo: 'bar' }); + + assert.equal(res.headers['set-cookie'].length, 2); + }); + }); + + describe('when prefix present', () => { + it('should still work', async () => { + const app = App({ prefix: 'sess:' }); + + app.use(async (ctx: Koa.Context) => { + if (ctx.method === 'POST') { + ctx.session!.string = ';'; + ctx.status = 204; + } else { + ctx.body = ctx.session!.string; + } + }); + + const server = app.callback(); + const res = await request(server) + .post('/') + .expect(204); + + const cookie = res.get('Set-Cookie')!; + assert(cookie.join().includes('koa.sess=sess:')); + + await request(server) + .get('/') + .set('Cookie', cookie.join(';')) + .expect(';'); + }); + }); + + describe('when renew set to true', () => { + let app: Koa; + before(() => { + app = App({ renew: true, maxAge: 2000 }); + + app.use(async (ctx: Koa.Context) => { + if (ctx.path === '/set') ctx.session = { foo: 'bar' }; + ctx.body = ctx.session; + }); + }); + + it('should not send set-cookie when session not exists', async () => { + const res = await request(app.callback()) + .get('/') + .expect({}); + + assert(!res.headers['set-cookie']); + }); + + it('should send set-cookie when session near expire and not change', async () => { + let res = await request(app.callback()) + .get('/set') + .expect({ foo: 'bar' }); + + assert.equal(res.get('Set-Cookie')!.length, 2); + const cookie = res.get('Set-Cookie')!.join(';'); + + await scheduler.wait(1200); + + res = await request(app.callback()) + .get('/') + .set('cookie', cookie) + .expect({ foo: 'bar' }); + + assert.equal(res.headers['set-cookie'].length, 2); + }); + + it('should not send set-cookie when session not near expire and not change', async () => { + let res = await request(app.callback()) + .get('/set') + .expect({ foo: 'bar' }); + + assert.equal(res.headers['set-cookie'].length, 2); + const cookie = res.get('Set-Cookie')!.join(';'); + + await scheduler.wait(500); + + res = await request(app.callback()) + .get('/') + .set('cookie', cookie) + .expect({ foo: 'bar' }); + + assert(!res.headers['set-cookie']); + }); + }); + + describe('when get session before middleware', () => { + it('should return empty session', async () => { + const app = new Koa(); + app.keys = [ 'a', 'b' ]; + const options = { store }; + + app.use(async (ctx: Koa.Context, next: Koa.Next) => { + assert(ctx.session); + ctx.session.foo = '123'; + await next(); + }); + + app.use(session(options, app)); + + app.use(async (ctx: Koa.Context) => { + if (ctx.path === '/set') ctx.session = { foo: 'bar' }; + ctx.body = ctx.session; + }); + + let res = await request(app.callback()) + .get('/') + .expect({}); + + res = await request(app.callback()) + .get('/set') + .expect({ foo: 'bar' }); + + const cookie = res.get('Set-Cookie')!.join(';'); + await scheduler.wait(1200); + + res = await request(app.callback()) + .get('/') + .set('cookie', cookie) + .expect({ foo: 'bar' }); + }); + }); +}); diff --git a/test/store.ts b/test/store.ts new file mode 100644 index 0000000..43ccf80 --- /dev/null +++ b/test/store.ts @@ -0,0 +1,15 @@ +const sessions: Record = {}; + +export default { + async get(key: string) { + return sessions[key]; + }, + + async set(key: string, value: unknown) { + sessions[key] = value; + }, + + async destroy(key: string) { + sessions[key] = undefined; + }, +}; diff --git a/test/store_with_ctx.test.js b/test/store_with_ctx.test.ts similarity index 59% rename from test/store_with_ctx.test.js rename to test/store_with_ctx.test.ts index 21fe43a..e7331a5 100644 --- a/test/store_with_ctx.test.js +++ b/test/store_with_ctx.test.ts @@ -1,40 +1,49 @@ -'use strict'; +import { strict as assert } from 'node:assert'; +import Koa from 'koa'; +import { request } from '@eggjs/supertest'; +import session, { type CreateSessionOptions } from '../src/index.js'; +import store from './store_with_ctx.js'; -const Koa = require('koa'); -const request = require('supertest'); -const session = require('..'); -const store = require('./store_with_ctx'); +function App(options: CreateSessionOptions = {}) { + const app = new Koa(); + app.keys = [ 'a', 'b' ]; + options.store = store; + app.use(async (ctx, next) => { + await next(); + ctx.body = ctx.state.test === undefined ? 'undefined' : ctx.state.test; + }); + + app.use(session(options, app)); + return app; +} -describe('Koa Session External Store methods can acceess Koa context', () => { - let cookie; +describe('Koa Session External Store methods can access Koa context', () => { + let cookie: string; describe('new session', () => { describe('when not accessed', () => { it('should not set ctx.state.test variable', async () => { const app = App(); - request(app.callback()) + await request(app.callback()) .get('/') .expect('undefined'); }); }); describe('when populated', () => { - it('should set ctx.state.test variable', done => { + it('should set ctx.state.test variable', async () => { const app = App(); app.use(async ctx => { if (ctx.path === '/set') ctx.session = { foo: 'bar' }; }); - request(app.listen()) - .get('/set') - .expect(200, (err, res) => { - if (err) return done(err); - cookie = res.header['set-cookie'].join(';'); - res.text.should.equal('set'); - done(); - }); + const res = await request(app.callback()) + .get('/set') + .expect(200); + cookie = res.get('Set-Cookie')!.join(';'); + assert.equal(res.text, 'set'); }); }); @@ -42,7 +51,7 @@ describe('Koa Session External Store methods can acceess Koa context', () => { it('should access ctx.state.test variable', async () => { const app = App(); - request(app.callback()) + await request(app.callback()) .get('/') .set('Cookie', cookie) .expect('get'); @@ -54,10 +63,12 @@ describe('Koa Session External Store methods can acceess Koa context', () => { const app = App(); app.use(async ctx => { - if (ctx.path === '/destroy') ctx.session = null; + if (ctx.path === '/destroy') { + ctx.session = null; + } }); - request(app.callback()) + await request(app.callback()) .get('/destroy') .set('Cookie', cookie) .expect('destroyed'); @@ -65,17 +76,3 @@ describe('Koa Session External Store methods can acceess Koa context', () => { }); }); }); - -function App(options) { - const app = new Koa(); - app.keys = [ 'a', 'b' ]; - options = options || {}; - options.store = store; - app.use(async (ctx, next) => { - await next(); - ctx.body = ctx.state.test === undefined ? 'undefined' : ctx.state.test; - }); - - app.use(session(options, app)); - return app; -} diff --git a/test/store_with_ctx.js b/test/store_with_ctx.ts similarity index 55% rename from test/store_with_ctx.js rename to test/store_with_ctx.ts index 2ebd234..7e50c7b 100644 --- a/test/store_with_ctx.js +++ b/test/store_with_ctx.ts @@ -1,21 +1,19 @@ -'use strict'; +const sessions: Record = {}; -const sessions = {}; - -module.exports = { - async get(key, maxAge, options) { +export default { + async get(key: string, _maxAge: number, options: any) { // check access to options.ctx options.ctx.state.test = 'get'; return sessions[key]; }, - async set(key, sess, maxAge, options) { + async set(key: string, sess: Record, _maxAge: number, options: any) { // check access to options.ctx options.ctx.state.test = 'set'; sessions[key] = sess; }, - async destroy(key, options) { + async destroy(key: string, options: any) { // check access to options.ctx options.ctx.state.test = 'destroyed'; sessions[key] = undefined; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ff41b73 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@eggjs/tsconfig", + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +} From 6d068ffcb7a0976a8da6d70f32624575f9061393 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 19 Jan 2025 07:32:03 +0000 Subject: [PATCH 20/25] Release 7.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] ## [7.0.0](https://github.com/koajs/session/compare/v6.4.0...v7.0.0) (2025-01-19) ### ⚠ BREAKING CHANGES * drop Node.js < 18.19.0 support ### Features * support cjs and esm both by tshy ([#228](https://github.com/koajs/session/issues/228)) ([575864c](https://github.com/koajs/session/commit/575864c6ae7504da15e72e8112a99757f3eee188)) --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e86323..f2a9a24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# Changelog + +## [7.0.0](https://github.com/koajs/session/compare/v6.4.0...v7.0.0) (2025-01-19) + + +### ⚠ BREAKING CHANGES + +* drop Node.js < 18.19.0 support + +### Features + +* support cjs and esm both by tshy ([#228](https://github.com/koajs/session/issues/228)) ([575864c](https://github.com/koajs/session/commit/575864c6ae7504da15e72e8112a99757f3eee188)) 6.4.0 / 2023-02-04 ================== diff --git a/package.json b/package.json index 725d6c3..eea068b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "git", "url": "git@github.com:koajs/session.git" }, - "version": "6.4.0", + "version": "7.0.0", "keywords": [ "koa", "middleware", From 3421008381a3aacf2e43d52314749eb850851193 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sun, 19 Jan 2025 16:16:45 +0800 Subject: [PATCH 21/25] fix: remove unused dependencies (#229) --- __snapshots__/index.test.ts.js | 9 +++++++++ package.json | 3 +-- src/index.ts | 8 ++++---- test/index.test.ts | 11 +++++++++++ 4 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 __snapshots__/index.test.ts.js create mode 100644 test/index.test.ts diff --git a/__snapshots__/index.test.ts.js b/__snapshots__/index.test.ts.js new file mode 100644 index 0000000..6b9c1e0 --- /dev/null +++ b/__snapshots__/index.test.ts.js @@ -0,0 +1,9 @@ +exports['test/index.test.ts SessionOptions schema should have a valid schema 1'] = { + "key": "koa.sess", + "autoCommit": true, + "overwrite": true, + "httpOnly": true, + "signed": true, + "rolling": false, + "renew": false +} diff --git a/package.json b/package.json index eea068b..c92a61c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "koa": "2", "mm": "4", "rimraf": "6", + "snap-shot-it": "^7.9.10", "tshy": "3", "tshy-after": "1", "typescript": "5" @@ -33,9 +34,7 @@ "license": "MIT", "dependencies": { "crc": "^3.8.0", - "debug": "^4.3.3", "is-type-of": "^2.2.0", - "uuid": "^8.3.2", "zod": "^3.24.1" }, "engines": { diff --git a/src/index.ts b/src/index.ts index 0652ffe..e116660 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ const debug = debuglog('koa-session'); const GET_CONTEXT_SESSION = Symbol('get contextSession'); const CONTEXT_SESSION_INSTANCE = Symbol('contextSession instance'); -const SessionOptionsSchema = z.object({ +export const SessionOptions = z.object({ /** * cookie key * Default is `koa.sess` @@ -159,9 +159,9 @@ const SessionOptionsSchema = z.object({ .optional(), }); -const DEFAULT_SESSION_OPTIONS = SessionOptionsSchema.parse({}); +const DEFAULT_SESSION_OPTIONS = SessionOptions.parse({}); -export type SessionOptions = z.infer; +export type SessionOptions = z.infer; export type CreateSessionOptions = Partial; type Middleware = (ctx: any, next: any) => Promise; @@ -199,7 +199,7 @@ export function createSession(opts: CreateSessionOptions | any, app: any): Middl ...DEFAULT_SESSION_OPTIONS, ...opts, }; - SessionOptionsSchema.parse(options); + SessionOptions.parse(options); options = formatOptions(options); extendContext(app.context, options); diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 0000000..3b49ed6 --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,11 @@ +import snapshot from 'snap-shot-it'; +import { SessionOptions } from '../src/index.js'; + +describe('test/index.test.ts', () => { + describe('SessionOptions schema', () => { + it('should have a valid schema', () => { + const parsed = SessionOptions.parse({}); + snapshot(parsed); + }); + }); +}); From 6dfdcf675f28d2661bbeb59b9d42bf5c5afdb47f Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 19 Jan 2025 08:17:28 +0000 Subject: [PATCH 22/25] Release 7.0.1 [skip ci] ## [7.0.1](https://github.com/koajs/session/compare/v7.0.0...v7.0.1) (2025-01-19) ### Bug Fixes * remove unused dependencies ([#229](https://github.com/koajs/session/issues/229)) ([3421008](https://github.com/koajs/session/commit/3421008381a3aacf2e43d52314749eb850851193)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a9a24..0f1bd57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [7.0.1](https://github.com/koajs/session/compare/v7.0.0...v7.0.1) (2025-01-19) + + +### Bug Fixes + +* remove unused dependencies ([#229](https://github.com/koajs/session/issues/229)) ([3421008](https://github.com/koajs/session/commit/3421008381a3aacf2e43d52314749eb850851193)) + ## [7.0.0](https://github.com/koajs/session/compare/v6.4.0...v7.0.0) (2025-01-19) diff --git a/package.json b/package.json index c92a61c..2cfa6bc 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "git", "url": "git@github.com:koajs/session.git" }, - "version": "7.0.0", + "version": "7.0.1", "keywords": [ "koa", "middleware", From c45bf8b236f8b79f4d2e4ed40b7fae585f30330b Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sun, 19 Jan 2025 17:34:26 +0800 Subject: [PATCH 23/25] fix: make sure options instance is not mutated (#230) --- src/index.ts | 19 +++++++++++-------- test/store.test.ts | 40 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/src/index.ts b/src/index.ts index e116660..74ec11a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -188,19 +188,23 @@ export function createSession(opts: CreateSessionOptions | any, app: any): Middl throw new TypeError('app instance required: `session(opts, app)`'); } + const options: SessionOptions = opts ?? {}; + // back-compat maxage - if (opts && !('maxAge' in opts) && 'maxage' in opts) { - Reflect.set(opts, 'maxAge', Reflect.get(opts, 'maxage')); + if (!('maxAge' in options) && 'maxage' in options) { + Reflect.set(options, 'maxAge', Reflect.get(options, 'maxage')); if (process.env.NODE_ENV !== 'production') { - console.warn('DeprecationWarning: `maxage` option has been renamed to `maxAge`'); + console.warn('[koa-session] DeprecationWarning: `maxage` option has been renamed to `maxAge`'); } } - let options = { + + // keep backwards compatibility: make sure options instance is not mutated + Object.assign(options, { ...DEFAULT_SESSION_OPTIONS, - ...opts, - }; + ...options, + }); SessionOptions.parse(options); - options = formatOptions(options); + formatOptions(options); extendContext(app.context, options); return async function session(ctx: any, next: any) { @@ -264,7 +268,6 @@ function formatOptions(opts: SessionOptions) { opts.genid = () => randomUUID(); } } - return opts; } /** diff --git a/test/store.test.ts b/test/store.test.ts index 39ba1b8..3f300ab 100644 --- a/test/store.test.ts +++ b/test/store.test.ts @@ -11,7 +11,7 @@ const inspect = Symbol.for('nodejs.util.inspect.custom'); function App(options: CreateSessionOptions = {}) { const app = new Koa(); app.keys = [ 'a', 'b' ]; - options.store = store; + options.store = options.store ?? store; app.use(session(options, app)); return app; } @@ -21,14 +21,15 @@ describe('Koa Session External Store', () => { describe('when the session contains a ;', () => { it('should still work', async () => { - const app = App(); + const options: CreateSessionOptions = { store }; + const app = App(options); app.use(async (ctx: Koa.Context) => { if (ctx.method === 'POST') { - ctx.session!.string = ';'; + ctx.session.string = ';'; ctx.status = 204; } else { - ctx.body = ctx.session!.string; + ctx.body = ctx.session.string; } }); @@ -43,6 +44,37 @@ describe('Koa Session External Store', () => { .set('Cookie', cookie.join(';')) .expect(';'); }); + + it('should disable store on options', async () => { + const options: CreateSessionOptions = { store }; + const app = App(options); + + app.use(async (ctx: Koa.Context) => { + if (ctx.method === 'POST') { + ctx.session.string = ';'; + ctx.status = 204; + } else { + ctx.body = ctx.session.string ?? 'new session create'; + } + }); + + const server = app.callback(); + const res = await request(server) + .post('/') + .expect(204); + + const cookie = res.get('Set-Cookie')!; + await request(server) + .get('/') + .set('Cookie', cookie.join(';')) + .expect(';'); + + options.store = undefined; + await request(server) + .get('/') + .set('Cookie', cookie.join(';')) + .expect('new session create'); + }); }); describe('new session', () => { From 36beadadf9db645ae5a0c8544544e4e6cfb203cc Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 19 Jan 2025 09:35:09 +0000 Subject: [PATCH 24/25] Release 7.0.2 [skip ci] ## [7.0.2](https://github.com/koajs/session/compare/v7.0.1...v7.0.2) (2025-01-19) ### Bug Fixes * make sure options instance is not mutated ([#230](https://github.com/koajs/session/issues/230)) ([c45bf8b](https://github.com/koajs/session/commit/c45bf8b236f8b79f4d2e4ed40b7fae585f30330b)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f1bd57..10dfcc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [7.0.2](https://github.com/koajs/session/compare/v7.0.1...v7.0.2) (2025-01-19) + + +### Bug Fixes + +* make sure options instance is not mutated ([#230](https://github.com/koajs/session/issues/230)) ([c45bf8b](https://github.com/koajs/session/commit/c45bf8b236f8b79f4d2e4ed40b7fae585f30330b)) + ## [7.0.1](https://github.com/koajs/session/compare/v7.0.0...v7.0.1) (2025-01-19) diff --git a/package.json b/package.json index 2cfa6bc..808789e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "git", "url": "git@github.com:koajs/session.git" }, - "version": "7.0.1", + "version": "7.0.2", "keywords": [ "koa", "middleware", From 968ae94b8a1f3f8ed53cb1e6dbab1c1c6704329f Mon Sep 17 00:00:00 2001 From: "Sixia \"Leask\" Huang" Date: Sun, 6 Apr 2025 18:00:42 -0400 Subject: [PATCH 25/25] Update Readme.md (#231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Providing `nul` as `sameSite` will lead to errors. Zod requires sameSite to be a string, so the description in the documentation was inaccurate and has been corrected. ``` ZodError: [ { "code": "invalid_type", "expected": "string", "received": "null", "path": [ "sameSite" ], "message": "Expected string, received null" } ] ``` ## Checklist - [✅] I have ensured my pull request is not behind the main or master branch of the original repository. - [✅] I have rebased all commits where necessary so that reviewing this pull request can be done without having to merge it first. - [✅] I have written a commit message that passes commitlint linting. - [✅] I have ensured that my code changes pass linting tests. - [✅] I have ensured that my code changes pass unit tests. - [✅] I have described my pull request and the reasons for code changes along with context if necessary. --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 34ee099..61c14ac 100644 --- a/Readme.md +++ b/Readme.md @@ -58,7 +58,7 @@ const CONFIG = { rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. (default is false) */ renew: false, /** (boolean) renew session when session is nearly expired, so we can always keep user logged in. (default is false)*/ secure: true, /** (boolean) secure cookie*/ - sameSite: null, /** (string) session cookie sameSite options (default null, don't set it) */ + sameSite: null, /** (string) session cookie sameSite options (default null, do not provide this key if you are not restricting sameSite) */ }; app.use(session(CONFIG, app));