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 new file mode 100644 index 0000000..c70132d --- /dev/null +++ b/.github/workflows/nodejs.yml @@ -0,0 +1,17 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + 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/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ec9e856..0000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -sudo: false -language: node_js -node_js: - - '7' - - '8' -script: 'npm run test-travis' -after_script: 'npm install coveralls@2 && cat ./coverage/lcov.info | coveralls' diff --git a/History.md b/CHANGELOG.md similarity index 56% rename from History.md rename to CHANGELOG.md index 22f0972..10dfcc6 100644 --- a/History.md +++ b/CHANGELOG.md @@ -1,3 +1,123 @@ +# 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) + + +### 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) + + +### ⚠ 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 +================== + +**features** + * [[`4cd3bef`](http://github.com/koajs/session/commit/4cd3bef4fc1900b847d2e133e3b2599a711f1aea)] - feat: Add Session.regenerate() method (#221) (Jürg Lehni <>) + +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 +================== + +**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 +================== + +**features** + * [[`7cde341`](http://github.com/koajs/session/commit/7cde341db1691ee6885eb1b11ff3a3a3632fd5ce)] - feat: add session.externalKey (#207) (Yiyu He <>) + +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 +================== + +**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 +================== + +**fixes** + * [[`ecd1f5e`](http://github.com/koajs/session/commit/ecd1f5edaa6ff1e77cc461d1107432b394ce21d2)] - fix: don't set any value to sameSite by default (#194) (fengmk2 <>) + +5.13.0 / 2020-02-01 +================== + +**features** + * [[`cb09a09`](http://github.com/koajs/session/commit/cb09a09cfa4767610d7cc7282a0de2a3a651c6ae)] - feat: support session cookie sameSite options (#193) (fengmk2 <>) + +5.12.3 / 2019-08-23 +================== + +**fixes** + * [[`909d93f`](http://github.com/koajs/session/commit/909d93fc6b74c6e29b0e83f555f1fc4002a6a108)] - fix: correctly expire cookies for nullified sessions (Justin <>) + +5.12.2 / 2019-07-10 +================== + +**fixes** + * [[`c23bab4`](http://github.com/koajs/session/commit/c23bab4023b95c65be46b4eeaf089608ddaa738e)] - fix: remvoe unused code (dead-horse <>) + +5.12.1 / 2019-07-10 +================== + +**fixes** + * [[`77968e3`](http://github.com/koajs/session/commit/77968e3ff6fb5d4f4a36665474ccd992fed689ec)] - fix: ensure ctx.session always has value (dead-horse <>) + +5.12.0 / 2019-05-17 +================== + +**features** + * [[`39ca830`](http://github.com/koajs/session/commit/39ca830a99ae7fcab2bd499a1f2a87de53fd1944)] - feat: add the parameter "ctx" to the function "genid" so can get the … (#173) (松松 <<1733458402@qq.com>>) + +**others** + * [[`3d57a44`](http://github.com/koajs/session/commit/3d57a443c7e0050d4066c871bf8da2656cda99f1)] - docs: add genid(ctx) in readme (dead-horse <>) + +5.11.0 / 2019-04-29 +================== + +**features** + * [[`b79134d`](http://github.com/koajs/session/commit/b79134d6854173bf46d6703e79636a58f9282e15)] - feat: make sure session id is global unique (#170) (fengmk2 <>) + +**fixes** + * [[`c2b4259`](http://github.com/koajs/session/commit/c2b4259ccef6095cad2f3ff51968b21cea993d13)] - fix: remove package-lock.json (fengmk2 <>) + +**others** + * [[`23ad871`](http://github.com/koajs/session/commit/23ad8718a9a392c0c563893a10b5ca9f6fd70ebe)] - deps: Fix security vulnerabilities from npm audit (#163) (Douglas Wade <>) + * [[`1600aab`](http://github.com/koajs/session/commit/1600aabdfa6a86973e3fab9f4064c3ed82b10604)] - test: changed "ctx.session is mockable" tests names to more appropriate (#158) (Vitaliy Zaytsev <>) 5.10.1 / 2018-12-18 ================== 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. diff --git a/Readme.md b/Readme.md index 6a00431..61c14ac 100644 --- a/Readme.md +++ b/Readme.md @@ -1,54 +1,52 @@ # 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) +[![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 -[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/ +[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 7.6 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 + +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']; 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 */ @@ -59,6 +57,8 @@ 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, do not provide this key if you are not restricting sameSite) */ }; app.use(session(CONFIG, app)); @@ -81,41 +81,38 @@ 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 })`: 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!** + The way of generating external session id is controlled by the `options.genid(ctx)`, which defaults to `uuid.v4()`. - The way of generating external session id is controlled by the `options.genid`, which defaults to `Date.now() + '-' + uid.sync(24)`. - - If you want to add prefix for all external session id, you can use `options.prefix`, it will not work if `options.genid` present. + 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. 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. @@ -127,7 +124,7 @@ console.log('listening on port 3000'); - `session:invalid`: session value is invalid. - `session:expired`: session value is expired. -### Custom External Key +### Custom External Key External key is used the cookie by default, but you can use `options.externalKey` to customize your own external key methods. `options.externalKey` with two methods: @@ -136,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) { @@ -148,23 +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. ### 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; @@ -172,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/__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/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 f2db549..0000000 --- a/index.js +++ /dev/null @@ -1,149 +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 uid = require('uid-safe'); -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; - 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}${Date.now()}-${uid.sync(24)}`; - else opts.genid = () => `${Date.now()}-${uid.sync(24)}`; - } - - 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 6961e3b..0000000 --- a/lib/session.js +++ /dev/null @@ -1,128 +0,0 @@ -'use strict'; - -/** - * Session model. - */ - -class Session { - /** - * Session constructor - * @param {Context} ctx - * @param {Object} obj - * @api private - */ - - constructor(sessionContext, obj) { - this._sessCtx = sessionContext; - this._ctx = sessionContext.ctx; - 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; - } - - /** - * save this session no matter whether it is populated - * - * @api public - */ - - save() { - this._requireSave = true; - } - - /** - * commit this session's headers if autoCommit is set to false - * - * @api public - */ - - async manuallyCommit() { - await this._sessCtx.commit(); - } - -} - -module.exports = Session; diff --git a/lib/util.js b/lib/util.js deleted file mode 100644 index 759197c..0000000 --- a/lib/util.js +++ /dev/null @@ -1,37 +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)); - }, -}; diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 01bbb9f..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2001 +0,0 @@ -{ - "name": "koa-session", - "version": "5.10.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.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", - "dev": true, - "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" - } - }, - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", - "dev": true - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "http://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": "http://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": "http://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": "http://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.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" - }, - "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.2.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", - "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", - "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": "http://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "http://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.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "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.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "dev": true, - "optional": true - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "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.7.3", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.3.tgz", - "integrity": "sha512-+gixgxYSgQLTaTIilDHAdlNPZDENDQernEMiIcZpYYP14zgHsCt4Ce1FEjFtcp6GefhozebB6orvhAAWx/IS0A==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "keygrip": "~1.0.3" - } - }, - "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.0", - "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "dev": true, - "requires": { - "es5-ext": "^0.10.9" - } - }, - "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 - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "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.46", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz", - "integrity": "sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw==", - "dev": true, - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "1" - } - }, - "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" - } - }, - "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-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.14", - "es6-iterator": "^2.0.1", - "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": "http://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.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", - "dev": true, - "requires": { - "estraverse": "^4.0.0" - } - }, - "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.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "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 - }, - "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.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "formidable": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", - "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", - "dev": true - }, - "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.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "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.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", - "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.0.12", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz", - "integrity": "sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==", - "dev": true, - "requires": { - "async": "^2.5.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - }, - "dependencies": { - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "dev": true, - "requires": { - "lodash": "^4.17.10" - } - }, - "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.0", - "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.0.tgz", - "integrity": "sha512-tPVv62a6l3BbQoM/N5qo969l0OFxqpnQzNUPeYfTP6Spo4zkgWeDBD1D5thI7sDLg7jCCihXTLB0X8UtdyAy8A==", - "dev": true, - "requires": { - "deep-equal": "~1.0.1", - "http-errors": "~1.7.1" - } - }, - "http-errors": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.1.tgz", - "integrity": "sha512-jWEUgtZWGSMba9I1N3gc1HmvpBUaNC9vDdA46yScAdp+C5rdEuKWUBLWTQpW9FwSWSbYYs++b6SDCxf9UEJzfw==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "ieee754": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", - "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" - }, - "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.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "inquirer": { - "version": "0.12.0", - "resolved": "http://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.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", - "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.19.0", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz", - "integrity": "sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q==", - "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": "http://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.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", - "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.0.3", - "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.3.tgz", - "integrity": "sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g==", - "dev": true - }, - "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.6.2", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.6.2.tgz", - "integrity": "sha512-KdnBFhTgh9ysMMoYe4J4fLvaKjT7mF3nRYV8MjxLzx6qywFNeptqi4xevyUltg1fZl2CFJ+HeLXuCGx07Yvl/A==", - "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.7.1", - "debug": "~3.1.0", - "delegates": "^1.0.0", - "depd": "^1.1.2", - "destroy": "^1.0.4", - "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", - "koa-is-json": "^1.0.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" - } - } - } - }, - "koa-is-json": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", - "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=", - "dev": true - }, - "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.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - }, - "media-typer": { - "version": "0.3.0", - "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "mime-db": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", - "dev": true - }, - "mime-types": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", - "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", - "dev": true, - "requires": { - "mime-db": "~1.37.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "mm": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/mm/-/mm-2.4.1.tgz", - "integrity": "sha512-NXWPSHcaTtC8GqcJ5w1FvX28ieWwEobrsfngBugrbyhAEg710i2W+vZuMuIBW6VnFt0l4b9WsjvqMytvmc3HmQ==", - "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": "http://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 - }, - "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.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - }, - "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.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", - "dev": true - }, - "next-tick": { - "version": "1.0.0", - "resolved": "http://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": "http://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 - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - } - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "http://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 - }, - "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.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "progress": { - "version": "1.1.8", - "resolved": "http://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.6.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.6.0.tgz", - "integrity": "sha512-KIJqT9jQJDQx5h5uAVPimw6yVg2SekOKu959OCtktD3FjzbpvaPr8i4zzg07DOMz+igA4W/aNM7OV8H37pFYfA==", - "dev": true - }, - "random-bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "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": "http://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.9.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz", - "integrity": "sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==", - "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.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, - "requires": { - "glob": "^7.0.5" - } - }, - "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.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "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": "http://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": "http://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": "http://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": "http://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": "http://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": "http://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": "http://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.3.0", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.3.0.tgz", - "integrity": "sha512-dMQSzYdaZRSANH5LL8kX3UpgK9G1LRh/jnggs/TI0W2Sz7rkMx9Y48uia3K9NgcaWEV28tYkBnXE4tiFC77ygQ==", - "dev": true, - "requires": { - "methods": "^1.1.2", - "superagent": "^3.8.3" - } - }, - "supports-color": { - "version": "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": "http://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": "http://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 - }, - "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.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.18" - } - }, - "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.4.9", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", - "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", - "dev": true, - "optional": true, - "requires": { - "commander": "~2.17.1", - "source-map": "~0.6.1" - }, - "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, - "optional": true - } - } - }, - "uid-safe": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", - "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", - "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 - }, - "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" - } - }, - "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.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "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/package.json b/package.json index ff25fb3..808789e 100644 --- a/package.json +++ b/package.json @@ -1,44 +1,80 @@ { "name": "koa-session", "description": "Koa cookie session middleware with external store support", - "repository": "koajs/session", - "version": "5.10.1", + "repository": { + "type": "git", + "url": "git@github.com:koajs/session.git" + }, + "version": "7.0.2", "keywords": [ "koa", "middleware", "session", "cookie" ], - "files": [ - "index.js", - "lib" - ], "devDependencies": { - "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" + "mm": "4", + "rimraf": "6", + "snap-shot-it": "^7.9.10", + "tshy": "3", + "tshy-after": "1", + "typescript": "5" }, "license": "MIT", "dependencies": { - "crc": "^3.4.4", - "debug": "^3.1.0", - "is-type-of": "^1.0.0", - "uid-safe": "^2.1.3" + "crc": "^3.8.0", + "is-type-of": "^2.2.0", + "zod": "^3.24.1" }, "engines": { - "node": ">=7.6" + "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", - "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", - "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 51% rename from lib/context.js rename to src/context.ts index 410996b..c31a751 100644 --- a/lib/context.js +++ b/src/context.ts @@ -1,51 +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; - // cookie session store - if (!this.store) this.initFromCookie(); - return this.session; + // create an empty session or init from cookie + this.store ? this.create() : this.initFromCookie(); + 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; @@ -62,7 +63,7 @@ class ContextSession { * init session from external store * will be called in the front of session middleware * - * @api public + * @public */ async initFromExternal() { @@ -86,23 +87,23 @@ class ContextSession { return; } - const json = await this.store.get(externalKey, opts.maxAge, { 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; @@ -114,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; @@ -184,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); }); @@ -195,30 +195,29 @@ 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.session = new Session(this, val); + 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() { + 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) { @@ -226,9 +225,19 @@ class ContextSession { return; } - const reason = this._shouldSaveSession(); - debug('should save session: %s', reason); - if (!reason) return; + if (regenerate) { + await this.remove(); + 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: %j', reason); + if (!reason) { + return; + } if (typeof opts.beforeSave === 'function') { debug('before save'); @@ -240,80 +249,92 @@ class ContextSession { _shouldSaveSession() { const prevHash = this.prevHash; - const session = this.session; - - // force save session when `session._requireSave` set - if (session._requireSave) return 'force'; + 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() { - const opts = this.opts; + // Override the default options so that we can properly expire the session cookies + 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); + 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, }); if (opts.externalKey) { @@ -324,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..74ec11a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,308 @@ +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'); + +export const SessionOptions = 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 = SessionOptions.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)`'); + } + + const options: SessionOptions = opts ?? {}; + + // back-compat maxage + if (!('maxAge' in options) && 'maxage' in options) { + Reflect.set(options, 'maxAge', Reflect.get(options, 'maxage')); + if (process.env.NODE_ENV !== 'production') { + console.warn('[koa-session] DeprecationWarning: `maxage` option has been renamed to `maxAge`'); + } + } + + // keep backwards compatibility: make sure options instance is not mutated + Object.assign(options, { + ...DEFAULT_SESSION_OPTIONS, + ...options, + }); + SessionOptions.parse(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(); + } + } +} + +/** + * 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 1d3058e..0000000 --- a/test/contextstore.test.js +++ /dev/null @@ -1,734 +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'); - -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 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 = () => { - return Date.now() + '_suffix'; - }; - 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 bf0e569..0000000 --- a/test/cookie.test.js +++ /dev/null @@ -1,818 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const Koa = require('koa'); -const request = require('supertest'); -const should = require('should'); -const session = require('..'); - -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']; - 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 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('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('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 c3625cb..0000000 --- a/test/externalkey.test.js +++ /dev/null @@ -1,56 +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; - } else { - ctx.body = ctx.session.string; - } - }); - 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/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); + }); + }); +}); 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 a5e8147..0000000 --- a/test/store.test.js +++ /dev/null @@ -1,781 +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'); - -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(/^\d+-/)); - 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('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']); - }); - }); -}); - -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..3f300ab --- /dev/null +++ b/test/store.test.ts @@ -0,0 +1,832 @@ +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 = 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 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; + } + }); + + 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(';'); + }); + + 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', () => { + 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.ts b/test/store_with_ctx.test.ts new file mode 100644 index 0000000..e7331a5 --- /dev/null +++ b/test/store_with_ctx.test.ts @@ -0,0 +1,78 @@ +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'; + +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 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(); + + await request(app.callback()) + .get('/') + .expect('undefined'); + }); + }); + + describe('when populated', () => { + it('should set ctx.state.test variable', async () => { + const app = App(); + + app.use(async ctx => { + if (ctx.path === '/set') ctx.session = { foo: 'bar' }; + }); + + const res = await request(app.callback()) + .get('/set') + .expect(200); + cookie = res.get('Set-Cookie')!.join(';'); + assert.equal(res.text, 'set'); + }); + }); + + describe('when accessed', () => { + it('should access ctx.state.test variable', async () => { + const app = App(); + + await 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; + } + }); + + await request(app.callback()) + .get('/destroy') + .set('Cookie', cookie) + .expect('destroyed'); + }); + }); + }); +}); diff --git a/test/store_with_ctx.ts b/test/store_with_ctx.ts new file mode 100644 index 0000000..7e50c7b --- /dev/null +++ b/test/store_with_ctx.ts @@ -0,0 +1,21 @@ +const sessions: Record = {}; + +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: string, sess: Record, _maxAge: number, options: any) { + // check access to options.ctx + options.ctx.state.test = 'set'; + sessions[key] = sess; + }, + + 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" + } +}