diff --git a/.gitignore b/.gitignore index a6c6005e8..56074c756 100644 --- a/.gitignore +++ b/.gitignore @@ -104,6 +104,7 @@ fabric.properties # auto-generated tag files tags ======= - -.idea/ -manual_test/.serverless + +.idea/ +.serverless +.dynamodb diff --git a/README.md b/README.md index aeb5b9d2d..194afbfe4 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ This plugin is updated by its users, I just do maintenance and ensure that PRs a * [Custom headers](#custom-headers) * [Environment variables](#environment-variables) * [AWS API Gateway features](#aws-api-gateway-features) +* [WebSocket](#websocket) * [Usage with Webpack](#usage-with-webpack) * [Velocity nuances](#velocity-nuances) * [Debug process](#debug-process) @@ -358,6 +359,31 @@ resources: To disable the model validation you can use `--disableModelValidation`. +## WebSocket +`This has experimental functionality. Please report any bugs or missing features.` + +serverless-offline suports running a WebSocket local endpoint. The `port` used is one port above the HTTP port. +By default, HTTP port is 3000 and hence WebSocket port is 3001. + +Usage in order to send messages back to clients: + +`POST http://localhost:{port+1}/@connections/{connectionId}` + +Or, + +``` +let endpoint=event.apiGatewayUrl; +if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; +const apiVersion='2018-11-29'; +const apiGM=new API.ApiGatewayManagementApi({ apiVersion, endpoint }); +apiGM.postToConnection({ConnectionId, Data}); +``` +Where the `event` is received in the lambda hanlder function. + +There's support for `websocketsApiRouteSelectionExpression` in it's basic form: `$request.body.x.y.z`, where the default value is `$request.body.action`. + +Authorizers and WSS:// are currectly not supoprted in serverless-offline. + ## Usage with Webpack Use [serverless-webpack](https://github.com/serverless-heaven/serverless-webpack) to compile and bundle your ES-next code diff --git a/manual_test_websocket/README.md b/manual_test_websocket/README.md index d1ad7bddb..7eb75bafd 100644 --- a/manual_test_websocket/README.md +++ b/manual_test_websocket/README.md @@ -8,6 +8,7 @@ Set AWS credentials, e.g.: `export AWS_PROFILE=...` To start AWS DynamoDB locally (can run only after first deploying locally): `sls dynamodb install` `sls dynamodb start` +### In either main or RouteSelection folder the following: ## Deploying locally @@ -26,28 +27,24 @@ To start AWS DynamoDB locally (can run only after first deploying locally): `sls ## Testing on AWS -`npm --endpoint={WebSocket endpoint URL on AWS} run test` +`npm --endpoint={WebSocket endpoint URL on AWS} --timeout={timeout in ms} run test` -## Usage Assumption - In order to send messages back to clients -`const newAWSApiGatewayManagementApi=(event, context)=>{` +## Usage in order to send messages back to clients + +`POST http://localhost:3001/@connections/{connectionId}` -` const endpoint=event.requestContext.domainName+'/'+event.requestContext.stage;` +Or, -` const apiVersion='2018-11-29';` +`let endpoint=event.apiGatewayUrl;` -` let API=context.API;` +`if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage;` -` if (!process.env.IS_OFFLINE) {` +`const apiVersion='2018-11-29';` -` API = require('aws-sdk');` +`const apiGM=new API.ApiGatewayManagementApi({ apiVersion, endpoint });` -` require('aws-sdk/clients/apigatewaymanagementapi');` +`apiGM.postToConnection({ConnectionId, Data});` -` }` - -` return new API.ApiGatewayManagementApi({ apiVersion, endpoint });` - -`};` diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/handler.js b/manual_test_websocket/manual_test_websocket_RouteSelection/handler.js new file mode 100644 index 000000000..e8da78fd7 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_RouteSelection/handler.js @@ -0,0 +1,37 @@ +'use strict'; + +const AWS = require('aws-sdk'); + + +const successfullResponse = { + statusCode: 200, + body: 'Request is OK.' +}; + +const errorResponse = { + statusCode: 400, + body: 'Request is not OK.' +}; + +module.exports.echo = async (event, context) => { + const action = JSON.parse(event.body); + + await sendToClient(action.message, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)); + return successfullResponse; +}; + +const newAWSApiGatewayManagementApi=(event, context)=>{ + let endpoint=event.apiGatewayUrl; + + if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; + const apiVersion='2018-11-29'; + return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); +}; + +const sendToClient = (data, connectionId, apigwManagementApi) => { + // console.log(`sendToClient:${connectionId}`); + let sendee=data; + if ('object'==typeof data) sendee=JSON.stringify(data); + + return apigwManagementApi.postToConnection({ConnectionId: connectionId, Data: sendee}).promise(); +}; diff --git a/manual_test_websocket/package-lock.json b/manual_test_websocket/manual_test_websocket_RouteSelection/package-lock.json similarity index 82% rename from manual_test_websocket/package-lock.json rename to manual_test_websocket/manual_test_websocket_RouteSelection/package-lock.json index acb22d0c2..ba31f9664 100644 --- a/manual_test_websocket/package-lock.json +++ b/manual_test_websocket/manual_test_websocket_RouteSelection/package-lock.json @@ -4,6 +4,34 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/chai": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", + "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", + "dev": true + }, + "@types/cookiejar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz", + "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==", + "dev": true + }, + "@types/node": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz", + "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==", + "dev": true + }, + "@types/superagent": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.7.tgz", + "integrity": "sha512-9KhCkyXv268A2nZ1Wvu7rQWM+BmdYUVkycFeNnYrUL5Zwu7o8wPQ3wBfW59dDP+wuoxw0ww8YKgTNv8j/cgscA==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, "ansi-colors": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", @@ -46,10 +74,16 @@ "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, "aws-sdk": { - "version": "2.449.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.449.0.tgz", - "integrity": "sha512-ywvqLoBUlibAkud+A3eXZbGv6pBZwqb/DolYvJJR834E8Dvp8+bYZY1+gCDe9a5hp15ICb2jD+vOM2W6ljUlHw==", + "version": "2.473.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.473.0.tgz", + "integrity": "sha512-1Qr16lOcz4ANzl/oPQRR+fxchfvUx4PVQhUNnDU3FH9OBfU3Xj+Vh6bGYFbreFQgqIqXUTEuJR5pC44uK70YfA==", "requires": { "buffer": "4.9.1", "events": "1.1.1", @@ -62,6 +96,18 @@ "xml2js": "0.4.19" } }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "awscred": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/awscred/-/awscred-1.4.2.tgz", + "integrity": "sha512-j3Vehf6PCFzzPZKkzEcj0Y2QO8w8UBbgobnl3DwHMiAE9A2mfJxTkq3cX4UNWHmrTAR0rj5BC/ts90Ok4Pg6rw==", + "dev": true + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -134,6 +180,21 @@ "type-detect": "^4.0.5" } }, + "chai-http": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-4.3.0.tgz", + "integrity": "sha512-zFTxlN7HLMv+7+SPXZdkd5wUlK+KxH6Q7bIEMiEx0FK3zuuMqL7cwICAQ0V1+yYRozBburYuxN1qZstgHpFZQg==", + "dev": true, + "requires": { + "@types/chai": "4", + "@types/superagent": "^3.8.3", + "cookiejar": "^2.1.1", + "is-ip": "^2.0.0", + "methods": "^1.1.2", + "qs": "^6.5.1", + "superagent": "^3.7.0" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -194,12 +255,39 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -246,6 +334,12 @@ "object-keys": "^1.0.12" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -336,6 +430,12 @@ "strip-eof": "^1.0.0" } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -354,6 +454,23 @@ "is-buffer": "~2.0.3" } }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -479,6 +596,12 @@ "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", "dev": true }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, "is": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/is/-/is-0.2.7.tgz", @@ -509,6 +632,15 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, + "is-ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", + "integrity": "sha1-aO6gfooKCpTC0IDdZ0xzGrKkYas=", + "dev": true, + "requires": { + "ip-regex": "^2.0.0" + } + }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -613,6 +745,33 @@ "p-is-promise": "^2.0.0" } }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "dev": true + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "dev": true, + "requires": { + "mime-db": "1.40.0" + } + }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -674,6 +833,12 @@ "yargs-unparser": "1.5.0" } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==", + "dev": true + }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -852,6 +1017,12 @@ "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", "dev": true }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, "progress": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", @@ -873,11 +1044,32 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -908,6 +1100,12 @@ "node.flow": "1.2.3" } }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "sax": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", @@ -974,6 +1172,15 @@ "strip-ansi": "^4.0.0" } }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -995,6 +1202,24 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + } + }, "supports-color": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", @@ -1030,6 +1255,12 @@ "querystring": "0.2.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", @@ -1113,12 +1344,12 @@ "dev": true }, "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.0.0.tgz", + "integrity": "sha512-cknCal4k0EAOrh1SHHPPWWh4qm93g1IuGGGwBjWkXmCG7LsDtL8w9w+YVfaF+KSVwiHQKDIMsSLBVftKf9d1pg==", "dev": true, "requires": { - "async-limiter": "~1.0.0" + "async-limiter": "^1.0.0" } }, "xml2js": { diff --git a/manual_test_websocket/package.json b/manual_test_websocket/manual_test_websocket_RouteSelection/package.json similarity index 76% rename from manual_test_websocket/package.json rename to manual_test_websocket/manual_test_websocket_RouteSelection/package.json index a74701349..e2e5c1f63 100644 --- a/manual_test_websocket/package.json +++ b/manual_test_websocket/manual_test_websocket_RouteSelection/package.json @@ -13,12 +13,16 @@ "author": "", "license": "MIT", "dependencies": { - "aws-sdk": "^2.449.0" + "aws-sdk": "^2.473.0" }, "devDependencies": { + "aws4": "^1.8.0", + "awscred": "^1.4.2", "chai": "^4.2.0", + "chai-http": "^4.3.0", "mocha": "^6.1.4", + "moment": "^2.24.0", "serverless-dynamodb-local": "^0.2.37", - "ws": "^6.2.1" + "ws": "^7.0.0" } } diff --git a/manual_test_websocket/scripts/deploy_to_aws.sh b/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_aws.sh similarity index 100% rename from manual_test_websocket/scripts/deploy_to_aws.sh rename to manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_aws.sh diff --git a/manual_test_websocket/scripts/deploy_to_offline.sh b/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_offline.sh similarity index 100% rename from manual_test_websocket/scripts/deploy_to_offline.sh rename to manual_test_websocket/manual_test_websocket_RouteSelection/scripts/deploy_to_offline.sh diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless..yml b/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless..yml new file mode 100644 index 000000000..0de4ad4e9 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless..yml @@ -0,0 +1,28 @@ +# Welcome to Serverless! +# +# This file is the main config file for your service. +# It's very minimal at this point and uses default values. +# You can always add more config options for more control. +# We've included some commented out config examples here. +# Just uncomment any of them to get that config option. +# +# For full config options, check the docs: +# docs.serverless.com +# +# Happy Coding! + +service: manual-test-websocket-RouteSelection + +provider: + name: aws + runtime: nodejs8.10 + websocketsApiRouteSelectionExpression: $request.body.service.do + + +functions: + echo: + handler: handler.echo + events: + - websocket: + route: echo + diff --git a/manual_test_websocket/scripts/serverless.aws.yml b/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.aws.yml similarity index 100% rename from manual_test_websocket/scripts/serverless.aws.yml rename to manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.aws.yml diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.offline.yml b/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.offline.yml new file mode 100644 index 000000000..b3bcbfa71 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_RouteSelection/scripts/serverless.offline.yml @@ -0,0 +1,7 @@ +plugins: + - serverless-offline + +custom: + serverless-offline: + port: 3004 + diff --git a/manual_test_websocket/serverless.yml b/manual_test_websocket/manual_test_websocket_RouteSelection/serverless.yml similarity index 100% rename from manual_test_websocket/serverless.yml rename to manual_test_websocket/manual_test_websocket_RouteSelection/serverless.yml diff --git a/manual_test_websocket/manual_test_websocket_RouteSelection/test/e2e/ws.e2e.js b/manual_test_websocket/manual_test_websocket_RouteSelection/test/e2e/ws.e2e.js new file mode 100644 index 000000000..a5659364d --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_RouteSelection/test/e2e/ws.e2e.js @@ -0,0 +1,53 @@ +const chai = require('chai'); +const expect = chai.expect; +const endpoint = process.env.npm_config_endpoint||'ws://localhost:3005'; +const timeout = process.env.npm_config_timeout?parseInt(process.env.npm_config_timeout):1000; +const WebSocketTester=require('../support/WebSocketTester'); + +describe('serverless', ()=>{ + describe('with WebSocket support', ()=>{ + let clients=[]; let req=null; let cred=null; + const createWebSocket=async (qs)=>{ + const ws=new WebSocketTester(); + let url=endpoint; + if (qs) url=`${endpoint}?${qs}`; + await ws.open(url); + clients.push(ws); + return ws; + }; + const createClient=async (qs)=>{ + const ws=await createWebSocket(qs); + ws.send(JSON.stringify({action:'getClientInfo'})); + const json=await ws.receive1(); + const id=JSON.parse(json).info.id; + return {ws, id}; + }; + + beforeEach(()=>{ + clients=[]; + }); + afterEach(async ()=>{ + await Promise.all(clients.map(async (ws, i)=>{ + const n=ws.countUnrecived(); + + if (n>0) { + console.log(`unreceived:[i=${i}]`); + (await ws.receive(n)).forEach(m=>console.log(m)); + } + expect(n).to.equal(0); + ws.close(); + })); + clients=[]; + }); + + it(`should call action 'echo' handler located at service.do`, async ()=>{ + const ws=await createWebSocket(); + const now=""+Date.now(); + const payload=JSON.stringify({service:{do:'echo'}, message:now}); + ws.send(payload); + expect(await ws.receive1()).to.equal(`${now}`); + }).timeout(timeout); + + + }); +}); \ No newline at end of file diff --git a/manual_test_websocket/test/support/WebSocketTester.js b/manual_test_websocket/manual_test_websocket_RouteSelection/test/support/WebSocketTester.js similarity index 100% rename from manual_test_websocket/test/support/WebSocketTester.js rename to manual_test_websocket/manual_test_websocket_RouteSelection/test/support/WebSocketTester.js diff --git a/manual_test_websocket/handler.js b/manual_test_websocket/manual_test_websocket_main/handler.js similarity index 51% rename from manual_test_websocket/handler.js rename to manual_test_websocket/manual_test_websocket_main/handler.js index 74bc6f6ab..7ada0f080 100644 --- a/manual_test_websocket/handler.js +++ b/manual_test_websocket/manual_test_websocket_main/handler.js @@ -17,81 +17,132 @@ const errorResponse = { body: 'Request is not OK.' }; +// const generatePolicy = function(principalId, effect, resource) { +// const authResponse = {}; +// authResponse.principalId = principalId; +// if (effect && resource) { +// const policyDocument = {}; +// policyDocument.Version = '2012-10-17'; +// policyDocument.Statement = []; +// const statementOne = {}; +// statementOne.Action = 'execute-api:Invoke'; +// statementOne.Effect = effect; +// statementOne.Resource = resource; +// policyDocument.Statement[0] = statementOne; +// authResponse.policyDocument = policyDocument; +// } +// return authResponse; +// }; + +// module.exports.http = async (event, context) => { +// return successfullResponse; +// }; + module.exports.connect = async (event, context) => { + // console.log('connect:'); const listener=await ddb.get({TableName:'listeners', Key:{name:'default'}}).promise(); - if (listener.Item) await sendToClient(JSON.stringify({action:'update', event:'connect', info:{id:event.requestContext.connectionId}}), listener.Item.id, newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); - return successfullResponse; + + if (listener.Item) { + const timeout=new Promise((resolve) => setTimeout(resolve,100)); + const send=sendToClient( // sendToClient won't return on AWS when client doesn't exits so we set a timeout + JSON.stringify({action:'update', event:'connect', info:{id:event.requestContext.connectionId, event:{...event, apiGatewayUrl:`${event.apiGatewayUrl}`}, context}}), + listener.Item.id, + newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); + await Promise.race([send, timeout]); + } + return successfullResponse; }; +// module.export.auth = (event, context, callback) => { +// //console.log('auth:'); +// const token = event.headers["Authorization"]; + +// if ('deny'===token) callback(null, generatePolicy('user', 'Deny', event.methodArn)); +// else callback(null, generatePolicy('user', 'Allow', event.methodArn));; +// }; + module.exports.disconnect = async (event, context) => { const listener=await ddb.get({TableName:'listeners', Key:{name:'default'}}).promise(); - if (listener.Item) await sendToClient(JSON.stringify({action:'update', event:'disconnect', info:{id:event.requestContext.connectionId}}), listener.Item.id, newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); - return successfullResponse; + if (listener.Item) await sendToClient(JSON.stringify({action:'update', event:'disconnect', info:{id:event.requestContext.connectionId, event:{...event, apiGatewayUrl:`${event.apiGatewayUrl}`}, context}}), listener.Item.id, newAWSApiGatewayManagementApi(event, context)).catch(()=>{}); + return successfullResponse; }; module.exports.defaultHandler = async (event, context) => { await sendToClient(`Error: No Supported Action in Payload '${event.body}'`, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); - return successfullResponse; + return successfullResponse; }; module.exports.getClientInfo = async (event, context) => { + // console.log('getClientInfo:'); await sendToClient({action:'update', event:'client-info', info:{id:event.requestContext.connectionId}}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); - return successfullResponse; + return successfullResponse; +}; + +module.exports.getCallInfo = async (event, context) => { + await sendToClient({action:'update', event:'call-info', info:{event:{...event, apiGatewayUrl:`${event.apiGatewayUrl}`}, context}}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); + return successfullResponse; }; module.exports.makeError = async (event, context) => { const obj=null; obj.non.non=1; - return successfullResponse; + return successfullResponse; +}; + +module.exports.replyViaCallback = (event, context, callback) => { + sendToClient({action:'update', event:'reply-via-callback'}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); + callback(); +}; + +module.exports.replyErrorViaCallback = (event, context, callback) => { + return callback("error error error"); }; module.exports.multiCall1 = async (event, context) => { await sendToClient({action:'update', event:'made-call-1'}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); - return successfullResponse; + return successfullResponse; }; module.exports.multiCall2 = async (event, context) => { await sendToClient({action:'update', event:'made-call-2'}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); - return successfullResponse; + return successfullResponse; }; module.exports.send = async (event, context) => { const action = JSON.parse(event.body); const sents=[]; - action.clients.forEach((connectionId)=>{ + action.clients.forEach((connectionId)=>{ const sent=sendToClient(action.data, connectionId, newAWSApiGatewayManagementApi(event, context)); sents.push(sent); }); const noErr=await Promise.all(sents).then(()=>true).catch(()=>false); if (!noErr) await sendToClient('Error: Could not Send all Messages', event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)); - return successfullResponse; + return successfullResponse; }; module.exports.registerListener = async (event, context) => { await ddb.put({TableName:'listeners', Item:{name:'default', id:event.requestContext.connectionId}}).promise(); await sendToClient({action:'update', event:'register-listener', info:{id:event.requestContext.connectionId}}, event.requestContext.connectionId, newAWSApiGatewayManagementApi(event, context)).catch(err=>console.log(err)); - return successfullResponse; + return successfullResponse; }; module.exports.deleteListener = async (event, context) => { await ddb.delete({TableName:'listeners', Key:{name:'default'}}).promise(); - return successfullResponse; + return successfullResponse; }; const newAWSApiGatewayManagementApi=(event, context)=>{ - const endpoint=event.requestContext.domainName+'/'+event.requestContext.stage; + let endpoint=event.apiGatewayUrl; + + if (!endpoint) endpoint = event.requestContext.domainName+'/'+event.requestContext.stage; const apiVersion='2018-11-29'; - let API=context.API; - if (!process.env.IS_OFFLINE) { - API = require('aws-sdk'); - require('aws-sdk/clients/apigatewaymanagementapi'); - } - return new API.ApiGatewayManagementApi({ apiVersion, endpoint }); + return new AWS.ApiGatewayManagementApi({ apiVersion, endpoint }); }; const sendToClient = (data, connectionId, apigwManagementApi) => { + // console.log(`sendToClient:${connectionId}`); let sendee=data; if ('object'==typeof data) sendee=JSON.stringify(data); diff --git a/manual_test_websocket/manual_test_websocket_main/package-lock.json b/manual_test_websocket/manual_test_websocket_main/package-lock.json new file mode 100644 index 000000000..ba31f9664 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_main/package-lock.json @@ -0,0 +1,1488 @@ +{ + "name": "manual_test", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/chai": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", + "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", + "dev": true + }, + "@types/cookiejar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz", + "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==", + "dev": true + }, + "@types/node": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz", + "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==", + "dev": true + }, + "@types/superagent": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.7.tgz", + "integrity": "sha512-9KhCkyXv268A2nZ1Wvu7rQWM+BmdYUVkycFeNnYrUL5Zwu7o8wPQ3wBfW59dDP+wuoxw0ww8YKgTNv8j/cgscA==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "aws-sdk": { + "version": "2.473.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.473.0.tgz", + "integrity": "sha512-1Qr16lOcz4ANzl/oPQRR+fxchfvUx4PVQhUNnDU3FH9OBfU3Xj+Vh6bGYFbreFQgqIqXUTEuJR5pC44uK70YfA==", + "requires": { + "buffer": "4.9.1", + "events": "1.1.1", + "ieee754": "1.1.8", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + } + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "awscred": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/awscred/-/awscred-1.4.2.tgz", + "integrity": "sha512-j3Vehf6PCFzzPZKkzEcj0Y2QO8w8UBbgobnl3DwHMiAE9A2mfJxTkq3cX4UNWHmrTAR0rj5BC/ts90Ok4Pg6rw==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "~2.0.0" + } + }, + "bluebird": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", + "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chai-http": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-4.3.0.tgz", + "integrity": "sha512-zFTxlN7HLMv+7+SPXZdkd5wUlK+KxH6Q7bIEMiEx0FK3zuuMqL7cwICAQ0V1+yYRozBburYuxN1qZstgHpFZQg==", + "dev": true, + "requires": { + "@types/chai": "4", + "@types/superagent": "^3.8.3", + "cookiejar": "^2.1.1", + "is-ip": "^2.0.0", + "methods": "^1.1.2", + "qs": "^6.5.1", + "superagent": "^3.7.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "dynamodb-localhost": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/dynamodb-localhost/-/dynamodb-localhost-0.0.7.tgz", + "integrity": "sha512-Xyv0EqQDuOVjA8XGVOo3SuzQ5jKA8/gBKUeKRP3V586Fh9abWXLXOGjf7mPO8sWzddzGqyQx2mALj9IWSotg7A==", + "dev": true, + "requires": { + "mkdirp": "^0.5.0", + "progress": "^1.1.8", + "rmdir": "^1.2.0", + "tar": "^2.0.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + } + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "is": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/is/-/is-0.2.7.tgz", + "integrity": "sha1-OzSixI81mXLzUEKEkZOucmS2NWI=", + "dev": true + }, + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", + "dev": true + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", + "integrity": "sha1-aO6gfooKCpTC0IDdZ0xzGrKkYas=", + "dev": true, + "requires": { + "ip-regex": "^2.0.0" + } + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "dev": true + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "dev": true, + "requires": { + "mime-db": "1.40.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", + "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", + "dev": true, + "requires": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "2.2.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "ms": "2.1.1", + "node-environment-flags": "1.0.5", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.2.2", + "yargs-parser": "13.0.0", + "yargs-unparser": "1.5.0" + } + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==", + "dev": true + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-environment-flags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", + "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + } + }, + "node.extend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-1.0.8.tgz", + "integrity": "sha1-urBDefc4P0WHmQyd8Htqf2Xbdys=", + "dev": true, + "requires": { + "is": "~0.2.6", + "object-keys": "~0.4.0" + }, + "dependencies": { + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", + "dev": true + } + } + }, + "node.flow": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/node.flow/-/node.flow-1.2.3.tgz", + "integrity": "sha1-4cRKgq7KjXi0WKd/s9xkLy66Jkk=", + "dev": true, + "requires": { + "node.extend": "1.0.8" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rmdir": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rmdir/-/rmdir-1.2.0.tgz", + "integrity": "sha1-T+A1fLBhaMJY5z6WgJPcTooPMlM=", + "dev": true, + "requires": { + "node.flow": "1.2.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "serverless-dynamodb-local": { + "version": "0.2.37", + "resolved": "https://registry.npmjs.org/serverless-dynamodb-local/-/serverless-dynamodb-local-0.2.37.tgz", + "integrity": "sha512-1q3rfmn+Y1nZQhmuVdFLJcMTc6la1zY6sT98TS6Af0vRG//oa7CuiDEox0XEzuj7KZ0TodwmXmnRdzt9VRm3fA==", + "dev": true, + "requires": { + "aws-sdk": "^2.7.0", + "bluebird": "^3.4.6", + "dynamodb-localhost": "^0.0.7", + "lodash": "^4.17.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + } + }, + "supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "dev": true, + "requires": { + "block-stream": "*", + "fstream": "^1.0.2", + "inherits": "2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.0.0.tgz", + "integrity": "sha512-cknCal4k0EAOrh1SHHPPWWh4qm93g1IuGGGwBjWkXmCG7LsDtL8w9w+YVfaF+KSVwiHQKDIMsSLBVftKf9d1pg==", + "dev": true, + "requires": { + "async-limiter": "^1.0.0" + } + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", + "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", + "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yargs-unparser": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", + "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", + "dev": true, + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.11", + "yargs": "^12.0.5" + }, + "dependencies": { + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + } + } +} diff --git a/manual_test_websocket/manual_test_websocket_main/package.json b/manual_test_websocket/manual_test_websocket_main/package.json new file mode 100644 index 000000000..e2e5c1f63 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_main/package.json @@ -0,0 +1,28 @@ +{ + "name": "manual_test", + "version": "0.0.0", + "description": "", + "main": "handler.js", + "scripts": { + "test": "mocha ./test/*", + "test-only": "mocha ", + "start": "sls offline", + "deploy-aws": "./scripts/deploy_to_aws.sh", + "deploy-offline": "./scripts/deploy_to_offline.sh" + }, + "author": "", + "license": "MIT", + "dependencies": { + "aws-sdk": "^2.473.0" + }, + "devDependencies": { + "aws4": "^1.8.0", + "awscred": "^1.4.2", + "chai": "^4.2.0", + "chai-http": "^4.3.0", + "mocha": "^6.1.4", + "moment": "^2.24.0", + "serverless-dynamodb-local": "^0.2.37", + "ws": "^7.0.0" + } +} diff --git a/manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_aws.sh b/manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_aws.sh new file mode 100755 index 000000000..590bef430 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_aws.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +echo "Deploying to AWS ..." +echo "Removing node modules ..." +rm -fr ./node_modules +echo "Instaing aws-sdk ..." +npm i aws-sdk +echo "Copying serverless.yml ..." +cp ./scripts/serverless..yml ./serverless.yml +cat ./scripts/serverless.aws.yml >> ./serverless.yml +echo "Deploying to AWS ..." +sls deploy diff --git a/manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_offline.sh b/manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_offline.sh new file mode 100755 index 000000000..adbb643a3 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_main/scripts/deploy_to_offline.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +echo "Deploying to Offline ..." +echo "Removing node modules ..." +rm -fr ./node_modules +echo "Instaing node modules ..." +npm i +echo "Linking serverless-offline ..." +npm link serverless-offline +echo "Copying serverless.yml ..." +cp ./scripts/serverless..yml ./serverless.yml +cat ./scripts/serverless.offline.yml >> ./serverless.yml +echo "Deploying to Offline ..." +sls offline diff --git a/manual_test_websocket/scripts/serverless..yml b/manual_test_websocket/manual_test_websocket_main/scripts/serverless..yml similarity index 79% rename from manual_test_websocket/scripts/serverless..yml rename to manual_test_websocket/manual_test_websocket_main/scripts/serverless..yml index 07fb676c0..729d3ad14 100644 --- a/manual_test_websocket/scripts/serverless..yml +++ b/manual_test_websocket/manual_test_websocket_main/scripts/serverless..yml @@ -11,7 +11,7 @@ # # Happy Coding! -service: manual-test-websocket +service: manual-test-websocket-main provider: name: aws @@ -35,11 +35,20 @@ provider: - "arn:aws:execute-api:*:*:**/@connections/*" functions: + # http: + # handler: handler.http + # events: + # - http: + # path: /http + # method: GET connect: handler: handler.connect events: - websocket: route: $connect + # authorizer: auth + # auth: + # handler: handler.auth disconnect: handler: handler.disconnect events: @@ -55,11 +64,26 @@ functions: events: - websocket: route: getClientInfo + getCallInfo: + handler: handler.getCallInfo + events: + - websocket: + route: getCallInfo makeError: handler: handler.makeError events: - websocket: route: makeError + replyViaCallback: + handler: handler.replyViaCallback + events: + - websocket: + route: replyViaCallback + replyErrorViaCallback: + handler: handler.replyErrorViaCallback + events: + - websocket: + route: replyErrorViaCallback multiCall1: handler: handler.multiCall1 events: diff --git a/manual_test_websocket/manual_test_websocket_main/scripts/serverless.aws.yml b/manual_test_websocket/manual_test_websocket_main/scripts/serverless.aws.yml new file mode 100644 index 000000000..64289757a --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_main/scripts/serverless.aws.yml @@ -0,0 +1,9 @@ +plugins: + # - serverless-offline + +package: + exclude: + - ./** + include: + - handler.js + - node_modules/** diff --git a/manual_test_websocket/scripts/serverless.offline.yml b/manual_test_websocket/manual_test_websocket_main/scripts/serverless.offline.yml similarity index 100% rename from manual_test_websocket/scripts/serverless.offline.yml rename to manual_test_websocket/manual_test_websocket_main/scripts/serverless.offline.yml diff --git a/manual_test_websocket/manual_test_websocket_main/serverless.yml b/manual_test_websocket/manual_test_websocket_main/serverless.yml new file mode 100644 index 000000000..596836955 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_main/serverless.yml @@ -0,0 +1,7 @@ +###################################### +### DO NOT EDIT THIS FILE DIRECTLY ### +### ### +### User either: ### +### 'npm run deploy-offline' or ### +### 'npm run deploy-aws' ### +###################################### \ No newline at end of file diff --git a/manual_test_websocket/manual_test_websocket_main/test/e2e/ws.e2e.js b/manual_test_websocket/manual_test_websocket_main/test/e2e/ws.e2e.js new file mode 100644 index 000000000..d8db0b706 --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_main/test/e2e/ws.e2e.js @@ -0,0 +1,342 @@ +const chai = require('chai'); +const chaiHttp = require('chai-http'); +chai.use(chaiHttp); +const expect = chai.expect; +const aws4 = require('aws4'); +const awscred = require('awscred'); +const moment = require('moment'); +const endpoint = process.env.npm_config_endpoint||'ws://localhost:3001'; +const timeout = process.env.npm_config_timeout?parseInt(process.env.npm_config_timeout):1000; +const WebSocketTester=require('../support/WebSocketTester'); + +describe('serverless', ()=>{ + describe('with WebSocket support', ()=>{ + let clients=[]; let req=null; let cred=null; + const createWebSocket=async (qs)=>{ + const ws=new WebSocketTester(); + let url=endpoint; + if (qs) url=`${endpoint}?${qs}`; + await ws.open(url); + clients.push(ws); + return ws; + }; + const createClient=async (qs)=>{ + const ws=await createWebSocket(qs); + ws.send(JSON.stringify({action:'getClientInfo'})); + const json=await ws.receive1(); + const id=JSON.parse(json).info.id; + return {ws, id}; + }; + before(async ()=>{ + req=chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); + // req=chai.request('http://localhost:3001/dev').keepOpen(); + cred=await new Promise((resolve, reject)=>{ + awscred.loadCredentials(function(err, data) { if (err) reject(err); else resolve(data); }); + }); + }); + + beforeEach(()=>{ + clients=[]; + }); + afterEach(async ()=>{ + await Promise.all(clients.map(async (ws, i)=>{ + const n=ws.countUnrecived(); + + if (n>0) { + console.log(`unreceived:[i=${i}]`); + (await ws.receive(n)).forEach(m=>console.log(m)); + } + expect(n).to.equal(0); + ws.close(); + })); + clients=[]; + }); + + it('should request to upgade to WebSocket when receving an HTTP request', async ()=>{ + const req=chai.request(`${endpoint.replace('ws://', 'http://').replace('wss://', 'https://')}`).keepOpen(); + let res=await req.get(`/${Date.now()}`);//.set('Authorization', user.accessToken); + expect(res).to.have.status(426); + res=await req.get(`/${Date.now()}/${Date.now()}`);//.set('Authorization', user.accessToken); + expect(res).to.have.status(426); + }).timeout(timeout); + + it('should open a WebSocket', async ()=>{ + const ws=await createWebSocket(); + expect(ws).not.to.be.undefined; + }).timeout(timeout); + + it('should receive client connection info', async ()=>{ + const ws=await createWebSocket(); + ws.send(JSON.stringify({action:'getClientInfo'})); + const clientInfo=JSON.parse(await ws.receive1()); + expect(clientInfo).to.deep.equal({action:'update', event:'client-info', info:{id:clientInfo.info.id}}); + }).timeout(timeout); + + it('should call default handler when no such action exists', async ()=>{ + const ws=await createWebSocket(); + const payload=JSON.stringify({action:'action'+Date.now()}); + ws.send(payload); + expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '${payload}'`); + }).timeout(timeout); + + it('should call default handler when no action provided', async ()=>{ + const ws=await createWebSocket(); + ws.send(JSON.stringify({hello:'world'})); + expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '{"hello":"world"}'`); + }).timeout(timeout); + + it('should send & receive data', async ()=>{ + const c1=await createClient(); + const c2=await createClient(); + const c3=await createClient(); + c1.ws.send(JSON.stringify({action:'send', data:'Hello World!', clients:[c1.id, c3.id]})); + expect(await c1.ws.receive1()).to.equal('Hello World!'); + expect(await c3.ws.receive1()).to.equal('Hello World!'); + }).timeout(timeout); + + it('should respond when having an internal server error', async ()=>{ + const conn=await createClient(); + conn.ws.send(JSON.stringify({action:'makeError'})); + const res=JSON.parse(await conn.ws.receive1()); + expect(res).to.deep.equal({message:'Internal server error', connectionId:conn.id, requestId:res.requestId}); + }).timeout(timeout); + + it('should respond via callback', async ()=>{ + const ws=await createWebSocket(); + ws.send(JSON.stringify({action:'replyViaCallback'})); + const res=JSON.parse(await ws.receive1()); + expect(res).to.deep.equal({action:'update', event:'reply-via-callback'}); + }).timeout(timeout); + + it('should respond with error when calling callback(error)', async ()=>{ + const conn=await createClient(); + conn.ws.send(JSON.stringify({action:'replyErrorViaCallback'})); + const res=JSON.parse(await conn.ws.receive1()); + expect(res).to.deep.equal({message:'Internal server error', connectionId:conn.id, requestId:res.requestId}); + }).timeout(timeout); + + it('should respond with only the last action when there are more than one in the serverless.yml file', async ()=>{ + const ws=await createWebSocket(); + ws.send(JSON.stringify({action:'makeMultiCalls'})); + const res=JSON.parse(await ws.receive1()); + expect(res).to.deep.equal({action:'update', event:'made-call-2'}); + }).timeout(timeout); + + it('should not send to non existing client', async ()=>{ + const c1=await createClient(); + c1.ws.send(JSON.stringify({action:'send', data:'Hello World!', clients:["non-existing-id"]})); + expect(await c1.ws.receive1()).to.equal('Error: Could not Send all Messages'); + }).timeout(timeout); + + it('should connect & disconnect', async ()=>{ + const ws=await createWebSocket(); + await ws.send(JSON.stringify({action:'registerListener'})); + await ws.receive1(); + + const c1=await createClient(); + const connect1 = JSON.parse(await ws.receive1()); delete connect1.info.event; delete delete connect1.info.context; + expect(connect1).to.deep.equal({action:'update', event:'connect', info:{id:c1.id}}); + + const c2=await createClient(); + const connect2 = JSON.parse(await ws.receive1()); delete connect2.info.event; delete delete connect2.info.context; + expect(connect2).to.deep.equal({action:'update', event:'connect', info:{id:c2.id}}); + + c2.ws.close(); + const disconnect2 = JSON.parse(await ws.receive1()); delete disconnect2.info.event; delete delete disconnect2.info.context; + expect(disconnect2).to.deep.equal({action:'update', event:'disconnect', info:{id:c2.id}}); + + const c3=await createClient(); + const connect3 = JSON.parse(await ws.receive1()); delete connect3.info.event; delete delete connect3.info.context; + expect(connect3).to.deep.equal({action:'update', event:'connect', info:{id:c3.id}}); + + c1.ws.close(); + const disconnect1 = JSON.parse(await ws.receive1()); delete disconnect1.info.event; delete delete disconnect1.info.context; + expect(disconnect1).to.deep.equal({action:'update', event:'disconnect', info:{id:c1.id}}); + + c3.ws.close(); + const disconnect3 = JSON.parse(await ws.receive1()); delete disconnect3.info.event; delete delete disconnect3.info.context; + expect(disconnect3).to.deep.equal({action:'update', event:'disconnect', info:{id:c3.id}}); + }).timeout(timeout); + + const createExpectedEvent=(connectionId, action, eventType, actualEvent)=>{ + const url=new URL(endpoint); + const expected={ + apiGatewayUrl: `${actualEvent.apiGatewayUrl}`, + isBase64Encoded: false, + requestContext: { + apiId: actualEvent.requestContext.apiId, + connectedAt: actualEvent.requestContext.connectedAt, + connectionId: `${connectionId}`, + domainName: url.hostname, + eventType, + extendedRequestId: actualEvent.requestContext.extendedRequestId, + identity: { + accessKey: null, + accountId: null, + caller: null, + cognitoAuthenticationProvider: null, + cognitoAuthenticationType: null, + cognitoIdentityId: null, + cognitoIdentityPoolId: null, + principalOrgId: null, + sourceIp: actualEvent.requestContext.identity.sourceIp, + user: null, + userAgent: null, + userArn: null, + }, + messageDirection: 'IN', + messageId: actualEvent.requestContext.messageId, + requestId: actualEvent.requestContext.requestId, + requestTime: actualEvent.requestContext.requestTime, + requestTimeEpoch: actualEvent.requestContext.requestTimeEpoch, + routeKey: action, + stage: actualEvent.requestContext.stage, + }, + }; + + return expected; + }; + + const createExpectedContext=(actualContext)=>{ + const expected={ + awsRequestId: actualContext.awsRequestId, + callbackWaitsForEmptyEventLoop: true, + functionName: actualContext.functionName, + functionVersion: '$LATEST', + invokedFunctionArn: actualContext.invokedFunctionArn, + invokeid: actualContext.invokeid, + logGroupName: actualContext.logGroupName, + logStreamName: actualContext.logStreamName, + memoryLimitInMB: actualContext.memoryLimitInMB, + }; + + return expected; + }; + + const createExpectedConnectHeaders=(actualHeaders)=>{ + const url=new URL(endpoint); + const expected={ + Host: url.hostname, + 'Sec-WebSocket-Extensions': actualHeaders['Sec-WebSocket-Extensions'], + 'Sec-WebSocket-Key': actualHeaders['Sec-WebSocket-Key'], + 'Sec-WebSocket-Version': actualHeaders['Sec-WebSocket-Version'], + 'X-Amzn-Trace-Id': actualHeaders['X-Amzn-Trace-Id'], + 'X-Forwarded-For': actualHeaders['X-Forwarded-For'], + 'X-Forwarded-Port': `${url.port||443}`, + 'X-Forwarded-Proto': `${url.protocol.replace('ws', 'http').replace('wss', 'https').replace(':', '')}` + }; + + return expected; + }; + + const createExpectedDisconnectHeaders=(actualHeaders)=>{ + const url=new URL(endpoint); + const expected={ + Host: url.hostname, + 'x-api-key': '', + 'x-restapi': '', + }; + + return expected; + }; + + const createExpectedConnectMultiValueHeaders=(actualHeaders)=>{ + const expected=createExpectedConnectHeaders(actualHeaders); + Object.keys(expected).map((key, index)=>{ + expected[key] = [expected[key]]; + }); + return expected; + }; + + const createExpectedDisconnectMultiValueHeaders=(actualHeaders)=>{ + const expected=createExpectedDisconnectHeaders(actualHeaders); + Object.keys(expected).map((key, index)=>{ + expected[key] = [expected[key]]; + }); + return expected; + }; + + it('should receive correct call info', async ()=>{ + const ws=await createWebSocket(); + await ws.send(JSON.stringify({action:'registerListener'})); + await ws.receive1(); + + // connect + const c=await createClient(); + const connect=JSON.parse(await ws.receive1()); + let now=Date.now(); + let expectedCallInfo={id:c.id, event:{headers:createExpectedConnectHeaders(connect.info.event.headers), multiValueHeaders:createExpectedConnectMultiValueHeaders(connect.info.event.headers), ...createExpectedEvent(c.id, '$connect', 'CONNECT', connect.info.event)}, context:createExpectedContext(connect.info.context)}; + expect(connect).to.deep.equal({action:'update', event:'connect', info:expectedCallInfo}); + expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(connect.info.event.requestContext.connectedAt-10, connect.info.event.requestContext.requestTimeEpoch+10); + expect(connect.info.event.requestContext.connectedAt).to.be.within(now-timeout, now); + expect(connect.info.event.requestContext.requestTimeEpoch).to.be.within(now-timeout, now); + expect(moment.utc(connect.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now-timeout, now); + if (endpoint.startsWith('ws://locahost')) { + expect(connect.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); + expect(connect.info.event.headers['X-Forwarded-For']).to.be.equal('127.0.0.1'); + } + + // getCallInfo + c.ws.send(JSON.stringify({action:'getCallInfo'})); + const callInfo=JSON.parse(await c.ws.receive1()); + now=Date.now(); + expectedCallInfo={event:{body: '{\"action\":\"getCallInfo\"}', ...createExpectedEvent(c.id, 'getCallInfo', 'MESSAGE', callInfo.info.event)}, context:createExpectedContext(callInfo.info.context)}; + expect(callInfo).to.deep.equal({action:'update', event:'call-info', info:expectedCallInfo}); + expect(callInfo.info.event.requestContext.connectedAt).to.be.lt(callInfo.info.event.requestContext.requestTimeEpoch); + expect(callInfo.info.event.requestContext.connectedAt).to.be.within(now-timeout, now); + expect(callInfo.info.event.requestContext.requestTimeEpoch).to.be.within(now-timeout, now); + expect(moment.utc(callInfo.info.event.requestContext.requestTime, 'D/MMM/YYYY:H:m:s Z').toDate().getTime()).to.be.within(now-timeout, now); + if (endpoint.startsWith('ws://locahost')) expect(callInfo.info.event.apiGatewayUrl).to.equal(endpoint.replace('ws://', 'http://').replace('wss://', 'https://')); + + // disconnect + c.ws.close(); + const disconnect=JSON.parse(await ws.receive1()); + now=Date.now(); + expectedCallInfo={id:c.id, event:{headers:createExpectedDisconnectHeaders(disconnect.info.event.headers), multiValueHeaders:createExpectedDisconnectMultiValueHeaders(disconnect.info.event.headers), ...createExpectedEvent(c.id, '$disconnect', 'DISCONNECT', disconnect.info.event)}, context:createExpectedContext(disconnect.info.context)}; + expect(disconnect).to.deep.equal({action:'update', event:'disconnect', info:expectedCallInfo}); + }).timeout(timeout); + + it('should be able to parse query string', async ()=>{ + const now=''+Date.now(); + const ws=await createWebSocket(); + await ws.send(JSON.stringify({action:'registerListener'})); + await ws.receive1(); + + const c1=await createClient(); + const c2=await createClient(`now=${now}&before=123456789`); + expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.be.undefined; + expect(JSON.parse(await ws.receive1()).info.event.queryStringParameters).to.deep.equal({now, before:'123456789'}); + }).timeout(timeout); + + it('should be able to receive messages via REST API', async ()=>{ + const c1=await createClient(); + const c2=await createClient(); + const url=new URL(endpoint); + const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c2.id}`, method: 'POST', body:'Hello World!', headers:{'Content-Type':'text/plain'/*'application/text'*/}}; + aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); + const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send('Hello World!'); + expect(res).to.have.status(200); + expect(await c2.ws.receive1()).to.equal('Hello World!'); + }).timeout(timeout); + + it('should receive error code when sending to non existing client via REST API', async ()=>{ + const c='aJz0Md6VoAMCIbQ='; + const url=new URL(endpoint); + const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c}`, method: 'POST', body:'Hello World!', headers:{'Content-Type':'text/plain'/*'application/text'*/}}; + aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); + const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send('Hello World!'); + expect(res).to.have.status(410); + }).timeout(timeout); + + // UNABLE TO TEST HIS SCENARIO BECAUSE AWS DOESN'T RETURN ANYTHING + // it('should not receive anything when POSTing nothing', async ()=>{ + // const c1=await createClient(); + // const url=new URL(endpoint); + // const signature = {service: 'execute-api', host:url.host, path:`${url.pathname}/@connections/${c1.id}`, method: 'POST'/*, body:'Hello World!'*/, headers:{'Content-Type':'text/plain'/*'application/text'*/}}; + // aws4.sign(signature, {accessKeyId: cred.accessKeyId, secretAccessKey: cred.secretAccessKey}); + // const res=await req.post(signature.path.replace(url.pathname, '')).set('X-Amz-Date', signature.headers['X-Amz-Date']).set('Authorization', signature.headers['Authorization']).set('Content-Type', signature.headers['Content-Type']).send(/*'Hello World!'*/); + // expect(res).to.have.status(200); + // }).timeout(timeout); + + }); +}); \ No newline at end of file diff --git a/manual_test_websocket/manual_test_websocket_main/test/support/WebSocketTester.js b/manual_test_websocket/manual_test_websocket_main/test/support/WebSocketTester.js new file mode 100644 index 000000000..ee5c8d6fb --- /dev/null +++ b/manual_test_websocket/manual_test_websocket_main/test/support/WebSocketTester.js @@ -0,0 +1,60 @@ +const WebSocket = require('ws'); + +class WebSocketTester { + constructor() { + this.messages=[]; this.receivers=[]; + } + + open(url) { + if (null!=this.ws) return; + const ws=this.ws=new WebSocket(url); + ws.on('message', (message)=>{ + // console.log('Received: '+message); + if (0 { + ws.on('open', ()=>{ + resolve(true); + }); + }); + } + + send(data) { + this.ws.send(data); + } + + receive1() { + return new Promise((resolve/*, reject*/)=>{ + if (0{ + const messages=[]; + for (let i=0; i{ + messages[i]=message; + if (i===n-1) resolve(messages); + }); + } + }); + } + + skip() { + if (0{}); + } + + countUnrecived() { + return this.messages.length; + } + + close() { + if (null!=this.ws) this.ws.close(); + } +}; + +module.exports=WebSocketTester; \ No newline at end of file diff --git a/manual_test_websocket/test/e2e/ws.e2e.js b/manual_test_websocket/test/e2e/ws.e2e.js deleted file mode 100644 index 718831e9f..000000000 --- a/manual_test_websocket/test/e2e/ws.e2e.js +++ /dev/null @@ -1,122 +0,0 @@ -'use strict'; - -const chai = require('chai'); -const expect = chai.expect; -const endpoint=process.env.npm_config_endpoint||'ws://localhost:3000/dev'; -const timeout=6000; - -const WebSocketTester=require('../support/WebSocketTester'); - -describe('serverless', ()=>{ - describe('with WebSocket support', ()=>{ - let clients=[]; - const createWebSocket=async ()=>{ - const ws=new WebSocketTester(); - await ws.open(endpoint); - clients.push(ws); - return ws; - }; - const createClient=async ()=>{ - const ws=await createWebSocket(); - ws.send(JSON.stringify({action:'getClientInfo'})); - const json=await ws.receive1(); - const id=JSON.parse(json).info.id; - return {ws, id}; - }; - - beforeEach(()=>{ - clients=[]; - }); - afterEach(async ()=>{ - await Promise.all(clients.map(async (ws, i)=>{ - const n=ws.countUnrecived(); - - if (n>0) { - console.log(`unreceived:[i=${i}]`); - (await ws.receive(n)).forEach(m=>console.log(m)); - } - expect(n).to.equal(0); - ws.close(); - })); - clients=[]; - }); - - it('should open a WebSocket', async ()=>{ - const ws=await createWebSocket(); - expect(ws).not.to.be.undefined; - }); - - it('should receive client connection info', async ()=>{ - const ws=await createWebSocket(); - ws.send(JSON.stringify({action:'getClientInfo'})); - const clientInfo=JSON.parse(await ws.receive1()); - expect(clientInfo).to.deep.equal({action:'update', event:'client-info', info:{id:clientInfo.info.id}}); - }); - - it('should call default handler when no such action exists', async ()=>{ - const ws=await createWebSocket(); - const payload=JSON.stringify({action:'action'+Date.now()}); - ws.send(payload); - expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '${payload}'`); - }); - - it('should call default handler when no action provided', async ()=>{ - const ws=await createWebSocket(); - ws.send(JSON.stringify({hello:'world'})); - expect(await ws.receive1()).to.equal(`Error: No Supported Action in Payload '{"hello":"world"}'`); - }); - - it('should send & receive data', async ()=>{ - const c1=await createClient(); - const c2=await createClient(); - const c3=await createClient(); - c1.ws.send(JSON.stringify({action:'send', data:'Hello World!', clients:[c1.id, c3.id]})); - expect(await c1.ws.receive1()).to.equal('Hello World!'); - expect(await c3.ws.receive1()).to.equal('Hello World!'); - }).timeout(timeout); - - it('should response when having an internal server error', async ()=>{ - const conn=await createClient(); - conn.ws.send(JSON.stringify({action:'makeError'})); - const res=JSON.parse(await conn.ws.receive1()); - expect(res).to.deep.equal({message:'Internal server error', connectionId:conn.id, requestId:res.requestId}); - }); - - it('should response with only the last action when there are more than one in the serverless.yml file', async ()=>{ - const ws=await createWebSocket(); - ws.send(JSON.stringify({action:'makeMultiCalls'})); - const res=JSON.parse(await ws.receive1()); - expect(res).to.deep.equal({action:'update', event:'made-call-2'}); - }); - - it('should not send to non existing client', async ()=>{ - const c1=await createClient(); - c1.ws.send(JSON.stringify({action:'send', data:'Hello World!', clients:["non-existing-id"]})); - expect(await c1.ws.receive1()).to.equal('Error: Could not Send all Messages'); - }); - - it('should connect & disconnect', async ()=>{ - const ws=await createWebSocket(); - await ws.send(JSON.stringify({action:'registerListener'})); - await ws.receive1(); - - const c1=await createClient(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c1.id}}); - - const c2=await createClient(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c2.id}}); - - c2.ws.close(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'disconnect', info:{id:c2.id}}); - - const c3=await createClient(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'connect', info:{id:c3.id}}); - - c1.ws.close(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'disconnect', info:{id:c1.id}}); - - c3.ws.close(); - expect(JSON.parse(await ws.receive1())).to.deep.equal({action:'update', event:'disconnect', info:{id:c3.id}}); - }).timeout(6000); - }); -}); diff --git a/package-lock.json b/package-lock.json index da73a5600..63cb9180a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "serverless-offline", - "version": "5.2.0", + "version": "5.3.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -785,6 +785,11 @@ "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1056,6 +1061,11 @@ "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", "dev": true }, + "bignumber.js": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-8.1.1.tgz", + "integrity": "sha512-QD46ppGintwPGuL1KqmwhR0O+N2cZUg8JG/VzwI2e28sM9TqHjQB10lI4QAaMHVbLzwVLLAwEglpKPViWX+5NQ==" + }, "bl": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", @@ -1107,6 +1117,14 @@ } } }, + "boom": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-7.3.0.tgz", + "integrity": "sha512-Swpoyi2t5+GhOEGw8rEsKvTxFLIDiiKoUc2gsoV6Lyr43LHBIzch3k2MvYUs8RTROrIkVJ3Al0TkaOGjnb+B6A==", + "requires": { + "hoek": "6.x.x" + } + }, "boxen": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", @@ -1340,6 +1358,22 @@ "url-to-options": "^1.0.1" } }, + "cbor": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-4.1.4.tgz", + "integrity": "sha512-SqNWyQnnYtKAPLA7lupvuGKrEgoF2rR/7I9rXdmW/9uxtmKdltthHTf8hfLLN1SIkoAFwz/jb6+VZuaHv3Lv6Q==", + "requires": { + "bignumber.js": "^8.0.1", + "commander": "^2.19.0", + "json-text-sequence": "^0.1", + "nofilter": "^1.0.1" + } + }, + "cbor-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cbor-js/-/cbor-js-0.1.0.tgz", + "integrity": "sha1-yAzmEg84fo+qdDcN/aIdlluPx/k=" + }, "chai": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", @@ -1556,8 +1590,7 @@ "commander": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" }, "component-emitter": { "version": "1.3.0", @@ -1920,6 +1953,11 @@ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true }, + "delimit-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/delimit-stream/-/delimit-stream-0.1.0.tgz", + "integrity": "sha1-m4MZR3wOX4rrPONXrjBfwl6hzSs=" + }, "depcheck": { "version": "0.6.11", "resolved": "https://registry.npmjs.org/depcheck/-/depcheck-0.6.11.tgz", @@ -2222,6 +2260,17 @@ "iconv-lite": "~0.4.13" } }, + "encodr": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/encodr/-/encodr-1.2.0.tgz", + "integrity": "sha512-OHAfuXxoeXEeXFZ0Vu3CGegIVI1iuLLdVMy1EIVDBfvff1tMjVwRNBFuo5UbjBm3Efcu+GiIYGOt0H3NKDjPrw==", + "requires": { + "cbor": "4.1.4", + "cbor-js": "0.1.0", + "msgpack-lite": "0.1.26", + "utf8": "3.0.0" + } + }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -2603,6 +2652,16 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "dev": true }, + "event-lite": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.2.tgz", + "integrity": "sha512-HnSYx1BsJ87/p6swwzv+2v6B4X+uxUteoDfRxsAb1S1BePzQqOLevVmkdA15GHJVd9A9Ok6wygUR18Hu0YeV9g==" + }, + "eventemitter3": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", + "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==" + }, "events": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", @@ -3157,6 +3216,18 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "hapi-plugin-websocket": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/hapi-plugin-websocket/-/hapi-plugin-websocket-2.0.16.tgz", + "integrity": "sha512-UE2JSrNIalXJmw4Qoi2FqhcguqTYQhjgPAVCtJUBhhZXD6IyA15guTdSeshBLxGfKcAFJmlpXs2LUMB25ARfOA==", + "requires": { + "boom": "7.3.0", + "hoek": "6.1.2", + "urijs": "1.19.1", + "websocket-framed": "1.2.0", + "ws": "6.1.2" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -3279,6 +3350,11 @@ } } }, + "hoek": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.2.tgz", + "integrity": "sha512-6qhh/wahGYZHFSFw12tBbJw5fsAhhwrrG/y3Cs0YMTv2WzMnL0oLPnQJjv1QJvEfylRSOFuP+xCu+tdx0tD16Q==" + }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -3340,8 +3416,7 @@ "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "ignore": { "version": "4.0.6", @@ -3446,6 +3521,11 @@ } } }, + "int64-buffer": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.10.tgz", + "integrity": "sha1-J3siiofZWtd30HwTgyAiQGpHNCM=" + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -3706,8 +3786,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -3830,6 +3909,14 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, + "json-text-sequence": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/json-text-sequence/-/json-text-sequence-0.1.1.tgz", + "integrity": "sha1-py8hfcSvxGKf/1/rME3BvVGi89I=", + "requires": { + "delimit-stream": "0.1.0" + } + }, "jsonata": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-1.6.4.tgz", @@ -4520,6 +4607,17 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, + "msgpack-lite": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz", + "integrity": "sha1-3TxQsm8FnyXn7e42REGDWOKprYk=", + "requires": { + "event-lite": "^0.1.1", + "ieee754": "^1.1.8", + "int64-buffer": "^0.1.9", + "isarray": "^1.0.0" + } + }, "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", @@ -4620,6 +4718,11 @@ "is-stream": "^1.0.1" } }, + "nofilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.2.tgz", + "integrity": "sha512-d38SORxm9UNoDsnPXajV9nBEebKX4/paXAlyRGnSjZuFbLLZDFUO4objr+tbybqsbqGXDWllb6gQoKUDc9q3Cg==" + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -7157,6 +7260,11 @@ "punycode": "^2.1.0" } }, + "urijs": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", + "integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==" + }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", @@ -7202,6 +7310,11 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==" + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7247,6 +7360,15 @@ "integrity": "sha1-oW0CXrkxvQO1LzCMrtD0D86+lTI=", "dev": true }, + "websocket-framed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/websocket-framed/-/websocket-framed-1.2.0.tgz", + "integrity": "sha512-rnVf9NisrPAKIzB0LLgLdnbiElRNZSeahKKXcicxzOxJdW4ZaCE7xR7nheBIoN2j++2fk6FD1GTg9d+rw/X9+g==", + "requires": { + "encodr": "1.2.0", + "eventemitter3": "3.1.0" + } + }, "whatwg-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", @@ -7375,6 +7497,14 @@ "signal-exit": "^3.0.2" } }, + "ws": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz", + "integrity": "sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==", + "requires": { + "async-limiter": "~1.0.0" + } + }, "xdg-basedir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", diff --git a/package.json b/package.json index bfc1a8328..6d8528536 100644 --- a/package.json +++ b/package.json @@ -125,13 +125,15 @@ "Selcuk Cihan (https://github.com/selcukcihan)", "G Roques (https://github.com/gbroques)", "Dustin Belliston (https://github.com/dwbelliston)", - "kobanyan (https://github.com/kobanyan)" + "kobanyan (https://github.com/kobanyan)", + "Ram Hardy (https://github.com/computerpunc)" ], "dependencies": { "@hapi/boom": "^7.4.2", "@hapi/cryptiles": "^4.2.0", "@hapi/h2o2": "^8.3.0", "@hapi/hapi": "^18.3.1", + "hapi-plugin-websocket": "^2.0.16", "js-string-escape": "^1.0.1", "jsonpath-plus": "^0.20.0", "jsonschema": "^1.2.4", diff --git a/src/index.js b/src/index.js index a42033942..fa8b65f28 100755 --- a/src/index.js +++ b/src/index.js @@ -24,6 +24,7 @@ const parseResources = require('./parseResources'); const { createDefaultApiKey, detectEncoding, randomId } = require('./utils'); const authFunctionNameExtractor = require('./authFunctionNameExtractor'); const requestBodyValidator = require('./requestBodyValidator'); +const wsHelpers = require('./websocketHelpers'); /* I'm against monolithic code like this file @@ -37,6 +38,9 @@ class Offline { this.serverlessLog = serverless.cli.log.bind(serverless.cli); this.options = options; this.exitCode = 0; + this.clients = new Map(); + this.wsActions = {}; + this.websocketsApiRouteSelectionExpression = serverless.service.provider.websocketsApiRouteSelectionExpression || '$request.body.action'; this.commands = { offline: { @@ -244,6 +248,7 @@ class Offline { this._createRoutes(); // API Gateway emulation this._createResourceRoutes(); // HTTP Proxy defined in Resource this._create404Route(); // Not found handling + this._createWebSocket(); return this.server; } @@ -382,6 +387,235 @@ class Offline { }); } + _createWebSocket() { + // start COPY PASTE FROM HTTP SERVER CODE + const serverOptions = { + host: this.options.host, + port: this.options.port + 1, + router: { + stripTrailingSlash: !this.options.preserveTrailingSlash, // removes trailing slashes on incoming paths. + }, + }; + + const httpsDir = this.options.httpsProtocol; + + // HTTPS support + if (typeof httpsDir === 'string' && httpsDir.length > 0) { + serverOptions.tls = { + key: fs.readFileSync(path.resolve(httpsDir, 'key.pem'), 'ascii'), + cert: fs.readFileSync(path.resolve(httpsDir, 'cert.pem'), 'ascii'), + }; + } + + serverOptions.state = this.options.enforceSecureCookies ? { + isHttpOnly: true, + isSecure: true, + isSameSite: false, + } : { + isHttpOnly: false, + isSecure: false, + isSameSite: false, + }; + + // Hapijs server creation + this.wsServer = hapi.server(serverOptions); + + this.wsServer.register(h2o2).catch(err => err && this.serverlessLog(err)); + + // Enable CORS preflight response + this.wsServer.ext('onPreResponse', (request, h) => { + if (request.headers.origin) { + const response = request.response.isBoom ? request.response.output : request.response; + + response.headers['access-control-allow-origin'] = request.headers.origin; + response.headers['access-control-allow-credentials'] = 'true'; + + if (request.method === 'options') { + response.statusCode = 200; + response.headers['access-control-expose-headers'] = 'content-type, content-length, etag'; + response.headers['access-control-max-age'] = 60 * 10; + + if (request.headers['access-control-request-headers']) { + response.headers['access-control-allow-headers'] = request.headers['access-control-request-headers']; + } + + if (request.headers['access-control-request-method']) { + response.headers['access-control-allow-methods'] = request.headers['access-control-request-method']; + } + } + } + + return h.continue; + }); + // end COPY PASTE FROM HTTP SERVER CODE + + this.wsServer.register(require('hapi-plugin-websocket')).catch(err => err && this.serverlessLog(err)); + + const doAction = (ws, connectionId, name, event, context, doDeafultAction/* , onError */) => { + const sendError = err => { + if (ws.readyState === /* OPEN */1) ws.send(JSON.stringify({ message:'Internal server error', connectionId, requestId:'1234567890' })); + debugLog(`Error in handler of action ${action}`, err); + }; + let action = this.wsActions[name]; + if (!action && doDeafultAction) action = this.wsActions.$default; + if (!action) return; + let p = null; + try { + p = action.handler(event, context, err => { + if (!err) return; + sendError(err); + }); + } + catch (err) { + sendError(err); + } + + if (p) { + p.catch(err => { + sendError(err); + }); + } + }; + + this.wsServer.route({ + method: 'POST', + path: '/', + config: { + payload: { output: 'data', parse: true, allow: 'application/json' }, + plugins: { + websocket: { + only: true, + initially: false, + connect: ({ ws, req }) => { + const parseQuery = queryString => { + const query = {}; const parts = queryString.split('?'); + if (parts.length < 2) return {}; + const pairs = parts[1].split('&'); + pairs.forEach(pair => { + const kv = pair.split('='); + query[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1] || ''); + }); + + return query; + }; + + const queryStringParameters = parseQuery(req.url); + const connection = { connectionId:randomId(), connectionTime:Date.now() }; + debugLog(`connect:${connection.connectionId}`); + + this.clients.set(ws, connection); + let event = wsHelpers.createConnectEvent('$connect', 'CONNECT', connection, this.options); + if (Object.keys(queryStringParameters).length > 0) event = { queryStringParameters, ...event }; + const context = wsHelpers.createContext('$connect'); + + doAction(ws, connection.connectionId, '$connect', event, context); + }, + disconnect: ({ ws }) => { + const connection = this.clients.get(ws); + debugLog(`disconnect:${connection.connectionId}`); + this.clients.delete(ws); + const event = wsHelpers.createDisconnectEvent('$disconnect', 'DISCONNECT', connection, this.options); + const context = wsHelpers.createContext('$disconnect', this.options); + + doAction(ws, connection.connectionId, '$disconnect', event, context); + }, + }, + }, + }, + handler: (request, h) => { + const { initially, ws } = request.websocket(); + if (!request.payload || initially) return h.response().code(204); + const connection = this.clients.get(ws); + let actionName = null; + if (this.websocketsApiRouteSelectionExpression.startsWith('$request.body.')) { + actionName = request.payload; + if (typeof actionName === 'object') { + this.websocketsApiRouteSelectionExpression.replace('$request.body.', '').split('.').forEach(key => { + if (actionName) actionName = actionName[key]; + }); + } + else actionName = null; + } + if (typeof actionName !== 'string') actionName = null; + const action = actionName || '$default'; + debugLog(`action:${action} on connection=${connection.connectionId}`); + const event = wsHelpers.createEvent(action, 'MESSAGE', connection, request.payload, this.options); + const context = wsHelpers.createContext(action, this.options); + + doAction(ws, connection.connectionId, action, event, context, true); + + return h.response().code(204); + }, + }); + this.wsServer.route({ + method: 'GET', + path: '/{path*}', + handler: (request, h) => h.response().code(426), + }); + this.wsServer.route({ + method: 'POST', + path: '/@connections/{connectionId}', + config: { payload: { parse: false } }, + handler: (request, h) => { + debugLog(`got POST to ${request.url}`); + const getByConnectionId = (map, searchValue) => { + for (const [key, connection] of map.entries()) { + if (connection.connectionId === searchValue) return key; + } + + return undefined; + }; + + const ws = getByConnectionId(this.clients, request.params.connectionId); + if (!ws) return h.response().code(410); + if (!request.payload) return ''; + ws.send(request.payload.toString()); + // console.log(`sent "${request.payload.toString().substring}" to ${request.params.connectionId}`); + debugLog(`sent data to connection:${request.params.connectionId}`); + + return ''; + }, + }); + } + + _createWsAction(fun, funName, servicePath, funOptions, event) { + let handler; // The lambda function + Object.assign(process.env, this.originalEnvironment); + + try { + if (this.options.noEnvironment) { + // This evict errors in server when we use aws services like ssm + const baseEnvironment = { + AWS_REGION: 'dev', + }; + if (!process.env.AWS_PROFILE) { + baseEnvironment.AWS_ACCESS_KEY_ID = 'dev'; + baseEnvironment.AWS_SECRET_ACCESS_KEY = 'dev'; + } + + process.env = Object.assign(baseEnvironment, process.env); + } + else { + Object.assign( + process.env, + { AWS_REGION: this.service.provider.region }, + this.service.provider.environment, + this.service.functions[funName].environment + ); + } + process.env._HANDLER = fun.handler; + handler = functionHelper.createHandler(funOptions, this.options); + } + catch (err) { + return this.serverlessLog(`Error while loading ${funName}`, err); + } + + const actionName = event.websocket.route; + const action = { funName, fun, funOptions, servicePath, handler }; + this.wsActions[actionName] = action; + this.serverlessLog(`Action '${event.websocket.route}'`); + } + _createRoutes() { let serviceRuntime = this.service.provider.runtime; const defaultContentType = 'application/json'; @@ -460,7 +694,13 @@ class Offline { }, }); // Adds a route for each http endpoint + // eslint-disable-next-line fun.events.forEach(event => { + if (event.websocket) { + this._createWsAction(fun, funName, servicePath, funOptions, event); + + return; + } if (!event.http) return; // Handle Simple http setup, ex. - http: GET users/index @@ -1122,14 +1362,27 @@ class Offline { await this.server.start(); } catch (e) { - console.error('Unexpected error while starting serverless-offline server:', e); + console.error(`Unexpected error while starting serverless-offline server on port ${this.options.port}:`, e); process.exit(1); } this.printBlankLine(); this.serverlessLog(`Offline listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port}`); - return this.server; + try { + await this.wsServer.start(); + } + catch (e) { + console.error(`Unexpected error while starting serverless-offline server on port ${this.options.port + 1}:`, e); + process.exit(1); + } + + this.printBlankLine(); + this.serverlessLog(`Offline listening on ws${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}`); + + this.printBlankLine(); + this.serverlessLog(`Offline listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port + 1}/@connections/{connectionId}`); + } end() { diff --git a/src/websocketHelpers.js b/src/websocketHelpers.js new file mode 100644 index 000000000..bb4303e5b --- /dev/null +++ b/src/websocketHelpers.js @@ -0,0 +1,106 @@ +const { randomId } = require('./utils'); + +const createRequestContext = (action, eventType, connection) => { + const now = new Date(); + const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + const requestContext = { + routeKey: action, + messageId: `${randomId()}`, + eventType, + extendedRequestId: `${randomId()}`, + requestTime: `${now.getUTCDate()}/${months[now.getUTCMonth()]}/${now.getUTCFullYear()}:${now.getUTCHours()}:${now.getUTCMinutes()}:${now.getSeconds()} +0000`, + messageDirection: 'IN', + stage: 'local', + connectedAt: connection.connectionTime, + requestTimeEpoch: now.getTime(), + identity: + { cognitoIdentityPoolId: null, + cognitoIdentityId: null, + principalOrgId: null, + cognitoAuthenticationType: null, + userArn: null, + userAgent: null, + accountId: null, + caller: null, + sourceIp: '127.0.0.1', + accessKey: null, + cognitoAuthenticationProvider: null, + user: null }, + requestId: `${randomId()}`, + domainName: 'localhost', + connectionId:connection.connectionId, + apiId: 'private', + }; + + return requestContext; +}; + +module.exports.createEvent = (action, eventType, connection, payload, options) => { + const event = { + requestContext: createRequestContext(action, eventType, connection), + body: JSON.stringify(payload), + isBase64Encoded: false, + apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, + }; + + return event; +}; + +module.exports.createConnectEvent = (action, eventType, connection, options) => { + const headers = { + Host: 'localhost', + 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits', + 'Sec-WebSocket-Key': `${randomId()}`, + 'Sec-WebSocket-Version': '13', + 'X-Amzn-Trace-Id': `Root=${randomId()}`, + 'X-Forwarded-For': '127.0.0.1', + 'X-Forwarded-Port': `${options.port + 1}`, + 'X-Forwarded-Proto': `http${options.httpsProtocol ? 's' : ''}`, + }; + const multiValueHeaders = { ...headers }; + Object.keys(multiValueHeaders).map(key => multiValueHeaders[key] = [multiValueHeaders[key]]); + const event = { + headers, + multiValueHeaders, + requestContext: createRequestContext(action, eventType, connection), + apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, + isBase64Encoded: false, + }; + + return event; +}; + +module.exports.createDisconnectEvent = (action, eventType, connection, options) => { + const headers = { + Host: 'localhost', + 'x-api-key': '', + 'x-restapi': '', + }; + const multiValueHeaders = { ...headers }; + Object.keys(multiValueHeaders).map(key => multiValueHeaders[key] = [multiValueHeaders[key]]); + const event = { + headers, + multiValueHeaders, + requestContext: createRequestContext(action, eventType, connection), + apiGatewayUrl: `http${options.httpsProtocol ? 's' : ''}://${options.host}:${options.port + 1}`, + isBase64Encoded: false, + }; + + return event; +}; + +module.exports.createContext = action => { + const context = { + awsRequestId: `offline_awsRequestId_for_${action}`, + callbackWaitsForEmptyEventLoop: true, + functionName: action, + functionVersion: '$LATEST', + invokedFunctionArn: `offline_invokedFunctionArn_for_${action}`, + invokeid: `offline_invokeid_for_${action}`, + logGroupName: `offline_logGroupName_for_${action}`, + logStreamName: `offline_logStreamName_for_${action}`, + memoryLimitInMB: '1024', + }; + + return context; +};