├── .dockerignore ├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .prettierrc.js ├── .yarnrc ├── LICENSE ├── README.md ├── examples ├── README.md ├── adal-node │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── auth.ts │ │ ├── config.ts │ │ ├── index.ts │ │ └── types.ts │ ├── tsconfig.json │ └── yarn.lock ├── errors │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── errors.ts │ │ └── index.ts │ ├── tsconfig.json │ └── yarn.lock ├── external-api-call │ ├── README.md │ ├── index.js │ ├── package.json │ └── yarn.lock ├── json-body-parsing │ ├── README.md │ ├── index.js │ ├── package.json │ └── yarn.lock ├── router │ ├── .gitignore │ ├── node_modules │ │ └── .bin │ │ │ ├── tsc │ │ │ └── tsserver │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── yarn.lock ├── socket.io-chat-app │ ├── README.md │ ├── index.html │ ├── index.js │ ├── package.json │ ├── websocket-server.js │ └── yarn.lock ├── urlencoded-body-parsing │ ├── README.md │ ├── index.js │ ├── package.json │ └── yarn.lock ├── with-graphql-request │ ├── README.md │ ├── index.js │ ├── package.json │ └── yarn.lock ├── with-http2 │ ├── README.md │ ├── index.js │ ├── package.json │ └── yarn.lock ├── with-https │ ├── README.md │ ├── cert.pem │ ├── index.js │ ├── key.pem │ ├── package.json │ └── yarn.lock ├── with-https2 │ ├── README.md │ ├── cert.pem │ ├── index.js │ ├── key.pem │ ├── package.json │ └── yarn.lock └── with-worker │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ ├── index.ts │ ├── opts.ts │ ├── prime.ts │ └── stream.ts │ ├── tsconfig.json │ └── yarn.lock ├── lib └── worker-wrapper.js ├── package.json ├── src ├── body.ts ├── errors.ts ├── index.ts ├── router.ts ├── serve.ts ├── types.ts └── with-worker.ts ├── test ├── basic.test.ts ├── development.test.ts └── router.test.ts ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | tab_width = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [{*.json,*.json.example,*.gyp,*.yml,*.yaml}] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [{*.py,*.asm}] 17 | indent_style = space 18 | 19 | [*.py] 20 | indent_size = 4 21 | 22 | [*.asm] 23 | indent_size = 8 24 | 25 | [*.md] 26 | trim_trailing_whitespace = false 27 | 28 | # Ideal settings - some plugins might support these. 29 | [*.js] 30 | quote_type = single 31 | 32 | [{*.c,*.cc,*.h,*.hh,*.cpp,*.hpp,*.m,*.mm,*.mpp,*.js,*.java,*.go,*.rs,*.php,*.ng,*.jsx,*.ts,*.d,*.cs,*.swift}] 33 | curly_bracket_next_line = false 34 | spaces_around_operators = true 35 | spaces_around_brackets = outside 36 | # close enough to 1TB 37 | indent_brace_style = K&R 38 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', 5 | 'prettier', 6 | 'plugin:prettier/recommended', 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2018, 10 | sourceType: 'module', 11 | }, 12 | rules: { 13 | "@typescript-eslint/no-explicit-any": "off" 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [12.x, 14.x, 16.x, 18.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm install 23 | npm run build --if-present 24 | npm test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | package-lock.json 4 | 5 | # logs 6 | npm-debug.log 7 | yarn-error.log 8 | 9 | # coverage 10 | coverage 11 | .nyc_output 12 | 13 | # build results 14 | /dist 15 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | semi: true, 4 | singleQuote: true, 5 | tabWidth: 4, 6 | useTabs: true, 7 | }; 8 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | save-prefix "" 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Olli Vanhoja 4 | Copyright (c) 2018 ZEIT, Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | _**Micri** — Asynchronous HTTP microservices_ 2 | 3 | > micri is an archaic non-SI decimal metric prefix for 10−14. Its symbol was mc. 4 | 5 | [Wikipedia - Micri-](https://en.wikipedia.org/wiki/Micri-) 6 | 7 | [![npm version](https://badge.fury.io/js/micri.svg)](https://badge.fury.io/js/micri) 8 | [![Install Size](https://packagephobia.now.sh/badge?p=micri)](https://packagephobia.now.sh/result?p=micri) 9 | 10 | ## Features 11 | 12 | * **Easy**: Designed for usage with `async` and `await` ([more](https://zeit.co/blog/async-and-await)) 13 | * **Fast**: Ultra-high performance (even JSON parsing is opt-in) 14 | * **Micri**: The whole project is ~500 lines of code 15 | * **Agile**: Super easy deployment and containerization 16 | * **Simple**: Oriented for single purpose modules (function) 17 | * **Standard**: Just HTTP! 18 | * **Explicit**: No middleware - modules declare all [dependencies](https://github.com/amio/awesome-micro) 19 | * **Lightweight**: [![Install Size](https://packagephobia.now.sh/badge?p=micri)](https://packagephobia.now.sh/result?p=micri) 20 | 21 | 22 | ## Usage 23 | 24 | ```js 25 | const { serve } = require('micri') 26 | 27 | const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); 28 | 29 | const server = serve(async (req, res) => { 30 | await sleep(500) 31 | return 'Hello world' 32 | }) 33 | 34 | server.listen(3000) 35 | ``` 36 | 37 | And go to this URL: `http://localhost:3000` - 🎉 38 | 39 | ### `async` & `await` 40 | 41 |

42 | Examples 43 | 44 |

45 | 46 | Micri is built for usage with async/await. You can read more about async / await [here](https://zeit.co/blog/async-and-await) 47 | 48 | ```js 49 | const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); 50 | 51 | module.exports = async (req, res) => { 52 | await sleep(500); 53 | return 'Ready!'; 54 | } 55 | ``` 56 | 57 | ### Body parsing 58 | 59 |

60 | Examples 61 | 65 |

66 | 67 | For parsing the incoming request body we included an async functions `buffer`, `text` and `json` 68 | 69 | ```js 70 | const {buffer, text, json} = require('micri') 71 | 72 | module.exports = async (req, res) => { 73 | const buf = await buffer(req) 74 | console.log(buf) 75 | // 76 | const txt = await text(req) 77 | console.log(txt) 78 | // '{"price": 9.99}' 79 | const js = await json(req) 80 | console.log(js.price) 81 | // 9.99 82 | return '' 83 | } 84 | ``` 85 | 86 | ### Routing 87 | 88 | Micri has a simple built-in function router. The idea is fairly simple, you can 89 | use it as a wrapper virtually anywhere where it will be called with 90 | `(req, res, optionalArgs)` and can return a promise as a response to `micri()`. 91 | 92 | Firstly you create a router by calling the `router(...)` function. The router 93 | function takes routes as arguments. Routes are created by calling functions 94 | under `on` map, and the functions are organized there by HTTP method name. These 95 | functions in turn take two arguments, a predicate and request handler functions. 96 | 97 | A predicate function gets the usual arguments `(req, res, opts?)`. A predicate 98 | function may return a truthy value if the handler function should take care of 99 | this request, or it may return a falsy value if the handler should not take 100 | this request. 101 | 102 | Multiple predicates can be combined by using `Router.everyPredicate(...)` that 103 | takes predicate functions as arguments. The function returns true if every 104 | predicate function given as an argument returns true. 105 | 106 | The order of the route arguments marks the priority order of the routes. 107 | Therefore if two routes would match to a request the one that was passed earlier 108 | in the arguments list to the `router()` function will handle the request. 109 | 110 | `otherwise()` is a special route function that will always match and thus can be 111 | used as the last route rule for sending an error and avoid throwing an exception 112 | in case no other route predicate matches. 113 | 114 | ```js 115 | const { Router: { router } } = require('micri'); 116 | 117 | micri(router( 118 | on.get((req) => req.url === '/', (req, _res) => ({ message: 'Hello world!'})), 119 | on.post((req) => req.url === '/', (req) => text(req)), 120 | otherwise((req, res) => send(res, 400, 'Method Not Accepted')))) 121 | .listen(3000); 122 | ``` 123 | 124 | 125 | ### Worker Threads 126 | 127 | Micri supports offloading computationally heavy request handlers to worker 128 | threads seamlessly. The offloading is configured per handler by wrapping the 129 | handler function with `withWorker()`. It works directly at the top-level or per 130 | route when using the router. See [with-workerthreads](examples/with-worker) for 131 | a couple of examples how to use it. 132 | 133 | ```js 134 | micri(withWorker(() => doSomethingCPUHeavy)) 135 | ``` 136 | 137 | Offloading requests to a worker may improve the responsiveness of a busy API 138 | significantly, as it removes almost all blocking from the main thread. In the 139 | following examples we first try to find prime numbers and finally return one 140 | as a response. In both cases we do two concurrent HTTP `GET` requests using 141 | `curl`. 142 | 143 | Finding prime numbers using the main thread: 144 | 145 | ``` 146 | ~% time curl 127.0.0.1:3000/main 147 | 299993curl 127.0.0.1:3000/main 0.01s user 0.00s system 0% cpu 8.791 total 148 | ~% time curl 127.0.0.1:3000/main 149 | 299993curl 127.0.0.1:3000/main 0.00s user 0.00s system 0% cpu 16.547 total 150 | ``` 151 | 152 | Notice that the second curl needs to wait until the first request finishes. 153 | 154 | Finding prime numbers using a worker thread: 155 | 156 | ``` 157 | ~% time curl 127.0.0.1:3000/worker 158 | 299993curl 127.0.0.1:3000/worker 0.00s user 0.00s system 0% cpu 9.025 total 159 | ~% time curl 127.0.0.1:3000/worker 160 | 299993curl 127.0.0.1:3000/worker 0.00s user 0.00s system 0% cpu 9.026 total 161 | ``` 162 | 163 | Note how both concurrently executed requests took the same time to finish. 164 | 165 | 166 | ## API 167 | 168 | ##### `buffer(req, { limit = '1mb', encoding = 'utf8' })` 169 | ##### `text(req, { limit = '1mb', encoding = 'utf8' })` 170 | ##### `json(req, { limit = '1mb', encoding = 'utf8' })` 171 | 172 | - Buffers and parses the incoming body and returns it. 173 | - Exposes an `async` function that can be run with `await`. 174 | - Can be called multiple times, as it caches the raw request body the first time. 175 | - `limit` is how much data is aggregated before parsing at max. Otherwise, an `Error` is thrown with `statusCode` set to `413` (see [Error Handling](#error-handling)). It can be a `Number` of bytes or [a string](https://www.npmjs.com/package/bytes) like `'1mb'`. 176 | - If JSON parsing fails, an `Error` is thrown with `statusCode` set to `400` (see [Error Handling](#error-handling)) 177 | 178 | For other types of data check the [examples](#body-parsing-examples) 179 | 180 | ### Sending a different status code 181 | 182 | So far we have used `return` to send data to the client. `return 'Hello World'` is the equivalent of `send(res, 200, 'Hello World')`. 183 | 184 | ```js 185 | const {send} = require('micri') 186 | 187 | module.exports = async (req, res) => { 188 | const statusCode = 400 189 | const data = { error: 'Custom error message' } 190 | 191 | send(res, statusCode, data) 192 | } 193 | ``` 194 | 195 | ##### `send(res, statusCode, data = null)` 196 | 197 | - Use `require('micri').send`. 198 | - `statusCode` is a `Number` with the HTTP status code, and must always be supplied. 199 | - If `data` is supplied it is sent in the response. Different input types are processed appropriately, and `Content-Type` and `Content-Length` are automatically set. 200 | - `Stream`: `data` is piped as an `octet-stream`. Note: it is _your_ responsibility to handle the `error` event in this case (usually, simply logging the error and aborting the response is enough). 201 | - `Buffer`: `data` is written as an `octet-stream`. 202 | - `object`: `data` is serialized as JSON. 203 | - `string`: `data` is written as-is. 204 | - If JSON serialization fails (for example, if a cyclical reference is found), a `400` error is thrown. See [Error Handling](#error-handling). 205 | 206 | ##### micri(fn) 207 | 208 | - This function is exposed as the `default` export. 209 | - Use `require('micri')`. 210 | - Returns a [`http.Server`](https://nodejs.org/dist/latest-v6.x/docs/api/http.html#http_class_http_server) that uses the provided `function` as the request handler. 211 | - The supplied function is run with `await`. So it can be `async` 212 | 213 | ##### sendError(req, res, error) 214 | 215 | - Use `require('micri').sendError`. 216 | - Used as the default handler for errors thrown. 217 | - Automatically sets the status code of the response based on `error.statusCode`. 218 | - Sends the `error.message` as the body. 219 | - Stacks are printed out with `console.error` and during development (when `NODE_ENV` is set to `'development'`) also sent in responses. 220 | - Usually, you don't need to invoke this method yourself, as you can use the [built-in error handling](#error-handling) flow with `throw`. 221 | 222 | ## Error Handling 223 | 224 | Micri allows you to write robust microservices. This is accomplished primarily 225 | by bringing sanity back to error handling and avoiding callback soup. 226 | 227 | If an error is thrown and not caught by you, the response will automatically be 228 | `500`. **Important:** Error stacks will be printed as `console.error` and during 229 | development mode (if the env variable `NODE_ENV` is `'development'`), they will 230 | also be included in the responses. 231 | 232 | If the error object throw is an instance of `MicriError` the `message`, 233 | `statusCode` and `code` properties of the object are used for the HTTP response. 234 | 235 | Let's say you want to write a rate limiting module: 236 | 237 | ```js 238 | const rateLimit = require('my-rate-limit') 239 | 240 | micri((req, res) => { 241 | await rateLimit(req); 242 | // ... your code 243 | }).listen(3000); 244 | ``` 245 | 246 | If the API endpoint is abused, it can throw a `MicriError` like so: 247 | 248 | ```js 249 | if (tooMany) { 250 | throw MicriError(429, 'rate_limited' 'Rate limit exceeded'); 251 | } 252 | ``` 253 | 254 | The nice thing about this model is that the `statusCode` is merely a suggestion. 255 | The user can override it: 256 | 257 | ```js 258 | try { 259 | await rateLimit(req) 260 | } catch (err) { 261 | if (429 == err.statusCode) { 262 | // perhaps send 500 instead? 263 | send(res, 500); 264 | } 265 | } 266 | ``` 267 | 268 | If the error is based on another error that **Micri** caught, like a `JSON.parse` 269 | exception, then `originalError` will point to it. If a generic error is caught, 270 | the status will be set to `500`. 271 | 272 | In order to set up your own error handling mechanism, you can use composition in 273 | your handler: 274 | 275 | ```js 276 | const {send} = require('micri'); 277 | 278 | const handleErrors = fn => async (req, res) => { 279 | try { 280 | return await fn(req, res) 281 | } catch (err) { 282 | console.log(err.stack) 283 | send(res, 500, 'My custom error!') 284 | } 285 | } 286 | 287 | micri(handleErrors(async (req, res) => { 288 | throw new Error('What happened here?') 289 | })).listen(3000); 290 | ``` 291 | 292 | ## Contributing 293 | 294 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device 295 | 2. Link the package to the global module directory: `npm link` 296 | 3. Within the module you want to test your local development instance of Micri, just link it to the dependencies: `npm link micri`. Instead of the default one from npm, node will now use your clone of Micri! 297 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Micri Examples 2 | ============== 3 | 4 | This is a collection of examples on how to use `micri` to create HTTP backed 5 | services. 6 | 7 | How to Use 8 | ---------- 9 | 10 | All the examples under this directory requires that you have first built 11 | `micri` in the root directory of this repository. 12 | 13 | ```bash 14 | # npm 15 | npm install 16 | npm run build 17 | # yarn 18 | yarn install 19 | yarn build 20 | ``` 21 | 22 | Once that's done you can `cd` into one of the example directories and check the 23 | `README.md` file there. Most of the examples require `install` and `build` 24 | before running the `start` script. 25 | -------------------------------------------------------------------------------- /examples/adal-node/.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | -------------------------------------------------------------------------------- /examples/adal-node/README.md: -------------------------------------------------------------------------------- 1 | adal-node 2 | ========= 3 | 4 | This example shows how you can integrate `micri` with Microsoft Azure AD 5 | authentication using `adal-node`. 6 | 7 | In this example the application and AD configuration is located in 8 | `src/config.ts`. In a production deployment you might want to consider some 9 | other way of storing the configuration. 10 | 11 | See Microsoft's [Node.js example](https://github.com/microsoftgraph/nodejs-security-sample/) 12 | for how to setup a new application in Microsoft Azure AD. In brief it needs to 13 | be a Web API application with at least a *Redirect URI* to `/auth`$, an 14 | *API Permission* to do `User.read`, and finally you'll need an API secret. 15 | 16 | This example enforces and verifies authentication on all URLs. To make things 17 | faster and more flexible, it uses [JWT](https://jwt.io/) to verify that the 18 | user is authenticated. The JWT token is stored in a cookie (`Secure`, 19 | `HTTPOnly`, `SameSite`). 20 | 21 | | Path | Description | 22 | |-----------|-----------------------------------------------| 23 | | `/` | The front page that says hello to the user. | 24 | | `/auth` | Authentication starts from here. | 25 | | `/token` | This endpoint refreshes the authentication. | 26 | | `/logout` | Clears the authentication token. | 27 | -------------------------------------------------------------------------------- /examples/adal-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "router", 3 | "description": "Router example", 4 | "main": "lib/index.js", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "node lib/" 8 | }, 9 | "devDependencies": { 10 | "@types/cookie": "0.3.3", 11 | "@types/cookies": "0.7.4", 12 | "@types/node": "12.7.11", 13 | "typescript": "3.8.3" 14 | }, 15 | "dependencies": { 16 | "@types/jsonwebtoken": "8.3.9", 17 | "adal-node": "0.2.1", 18 | "cookies": "0.8.0", 19 | "jsonwebtoken": "8.5.1", 20 | "micri": "../../dist" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/adal-node/src/auth.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | import { promisify } from 'util'; 3 | import { IncomingMessage, ServerResponse } from 'http'; 4 | import { send } from 'micri'; 5 | // MSAL doesn't yet support server side auth so we use ADAL for now 6 | import { AuthenticationContext, TokenResponse } from 'adal-node'; 7 | import jwt from 'jsonwebtoken'; 8 | import { 9 | clientId, 10 | clientSecret, 11 | authorityUrl, 12 | redirectUri, 13 | resource, 14 | jwtSecret, 15 | authCookieName, 16 | useSecureCookies 17 | } from './config'; 18 | import {Opts} from './types'; 19 | 20 | 21 | const randomBytes = promisify(crypto.randomBytes); 22 | const createAuthorizationUrl = (state: string) => 23 | `${authorityUrl}/oauth2/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&state=${state}&resource=${resource}`; 24 | 25 | // Clients get redirected here in order to create an OAuth authorize url and redirect them to AAD. 26 | // There they will authenticate and give their consent to allow this app access to 27 | // some resource they own. 28 | export async function auth(_req: IncomingMessage, res: ServerResponse, opts: Opts) { 29 | const buf = await randomBytes(48); 30 | const token = buf.toString('base64').replace(/\//g,'_').replace(/\+/g,'-'); 31 | const maxAge = 60 * 60 * 1000; 32 | 33 | opts.cookies.set('authstate', token, { maxAge, httpOnly: true, secure: useSecureCookies, sameSite: 'strict' }); 34 | var authorizationUrl = createAuthorizationUrl(token); 35 | 36 | res.statusCode = 307; 37 | res.setHeader('Location', authorizationUrl); 38 | res.end(); 39 | }; 40 | 41 | // After consent is granted AAD redirects here. The ADAL library is invoked via the 42 | // AuthenticationContext and retrieves an access token that can be used to access the 43 | // user owned resource. 44 | export async function getAToken(_req: IncomingMessage, res: ServerResponse, opts: Opts) { 45 | const { query } = opts; 46 | const authstate = opts.cookies.get('authstate'); 47 | 48 | // TODO parse query 49 | if (authstate !== query.state) { 50 | // TODO send proper error 51 | return send(res, 200, 'error: state does not match'); 52 | } 53 | 54 | const authenticationContext = new AuthenticationContext(authorityUrl); 55 | 56 | const code = Array.isArray(query.code) ? query.code[0] : query.code; 57 | authenticationContext.acquireTokenWithAuthorizationCode( 58 | code, 59 | redirectUri, 60 | resource, 61 | clientId, 62 | clientSecret, 63 | (err, response) => { 64 | if (err) { 65 | console.error(err); 66 | return send(res, 500); 67 | } 68 | if (response.error) { 69 | return send(res, 400); 70 | } 71 | 72 | const { accessToken, tenantId, userId, givenName, expiresIn } = response as TokenResponse; 73 | const token = jwt.sign({ 74 | sub: userId, 75 | name: givenName, 76 | my_accessToken: accessToken, 77 | my_tenant: tenantId 78 | }, jwtSecret, { 79 | expiresIn 80 | }); 81 | const expires = new Date(Date.now() + expiresIn * 1000); 82 | opts.cookies.set(authCookieName, token, { expires, httpOnly: true, secure: useSecureCookies, sameSite: 'strict' }); 83 | 84 | // To avoid a pointless redirect you could also start serving directly from here 85 | res.statusCode = 307; 86 | res.setHeader('Location', '/'); 87 | res.end(); 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /examples/adal-node/src/config.ts: -------------------------------------------------------------------------------- 1 | export const clientId = '12345678-1234-1234-1234-1234567890ab'; 2 | export const clientSecret = 'verysecret'; 3 | export const authorityHostUrl = 'https://login.windows.net'; 4 | export const tenant = '12345678-1234-1234-1234-1234567890ab'; 5 | export const authorityUrl = authorityHostUrl + '/' + tenant; 6 | export const redirectUri = 'http://localhost:3000/token'; 7 | export const resource = 'https://graph.microsoft.com'; 8 | export const authCookieName = 'access_token'; 9 | export const jwtSecret = 'secret'; // Should be more secure than this IRL 10 | export const useSecureCookies = process.env.NODE_ENV === 'production'; 11 | -------------------------------------------------------------------------------- /examples/adal-node/src/index.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'url'; 2 | import { promisify } from 'util'; 3 | import micri, { 4 | IncomingMessage, 5 | ServerResponse, 6 | MicriHandler, 7 | Router, 8 | send 9 | } from 'micri'; 10 | import Cookies from 'cookies'; 11 | import jwt from 'jsonwebtoken'; 12 | import { auth, getAToken } from './auth'; 13 | import { Opts } from './types'; 14 | import {jwtSecret, useSecureCookies, authCookieName } from './config'; 15 | 16 | const { router, on, otherwise } = Router; 17 | const jwtVerify = promisify(jwt.verify); 18 | 19 | function logout(_req: IncomingMessage, res: ServerResponse, opts: Opts) { 20 | const expires = new Date(0); 21 | 22 | // Clear cookies 23 | opts.cookies.set('authstate', '', { expires, secure: useSecureCookies, sameSite: 'strict' }); 24 | opts.cookies.set(authCookieName, '', { expires, secure: useSecureCookies, sameSite: 'strict' }) 25 | 26 | res.statusCode = 307; 27 | res.setHeader('Location', '/'); 28 | res.end(); 29 | } 30 | 31 | const authReq = (hndl: MicriHandler): MicriHandler => 32 | async (req: IncomingMessage, res: ServerResponse, opts: Opts) => { 33 | if (['/auth', '/token'].includes(opts.path)) { 34 | return hndl(req, res, opts); 35 | } 36 | 37 | try { 38 | const token = opts.cookies.get(authCookieName); 39 | 40 | if (!token) { 41 | // Not authenticated so redirect to auth on server-side. 42 | return auth(req, res, opts); 43 | } 44 | 45 | const decoded = await jwtVerify(token, jwtSecret) as {name: string, my_tenant: string, my_accessToken: string }; 46 | 47 | if (typeof decoded.name !== 'string' || 48 | typeof decoded.my_tenant !== 'string' || 49 | typeof decoded.my_accessToken !== 'string') { 50 | throw new Error('Bail out'); 51 | } 52 | 53 | return hndl(req, res, { 54 | ...opts, 55 | user: { 56 | name: decoded.name, 57 | tenantId: decoded.my_tenant, 58 | accessToken: decoded.my_accessToken 59 | } 60 | }); 61 | } catch (err) { 62 | console.error(err); 63 | return send(res, 400, 'Stop hacking'); 64 | } 65 | }; 66 | 67 | const parsePath = (hndl: MicriHandler): MicriHandler => 68 | (req: IncomingMessage, res: ServerResponse, opts: Opts) => { 69 | const url = parse(req.url || '/', true); 70 | 71 | return hndl(req, res, { 72 | ...(opts || {}), 73 | path: url.pathname || '/', 74 | query: url.query, 75 | cookies: new Cookies(req, res) 76 | }); 77 | }; 78 | 79 | micri(parsePath(authReq(router( 80 | on.get((_req: IncomingMessage, _res: ServerResponse, opts: Opts) => opts.path === '/auth', auth), 81 | on.get((_req: IncomingMessage, _res: ServerResponse, opts: Opts) => opts.path === '/token', getAToken), 82 | on.get((_req: IncomingMessage, _res: ServerResponse, opts: Opts) => opts.path === '/logout', logout), 83 | on.get(() => true, (_req: IncomingMessage, _res: ServerResponse, opts: Opts) => `Hello ${opts.user?.name}`), 84 | otherwise((_req: IncomingMessage, res: ServerResponse) => send(res, 400, 'Method Not Accepted')))))) 85 | .listen(3000); 86 | -------------------------------------------------------------------------------- /examples/adal-node/src/types.ts: -------------------------------------------------------------------------------- 1 | import {ParsedUrlQuery} from 'querystring'; 2 | import Cookies from 'cookies'; 3 | 4 | export interface Opts { 5 | path: string; 6 | query: ParsedUrlQuery; 7 | cookies: Cookies; 8 | user?: { 9 | name: string; 10 | tenantId: string; 11 | accessToken: string; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/adal-node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "lib": ["esnext"], 6 | "module": "CommonJS", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "strictNullChecks": true, 10 | "noImplicitAny": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "outDir": "./lib", 14 | "strict": true, 15 | "target": "ES2018", 16 | "inlineSourceMap": true, 17 | "types": ["node"], 18 | "typeRoots": [ 19 | "./node_modules/@types" 20 | ] 21 | }, 22 | "include": ["./src"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/adal-node/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/body-parser@*": 6 | version "1.19.0" 7 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" 8 | integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== 9 | dependencies: 10 | "@types/connect" "*" 11 | "@types/node" "*" 12 | 13 | "@types/connect@*": 14 | version "3.4.33" 15 | resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" 16 | integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== 17 | dependencies: 18 | "@types/node" "*" 19 | 20 | "@types/cookie@0.3.3": 21 | version "0.3.3" 22 | resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" 23 | integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== 24 | 25 | "@types/cookies@0.7.4": 26 | version "0.7.4" 27 | resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.4.tgz#26dedf791701abc0e36b5b79a5722f40e455f87b" 28 | integrity sha512-oTGtMzZZAVuEjTwCjIh8T8FrC8n/uwy+PG0yTvQcdZ7etoel7C7/3MSd7qrukENTgQtotG7gvBlBojuVs7X5rw== 29 | dependencies: 30 | "@types/connect" "*" 31 | "@types/express" "*" 32 | "@types/keygrip" "*" 33 | "@types/node" "*" 34 | 35 | "@types/express-serve-static-core@*": 36 | version "4.17.5" 37 | resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.5.tgz#a00ac7dadd746ae82477443e4d480a6a93ea083c" 38 | integrity sha512-578YH5Lt88AKoADy0b2jQGwJtrBxezXtVe/MBqWXKZpqx91SnC0pVkVCcxcytz3lWW+cHBYDi3Ysh0WXc+rAYw== 39 | dependencies: 40 | "@types/node" "*" 41 | "@types/range-parser" "*" 42 | 43 | "@types/express@*": 44 | version "4.17.6" 45 | resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.6.tgz#6bce49e49570507b86ea1b07b806f04697fac45e" 46 | integrity sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w== 47 | dependencies: 48 | "@types/body-parser" "*" 49 | "@types/express-serve-static-core" "*" 50 | "@types/qs" "*" 51 | "@types/serve-static" "*" 52 | 53 | "@types/jsonwebtoken@8.3.9": 54 | version "8.3.9" 55 | resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.3.9.tgz#48da9a49997e4eb046733e6878f583d7448f0594" 56 | integrity sha512-00rI8GbOKuRtoYxltFSRTVUXCRLbuYwln2/nUMPtFU9JGS7if+nnmLjeoFGmqsNCmblPLAaeQ/zMLVsHr6T5bg== 57 | dependencies: 58 | "@types/node" "*" 59 | 60 | "@types/keygrip@*": 61 | version "1.0.2" 62 | resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" 63 | integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw== 64 | 65 | "@types/mime@*": 66 | version "2.0.1" 67 | resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" 68 | integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== 69 | 70 | "@types/node@*": 71 | version "13.13.4" 72 | resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.4.tgz#1581d6c16e3d4803eb079c87d4ac893ee7501c2c" 73 | integrity sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA== 74 | 75 | "@types/node@12.7.11": 76 | version "12.7.11" 77 | resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.11.tgz#be879b52031cfb5d295b047f5462d8ef1a716446" 78 | integrity sha512-Otxmr2rrZLKRYIybtdG/sgeO+tHY20GxeDjcGmUnmmlCWyEnv2a2x1ZXBo3BTec4OiTXMQCiazB8NMBf0iRlFw== 79 | 80 | "@types/node@^8.0.47": 81 | version "8.10.60" 82 | resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.60.tgz#73eb4d1e1c8aa5dc724363b57db019cf28863ef7" 83 | integrity sha512-YjPbypHFuiOV0bTgeF07HpEEqhmHaZqYNSdCKeBJa+yFoQ/7BC+FpJcwmi34xUIIRVFktnUyP1dPU8U0612GOg== 84 | 85 | "@types/qs@*": 86 | version "6.9.1" 87 | resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.1.tgz#937fab3194766256ee09fcd40b781740758617e7" 88 | integrity sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw== 89 | 90 | "@types/range-parser@*": 91 | version "1.2.3" 92 | resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" 93 | integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== 94 | 95 | "@types/serve-static@*": 96 | version "1.13.3" 97 | resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" 98 | integrity sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g== 99 | dependencies: 100 | "@types/express-serve-static-core" "*" 101 | "@types/mime" "*" 102 | 103 | adal-node@0.2.1: 104 | version "0.2.1" 105 | resolved "https://registry.yarnpkg.com/adal-node/-/adal-node-0.2.1.tgz#19e401bd579977448c1a77ce0e5b4c9accdc334e" 106 | integrity sha512-C/oasZuTy0NIqh5wPWjG/09XaG+zS7elC8upf1ZVExt9lSRncme4Ejbx8CKYk+wsGgj609y84txtRAXQVvqApg== 107 | dependencies: 108 | "@types/node" "^8.0.47" 109 | async "^2.6.3" 110 | date-utils "*" 111 | jws "3.x.x" 112 | request "^2.88.0" 113 | underscore ">= 1.3.1" 114 | uuid "^3.1.0" 115 | xmldom ">= 0.1.x" 116 | xpath.js "~1.1.0" 117 | 118 | ajv@^6.5.5: 119 | version "6.12.6" 120 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" 121 | integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== 122 | dependencies: 123 | fast-deep-equal "^3.1.1" 124 | fast-json-stable-stringify "^2.0.0" 125 | json-schema-traverse "^0.4.1" 126 | uri-js "^4.2.2" 127 | 128 | asn1@~0.2.3: 129 | version "0.2.4" 130 | resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" 131 | integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== 132 | dependencies: 133 | safer-buffer "~2.1.0" 134 | 135 | assert-plus@1.0.0, assert-plus@^1.0.0: 136 | version "1.0.0" 137 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" 138 | integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= 139 | 140 | async@^2.6.3: 141 | version "2.6.4" 142 | resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" 143 | integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== 144 | dependencies: 145 | lodash "^4.17.14" 146 | 147 | asynckit@^0.4.0: 148 | version "0.4.0" 149 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 150 | integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= 151 | 152 | aws-sign2@~0.7.0: 153 | version "0.7.0" 154 | resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" 155 | integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= 156 | 157 | aws4@^1.8.0: 158 | version "1.9.1" 159 | resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" 160 | integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== 161 | 162 | bcrypt-pbkdf@^1.0.0: 163 | version "1.0.2" 164 | resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" 165 | integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= 166 | dependencies: 167 | tweetnacl "^0.14.3" 168 | 169 | buffer-equal-constant-time@1.0.1: 170 | version "1.0.1" 171 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 172 | integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= 173 | 174 | caseless@~0.12.0: 175 | version "0.12.0" 176 | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" 177 | integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= 178 | 179 | combined-stream@^1.0.6, combined-stream@~1.0.6: 180 | version "1.0.8" 181 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 182 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 183 | dependencies: 184 | delayed-stream "~1.0.0" 185 | 186 | cookies@0.8.0: 187 | version "0.8.0" 188 | resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" 189 | integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow== 190 | dependencies: 191 | depd "~2.0.0" 192 | keygrip "~1.1.0" 193 | 194 | core-util-is@1.0.2: 195 | version "1.0.2" 196 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 197 | integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= 198 | 199 | dashdash@^1.12.0: 200 | version "1.14.1" 201 | resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" 202 | integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= 203 | dependencies: 204 | assert-plus "^1.0.0" 205 | 206 | date-utils@*: 207 | version "1.2.21" 208 | resolved "https://registry.yarnpkg.com/date-utils/-/date-utils-1.2.21.tgz#61fb16cdc1274b3c9acaaffe9fc69df8720a2b64" 209 | integrity sha1-YfsWzcEnSzyayq/+n8ad+HIKK2Q= 210 | 211 | delayed-stream@~1.0.0: 212 | version "1.0.0" 213 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 214 | integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= 215 | 216 | depd@~2.0.0: 217 | version "2.0.0" 218 | resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" 219 | integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== 220 | 221 | ecc-jsbn@~0.1.1: 222 | version "0.1.2" 223 | resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" 224 | integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= 225 | dependencies: 226 | jsbn "~0.1.0" 227 | safer-buffer "^2.1.0" 228 | 229 | ecdsa-sig-formatter@1.0.11: 230 | version "1.0.11" 231 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" 232 | integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== 233 | dependencies: 234 | safe-buffer "^5.0.1" 235 | 236 | extend@~3.0.2: 237 | version "3.0.2" 238 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 239 | integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== 240 | 241 | extsprintf@1.3.0: 242 | version "1.3.0" 243 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" 244 | integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= 245 | 246 | extsprintf@^1.2.0: 247 | version "1.4.0" 248 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" 249 | integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= 250 | 251 | fast-deep-equal@^3.1.1: 252 | version "3.1.3" 253 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" 254 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 255 | 256 | fast-json-stable-stringify@^2.0.0: 257 | version "2.1.0" 258 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" 259 | integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== 260 | 261 | forever-agent@~0.6.1: 262 | version "0.6.1" 263 | resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" 264 | integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= 265 | 266 | form-data@~2.3.2: 267 | version "2.3.3" 268 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" 269 | integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== 270 | dependencies: 271 | asynckit "^0.4.0" 272 | combined-stream "^1.0.6" 273 | mime-types "^2.1.12" 274 | 275 | getpass@^0.1.1: 276 | version "0.1.7" 277 | resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" 278 | integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= 279 | dependencies: 280 | assert-plus "^1.0.0" 281 | 282 | har-schema@^2.0.0: 283 | version "2.0.0" 284 | resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" 285 | integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= 286 | 287 | har-validator@~5.1.3: 288 | version "5.1.3" 289 | resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" 290 | integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== 291 | dependencies: 292 | ajv "^6.5.5" 293 | har-schema "^2.0.0" 294 | 295 | http-signature@~1.2.0: 296 | version "1.2.0" 297 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" 298 | integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= 299 | dependencies: 300 | assert-plus "^1.0.0" 301 | jsprim "^1.2.2" 302 | sshpk "^1.7.0" 303 | 304 | is-typedarray@~1.0.0: 305 | version "1.0.0" 306 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 307 | integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= 308 | 309 | isstream@~0.1.2: 310 | version "0.1.2" 311 | resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 312 | integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= 313 | 314 | jsbn@~0.1.0: 315 | version "0.1.1" 316 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" 317 | integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= 318 | 319 | json-schema-traverse@^0.4.1: 320 | version "0.4.1" 321 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 322 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 323 | 324 | json-schema@0.2.3: 325 | version "0.2.3" 326 | resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" 327 | integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= 328 | 329 | json-stringify-safe@~5.0.1: 330 | version "5.0.1" 331 | resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 332 | integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= 333 | 334 | jsonwebtoken@8.5.1: 335 | version "8.5.1" 336 | resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" 337 | integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== 338 | dependencies: 339 | jws "^3.2.2" 340 | lodash.includes "^4.3.0" 341 | lodash.isboolean "^3.0.3" 342 | lodash.isinteger "^4.0.4" 343 | lodash.isnumber "^3.0.3" 344 | lodash.isplainobject "^4.0.6" 345 | lodash.isstring "^4.0.1" 346 | lodash.once "^4.0.0" 347 | ms "^2.1.1" 348 | semver "^5.6.0" 349 | 350 | jsprim@^1.2.2: 351 | version "1.4.1" 352 | resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" 353 | integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= 354 | dependencies: 355 | assert-plus "1.0.0" 356 | extsprintf "1.3.0" 357 | json-schema "0.2.3" 358 | verror "1.10.0" 359 | 360 | jwa@^1.4.1: 361 | version "1.4.1" 362 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" 363 | integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== 364 | dependencies: 365 | buffer-equal-constant-time "1.0.1" 366 | ecdsa-sig-formatter "1.0.11" 367 | safe-buffer "^5.0.1" 368 | 369 | jws@3.x.x, jws@^3.2.2: 370 | version "3.2.2" 371 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" 372 | integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== 373 | dependencies: 374 | jwa "^1.4.1" 375 | safe-buffer "^5.0.1" 376 | 377 | keygrip@~1.1.0: 378 | version "1.1.0" 379 | resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" 380 | integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ== 381 | dependencies: 382 | tsscmp "1.0.6" 383 | 384 | lodash.includes@^4.3.0: 385 | version "4.3.0" 386 | resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" 387 | integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= 388 | 389 | lodash.isboolean@^3.0.3: 390 | version "3.0.3" 391 | resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" 392 | integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= 393 | 394 | lodash.isinteger@^4.0.4: 395 | version "4.0.4" 396 | resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" 397 | integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= 398 | 399 | lodash.isnumber@^3.0.3: 400 | version "3.0.3" 401 | resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" 402 | integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= 403 | 404 | lodash.isplainobject@^4.0.6: 405 | version "4.0.6" 406 | resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" 407 | integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= 408 | 409 | lodash.isstring@^4.0.1: 410 | version "4.0.1" 411 | resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" 412 | integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= 413 | 414 | lodash.once@^4.0.0: 415 | version "4.1.1" 416 | resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" 417 | integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= 418 | 419 | lodash@^4.17.14: 420 | version "4.17.21" 421 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 422 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 423 | 424 | micri@../../dist: 425 | version "0.0.0" 426 | 427 | mime-db@1.44.0: 428 | version "1.44.0" 429 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" 430 | integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== 431 | 432 | mime-types@^2.1.12, mime-types@~2.1.19: 433 | version "2.1.27" 434 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" 435 | integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== 436 | dependencies: 437 | mime-db "1.44.0" 438 | 439 | ms@^2.1.1: 440 | version "2.1.2" 441 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 442 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 443 | 444 | oauth-sign@~0.9.0: 445 | version "0.9.0" 446 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" 447 | integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== 448 | 449 | performance-now@^2.1.0: 450 | version "2.1.0" 451 | resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" 452 | integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= 453 | 454 | psl@^1.1.28: 455 | version "1.8.0" 456 | resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" 457 | integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== 458 | 459 | punycode@^2.1.0, punycode@^2.1.1: 460 | version "2.1.1" 461 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" 462 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== 463 | 464 | qs@~6.5.2: 465 | version "6.5.2" 466 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" 467 | integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== 468 | 469 | request@^2.88.0: 470 | version "2.88.2" 471 | resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" 472 | integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== 473 | dependencies: 474 | aws-sign2 "~0.7.0" 475 | aws4 "^1.8.0" 476 | caseless "~0.12.0" 477 | combined-stream "~1.0.6" 478 | extend "~3.0.2" 479 | forever-agent "~0.6.1" 480 | form-data "~2.3.2" 481 | har-validator "~5.1.3" 482 | http-signature "~1.2.0" 483 | is-typedarray "~1.0.0" 484 | isstream "~0.1.2" 485 | json-stringify-safe "~5.0.1" 486 | mime-types "~2.1.19" 487 | oauth-sign "~0.9.0" 488 | performance-now "^2.1.0" 489 | qs "~6.5.2" 490 | safe-buffer "^5.1.2" 491 | tough-cookie "~2.5.0" 492 | tunnel-agent "^0.6.0" 493 | uuid "^3.3.2" 494 | 495 | safe-buffer@^5.0.1, safe-buffer@^5.1.2: 496 | version "5.2.0" 497 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" 498 | integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== 499 | 500 | safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: 501 | version "2.1.2" 502 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 503 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 504 | 505 | semver@^5.6.0: 506 | version "5.7.1" 507 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" 508 | integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== 509 | 510 | sshpk@^1.7.0: 511 | version "1.16.1" 512 | resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" 513 | integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== 514 | dependencies: 515 | asn1 "~0.2.3" 516 | assert-plus "^1.0.0" 517 | bcrypt-pbkdf "^1.0.0" 518 | dashdash "^1.12.0" 519 | ecc-jsbn "~0.1.1" 520 | getpass "^0.1.1" 521 | jsbn "~0.1.0" 522 | safer-buffer "^2.0.2" 523 | tweetnacl "~0.14.0" 524 | 525 | tough-cookie@~2.5.0: 526 | version "2.5.0" 527 | resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" 528 | integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== 529 | dependencies: 530 | psl "^1.1.28" 531 | punycode "^2.1.1" 532 | 533 | tsscmp@1.0.6: 534 | version "1.0.6" 535 | resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" 536 | integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== 537 | 538 | tunnel-agent@^0.6.0: 539 | version "0.6.0" 540 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 541 | integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= 542 | dependencies: 543 | safe-buffer "^5.0.1" 544 | 545 | tweetnacl@^0.14.3, tweetnacl@~0.14.0: 546 | version "0.14.5" 547 | resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" 548 | integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= 549 | 550 | typescript@3.8.3: 551 | version "3.8.3" 552 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" 553 | integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== 554 | 555 | "underscore@>= 1.3.1": 556 | version "1.10.2" 557 | resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.10.2.tgz#73d6aa3668f3188e4adb0f1943bd12cfd7efaaaf" 558 | integrity sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg== 559 | 560 | uri-js@^4.2.2: 561 | version "4.4.1" 562 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" 563 | integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== 564 | dependencies: 565 | punycode "^2.1.0" 566 | 567 | uuid@^3.1.0, uuid@^3.3.2: 568 | version "3.4.0" 569 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" 570 | integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== 571 | 572 | verror@1.10.0: 573 | version "1.10.0" 574 | resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" 575 | integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= 576 | dependencies: 577 | assert-plus "^1.0.0" 578 | core-util-is "1.0.2" 579 | extsprintf "^1.2.0" 580 | 581 | "xmldom@>= 0.1.x": 582 | version "0.3.0" 583 | resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.3.0.tgz#e625457f4300b5df9c2e1ecb776147ece47f3e5a" 584 | integrity sha512-z9s6k3wxE+aZHgXYxSTpGDo7BYOUfJsIRyoZiX6HTjwpwfS2wpQBQKa2fD+ShLyPkqDYo5ud7KitmLZ2Cd6r0g== 585 | 586 | xpath.js@~1.1.0: 587 | version "1.1.0" 588 | resolved "https://registry.yarnpkg.com/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1" 589 | integrity sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ== 590 | -------------------------------------------------------------------------------- /examples/errors/.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | -------------------------------------------------------------------------------- /examples/errors/README.md: -------------------------------------------------------------------------------- 1 | Errors 2 | ====== 3 | 4 | While `micri` provides some basic error handling primitives a real application 5 | may need more capable and well-defined error handling. This example shows how 6 | to format errors according to `Accept` header in a standard format. 7 | -------------------------------------------------------------------------------- /examples/errors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "router", 3 | "description": "Router example", 4 | "main": "lib/index.js", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "node lib/" 8 | }, 9 | "dependencies": { 10 | "@hapi/accept": "5.0.1", 11 | "micri": "../../dist" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "12.7.11", 15 | "typescript": "3.6.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/errors/src/errors.ts: -------------------------------------------------------------------------------- 1 | import { STATUS_CODES } from 'http'; 2 | import { send, IncomingMessage, ServerResponse } from 'micri'; 3 | import { ErrorObject } from 'ajv'; 4 | import { parseAll } from '@hapi/accept'; 5 | 6 | export interface MyError { 7 | code: string; 8 | message: string; 9 | [x: string]: any; 10 | }; 11 | 12 | /** 13 | * Send a standardized error to the HTTP client. 14 | * @param req is the incoming request. 15 | * @param res is the outgoing response. 16 | * @param error is an object describing the error condition. 17 | */ 18 | export function sendError(req: IncomingMessage, res: ServerResponse, statusCode: number, error: MyError) { 19 | let types = ['*/*']; 20 | 21 | if (!error.code) { 22 | throw new Error('Error "code" is missing'); 23 | } 24 | 25 | if (!error.message) { 26 | throw new Error('Error "message" is missing'); 27 | } 28 | 29 | try { 30 | const parsed = parseAll(req.headers); 31 | types = parsed.mediaTypes; 32 | } catch (err) { 33 | console.error(err); 34 | } 35 | 36 | if (types.includes('text/html')) { 37 | return send(res, statusCode, ` 38 | 39 |

${STATUS_CODES[statusCode] || 'Internal Server Error'}

40 |

${error.message}

41 | `); 42 | } else if (types.includes('*/*')) { 43 | return send(res, statusCode, { 44 | error 45 | }); 46 | } else if (types.includes('text/plain')) { 47 | return send(res, statusCode, error.message) 48 | } else { 49 | return send(res, statusCode, { 50 | error 51 | }); 52 | } 53 | } 54 | 55 | /** 56 | * Send a thing not found error. 57 | * @param req is the incoming request. 58 | * @param res is the outgoing response. 59 | * @param id is the id of the thing that was not found. 60 | */ 61 | export function sendThingNotFound(req: IncomingMessage, res: ServerResponse, id: string) { 62 | sendError(req, res, 404, { 63 | code: 'thing_not_found', 64 | message: `Thing not found: "${id}"`, 65 | id 66 | }); 67 | } 68 | 69 | /** 70 | * Send bad request error. 71 | * @param req is the incoming request. 72 | * @param res is the outgoing response. 73 | */ 74 | export function sendBadRequest(req: IncomingMessage, res: ServerResponse) { 75 | sendError(req, res, 400, { 76 | code: 'bad_request', 77 | message: 'Invalid request method or path' 78 | }); 79 | } 80 | 81 | /** 82 | * Send an Ajv based error on resource creation. 83 | * @param type is the resource type being created. 84 | */ 85 | export function sendAjvError(req: IncomingMessage, res: ServerResponse, type: string, errors: ErrorObject[]) { 86 | sendError(req, res, 400, { 87 | code: 'configuration_error', 88 | message: `There is an error in the ${type} configuration`, 89 | errors: errors.map(e => `"${e.dataPath}" ${e.message}`) 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /examples/errors/src/index.ts: -------------------------------------------------------------------------------- 1 | import micri, { 2 | IncomingMessage, 3 | ServerResponse, 4 | Router, 5 | } from 'micri'; 6 | import {sendBadRequest, sendThingNotFound} from './errors'; 7 | 8 | const { router, on, otherwise } = Router; 9 | 10 | micri(router( 11 | on.get(() => true, (req: IncomingMessage, res: ServerResponse) => sendThingNotFound(req, res, req.url || '')), 12 | otherwise((req: IncomingMessage, res: ServerResponse) => sendBadRequest(req, res)))) 13 | .listen(3000); 14 | -------------------------------------------------------------------------------- /examples/errors/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "lib": ["esnext"], 6 | "module": "CommonJS", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "strictNullChecks": true, 10 | "noImplicitAny": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "outDir": "./lib", 14 | "strict": true, 15 | "target": "ES2018", 16 | "inlineSourceMap": true, 17 | "types": ["node"], 18 | "typeRoots": [ 19 | "./node_modules/@types" 20 | ] 21 | }, 22 | "include": ["./src"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/errors/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@hapi/accept@5.0.1": 6 | version "5.0.1" 7 | resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.1.tgz#068553e867f0f63225a506ed74e899441af53e10" 8 | integrity sha512-fMr4d7zLzsAXo28PRRQPXR1o2Wmu+6z+VY1UzDp0iFo13Twj8WePakwXBiqn3E1aAlTpSNzCXdnnQXFhst8h8Q== 9 | dependencies: 10 | "@hapi/boom" "9.x.x" 11 | "@hapi/hoek" "9.x.x" 12 | 13 | "@hapi/boom@9.x.x": 14 | version "9.1.0" 15 | resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.0.tgz#0d9517657a56ff1e0b42d0aca9da1b37706fec56" 16 | integrity sha512-4nZmpp4tXbm162LaZT45P7F7sgiem8dwAh2vHWT6XX24dozNjGMg6BvKCRvtCUcmcXqeMIUqWN8Rc5X8yKuROQ== 17 | dependencies: 18 | "@hapi/hoek" "9.x.x" 19 | 20 | "@hapi/hoek@9.x.x": 21 | version "9.0.4" 22 | resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.0.4.tgz#e80ad4e8e8d2adc6c77d985f698447e8628b6010" 23 | integrity sha512-EwaJS7RjoXUZ2cXXKZZxZqieGtc7RbvQhUy8FwDoMQtxWVi14tFjeFCYPZAM1mBCpOpiBpyaZbb9NeHc7eGKgw== 24 | 25 | "@types/node@12.7.11": 26 | version "12.7.11" 27 | resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.11.tgz#be879b52031cfb5d295b047f5462d8ef1a716446" 28 | integrity sha512-Otxmr2rrZLKRYIybtdG/sgeO+tHY20GxeDjcGmUnmmlCWyEnv2a2x1ZXBo3BTec4OiTXMQCiazB8NMBf0iRlFw== 29 | 30 | micri@../../dist: 31 | version "0.0.0" 32 | 33 | typescript@3.6.3: 34 | version "3.6.3" 35 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.3.tgz#fea942fabb20f7e1ca7164ff626f1a9f3f70b4da" 36 | integrity sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw== 37 | -------------------------------------------------------------------------------- /examples/external-api-call/README.md: -------------------------------------------------------------------------------- 1 | External API Call Example 2 | ========================= 3 | 4 | Shows how to get data from an external api using async/await. 5 | 6 | ## How to use 7 | 8 | Install it and run: 9 | 10 | ```bash 11 | npm install 12 | npm run start 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/external-api-call/index.js: -------------------------------------------------------------------------------- 1 | const fetch = require('@turist/fetch').default(); 2 | const { serve } = require('micri'); 3 | 4 | serve(async () => { 5 | const response = await fetch('https://api.example.com'); 6 | const json = await response.json(); 7 | 8 | return json; 9 | }).listen(3000); 10 | -------------------------------------------------------------------------------- /examples/external-api-call/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "external-api-call", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "node index.js" 7 | }, 8 | "dependencies": { 9 | "@turist/fetch": "7.2.0", 10 | "micri": "../../dist", 11 | "node-fetch": "2.6.7" 12 | }, 13 | "author": "", 14 | "license": "MIT" 15 | } 16 | -------------------------------------------------------------------------------- /examples/external-api-call/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@turist/fetch@7.2.0": 6 | version "7.2.0" 7 | resolved "https://registry.yarnpkg.com/@turist/fetch/-/fetch-7.2.0.tgz#57df869df1cd9b299588554eec4b8543effcc714" 8 | integrity sha512-2x7EGw+6OJ29phunsbGvtxlNmSfcuPcyYudkMbi8gARCP9eJ1CtuMvnVUHL//O9Ixi9SJiug8wNt6lj86pN8XQ== 9 | dependencies: 10 | "@types/node-fetch" "2" 11 | 12 | "@types/node-fetch@2": 13 | version "2.5.7" 14 | resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c" 15 | integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw== 16 | dependencies: 17 | "@types/node" "*" 18 | form-data "^3.0.0" 19 | 20 | "@types/node@*": 21 | version "14.0.1" 22 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.1.tgz#5d93e0a099cd0acd5ef3d5bde3c086e1f49ff68c" 23 | integrity sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA== 24 | 25 | asynckit@^0.4.0: 26 | version "0.4.0" 27 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 28 | integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= 29 | 30 | combined-stream@^1.0.8: 31 | version "1.0.8" 32 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 33 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 34 | dependencies: 35 | delayed-stream "~1.0.0" 36 | 37 | delayed-stream@~1.0.0: 38 | version "1.0.0" 39 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 40 | integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= 41 | 42 | form-data@^3.0.0: 43 | version "3.0.0" 44 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" 45 | integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg== 46 | dependencies: 47 | asynckit "^0.4.0" 48 | combined-stream "^1.0.8" 49 | mime-types "^2.1.12" 50 | 51 | micri@../../dist: 52 | version "0.0.0" 53 | 54 | mime-db@1.44.0: 55 | version "1.44.0" 56 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" 57 | integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== 58 | 59 | mime-types@^2.1.12: 60 | version "2.1.27" 61 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" 62 | integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== 63 | dependencies: 64 | mime-db "1.44.0" 65 | 66 | node-fetch@2.6.7: 67 | version "2.6.7" 68 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" 69 | integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== 70 | dependencies: 71 | whatwg-url "^5.0.0" 72 | 73 | tr46@~0.0.3: 74 | version "0.0.3" 75 | resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" 76 | integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= 77 | 78 | webidl-conversions@^3.0.0: 79 | version "3.0.1" 80 | resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" 81 | integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= 82 | 83 | whatwg-url@^5.0.0: 84 | version "5.0.0" 85 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" 86 | integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= 87 | dependencies: 88 | tr46 "~0.0.3" 89 | webidl-conversions "^3.0.0" 90 | -------------------------------------------------------------------------------- /examples/json-body-parsing/README.md: -------------------------------------------------------------------------------- 1 | Parse JSON Body Example 2 | ======================= 3 | 4 | Shows how to get data posted to your microservice using async/await. 5 | 6 | How to Use 7 | ---------- 8 | 9 | Install it and run: 10 | 11 | ```bash 12 | npm install 13 | npm run start 14 | ``` 15 | -------------------------------------------------------------------------------- /examples/json-body-parsing/index.js: -------------------------------------------------------------------------------- 1 | const {serve, json} = require('micri'); 2 | 3 | const server = serve(async req => { 4 | const data = await json(req); 5 | console.log(data); 6 | 7 | return 'Data logged to your console'; 8 | }); 9 | 10 | server.listen(3000); 11 | -------------------------------------------------------------------------------- /examples/json-body-parsing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-body-parsing", 3 | "scripts": { 4 | "start": "node index.js" 5 | }, 6 | "dependencies": { 7 | "micri": "../../dist" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/json-body-parsing/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | micri@../../dist: 6 | version "0.0.0" 7 | -------------------------------------------------------------------------------- /examples/router/.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | -------------------------------------------------------------------------------- /examples/router/node_modules/.bin/tsc: -------------------------------------------------------------------------------- 1 | ../typescript/bin/tsc -------------------------------------------------------------------------------- /examples/router/node_modules/.bin/tsserver: -------------------------------------------------------------------------------- 1 | ../typescript/bin/tsserver -------------------------------------------------------------------------------- /examples/router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "router", 3 | "description": "Router example", 4 | "main": "lib/index.js", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "node lib/" 8 | }, 9 | "devDependencies": { 10 | "micri": "../../dist", 11 | "@types/node": "12.7.11", 12 | "typescript": "3.6.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/router/src/index.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'url'; 2 | import micri, { 3 | MicriHandler, 4 | IncomingMessage, 5 | ServerResponse, 6 | Router, 7 | send, 8 | text 9 | } from 'micri'; 10 | 11 | const { router, on, otherwise } = Router; 12 | 13 | const parsePath = (req: IncomingMessage): string => parse(req.url || '/').path || '/'; 14 | const auth = (accept: MicriHandler) => (req: IncomingMessage, res: ServerResponse, opts?: object) => 15 | req.headers.authorization === 'Bearer xyz' ? accept(req, res, { ...(opts || {}), user: 'hbp' }) : send(res, 403, 'Forbidden'); 16 | 17 | micri(auth(router( 18 | on.get((req: IncomingMessage) => parsePath(req) === '/users', (_req: IncomingMessage, _res: ServerResponse, {user}: {user?: string} = { user: 'Unknown'}) => `Hello ${user}`), 19 | on.post((req: IncomingMessage) => parsePath(req) === '/users', (req: IncomingMessage) => text(req)), 20 | otherwise((_req: IncomingMessage, res: ServerResponse) => send(res, 400, 'Method Not Accepted'))))) 21 | .listen(3000); 22 | -------------------------------------------------------------------------------- /examples/router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "lib": ["esnext"], 6 | "module": "CommonJS", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "strictNullChecks": true, 10 | "noImplicitAny": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "outDir": "./lib", 14 | "strict": true, 15 | "target": "ES2018", 16 | "inlineSourceMap": true, 17 | "types": ["node"], 18 | "typeRoots": [ 19 | "./node_modules/@types" 20 | ] 21 | }, 22 | "include": ["./src"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/router/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/node@12.7.11": 6 | version "12.7.11" 7 | resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.11.tgz#be879b52031cfb5d295b047f5462d8ef1a716446" 8 | integrity sha512-Otxmr2rrZLKRYIybtdG/sgeO+tHY20GxeDjcGmUnmmlCWyEnv2a2x1ZXBo3BTec4OiTXMQCiazB8NMBf0iRlFw== 9 | 10 | micri@../../dist: 11 | version "0.0.0" 12 | 13 | typescript@3.6.3: 14 | version "3.6.3" 15 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.3.tgz#fea942fabb20f7e1ca7164ff626f1a9f3f70b4da" 16 | integrity sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw== 17 | -------------------------------------------------------------------------------- /examples/socket.io-chat-app/README.md: -------------------------------------------------------------------------------- 1 | Chat App with socket.io 2 | ======================= 3 | 4 | Shows how to make use of socket.io with `micri`, to deploy on now. 5 | 6 | Code adapted from the corresponding tutorial 7 | [Building a Realtime Chat Webapp, by @notquiteleo](https://zeit.co/docs/examples/chat) 8 | 9 | How to Use 10 | ---------- 11 | 12 | ```bash 13 | npm install 14 | npm run start 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/socket.io-chat-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Socket.IO Chat Example 6 | 7 | 8 | 9 | 19 | 20 |
    21 | 22 |
    23 | 24 |
    25 | 26 | 27 | 28 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/socket.io-chat-app/index.js: -------------------------------------------------------------------------------- 1 | const {serve} = require('micri'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const document = path.join(__dirname, 'index.html'); 6 | const html = fs.readFileSync(document); 7 | 8 | const server = serve(async (req, res) => { 9 | console.log('Serving index.html'); 10 | res.end(html); 11 | }); 12 | 13 | const io = require('socket.io')(server); 14 | 15 | // socket-io handlers are in websocket-server.js 16 | require('./websocket-server.js')(io); 17 | 18 | server.listen(4000, () => console.log('Listening on localhost:4000')); 19 | -------------------------------------------------------------------------------- /examples/socket.io-chat-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatnow", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "node ." 7 | }, 8 | "author": "Lucas Kostka", 9 | "license": "ISC", 10 | "dependencies": { 11 | "micri": "../../dist", 12 | "socket.io": "2.4.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/socket.io-chat-app/websocket-server.js: -------------------------------------------------------------------------------- 1 | module.exports = function startServer(io) { 2 | io.on('connection', socket => { 3 | console.log('a user connected'); 4 | 5 | socket.on('disconnect', () => { 6 | console.log('user disconnected'); 7 | }); 8 | 9 | socket.on('chat message', msg => { 10 | console.log(`message: ${msg}`); 11 | io.emit('chat message', msg); 12 | }); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /examples/socket.io-chat-app/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@~1.3.4: 6 | version "1.3.7" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" 8 | integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== 9 | dependencies: 10 | mime-types "~2.1.24" 11 | negotiator "0.6.2" 12 | 13 | after@0.8.2: 14 | version "0.8.2" 15 | resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" 16 | integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= 17 | 18 | arraybuffer.slice@~0.0.7: 19 | version "0.0.7" 20 | resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" 21 | integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== 22 | 23 | backo2@1.0.2: 24 | version "1.0.2" 25 | resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" 26 | integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= 27 | 28 | base64-arraybuffer@0.1.5: 29 | version "0.1.5" 30 | resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" 31 | integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= 32 | 33 | base64id@2.0.0: 34 | version "2.0.0" 35 | resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" 36 | integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== 37 | 38 | blob@0.0.5: 39 | version "0.0.5" 40 | resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" 41 | integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== 42 | 43 | component-bind@1.0.0: 44 | version "1.0.0" 45 | resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" 46 | integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= 47 | 48 | component-emitter@1.2.1: 49 | version "1.2.1" 50 | resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" 51 | integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= 52 | 53 | component-emitter@~1.3.0: 54 | version "1.3.0" 55 | resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" 56 | integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== 57 | 58 | component-inherit@0.0.3: 59 | version "0.0.3" 60 | resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" 61 | integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= 62 | 63 | cookie@~0.4.1: 64 | version "0.4.1" 65 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" 66 | integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== 67 | 68 | debug@~3.1.0: 69 | version "3.1.0" 70 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 71 | integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== 72 | dependencies: 73 | ms "2.0.0" 74 | 75 | debug@~4.1.0: 76 | version "4.1.1" 77 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" 78 | integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== 79 | dependencies: 80 | ms "^2.1.1" 81 | 82 | engine.io-client@~3.5.0: 83 | version "3.5.0" 84 | resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.5.0.tgz#fc1b4d9616288ce4f2daf06dcf612413dec941c7" 85 | integrity sha512-12wPRfMrugVw/DNyJk34GQ5vIVArEcVMXWugQGGuw2XxUSztFNmJggZmv8IZlLyEdnpO1QB9LkcjeWewO2vxtA== 86 | dependencies: 87 | component-emitter "~1.3.0" 88 | component-inherit "0.0.3" 89 | debug "~3.1.0" 90 | engine.io-parser "~2.2.0" 91 | has-cors "1.1.0" 92 | indexof "0.0.1" 93 | parseqs "0.0.6" 94 | parseuri "0.0.6" 95 | ws "~7.4.2" 96 | xmlhttprequest-ssl "~1.5.4" 97 | yeast "0.1.2" 98 | 99 | engine.io-parser@~2.2.0: 100 | version "2.2.0" 101 | resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed" 102 | integrity sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w== 103 | dependencies: 104 | after "0.8.2" 105 | arraybuffer.slice "~0.0.7" 106 | base64-arraybuffer "0.1.5" 107 | blob "0.0.5" 108 | has-binary2 "~1.0.2" 109 | 110 | engine.io@~3.5.0: 111 | version "3.5.0" 112 | resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.5.0.tgz#9d6b985c8a39b1fe87cd91eb014de0552259821b" 113 | integrity sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA== 114 | dependencies: 115 | accepts "~1.3.4" 116 | base64id "2.0.0" 117 | cookie "~0.4.1" 118 | debug "~4.1.0" 119 | engine.io-parser "~2.2.0" 120 | ws "~7.4.2" 121 | 122 | has-binary2@~1.0.2: 123 | version "1.0.3" 124 | resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" 125 | integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== 126 | dependencies: 127 | isarray "2.0.1" 128 | 129 | has-cors@1.1.0: 130 | version "1.1.0" 131 | resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" 132 | integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= 133 | 134 | indexof@0.0.1: 135 | version "0.0.1" 136 | resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" 137 | integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= 138 | 139 | isarray@2.0.1: 140 | version "2.0.1" 141 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" 142 | integrity sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ== 143 | 144 | micri@../../dist: 145 | version "0.0.0" 146 | 147 | mime-db@1.44.0: 148 | version "1.44.0" 149 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" 150 | integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== 151 | 152 | mime-types@~2.1.24: 153 | version "2.1.27" 154 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" 155 | integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== 156 | dependencies: 157 | mime-db "1.44.0" 158 | 159 | ms@2.0.0: 160 | version "2.0.0" 161 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 162 | integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== 163 | 164 | ms@^2.1.1: 165 | version "2.1.2" 166 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 167 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 168 | 169 | negotiator@0.6.2: 170 | version "0.6.2" 171 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" 172 | integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== 173 | 174 | parseqs@0.0.6: 175 | version "0.0.6" 176 | resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5" 177 | integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w== 178 | 179 | parseuri@0.0.6: 180 | version "0.0.6" 181 | resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a" 182 | integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow== 183 | 184 | socket.io-adapter@~1.1.0: 185 | version "1.1.2" 186 | resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz#ab3f0d6f66b8fc7fca3959ab5991f82221789be9" 187 | integrity sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g== 188 | 189 | socket.io-client@2.4.0: 190 | version "2.4.0" 191 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.4.0.tgz#aafb5d594a3c55a34355562fc8aea22ed9119a35" 192 | integrity sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ== 193 | dependencies: 194 | backo2 "1.0.2" 195 | component-bind "1.0.0" 196 | component-emitter "~1.3.0" 197 | debug "~3.1.0" 198 | engine.io-client "~3.5.0" 199 | has-binary2 "~1.0.2" 200 | indexof "0.0.1" 201 | parseqs "0.0.6" 202 | parseuri "0.0.6" 203 | socket.io-parser "~3.3.0" 204 | to-array "0.1.4" 205 | 206 | socket.io-parser@~3.3.0: 207 | version "3.3.3" 208 | resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.3.tgz#3a8b84823eba87f3f7624e64a8aaab6d6318a72f" 209 | integrity sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg== 210 | dependencies: 211 | component-emitter "~1.3.0" 212 | debug "~3.1.0" 213 | isarray "2.0.1" 214 | 215 | socket.io-parser@~3.4.0: 216 | version "3.4.1" 217 | resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.1.tgz#b06af838302975837eab2dc980037da24054d64a" 218 | integrity sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A== 219 | dependencies: 220 | component-emitter "1.2.1" 221 | debug "~4.1.0" 222 | isarray "2.0.1" 223 | 224 | socket.io@2.4.0: 225 | version "2.4.0" 226 | resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.4.0.tgz#01030a2727bd8eb2e85ea96d69f03692ee53d47e" 227 | integrity sha512-9UPJ1UTvKayuQfVv2IQ3k7tCQC/fboDyIK62i99dAQIyHKaBsNdTpwHLgKJ6guRWxRtC9H+138UwpaGuQO9uWQ== 228 | dependencies: 229 | debug "~4.1.0" 230 | engine.io "~3.5.0" 231 | has-binary2 "~1.0.2" 232 | socket.io-adapter "~1.1.0" 233 | socket.io-client "2.4.0" 234 | socket.io-parser "~3.4.0" 235 | 236 | to-array@0.1.4: 237 | version "0.1.4" 238 | resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" 239 | integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= 240 | 241 | ws@~7.4.2: 242 | version "7.4.6" 243 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" 244 | integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== 245 | 246 | xmlhttprequest-ssl@~1.5.4: 247 | version "1.5.5" 248 | resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" 249 | integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= 250 | 251 | yeast@0.1.2: 252 | version "0.1.2" 253 | resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" 254 | integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= 255 | -------------------------------------------------------------------------------- /examples/urlencoded-body-parsing/README.md: -------------------------------------------------------------------------------- 1 | Urlencoded Body Parsing Example 2 | =============================== 3 | 4 | Shows how to get urlencoded (html form post) data posted to your microservice 5 | using async/await. 6 | 7 | How to Use 8 | ---------- 9 | 10 | Install it and run: 11 | 12 | ```bash 13 | npm install 14 | npm run start 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/urlencoded-body-parsing/index.js: -------------------------------------------------------------------------------- 1 | const {serve} = require('micri'); 2 | const parse = require('urlencoded-body-parser'); 3 | 4 | serve(async req => { 5 | const data = await parse(req); 6 | console.log(data); 7 | 8 | return 'Data logged to your console'; 9 | }).listen(3000); 10 | -------------------------------------------------------------------------------- /examples/urlencoded-body-parsing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "urlencoded-body-parsing", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "node ." 7 | }, 8 | "dependencies": { 9 | "micri": "../../dist", 10 | "urlencoded-body-parser": "3.0.0" 11 | }, 12 | "author": "", 13 | "license": "MIT" 14 | } 15 | -------------------------------------------------------------------------------- /examples/urlencoded-body-parsing/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | bytes@3.1.0: 6 | version "3.1.0" 7 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" 8 | integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== 9 | 10 | depd@~1.1.2: 11 | version "1.1.2" 12 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 13 | integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= 14 | 15 | http-errors@1.7.3: 16 | version "1.7.3" 17 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" 18 | integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== 19 | dependencies: 20 | depd "~1.1.2" 21 | inherits "2.0.4" 22 | setprototypeof "1.1.1" 23 | statuses ">= 1.5.0 < 2" 24 | toidentifier "1.0.0" 25 | 26 | iconv-lite@0.4.24: 27 | version "0.4.24" 28 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 29 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 30 | dependencies: 31 | safer-buffer ">= 2.1.2 < 3" 32 | 33 | inherits@2.0.4: 34 | version "2.0.4" 35 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 36 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 37 | 38 | media-typer@^0.3.0: 39 | version "0.3.0" 40 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 41 | integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= 42 | 43 | micri@../../dist: 44 | version "0.0.0" 45 | 46 | qs@^6.2.1: 47 | version "6.9.4" 48 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" 49 | integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== 50 | 51 | raw-body@^2.1.7: 52 | version "2.4.1" 53 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" 54 | integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== 55 | dependencies: 56 | bytes "3.1.0" 57 | http-errors "1.7.3" 58 | iconv-lite "0.4.24" 59 | unpipe "1.0.0" 60 | 61 | "safer-buffer@>= 2.1.2 < 3": 62 | version "2.1.2" 63 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 64 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 65 | 66 | setprototypeof@1.1.1: 67 | version "1.1.1" 68 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" 69 | integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== 70 | 71 | "statuses@>= 1.5.0 < 2": 72 | version "1.5.0" 73 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 74 | integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= 75 | 76 | toidentifier@1.0.0: 77 | version "1.0.0" 78 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" 79 | integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== 80 | 81 | unpipe@1.0.0: 82 | version "1.0.0" 83 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 84 | integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= 85 | 86 | urlencoded-body-parser@3.0.0: 87 | version "3.0.0" 88 | resolved "https://registry.yarnpkg.com/urlencoded-body-parser/-/urlencoded-body-parser-3.0.0.tgz#e55329c6feccb2ede0fd0c027d8c105c063ba5e0" 89 | integrity sha512-Wv99/IIj6mZTawqmG2BOSgGYCRi3P/oW3LHNjTW+UWrDQ3sqRrONJ1Yed5REmUqI3E5uH3WCdLyOiq2JxrWwfw== 90 | dependencies: 91 | media-typer "^0.3.0" 92 | qs "^6.2.1" 93 | raw-body "^2.1.7" 94 | -------------------------------------------------------------------------------- /examples/with-graphql-request/README.md: -------------------------------------------------------------------------------- 1 | GraphQL Request Example 2 | ======================= 3 | 4 | Shows how to get data from a GraphQL endpoint using [GraphQL Request](https://github.com/graphcool/graphql-request). 5 | This example relies on [graph.cool](https://www.graph.cool) for its GraphQL backend. 6 | 7 | How to Use 8 | ---------- 9 | 10 | Install it and run: 11 | 12 | ```bash 13 | $ yarn install # (or `$ npm install`) 14 | $ yarn run start # (or `$ npm run start`) 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/with-graphql-request/index.js: -------------------------------------------------------------------------------- 1 | const {serve} = require('micri'); 2 | const {request} = require('graphql-request'); 3 | const endpoint = 'https://api.graph.cool/simple/v1/movies'; 4 | 5 | // Prepare simple query 6 | const query = ` 7 | query Movie($title: String!) { 8 | movie: Movie(title: $title) { 9 | releaseDate 10 | actors { 11 | name 12 | } 13 | } 14 | } 15 | `; 16 | 17 | serve(async () => { 18 | // Perform query 19 | const data = await request(endpoint, query, {title: 'Inception'}); 20 | 21 | // Return Movie 22 | return data.movie; 23 | }).listen(3000); 24 | -------------------------------------------------------------------------------- /examples/with-graphql-request/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-graphql-request", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "node ." 7 | }, 8 | "dependencies": { 9 | "graphql-request": "1.8.2", 10 | "micri": "../../dist" 11 | }, 12 | "license": "MIT" 13 | } 14 | -------------------------------------------------------------------------------- /examples/with-graphql-request/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | cross-fetch@2.2.2: 6 | version "2.2.2" 7 | resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-2.2.2.tgz#a47ff4f7fc712daba8f6a695a11c948440d45723" 8 | integrity sha1-pH/09/xxLauo9qaVoRyUhEDUVyM= 9 | dependencies: 10 | node-fetch "2.1.2" 11 | whatwg-fetch "2.0.4" 12 | 13 | graphql-request@1.8.2: 14 | version "1.8.2" 15 | resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-1.8.2.tgz#398d10ae15c585676741bde3fc01d5ca948f8fbe" 16 | integrity sha512-dDX2M+VMsxXFCmUX0Vo0TopIZIX4ggzOtiCsThgtrKR4niiaagsGTDIHj3fsOMFETpa064vzovI+4YV4QnMbcg== 17 | dependencies: 18 | cross-fetch "2.2.2" 19 | 20 | micri@../../dist: 21 | version "0.0.0" 22 | 23 | node-fetch@2.1.2: 24 | version "2.1.2" 25 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5" 26 | integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U= 27 | 28 | whatwg-fetch@2.0.4: 29 | version "2.0.4" 30 | resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" 31 | integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== 32 | -------------------------------------------------------------------------------- /examples/with-http2/README.md: -------------------------------------------------------------------------------- 1 | Micri App with HTTP/2 2 | ===================== 3 | 4 | Shows how to create a HTTP/2 server with Micri. However, done this way the 5 | feature set available is mostly limited to that of HTTP/1.1 API in Node.js. 6 | 7 | How to Use 8 | ---------- 9 | 10 | Install it and run: 11 | 12 | ```bash 13 | npm install 14 | npm run start 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/with-http2/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const http2 = require('http2'); 3 | const {run, send} = require('micri'); 4 | 5 | const micriHttp2 = fn => http2.createServer((req, res) => run(req, res, fn)); 6 | const server = micriHttp2(async (req, res) => { 7 | send(res, 200, 'Hello world!'); 8 | }); 9 | 10 | server.listen(3000); 11 | -------------------------------------------------------------------------------- /examples/with-http2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-http2", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "node ." 8 | }, 9 | "dependencies": { 10 | "micri": "../../dist" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/with-http2/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | micri@../../dist: 6 | version "0.0.0" 7 | -------------------------------------------------------------------------------- /examples/with-https/README.md: -------------------------------------------------------------------------------- 1 | Micri app with HTTPS 2 | ==================== 3 | 4 | Shows how to make use of HTTPS requests with `micri`. 5 | 6 | How to Use 7 | ---------- 8 | 9 | Install it and run: 10 | 11 | ```bash 12 | npm install 13 | npm run start 14 | ``` 15 | 16 | Optionally, create a new self-signed certificate: 17 | 18 | ``` 19 | $ openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 999999 -nodes 20 | Generating a RSA private key 21 | ...+++++ 22 | ........................................................+++++ 23 | writing new private key to 'key.pem' 24 | ----- 25 | You are about to be asked to enter information that will be incorporated 26 | into your certificate request. 27 | What you are about to enter is what is called a Distinguished Name or a DN. 28 | There are quite a few fields but you can leave some blank 29 | For some fields there will be a default value, 30 | If you enter '.', the field will be left blank. 31 | ----- 32 | Country Name (2 letter code) [XX]: 33 | State or Province Name (full name) []: 34 | Locality Name (eg, city) [Default City]: 35 | Organization Name (eg, company) [Default Company Ltd]: 36 | Organizational Unit Name (eg, section) []: 37 | Common Name (eg, your name or your server's hostname) []: 38 | Email Address []: 39 | ``` 40 | -------------------------------------------------------------------------------- /examples/with-https/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDZzCCAk+gAwIBAgIUA3qlAYwN+iiFdYn2OF5Ewj9g27EwDQYJKoZIhvcNAQEL 3 | BQAwQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE 4 | CgwTRGVmYXVsdCBDb21wYW55IEx0ZDAgFw0yMDA1MTcxMzEzNTNaGA80NzU4MDQx 5 | MzEzMTM1M1owQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEc 6 | MBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQAD 7 | ggEPADCCAQoCggEBAOOrBK42CoA4rX+t+Hx0raxovnJDv2TLeIiT/V3f4yZ+I1cY 8 | 5q3MS4aeQwiIINfAbMpkalW+NPpR/90izXPOQ8/JXwLlq/1eCZocM7f2DwZtYTSj 9 | IEtTgx7LbjwNXGeBJgcdMUkJkZXF3YdlaJoGlP94NjP192qHm8AKeGCQLyqb0TBJ 10 | 8qnLksQHEU4xNFdE3tfiOM946P91qxKa4fZW634FFr5HWte/6MCvT4CknKt3Hoca 11 | NS6MIpFw1Py9pk8t/3DhXOSPhAUSBMxn7B6nQD8L/fIxmSkn1JMSY2FWJfUIYD4K 12 | AcSw+pxbXiNd25vAyfmQY6BEqa18/j3h+9ktCwkCAwEAAaNTMFEwHQYDVR0OBBYE 13 | FGRNFPsyXoWLiYgBbfNiLJfXKI9lMB8GA1UdIwQYMBaAFGRNFPsyXoWLiYgBbfNi 14 | LJfXKI9lMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBANQG8LQ4 15 | HQ9UTC4ijP4UXG4WquXFIR1VqOTs2pCZatfYsVIwvPvL+uxUtiudm6hggPUTP2Nh 16 | WZ6TQwXctodJf51/audPnD6Z7jrz97O3tyJegTKPZHy/JmPOcVqp1j7KznsAqIUM 17 | Xe2r2Sm+qtuqKiyTuuhupG2gT3UquOdZ/IG9o+0ADtt/JBBs9cf1HbInk+gXYQNx 18 | 55vqtlvyqC9ycd3uFLAMmX4y+ItI7AkLpQQ34HPZNcjwVPmL75YsiSDwAOW37G4G 19 | HN0tQqfrtVn+WxvfoxhEwW4GJFsEz2185mFYN4OTlr29A7gIuzuQM4vG3noLeN3o 20 | FQCNEM7qO3WllfA= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /examples/with-https/index.js: -------------------------------------------------------------------------------- 1 | const {readFileSync} = require('fs'); 2 | const path = require('path'); 3 | const https = require('https'); 4 | const {run, send} = require('micri'); 5 | 6 | const PORT = process.env.PORT || 3443; 7 | const cert = readFileSync(path.join(__dirname, './cert.pem')); 8 | const key = readFileSync(path.join(__dirname, './key.pem')); 9 | 10 | const micriHttps = fn => https.createServer({ 11 | key, 12 | cert 13 | }, (req, res) => run(req, res, fn)); 14 | 15 | const server = micriHttps(async (req, res) => { 16 | send(res, 200, {encrypted: req.client.encrypted}); 17 | }); 18 | 19 | server.listen(PORT); 20 | console.log(`Listening on https://localhost:${PORT}`); 21 | -------------------------------------------------------------------------------- /examples/with-https/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDjqwSuNgqAOK1/ 3 | rfh8dK2saL5yQ79ky3iIk/1d3+MmfiNXGOatzEuGnkMIiCDXwGzKZGpVvjT6Uf/d 4 | Is1zzkPPyV8C5av9XgmaHDO39g8GbWE0oyBLU4Mey248DVxngSYHHTFJCZGVxd2H 5 | ZWiaBpT/eDYz9fdqh5vACnhgkC8qm9EwSfKpy5LEBxFOMTRXRN7X4jjPeOj/dasS 6 | muH2Vut+BRa+R1rXv+jAr0+ApJyrdx6HGjUujCKRcNT8vaZPLf9w4Vzkj4QFEgTM 7 | Z+wep0A/C/3yMZkpJ9STEmNhViX1CGA+CgHEsPqcW14jXdubwMn5kGOgRKmtfP49 8 | 4fvZLQsJAgMBAAECggEBAKCslYUNhqOvXGL7uJ69CcTft2iU/4J3Rv2809doprsH 9 | 0pEpdcO6pqK/FrfyupsJpr2/2Hvi3Si8rkK2ySCttZXznCZDSPprX4Nv2+A8u6Xh 10 | k/S+G5CNw7/7+jRPiaNSti/XHm1ZX3HATvO7UfNCKe2SPU62jIKWaglCWmpr153N 11 | PLXAYEpBkelsrfFKmYpyByKkD7obthTwqLiFjYVbCyRL4q2DlgQ78EAq+KdAzQxe 12 | ZXiQsQtS2MZchWkjPATRyLKn7h5GoPOq5pSWFrb9dMB1HvHIK3Wutvxd/bYK5Ltp 13 | 8CqgAgp/sVaIZG+leycBmxK2cDIiCVJJB0HPlJYNqbECgYEA/t/5rpxXW9zyMUtV 14 | K8O+kOfaC7wXNRcp3KbnpkPxAG8qY6XOFxEuLo6LpVuqnffooMSb5BQlzdUdHA6s 15 | 86e6rWE6una20jH7x/WqEewt39KHxbY0WtmImLAClWlYR24wOKe4eSQ+ldMzM7sW 16 | YeGWF+m+NAKo04NdhszfEXoSpgsCgYEA5KxMKKESBkR/CbBooFZa9/GbuSm5PdsE 17 | 81IjcorrjMqUydjrqZ5lsGMuy9nIT+F6KCyCBwB5kds6Cq6nZUalb6jJdK0vBWkx 18 | raSR0hDjLuMV8hoqM7fSLGTzAvh/XtXB1xtPXD7B5djjeORWvH9cHyn1ozQI69wL 19 | taNRpxo247sCgYAmS5LhaH47wEPnojEG5V9huGRrtPQrtvJERBO0SKsSbKGK3WPM 20 | 1XkB58sVqmNFXvVmCoR9zYUsmyPHjnDwHziOSnifgazQglAZaFEIZlsyMRjTXSAa 21 | smqyvmd0+aIZAXZfXNv737VDn7smv6qKRHHf3/ADMo8ZULihw3CU2KmqpwKBgEg0 22 | t5+krKJaAps2nvLyEat1GSzwpdOIUxnZ9+N1RkeguDa8Tj2zDH0T9HTeAchndEE3 23 | PJ+lYL3TsZIp8CWklxyY+XPkM5LL0qn36yuftXzaq9VUubq28qCeB7YKbX2ulZSs 24 | xPRlxKglQj6sK08+EAKvJycviRmg0/m45lGTj319AoGAXAOJo21DnwFF4yJY7/ab 25 | PZz3USeaHmjD95DF3slhI0ux4bWi/FFUBi0tmwUmC2O4I3xaPyMbFisiPH8MJ5m1 26 | 6kLp4EUrVXtmEs02k3HaV89l5UJ9IZ/+Fq+62O2i4I3qrOKLHB9JGzbHZE9499cd 27 | tKbrzEM0vaptCYfOeW+K+Ys= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /examples/with-https/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-https", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "node ." 8 | }, 9 | "dependencies": { 10 | "micri": "../../dist" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/with-https/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | micri@../../dist: 6 | version "0.0.0" 7 | -------------------------------------------------------------------------------- /examples/with-https2/README.md: -------------------------------------------------------------------------------- 1 | Micri App with HTTPS/2 2 | ====================== 3 | 4 | Shows how to create a HTTP/2 server with Micri. However, done this way the 5 | feature set available is mostly limited to that of HTTP/1.1 API in Node.js. 6 | 7 | This example also supports ALPN negotiation meaning that it can serve HTTP/1.1 8 | and HTTP/2 over the same socket, but only using TLS. 9 | 10 | How to Use 11 | ---------- 12 | 13 | Install it and run: 14 | 15 | ```bash 16 | npm install 17 | npm run start 18 | ``` 19 | 20 | Optionally, create a new self-signed certificate: 21 | 22 | ``` 23 | $ openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 999999 -nodes 24 | Generating a RSA private key 25 | ...+++++ 26 | ........................................................+++++ 27 | writing new private key to 'key.pem' 28 | ----- 29 | You are about to be asked to enter information that will be incorporated 30 | into your certificate request. 31 | What you are about to enter is what is called a Distinguished Name or a DN. 32 | There are quite a few fields but you can leave some blank 33 | For some fields there will be a default value, 34 | If you enter '.', the field will be left blank. 35 | ----- 36 | Country Name (2 letter code) [XX]: 37 | State or Province Name (full name) []: 38 | Locality Name (eg, city) [Default City]: 39 | Organization Name (eg, company) [Default Company Ltd]: 40 | Organizational Unit Name (eg, section) []: 41 | Common Name (eg, your name or your server's hostname) []: 42 | Email Address []: 43 | ``` 44 | -------------------------------------------------------------------------------- /examples/with-https2/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDZzCCAk+gAwIBAgIUA3qlAYwN+iiFdYn2OF5Ewj9g27EwDQYJKoZIhvcNAQEL 3 | BQAwQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE 4 | CgwTRGVmYXVsdCBDb21wYW55IEx0ZDAgFw0yMDA1MTcxMzEzNTNaGA80NzU4MDQx 5 | MzEzMTM1M1owQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEc 6 | MBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQAD 7 | ggEPADCCAQoCggEBAOOrBK42CoA4rX+t+Hx0raxovnJDv2TLeIiT/V3f4yZ+I1cY 8 | 5q3MS4aeQwiIINfAbMpkalW+NPpR/90izXPOQ8/JXwLlq/1eCZocM7f2DwZtYTSj 9 | IEtTgx7LbjwNXGeBJgcdMUkJkZXF3YdlaJoGlP94NjP192qHm8AKeGCQLyqb0TBJ 10 | 8qnLksQHEU4xNFdE3tfiOM946P91qxKa4fZW634FFr5HWte/6MCvT4CknKt3Hoca 11 | NS6MIpFw1Py9pk8t/3DhXOSPhAUSBMxn7B6nQD8L/fIxmSkn1JMSY2FWJfUIYD4K 12 | AcSw+pxbXiNd25vAyfmQY6BEqa18/j3h+9ktCwkCAwEAAaNTMFEwHQYDVR0OBBYE 13 | FGRNFPsyXoWLiYgBbfNiLJfXKI9lMB8GA1UdIwQYMBaAFGRNFPsyXoWLiYgBbfNi 14 | LJfXKI9lMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBANQG8LQ4 15 | HQ9UTC4ijP4UXG4WquXFIR1VqOTs2pCZatfYsVIwvPvL+uxUtiudm6hggPUTP2Nh 16 | WZ6TQwXctodJf51/audPnD6Z7jrz97O3tyJegTKPZHy/JmPOcVqp1j7KznsAqIUM 17 | Xe2r2Sm+qtuqKiyTuuhupG2gT3UquOdZ/IG9o+0ADtt/JBBs9cf1HbInk+gXYQNx 18 | 55vqtlvyqC9ycd3uFLAMmX4y+ItI7AkLpQQ34HPZNcjwVPmL75YsiSDwAOW37G4G 19 | HN0tQqfrtVn+WxvfoxhEwW4GJFsEz2185mFYN4OTlr29A7gIuzuQM4vG3noLeN3o 20 | FQCNEM7qO3WllfA= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /examples/with-https2/index.js: -------------------------------------------------------------------------------- 1 | const {readFileSync} = require('fs'); 2 | const path = require('path'); 3 | const { createSecureServer } = require('http2'); 4 | const {run, send} = require('micri'); 5 | 6 | const PORT = process.env.PORT || 4443; 7 | const cert = readFileSync(path.join(__dirname, './cert.pem')); 8 | const key = readFileSync(path.join(__dirname, './key.pem')); 9 | 10 | async function onRequest(req, res) { 11 | // Detects if it is a HTTPS request or HTTP/2 12 | const { socket: { alpnProtocol } } = req.httpVersion === '2.0' ? req.stream.session : req; 13 | 14 | send(res, 200, { 15 | alpnProtocol, 16 | httpVersion: req.httpVersion 17 | }); 18 | } 19 | 20 | createSecureServer( 21 | { cert, key, allowHTTP1: true }, 22 | (req, res) => run(req, res, onRequest) 23 | ).listen(PORT); 24 | -------------------------------------------------------------------------------- /examples/with-https2/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDjqwSuNgqAOK1/ 3 | rfh8dK2saL5yQ79ky3iIk/1d3+MmfiNXGOatzEuGnkMIiCDXwGzKZGpVvjT6Uf/d 4 | Is1zzkPPyV8C5av9XgmaHDO39g8GbWE0oyBLU4Mey248DVxngSYHHTFJCZGVxd2H 5 | ZWiaBpT/eDYz9fdqh5vACnhgkC8qm9EwSfKpy5LEBxFOMTRXRN7X4jjPeOj/dasS 6 | muH2Vut+BRa+R1rXv+jAr0+ApJyrdx6HGjUujCKRcNT8vaZPLf9w4Vzkj4QFEgTM 7 | Z+wep0A/C/3yMZkpJ9STEmNhViX1CGA+CgHEsPqcW14jXdubwMn5kGOgRKmtfP49 8 | 4fvZLQsJAgMBAAECggEBAKCslYUNhqOvXGL7uJ69CcTft2iU/4J3Rv2809doprsH 9 | 0pEpdcO6pqK/FrfyupsJpr2/2Hvi3Si8rkK2ySCttZXznCZDSPprX4Nv2+A8u6Xh 10 | k/S+G5CNw7/7+jRPiaNSti/XHm1ZX3HATvO7UfNCKe2SPU62jIKWaglCWmpr153N 11 | PLXAYEpBkelsrfFKmYpyByKkD7obthTwqLiFjYVbCyRL4q2DlgQ78EAq+KdAzQxe 12 | ZXiQsQtS2MZchWkjPATRyLKn7h5GoPOq5pSWFrb9dMB1HvHIK3Wutvxd/bYK5Ltp 13 | 8CqgAgp/sVaIZG+leycBmxK2cDIiCVJJB0HPlJYNqbECgYEA/t/5rpxXW9zyMUtV 14 | K8O+kOfaC7wXNRcp3KbnpkPxAG8qY6XOFxEuLo6LpVuqnffooMSb5BQlzdUdHA6s 15 | 86e6rWE6una20jH7x/WqEewt39KHxbY0WtmImLAClWlYR24wOKe4eSQ+ldMzM7sW 16 | YeGWF+m+NAKo04NdhszfEXoSpgsCgYEA5KxMKKESBkR/CbBooFZa9/GbuSm5PdsE 17 | 81IjcorrjMqUydjrqZ5lsGMuy9nIT+F6KCyCBwB5kds6Cq6nZUalb6jJdK0vBWkx 18 | raSR0hDjLuMV8hoqM7fSLGTzAvh/XtXB1xtPXD7B5djjeORWvH9cHyn1ozQI69wL 19 | taNRpxo247sCgYAmS5LhaH47wEPnojEG5V9huGRrtPQrtvJERBO0SKsSbKGK3WPM 20 | 1XkB58sVqmNFXvVmCoR9zYUsmyPHjnDwHziOSnifgazQglAZaFEIZlsyMRjTXSAa 21 | smqyvmd0+aIZAXZfXNv737VDn7smv6qKRHHf3/ADMo8ZULihw3CU2KmqpwKBgEg0 22 | t5+krKJaAps2nvLyEat1GSzwpdOIUxnZ9+N1RkeguDa8Tj2zDH0T9HTeAchndEE3 23 | PJ+lYL3TsZIp8CWklxyY+XPkM5LL0qn36yuftXzaq9VUubq28qCeB7YKbX2ulZSs 24 | xPRlxKglQj6sK08+EAKvJycviRmg0/m45lGTj319AoGAXAOJo21DnwFF4yJY7/ab 25 | PZz3USeaHmjD95DF3slhI0ux4bWi/FFUBi0tmwUmC2O4I3xaPyMbFisiPH8MJ5m1 26 | 6kLp4EUrVXtmEs02k3HaV89l5UJ9IZ/+Fq+62O2i4I3qrOKLHB9JGzbHZE9499cd 27 | tKbrzEM0vaptCYfOeW+K+Ys= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /examples/with-https2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-http2", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "node ." 8 | }, 9 | "dependencies": { 10 | "micri": "../../dist" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/with-https2/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | micri@../../dist: 6 | version "0.0.0" 7 | -------------------------------------------------------------------------------- /examples/with-worker/.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | -------------------------------------------------------------------------------- /examples/with-worker/README.md: -------------------------------------------------------------------------------- 1 | Worker Threads 2 | ============== 3 | 4 | Two concurrent requests running a CPU intensive request method in the 5 | main thread: 6 | 7 | ``` 8 | ~% time curl 127.0.0.1:3000/main 9 | 299993curl 127.0.0.1:3000/main 0.01s user 0.00s system 0% cpu 8.791 total 10 | ~% time curl 127.0.0.1:3000/main 11 | 299993curl 127.0.0.1:3000/main 0.00s user 0.00s system 0% cpu 16.547 total 12 | ``` 13 | 14 | Two concurrent requests running a CPU intensive request method in worker 15 | threads: 16 | 17 | ``` 18 | ~% time curl 127.0.0.1:3000/worker 19 | 299993curl 127.0.0.1:3000/worker 0.00s user 0.00s system 0% cpu 9.025 total 20 | ~% time curl 127.0.0.1:3000/worker 21 | 299993curl 127.0.0.1:3000/worker 0.00s user 0.00s system 0% cpu 9.026 total 22 | ``` 23 | -------------------------------------------------------------------------------- /examples/with-worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "router", 3 | "description": "Router example", 4 | "main": "lib/index.js", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "node lib/" 8 | }, 9 | "dependencies": { 10 | "micri": "../../dist" 11 | }, 12 | "devDependencies": { 13 | "@types/node": "12.7.11", 14 | "typescript": "3.6.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/with-worker/src/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { parse } from 'url'; 3 | import micri, { 4 | IncomingMessage, 5 | ServerResponse, 6 | MicriHandler, 7 | Router, 8 | send, 9 | withWorker 10 | } from 'micri'; 11 | import prime from './prime'; 12 | 13 | const { router, on, otherwise } = Router; 14 | 15 | const parsePath = (req: IncomingMessage): string => parse(req.url || '/').path || '/'; 16 | const withCustomOpts = (hndl: MicriHandler): MicriHandler => 17 | (req: IncomingMessage, res: ServerResponse) => 18 | hndl(req, res, { optX: 'Hello world!' }); 19 | 20 | micri(router( 21 | on.get((req: IncomingMessage) => parsePath(req) === '/main', prime), 22 | on.get((req: IncomingMessage) => parsePath(req) === '/worker', withWorker(path.join(__dirname, './prime.js'))), 23 | on.get((req: IncomingMessage) => parsePath(req) === '/stream', withWorker(path.join(__dirname, './stream.js'))), 24 | on.get((req: IncomingMessage) => parsePath(req) === '/opts', withCustomOpts(withWorker(path.join(__dirname, './opts.js')))), 25 | otherwise((_req: IncomingMessage, res: ServerResponse) => send(res, 400, 'Method Not Accepted')))) 26 | .listen(3000); 27 | -------------------------------------------------------------------------------- /examples/with-worker/src/opts.ts: -------------------------------------------------------------------------------- 1 | export default function fn(_req: any, _res: any, { optX }: { optX: string }) { 2 | return optX; 3 | } 4 | -------------------------------------------------------------------------------- /examples/with-worker/src/prime.ts: -------------------------------------------------------------------------------- 1 | export default function fn() { 2 | const limit = 3e5; 3 | let r; 4 | 5 | console.log('This is going to take a while...'); 6 | 7 | for (let n = 2; n <= limit; n++) { 8 | let isPrime = true; 9 | for(let factor = 2; factor < n; factor++) { 10 | if(n % factor == 0) { 11 | isPrime = false; 12 | break; 13 | } 14 | } 15 | if(isPrime) { 16 | r = n; 17 | } 18 | } 19 | 20 | return r; 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-worker/src/stream.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from "http"; 2 | 3 | function getNumber() { 4 | return new Promise((resolve) => { 5 | setTimeout(() => { 6 | const x = Math.round(Math.random() * 100); 7 | 8 | resolve(x); 9 | }, 300); 10 | }); 11 | } 12 | 13 | export default async function wrkStream(_req: IncomingMessage, res: ServerResponse) { 14 | res.writeHead(200, { 15 | 'X-Custom-Header': "I'm a header" 16 | }); 17 | 18 | for (let i = 0; i < 30; i++) { 19 | const x = await getNumber(); 20 | res.write(`${x}\n`); 21 | } 22 | res.end(); 23 | } 24 | -------------------------------------------------------------------------------- /examples/with-worker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "lib": ["esnext"], 6 | "module": "CommonJS", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "strictNullChecks": true, 10 | "noImplicitAny": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "outDir": "./lib", 14 | "strict": true, 15 | "target": "ES2018", 16 | "inlineSourceMap": true, 17 | "types": ["node"], 18 | "typeRoots": [ 19 | "./node_modules/@types" 20 | ] 21 | }, 22 | "include": ["./src"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/with-worker/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/node@12.7.11": 6 | version "12.7.11" 7 | resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.11.tgz#be879b52031cfb5d295b047f5462d8ef1a716446" 8 | integrity sha512-Otxmr2rrZLKRYIybtdG/sgeO+tHY20GxeDjcGmUnmmlCWyEnv2a2x1ZXBo3BTec4OiTXMQCiazB8NMBf0iRlFw== 9 | 10 | micri@../../dist: 11 | version "0.0.0" 12 | 13 | typescript@3.6.3: 14 | version "3.6.3" 15 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.3.tgz#fea942fabb20f7e1ca7164ff626f1a9f3f70b4da" 16 | integrity sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw== 17 | -------------------------------------------------------------------------------- /lib/worker-wrapper.js: -------------------------------------------------------------------------------- 1 | const handlerAgent = require('handler-agent'); 2 | const { request } = require('http'); 3 | const { run } = require('./index'); 4 | const { parentPort, workerData } = require('worker_threads'); 5 | 6 | module.exports = async function wrap(fn) { 7 | try { 8 | const buf = await new Promise((resolve) => { 9 | parentPort.on('message', (body) => resolve(body)); 10 | }); 11 | 12 | const agent = handlerAgent((req, res) => run(req, res, (req, res) => fn(req, res, workerData.opts))); 13 | const intReq = request( 14 | `http://127.0.0.1${workerData.req.url}`, 15 | { 16 | agent, 17 | hostname: '127.0.0.1', 18 | port: 1337, 19 | method: workerData.req.method, 20 | headers: { 21 | ...workerData.req.headers, 22 | 'Content-Length': buf.length, 23 | }, 24 | }, 25 | (res) => { 26 | const head = { 27 | statusCode: res.statusCode || 200, 28 | statusMessage: res.statusMessage, 29 | headers: res.headers, 30 | }; 31 | 32 | parentPort.postMessage(head); 33 | 34 | res.on('data', (chunk) => { 35 | parentPort.postMessage(chunk); 36 | }); 37 | res.on('end', () => { 38 | process.exit(0); 39 | }); 40 | } 41 | ); 42 | intReq.on('error', (err) => { 43 | console.error(err); 44 | process.exit(1); 45 | }); 46 | 47 | intReq.write(Buffer.from(buf)); 48 | intReq.end(); 49 | } catch (err) { 50 | console.error(err); 51 | process.exit(1); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "micri", 3 | "version": "4.5.1", 4 | "description": "Asynchronous HTTP microservices", 5 | "main": "./dist/index.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "scripts": { 10 | "build": "rm -rf ./dist && ncc build -m -s ./src/index.ts && cp ./lib/worker-wrapper.js ./dist/", 11 | "test": "npm run lint && NODE_ENV=test jest", 12 | "lint": "eslint --ext .ts ./src", 13 | "git-pre-commit": "eslint --ext .ts ./src", 14 | "prepublish": "npm run build", 15 | "prettier": "prettier --write './{src,test}/**/*.ts' && prettier --write './lib/*.js'" 16 | }, 17 | "engines": { 18 | "node": ">= 12.0.0" 19 | }, 20 | "repository": "turist-cloud/micri", 21 | "keywords": [ 22 | "micro", 23 | "micri", 24 | "service", 25 | "microservice", 26 | "API" 27 | ], 28 | "license": "MIT", 29 | "devDependencies": { 30 | "@types/bytes": "3.1.0", 31 | "@types/content-type": "1.1.3", 32 | "@types/jest": "28.1.4", 33 | "@types/node": "13.9.1", 34 | "@types/test-listen": "1.1.0", 35 | "@typescript-eslint/eslint-plugin": "5.30.5", 36 | "@typescript-eslint/parser": "5.30.5", 37 | "@vercel/ncc": "0.34.0", 38 | "@zeit/fetch-retry": "4.0.1", 39 | "@zeit/git-hooks": "0.1.4", 40 | "bytes": "3.1.2", 41 | "content-type": "1.0.4", 42 | "eslint": "8.19.0", 43 | "eslint-config-prettier": "8.5.0", 44 | "eslint-plugin-prettier": "4.2.1", 45 | "jest": "28.1.2", 46 | "node-fetch": "2.6.7", 47 | "prettier": "2.7.1", 48 | "raw-body": "2.5.1", 49 | "resumer": "0.0.0", 50 | "test-listen": "1.1.0", 51 | "ts-jest": "28.0.5", 52 | "typescript": "4.7.4" 53 | }, 54 | "dependencies": { 55 | "handler-agent": "0.2.0" 56 | }, 57 | "jest": { 58 | "preset": "ts-jest", 59 | "testMatch": [ 60 | "**/test/**/*.test.ts" 61 | ], 62 | "collectCoverage": true, 63 | "collectCoverageFrom": [ 64 | "src/*.ts" 65 | ], 66 | "verbose": true 67 | }, 68 | "git": { 69 | "pre-commit": "lint-staged" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/body.ts: -------------------------------------------------------------------------------- 1 | // Native 2 | import { IncomingMessage } from 'http'; 3 | 4 | // Packages 5 | import contentType from 'content-type'; 6 | import getRawBody from 'raw-body'; 7 | 8 | // Utilities 9 | import { IncomingOpts } from './types'; 10 | import { MicriError, MicriBodyError } from './errors'; 11 | 12 | // Maps requests to buffered raw bodies so that 13 | // multiple calls to `json` work as expected 14 | const rawBodyMap = new WeakMap(); 15 | 16 | export async function buffer( 17 | req: IncomingMessage, 18 | { limit = '1mb' }: IncomingOpts = { limit: '1mb' } 19 | ): Promise { 20 | const length = req.headers['content-length']; 21 | 22 | const body = rawBodyMap.get(req); 23 | if (body) { 24 | return body; 25 | } 26 | 27 | try { 28 | const buf = await getRawBody(req, { limit, length }); 29 | rawBodyMap.set(req, buf); 30 | 31 | return buf; 32 | } catch (err: any) { 33 | throw new MicriBodyError(err, limit); 34 | } 35 | } 36 | 37 | export async function text( 38 | req: IncomingMessage, 39 | { limit = '1mb', encoding }: IncomingOpts = { limit: '1mb' } 40 | ): Promise { 41 | const type = req.headers['content-type'] || 'text/plain'; 42 | const length = req.headers['content-length']; 43 | 44 | const body = rawBodyMap.get(req); 45 | if (body) { 46 | return body; 47 | } 48 | 49 | // eslint-disable-next-line no-undefined 50 | if (encoding === undefined) { 51 | encoding = contentType.parse(type).parameters.charset; 52 | } 53 | 54 | try { 55 | const buf = await getRawBody(req, { limit, length, encoding }); 56 | rawBodyMap.set(req, buf); 57 | 58 | // toString() shouldn't be needed here but it doesn't hurt 59 | return buf.toString(); 60 | } catch (err: any) { 61 | throw new MicriBodyError(err, limit); 62 | } 63 | } 64 | 65 | function parseJSON(str: string): ReturnType { 66 | try { 67 | return JSON.parse(str); 68 | } catch (err: any) { 69 | throw new MicriError(400, 'invalid_json', 'Invalid JSON', err); 70 | } 71 | } 72 | 73 | export function json(req: IncomingMessage, opts?: IncomingOpts): Promise { 74 | return text(req, opts).then((body) => parseJSON(body)); 75 | } 76 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | // Packages 2 | import bytes from 'bytes'; 3 | import { RawBodyError } from './types'; 4 | 5 | export class MicriError extends Error { 6 | statusCode: number; 7 | code: string; 8 | originalError: Error | null; 9 | 10 | constructor(statusCode: number, code: string, message: string, originalError?: Error) { 11 | super(message); 12 | 13 | this.statusCode = statusCode; 14 | this.code = code; 15 | this.originalError = originalError || null; 16 | } 17 | } 18 | 19 | export class MicriBodyError extends MicriError { 20 | constructor(err: RawBodyError, limit: string | number) { 21 | let statusCode = 400; 22 | let code = 'invalid_body'; 23 | let message = 'Invalid body'; 24 | 25 | if (err.type === 'entity.too.large') { 26 | statusCode = 413; 27 | code = 'request_entity_too_large'; 28 | message = `Body exceeded ${typeof limit === 'string' ? limit : bytes(limit)} limit`; 29 | } 30 | 31 | super(statusCode, code, message, err); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Utilities 2 | import { serve } from './serve'; 3 | import * as Router from './router'; 4 | import withWorker from './with-worker'; 5 | 6 | export default serve; 7 | export * from './body'; 8 | export * from './serve'; 9 | export * from './types'; 10 | export { Router }; 11 | export { withWorker }; 12 | export { MicriError } from './errors'; 13 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | // Native 2 | import { METHODS } from 'http'; 3 | 4 | // Utilities 5 | import { MicriHandler, IncomingMessage, ServerResponse } from './types'; 6 | 7 | type Predicate = (req: IncomingMessage, res: ServerResponse, opts?: OptsType) => boolean; 8 | type OnFunction = ( 9 | pred: Predicate, 10 | hndl: MicriHandler 11 | ) => [Predicate, MicriHandler]; 12 | 13 | const router = 14 | (...rest: [Predicate, MicriHandler][]): MicriHandler => 15 | (req: IncomingMessage, res: ServerResponse, opts?: OptsType): any => 16 | (rest.find((route) => route[0](req, res, opts)) || [ 17 | null, 18 | (): void => { 19 | throw Error('No matching route was found'); 20 | }, 21 | ])[1](req, res, opts); 22 | 23 | const onInit: { [index: string]: OnFunction } = {}; 24 | const on = METHODS.map((method) => [ 25 | method.toLowerCase(), 26 | (pred: Predicate, fn: MicriHandler): [Predicate, MicriHandler] => [ 27 | (req, res, opts): boolean => req.method === method && pred(req, res, opts), 28 | fn, 29 | ], 30 | ]).reduce((acc: { [index: string]: OnFunction }, curr: any) => ({ ...acc, ...{ [curr[0]]: curr[1] } }), onInit); 31 | const otherwise = (fn: MicriHandler): [Predicate, MicriHandler] => [ 32 | (): boolean => true, 33 | fn, 34 | ]; 35 | 36 | function everyPredicate(...t: Predicate[]): Predicate { 37 | return (req: IncomingMessage, res: ServerResponse, opts?: OptsType): boolean => 38 | t.every((f): boolean => f(req, res, opts)); 39 | } 40 | 41 | export { Predicate, router, on, otherwise, everyPredicate }; 42 | -------------------------------------------------------------------------------- /src/serve.ts: -------------------------------------------------------------------------------- 1 | // Native 2 | import { IncomingMessage, ServerResponse, IncomingHttpHeaders, Server, createServer } from 'http'; 3 | 4 | // Packages 5 | import contentType from 'content-type'; 6 | 7 | // Utilities 8 | import { MicriHandler } from './types'; 9 | import { MicriError } from './errors'; 10 | 11 | const { NODE_ENV } = process.env; 12 | const DEV = NODE_ENV === 'development'; 13 | const jsonStringify = DEV 14 | ? (obj: any): string => JSON.stringify(obj, null, 2) 15 | : (obj: any): string => JSON.stringify(obj); 16 | 17 | const isStream = (data: any) => !!data && typeof data.pipe === 'function'; 18 | 19 | export function send(res: ServerResponse, statusCode: number, obj: any = null) { 20 | res.statusCode = statusCode; 21 | 22 | if (obj === null) { 23 | res.end(); 24 | return; 25 | } 26 | 27 | if (Buffer.isBuffer(obj)) { 28 | if (!res.getHeader('Content-Type')) { 29 | res.setHeader('Content-Type', 'application/octet-stream'); 30 | } 31 | 32 | res.setHeader('Content-Length', obj.length); 33 | res.end(obj); 34 | return; 35 | } 36 | 37 | if (isStream(obj)) { 38 | if (!res.getHeader('Content-Type')) { 39 | res.setHeader('Content-Type', 'application/octet-stream'); 40 | } 41 | 42 | obj.pipe(res); 43 | return; 44 | } 45 | 46 | let str: string = obj; 47 | const typeObj = typeof obj; 48 | 49 | if (typeObj === 'object' || typeObj === 'number' || typeObj === 'boolean') { 50 | // We stringify before setting the header 51 | // in case `JSON.stringify` throws and a 52 | // 500 has to be sent instead 53 | str = jsonStringify(obj); 54 | 55 | if (!res.getHeader('Content-Type')) { 56 | res.setHeader('Content-Type', 'application/json; charset=utf-8'); 57 | } 58 | } else { 59 | if (!res.getHeader('Content-Type')) { 60 | res.setHeader('Content-Type', 'text/plain; charset=utf-8'); 61 | } 62 | } 63 | 64 | res.setHeader('Content-Length', Buffer.byteLength(str)); 65 | res.end(str); 66 | } 67 | 68 | function isAcceptJson(headers: IncomingHttpHeaders) { 69 | const { accept } = headers; 70 | let type = '*/*'; 71 | 72 | try { 73 | const ct = contentType.parse(accept || type); 74 | type = ct.type || '*/*'; 75 | } catch (err) { 76 | // NOP 77 | } 78 | 79 | return type === '*/*' || accept === 'application/json'; 80 | } 81 | 82 | export const sendError = (req: IncomingMessage, res: ServerResponse, errorObj: MicriError | Error) => { 83 | const acceptJson = isAcceptJson(req.headers); 84 | let statusCode = 500; 85 | let body: any = acceptJson 86 | ? { 87 | error: { 88 | code: 'internal_server_error', 89 | message: 'Internal Server Error', 90 | }, 91 | } 92 | : 'Internal Server Error'; 93 | 94 | if (errorObj instanceof MicriError) { 95 | statusCode = errorObj.statusCode || 500; 96 | const code = errorObj.code || 'internal_server_error'; 97 | 98 | if (acceptJson) { 99 | if (DEV) { 100 | body = { 101 | error: { 102 | code, 103 | message: errorObj.message, 104 | stack: errorObj.stack, 105 | originalError: errorObj.originalError || null, 106 | }, 107 | }; 108 | } else { 109 | const message = errorObj.message || 'Internal Server Error'; 110 | 111 | body = { 112 | error: { 113 | code, 114 | message, 115 | }, 116 | }; 117 | } 118 | } else { 119 | const message = errorObj.message || 'Internal Server Error'; 120 | 121 | body = DEV ? errorObj.stack : message; 122 | } 123 | } else if (errorObj instanceof Error) { 124 | console.error(errorObj); 125 | } else { 126 | console.warn('thrown error must be an instance Error'); 127 | } 128 | 129 | send(res, statusCode, body); 130 | }; 131 | 132 | export function run(req: IncomingMessage, res: ServerResponse, fn: MicriHandler) { 133 | return new Promise((resolve) => resolve(fn(req, res))) 134 | .then((val) => { 135 | if (val === null) { 136 | send(res, 204, null); 137 | return; 138 | } 139 | 140 | // Send value if it is not undefined, otherwise assume res.end 141 | // will be called later 142 | // eslint-disable-next-line no-undefined 143 | if (val !== undefined) { 144 | send(res, res.statusCode || 200, val); 145 | } 146 | }) 147 | .catch((err) => sendError(req, res, err)); 148 | } 149 | 150 | export const serve = (fn: MicriHandler): Server => 151 | createServer((req: IncomingMessage, res: ServerResponse) => run(req, res, fn)); 152 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // Native 2 | import { IncomingMessage, ServerResponse, IncomingHttpHeaders, Server } from 'http'; 3 | 4 | export type MicriHandler = (req: IncomingMessage, res: ServerResponse, opts?: OptsType) => any; 5 | export { IncomingMessage, ServerResponse, IncomingHttpHeaders, Server }; 6 | export interface IncomingOpts { 7 | limit?: string | number; 8 | encoding?: string | null; 9 | } 10 | 11 | // The following type is borrowed from raw-body to avoid depending on the whole 12 | // library twice just for proper typing. 13 | export interface RawBodyError extends Error { 14 | /** 15 | * The limit in bytes. 16 | */ 17 | limit?: number; 18 | /** 19 | * The expected length of the stream. 20 | */ 21 | length?: number; 22 | expected?: number; 23 | /** 24 | * The received bytes. 25 | */ 26 | received?: number; 27 | /** 28 | * The encoding. 29 | */ 30 | encoding?: string; 31 | /** 32 | * The corresponding status code for the error. 33 | */ 34 | status: number; 35 | statusCode: number; 36 | /** 37 | * The error type. 38 | */ 39 | type: string; 40 | } 41 | -------------------------------------------------------------------------------- /src/with-worker.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { IncomingMessage, ServerResponse } from 'http'; 3 | import { buffer } from './body'; 4 | import { 5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 6 | // @ts-ignore 7 | SHARE_ENV, 8 | Worker, 9 | isMainThread, 10 | } from 'worker_threads'; 11 | 12 | export interface WorkerResponseHeader { 13 | statusCode: number; 14 | statusMessage?: string; 15 | headers: { 16 | [index: string]: string | string[] | undefined; 17 | }; 18 | } 19 | 20 | export default function withWorker( 21 | handlerPath: string, 22 | workerOpts?: { 23 | eval?: boolean; // eval handlerPath as code 24 | limit?: string; // limit the body size 25 | env?: { [index: string]: string | undefined }; 26 | } 27 | ) { 28 | if (!isMainThread) { 29 | throw new Error('withWorker() can be only used in the main thread'); 30 | } 31 | 32 | const trampoline = workerOpts?.eval 33 | ? `require('${path.join(__dirname, './worker-wrapper')}')(${handlerPath})` 34 | : `const p=require('${handlerPath}'); require('${path.join(__dirname, './worker-wrapper')}')(p.default || p)`; 35 | 36 | return async (req: IncomingMessage, res: ServerResponse, opts: OptsType) => { 37 | const body = await buffer(req, { limit: workerOpts?.limit ?? undefined }); 38 | 39 | return new Promise((resolve, reject) => { 40 | const worker = new Worker(trampoline, { 41 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 42 | // @ts-ignore 43 | env: workerOpts?.env || SHARE_ENV, 44 | eval: true, 45 | workerData: { 46 | req: { 47 | method: req.method, 48 | url: req.url, 49 | headers: req.headers, 50 | // Trailers not supported 51 | }, 52 | opts, 53 | }, 54 | }); 55 | let writeFn = (msg: WorkerResponseHeader) => { 56 | res.writeHead(msg.statusCode, msg.headers); 57 | if (msg.statusMessage) { 58 | res.statusMessage = msg.statusMessage; 59 | } 60 | 61 | // Switch to writing the response body after the headers have 62 | // been received. 63 | writeFn = (msg) => { 64 | res.write(Buffer.from(msg)); 65 | }; 66 | }; 67 | worker.on('message', (chunk: any) => { 68 | writeFn(chunk); 69 | }); 70 | worker.on('error', reject); 71 | worker.on('exit', (code: number) => { 72 | if (code !== 0) { 73 | reject(new Error(`Worker stopped with exit code ${code}`)); 74 | } else { 75 | res.end(); 76 | resolve(); 77 | } 78 | }); 79 | worker.postMessage(body); 80 | }); 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /test/basic.test.ts: -------------------------------------------------------------------------------- 1 | const fetch = require('@zeit/fetch-retry')(require('node-fetch')); 2 | const resumer = require('resumer'); 3 | import listen from 'test-listen'; 4 | import micri from '../src/'; 5 | import { 6 | IncomingMessage, 7 | ServerResponse, 8 | Server, 9 | MicriError, 10 | MicriHandler, 11 | run, 12 | send, 13 | sendError, 14 | buffer, 15 | text, 16 | json, 17 | } from '../src/'; 18 | import { setTimeout } from 'timers'; 19 | 20 | const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); 21 | 22 | let srv: Server; 23 | 24 | const getUrl = (fn: (req: IncomingMessage, res: ServerResponse) => any) => { 25 | srv = micri(fn); 26 | 27 | return listen(srv); 28 | }; 29 | 30 | afterEach(() => { 31 | if (srv && srv.close) { 32 | srv.close(); 33 | } 34 | }); 35 | 36 | test('send(200, )', async () => { 37 | const fn = async (_: IncomingMessage, res: ServerResponse) => { 38 | send(res, 200, 'woot'); 39 | }; 40 | 41 | const url = await getUrl(fn); 42 | const res = await fetch(url); 43 | const body = await res.text(); 44 | 45 | expect(res.status).toBe(200); 46 | expect(body).toBe('woot'); 47 | }); 48 | 49 | test('send(200, )', async () => { 50 | const fn = async (_: IncomingMessage, res: ServerResponse) => { 51 | send(res, 200, { 52 | a: 'b', 53 | }); 54 | }; 55 | 56 | const url = await getUrl(fn); 57 | const res = await fetch(url); 58 | const body = await res.json(); 59 | 60 | expect(res.status).toBe(200); 61 | expect(body).toEqual({ 62 | a: 'b', 63 | }); 64 | }); 65 | 66 | test('send(200, )', async () => { 67 | const fn = async (_: IncomingMessage, res: ServerResponse) => { 68 | // Chosen by fair dice roll. guaranteed to be random. 69 | send(res, 200, 4); 70 | }; 71 | 72 | const url = await getUrl(fn); 73 | const res = await fetch(url); 74 | const body = await res.json(); 75 | 76 | expect(res.status).toBe(200); 77 | expect(body).toBe(4); 78 | }); 79 | 80 | test('send(200, )', async () => { 81 | const fn = async (_: IncomingMessage, res: ServerResponse) => { 82 | send(res, 200, Buffer.from('muscle')); 83 | }; 84 | 85 | const url = await getUrl(fn); 86 | const res = await fetch(url); 87 | const body = await res.text(); 88 | 89 | expect(res.status).toBe(200); 90 | expect(body).toBe('muscle'); 91 | }); 92 | 93 | test('send(200, )', async () => { 94 | const fn = async (_: IncomingMessage, res: ServerResponse) => { 95 | send(res, 200, 'waterfall'); 96 | }; 97 | 98 | const url = await getUrl(fn); 99 | const res = await fetch(url); 100 | const body = await res.text(); 101 | 102 | expect(res.status).toBe(200); 103 | expect(body).toBe('waterfall'); 104 | }); 105 | 106 | test('send()', async () => { 107 | const fn = async (_: IncomingMessage, res: ServerResponse) => { 108 | send(res, 404); 109 | }; 110 | 111 | const url = await getUrl(fn); 112 | const res = await fetch(url); 113 | 114 | expect(res.status).toBe(404); 115 | }); 116 | 117 | test('return ', async () => { 118 | const fn = async () => 'woot'; 119 | 120 | const url = await getUrl(fn); 121 | const res = await fetch(url); 122 | const body = await res.text(); 123 | 124 | expect(res.status).toBe(200); 125 | expect(body).toBe('woot'); 126 | }); 127 | 128 | test('return ', async () => { 129 | const fn = async () => 130 | new Promise(async (resolve) => { 131 | await sleep(100); 132 | resolve('I Promise'); 133 | }); 134 | 135 | const url = await getUrl(fn); 136 | const res = await fetch(url); 137 | const body = await res.text(); 138 | 139 | expect(res.status).toBe(200); 140 | expect(body).toBe('I Promise'); 141 | }); 142 | 143 | test('sync return ', async () => { 144 | const fn = () => 'argon'; 145 | 146 | const url = await getUrl(fn); 147 | const res = await fetch(url); 148 | const body = await res.text(); 149 | 150 | expect(res.status).toBe(200); 151 | expect(body).toBe('argon'); 152 | }); 153 | 154 | test('return empty string', async () => { 155 | const fn = async () => ''; 156 | 157 | const url = await getUrl(fn); 158 | const res = await fetch(url); 159 | const body = await res.text(); 160 | 161 | expect(res.status).toBe(200); 162 | expect(body).toBe(''); 163 | }); 164 | 165 | test('return ', async () => { 166 | const fn = async () => ({ 167 | a: 'b', 168 | }); 169 | 170 | const url = await getUrl(fn); 171 | const res = await fetch(url); 172 | const body = await res.json(); 173 | 174 | expect(res.status).toBe(200); 175 | expect(body).toEqual({ 176 | a: 'b', 177 | }); 178 | }); 179 | 180 | test('return ', async () => { 181 | const fn = async () => { 182 | // Chosen by fair dice roll. guaranteed to be random. 183 | return 4; 184 | }; 185 | 186 | const url = await getUrl(fn); 187 | const res = await fetch(url); 188 | const body = await res.json(); 189 | 190 | expect(res.status).toBe(200); 191 | expect(body).toBe(4); 192 | }); 193 | 194 | test('return ', async () => { 195 | const fn = async () => Buffer.from('Hammer'); 196 | 197 | const url = await getUrl(fn); 198 | const res = await fetch(url); 199 | const body = await res.text(); 200 | 201 | expect(res.status).toBe(200); 202 | expect(body).toBe('Hammer'); 203 | }); 204 | 205 | test('return ', async () => { 206 | const fn = async () => resumer().queue('River').end(); 207 | 208 | const url = await getUrl(fn); 209 | const res = await fetch(url); 210 | const body = await res.text(); 211 | 212 | expect(res.status).toBe(200); 213 | expect(body).toBe('River'); 214 | }); 215 | 216 | test('return ', async () => { 217 | const fn = async () => null; 218 | 219 | const url = await getUrl(fn); 220 | const res = await fetch(url); 221 | const body = await res.text(); 222 | 223 | expect(res.status).toBe(204); 224 | expect(body).toBe(''); 225 | }); 226 | 227 | test('return calls res.end once', async () => { 228 | const fn = async () => null; 229 | 230 | let i = 0; 231 | // @ts-ignore 232 | await run({}, { end: () => i++ }, fn); 233 | 234 | expect(i).toBe(1); 235 | }); 236 | 237 | test('throw sends 500 and json body', async () => { 238 | const fn = () => { 239 | throw new Error('Error from test (expected)'); 240 | }; 241 | 242 | const url = await getUrl(fn); 243 | const res = await fetch(url); 244 | const body = await res.json(); 245 | 246 | expect(res.status).toBe(500); 247 | expect(res.headers.get('content-type')).toBe('application/json; charset=utf-8'); 248 | expect(body).toEqual({ 249 | error: { 250 | code: 'internal_server_error', 251 | message: 'Internal Server Error', 252 | }, 253 | }); 254 | }); 255 | 256 | test('throw sends 500 and text body when requested', async () => { 257 | const fn = () => { 258 | throw new Error('Error from test (expected)'); 259 | }; 260 | 261 | const url = await getUrl(fn); 262 | const res = await fetch(url, { 263 | headers: { 264 | accept: 'text/plain', 265 | }, 266 | }); 267 | const body = await res.text(); 268 | 269 | expect(res.status).toBe(500); 270 | expect(body).toBe('Internal Server Error'); 271 | }); 272 | 273 | test('throw with statusCode sends 500', async () => { 274 | class MyError extends Error { 275 | statusCode: number | undefined; 276 | } 277 | 278 | const fn = async () => { 279 | await sleep(100); 280 | const err = new MyError('Error from test (expected)'); 281 | err.statusCode = 402; 282 | throw err; 283 | }; 284 | 285 | const url = await getUrl(fn); 286 | const res = await fetch(url); 287 | 288 | expect(res.status).toBe(500); 289 | }); 290 | 291 | test('throw with MicriError', async () => { 292 | const fn = async () => { 293 | await sleep(100); 294 | throw new MicriError(402, 'error', 'Error from test (expected)'); 295 | }; 296 | 297 | const url = await getUrl(fn); 298 | const res = await fetch(url); 299 | 300 | expect(res.status).toBe(402); 301 | }); 302 | 303 | test('throw (500)', async () => { 304 | const fn = async () => { 305 | throw new Error('500 from test (expected)'); 306 | }; 307 | 308 | const url = await getUrl(fn); 309 | const res = await fetch(url); 310 | 311 | expect(res.status).toBe(500); 312 | }); 313 | 314 | test('throw (500) sync', async () => { 315 | const fn = () => { 316 | throw new Error('500 from test (expected)'); 317 | }; 318 | 319 | const url = await getUrl(fn); 320 | const res = await fetch(url); 321 | 322 | expect(res.status).toBe(500); 323 | }); 324 | 325 | test('send(200, ) with error on same tick', async () => { 326 | const fn = async (_: IncomingMessage, res: ServerResponse) => { 327 | const stream = resumer().queue('error-stream'); 328 | send(res, 200, stream); 329 | 330 | stream.emit('error', new Error('500 from test (expected)')); 331 | stream.end(); 332 | }; 333 | 334 | const url = await getUrl(fn); 335 | const res = await fetch(url); 336 | 337 | expect(res.status).toBe(500); 338 | }); 339 | 340 | test('custom error', async () => { 341 | const fn = () => { 342 | sleep(50); 343 | throw new Error('500 from test (expected)'); 344 | }; 345 | 346 | const handleErrors = (ofn: MicriHandler) => (req: IncomingMessage, res: ServerResponse) => { 347 | try { 348 | return ofn(req, res); 349 | } catch (err: any) { 350 | send(res, 400, 'My custom error!'); 351 | } 352 | }; 353 | 354 | const url = await getUrl(handleErrors(fn)); 355 | const res = await fetch(url); 356 | const body = await res.text(); 357 | 358 | expect(res.status).toBe(400); 359 | expect(body).toBe('My custom error!'); 360 | }); 361 | 362 | test('custom async error', async () => { 363 | const fn = async () => { 364 | sleep(50); 365 | throw new Error('500 from test (expected)'); 366 | }; 367 | 368 | const handleErrors = (ofn: MicriHandler) => async (req: IncomingMessage, res: ServerResponse) => { 369 | try { 370 | return await ofn(req, res); 371 | } catch (err: any) { 372 | send(res, 400, 'My custom error!'); 373 | } 374 | }; 375 | 376 | const url = await getUrl(handleErrors(fn)); 377 | const res = await fetch(url); 378 | const body = await res.text(); 379 | 380 | expect(res.status).toBe(400); 381 | expect(body).toBe('My custom error!'); 382 | }); 383 | 384 | test('json parse error', async () => { 385 | const fn = async (req: IncomingMessage, res: ServerResponse) => { 386 | const body = await json(req); 387 | send(res, 200, body.woot); 388 | }; 389 | 390 | const url = await getUrl(fn); 391 | const res = await fetch(url, { 392 | method: 'POST', 393 | headers: { 394 | 'Content-Type': 'application/json', 395 | }, 396 | body: '{ "bad json" }', 397 | }); 398 | 399 | expect(res.status).toBe(400); 400 | }); 401 | 402 | test('json', async () => { 403 | const fn = async (req: IncomingMessage, res: ServerResponse) => { 404 | const body = await json(req); 405 | 406 | send(res, 200, { 407 | response: body.some.cool, 408 | }); 409 | }; 410 | 411 | const url = await getUrl(fn); 412 | const res = await fetch(url, { 413 | method: 'POST', 414 | body: JSON.stringify({ 415 | some: { 416 | cool: 'json', 417 | }, 418 | }), 419 | }); 420 | const body = await res.json(); 421 | 422 | expect(res.status).toBe(200); 423 | expect(body.response).toBe('json'); 424 | }); 425 | 426 | test('json limit (below)', async () => { 427 | const fn = async (req: IncomingMessage, res: ServerResponse) => { 428 | const body = await json(req, { 429 | limit: 100, 430 | }); 431 | 432 | send(res, 200, { 433 | response: body.some.cool, 434 | }); 435 | }; 436 | 437 | const url = await getUrl(fn); 438 | const res = await fetch(url, { 439 | method: 'POST', 440 | body: JSON.stringify({ 441 | some: { 442 | cool: 'json', 443 | }, 444 | }), 445 | }); 446 | const body = await res.json(); 447 | 448 | expect(res.status).toBe(200); 449 | expect(body.response).toBe('json'); 450 | }); 451 | 452 | test('json limit (over)', async () => { 453 | const fn = async (req: IncomingMessage, res: ServerResponse) => { 454 | try { 455 | await json(req, { 456 | limit: 3, 457 | }); 458 | } catch (err: any) { 459 | expect(err.statusCode).toBe(413); 460 | throw err; 461 | } 462 | 463 | send(res, 200, 'ok'); 464 | }; 465 | 466 | const url = await getUrl(fn); 467 | const res = await fetch(url, { 468 | method: 'POST', 469 | body: JSON.stringify({ 470 | some: { 471 | cool: 'json', 472 | }, 473 | }), 474 | }); 475 | 476 | expect(res.status).toBe(413); 477 | }); 478 | 479 | test('json circular', async () => { 480 | const fn = async (_: IncomingMessage, res: ServerResponse) => { 481 | const obj = { 482 | circular: true, 483 | }; 484 | 485 | // @ts-ignore 486 | obj.obj = obj; 487 | send(res, 200, obj); 488 | }; 489 | 490 | const url = await getUrl(fn); 491 | const res = await fetch(url); 492 | 493 | expect(res.status).toBe(500); 494 | }); 495 | 496 | test('no async', async () => { 497 | const fn = (_: IncomingMessage, res: ServerResponse) => { 498 | send(res, 200, { 499 | a: 'b', 500 | }); 501 | }; 502 | 503 | const url = await getUrl(fn); 504 | const res = await fetch(url); 505 | const obj = await res.json(); 506 | 507 | expect(res.status).toBe(200); 508 | expect(obj.a).toBe('b'); 509 | }); 510 | 511 | test('limit included in error', async () => { 512 | const fn = async (req: IncomingMessage, res: ServerResponse) => { 513 | let body: any; 514 | 515 | try { 516 | body = await json(req, { 517 | limit: 3, 518 | }); 519 | } catch (err: any) { 520 | expect(err.message).toEqual(expect.stringContaining('exceeded 3B limit')); 521 | throw err; 522 | } 523 | 524 | send(res, 200, { 525 | response: body.some.cool, 526 | }); 527 | }; 528 | 529 | const url = await getUrl(fn); 530 | const res = await fetch(url, { 531 | method: 'POST', 532 | body: JSON.stringify({ 533 | some: { 534 | cool: 'json', 535 | }, 536 | }), 537 | }); 538 | 539 | expect(res.status).toBe(413); 540 | }); 541 | 542 | test('support for status fallback in errors', async () => { 543 | const fn = (req: IncomingMessage, res: ServerResponse) => { 544 | const err = new MicriError(403, 'xyz', 'Custom'); 545 | sendError(req, res, err); 546 | }; 547 | 548 | const url = await getUrl(fn); 549 | const res = await fetch(url); 550 | 551 | expect(res.status).toBe(403); 552 | }); 553 | 554 | test('support for non-Error errors', async () => { 555 | const fn = (req: IncomingMessage, res: ServerResponse) => { 556 | const err = 'String error'; 557 | // @ts-ignore 558 | sendError(req, res, err); 559 | }; 560 | 561 | const url = await getUrl(fn); 562 | const res = await fetch(url); 563 | 564 | expect(res.status).toBe(500); 565 | }); 566 | 567 | test('json from rawBodyMap works', async () => { 568 | const fn = async (req: IncomingMessage, res: ServerResponse) => { 569 | const bodyOne = await json(req); 570 | const bodyTwo = await json(req); 571 | 572 | expect(bodyOne).toEqual(bodyTwo); 573 | 574 | send(res, 200, { 575 | response: bodyOne.some.cool, 576 | }); 577 | }; 578 | 579 | const url = await getUrl(fn); 580 | const res = await fetch(url, { 581 | method: 'POST', 582 | body: JSON.stringify({ 583 | some: { 584 | cool: 'json', 585 | }, 586 | }), 587 | }); 588 | const body = await res.json(); 589 | 590 | expect(res.status).toBe(200); 591 | expect(body.response).toBe('json'); 592 | }); 593 | 594 | test('statusCode defaults to 200', async () => { 595 | const fn = (_: IncomingMessage, res: ServerResponse) => { 596 | // eslint-disable-next-line no-undefined 597 | // @ts-ignore 598 | res.statusCode = undefined; 599 | return 'woot'; 600 | }; 601 | 602 | const url = await getUrl(fn); 603 | const res = await fetch(url); 604 | const body = await res.text(); 605 | 606 | expect(res.status).toBe(200); 607 | expect(body).toBe('woot'); 608 | }); 609 | 610 | test('statusCode on response works', async () => { 611 | const fn = async (_: IncomingMessage, res: ServerResponse) => { 612 | res.statusCode = 400; 613 | return 'woot'; 614 | }; 615 | 616 | const url = await getUrl(fn); 617 | const res = await fetch(url); 618 | 619 | expect(res.status).toBe(400); 620 | }); 621 | 622 | test('Content-Type header is preserved on string', async () => { 623 | const fn = async (_: IncomingMessage, res: ServerResponse) => { 624 | res.setHeader('Content-Type', 'text/html'); 625 | return 'woot'; 626 | }; 627 | 628 | const url = await getUrl(fn); 629 | const res = await fetch(url); 630 | 631 | expect(res.status).toBe(200); 632 | expect(res.headers.get('content-type')).toBe('text/html'); 633 | }); 634 | 635 | test('Content-Type header is preserved on stream', async () => { 636 | const fn = async (_: IncomingMessage, res: ServerResponse) => { 637 | res.setHeader('Content-Type', 'text/html'); 638 | return resumer().queue('River').end(); 639 | }; 640 | 641 | const url = await getUrl(fn); 642 | const res = await fetch(url); 643 | 644 | expect(res.status).toBe(200); 645 | expect(res.headers.get('content-type')).toBe('text/html'); 646 | }); 647 | 648 | test('Content-Type header is preserved on buffer', async () => { 649 | const fn = async (_: IncomingMessage, res: ServerResponse) => { 650 | res.setHeader('Content-Type', 'text/html'); 651 | return Buffer.from('hello'); 652 | }; 653 | 654 | const url = await getUrl(fn); 655 | const res = await fetch(url); 656 | 657 | expect(res.status).toBe(200); 658 | expect(res.headers.get('content-type')).toBe('text/html'); 659 | }); 660 | 661 | test('Content-Type header is preserved on object', async () => { 662 | const fn = async (_: IncomingMessage, res: ServerResponse) => { 663 | res.setHeader('Content-Type', 'text/html'); 664 | return {}; 665 | }; 666 | 667 | const url = await getUrl(fn); 668 | const res = await fetch(url); 669 | 670 | expect(res.status).toBe(200); 671 | expect(res.headers.get('content-type')).toBe('text/html'); 672 | }); 673 | 674 | test('res.end is working', async () => { 675 | const fn = (_: IncomingMessage, res: ServerResponse) => { 676 | setTimeout(() => res.end('woot'), 100); 677 | }; 678 | 679 | const url = await getUrl(fn); 680 | const res = await fetch(url); 681 | const body = await res.text(); 682 | 683 | expect(res.status).toBe(200); 684 | expect(body).toBe('woot'); 685 | }); 686 | 687 | test('json should throw 400 on empty body with no headers', async () => { 688 | const fn = async (req: IncomingMessage) => json(req); 689 | 690 | const url = await getUrl(fn); 691 | const res = await fetch(url); 692 | const body = await res.json(); 693 | 694 | expect(res.status).toBe(400); 695 | expect(body.error.message).toBe('Invalid JSON'); 696 | }); 697 | 698 | test('text should throw 400 on invalid encoding', async () => { 699 | const fn = async (req: IncomingMessage) => text(req, { encoding: 'lol' }); 700 | 701 | const url = await getUrl(fn); 702 | const res = await fetch(url, { 703 | method: 'POST', 704 | body: '❤️', 705 | }); 706 | const body = await res.json(); 707 | 708 | expect(res.status).toBe(400); 709 | expect(body.error.message).toBe('Invalid body'); 710 | }); 711 | 712 | test('buffer works', async () => { 713 | const fn = async (req: IncomingMessage) => buffer(req); 714 | const url = await getUrl(fn); 715 | const res = await fetch(url, { 716 | method: 'POST', 717 | body: '❤️', 718 | }); 719 | const body = await res.text(); 720 | 721 | expect(body).toBe('❤️'); 722 | }); 723 | 724 | test('buffer cacheing works', async () => { 725 | const fn = async (req: IncomingMessage) => { 726 | const buf1 = await buffer(req); 727 | const buf2 = await buffer(req); 728 | 729 | expect(buf2).toBe(buf1); 730 | 731 | return ''; 732 | }; 733 | const url = await getUrl(fn); 734 | const res = await fetch(url, { 735 | method: 'POST', 736 | body: '❤️', 737 | }); 738 | 739 | expect(res.status).toBe(200); 740 | }); 741 | 742 | test("buffer doesn't care about client encoding", async () => { 743 | const fn = async (req: IncomingMessage) => buffer(req); 744 | const url = await getUrl(fn); 745 | const res = await fetch(url, { 746 | method: 'POST', 747 | headers: { 748 | 'content-type': 'application/json; charset=base64', 749 | }, 750 | body: '❤️', 751 | }); 752 | const body = await res.text(); 753 | 754 | expect(body).toBe('❤️'); 755 | }); 756 | 757 | test('buffer should throw when limit is exceeded', async () => { 758 | const fn = async (req: IncomingMessage) => buffer(req, { limit: 1 }); 759 | const url = await getUrl(fn); 760 | const res = await fetch(url, { 761 | method: 'POST', 762 | headers: { 763 | 'content-type': 'application/json; charset=base64', 764 | }, 765 | body: '❤️', 766 | }); 767 | 768 | expect(res.status).toBe(413); 769 | }); 770 | 771 | test('Content-Type header for JSON is set', async () => { 772 | const url = await getUrl(() => ({})); 773 | const res = await fetch(url); 774 | 775 | expect(res.headers.get('content-type')).toBe('application/json; charset=utf-8'); 776 | }); 777 | -------------------------------------------------------------------------------- /test/development.test.ts: -------------------------------------------------------------------------------- 1 | // Packages 2 | const fetch = require('@zeit/fetch-retry')(require('node-fetch')); 3 | import listen from 'test-listen'; 4 | 5 | process.env.NODE_ENV = 'development'; 6 | import micri, { MicriError, Server, IncomingMessage, ServerResponse } from '../src/'; 7 | 8 | let srv: Server; 9 | 10 | const getUrl = (fn: (req: IncomingMessage, res: ServerResponse) => any) => { 11 | srv = micri(fn); 12 | 13 | return listen(srv); 14 | }; 15 | 16 | afterEach(() => { 17 | if (srv && srv.close) { 18 | srv.close(); 19 | } 20 | }); 21 | 22 | test('send(200, ) is pretty-printed', async () => { 23 | const fn = () => ({ woot: 'yes' }); 24 | 25 | const url = await getUrl(fn); 26 | const res = await fetch(url); 27 | const body = await res.text(); 28 | 29 | expect(body).toEqual('{\n "woot": "yes"\n}'); 30 | }); 31 | 32 | test('sendError sends Internal Server Error with Error', async () => { 33 | const fn = () => { 34 | throw new Error('Custom'); 35 | }; 36 | 37 | const url = await getUrl(fn); 38 | const res = await fetch(url, { 39 | headers: { 40 | Accept: 'application/json', 41 | }, 42 | }); 43 | const body = await res.json(); 44 | 45 | expect(res.status).toEqual(500); 46 | expect(body.error.message).toEqual('Internal Server Error'); 47 | }); 48 | 49 | test('sendError sends plain text Internal Server Error with Error', async () => { 50 | const fn = () => { 51 | throw new Error('Custom'); 52 | }; 53 | 54 | const url = await getUrl(fn); 55 | const res = await fetch(url, { 56 | headers: { 57 | Accept: 'text/plain', 58 | }, 59 | }); 60 | const body = await res.text(); 61 | 62 | expect(res.status).toEqual(500); 63 | expect(body).toEqual('Internal Server Error'); 64 | }); 65 | 66 | test('sendError shows stack in development with statusCode', async () => { 67 | const fn = () => { 68 | throw new MicriError(503, 'test', 'Custom'); 69 | }; 70 | 71 | const url = await getUrl(fn); 72 | const res = await fetch(url); 73 | const body = await res.json(); 74 | 75 | expect(res.status).toEqual(503); 76 | expect(body.error.code).toEqual('test'); 77 | expect(body.error.message).toEqual('Custom'); 78 | expect(body.error.stack).toEqual(expect.stringContaining('at fn')); 79 | }); 80 | 81 | test('sendError shows plain text stack in development with statusCode', async () => { 82 | const fn = () => { 83 | throw new MicriError(503, 'test', 'Custom'); 84 | }; 85 | 86 | const url = await getUrl(fn); 87 | const res = await fetch(url, { 88 | headers: { 89 | accept: 'text/plain', 90 | }, 91 | }); 92 | const body = await res.text(); 93 | 94 | expect(res.status).toEqual(503); 95 | expect(body).toEqual(expect.stringContaining('Custom')); 96 | expect(body).toEqual(expect.stringContaining('at fn')); 97 | }); 98 | -------------------------------------------------------------------------------- /test/router.test.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, Router } from '../src'; 2 | 3 | const { router, on, otherwise } = Router; 4 | 5 | class Message extends IncomingMessage { 6 | method: string; 7 | url: string; 8 | 9 | constructor(method: string, url: string) { 10 | // @ts-ignore We don't need to care 11 | super(null); 12 | 13 | this.method = method; 14 | this.url = url; 15 | } 16 | } 17 | 18 | test('predicate is called and truthy return value selects the route', () => { 19 | const msg = new Message('GET', '/'); 20 | const opts = {}; 21 | const predicate = jest.fn(() => true); 22 | const getHandler = jest.fn(() => 'hello'); 23 | const r = router(on.get(predicate, getHandler)); 24 | 25 | // @ts-ignore the other args are not consumed 26 | const body = r(msg, null, opts); 27 | 28 | expect(predicate).toHaveBeenCalledWith(msg, null, opts); 29 | expect(getHandler).toHaveBeenCalledWith(msg, null, opts); 30 | expect(getHandler).toHaveBeenCalledTimes(1); 31 | expect(body).toBe('hello'); 32 | }); 33 | 34 | test('predicate is called and falsy return value skips the route', () => { 35 | const msg = new Message('GET', '/'); 36 | const opts = {}; 37 | const predicate = jest.fn(() => false); 38 | const getHandler = jest.fn(() => 'hello'); 39 | const otherwiseHandler = jest.fn(() => 'hi'); 40 | const r = router(on.get(predicate, getHandler), otherwise(otherwiseHandler)); 41 | 42 | // @ts-ignore the other args are not consumed 43 | const body = r(msg, null, opts); 44 | 45 | expect(predicate).toHaveBeenCalledWith(msg, null, opts); 46 | expect(getHandler).toHaveBeenCalledTimes(0); 47 | expect(otherwiseHandler).toHaveBeenCalledWith(msg, null, opts); 48 | expect(otherwiseHandler).toHaveBeenCalledTimes(1); 49 | expect(body).toBe('hi'); 50 | }); 51 | 52 | test('throws if nothing matches', () => { 53 | const msg = new Message('GET', '/'); 54 | const opts = {}; 55 | const predicate1 = jest.fn(() => false); 56 | const predicate2 = jest.fn((req: IncomingMessage) => req.url === '/lol'); 57 | const getHandler = jest.fn(() => 'hello'); 58 | const r = router(on.get(predicate1, getHandler), on.get(predicate2, getHandler)); 59 | 60 | // @ts-ignore the other args are not consumed 61 | expect(() => r(msg, null, opts)).toThrow(); 62 | }); 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "lib": ["esnext"], 6 | "module": "CommonJS", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "strictNullChecks": true, 10 | "noImplicitAny": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "removeComments": true, 14 | "strict": true, 15 | "target": "ES2018", 16 | "inlineSourceMap": true, 17 | "types": ["jest","node"], 18 | "typeRoots": [ 19 | "./types", 20 | "./node_modules/@types" 21 | ] 22 | }, 23 | "include": ["./src", "./types"] 24 | } 25 | --------------------------------------------------------------------------------