├── .gitattributes ├── .travis.yml ├── LICENSE ├── README.md ├── README_CN.md ├── lib ├── application.js ├── application.ts ├── context.js ├── context.ts ├── request.js ├── request.ts ├── response.js └── response.ts ├── package-lock.json ├── package.json ├── source ├── logo small.png ├── logo.png ├── tkoa logo square.png └── ts logo.png ├── test.js └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-language=JavaScript 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '7.6' 4 | 5 | cache: 6 | directories: 7 | - node_modules 8 | 9 | 10 | install: 11 | - npm i --global ava 12 | - npm i --save-dev ava 13 | - npm install --dependencies 14 | 15 | before_script: 16 | 17 | script: 18 | - npm run test 19 | 20 | branches: 21 | only: 22 | - master 23 | env: 24 | global: 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 tkoajs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![tkoa logo](https://raw.githubusercontent.com/tkoajs/tkoa/master/source/logo.png) 2 | 3 |

4 | tkoa build badge 5 | tkoa npm badge 6 | tkoa npm download badge 7 | tkoa gitter badge 8 |

9 | 10 | 🌈 Tkoa is a Koa web app framework written in typescript. ![typescript logo](https://raw.githubusercontent.com/tkoajs/tkoa/master/source/ts%20logo.png) 11 | 12 | Although written in typescript, you can still use the nodejs framework and koa middleware. 13 | 14 | Not only that, you can also enjoy the type checking and convenient testing with typescript! 15 | 16 | ## Installation 17 | TKoa requires **>= typescript v3.1.0** and **node v7.6.0** for ES2015 and async function support. 18 | 19 | ```shell 20 | $ npm install tkoa 21 | ``` 22 | 23 | ### Hello T-koa 24 | 25 | ```typescript 26 | import tKoa = require('tkoa'); 27 | 28 | interface ctx { 29 | res: { 30 | end: Function 31 | } 32 | } 33 | 34 | const app = new tKoa(); 35 | 36 | // response 37 | app.use((ctx: ctx) => { 38 | ctx.res.end('Hello T-koa!'); 39 | }); 40 | 41 | app.listen(3000); 42 | ``` 43 | 44 | ### Middleware 45 | Tkoa is a middleware framework that can take two different kinds of functions as middleware: 46 | 47 | - async function 48 | - common function 49 | 50 | Here is an example of logger middleware with each of the different functions: 51 | 52 | #### async functions (node v7.6+): 53 | 54 | ```typescript 55 | interface ctx { 56 | method: string, 57 | url: string 58 | } 59 | 60 | app.use(async (ctx: ctx, next: Function) => { 61 | const start = Date.now(); 62 | await next(); 63 | const ms = Date.now() - start; 64 | console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); 65 | }); 66 | ``` 67 | 68 | #### Common function 69 | ```typescript 70 | // Middleware normally takes two parameters (ctx, next), ctx is the context for one request, 71 | // next is a function that is invoked to execute the downstream middleware. It returns a Promise with a then function for running code after completion. 72 | 73 | interface ctx { 74 | method: string, 75 | url: string 76 | } 77 | 78 | app.use((ctx: ctx, next: Function) => { 79 | const start = Date.now(); 80 | return next().then(() => { 81 | const ms = Date.now() - start; 82 | console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); 83 | }); 84 | }); 85 | ``` 86 | 87 | ## Getting started 88 | - [Tkoa - wiki](https://github.com/tkoajs/tkoa/wiki) 89 | - [zhcn - 中文文档](https://github.com/tkoajs/tkoa/blob/master/README_CN.md) 90 | - [Gitter](https://gitter.im/tkoa-js/community) 91 | 92 | ## Support 93 | ### TypeScript 94 | - Higher than version v3.1 95 | ### Node.js 96 | - Higher than version v7.6.0 97 | 98 | ## License 99 | [MIT](https://github.com/tkoajs/tkoa/blob/master/LICENSE) 100 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | ![tkoa logo](https://raw.githubusercontent.com/tkoajs/tkoa/master/source/logo.png) 2 | 3 |

4 | tkoa build badge 5 | tkoa npm badge 6 | tkoa npm download badge 7 | tkoa gitter badge 8 |

9 | 10 | 🌈 Tkoa是使用 typescript 编写的 koa 框架! ![typescript logo](https://raw.githubusercontent.com/tkoajs/tkoa/master/source/ts%20logo.png) 11 | 12 | 尽管它是基于 typescript 编写,但是你依然还是可以使用一些 node.js 框架和基于 koa 的中间件。 13 | 14 | 不仅如此,你还可以享受 typescript 的类型检查系统和方便地使用 typescript 进行测试! 15 | 16 | ## 安装 17 | TKoa 需要 **>= typescript v3.1.0** 和 **node v7.6.0** 版本。 18 | 19 | ```shell 20 | $ npm install tkoa 21 | ``` 22 | 23 | ### Hello T-koa 24 | 25 | ```typescript 26 | import tKoa = require('tkoa'); 27 | 28 | interface ctx { 29 | res: { 30 | end: Function 31 | } 32 | } 33 | 34 | const app = new tKoa(); 35 | 36 | // 响应 37 | app.use((ctx: ctx) => { 38 | ctx.res.end('Hello T-koa!'); 39 | }); 40 | 41 | app.listen(3000); 42 | ``` 43 | 44 | ### Middleware 45 | Tkoa 是一个中间件框架,拥有两种中间件: 46 | 47 | - 异步中间件 48 | - 普通中间件 49 | 50 | 下面是一个日志记录中间件示例,其中使用了不同的中间件类型: 51 | 52 | #### async functions (node v7.6+): 53 | 54 | ```typescript 55 | interface ctx { 56 | method: string, 57 | url: string 58 | } 59 | 60 | app.use(async (ctx: ctx, next: Function) => { 61 | const start = Date.now(); 62 | await next(); 63 | const ms = Date.now() - start; 64 | console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); 65 | }); 66 | ``` 67 | 68 | #### Common function 69 | ```typescript 70 | // 中间件通常需要两个参数(ctx,next),ctx是一个请求的上下文,next是一个被调用来执行下游中间件的函数。它返回一个带有then函数的Promise,用于在完成后运行代码。 71 | 72 | interface ctx { 73 | method: string, 74 | url: string 75 | } 76 | 77 | app.use((ctx: ctx, next: Function) => { 78 | const start = Date.now(); 79 | return next().then(() => { 80 | const ms = Date.now() - start; 81 | console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); 82 | }); 83 | }); 84 | ``` 85 | 86 | ## Getting started 87 | - [Tkoa - 教程](https://github.com/tkoajs/tkoa/wiki) 88 | - [en - english readme](https://github.com/tkoajs/tkoa/blob/master/README.md) 89 | - [Gitter - 聊天室](https://gitter.im/tkoa-js/community_中文) 90 | 91 | ## Support 92 | ### TypeScript 93 | - 大于等于 v3.1 版本 94 | ### Node.js 95 | - 大于等于 v7.6.0 版本 96 | 97 | ## License 98 | [MIT](https://github.com/tkoajs/tkoa/blob/master/LICENSE) 99 | -------------------------------------------------------------------------------- /lib/application.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Module dependencies. 4 | */ 5 | const onFinished = require("on-finished"); 6 | const compose = require("koa-compose"); 7 | const isJSON = require("koa-is-json"); 8 | const statuses = require("statuses"); 9 | const Emitter = require("events"); 10 | const util = require("util"); 11 | const Stream = require("stream"); 12 | const http = require("http"); 13 | const only = require("only"); 14 | const context = require("./context"); 15 | const request = require("./request"); 16 | const response = require("./response"); 17 | const debug = require('debug')('koa:application'); 18 | /** 19 | * Response helper. 20 | */ 21 | function respond(ctx) { 22 | // allow bypassing koa 23 | if (false === ctx.respond) 24 | return; 25 | if (!ctx.writable) 26 | return; 27 | const res = ctx.res; 28 | let body = ctx.body; 29 | const code = ctx.status; 30 | // ignore body 31 | if (statuses.empty[code]) { 32 | // strip headers 33 | ctx.body = null; 34 | return res.end(); 35 | } 36 | if ('HEAD' == ctx.method) { 37 | if (!res.headersSent && isJSON(body)) { 38 | ctx.length = Buffer.byteLength(JSON.stringify(body)); 39 | } 40 | return res.end(); 41 | } 42 | // status body 43 | if (null == body) { 44 | if (ctx.req.httpVersionMajor >= 2) { 45 | body = String(code); 46 | } 47 | else { 48 | body = ctx.message || String(code); 49 | } 50 | if (!res.headersSent) { 51 | ctx.type = 'text'; 52 | ctx.length = Buffer.byteLength(body); 53 | } 54 | return res.end(body); 55 | } 56 | // responses 57 | if (Buffer.isBuffer(body)) 58 | return res.end(body); 59 | if ('string' == typeof body) 60 | return res.end(body); 61 | if (body instanceof Stream) 62 | return body.pipe(res); 63 | // body: json 64 | body = JSON.stringify(body); 65 | if (!res.headersSent) { 66 | ctx.length = Buffer.byteLength(body); 67 | } 68 | res.end(body); 69 | } 70 | module.exports = class Application extends Emitter { 71 | constructor() { 72 | super(); 73 | this.proxy = false; 74 | this.middleware = []; 75 | this.subdomainOffset = 2; 76 | this.env = process.env.NODE_ENV || 'development'; 77 | this.context = Object.create(context); 78 | this.request = Object.create(request); 79 | this.response = Object.create(response); 80 | if (util.inspect.custom) { 81 | this[util.inspect.custom] = this.inspect; 82 | } 83 | } 84 | /** 85 | * Shorthand for: 86 | * 87 | * http.createServer(app.callback()).listen(...) 88 | * 89 | * @param {Mixed} ... 90 | * @return {Server} 91 | * @api public 92 | */ 93 | listen(...args) { 94 | debug('listen'); 95 | const server = http.createServer(this.callback()); 96 | return server.listen(...args); 97 | } 98 | /** 99 | * Return JSON representation. 100 | * We only bother showing settings. 101 | * 102 | * @return {Object} 103 | * @api public 104 | */ 105 | toJSON() { 106 | return only(this, [ 107 | 'subdomainOffset', 108 | 'proxy', 109 | 'env' 110 | ]); 111 | } 112 | /** 113 | * Inspect implementation. 114 | * 115 | * @return {Object} 116 | * @api public 117 | */ 118 | inspect() { 119 | return this.toJSON(); 120 | } 121 | /** 122 | * Use the given middleware `fn`. 123 | * 124 | * Old-style middleware will not be converted. 125 | * 126 | * @param {Function} fn 127 | * @return {Application} self 128 | * @api public 129 | */ 130 | use(fn) { 131 | debug('use %s', fn.name || '-'); 132 | this.middleware.push(fn); 133 | return this; 134 | } 135 | /** 136 | * Return a request handler callback 137 | * for node's native http server. 138 | * 139 | * @return {Function} 140 | * @api public 141 | */ 142 | callback() { 143 | const fn = compose(this.middleware); 144 | if (!this.listenerCount('error')) 145 | this.on('error', this.onerror); 146 | const handleRequest = (req, res) => { 147 | const ctx = this.createContext(req, res); 148 | return this.handleRequest(ctx, fn); 149 | }; 150 | return handleRequest; 151 | } 152 | /** 153 | * Handle request in callback. 154 | * 155 | * @api private 156 | */ 157 | handleRequest(ctx, fnMiddleware) { 158 | const res = ctx.res; 159 | res.statusCode = 404; 160 | const onerror = (err) => ctx.onerror(err); 161 | const handleResponse = () => respond(ctx); 162 | onFinished(res, onerror); 163 | return fnMiddleware(ctx).then(handleResponse).catch(onerror); 164 | } 165 | /** 166 | * Initialize a new context. 167 | * 168 | * @api private 169 | */ 170 | createContext(req, res) { 171 | const context = Object.create(this.context); 172 | const request = context.request = Object.create(this.request); 173 | const response = context.response = Object.create(this.response); 174 | context.app = request.app = response.app = this; 175 | context.req = request.req = response.req = req; 176 | context.res = request.res = response.res = res; 177 | request.ctx = response.ctx = context; 178 | request.response = response; 179 | response.request = request; 180 | context.originalUrl = request.originalUrl = req.url; 181 | context.state = {}; 182 | return context; 183 | } 184 | /** 185 | * Default error handler. 186 | * 187 | * @param {Error} err 188 | * @api private 189 | */ 190 | onerror(err) { 191 | if (404 == err.status || err.expose) 192 | return; 193 | if (this.silent) 194 | return; 195 | const msg = err.stack || err.toString(); 196 | console.error(); 197 | console.error(msg.replace(/^/gm, ' ')); 198 | console.error(); 199 | } 200 | }; 201 | -------------------------------------------------------------------------------- /lib/application.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | import onFinished = require('on-finished'); 8 | import compose = require('koa-compose'); 9 | import isJSON = require('koa-is-json'); 10 | import statuses = require('statuses'); 11 | import Emitter = require('events'); 12 | import util = require('util'); 13 | import Stream = require('stream'); 14 | import http = require('http'); 15 | import only = require('only'); 16 | import context = require('./context'); 17 | import request = require('./request'); 18 | import response = require('./response'); 19 | const debug = require('debug')('koa:application'); 20 | 21 | /** 22 | * Expose `Application` class. 23 | * Inherits from `Emitter.prototype`. 24 | */ 25 | 26 | export = class Application extends Emitter { 27 | 28 | /** 29 | * Initialize a new `Application`. 30 | * 31 | * @api public 32 | */ 33 | 34 | proxy: boolean; 35 | middleware: any[]; 36 | subdomainOffset: number; 37 | env: string; 38 | context: any; 39 | request: any; 40 | response: any; 41 | silent: any; 42 | 43 | constructor() { 44 | super(); 45 | 46 | this.proxy = false; 47 | this.middleware = []; 48 | this.subdomainOffset = 2; 49 | this.env = process.env.NODE_ENV || 'development'; 50 | this.context = Object.create(context); 51 | this.request = Object.create(request); 52 | this.response = Object.create(response); 53 | if (util.inspect.custom) { 54 | this[util.inspect.custom] = this.inspect; 55 | } 56 | } 57 | 58 | /** 59 | * Shorthand for: 60 | * 61 | * http.createServer(app.callback()).listen(...) 62 | * 63 | * @param {Mixed} ... 64 | * @return {Server} 65 | * @api public 66 | */ 67 | 68 | listen(...args: any[]): object { 69 | debug('listen'); 70 | const server = http.createServer(this.callback()); 71 | return server.listen(...args); 72 | } 73 | 74 | /** 75 | * Return JSON representation. 76 | * We only bother showing settings. 77 | * 78 | * @return {Object} 79 | * @api public 80 | */ 81 | 82 | toJSON(): object { 83 | return only(this, [ 84 | 'subdomainOffset', 85 | 'proxy', 86 | 'env' 87 | ]); 88 | } 89 | 90 | /** 91 | * Inspect implementation. 92 | * 93 | * @return {Object} 94 | * @api public 95 | */ 96 | 97 | inspect(): object { 98 | return this.toJSON(); 99 | } 100 | 101 | /** 102 | * Use the given middleware `fn`. 103 | * 104 | * Old-style middleware will not be converted. 105 | * 106 | * @param {Function} fn 107 | * @return {Application} self 108 | * @api public 109 | */ 110 | 111 | use(fn: (ctx: object, next: Function) => void): this { 112 | debug('use %s', fn.name || '-'); 113 | this.middleware.push(fn); 114 | return this; 115 | } 116 | 117 | /** 118 | * Return a request handler callback 119 | * for node's native http server. 120 | * 121 | * @return {Function} 122 | * @api public 123 | */ 124 | 125 | callback(): object { 126 | const fn = compose(this.middleware); 127 | 128 | if (!this.listenerCount('error')) this.on('error', this.onerror); 129 | 130 | const handleRequest = (req: object, res: object) => { 131 | const ctx = this.createContext(req, res); 132 | return this.handleRequest(ctx, fn); 133 | }; 134 | 135 | return handleRequest; 136 | } 137 | 138 | /** 139 | * Handle request in callback. 140 | * 141 | * @api private 142 | */ 143 | 144 | handleRequest(ctx: any, fnMiddleware: any): any { 145 | const res = ctx.res; 146 | res.statusCode = 404; 147 | const onerror = (err: any) => ctx.onerror(err); 148 | const handleResponse = () => respond(ctx); 149 | onFinished(res, onerror); 150 | return fnMiddleware(ctx).then(handleResponse).catch(onerror); 151 | } 152 | 153 | /** 154 | * Initialize a new context. 155 | * 156 | * @api private 157 | */ 158 | 159 | createContext(req: any, res: any): object { 160 | const context = Object.create(this.context); 161 | const request = context.request = Object.create(this.request); 162 | const response = context.response = Object.create(this.response); 163 | context.app = request.app = response.app = this; 164 | context.req = request.req = response.req = req; 165 | context.res = request.res = response.res = res; 166 | request.ctx = response.ctx = context; 167 | request.response = response; 168 | response.request = request; 169 | context.originalUrl = request.originalUrl = req.url; 170 | context.state = {}; 171 | return context; 172 | } 173 | 174 | /** 175 | * Default error handler. 176 | * 177 | * @param {Error} err 178 | * @api private 179 | */ 180 | 181 | onerror(err: any): void { 182 | 183 | if (404 == err.status || err.expose) return; 184 | if (this.silent) return; 185 | 186 | const msg = err.stack || err.toString(); 187 | console.error(); 188 | console.error(msg.replace(/^/gm, ' ')); 189 | console.error(); 190 | } 191 | } 192 | 193 | /** 194 | * Response helper. 195 | */ 196 | 197 | function respond(ctx: any): any { 198 | // allow bypassing koa 199 | if (false === ctx.respond) return; 200 | 201 | if (!ctx.writable) return; 202 | 203 | const res = ctx.res; 204 | let body = ctx.body; 205 | const code = ctx.status; 206 | 207 | // ignore body 208 | if (statuses.empty[code]) { 209 | // strip headers 210 | ctx.body = null; 211 | return res.end(); 212 | } 213 | 214 | if ('HEAD' == ctx.method) { 215 | if (!res.headersSent && isJSON(body)) { 216 | ctx.length = Buffer.byteLength(JSON.stringify(body)); 217 | } 218 | return res.end(); 219 | } 220 | 221 | // status body 222 | if (null == body) { 223 | if (ctx.req.httpVersionMajor >= 2) { 224 | body = String(code); 225 | } else { 226 | body = ctx.message || String(code); 227 | } 228 | if (!res.headersSent) { 229 | ctx.type = 'text'; 230 | ctx.length = Buffer.byteLength(body); 231 | } 232 | return res.end(body); 233 | } 234 | 235 | // responses 236 | if (Buffer.isBuffer(body)) return res.end(body); 237 | if ('string' == typeof body) return res.end(body); 238 | if (body instanceof Stream) return body.pipe(res); 239 | 240 | // body: json 241 | body = JSON.stringify(body); 242 | if (!res.headersSent) { 243 | ctx.length = Buffer.byteLength(body); 244 | } 245 | res.end(body); 246 | } 247 | -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Module dependencies. 4 | */ 5 | const util = require("util"); 6 | const createError = require("http-errors"); 7 | const httpAssert = require("http-assert"); 8 | const delegate = require("delegates"); 9 | const statuses = require("statuses"); 10 | const Cookies = require("cookies"); 11 | const COOKIES = Symbol('context#cookies'); 12 | /** 13 | * Context prototype. 14 | */ 15 | const proto = { 16 | /** 17 | * util.inspect() implementation, which 18 | * just returns the JSON output. 19 | * 20 | * @return {Object} 21 | * @api public 22 | */ 23 | inspect() { 24 | if (this === proto) 25 | return this; 26 | return this.toJSON(); 27 | }, 28 | /** 29 | * Return JSON representation. 30 | * 31 | * Here we explicitly invoke .toJSON() on each 32 | * object, as iteration will otherwise fail due 33 | * to the getters and cause utilities such as 34 | * clone() to fail. 35 | * 36 | * @return {Object} 37 | * @api public 38 | */ 39 | toJSON() { 40 | return { 41 | request: this.request.toJSON(), 42 | response: this.response.toJSON(), 43 | app: this.app.toJSON(), 44 | originalUrl: this.originalUrl, 45 | req: '', 46 | res: '', 47 | socket: '' 48 | }; 49 | }, 50 | /** 51 | * Similar to .throw(), adds assertion. 52 | * 53 | * this.assert(this.user, 401, 'Please login!'); 54 | * 55 | * See: https://github.com/jshttp/http-assert 56 | * 57 | * @param {Mixed} test 58 | * @param {Number} status 59 | * @param {String} message 60 | * @api public 61 | */ 62 | assert: httpAssert, 63 | /** 64 | * Throw an error with `status` (default 500) and 65 | * `msg`. Note that these are user-level 66 | * errors, and the message may be exposed to the client. 67 | * 68 | * this.throw(403) 69 | * this.throw(400, 'name required') 70 | * this.throw('something exploded') 71 | * this.throw(new Error('invalid')) 72 | * this.throw(400, new Error('invalid')) 73 | * 74 | * See: https://github.com/jshttp/http-errors 75 | * 76 | * Note: `status` should only be passed as the first parameter. 77 | * 78 | * @param {String|Number|Error} err, msg or status 79 | * @param {String|Number|Error} [err, msg or status] 80 | * @param {Object} [props] 81 | * @api public 82 | */ 83 | throw(...args) { 84 | throw createError(...args); 85 | }, 86 | /** 87 | * Default error handling. 88 | * 89 | * @param {Error} err 90 | * @api private 91 | */ 92 | onerror(err) { 93 | // don't do anything if there is no error. 94 | // this allows you to pass `this.onerror` 95 | // to node-style callbacks. 96 | if (null == err) 97 | return; 98 | if (!(err instanceof Error)) 99 | err = new Error(util.format('non-error thrown: %j', err)); 100 | let headerSent = false; 101 | if (this.headerSent || !this.writable) { 102 | headerSent = err.headerSent = true; 103 | } 104 | // delegate 105 | this.app.emit('error', err, this); 106 | // nothing we can do here other 107 | // than delegate to the app-level 108 | // handler and log. 109 | if (headerSent) { 110 | return; 111 | } 112 | const { res } = this; 113 | // first unset all headers 114 | /* istanbul ignore else */ 115 | if (typeof res.getHeaderNames === 'function') { 116 | res.getHeaderNames().forEach(name => res.removeHeader(name)); 117 | } 118 | else { 119 | res._headers = {}; // Node < 7.7 120 | } 121 | // then set those specified 122 | this.set(err.headers); 123 | // force text/plain 124 | this.type = 'text'; 125 | // ENOENT support 126 | if ('ENOENT' == err.code) 127 | err.status = 404; 128 | // default to 500 129 | if ('number' != typeof err.status || !statuses[err.status]) 130 | err.status = 500; 131 | // respond 132 | const code = statuses[err.status]; 133 | const msg = err.expose ? err.message : code; 134 | this.status = err.status; 135 | this.length = Buffer.byteLength(msg); 136 | res.end(msg); 137 | }, 138 | get cookies() { 139 | if (!this[COOKIES]) { 140 | this[COOKIES] = new Cookies(this.req, this.res, { 141 | keys: this.app.keys, 142 | secure: this.request.secure 143 | }); 144 | } 145 | return this[COOKIES]; 146 | }, 147 | set cookies(_cookies) { 148 | this[COOKIES] = _cookies; 149 | } 150 | }; 151 | /** 152 | * Custom inspection implementation for newer Node.js versions. 153 | * 154 | * @return {Object} 155 | * @api public 156 | */ 157 | /* istanbul ignore else */ 158 | if (util.inspect.custom) { 159 | module.exports[util.inspect.custom] = module.exports.inspect; 160 | } 161 | /** 162 | * Response delegation. 163 | */ 164 | delegate(proto, 'response') 165 | .method('attachment') 166 | .method('redirect') 167 | .method('remove') 168 | .method('vary') 169 | .method('set') 170 | .method('append') 171 | .method('flushHeaders') 172 | .access('status') 173 | .access('message') 174 | .access('body') 175 | .access('length') 176 | .access('type') 177 | .access('lastModified') 178 | .access('etag') 179 | .getter('headerSent') 180 | .getter('writable'); 181 | /** 182 | * Request delegation. 183 | */ 184 | delegate(proto, 'request') 185 | .method('acceptsLanguages') 186 | .method('acceptsEncodings') 187 | .method('acceptsCharsets') 188 | .method('accepts') 189 | .method('get') 190 | .method('is') 191 | .access('querystring') 192 | .access('idempotent') 193 | .access('socket') 194 | .access('search') 195 | .access('method') 196 | .access('query') 197 | .access('path') 198 | .access('url') 199 | .access('accept') 200 | .getter('origin') 201 | .getter('href') 202 | .getter('subdomains') 203 | .getter('protocol') 204 | .getter('host') 205 | .getter('hostname') 206 | .getter('URL') 207 | .getter('header') 208 | .getter('headers') 209 | .getter('secure') 210 | .getter('stale') 211 | .getter('fresh') 212 | .getter('ips') 213 | .getter('ip'); 214 | module.exports = proto; 215 | -------------------------------------------------------------------------------- /lib/context.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | import util = require('util'); 8 | import createError = require('http-errors'); 9 | import httpAssert = require('http-assert'); 10 | import delegate = require('delegates'); 11 | import statuses = require('statuses'); 12 | import Cookies = require('cookies'); 13 | 14 | const COOKIES = Symbol('context#cookies'); 15 | 16 | /** 17 | * Context prototype. 18 | */ 19 | 20 | const proto = { 21 | 22 | /** 23 | * util.inspect() implementation, which 24 | * just returns the JSON output. 25 | * 26 | * @return {Object} 27 | * @api public 28 | */ 29 | 30 | inspect(): object { 31 | if (this === proto) return this; 32 | return this.toJSON(); 33 | }, 34 | 35 | /** 36 | * Return JSON representation. 37 | * 38 | * Here we explicitly invoke .toJSON() on each 39 | * object, as iteration will otherwise fail due 40 | * to the getters and cause utilities such as 41 | * clone() to fail. 42 | * 43 | * @return {Object} 44 | * @api public 45 | */ 46 | 47 | toJSON(): object { 48 | return { 49 | request: this.request.toJSON(), 50 | response: this.response.toJSON(), 51 | app: this.app.toJSON(), 52 | originalUrl: this.originalUrl, 53 | req: '', 54 | res: '', 55 | socket: '' 56 | }; 57 | }, 58 | 59 | /** 60 | * Similar to .throw(), adds assertion. 61 | * 62 | * this.assert(this.user, 401, 'Please login!'); 63 | * 64 | * See: https://github.com/jshttp/http-assert 65 | * 66 | * @param {Mixed} test 67 | * @param {Number} status 68 | * @param {String} message 69 | * @api public 70 | */ 71 | 72 | assert: httpAssert, 73 | 74 | /** 75 | * Throw an error with `status` (default 500) and 76 | * `msg`. Note that these are user-level 77 | * errors, and the message may be exposed to the client. 78 | * 79 | * this.throw(403) 80 | * this.throw(400, 'name required') 81 | * this.throw('something exploded') 82 | * this.throw(new Error('invalid')) 83 | * this.throw(400, new Error('invalid')) 84 | * 85 | * See: https://github.com/jshttp/http-errors 86 | * 87 | * Note: `status` should only be passed as the first parameter. 88 | * 89 | * @param {String|Number|Error} err, msg or status 90 | * @param {String|Number|Error} [err, msg or status] 91 | * @param {Object} [props] 92 | * @api public 93 | */ 94 | 95 | throw(...args: any[]): void { 96 | throw createError(...args); 97 | }, 98 | 99 | /** 100 | * Default error handling. 101 | * 102 | * @param {Error} err 103 | * @api private 104 | */ 105 | 106 | onerror(err: any): void { 107 | // don't do anything if there is no error. 108 | // this allows you to pass `this.onerror` 109 | // to node-style callbacks. 110 | if (null == err) return; 111 | 112 | if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err)); 113 | 114 | let headerSent = false; 115 | if (this.headerSent || !this.writable) { 116 | headerSent = err.headerSent = true; 117 | } 118 | 119 | // delegate 120 | this.app.emit('error', err, this); 121 | 122 | // nothing we can do here other 123 | // than delegate to the app-level 124 | // handler and log. 125 | if (headerSent) { 126 | return; 127 | } 128 | 129 | const { res } = this; 130 | 131 | // first unset all headers 132 | /* istanbul ignore else */ 133 | if (typeof res.getHeaderNames === 'function') { 134 | res.getHeaderNames().forEach(name => res.removeHeader(name)); 135 | } else { 136 | res._headers = {}; // Node < 7.7 137 | } 138 | 139 | // then set those specified 140 | this.set(err.headers); 141 | 142 | // force text/plain 143 | this.type = 'text'; 144 | 145 | // ENOENT support 146 | if ('ENOENT' == err.code) err.status = 404; 147 | 148 | // default to 500 149 | if ('number' != typeof err.status || !statuses[err.status]) err.status = 500; 150 | 151 | // respond 152 | const code = statuses[err.status]; 153 | const msg = err.expose ? err.message : code; 154 | this.status = err.status; 155 | this.length = Buffer.byteLength(msg); 156 | res.end(msg); 157 | }, 158 | 159 | get cookies(): Cookies { 160 | if (!this[COOKIES]) { 161 | this[COOKIES] = new Cookies(this.req, this.res, { 162 | keys: this.app.keys, 163 | secure: this.request.secure 164 | }); 165 | } 166 | return this[COOKIES]; 167 | }, 168 | 169 | set cookies(_cookies: Cookies) { 170 | this[COOKIES] = _cookies; 171 | } 172 | } 173 | 174 | export = proto; 175 | 176 | /** 177 | * Custom inspection implementation for newer Node.js versions. 178 | * 179 | * @return {Object} 180 | * @api public 181 | */ 182 | 183 | /* istanbul ignore else */ 184 | if (util.inspect.custom) { 185 | module.exports[util.inspect.custom] = module.exports.inspect; 186 | } 187 | 188 | /** 189 | * Response delegation. 190 | */ 191 | 192 | delegate(proto, 'response') 193 | .method('attachment') 194 | .method('redirect') 195 | .method('remove') 196 | .method('vary') 197 | .method('set') 198 | .method('append') 199 | .method('flushHeaders') 200 | .access('status') 201 | .access('message') 202 | .access('body') 203 | .access('length') 204 | .access('type') 205 | .access('lastModified') 206 | .access('etag') 207 | .getter('headerSent') 208 | .getter('writable'); 209 | 210 | /** 211 | * Request delegation. 212 | */ 213 | 214 | delegate(proto, 'request') 215 | .method('acceptsLanguages') 216 | .method('acceptsEncodings') 217 | .method('acceptsCharsets') 218 | .method('accepts') 219 | .method('get') 220 | .method('is') 221 | .access('querystring') 222 | .access('idempotent') 223 | .access('socket') 224 | .access('search') 225 | .access('method') 226 | .access('query') 227 | .access('path') 228 | .access('url') 229 | .access('accept') 230 | .getter('origin') 231 | .getter('href') 232 | .getter('subdomains') 233 | .getter('protocol') 234 | .getter('host') 235 | .getter('hostname') 236 | .getter('URL') 237 | .getter('header') 238 | .getter('headers') 239 | .getter('secure') 240 | .getter('stale') 241 | .getter('fresh') 242 | .getter('ips') 243 | .getter('ip'); 244 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | /** 4 | * Module dependencies. 5 | */ 6 | const net = require("net"); 7 | const accepts = require("accepts"); 8 | const contentType = require("content-type"); 9 | const parse = require("parseurl"); 10 | const qs = require("querystring"); 11 | const typeis = require("type-is"); 12 | const fresh = require("fresh"); 13 | const only = require("only"); 14 | const util = require("util"); 15 | const URL = require('url').URL; 16 | const stringify = require('url').format; 17 | const IP = Symbol('context#ip'); 18 | /** 19 | * Prototype. 20 | */ 21 | module.exports = { 22 | /** 23 | * Return request header. 24 | * 25 | * @return {Object} 26 | * @api public 27 | */ 28 | get header() { 29 | return this.req.headers; 30 | }, 31 | /** 32 | * Set request header. 33 | * 34 | * @api public 35 | */ 36 | set header(val) { 37 | this.req.headers = val; 38 | }, 39 | /** 40 | * Return request header, alias as request.header 41 | * 42 | * @return {Object} 43 | * @api public 44 | */ 45 | get headers() { 46 | return this.req.headers; 47 | }, 48 | /** 49 | * Set request header, alias as request.header 50 | * 51 | * @api public 52 | */ 53 | set headers(val) { 54 | this.req.headers = val; 55 | }, 56 | /** 57 | * Get request URL. 58 | * 59 | * @return {String} 60 | * @api public 61 | */ 62 | get url() { 63 | return this.req.url; 64 | }, 65 | /** 66 | * Set request URL. 67 | * 68 | * @api public 69 | */ 70 | set url(val) { 71 | this.req.url = val; 72 | }, 73 | /** 74 | * Get origin of URL. 75 | * 76 | * @return {String} 77 | * @api public 78 | */ 79 | get origin() { 80 | return `${this.protocol}://${this.host}`; 81 | }, 82 | /** 83 | * Get full request URL. 84 | * 85 | * @return {String} 86 | * @api public 87 | */ 88 | get href() { 89 | // support: `GET http://example.com/foo` 90 | if (/^https?:\/\//i.test(this.originalUrl)) 91 | return this.originalUrl; 92 | return this.origin + this.originalUrl; 93 | }, 94 | /** 95 | * Get request method. 96 | * 97 | * @return {String} 98 | * @api public 99 | */ 100 | get method() { 101 | return this.req.method; 102 | }, 103 | /** 104 | * Set request method. 105 | * 106 | * @param {String} val 107 | * @api public 108 | */ 109 | set method(val) { 110 | this.req.method = val; 111 | }, 112 | /** 113 | * Get request pathname. 114 | * 115 | * @return {String} 116 | * @api public 117 | */ 118 | get path() { 119 | return parse(this.req).pathname; 120 | }, 121 | /** 122 | * Set pathname, retaining the query-string when present. 123 | * 124 | * @param {String} path 125 | * @api public 126 | */ 127 | set path(path) { 128 | const url = parse(this.req); 129 | if (url.pathname === path) 130 | return; 131 | url.pathname = path; 132 | url.path = null; 133 | this.url = stringify(url); 134 | }, 135 | /** 136 | * Get parsed query-string. 137 | * 138 | * @return {Object} 139 | * @api public 140 | */ 141 | get query() { 142 | const str = this.querystring; 143 | const c = this._querycache = this._querycache || {}; 144 | return c[str] || (c[str] = qs.parse(str)); 145 | }, 146 | /** 147 | * Set query-string as an object. 148 | * 149 | * @param {Object} obj 150 | * @api public 151 | */ 152 | set query(obj) { 153 | this.querystring = qs.stringify(obj); 154 | }, 155 | /** 156 | * Get query string. 157 | * 158 | * @return {String} 159 | * @api public 160 | */ 161 | get querystring() { 162 | if (!this.req) 163 | return ''; 164 | return parse(this.req).query || ''; 165 | }, 166 | /** 167 | * Set querystring. 168 | * 169 | * @param {String} str 170 | * @api public 171 | */ 172 | set querystring(str) { 173 | const url = parse(this.req); 174 | if (url.search === `?${str}`) 175 | return; 176 | url.search = str; 177 | url.path = null; 178 | this.url = stringify(url); 179 | }, 180 | /** 181 | * Get the search string. Same as the querystring 182 | * except it includes the leading ?. 183 | * 184 | * @return {String} 185 | * @api public 186 | */ 187 | get search() { 188 | if (!this.querystring) 189 | return ''; 190 | return `?${this.querystring}`; 191 | }, 192 | /** 193 | * Set the search string. Same as 194 | * request.querystring= but included for ubiquity. 195 | * 196 | * @param {String} str 197 | * @api public 198 | */ 199 | set search(str) { 200 | this.querystring = str; 201 | }, 202 | /** 203 | * Parse the "Host" header field host 204 | * and support X-Forwarded-Host when a 205 | * proxy is enabled. 206 | * 207 | * @return {String} hostname:port 208 | * @api public 209 | */ 210 | get host() { 211 | const proxy = this.app.proxy; 212 | let host = proxy && this.get('X-Forwarded-Host'); 213 | if (!host) { 214 | if (this.req.httpVersionMajor >= 2) 215 | host = this.get(':authority'); 216 | if (!host) 217 | host = this.get('Host'); 218 | } 219 | if (!host) 220 | return ''; 221 | return host.split(/\s*,\s*/, 1)[0]; 222 | }, 223 | /** 224 | * Parse the "Host" header field hostname 225 | * and support X-Forwarded-Host when a 226 | * proxy is enabled. 227 | * 228 | * @return {String} hostname 229 | * @api public 230 | */ 231 | get hostname() { 232 | const host = this.host; 233 | if (!host) 234 | return ''; 235 | if ('[' == host[0]) 236 | return this.URL.hostname || ''; // IPv6 237 | return host.split(':', 1)[0]; 238 | }, 239 | /** 240 | * Get WHATWG parsed URL. 241 | * Lazily memoized. 242 | * 243 | * @return {URL|Object} 244 | * @api public 245 | */ 246 | get URL() { 247 | /* istanbul ignore else */ 248 | if (!this.memoizedURL) { 249 | const protocol = this.protocol; 250 | const host = this.host; 251 | const originalUrl = this.originalUrl || ''; // avoid undefined in template string 252 | try { 253 | this.memoizedURL = new URL(`${protocol}://${host}${originalUrl}`); 254 | } 255 | catch (err) { 256 | this.memoizedURL = Object.create(null); 257 | } 258 | } 259 | return this.memoizedURL; 260 | }, 261 | /** 262 | * Check if the request is fresh, aka 263 | * Last-Modified and/or the ETag 264 | * still match. 265 | * 266 | * @return {Boolean} 267 | * @api public 268 | */ 269 | get fresh() { 270 | const method = this.method; 271 | const s = this.ctx.status; 272 | // GET or HEAD for weak freshness validation only 273 | if ('GET' != method && 'HEAD' != method) 274 | return false; 275 | // 2xx or 304 as per rfc2616 14.26 276 | if ((s >= 200 && s < 300) || 304 == s) { 277 | return fresh(this.header, this.response.header); 278 | } 279 | return false; 280 | }, 281 | /** 282 | * Check if the request is stale, aka 283 | * "Last-Modified" and / or the "ETag" for the 284 | * resource has changed. 285 | * 286 | * @return {Boolean} 287 | * @api public 288 | */ 289 | get stale() { 290 | return !this.fresh; 291 | }, 292 | /** 293 | * Check if the request is idempotent. 294 | * 295 | * @return {Boolean} 296 | * @api public 297 | */ 298 | get idempotent() { 299 | const methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']; 300 | return !!~methods.indexOf(this.method); 301 | }, 302 | /** 303 | * Return the request socket. 304 | * 305 | * @return {Connection} 306 | * @api public 307 | */ 308 | get socket() { 309 | return this.req.socket; 310 | }, 311 | /** 312 | * Get the charset when present or undefined. 313 | * 314 | * @return {String} 315 | * @api public 316 | */ 317 | get charset() { 318 | try { 319 | const { parameters } = contentType.parse(this.req); 320 | return parameters.charset || ''; 321 | } 322 | catch (e) { 323 | return ''; 324 | } 325 | }, 326 | /** 327 | * Return parsed Content-Length when present. 328 | * 329 | * @return {Number} 330 | * @api public 331 | */ 332 | get length() { 333 | const len = this.get('Content-Length'); 334 | if (len == '') 335 | return; 336 | return ~~len; 337 | }, 338 | /** 339 | * Return the protocol string "http" or "https" 340 | * when requested with TLS. When the proxy setting 341 | * is enabled the "X-Forwarded-Proto" header 342 | * field will be trusted. If you're running behind 343 | * a reverse proxy that supplies https for you this 344 | * may be enabled. 345 | * 346 | * @return {String} 347 | * @api public 348 | */ 349 | get protocol() { 350 | if (this.socket.encrypted) 351 | return 'https'; 352 | if (!this.app.proxy) 353 | return 'http'; 354 | const proto = this.get('X-Forwarded-Proto'); 355 | return proto ? proto.split(/\s*,\s*/, 1)[0] : 'http'; 356 | }, 357 | /** 358 | * Short-hand for: 359 | * 360 | * this.protocol == 'https' 361 | * 362 | * @return {Boolean} 363 | * @api public 364 | */ 365 | get secure() { 366 | return 'https' == this.protocol; 367 | }, 368 | /** 369 | * When `app.proxy` is `true`, parse 370 | * the "X-Forwarded-For" ip address list. 371 | * 372 | * For example if the value were "client, proxy1, proxy2" 373 | * you would receive the array `["client", "proxy1", "proxy2"]` 374 | * where "proxy2" is the furthest down-stream. 375 | * 376 | * @return {Array} 377 | * @api public 378 | */ 379 | get ips() { 380 | const proxy = this.app.proxy; 381 | const val = this.get('X-Forwarded-For'); 382 | return proxy && val 383 | ? val.split(/\s*,\s*/) 384 | : []; 385 | }, 386 | /** 387 | * Return request's remote address 388 | * When `app.proxy` is `true`, parse 389 | * the "X-Forwarded-For" ip address list and return the first one 390 | * 391 | * @return {String} 392 | * @api public 393 | */ 394 | get ip() { 395 | if (!this[IP]) { 396 | this[IP] = this.ips[0] || this.socket.remoteAddress || ''; 397 | } 398 | return this[IP]; 399 | }, 400 | set ip(_ip) { 401 | this[IP] = _ip; 402 | }, 403 | /** 404 | * Return subdomains as an array. 405 | * 406 | * Subdomains are the dot-separated parts of the host before the main domain 407 | * of the app. By default, the domain of the app is assumed to be the last two 408 | * parts of the host. This can be changed by setting `app.subdomainOffset`. 409 | * 410 | * For example, if the domain is "tobi.ferrets.example.com": 411 | * If `app.subdomainOffset` is not set, this.subdomains is 412 | * `["ferrets", "tobi"]`. 413 | * If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`. 414 | * 415 | * @return {Array} 416 | * @api public 417 | */ 418 | get subdomains() { 419 | const offset = this.app.subdomainOffset; 420 | const hostname = this.hostname; 421 | if (net.isIP(hostname)) 422 | return []; 423 | return hostname 424 | .split('.') 425 | .reverse() 426 | .slice(offset); 427 | }, 428 | /** 429 | * Get accept object. 430 | * Lazily memoized. 431 | * 432 | * @return {Object} 433 | * @api private 434 | */ 435 | get accept() { 436 | return this._accept || (this._accept = accepts(this.req)); 437 | }, 438 | /** 439 | * Set accept object. 440 | * 441 | * @param {Object} 442 | * @api private 443 | */ 444 | set accept(obj) { 445 | this._accept = obj; 446 | }, 447 | /** 448 | * Check if the given `type(s)` is acceptable, returning 449 | * the best match when true, otherwise `false`, in which 450 | * case you should respond with 406 "Not Acceptable". 451 | * 452 | * The `type` value may be a single mime type string 453 | * such as "application/json", the extension name 454 | * such as "json" or an array `["json", "html", "text/plain"]`. When a list 455 | * or array is given the _best_ match, if any is returned. 456 | * 457 | * Examples: 458 | * 459 | * // Accept: text/html 460 | * this.accepts('html'); 461 | * // => "html" 462 | * 463 | * // Accept: text/*, application/json 464 | * this.accepts('html'); 465 | * // => "html" 466 | * this.accepts('text/html'); 467 | * // => "text/html" 468 | * this.accepts('json', 'text'); 469 | * // => "json" 470 | * this.accepts('application/json'); 471 | * // => "application/json" 472 | * 473 | * // Accept: text/*, application/json 474 | * this.accepts('image/png'); 475 | * this.accepts('png'); 476 | * // => false 477 | * 478 | * // Accept: text/*;q=.5, application/json 479 | * this.accepts(['html', 'json']); 480 | * this.accepts('html', 'json'); 481 | * // => "json" 482 | * 483 | * @param {String|Array} type(s)... 484 | * @return {String|Array|false} 485 | * @api public 486 | */ 487 | accepts(...args) { 488 | return this.accept.types(...args); 489 | }, 490 | /** 491 | * Return accepted encodings or best fit based on `encodings`. 492 | * 493 | * Given `Accept-Encoding: gzip, deflate` 494 | * an array sorted by quality is returned: 495 | * 496 | * ['gzip', 'deflate'] 497 | * 498 | * @param {String|Array} encoding(s)... 499 | * @return {String|Array} 500 | * @api public 501 | */ 502 | acceptsEncodings(...args) { 503 | return this.accept.encodings(...args); 504 | }, 505 | /** 506 | * Return accepted charsets or best fit based on `charsets`. 507 | * 508 | * Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5` 509 | * an array sorted by quality is returned: 510 | * 511 | * ['utf-8', 'utf-7', 'iso-8859-1'] 512 | * 513 | * @param {String|Array} charset(s)... 514 | * @return {String|Array} 515 | * @api public 516 | */ 517 | acceptsCharsets(...args) { 518 | return this.accept.charsets(...args); 519 | }, 520 | /** 521 | * Return accepted languages or best fit based on `langs`. 522 | * 523 | * Given `Accept-Language: en;q=0.8, es, pt` 524 | * an array sorted by quality is returned: 525 | * 526 | * ['es', 'pt', 'en'] 527 | * 528 | * @param {String|Array} lang(s)... 529 | * @return {Array|String} 530 | * @api public 531 | */ 532 | acceptsLanguages(...args) { 533 | return this.accept.languages(...args); 534 | }, 535 | /** 536 | * Check if the incoming request contains the "Content-Type" 537 | * header field, and it contains any of the give mime `type`s. 538 | * If there is no request body, `null` is returned. 539 | * If there is no content type, `false` is returned. 540 | * Otherwise, it returns the first `type` that matches. 541 | * 542 | * Examples: 543 | * 544 | * // With Content-Type: text/html; charset=utf-8 545 | * this.is('html'); // => 'html' 546 | * this.is('text/html'); // => 'text/html' 547 | * this.is('text/*', 'application/json'); // => 'text/html' 548 | * 549 | * // When Content-Type is application/json 550 | * this.is('json', 'urlencoded'); // => 'json' 551 | * this.is('application/json'); // => 'application/json' 552 | * this.is('html', 'application/*'); // => 'application/json' 553 | * 554 | * this.is('html'); // => false 555 | * 556 | * @param {String|Array} types... 557 | * @return {String|false|null} 558 | * @api public 559 | */ 560 | is(types) { 561 | if (!types) 562 | return typeis(this.req); 563 | if (!Array.isArray(types)) 564 | types = [].slice.call(arguments); 565 | return typeis(this.req, types); 566 | }, 567 | /** 568 | * Return the request mime type void of 569 | * parameters such as "charset". 570 | * 571 | * @return {String} 572 | * @api public 573 | */ 574 | get type() { 575 | const type = this.get('Content-Type'); 576 | if (!type) 577 | return ''; 578 | return type.split(';')[0]; 579 | }, 580 | /** 581 | * Return request header. 582 | * 583 | * The `Referrer` header field is special-cased, 584 | * both `Referrer` and `Referer` are interchangeable. 585 | * 586 | * Examples: 587 | * 588 | * this.get('Content-Type'); 589 | * // => "text/plain" 590 | * 591 | * this.get('content-type'); 592 | * // => "text/plain" 593 | * 594 | * this.get('Something'); 595 | * // => '' 596 | * 597 | * @param {String} field 598 | * @return {String} 599 | * @api public 600 | */ 601 | get(field) { 602 | const req = this.req; 603 | switch (field = field.toLowerCase()) { 604 | case 'referer': 605 | case 'referrer': 606 | return req.headers.referrer || req.headers.referer || ''; 607 | default: 608 | return req.headers[field] || ''; 609 | } 610 | }, 611 | /** 612 | * Inspect implementation. 613 | * 614 | * @return {Object} 615 | * @api public 616 | */ 617 | inspect() { 618 | if (!this.req) 619 | return; 620 | return this.toJSON(); 621 | }, 622 | /** 623 | * Return JSON representation. 624 | * 625 | * @return {Object} 626 | * @api public 627 | */ 628 | toJSON() { 629 | return only(this, [ 630 | 'method', 631 | 'url', 632 | 'header' 633 | ]); 634 | } 635 | }; 636 | /** 637 | * Custom inspection implementation for newer Node.js versions. 638 | * 639 | * @return {Object} 640 | * @api public 641 | */ 642 | /* istanbul ignore else */ 643 | if (util.inspect.custom) { 644 | module.exports[util.inspect.custom] = module.exports.inspect; 645 | } 646 | -------------------------------------------------------------------------------- /lib/request.ts: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | /** 5 | * Module dependencies. 6 | */ 7 | 8 | import net = require('net'); 9 | import accepts = require('accepts'); 10 | import contentType = require('content-type'); 11 | import parse = require('parseurl'); 12 | import qs = require('querystring'); 13 | import typeis = require('type-is'); 14 | import fresh = require('fresh'); 15 | import only = require('only'); 16 | import util = require('util'); 17 | const URL = require('url').URL; 18 | const stringify = require('url').format; 19 | 20 | const IP = Symbol('context#ip'); 21 | 22 | /** 23 | * Prototype. 24 | */ 25 | 26 | module.exports = { 27 | 28 | /** 29 | * Return request header. 30 | * 31 | * @return {Object} 32 | * @api public 33 | */ 34 | 35 | get header(): object { 36 | return this.req.headers; 37 | }, 38 | 39 | /** 40 | * Set request header. 41 | * 42 | * @api public 43 | */ 44 | 45 | set header(val) { 46 | this.req.headers = val; 47 | }, 48 | 49 | /** 50 | * Return request header, alias as request.header 51 | * 52 | * @return {Object} 53 | * @api public 54 | */ 55 | 56 | get headers(): object { 57 | return this.req.headers; 58 | }, 59 | 60 | /** 61 | * Set request header, alias as request.header 62 | * 63 | * @api public 64 | */ 65 | 66 | set headers(val) { 67 | this.req.headers = val; 68 | }, 69 | 70 | /** 71 | * Get request URL. 72 | * 73 | * @return {String} 74 | * @api public 75 | */ 76 | 77 | get url(): string { 78 | return this.req.url; 79 | }, 80 | 81 | /** 82 | * Set request URL. 83 | * 84 | * @api public 85 | */ 86 | 87 | set url(val) { 88 | this.req.url = val; 89 | }, 90 | 91 | /** 92 | * Get origin of URL. 93 | * 94 | * @return {String} 95 | * @api public 96 | */ 97 | 98 | get origin(): string { 99 | return `${this.protocol}://${this.host}`; 100 | }, 101 | 102 | /** 103 | * Get full request URL. 104 | * 105 | * @return {String} 106 | * @api public 107 | */ 108 | 109 | get href(): string { 110 | // support: `GET http://example.com/foo` 111 | if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl; 112 | return this.origin + this.originalUrl; 113 | }, 114 | 115 | /** 116 | * Get request method. 117 | * 118 | * @return {String} 119 | * @api public 120 | */ 121 | 122 | get method(): string { 123 | return this.req.method; 124 | }, 125 | 126 | /** 127 | * Set request method. 128 | * 129 | * @param {String} val 130 | * @api public 131 | */ 132 | 133 | set method(val: string) { 134 | this.req.method = val; 135 | }, 136 | 137 | /** 138 | * Get request pathname. 139 | * 140 | * @return {String} 141 | * @api public 142 | */ 143 | 144 | get path(): string { 145 | return parse(this.req).pathname; 146 | }, 147 | 148 | /** 149 | * Set pathname, retaining the query-string when present. 150 | * 151 | * @param {String} path 152 | * @api public 153 | */ 154 | 155 | set path(path: string) { 156 | const url = parse(this.req); 157 | if (url.pathname === path) return; 158 | 159 | url.pathname = path; 160 | url.path = null; 161 | 162 | this.url = stringify(url); 163 | }, 164 | 165 | /** 166 | * Get parsed query-string. 167 | * 168 | * @return {Object} 169 | * @api public 170 | */ 171 | 172 | get query(): any { 173 | const str = this.querystring; 174 | const c = this._querycache = this._querycache || {}; 175 | return c[str] || (c[str] = qs.parse(str)); 176 | }, 177 | 178 | /** 179 | * Set query-string as an object. 180 | * 181 | * @param {Object} obj 182 | * @api public 183 | */ 184 | 185 | set query(obj: any) { 186 | this.querystring = qs.stringify(obj); 187 | }, 188 | 189 | /** 190 | * Get query string. 191 | * 192 | * @return {String} 193 | * @api public 194 | */ 195 | 196 | get querystring(): any { 197 | if (!this.req) return ''; 198 | return parse(this.req).query || ''; 199 | }, 200 | 201 | /** 202 | * Set querystring. 203 | * 204 | * @param {String} str 205 | * @api public 206 | */ 207 | 208 | set querystring(str: any) { 209 | const url = parse(this.req); 210 | if (url.search === `?${str}`) return; 211 | 212 | url.search = str; 213 | url.path = null; 214 | 215 | this.url = stringify(url); 216 | }, 217 | 218 | /** 219 | * Get the search string. Same as the querystring 220 | * except it includes the leading ?. 221 | * 222 | * @return {String} 223 | * @api public 224 | */ 225 | 226 | get search(): string { 227 | if (!this.querystring) return ''; 228 | return `?${this.querystring}`; 229 | }, 230 | 231 | /** 232 | * Set the search string. Same as 233 | * request.querystring= but included for ubiquity. 234 | * 235 | * @param {String} str 236 | * @api public 237 | */ 238 | 239 | set search(str: string) { 240 | this.querystring = str; 241 | }, 242 | 243 | /** 244 | * Parse the "Host" header field host 245 | * and support X-Forwarded-Host when a 246 | * proxy is enabled. 247 | * 248 | * @return {String} hostname:port 249 | * @api public 250 | */ 251 | 252 | get host(): string { 253 | const proxy = this.app.proxy; 254 | let host = proxy && this.get('X-Forwarded-Host'); 255 | if (!host) { 256 | if (this.req.httpVersionMajor >= 2) host = this.get(':authority'); 257 | if (!host) host = this.get('Host'); 258 | } 259 | if (!host) return ''; 260 | return host.split(/\s*,\s*/, 1)[0]; 261 | }, 262 | 263 | /** 264 | * Parse the "Host" header field hostname 265 | * and support X-Forwarded-Host when a 266 | * proxy is enabled. 267 | * 268 | * @return {String} hostname 269 | * @api public 270 | */ 271 | 272 | get hostname(): string { 273 | const host = this.host; 274 | if (!host) return ''; 275 | if ('[' == host[0]) return this.URL.hostname || ''; // IPv6 276 | return host.split(':', 1)[0]; 277 | }, 278 | 279 | /** 280 | * Get WHATWG parsed URL. 281 | * Lazily memoized. 282 | * 283 | * @return {URL|Object} 284 | * @api public 285 | */ 286 | 287 | get URL(): URL | object { 288 | /* istanbul ignore else */ 289 | if (!this.memoizedURL) { 290 | const protocol = this.protocol; 291 | const host = this.host; 292 | const originalUrl = this.originalUrl || ''; // avoid undefined in template string 293 | try { 294 | this.memoizedURL = new URL(`${protocol}://${host}${originalUrl}`); 295 | } catch (err) { 296 | this.memoizedURL = Object.create(null); 297 | } 298 | } 299 | return this.memoizedURL; 300 | }, 301 | 302 | /** 303 | * Check if the request is fresh, aka 304 | * Last-Modified and/or the ETag 305 | * still match. 306 | * 307 | * @return {Boolean} 308 | * @api public 309 | */ 310 | 311 | get fresh(): boolean { 312 | const method = this.method; 313 | const s = this.ctx.status; 314 | 315 | // GET or HEAD for weak freshness validation only 316 | if ('GET' != method && 'HEAD' != method) return false; 317 | 318 | // 2xx or 304 as per rfc2616 14.26 319 | if ((s >= 200 && s < 300) || 304 == s) { 320 | return fresh(this.header, this.response.header); 321 | } 322 | 323 | return false; 324 | }, 325 | 326 | /** 327 | * Check if the request is stale, aka 328 | * "Last-Modified" and / or the "ETag" for the 329 | * resource has changed. 330 | * 331 | * @return {Boolean} 332 | * @api public 333 | */ 334 | 335 | get stale(): boolean { 336 | return !this.fresh; 337 | }, 338 | 339 | /** 340 | * Check if the request is idempotent. 341 | * 342 | * @return {Boolean} 343 | * @api public 344 | */ 345 | 346 | get idempotent(): boolean { 347 | const methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']; 348 | return !!~methods.indexOf(this.method); 349 | }, 350 | 351 | /** 352 | * Return the request socket. 353 | * 354 | * @return {Connection} 355 | * @api public 356 | */ 357 | 358 | get socket(): object { 359 | return this.req.socket; 360 | }, 361 | 362 | /** 363 | * Get the charset when present or undefined. 364 | * 365 | * @return {String} 366 | * @api public 367 | */ 368 | 369 | get charset(): string { 370 | try { 371 | const { parameters } = contentType.parse(this.req); 372 | return parameters.charset || ''; 373 | } catch (e) { 374 | return ''; 375 | } 376 | }, 377 | 378 | /** 379 | * Return parsed Content-Length when present. 380 | * 381 | * @return {Number} 382 | * @api public 383 | */ 384 | 385 | get length(): number { 386 | const len = this.get('Content-Length'); 387 | if (len == '') return; 388 | return ~~len; 389 | }, 390 | 391 | /** 392 | * Return the protocol string "http" or "https" 393 | * when requested with TLS. When the proxy setting 394 | * is enabled the "X-Forwarded-Proto" header 395 | * field will be trusted. If you're running behind 396 | * a reverse proxy that supplies https for you this 397 | * may be enabled. 398 | * 399 | * @return {String} 400 | * @api public 401 | */ 402 | 403 | get protocol(): string { 404 | if (this.socket.encrypted) return 'https'; 405 | if (!this.app.proxy) return 'http'; 406 | const proto = this.get('X-Forwarded-Proto'); 407 | return proto ? proto.split(/\s*,\s*/, 1)[0] : 'http'; 408 | }, 409 | 410 | /** 411 | * Short-hand for: 412 | * 413 | * this.protocol == 'https' 414 | * 415 | * @return {Boolean} 416 | * @api public 417 | */ 418 | 419 | get secure(): boolean { 420 | return 'https' == this.protocol; 421 | }, 422 | 423 | /** 424 | * When `app.proxy` is `true`, parse 425 | * the "X-Forwarded-For" ip address list. 426 | * 427 | * For example if the value were "client, proxy1, proxy2" 428 | * you would receive the array `["client", "proxy1", "proxy2"]` 429 | * where "proxy2" is the furthest down-stream. 430 | * 431 | * @return {Array} 432 | * @api public 433 | */ 434 | 435 | get ips(): Array { 436 | const proxy = this.app.proxy; 437 | const val = this.get('X-Forwarded-For'); 438 | return proxy && val 439 | ? val.split(/\s*,\s*/) 440 | : []; 441 | }, 442 | 443 | /** 444 | * Return request's remote address 445 | * When `app.proxy` is `true`, parse 446 | * the "X-Forwarded-For" ip address list and return the first one 447 | * 448 | * @return {String} 449 | * @api public 450 | */ 451 | 452 | get ip(): string { 453 | if (!this[IP]) { 454 | this[IP] = this.ips[0] || this.socket.remoteAddress || ''; 455 | } 456 | return this[IP]; 457 | }, 458 | 459 | set ip(_ip) { 460 | this[IP] = _ip; 461 | }, 462 | 463 | /** 464 | * Return subdomains as an array. 465 | * 466 | * Subdomains are the dot-separated parts of the host before the main domain 467 | * of the app. By default, the domain of the app is assumed to be the last two 468 | * parts of the host. This can be changed by setting `app.subdomainOffset`. 469 | * 470 | * For example, if the domain is "tobi.ferrets.example.com": 471 | * If `app.subdomainOffset` is not set, this.subdomains is 472 | * `["ferrets", "tobi"]`. 473 | * If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`. 474 | * 475 | * @return {Array} 476 | * @api public 477 | */ 478 | 479 | get subdomains(): Array { 480 | const offset = this.app.subdomainOffset; 481 | const hostname = this.hostname; 482 | if (net.isIP(hostname)) return []; 483 | return hostname 484 | .split('.') 485 | .reverse() 486 | .slice(offset); 487 | }, 488 | 489 | /** 490 | * Get accept object. 491 | * Lazily memoized. 492 | * 493 | * @return {Object} 494 | * @api private 495 | */ 496 | get accept(): object { 497 | return this._accept || (this._accept = accepts(this.req)); 498 | }, 499 | 500 | /** 501 | * Set accept object. 502 | * 503 | * @param {Object} 504 | * @api private 505 | */ 506 | set accept(obj: object) { 507 | this._accept = obj; 508 | }, 509 | 510 | /** 511 | * Check if the given `type(s)` is acceptable, returning 512 | * the best match when true, otherwise `false`, in which 513 | * case you should respond with 406 "Not Acceptable". 514 | * 515 | * The `type` value may be a single mime type string 516 | * such as "application/json", the extension name 517 | * such as "json" or an array `["json", "html", "text/plain"]`. When a list 518 | * or array is given the _best_ match, if any is returned. 519 | * 520 | * Examples: 521 | * 522 | * // Accept: text/html 523 | * this.accepts('html'); 524 | * // => "html" 525 | * 526 | * // Accept: text/*, application/json 527 | * this.accepts('html'); 528 | * // => "html" 529 | * this.accepts('text/html'); 530 | * // => "text/html" 531 | * this.accepts('json', 'text'); 532 | * // => "json" 533 | * this.accepts('application/json'); 534 | * // => "application/json" 535 | * 536 | * // Accept: text/*, application/json 537 | * this.accepts('image/png'); 538 | * this.accepts('png'); 539 | * // => false 540 | * 541 | * // Accept: text/*;q=.5, application/json 542 | * this.accepts(['html', 'json']); 543 | * this.accepts('html', 'json'); 544 | * // => "json" 545 | * 546 | * @param {String|Array} type(s)... 547 | * @return {String|Array|false} 548 | * @api public 549 | */ 550 | 551 | accepts(...args: any[]): string | Array | false { 552 | return this.accept.types(...args); 553 | }, 554 | 555 | /** 556 | * Return accepted encodings or best fit based on `encodings`. 557 | * 558 | * Given `Accept-Encoding: gzip, deflate` 559 | * an array sorted by quality is returned: 560 | * 561 | * ['gzip', 'deflate'] 562 | * 563 | * @param {String|Array} encoding(s)... 564 | * @return {String|Array} 565 | * @api public 566 | */ 567 | 568 | acceptsEncodings(...args: any[]): string | Array { 569 | return this.accept.encodings(...args); 570 | }, 571 | 572 | /** 573 | * Return accepted charsets or best fit based on `charsets`. 574 | * 575 | * Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5` 576 | * an array sorted by quality is returned: 577 | * 578 | * ['utf-8', 'utf-7', 'iso-8859-1'] 579 | * 580 | * @param {String|Array} charset(s)... 581 | * @return {String|Array} 582 | * @api public 583 | */ 584 | 585 | acceptsCharsets(...args: any[]): string | Array { 586 | return this.accept.charsets(...args); 587 | }, 588 | 589 | /** 590 | * Return accepted languages or best fit based on `langs`. 591 | * 592 | * Given `Accept-Language: en;q=0.8, es, pt` 593 | * an array sorted by quality is returned: 594 | * 595 | * ['es', 'pt', 'en'] 596 | * 597 | * @param {String|Array} lang(s)... 598 | * @return {Array|String} 599 | * @api public 600 | */ 601 | 602 | acceptsLanguages(...args: any[]): Array | string { 603 | return this.accept.languages(...args); 604 | }, 605 | 606 | /** 607 | * Check if the incoming request contains the "Content-Type" 608 | * header field, and it contains any of the give mime `type`s. 609 | * If there is no request body, `null` is returned. 610 | * If there is no content type, `false` is returned. 611 | * Otherwise, it returns the first `type` that matches. 612 | * 613 | * Examples: 614 | * 615 | * // With Content-Type: text/html; charset=utf-8 616 | * this.is('html'); // => 'html' 617 | * this.is('text/html'); // => 'text/html' 618 | * this.is('text/*', 'application/json'); // => 'text/html' 619 | * 620 | * // When Content-Type is application/json 621 | * this.is('json', 'urlencoded'); // => 'json' 622 | * this.is('application/json'); // => 'application/json' 623 | * this.is('html', 'application/*'); // => 'application/json' 624 | * 625 | * this.is('html'); // => false 626 | * 627 | * @param {String|Array} types... 628 | * @return {String|false|null} 629 | * @api public 630 | */ 631 | 632 | is(types: string[]): string | false | null { 633 | if (!types) return typeis(this.req); 634 | if (!Array.isArray(types)) types = [].slice.call(arguments); 635 | return typeis(this.req, types); 636 | }, 637 | 638 | /** 639 | * Return the request mime type void of 640 | * parameters such as "charset". 641 | * 642 | * @return {String} 643 | * @api public 644 | */ 645 | 646 | get type(): string { 647 | const type = this.get('Content-Type'); 648 | if (!type) return ''; 649 | return type.split(';')[0]; 650 | }, 651 | 652 | /** 653 | * Return request header. 654 | * 655 | * The `Referrer` header field is special-cased, 656 | * both `Referrer` and `Referer` are interchangeable. 657 | * 658 | * Examples: 659 | * 660 | * this.get('Content-Type'); 661 | * // => "text/plain" 662 | * 663 | * this.get('content-type'); 664 | * // => "text/plain" 665 | * 666 | * this.get('Something'); 667 | * // => '' 668 | * 669 | * @param {String} field 670 | * @return {String} 671 | * @api public 672 | */ 673 | 674 | get(field: string): string { 675 | const req = this.req; 676 | switch (field = field.toLowerCase()) { 677 | case 'referer': 678 | case 'referrer': 679 | return req.headers.referrer || req.headers.referer || ''; 680 | default: 681 | return req.headers[field] || ''; 682 | } 683 | }, 684 | 685 | /** 686 | * Inspect implementation. 687 | * 688 | * @return {Object} 689 | * @api public 690 | */ 691 | 692 | inspect(): object { 693 | if (!this.req) return; 694 | return this.toJSON(); 695 | }, 696 | 697 | /** 698 | * Return JSON representation. 699 | * 700 | * @return {Object} 701 | * @api public 702 | */ 703 | 704 | toJSON(): object { 705 | return only(this, [ 706 | 'method', 707 | 'url', 708 | 'header' 709 | ]); 710 | } 711 | }; 712 | 713 | /** 714 | * Custom inspection implementation for newer Node.js versions. 715 | * 716 | * @return {Object} 717 | * @api public 718 | */ 719 | 720 | /* istanbul ignore else */ 721 | if (util.inspect.custom) { 722 | module.exports[util.inspect.custom] = module.exports.inspect; 723 | } 724 | -------------------------------------------------------------------------------- /lib/response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | /** 4 | * Module dependencies. 5 | */ 6 | const contentDisposition = require("content-disposition"); 7 | const ensureErrorHandler = require("error-inject"); 8 | const getType = require("cache-content-type"); 9 | const onFinish = require("on-finished"); 10 | const isJSON = require("koa-is-json"); 11 | const escape = require("escape-html"); 12 | const statuses = require("statuses"); 13 | const destroy = require("destroy"); 14 | const assert = require("assert"); 15 | const vary = require("vary"); 16 | const only = require("only"); 17 | const util = require("util"); 18 | const extname = require('path').extname; 19 | const typeis = require('type-is').is; 20 | /** 21 | * Prototype. 22 | */ 23 | exports = { 24 | /** 25 | * Return the request socket. 26 | * 27 | * @return {Connection} 28 | * @api public 29 | */ 30 | get socket() { 31 | return this.res.socket; 32 | }, 33 | /** 34 | * Return response header. 35 | * 36 | * @return {Object} 37 | * @api public 38 | */ 39 | get header() { 40 | const { res } = this; 41 | return typeof res.getHeaders === 'function' 42 | ? res.getHeaders() 43 | : res._headers || {}; // Node < 7.7 44 | }, 45 | /** 46 | * Return response header, alias as response.header 47 | * 48 | * @return {Object} 49 | * @api public 50 | */ 51 | get headers() { 52 | return this.header; 53 | }, 54 | /** 55 | * Get response status code. 56 | * 57 | * @return {Number} 58 | * @api public 59 | */ 60 | get status() { 61 | return this.res.statusCode; 62 | }, 63 | /** 64 | * Set response status code. 65 | * 66 | * @param {Number} code 67 | * @api public 68 | */ 69 | set status(code) { 70 | if (this.headerSent) 71 | return; 72 | assert(Number.isInteger(code), 'status code must be a number'); 73 | assert(code >= 100 && code <= 999, `invalid status code: ${code}`); 74 | this._explicitStatus = true; 75 | this.res.statusCode = code; 76 | if (this.req.httpVersionMajor < 2) 77 | this.res.statusMessage = statuses[code]; 78 | if (this.body && statuses.empty[code]) 79 | this.body = null; 80 | }, 81 | /** 82 | * Get response status message 83 | * 84 | * @return {String} 85 | * @api public 86 | */ 87 | get message() { 88 | return this.res.statusMessage || statuses[this.status]; 89 | }, 90 | /** 91 | * Set response status message 92 | * 93 | * @param {String} msg 94 | * @api public 95 | */ 96 | set message(msg) { 97 | this.res.statusMessage = msg; 98 | }, 99 | /** 100 | * Get response body. 101 | * 102 | * @return {Mixed} 103 | * @api public 104 | */ 105 | get body() { 106 | return this._body; 107 | }, 108 | /** 109 | * Set response body. 110 | * 111 | * @param {String|Buffer|Object|Stream} val 112 | * @api public 113 | */ 114 | set body(val) { 115 | const original = this._body; 116 | this._body = val; 117 | // no content 118 | if (null == val) { 119 | if (!statuses.empty[this.status]) 120 | this.status = 204; 121 | this.remove('Content-Type'); 122 | this.remove('Content-Length'); 123 | this.remove('Transfer-Encoding'); 124 | return; 125 | } 126 | // set the status 127 | if (!this._explicitStatus) 128 | this.status = 200; 129 | // set the content-type only if not yet set 130 | const setType = !this.header['content-type']; 131 | // string 132 | if ('string' == typeof val) { 133 | if (setType) 134 | this.type = /^\s* this.ctx.onerror(err)); 149 | // overwriting 150 | if (null != original && original != val) 151 | this.remove('Content-Length'); 152 | if (setType) 153 | this.type = 'bin'; 154 | return; 155 | } 156 | // json 157 | this.remove('Content-Length'); 158 | this.type = 'json'; 159 | }, 160 | /** 161 | * Set Content-Length field to `n`. 162 | * 163 | * @param {Number} n 164 | * @api public 165 | */ 166 | set length(n) { 167 | this.set('Content-Length', n); 168 | }, 169 | /** 170 | * Return parsed response Content-Length when present. 171 | * 172 | * @return {Number} 173 | * @api public 174 | */ 175 | get length() { 176 | const len = this.header['content-length']; 177 | const body = this.body; 178 | if (null == len) { 179 | if (!body) 180 | return; 181 | if ('string' == typeof body) 182 | return Buffer.byteLength(body); 183 | if (Buffer.isBuffer(body)) 184 | return body.length; 185 | if (isJSON(body)) 186 | return Buffer.byteLength(JSON.stringify(body)); 187 | return; 188 | } 189 | return Math.trunc(len) || 0; 190 | }, 191 | /** 192 | * Check if a header has been written to the socket. 193 | * 194 | * @return {Boolean} 195 | * @api public 196 | */ 197 | get headerSent() { 198 | return this.res.headersSent; 199 | }, 200 | /** 201 | * Vary on `field`. 202 | * 203 | * @param {String} field 204 | * @api public 205 | */ 206 | vary(field) { 207 | if (this.headerSent) 208 | return; 209 | vary(this.res, field); 210 | }, 211 | /** 212 | * Perform a 302 redirect to `url`. 213 | * 214 | * The string "back" is special-cased 215 | * to provide Referrer support, when Referrer 216 | * is not present `alt` or "/" is used. 217 | * 218 | * Examples: 219 | * 220 | * this.redirect('back'); 221 | * this.redirect('back', '/index.html'); 222 | * this.redirect('/login'); 223 | * this.redirect('http://google.com'); 224 | * 225 | * @param {String} url 226 | * @param {String} [alt] 227 | * @api public 228 | */ 229 | redirect(url, alt) { 230 | // location 231 | if ('back' == url) 232 | url = this.ctx.get('Referrer') || alt || '/'; 233 | this.set('Location', url); 234 | // status 235 | if (!statuses.redirect[this.status]) 236 | this.status = 302; 237 | // html 238 | if (this.ctx.accepts('html')) { 239 | url = escape(url); 240 | this.type = 'text/html; charset=utf-8'; 241 | this.body = `Redirecting to ${url}.`; 242 | return; 243 | } 244 | // text 245 | this.type = 'text/plain; charset=utf-8'; 246 | this.body = `Redirecting to ${url}.`; 247 | }, 248 | /** 249 | * Set Content-Disposition header to "attachment" with optional `filename`. 250 | * 251 | * @param {String} filename 252 | * @api public 253 | */ 254 | attachment(filename, options) { 255 | if (filename) 256 | this.type = extname(filename); 257 | this.set('Content-Disposition', contentDisposition(filename, options)); 258 | }, 259 | /** 260 | * Set Content-Type response header with `type` through `mime.lookup()` 261 | * when it does not contain a charset. 262 | * 263 | * Examples: 264 | * 265 | * this.type = '.html'; 266 | * this.type = 'html'; 267 | * this.type = 'json'; 268 | * this.type = 'application/json'; 269 | * this.type = 'png'; 270 | * 271 | * @param {String} type 272 | * @api public 273 | */ 274 | set type(type) { 275 | type = getType(type); 276 | if (type) { 277 | this.set('Content-Type', type); 278 | } 279 | else { 280 | this.remove('Content-Type'); 281 | } 282 | }, 283 | /** 284 | * Set the Last-Modified date using a string or a Date. 285 | * 286 | * this.response.lastModified = new Date(); 287 | * this.response.lastModified = '2013-09-13'; 288 | * 289 | * @param {String|Date} type 290 | * @api public 291 | */ 292 | set lastModified(val) { 293 | if ('string' == typeof val) 294 | val = new Date(val); 295 | this.set('Last-Modified', val.toUTCString()); 296 | }, 297 | /** 298 | * Get the Last-Modified date in Date form, if it exists. 299 | * 300 | * @return {Date} 301 | * @api public 302 | */ 303 | get lastModified() { 304 | const date = this.get('last-modified'); 305 | if (date) 306 | return new Date(date); 307 | }, 308 | /** 309 | * Set the ETag of a response. 310 | * This will normalize the quotes if necessary. 311 | * 312 | * this.response.etag = 'md5hashsum'; 313 | * this.response.etag = '"md5hashsum"'; 314 | * this.response.etag = 'W/"123456789"'; 315 | * 316 | * @param {String} etag 317 | * @api public 318 | */ 319 | set etag(val) { 320 | if (!/^(W\/)?"/.test(val)) 321 | val = `"${val}"`; 322 | this.set('ETag', val); 323 | }, 324 | /** 325 | * Get the ETag of a response. 326 | * 327 | * @return {String} 328 | * @api public 329 | */ 330 | get etag() { 331 | return this.get('ETag'); 332 | }, 333 | /** 334 | * Return the response mime type void of 335 | * parameters such as "charset". 336 | * 337 | * @return {String} 338 | * @api public 339 | */ 340 | get type() { 341 | const type = this.get('Content-Type'); 342 | if (!type) 343 | return ''; 344 | return type.split(';', 1)[0]; 345 | }, 346 | /** 347 | * Check whether the response is one of the listed types. 348 | * Pretty much the same as `this.request.is()`. 349 | * 350 | * @param {String|Array} types... 351 | * @return {String|false} 352 | * @api public 353 | */ 354 | is(types) { 355 | const type = this.type; 356 | if (!types) 357 | return type || false; 358 | if (!Array.isArray(types)) 359 | types = [].slice.call(arguments); 360 | return typeis(type, types); 361 | }, 362 | /** 363 | * Return response header. 364 | * 365 | * Examples: 366 | * 367 | * this.get('Content-Type'); 368 | * // => "text/plain" 369 | * 370 | * this.get('content-type'); 371 | * // => "text/plain" 372 | * 373 | * @param {String} field 374 | * @return {String} 375 | * @api public 376 | */ 377 | get(field) { 378 | return this.header[field.toLowerCase()] || ''; 379 | }, 380 | /** 381 | * Set header `field` to `val`, or pass 382 | * an object of header fields. 383 | * 384 | * Examples: 385 | * 386 | * this.set('Foo', ['bar', 'baz']); 387 | * this.set('Accept', 'application/json'); 388 | * this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); 389 | * 390 | * @param {String|Object|Array} field 391 | * @param {String} val 392 | * @api public 393 | */ 394 | set(field, val) { 395 | if (this.headerSent) 396 | return; 397 | if (2 == arguments.length) { 398 | if (Array.isArray(val)) 399 | val = val.map(v => typeof v === 'string' ? v : String(v)); 400 | else if (typeof val !== 'string') 401 | val = String(val); 402 | this.res.setHeader(field, val); 403 | } 404 | else { 405 | for (const key in field) { 406 | this.set(key, field[key]); 407 | } 408 | } 409 | }, 410 | /** 411 | * Append additional header `field` with value `val`. 412 | * 413 | * Examples: 414 | * 415 | * ``` 416 | * this.append('Link', ['', '']); 417 | * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); 418 | * this.append('Warning', '199 Miscellaneous warning'); 419 | * ``` 420 | * 421 | * @param {String} field 422 | * @param {String|Array} val 423 | * @api public 424 | */ 425 | append(field, val) { 426 | const prev = this.get(field); 427 | if (prev) { 428 | val = Array.isArray(prev) 429 | ? prev.concat(val) 430 | : [prev].concat(val); 431 | } 432 | return this.set(field, val); 433 | }, 434 | /** 435 | * Remove header `field`. 436 | * 437 | * @param {String} name 438 | * @api public 439 | */ 440 | remove(field) { 441 | if (this.headerSent) 442 | return; 443 | this.res.removeHeader(field); 444 | }, 445 | /** 446 | * Checks if the request is writable. 447 | * Tests for the existence of the socket 448 | * as node sometimes does not set it. 449 | * 450 | * @return {Boolean} 451 | * @api private 452 | */ 453 | get writable() { 454 | // can't write any more after response finished 455 | if (this.res.finished) 456 | return false; 457 | const socket = this.res.socket; 458 | // There are already pending outgoing res, but still writable 459 | // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486 460 | if (!socket) 461 | return true; 462 | return socket.writable; 463 | }, 464 | /** 465 | * Inspect implementation. 466 | * 467 | * @return {Object} 468 | * @api public 469 | */ 470 | inspect() { 471 | if (!this.res) 472 | return; 473 | const o = this.toJSON(); 474 | o.body = this.body; 475 | return o; 476 | }, 477 | /** 478 | * Return JSON representation. 479 | * 480 | * @return {Object} 481 | * @api public 482 | */ 483 | toJSON() { 484 | return only(this, [ 485 | 'status', 486 | 'message', 487 | 'header' 488 | ]); 489 | }, 490 | /** 491 | * Flush any set headers, and begin the body 492 | */ 493 | flushHeaders() { 494 | this.res.flushHeaders(); 495 | } 496 | }; 497 | /** 498 | * Custom inspection implementation for newer Node.js versions. 499 | * 500 | * @return {Object} 501 | * @api public 502 | */ 503 | if (util.inspect.custom) { 504 | exports[util.inspect.custom] = exports.inspect; 505 | } 506 | -------------------------------------------------------------------------------- /lib/response.ts: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | /** 5 | * Module dependencies. 6 | */ 7 | 8 | import contentDisposition = require('content-disposition'); 9 | import ensureErrorHandler = require('error-inject'); 10 | import getType = require('cache-content-type'); 11 | import onFinish = require('on-finished'); 12 | import isJSON = require('koa-is-json'); 13 | import escape = require('escape-html'); 14 | import statuses = require('statuses'); 15 | import destroy = require('destroy'); 16 | import assert = require('assert'); 17 | import vary = require('vary'); 18 | import only = require('only'); 19 | import util = require('util'); 20 | const extname = require('path').extname; 21 | const typeis = require('type-is').is; 22 | 23 | /** 24 | * Prototype. 25 | */ 26 | 27 | exports = { 28 | 29 | /** 30 | * Return the request socket. 31 | * 32 | * @return {Connection} 33 | * @api public 34 | */ 35 | 36 | get socket(): object { 37 | return this.res.socket; 38 | }, 39 | 40 | /** 41 | * Return response header. 42 | * 43 | * @return {Object} 44 | * @api public 45 | */ 46 | 47 | get header(): object { 48 | const { res } = this; 49 | return typeof res.getHeaders === 'function' 50 | ? res.getHeaders() 51 | : res._headers || {}; // Node < 7.7 52 | }, 53 | 54 | /** 55 | * Return response header, alias as response.header 56 | * 57 | * @return {Object} 58 | * @api public 59 | */ 60 | 61 | get headers(): object { 62 | return this.header; 63 | }, 64 | 65 | /** 66 | * Get response status code. 67 | * 68 | * @return {Number} 69 | * @api public 70 | */ 71 | 72 | get status(): number { 73 | return this.res.statusCode; 74 | }, 75 | 76 | /** 77 | * Set response status code. 78 | * 79 | * @param {Number} code 80 | * @api public 81 | */ 82 | 83 | set status(code: number) { 84 | if (this.headerSent) return; 85 | 86 | assert(Number.isInteger(code), 'status code must be a number'); 87 | assert(code >= 100 && code <= 999, `invalid status code: ${code}`); 88 | this._explicitStatus = true; 89 | this.res.statusCode = code; 90 | if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code]; 91 | if (this.body && statuses.empty[code]) this.body = null; 92 | }, 93 | 94 | /** 95 | * Get response status message 96 | * 97 | * @return {String} 98 | * @api public 99 | */ 100 | 101 | get message(): string { 102 | return this.res.statusMessage || statuses[this.status]; 103 | }, 104 | 105 | /** 106 | * Set response status message 107 | * 108 | * @param {String} msg 109 | * @api public 110 | */ 111 | 112 | set message(msg: string) { 113 | this.res.statusMessage = msg; 114 | }, 115 | 116 | /** 117 | * Get response body. 118 | * 119 | * @return {Mixed} 120 | * @api public 121 | */ 122 | 123 | get body(): any { 124 | return this._body; 125 | }, 126 | 127 | /** 128 | * Set response body. 129 | * 130 | * @param {String|Buffer|Object|Stream} val 131 | * @api public 132 | */ 133 | 134 | set body(val: any) { 135 | const original = this._body; 136 | this._body = val; 137 | 138 | // no content 139 | if (null == val) { 140 | if (!statuses.empty[this.status]) this.status = 204; 141 | this.remove('Content-Type'); 142 | this.remove('Content-Length'); 143 | this.remove('Transfer-Encoding'); 144 | return; 145 | } 146 | 147 | // set the status 148 | if (!this._explicitStatus) this.status = 200; 149 | 150 | // set the content-type only if not yet set 151 | const setType = !this.header['content-type']; 152 | 153 | // string 154 | if ('string' == typeof val) { 155 | if (setType) this.type = /^\s* this.ctx.onerror(err)); 171 | 172 | // overwriting 173 | if (null != original && original != val) this.remove('Content-Length'); 174 | 175 | if (setType) this.type = 'bin'; 176 | return; 177 | } 178 | 179 | // json 180 | this.remove('Content-Length'); 181 | this.type = 'json'; 182 | }, 183 | 184 | /** 185 | * Set Content-Length field to `n`. 186 | * 187 | * @param {Number} n 188 | * @api public 189 | */ 190 | 191 | set length(n: number) { 192 | this.set('Content-Length', n); 193 | }, 194 | 195 | /** 196 | * Return parsed response Content-Length when present. 197 | * 198 | * @return {Number} 199 | * @api public 200 | */ 201 | 202 | get length(): number { 203 | const len = this.header['content-length']; 204 | const body = this.body; 205 | 206 | if (null == len) { 207 | if (!body) return; 208 | if ('string' == typeof body) return Buffer.byteLength(body); 209 | if (Buffer.isBuffer(body)) return body.length; 210 | if (isJSON(body)) return Buffer.byteLength(JSON.stringify(body)); 211 | return; 212 | } 213 | 214 | return Math.trunc(len) || 0; 215 | }, 216 | 217 | /** 218 | * Check if a header has been written to the socket. 219 | * 220 | * @return {Boolean} 221 | * @api public 222 | */ 223 | 224 | get headerSent(): boolean { 225 | return this.res.headersSent; 226 | }, 227 | 228 | /** 229 | * Vary on `field`. 230 | * 231 | * @param {String} field 232 | * @api public 233 | */ 234 | 235 | vary(field: string): void { 236 | if (this.headerSent) return; 237 | 238 | vary(this.res, field); 239 | }, 240 | 241 | /** 242 | * Perform a 302 redirect to `url`. 243 | * 244 | * The string "back" is special-cased 245 | * to provide Referrer support, when Referrer 246 | * is not present `alt` or "/" is used. 247 | * 248 | * Examples: 249 | * 250 | * this.redirect('back'); 251 | * this.redirect('back', '/index.html'); 252 | * this.redirect('/login'); 253 | * this.redirect('http://google.com'); 254 | * 255 | * @param {String} url 256 | * @param {String} [alt] 257 | * @api public 258 | */ 259 | 260 | redirect(url: string, alt: string): void { 261 | // location 262 | if ('back' == url) url = this.ctx.get('Referrer') || alt || '/'; 263 | this.set('Location', url); 264 | 265 | // status 266 | if (!statuses.redirect[this.status]) this.status = 302; 267 | 268 | // html 269 | if (this.ctx.accepts('html')) { 270 | url = escape(url); 271 | this.type = 'text/html; charset=utf-8'; 272 | this.body = `Redirecting to ${url}.`; 273 | return; 274 | } 275 | 276 | // text 277 | this.type = 'text/plain; charset=utf-8'; 278 | this.body = `Redirecting to ${url}.`; 279 | }, 280 | 281 | /** 282 | * Set Content-Disposition header to "attachment" with optional `filename`. 283 | * 284 | * @param {String} filename 285 | * @api public 286 | */ 287 | 288 | attachment(filename: string, options: contentDisposition.Options): void { 289 | if (filename) this.type = extname(filename); 290 | this.set('Content-Disposition', contentDisposition(filename, options)); 291 | }, 292 | 293 | /** 294 | * Set Content-Type response header with `type` through `mime.lookup()` 295 | * when it does not contain a charset. 296 | * 297 | * Examples: 298 | * 299 | * this.type = '.html'; 300 | * this.type = 'html'; 301 | * this.type = 'json'; 302 | * this.type = 'application/json'; 303 | * this.type = 'png'; 304 | * 305 | * @param {String} type 306 | * @api public 307 | */ 308 | 309 | set type(type: string) { 310 | type = getType(type); 311 | if (type) { 312 | this.set('Content-Type', type); 313 | } else { 314 | this.remove('Content-Type'); 315 | } 316 | }, 317 | 318 | /** 319 | * Set the Last-Modified date using a string or a Date. 320 | * 321 | * this.response.lastModified = new Date(); 322 | * this.response.lastModified = '2013-09-13'; 323 | * 324 | * @param {String|Date} type 325 | * @api public 326 | */ 327 | 328 | set lastModified(val: string | Date) { 329 | if ('string' == typeof val) val = new Date(val); 330 | this.set('Last-Modified', val.toUTCString()); 331 | }, 332 | 333 | /** 334 | * Get the Last-Modified date in Date form, if it exists. 335 | * 336 | * @return {Date} 337 | * @api public 338 | */ 339 | 340 | get lastModified(): string | Date { 341 | const date = this.get('last-modified'); 342 | if (date) return new Date(date); 343 | }, 344 | 345 | /** 346 | * Set the ETag of a response. 347 | * This will normalize the quotes if necessary. 348 | * 349 | * this.response.etag = 'md5hashsum'; 350 | * this.response.etag = '"md5hashsum"'; 351 | * this.response.etag = 'W/"123456789"'; 352 | * 353 | * @param {String} etag 354 | * @api public 355 | */ 356 | 357 | set etag(val: string) { 358 | if (!/^(W\/)?"/.test(val)) val = `"${val}"`; 359 | this.set('ETag', val); 360 | }, 361 | 362 | /** 363 | * Get the ETag of a response. 364 | * 365 | * @return {String} 366 | * @api public 367 | */ 368 | 369 | get etag(): string { 370 | return this.get('ETag'); 371 | }, 372 | 373 | /** 374 | * Return the response mime type void of 375 | * parameters such as "charset". 376 | * 377 | * @return {String} 378 | * @api public 379 | */ 380 | 381 | get type(): string { 382 | const type = this.get('Content-Type'); 383 | if (!type) return ''; 384 | return type.split(';', 1)[0]; 385 | }, 386 | 387 | /** 388 | * Check whether the response is one of the listed types. 389 | * Pretty much the same as `this.request.is()`. 390 | * 391 | * @param {String|Array} types... 392 | * @return {String|false} 393 | * @api public 394 | */ 395 | 396 | is(types: any): string | false { 397 | const type = this.type; 398 | if (!types) return type || false; 399 | if (!Array.isArray(types)) types = [].slice.call(arguments); 400 | return typeis(type, types); 401 | }, 402 | 403 | /** 404 | * Return response header. 405 | * 406 | * Examples: 407 | * 408 | * this.get('Content-Type'); 409 | * // => "text/plain" 410 | * 411 | * this.get('content-type'); 412 | * // => "text/plain" 413 | * 414 | * @param {String} field 415 | * @return {String} 416 | * @api public 417 | */ 418 | 419 | get(field: { toLowerCase: () => string | number; }): string { 420 | return this.header[field.toLowerCase()] || ''; 421 | }, 422 | 423 | /** 424 | * Set header `field` to `val`, or pass 425 | * an object of header fields. 426 | * 427 | * Examples: 428 | * 429 | * this.set('Foo', ['bar', 'baz']); 430 | * this.set('Accept', 'application/json'); 431 | * this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); 432 | * 433 | * @param {String|Object|Array} field 434 | * @param {String} val 435 | * @api public 436 | */ 437 | 438 | set(field: any, val: string | Array): void { 439 | if (this.headerSent) return; 440 | 441 | if (2 == arguments.length) { 442 | if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v)); 443 | else if (typeof val !== 'string') val = String(val); 444 | this.res.setHeader(field, val); 445 | } else { 446 | for (const key in field) { 447 | this.set(key, field[key]); 448 | } 449 | } 450 | }, 451 | 452 | /** 453 | * Append additional header `field` with value `val`. 454 | * 455 | * Examples: 456 | * 457 | * ``` 458 | * this.append('Link', ['', '']); 459 | * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); 460 | * this.append('Warning', '199 Miscellaneous warning'); 461 | * ``` 462 | * 463 | * @param {String} field 464 | * @param {String|Array} val 465 | * @api public 466 | */ 467 | 468 | append(field: string, val: string | Array) { 469 | const prev = this.get(field); 470 | 471 | if (prev) { 472 | val = Array.isArray(prev) 473 | ? prev.concat(val) 474 | : [prev].concat(val); 475 | } 476 | 477 | return this.set(field, val); 478 | }, 479 | 480 | /** 481 | * Remove header `field`. 482 | * 483 | * @param {String} name 484 | * @api public 485 | */ 486 | 487 | remove(field: any) { 488 | if (this.headerSent) return; 489 | 490 | this.res.removeHeader(field); 491 | }, 492 | 493 | /** 494 | * Checks if the request is writable. 495 | * Tests for the existence of the socket 496 | * as node sometimes does not set it. 497 | * 498 | * @return {Boolean} 499 | * @api private 500 | */ 501 | 502 | get writable(): boolean { 503 | // can't write any more after response finished 504 | if (this.res.finished) return false; 505 | 506 | const socket = this.res.socket; 507 | // There are already pending outgoing res, but still writable 508 | // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486 509 | if (!socket) return true; 510 | return socket.writable; 511 | }, 512 | 513 | /** 514 | * Inspect implementation. 515 | * 516 | * @return {Object} 517 | * @api public 518 | */ 519 | 520 | inspect(): object { 521 | if (!this.res) return; 522 | const o = this.toJSON(); 523 | o.body = this.body; 524 | return o; 525 | }, 526 | 527 | /** 528 | * Return JSON representation. 529 | * 530 | * @return {Object} 531 | * @api public 532 | */ 533 | 534 | toJSON(): object { 535 | return only(this, [ 536 | 'status', 537 | 'message', 538 | 'header' 539 | ]); 540 | }, 541 | 542 | /** 543 | * Flush any set headers, and begin the body 544 | */ 545 | flushHeaders() { 546 | this.res.flushHeaders(); 547 | } 548 | }; 549 | 550 | /** 551 | * Custom inspection implementation for newer Node.js versions. 552 | * 553 | * @return {Object} 554 | * @api public 555 | */ 556 | if (util.inspect.custom) { 557 | exports[util.inspect.custom] = exports.inspect; 558 | } 559 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tkoa", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/accepts": { 8 | "version": "1.3.5", 9 | "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", 10 | "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", 11 | "requires": { 12 | "@types/node": "*" 13 | } 14 | }, 15 | "@types/body-parser": { 16 | "version": "1.17.0", 17 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", 18 | "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", 19 | "requires": { 20 | "@types/connect": "*", 21 | "@types/node": "*" 22 | } 23 | }, 24 | "@types/connect": { 25 | "version": "3.4.32", 26 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", 27 | "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", 28 | "requires": { 29 | "@types/node": "*" 30 | } 31 | }, 32 | "@types/content-disposition": { 33 | "version": "0.5.2", 34 | "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.2.tgz", 35 | "integrity": "sha1-GMyw/RJoCOSYqCDLgUnAByorkGY=" 36 | }, 37 | "@types/content-type": { 38 | "version": "1.1.3", 39 | "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.3.tgz", 40 | "integrity": "sha512-pv8VcFrZ3fN93L4rTNIbbUzdkzjEyVMp5mPVjsFfOYTDOZMZiZ8P1dhu+kEv3faYyKzZgLlSvnyQNFg+p/v5ug==" 41 | }, 42 | "@types/cookies": { 43 | "version": "0.7.1", 44 | "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.1.tgz", 45 | "integrity": "sha512-ku6IvbucEyuC6i4zAVK/KnuzWNXdbFd1HkXlNLg/zhWDGTtQT5VhumiPruB/BHW34PWVFwyfwGftDQHfWNxu3Q==", 46 | "requires": { 47 | "@types/connect": "*", 48 | "@types/express": "*", 49 | "@types/keygrip": "*", 50 | "@types/node": "*" 51 | } 52 | }, 53 | "@types/destroy": { 54 | "version": "1.0.0", 55 | "resolved": "https://registry.npmjs.org/@types/destroy/-/destroy-1.0.0.tgz", 56 | "integrity": "sha512-nE3ePJLWPRu/qFHN8mj3fWnkr9K9ezwoiG4yOis2DuLeAawlnOOT/pM29JQkityrwfEvkblU5O9iS1bsiMqtDw==", 57 | "requires": { 58 | "@types/node": "*" 59 | } 60 | }, 61 | "@types/escape-html": { 62 | "version": "0.0.20", 63 | "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-0.0.20.tgz", 64 | "integrity": "sha512-6dhZJLbA7aOwkYB2GDGdIqJ20wmHnkDzaxV9PJXe7O02I2dSFTERzRB6JrX6cWKaS+VqhhY7cQUMCbO5kloFUw==" 65 | }, 66 | "@types/express": { 67 | "version": "4.16.1", 68 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.1.tgz", 69 | "integrity": "sha512-V0clmJow23WeyblmACoxbHBu2JKlE5TiIme6Lem14FnPW9gsttyHtk6wq7njcdIWH1njAaFgR8gW09lgY98gQg==", 70 | "requires": { 71 | "@types/body-parser": "*", 72 | "@types/express-serve-static-core": "*", 73 | "@types/serve-static": "*" 74 | } 75 | }, 76 | "@types/express-serve-static-core": { 77 | "version": "4.16.2", 78 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.2.tgz", 79 | "integrity": "sha512-qgc8tjnDrc789rAQed8NoiFLV5VGcItA4yWNFphqGU0RcuuQngD00g3LHhWIK3HQ2XeDgVCmlNPDlqi3fWBHnQ==", 80 | "requires": { 81 | "@types/node": "*", 82 | "@types/range-parser": "*" 83 | } 84 | }, 85 | "@types/fresh": { 86 | "version": "0.5.0", 87 | "resolved": "https://registry.npmjs.org/@types/fresh/-/fresh-0.5.0.tgz", 88 | "integrity": "sha512-eGPzuyc6wZM3sSHJdF7NM2jW6B/xsB014Rqg/iDa6xY02mlfy1w/TE2sYhR8vbHxkzJOXiGo6NuIk3xk35vsgQ==" 89 | }, 90 | "@types/http-assert": { 91 | "version": "1.4.0", 92 | "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.4.0.tgz", 93 | "integrity": "sha512-TZDqvFW4nQwL9DVSNJIJu4lPLttKgzRF58COa7Vs42Ki/MrhIqUbeIw0MWn4kGLiZLXB7oCBibm7nkSjPkzfKQ==" 94 | }, 95 | "@types/http-errors": { 96 | "version": "1.6.1", 97 | "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.6.1.tgz", 98 | "integrity": "sha512-s+RHKSGc3r0m3YEE2UXomJYrpQaY9cDmNDLU2XvG1/LAZsQ7y8emYkTLfcw/ByDtcsTyRQKwr76Bj4PkN2hfWg==" 99 | }, 100 | "@types/keygrip": { 101 | "version": "1.0.1", 102 | "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.1.tgz", 103 | "integrity": "sha1-/1QEYtL7TQqIRBzq8n0oewHD2Hg=" 104 | }, 105 | "@types/koa": { 106 | "version": "2.0.48", 107 | "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.0.48.tgz", 108 | "integrity": "sha512-CiIUYhHlOFJhSCTmsFoFkV2t9ij1JwW26nt0W9XZoWTvmAw6zTE0+k3IAoGICtjzIfhZpZcO323NHmI1LGmdDw==", 109 | "requires": { 110 | "@types/accepts": "*", 111 | "@types/cookies": "*", 112 | "@types/http-assert": "*", 113 | "@types/keygrip": "*", 114 | "@types/koa-compose": "*", 115 | "@types/node": "*" 116 | } 117 | }, 118 | "@types/koa-compose": { 119 | "version": "3.2.3", 120 | "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.3.tgz", 121 | "integrity": "sha512-kXvR0DPyZ3gaFxZs4WycA8lpzlPGtFmwdbgce+NWd+TG3PycPO3o5FkePH60HoBPd8BBaSiw3vhtgM42O2kQcg==", 122 | "requires": { 123 | "@types/koa": "*" 124 | } 125 | }, 126 | "@types/koa-convert": { 127 | "version": "1.2.2", 128 | "resolved": "https://registry.npmjs.org/@types/koa-convert/-/koa-convert-1.2.2.tgz", 129 | "integrity": "sha512-RhoEpAMFfj6CanUHwJBbMGaZ2y1WtD08n2NhLscYa/u8XSz0Mfuhg6M0Xm2HRPohAswEdgPngAxVe8b+04c7cQ==", 130 | "requires": { 131 | "@types/koa": "*" 132 | } 133 | }, 134 | "@types/mime": { 135 | "version": "2.0.1", 136 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", 137 | "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" 138 | }, 139 | "@types/node": { 140 | "version": "11.13.4", 141 | "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.4.tgz", 142 | "integrity": "sha512-+rabAZZ3Yn7tF/XPGHupKIL5EcAbrLxnTr/hgQICxbeuAfWtT0UZSfULE+ndusckBItcv4o6ZeOJplQikVcLvQ==" 143 | }, 144 | "@types/on-finished": { 145 | "version": "2.3.1", 146 | "resolved": "https://registry.npmjs.org/@types/on-finished/-/on-finished-2.3.1.tgz", 147 | "integrity": "sha512-mzVYaYcFs5Jd2n/O6uYIRUsFRR1cHyZLRvkLCU0E7+G5WhY0qBDAR5fUCeZbvecYOSh9ikhlesyi2UfI8B9ckQ==", 148 | "requires": { 149 | "@types/node": "*" 150 | } 151 | }, 152 | "@types/parseurl": { 153 | "version": "1.3.1", 154 | "resolved": "https://registry.npmjs.org/@types/parseurl/-/parseurl-1.3.1.tgz", 155 | "integrity": "sha1-48sRAhYOSO+ln0l8TsIt7k87Wyc=", 156 | "requires": { 157 | "@types/node": "*" 158 | } 159 | }, 160 | "@types/range-parser": { 161 | "version": "1.2.3", 162 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", 163 | "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" 164 | }, 165 | "@types/serve-static": { 166 | "version": "1.13.2", 167 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", 168 | "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", 169 | "requires": { 170 | "@types/express-serve-static-core": "*", 171 | "@types/mime": "*" 172 | } 173 | }, 174 | "@types/statuses": { 175 | "version": "1.5.0", 176 | "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-1.5.0.tgz", 177 | "integrity": "sha512-4zJN5gJH+Km6hA36z8MnOKas6EU0qwxItTXNijYDPuZUsSk4EpIAB56fwnxZIhi3tHx42J7wqNdQTqt49Ar9FQ==" 178 | }, 179 | "@types/type-is": { 180 | "version": "1.6.2", 181 | "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.2.tgz", 182 | "integrity": "sha512-q8d51ZdF/D8xebrtNDsZH+4XBUFdz8xEgWhE4U4F4WWmcBZ8+i/r/qs9DmjAprYh5qQTYlY4BxaVKDrWIwNQ9w==", 183 | "requires": { 184 | "@types/node": "*" 185 | } 186 | }, 187 | "@types/vary": { 188 | "version": "1.1.0", 189 | "resolved": "https://registry.npmjs.org/@types/vary/-/vary-1.1.0.tgz", 190 | "integrity": "sha512-LQWqrIa0dvEOOH37lGksMEXbypRLUFqu6Gx0pmX7zIUisD2I/qaVgEX/vJ/PSVSW0Hk6yz1BNkFpqg6dZm3Wug==", 191 | "requires": { 192 | "@types/node": "*" 193 | } 194 | }, 195 | "accepts": { 196 | "version": "1.3.5", 197 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 198 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 199 | "requires": { 200 | "mime-types": "~2.1.18", 201 | "negotiator": "0.6.1" 202 | } 203 | }, 204 | "any-promise": { 205 | "version": "1.3.0", 206 | "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", 207 | "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" 208 | }, 209 | "cache-content-type": { 210 | "version": "1.0.1", 211 | "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", 212 | "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", 213 | "requires": { 214 | "mime-types": "^2.1.18", 215 | "ylru": "^1.2.0" 216 | } 217 | }, 218 | "co": { 219 | "version": "4.6.0", 220 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 221 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" 222 | }, 223 | "content-disposition": { 224 | "version": "0.5.3", 225 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 226 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 227 | "requires": { 228 | "safe-buffer": "5.1.2" 229 | } 230 | }, 231 | "content-type": { 232 | "version": "1.0.4", 233 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 234 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 235 | }, 236 | "cookies": { 237 | "version": "0.7.3", 238 | "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.3.tgz", 239 | "integrity": "sha512-+gixgxYSgQLTaTIilDHAdlNPZDENDQernEMiIcZpYYP14zgHsCt4Ce1FEjFtcp6GefhozebB6orvhAAWx/IS0A==", 240 | "requires": { 241 | "depd": "~1.1.2", 242 | "keygrip": "~1.0.3" 243 | }, 244 | "dependencies": { 245 | "depd": { 246 | "version": "1.1.2", 247 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 248 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 249 | } 250 | } 251 | }, 252 | "debug": { 253 | "version": "4.1.1", 254 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 255 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 256 | "requires": { 257 | "ms": "^2.1.1" 258 | } 259 | }, 260 | "deep-equal": { 261 | "version": "1.0.1", 262 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 263 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" 264 | }, 265 | "delegates": { 266 | "version": "1.0.0", 267 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 268 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 269 | }, 270 | "destroy": { 271 | "version": "1.0.4", 272 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 273 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 274 | }, 275 | "ee-first": { 276 | "version": "1.1.1", 277 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 278 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 279 | }, 280 | "error-inject": { 281 | "version": "1.0.0", 282 | "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", 283 | "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" 284 | }, 285 | "escape-html": { 286 | "version": "1.0.3", 287 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 288 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 289 | }, 290 | "fresh": { 291 | "version": "0.5.2", 292 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 293 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 294 | }, 295 | "http-assert": { 296 | "version": "1.4.0", 297 | "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.0.tgz", 298 | "integrity": "sha512-tPVv62a6l3BbQoM/N5qo969l0OFxqpnQzNUPeYfTP6Spo4zkgWeDBD1D5thI7sDLg7jCCihXTLB0X8UtdyAy8A==", 299 | "requires": { 300 | "deep-equal": "~1.0.1", 301 | "http-errors": "~1.7.1" 302 | } 303 | }, 304 | "http-errors": { 305 | "version": "1.7.2", 306 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 307 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 308 | "requires": { 309 | "depd": "~1.1.2", 310 | "inherits": "2.0.3", 311 | "setprototypeof": "1.1.1", 312 | "statuses": ">= 1.5.0 < 2", 313 | "toidentifier": "1.0.0" 314 | }, 315 | "dependencies": { 316 | "depd": { 317 | "version": "1.1.2", 318 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 319 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 320 | } 321 | } 322 | }, 323 | "inherits": { 324 | "version": "2.0.3", 325 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 326 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 327 | }, 328 | "keygrip": { 329 | "version": "1.0.3", 330 | "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.3.tgz", 331 | "integrity": "sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g==" 332 | }, 333 | "koa-compose": { 334 | "version": "4.1.0", 335 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", 336 | "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==" 337 | }, 338 | "koa-convert": { 339 | "version": "1.2.0", 340 | "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", 341 | "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", 342 | "requires": { 343 | "co": "^4.6.0", 344 | "koa-compose": "^3.0.0" 345 | }, 346 | "dependencies": { 347 | "koa-compose": { 348 | "version": "3.2.1", 349 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", 350 | "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", 351 | "requires": { 352 | "any-promise": "^1.1.0" 353 | } 354 | } 355 | } 356 | }, 357 | "koa-is-json": { 358 | "version": "1.0.0", 359 | "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", 360 | "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" 361 | }, 362 | "media-typer": { 363 | "version": "0.3.0", 364 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 365 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 366 | }, 367 | "mime-db": { 368 | "version": "1.38.0", 369 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", 370 | "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" 371 | }, 372 | "mime-types": { 373 | "version": "2.1.22", 374 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", 375 | "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", 376 | "requires": { 377 | "mime-db": "~1.38.0" 378 | } 379 | }, 380 | "ms": { 381 | "version": "2.1.1", 382 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 383 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 384 | }, 385 | "negotiator": { 386 | "version": "0.6.1", 387 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 388 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 389 | }, 390 | "on-finished": { 391 | "version": "2.3.0", 392 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 393 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 394 | "requires": { 395 | "ee-first": "1.1.1" 396 | } 397 | }, 398 | "only": { 399 | "version": "0.0.2", 400 | "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", 401 | "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" 402 | }, 403 | "parseurl": { 404 | "version": "1.3.2", 405 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 406 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 407 | }, 408 | "safe-buffer": { 409 | "version": "5.1.2", 410 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 411 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 412 | }, 413 | "setprototypeof": { 414 | "version": "1.1.1", 415 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 416 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 417 | }, 418 | "statuses": { 419 | "version": "1.5.0", 420 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 421 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 422 | }, 423 | "toidentifier": { 424 | "version": "1.0.0", 425 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 426 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 427 | }, 428 | "type-is": { 429 | "version": "1.6.16", 430 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 431 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 432 | "requires": { 433 | "media-typer": "0.3.0", 434 | "mime-types": "~2.1.18" 435 | } 436 | }, 437 | "vary": { 438 | "version": "1.1.2", 439 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 440 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 441 | }, 442 | "ylru": { 443 | "version": "1.2.1", 444 | "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", 445 | "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==" 446 | } 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tkoa", 3 | "version": "1.0.1", 4 | "description": "Koa web app framework written in typescript.", 5 | "main": "./lib/application.js", 6 | "types": "./lib/application.ts", 7 | "scripts": { 8 | "test": "ava test.js" 9 | }, 10 | "keywords": [ 11 | "koa", 12 | "web", 13 | "app", 14 | "http", 15 | "application", 16 | "framework", 17 | "middleware", 18 | "typescript", 19 | "rack" 20 | ], 21 | "author": "All Contributor", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@types/content-disposition": "^0.5.2", 25 | "@types/content-type": "^1.1.3", 26 | "@types/destroy": "^1.0.0", 27 | "@types/escape-html": "0.0.20", 28 | "@types/fresh": "^0.5.0", 29 | "@types/http-errors": "^1.6.1", 30 | "@types/koa-convert": "^1.2.2", 31 | "@types/node": "^11.13.4", 32 | "@types/on-finished": "^2.3.1", 33 | "@types/parseurl": "^1.3.1", 34 | "@types/statuses": "^1.5.0", 35 | "@types/type-is": "^1.6.2", 36 | "@types/vary": "^1.1.0", 37 | "accepts": "^1.3.5", 38 | "cache-content-type": "^1.0.1", 39 | "content-disposition": "^0.5.3", 40 | "content-type": "^1.0.4", 41 | "cookies": "^0.7.3", 42 | "debug": "^4.1.1", 43 | "delegates": "^1.0.0", 44 | "destroy": "^1.0.4", 45 | "error-inject": "^1.0.0", 46 | "escape-html": "^1.0.3", 47 | "fresh": "^0.5.2", 48 | "http-assert": "^1.4.0", 49 | "http-errors": "^1.7.2", 50 | "koa-compose": "^4.1.0", 51 | "koa-convert": "^1.2.0", 52 | "koa-is-json": "^1.0.0", 53 | "on-finished": "^2.3.0", 54 | "only": "0.0.2", 55 | "parseurl": "^1.3.2", 56 | "statuses": "^1.5.0", 57 | "type-is": "^1.6.16", 58 | "typescript": "^3.4.3", 59 | "vary": "^1.1.2" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /source/logo small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkoajs/tkoa/09472fc2c8a414cae6a8195cfb25e433dd5eba3e/source/logo small.png -------------------------------------------------------------------------------- /source/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkoajs/tkoa/09472fc2c8a414cae6a8195cfb25e433dd5eba3e/source/logo.png -------------------------------------------------------------------------------- /source/tkoa logo square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkoajs/tkoa/09472fc2c8a414cae6a8195cfb25e433dd5eba3e/source/tkoa logo square.png -------------------------------------------------------------------------------- /source/ts logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkoajs/tkoa/09472fc2c8a414cae6a8195cfb25e433dd5eba3e/source/ts logo.png -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | let tKoa = require('./lib/application.js'); 4 | 5 | function newServer() { 6 | try { 7 | let app = new tKoa(); 8 | } catch (e) { 9 | return 'fail'; 10 | } 11 | return 'pass'; 12 | } 13 | 14 | test('newServer testing', t => { 15 | t.is(newServer(), 'pass'); 16 | }); 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib", 4 | "allowJs": true, 5 | "target": "es6", 6 | "noEmitOnError": true, 7 | "module": "commonjs" 8 | }, 9 | "include": [ 10 | "./lib/**/*" 11 | ], 12 | "exclude": [ 13 | "node_modules" 14 | ] 15 | } 16 | --------------------------------------------------------------------------------