[
  {
    "path": ".gitignore",
    "content": "# package directories\nnode_modules\njspm_packages\n\n# Serverless directories\n.serverless"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  // Use IntelliSense to learn about possible attributes.\n  // Hover to view descriptions of existing attributes.\n  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n  \"version\": \"0.2.0\",\n  \"configurations\": [  \n    {\n      \"name\": \"Attach to SAM Local\",\n      \"type\": \"node\",\n      \"request\": \"attach\",\n      \"address\": \"localhost\",\n      \"port\": 5858,\n      \"localRoot\": \"${workspaceRoot}\",\n      \"remoteRoot\": \"/var/task\"\n    },\n    {\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"name\": \"get-index\",\n      \"program\": \"${workspaceFolder}/node_modules/.bin/sls\",\n      \"args\": [\n        \"invoke\",\n        \"local\",\n        \"-f\",\n        \"get-index\",\n        \"-p\",\n        \"examples/get-index.json\"\n      ],\n      \"env\": {\n        \"restaurants_api\": \"https://8kbasri6v6.execute-api.us-east-1.amazonaws.com/dev/restaurants\",\n        \"cognito_user_pool_id\": \"us-east-1_DfuAwa0vB\",\n        \"cognito_client_id\": \"49lunjf7j7vsgmq9lhtfn4q7ma\",\n        \"SLS_DEBUG\": \"*\",\n        \"AWS_XRAY_CONTEXT_MISSING\":\"LOG_ERROR\"\n      }\n    },\n    {\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"name\": \"get-restaurants\",\n      \"program\": \"${workspaceFolder}/node_modules/.bin/sls\",\n      \"args\": [\n        \"invoke\",\n        \"local\",\n        \"-f\",\n        \"get-restaurants\",\n        \"-d\",\n        \"{}\"\n      ],\n      \"env\": {\n        \"restaurants_table\": \"restaurants\",\n        \"SLS_DEBUG\": \"*\",\n        \"AWS_XRAY_CONTEXT_MISSING\":\"LOG_ERROR\"\n      }\n    },\n    {\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"name\": \"search-restaurants\",\n      \"program\": \"${workspaceFolder}/node_modules/.bin/sls\",\n      \"args\": [\n        \"invoke\",\n        \"local\",\n        \"-f\",\n        \"search-restaurants\",\n        \"-p\",\n        \"examples/search-restaurants.json\"\n      ],\n      \"env\": {\n        \"restaurants_table\": \"restaurants\",\n        \"SLS_DEBUG\": \"*\",\n        \"AWS_XRAY_CONTEXT_MISSING\":\"LOG_ERROR\"\n      }\n    },\n    {\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"name\": \"create-alarm\",\n      \"program\": \"${workspaceFolder}/node_modules/.bin/sls\",\n      \"args\": [\n        \"invoke\",\n        \"local\",\n        \"-f\",\n        \"auto-create-api-alarms\",\n        \"-p\",\n        \"examples/create-alarm.json\"\n      ]\n    },\n    {\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"name\": \"notify-restaurant\",\n      \"program\": \"${workspaceFolder}/node_modules/.bin/sls\",\n      \"args\": [\n        \"invoke\",\n        \"local\",\n        \"-f\",\n        \"notify-restaurant\",\n        \"-p\",\n        \"examples/notify-restaurant.json\"\n      ],\n      \"env\": {\n        \"AWS_XRAY_CONTEXT_MISSING\":\"LOG_ERROR\"\n      }\n    },\n    {\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"name\": \"place-order\",\n      \"program\": \"${workspaceFolder}/node_modules/.bin/sls\",\n      \"args\": [\n        \"invoke\",\n        \"local\",\n        \"-f\",\n        \"place-order\",\n        \"-p\",\n        \"examples/place-order.json\"\n      ],\n      \"env\": {\n        \"AWS_XRAY_CONTEXT_MISSING\":\"LOG_ERROR\"\n      }\n    },\n    {\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"name\": \"integration tests\",\n      \"program\": \"${workspaceFolder}/node_modules/.bin/mocha\",\n      \"env\": {\n        \"TEST_MODE\": \"handler\"\n      },\n      \"args\": [\n        \"tests/test_cases\",\n        \"--reporter\",\n        \"spec\"\n      ]\n    },\n    {\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"name\": \"acceptance tests\",\n      \"program\": \"${workspaceFolder}/node_modules/.bin/mocha\",\n      \"env\": {\n        \"TEST_MODE\": \"http\",\n        \"TEST_ROOT\": \"https://8kbasri6v6.execute-api.us-east-1.amazonaws.com/dev/\"\n      },\n      \"args\": [\n        \"tests/test_cases\",\n        \"-t\",\n        \"2000\",\n        \"--reporter\",\n        \"spec\"\n      ]\n    },    \n  ]\n}"
  },
  {
    "path": "README.md",
    "content": "# manning-aws-lambda-operational-patterns-and-practices\nCode for the Manning video course \"AWS Lambda: Operational Patterns and Practices\"\n"
  },
  {
    "path": "build.sh",
    "content": "#!/bin/bash\nset -e\nset -o pipefail\n\ninstruction()\n{\n  echo \"usage: ./build.sh deploy <env>\"\n  echo \"\"\n  echo \"env: eg. int, staging, prod, ...\"\n  echo \"\"\n  echo \"for example: ./deploy.sh int\"\n}\n\nif [ $# -eq 0 ]; then\n  instruction\n  exit 1\nelif [ \"$1\" = \"int-test\" ] && [ $# -eq 1 ]; then\n  npm install\n\n  npm run integration-test\nelif [ \"$1\" = \"acceptance-test\" ] && [ $# -eq 1 ]; then\n  npm install\n\n  npm run acceptance-test\nelif [ \"$1\" = \"deploy\" ] && [ $# -eq 3 ]; then\n  STAGE=$2\n  REGION=$3\n\n  npm install\n  'node_modules/.bin/sls' deploy -s $STAGE -r $REGION\nelse\n  instruction\n  exit 1\nfi"
  },
  {
    "path": "buildspec.yml",
    "content": "version: 0.2\n\nphases:\n  build:\n    commands:\n      - chmod +x build.sh\n      - ./build.sh int-test\n      - ./build.sh deploy dev\n      - ./build.sh acceptance-test"
  },
  {
    "path": "examples/create-alarm.json",
    "content": "{\n  \"version\": \"0\",\n  \"id\": \"dee9a69c-8166-1ad7-41d4-1dad201e29f6\",\n  \"detail-type\": \"AWS API Call via CloudTrail\",\n  \"source\": \"aws.apigateway\",\n  \"account\": \"374852340823\",\n  \"time\": \"2018-04-09T00:17:47Z\",\n  \"region\": \"us-east-1\",\n  \"resources\": [],\n  \"detail\": {\n      \"eventVersion\": \"1.05\",\n      \"userIdentity\": {\n          \"type\": \"IAMUser\",\n          \"principalId\": \"AIDAIRMUZZEGPO27IPFYW\",\n          \"arn\": \"arn:aws:iam::374852340823:user/yan.cui\",\n          \"accountId\": \"374852340823\",\n          \"accessKeyId\": \"ASIAJNZDKN26DXPZFYQA\",\n          \"userName\": \"yan.cui\",\n          \"sessionContext\": {\n              \"attributes\": {\n                  \"mfaAuthenticated\": \"false\",\n                  \"creationDate\": \"2018-04-09T00:17:30Z\"\n              }\n          },\n          \"invokedBy\": \"cloudformation.amazonaws.com\"\n      },\n      \"eventTime\": \"2018-04-09T00:17:47Z\",\n      \"eventSource\": \"apigateway.amazonaws.com\",\n      \"eventName\": \"CreateDeployment\",\n      \"awsRegion\": \"us-east-1\",\n      \"sourceIPAddress\": \"cloudformation.amazonaws.com\",\n      \"userAgent\": \"cloudformation.amazonaws.com\",\n      \"requestParameters\": {\n          \"restApiId\": \"8kbasri6v6\",\n          \"createDeploymentInput\": {\n              \"stageName\": \"dev\"\n          },\n          \"template\": false\n      },\n      \"responseElements\": {\n          \"id\": \"cj2y0f\",\n          \"createdDate\": \"Apr 9, 2018 12:17:47 AM\",\n          \"deploymentUpdate\": {\n              \"restApiId\": \"8kbasri6v6\",\n              \"deploymentId\": \"cj2y0f\",\n              \"template\": false\n          },\n          \"deploymentStages\": {\n              \"deploymentId\": \"cj2y0f\",\n              \"restApiId\": \"8kbasri6v6\",\n              \"template\": false,\n              \"templateSkipList\": [\n                  \"position\"\n              ]\n          },\n          \"deploymentDelete\": {\n              \"deploymentId\": \"cj2y0f\",\n              \"restApiId\": \"8kbasri6v6\",\n              \"template\": false\n          },\n          \"self\": {\n              \"deploymentId\": \"cj2y0f\",\n              \"restApiId\": \"8kbasri6v6\",\n              \"template\": false\n          }\n      },\n      \"requestID\": \"6e25bd56-3b8b-11e8-a351-e5e3d3161fe7\",\n      \"eventID\": \"a150d941-7a54-4572-97b2-0614a81fd25b\",\n      \"readOnly\": false,\n      \"eventType\": \"AwsApiCall\"\n  }\n}"
  },
  {
    "path": "examples/get-index.json",
    "content": "{\n  \"body\": null,\n  \"resource\": \"/\",\n  \"requestContext\": {\n    \"resourceId\": \"123456\",\n    \"apiId\": \"1234567890\",\n    \"resourcePath\": \"/\",\n    \"httpMethod\": \"POST\",\n    \"requestId\": \"c6af9ac6-7b61-11e6-9a41-93e8deadbeef\",\n    \"accountId\": \"123456789012\",\n    \"identity\": {\n      \"apiKey\": null,\n      \"userArn\": null,\n      \"cognitoAuthenticationType\": null,\n      \"caller\": null,\n      \"userAgent\": \"Custom User Agent String\",\n      \"user\": null,\n      \"cognitoIdentityPoolId\": null,\n      \"cognitoIdentityId\": null,\n      \"cognitoAuthenticationProvider\": null,\n      \"sourceIp\": \"127.0.0.1\",\n      \"accountId\": null\n    },\n    \"authorizer\": {\n      \"claims\": {\n        \"sub\": \"fef3b4c8-cb16-4eb0-9d7c-201ac4cb0905\",\n        \"email_verified\": \"true\",\n        \"iss\": \"https:\\/\\/cognito-idp.us-east-1.amazonaws.com\\/us-east-1_DfuAwa0vB\",\n        \"cognito:username\": \"theburningmonk\",\n        \"given_name\": \"Yan\",\n        \"aud\": \"1tf8pb1s1n53p7u608e7ic5ih8\",\n        \"event_id\": \"e727e68e-1424-11e8-be95-8774da63618a\",\n        \"token_use\": \"id\",\n        \"auth_time\": \"1518986085\",\n        \"exp\": \"Sun Feb 18 21:34:45 UTC 2018\",\n        \"iat\": \"Sun Feb 18 20:34:45 UTC 2018\",\n        \"family_name\": \"Cui\",\n        \"email\": \"theburningmonk@gmail.com\"\n      }\n    },\n    \"stage\": \"prod\"\n  },\n  \"queryStringParameters\": {\n    \"foo\": \"bar\"\n  },\n  \"headers\": {\n    \"Via\": \"1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)\",\n    \"Accept-Language\": \"en-US,en;q=0.8\",\n    \"CloudFront-Is-Desktop-Viewer\": \"true\",\n    \"CloudFront-Is-SmartTV-Viewer\": \"false\",\n    \"CloudFront-Is-Mobile-Viewer\": \"false\",\n    \"X-Forwarded-For\": \"127.0.0.1, 127.0.0.2\",\n    \"CloudFront-Viewer-Country\": \"US\",\n    \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\",\n    \"Upgrade-Insecure-Requests\": \"1\",\n    \"X-Forwarded-Port\": \"443\",\n    \"Host\": \"1234567890.execute-api.us-east-1.amazonaws.com\",\n    \"X-Forwarded-Proto\": \"https\",\n    \"X-Amz-Cf-Id\": \"cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==\",\n    \"CloudFront-Is-Tablet-Viewer\": \"false\",\n    \"Cache-Control\": \"max-age=0\",\n    \"User-Agent\": \"Custom User Agent String\",\n    \"CloudFront-Forwarded-Proto\": \"https\",\n    \"Accept-Encoding\": \"gzip, deflate, sdch\"\n  },\n  \"pathParameters\": {\n  },\n  \"httpMethod\": \"GET\",\n  \"stageVariables\": {\n    \"baz\": \"qux\"\n  },\n  \"path\": \"/\"\n}"
  },
  {
    "path": "examples/notify-restaurant.json",
    "content": "{\n  \"Records\":[\n    {\n      \"kinesis\": {\n        \"kinesisSchemaVersion\":\"1.0\",\n        \"partitionKey\":\"e4a5eae6-19f7-5284-9cb5-f97d94518e34\",\n        \"sequenceNumber\":\"49584583598311915935008171659389880270421244691116720130\",\n        \"data\":\"eyJvcmRlcklkIjoiZTRhNWVhZTYtMTlmNy01Mjg0LTljYjUtZjk3ZDk0NTE4ZTM0IiwidXNlckVtYWlsIjoidGhlYnVybmluZ21vbmtAZ21haWwuY29tIiwicmVzdGF1cmFudE5hbWUiOiJGYW5ndGFzaWEiLCJldmVudFR5cGUiOiJvcmRlcl9wbGFjZWQifQ==\",\n        \"approximateArrivalTimestamp\":1527507873.753\n      },\n      \"eventSource\":\"aws:kinesis\",\n      \"eventVersion\":\"1.0\",\n      \"eventID\":\"shardId-000000000000:49584583598311915935008171659389880270421244691116720130\",\n      \"eventName\":\"aws:kinesis:record\",\n      \"invokeIdentityArn\":\"arn:aws:iam::374852340823:role/big-mouth-dev-notify-restaurant-us-east-1-lambdaRole\",\n      \"awsRegion\":\"us-east-1\",\n      \"eventSourceARN\":\"arn:aws:kinesis:us-east-1:374852340823:stream/order-events\"\n    }\n  ]\n}"
  },
  {
    "path": "examples/place-order.json",
    "content": "{\n  \"resource\": \"\\/orders\",\n  \"path\": \"\\/orders\",\n  \"httpMethod\": \"POST\",\n  \"headers\": {\n    \"Accept\": \"*\\/*\",\n    \"Accept-Encoding\": \"gzip, deflate, br\",\n    \"Accept-Language\": \"en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7,zh-CN;q=0.6,it;q=0.5\",\n    \"Authorization\": \"eyJraWQiOiJJTnFEZWM5a2crdGx3WnNISEhabjY3dWRyXC9Hc3BrSXE4QTMwNlwvV054VTg9IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJmZWYzYjRjOC1jYjE2LTRlYjAtOWQ3Yy0yMDFhYzRjYjA5MDUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tXC91cy1lYXN0LTFfRGZ1QXdhMHZCIiwiY29nbml0bzp1c2VybmFtZSI6InRoZWJ1cm5pbmdtb25rIiwiZ2l2ZW5fbmFtZSI6IllhbiIsImF1ZCI6IjF0ZjhwYjFzMW41M3A3dTYwOGU3aWM1aWg4IiwiZXZlbnRfaWQiOiJhMTA0YmZmMi02MjBkLTExZTgtODI4My1mNzgxOGJkMzhhNWEiLCJ0b2tlbl91c2UiOiJpZCIsImF1dGhfdGltZSI6MTUyNzQ2NzEzMSwiZXhwIjoxNTI4NjgxNzA4LCJpYXQiOjE1Mjg2NzgxMDgsImZhbWlseV9uYW1lIjoiQ3VpIiwiZW1haWwiOiJ0aGVidXJuaW5nbW9ua0BnbWFpbC5jb20ifQ.ejKf8d6f9jTU72mTmpSkuj_oHIqf_ZA9dUk3_92Y-NvmkEf3AVGJWYdLm2_MoAZwYrzHNQD5cxqIN973pgem73bI9SH0pJqZcbIZ7hS0ieGPkfn_LuMnf4_M3XMPEkGxTcGObl8batYJPeW5ohPE-rfmDjOjUVZxbzemIFY6Pf9ulZTvpVLg6VweR9637GcVktnc8pEKIggG33asqj693MEr_0xX-ioBMWb7NouSD9lz_c-XgiP13LkcNeWuC1DqACRlb8IxB7EzVfFhb-80TTXdYhVKMCnH7e1y5icipdp_lfm1aV9NlhnrWSrVctTWUSnesOmELImFrDQWlH_slg\",\n    \"CloudFront-Forwarded-Proto\": \"https\",\n    \"CloudFront-Is-Desktop-Viewer\": \"true\",\n    \"CloudFront-Is-Mobile-Viewer\": \"false\",\n    \"CloudFront-Is-SmartTV-Viewer\": \"false\",\n    \"CloudFront-Is-Tablet-Viewer\": \"false\",\n    \"CloudFront-Viewer-Country\": \"GB\",\n    \"content-type\": \"application\\/json\",\n    \"Host\": \"8kbasri6v6.execute-api.us-east-1.amazonaws.com\",\n    \"origin\": \"https:\\/\\/8kbasri6v6.execute-api.us-east-1.amazonaws.com\",\n    \"Referer\": \"https:\\/\\/8kbasri6v6.execute-api.us-east-1.amazonaws.com\\/dev\\/\",\n    \"User-Agent\": \"Mozilla\\/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit\\/537.36 (KHTML, like Gecko) Chrome\\/67.0.3396.79 Safari\\/537.36\",\n    \"Via\": \"2.0 37b010671d329179b4de819b0a4d4f15.cloudfront.net (CloudFront)\",\n    \"X-Amz-Cf-Id\": \"7cCnqSDjyqrFSTv3lxs-2EjD4gZfu-MowCk4xWkKmqXlDA6RxJZOCQ==\",\n    \"X-Amzn-Trace-Id\": \"Root=1-5b1dc99e-a55a324c20164c503b94e858\",\n    \"X-Forwarded-For\": \"88.98.204.234, 52.46.38.90\",\n    \"X-Forwarded-Port\": \"443\",\n    \"X-Forwarded-Proto\": \"https\"\n  },\n  \"queryStringParameters\": null,\n  \"pathParameters\": null,\n  \"stageVariables\": null,\n  \"requestContext\": {\n    \"resourceId\": \"7wew6z\",\n    \"authorizer\": {\n      \"claims\": {\n        \"sub\": \"fef3b4c8-cb16-4eb0-9d7c-201ac4cb0905\",\n        \"email_verified\": \"true\",\n        \"iss\": \"https:\\/\\/cognito-idp.us-east-1.amazonaws.com\\/us-east-1_DfuAwa0vB\",\n        \"cognito:username\": \"theburningmonk\",\n        \"given_name\": \"Yan\",\n        \"aud\": \"1tf8pb1s1n53p7u608e7ic5ih8\",\n        \"event_id\": \"a104bff2-620d-11e8-8283-f7818bd38a5a\",\n        \"token_use\": \"id\",\n        \"auth_time\": \"1527467131\",\n        \"exp\": \"Mon Jun 11 01:48:28 UTC 2018\",\n        \"iat\": \"Mon Jun 11 00:48:28 UTC 2018\",\n        \"family_name\": \"Cui\",\n        \"email\": \"theburningmonk@gmail.com\"\n      }\n    },\n    \"resourcePath\": \"\\/orders\",\n    \"httpMethod\": \"POST\",\n    \"extendedRequestId\": \"ISxwwGuhoAMFg-Q=\",\n    \"requestTime\": \"11\\/Jun\\/2018:01:00:14 +0000\",\n    \"path\": \"\\/dev\\/orders\",\n    \"accountId\": \"374852340823\",\n    \"protocol\": \"HTTP\\/1.1\",\n    \"stage\": \"dev\",\n    \"requestTimeEpoch\": 1528678814455,\n    \"requestId\": \"cc9e10de-6d12-11e8-ad56-9338b9713eb7\",\n    \"identity\": {\n      \"cognitoIdentityPoolId\": null,\n      \"accountId\": null,\n      \"cognitoIdentityId\": null,\n      \"caller\": null,\n      \"sourceIp\": \"88.98.204.234\",\n      \"accessKey\": null,\n      \"cognitoAuthenticationType\": null,\n      \"cognitoAuthenticationProvider\": null,\n      \"userArn\": null,\n      \"userAgent\": \"Mozilla\\/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit\\/537.36 (KHTML, like Gecko) Chrome\\/67.0.3396.79 Safari\\/537.36\",\n      \"user\": null\n    },\n    \"apiId\": \"8kbasri6v6\"\n  },\n  \"body\": \"{\\\"restaurantName\\\":\\\"Fangtasia\\\"}\",\n  \"isBase64Encoded\": false\n}"
  },
  {
    "path": "examples/retry-notify-restaurant.json",
    "content": "{\n  \"Records\": [\n    {\n      \"EventVersion\": \"1.0\",\n      \"EventSubscriptionArn\": \"arn:aws:sns:EXAMPLE\",\n      \"EventSource\": \"aws:sns\",\n      \"Sns\": {\n        \"SignatureVersion\": \"1\",\n        \"Timestamp\": \"1970-01-01T00:00:00.000Z\",\n        \"Signature\": \"EXAMPLE\",\n        \"SigningCertUrl\": \"EXAMPLE\",\n        \"MessageId\": \"95df01b4-ee98-5cb9-9903-4c221d41eb5e\",\n        \"Message\": \"{\\\"orderId\\\":\\\"875ae3c4-c21c-59e6-9057-23694b75f5a2\\\",\\\"userEmail\\\":\\\"theburningmonk@gmail.com\\\",\\\"restaurantName\\\":\\\"Fangtasia\\\"}\",\n        \"MessageAttributes\": {          \n        },\n        \"Type\": \"Notification\",\n        \"UnsubscribeUrl\": \"EXAMPLE\",\n        \"TopicArn\": \"arn:aws:sns:EXAMPLE\",\n        \"Subject\": \"TestInvoke\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "examples/search-restaurants.json",
    "content": "{\n  \"body\": \"{\\\"theme\\\":\\\"cartoon\\\"}\",\n  \"resource\": \"/{proxy+}\",\n  \"requestContext\": {\n    \"resourceId\": \"123456\",\n    \"apiId\": \"1234567890\",\n    \"resourcePath\": \"/{proxy+}\",\n    \"httpMethod\": \"POST\",\n    \"requestId\": \"c6af9ac6-7b61-11e6-9a41-93e8deadbeef\",\n    \"accountId\": \"123456789012\",\n    \"identity\": {\n      \"apiKey\": null,\n      \"userArn\": null,\n      \"cognitoAuthenticationType\": null,\n      \"caller\": null,\n      \"userAgent\": \"Custom User Agent String\",\n      \"user\": null,\n      \"cognitoIdentityPoolId\": null,\n      \"cognitoIdentityId\": null,\n      \"cognitoAuthenticationProvider\": null,\n      \"sourceIp\": \"127.0.0.1\",\n      \"accountId\": null\n    },\n    \"authorizer\": {\n      \"claims\": {\n        \"sub\": \"fef3b4c8-cb16-4eb0-9d7c-201ac4cb0905\",\n        \"email_verified\": \"true\",\n        \"iss\": \"https:\\/\\/cognito-idp.us-east-1.amazonaws.com\\/us-east-1_DfuAwa0vB\",\n        \"cognito:username\": \"theburningmonk\",\n        \"given_name\": \"Yan\",\n        \"aud\": \"1tf8pb1s1n53p7u608e7ic5ih8\",\n        \"event_id\": \"e727e68e-1424-11e8-be95-8774da63618a\",\n        \"token_use\": \"id\",\n        \"auth_time\": \"1518986085\",\n        \"exp\": \"Sun Feb 18 21:34:45 UTC 2018\",\n        \"iat\": \"Sun Feb 18 20:34:45 UTC 2018\",\n        \"family_name\": \"Cui\",\n        \"email\": \"theburningmonk@gmail.com\"\n      }\n    },\n    \"stage\": \"prod\"\n  },\n  \"queryStringParameters\": {\n    \"foo\": \"bar\"\n  },\n  \"headers\": {\n    \"Via\": \"1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)\",\n    \"Accept-Language\": \"en-US,en;q=0.8\",\n    \"CloudFront-Is-Desktop-Viewer\": \"true\",\n    \"CloudFront-Is-SmartTV-Viewer\": \"false\",\n    \"CloudFront-Is-Mobile-Viewer\": \"false\",\n    \"X-Forwarded-For\": \"127.0.0.1, 127.0.0.2\",\n    \"CloudFront-Viewer-Country\": \"US\",\n    \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\",\n    \"Upgrade-Insecure-Requests\": \"1\",\n    \"X-Forwarded-Port\": \"443\",\n    \"Host\": \"1234567890.execute-api.us-east-1.amazonaws.com\",\n    \"X-Forwarded-Proto\": \"https\",\n    \"X-Amz-Cf-Id\": \"cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==\",\n    \"CloudFront-Is-Tablet-Viewer\": \"false\",\n    \"Cache-Control\": \"max-age=0\",\n    \"User-Agent\": \"Custom User Agent String\",\n    \"CloudFront-Forwarded-Proto\": \"https\",\n    \"Accept-Encoding\": \"gzip, deflate, sdch\"\n  },\n  \"pathParameters\": {\n    \"proxy\": \"path/to/resource\"\n  },\n  \"httpMethod\": \"POST\",\n  \"stageVariables\": {\n    \"baz\": \"qux\"\n  },\n  \"path\": \"/path/to/resource\"\n}"
  },
  {
    "path": "functions/accept-order.js",
    "content": "'use strict';\n\nconst co         = require('co');\nconst kinesis    = require('../lib/kinesis');\nconst log        = require('../lib/log');\nconst cloudwatch = require('../lib/cloudwatch');\nconst wrapper    = require('../middleware/wrapper');\n\nconst streamName = process.env.order_events_stream;\n\nconst handler = co.wrap(function* (event, context, cb) {\n  let req = JSON.parse(event.body);\n  log.debug(`request body is valid JSON`, { requestBody: event.body });\n\n  let restaurantName = req.restaurantName;\n  let orderId = req.orderId;\n  let userEmail = req.userEmail;\n\n  correlationIds.set('order-id', orderId);\n  correlationIds.set('restaurant-name', restaurantName);\n  correlationIds.set('user-email', userEmail);\n\n  log.debug('restaurant accepted order', { orderId, restaurantName, userEmail });\n\n  let data = {\n    orderId,\n    userEmail,\n    restaurantName,\n    eventType: 'order_accepted'\n  }\n\n  let kinesisReq = {\n    Data: JSON.stringify(data), // the SDK would base64 encode this for us\n    PartitionKey: orderId,\n    StreamName: streamName\n  };\n\n  yield cloudwatch.trackExecTime(\n    \"KinesisPutRecordLatency\",\n    () => kinesis.putRecord(kinesisReq).promise()\n  );\n\n  log.debug(`published event into Kinesis`, { eventName: 'order_accepted' });\n\n  let response = {\n    statusCode: 200,\n    body: JSON.stringify({ orderId })\n  }\n\n  cb(null, response);\n});\n\nmodule.exports.handler = wrapper(handler);"
  },
  {
    "path": "functions/create-alarms.js",
    "content": "'use strict';\n\nconst _          = require('lodash');\nconst co         = require('co');\nconst AWS        = require('aws-sdk');\nconst apigateway = new AWS.APIGateway();\nconst cloudwatch = new AWS.CloudWatch();\nconst log        = require('../lib/log');\n\nconst alarmActions = (process.env.alarm_actions || '').split(',');\nconst okAction     = (process.env.ok_actions || '').split(',');\n\nlet enableDetailedMetrics = co.wrap(function* (restApiId, stageName) {\n  let getResp = yield apigateway.getStage({ restApiId, stageName }).promise();\n  log.debug('get stage settings', getResp.methodSettings);\n\n  let isDetailedMetricsEnabled = _.get(getResp, 'methodSettings.*/*.metricsEnabled', false);\n  if (isDetailedMetricsEnabled) {\n    log.debug('detailed metrics already enabled', { restApiId, stageName });\n  } else {\n    let updateReq = {\n      restApiId,\n      stageName,\n      patchOperations: [\n        {\n          path: \"/*/*/metrics/enabled\",\n          value: \"true\",\n          op: \"replace\"\n        }\n      ]\n    };\n    yield apigateway.updateStage(updateReq).promise();\n    log.debug('enabled detailed metrics', { restApiId, stageName });\n  }\n});\n\nlet getRestEndpoints = co.wrap(function* (restApiId) {\n  let resp = yield apigateway.getResources({ restApiId }).promise();\n  log.debug('got REST resources', { restApiId });\n\n  let resourceMethods = resp.items.map(x => {\n    let methods = _.keys(x.resourceMethods);\n    return methods.map(method => ({ resource: x.path, method }));\n  });\n\n  return _.flattenDeep(resourceMethods);\n});\n\nlet getRestApiName = co.wrap(function* (restApiId) {\n  let resp = yield apigateway.getRestApi({ restApiId }).promise();\n  log.debug('got REST api', { restApiId });\n\n  return resp.name;\n});\n\nlet createAlarmsForEndpoints = co.wrap(function* (restApiId, stageName) {\n  let apiName = yield getRestApiName(restApiId);\n  log.debug(`API name is ${apiName}`, { restApiId, stageName });\n\n  let restEndpoints = yield getRestEndpoints(restApiId);\n  log.debug('got REST endpoints', { restApiId, stageName, restEndpoints });\n\n  for (let endpoint of restEndpoints) {\n    let putReq = {\n      AlarmName: `API [${apiName}] stage [${stageName}] ${endpoint.method} ${endpoint.resource} : p99 > 1s`,\n      MetricName: 'Latency',\n      Dimensions: [\n        { Name: 'ApiName',  Value: apiName },\n        { Name: 'Resource', Value: endpoint.resource },\n        { Name: 'Method',   Value: endpoint.method },\n        { Name: 'Stage',    Value: stageName }\n      ],\n      Namespace: 'AWS/ApiGateway',\n      Threshold: 1000,      // 1s\n      ComparisonOperator: 'GreaterThanThreshold',      \n      Period: 60,           // per min\n      EvaluationPeriods: 5, \n      DatapointsToAlarm: 5, // 5 consecutive mins to trigger alarm\n      ExtendedStatistic: 'p99',\n      ActionsEnabled: true,\n      AlarmActions: alarmActions,\n      AlarmDescription: `auto-generated by Lambda [${process.env.AWS_LAMBDA_FUNCTION_NAME}]`,\n      OKActions: okAction,\n      Unit: 'Milliseconds'\n    };\n    yield cloudwatch.putMetricAlarm(putReq).promise();\n  }\n\n  log.debug('auto-created latency ALARMS for REST endpoints', { restApiId, stageName, restEndpoints });\n});\n\nmodule.exports.handler = co.wrap(function* (event, context, cb) {\n  let restApiId = event.detail.requestParameters.restApiId;\n  let stageName = event.detail.requestParameters.createDeploymentInput.stageName;\n\n  yield enableDetailedMetrics(restApiId, stageName);\n\n  yield createAlarmsForEndpoints(restApiId, stageName);\n\n  cb(null, 'ok');\n});"
  },
  {
    "path": "functions/fulfill-order.js",
    "content": "'use strict';\n\nconst co         = require('co');\nconst kinesis    = require('../lib/kinesis');\nconst log        = require('../lib/log');\nconst cloudwatch = require('../lib/cloudwatch');\nconst wrapper    = require('../middleware/wrapper');\n\nconst streamName = process.env.order_events_stream;\n\nconst handler = co.wrap(function* (event, context, cb) {\n  let body = JSON.parse(event.body);\n  log.debug(`request body is valid JSON`, { requestBody: event.body });\n\n  let restaurantName = body.restaurantName;\n  let orderId = body.orderId;\n  let userEmail = body.userEmail;\n\n  correlationIds.set('order-id', orderId);\n  correlationIds.set('restaurant-name', restaurantName);\n  correlationIds.set('user-email', userEmail);\n\n  log.debug('restaurant has fulfilled order', { orderId, restaurantName, userEmail });\n\n  let data = {\n    orderId,\n    userEmail,\n    restaurantName,\n    eventType: 'order_fulfilled'\n  }\n\n  let kinesisReq = {\n    Data: JSON.stringify(data), // the SDK would base64 encode this for us\n    PartitionKey: orderId,\n    StreamName: streamName\n  };\n\n  yield cloudwatch.trackExecTime(\n    \"KinesisPutRecordLatency\", \n    () => kinesis.putRecord(kinesisReq).promise()\n  );\n\n  log.debug(`published event into Kinesis`, { eventName: 'order_fulfilled' });\n\n  let response = {\n    statusCode: 200,\n    body: JSON.stringify({ orderId })\n  }\n\n  cb(null, response);\n});\n\nmodule.exports.handler = wrapper(handler);"
  },
  {
    "path": "functions/get-index.js",
    "content": "'use strict';\n\nconst co         = require(\"co\");\nconst Promise    = require(\"bluebird\");\nconst fs         = Promise.promisifyAll(require(\"fs\"));\nconst Mustache   = require('mustache');\nconst http       = require('../lib/http');\nconst URL        = require('url');\nconst aws4       = require('../lib/aws4');\nconst log        = require('../lib/log');\nconst cloudwatch = require('../lib/cloudwatch');\nconst AWSXRay    = require('aws-xray-sdk');\nconst wrapper    = require('../middleware/wrapper');\nconst { ssm, secretsManager } = require('middy/middlewares');\n\nconst STAGE     = process.env.STAGE;\nconst awsRegion = process.env.AWS_REGION;\n\nconst days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n\nvar html;\n\nfunction* loadHtml() {\n  if (!html) {\n    html = yield fs.readFileAsync('static/index.html', 'utf-8');\n  }\n\n  return html;\n}\n\nfunction* getRestaurants(restaurantsApiUrl) {\n  let url = URL.parse(restaurantsApiUrl);\n  let opts = {\n    host: url.hostname,\n    path: url.pathname\n  };\n\n  aws4.sign(opts);\n\n  let httpReq = http({\n    uri: restaurantsApiUrl,\n    headers: opts.headers\n  });\n  \n  return new Promise((resolve, reject) => {\n    let f = co.wrap(function* (subsegment) {\n      if (subsegment) {\n        subsegment.addMetadata('url', restaurantsApiUrl);\n      }\n\n      try {\n        let body = (yield httpReq).body;\n        if (subsegment) {\n          subsegment.close();\n        }\n        resolve(body);\n      } catch (err) {\n        if (subsegment) {\n          subsegment.close(err);\n        }\n        reject(err);\n      }\n    });\n\n    // the current sub/segment\n    let segment = AWSXRay.getSegment();\n\n    AWSXRay.captureAsyncFunc(\"getting restaurants\", f, segment);\n  });\n}\n\nconst handler = co.wrap(function* (event, context, callback) {\n  yield aws4.init();\n\n  let template = yield loadHtml();\n  log.debug(\"loaded HTML template\");\n\n  let restaurants = yield cloudwatch.trackExecTime(\n    \"GetRestaurantsLatency\",\n    () => getRestaurants(context.restaurants_api)\n  );\n  log.debug(`loaded ${restaurants.length} restaurants`);\n\n  let dayOfWeek = days[new Date().getDay()];\n  let view = {\n    dayOfWeek, \n    restaurants,\n    awsRegion,\n    cognitoUserPoolId: context.cognito.user_pool_id,\n    cognitoClientId: context.cognito.client_id,\n    searchUrl: `${context.restaurants_api}/search`,\n    placeOrderUrl: `${context.orders_api}`\n  };\n  let html = Mustache.render(template, view);\n  log.debug(`rendered HTML [${html.length} bytes]`);\n\n  // uncomment this to cause function to err\n  // yield http({ uri: 'https://theburningmonk.com' });\n\n  cloudwatch.incrCount('RestaurantsReturned', restaurants.length);\n\n  const response = {\n    statusCode: 200,\n    body: html,\n    headers: {\n      'content-type': 'text/html; charset=UTF-8'\n    }\n  };\n\n  callback(null, response);\n});\n\nmodule.exports.handler = wrapper(handler)\n  .use(ssm({\n    cache: true,\n    cacheExpiryInMillis: 3 * 60 * 1000, // 3 mins\n    setToContext: true,\n    names: {\n      restaurants_api: `/bigmouth/${STAGE}/restaurants_api`,\n      orders_api: `/bigmouth/${STAGE}/orders_api`\n    }\n  }))\n  .use(secretsManager({\n    cache: true,\n    cacheExpiryInMillis: 3 * 60 * 1000, // 3 mins\n    secrets: {\n      cognito: `/bigmouth/${STAGE}/cognito`\n    }\n  }));"
  },
  {
    "path": "functions/get-restaurants.js",
    "content": "'use strict';\n\nconst co         = require('co');\nconst AWSXRay    = require('aws-xray-sdk');\nconst AWS        = AWSXRay.captureAWS(require('aws-sdk'));\nconst dynamodb   = new AWS.DynamoDB.DocumentClient();\nconst log        = require('../lib/log');\nconst cloudwatch = require('../lib/cloudwatch');\nconst wrapper    = require('../middleware/wrapper');\n\nconst defaultResults = process.env.defaultResults || 8;\nconst tableName      = process.env.restaurants_table;\n\nfunction* getRestaurants(count) {\n  let req = {\n    TableName: tableName,\n    Limit: count\n  };\n\n  let resp = yield cloudwatch.trackExecTime(\n    \"DynamoDBScanLatency\",\n    () => dynamodb.scan(req).promise()\n  );\n  return resp.Items;\n}\n\nconst handler = co.wrap(function* (event, context, cb) {\n  let restaurants = yield getRestaurants(defaultResults);\n  log.debug(`loaded ${restaurants.length} restaurants`);\n\n  cloudwatch.incrCount(\"RestaurantsReturned\", restaurants.length);\n\n  let response = {\n    statusCode: 200,\n    body: JSON.stringify(restaurants)\n  }\n\n  cb(null, response);\n});\n\nmodule.exports.handler = wrapper(handler);"
  },
  {
    "path": "functions/notify-restaurant.js",
    "content": "'use strict';\n\nconst co     = require('co');\nconst notify = require('../lib/notify');\nconst retry  = require('../lib/retry');\nconst log    = require('../lib/log');\nconst wrapper = require('../middleware/wrapper');\nconst flushMetrics  = require('../middleware/flush-metrics');\n\nconst handler = co.wrap(function* (event, context, cb) {\n  let events = context.parsedKinesisEvents;\n  let orderPlaced = events.filter(r => r.eventType === 'order_placed');\n  log.debug(`found ${orderPlaced.length} 'order_placed' events`);\n\n  for (let order of orderPlaced) {\n    order.scopeToThis();\n\n    try {\n      yield notify.restaurantOfOrder(order);\n    } catch (err) {\n      yield retry.restaurantNotification(order);\n\n      let logContext = {\n        orderId: order.orderId,\n        restaurantName: order.restaurantName,\n        userEmail: order.userEmail\n      };\n      log.warn('failed to notify restaurant of new order', logContext, err);\n    }\n\n    order.unscope();\n  }\n  \n  cb(null, \"all done\");\n});\n\nmodule.exports.handler = wrapper(handler)\n  .use(flushMetrics);"
  },
  {
    "path": "functions/notify-user.js",
    "content": "'use strict';\n\nconst co     = require('co');\nconst notify = require('../lib/notify');\nconst retry  = require('../lib/retry');\nconst log    = require('../lib/log');\nconst wrapper = require('../middleware/wrapper');\n\nconst flushMetrics  = require('../middleware/flush-metrics');\n\nconst handler = co.wrap(function* (event, context, cb) {\n  let events = context.parsedKinesisEvents;\n  let orderAccepted = events.filter(r => r.eventType === 'order_accepted');\n  log.debug(`found ${orderAccepted.length} 'order_accepted' events`);\n\n  for (let order of orderAccepted) {\n    order.scopeToThis();\n\n    try {\n      yield notify.userOfOrderAccepted(order);\n    } catch (err) {\n      yield retry.userNotification(order);\n\n      let logContext = {\n        orderId: order.orderId,\n        restaurantName: order.restaurantName,\n        userEmail: order.userEmail\n      };\n      log.warn('failed to notify user of accepted order', logContext, err);\n    }\n\n    order.unscope();\n  }\n  \n  cb(null, \"all done\");\n});\n\nmodule.exports.handler = wrapper(handler)\n  .use(flushMetrics);"
  },
  {
    "path": "functions/place-order.js",
    "content": "'use strict';\n\nconst _              = require('lodash');\nconst co             = require('co');\nconst kinesis        = require('../lib/kinesis');\nconst chance         = require('chance').Chance();\nconst log            = require('../lib/log');\nconst cloudwatch     = require('../lib/cloudwatch');\nconst correlationIds = require('../lib/correlation-ids');\nconst wrapper        = require('../middleware/wrapper');\n\nconst streamName = process.env.order_events_stream;\n\nconst UNAUTHORIZED = {\n  statusCode: 401,\n  body: \"unauthorized\"\n}\n\nconst handler = co.wrap(function* (event, context, cb) {\n  let req = JSON.parse(event.body);\n  log.debug(`request body is valid JSON`, { requestBody: event.body });\n\n  let userEmail = _.get(event, 'requestContext.authorizer.claims.email');\n  if (!userEmail) {\n    cb(null, UNAUTHORIZED);\n    log.error('unauthorized request, user email is not provided');\n\n    return;\n  }\n\n  let restaurantName = req.restaurantName;\n  let orderId = chance.guid();\n\n  correlationIds.set('order-id', orderId);\n  correlationIds.set('restaurant-name', restaurantName);\n  correlationIds.set('user-email', userEmail);\n\n  log.debug(`placing order...`, { orderId, restaurantName, userEmail });\n\n  let data = {\n    orderId,\n    userEmail,\n    restaurantName,\n    eventType: 'order_placed'\n  }\n\n  let kinesisReq = {\n    Data: JSON.stringify(data), // the SDK would base64 encode this for us\n    PartitionKey: orderId,\n    StreamName: streamName\n  };\n\n  yield cloudwatch.trackExecTime(\n    \"KinesisPutRecordLatency\",\n    () => kinesis.putRecord(kinesisReq).promise()\n  );\n\n  log.debug(`published event into Kinesis`, { eventName: 'order_placed' });\n\n  let response = {\n    statusCode: 200,\n    body: JSON.stringify({ orderId })\n  }\n\n  cb(null, response);\n});\n\nmodule.exports.handler = wrapper(handler);"
  },
  {
    "path": "functions/retry-notify-restaurant.js",
    "content": "'use strict';\n\nconst co         = require('co');\nconst notify     = require('../lib/notify');\nconst log        = require('../lib/log');\nconst cloudwatch = require('../lib/cloudwatch');\nconst wrapper    = require('../middleware/wrapper');\n\nconst flushMetrics  = require('../middleware/flush-metrics');\n\nconst handler = co.wrap(function* (event, context, cb) {\n  let order = JSON.parse(event.Records[0].Sns.Message);\n  order.retried = true;\n\n  let logContext = {\n    orderId: order.orderId,\n    restaurantName: order.restaurantName,\n    userEmail: order.userEmail,\n    retry: true\n  };\n\n  try {\n    yield notify.restaurantOfOrder(order);\n    cb(null, \"all done\");\n  } catch (err) {\n    log.warn('failed to notify restaurant of new order', logContext, err);\n    \n    cb(err);\n  } finally {\n    cloudwatch.incrCount(\"NotifyRestaurantRetried\");\n  }\n});\n\nmodule.exports.handler = wrapper(handler)\n  .use(flushMetrics);"
  },
  {
    "path": "functions/retry-notify-user.js",
    "content": "'use strict';\n\nconst co         = require('co');\nconst notify     = require('../lib/notify');\nconst log        = require('../lib/log');\nconst cloudwatch = require('../lib/cloudwatch');\nconst wrapper    = require('../middleware/wrapper');\n\nconst flushMetrics = require('../middleware/flush-metrics');\n\nconst handler = co.wrap(function* (event, context, cb) {\n  let order = JSON.parse(event.Records[0].Sns.Message);\n  order.retried = true;\n\n  let logContext = {\n    orderId: order.orderId,\n    restaurantName: order.restaurantName,\n    userEmail: order.userEmail,\n    retry: true\n  };\n\n  try {\n    yield notify.userOfOrderAccepted(order);\n    cb(null, \"all done\");\n  } catch (err) {\n    log.warn('failed to notify user of accepted order', logContext, err);\n\n    cb(err);\n  } finally {\n    cloudwatch.incrCount(\"NotifyUserRetried\");\n  }\n});\n\nmodule.exports.handler = wrapper(handler)\n  .use(flushMetrics);"
  },
  {
    "path": "functions/search-restaurants.js",
    "content": "'use strict';\n\nconst co         = require('co');\nconst AWSXRay    = require('aws-xray-sdk');\nconst AWS        = AWSXRay.captureAWS(require('aws-sdk'));\nconst dynamodb   = new AWS.DynamoDB.DocumentClient();\nconst log        = require('../lib/log');\nconst cloudwatch = require('../lib/cloudwatch');\nconst wrapper    = require('../middleware/wrapper');\n\nconst defaultResults = process.env.defaultResults || 8;\nconst tableName      = process.env.restaurants_table;\n\nfunction* findRestaurantsByTheme(theme, count) {\n  let req = {\n    TableName: tableName,\n    Limit: count,\n    FilterExpression: \"contains(themes, :theme)\",\n    ExpressionAttributeValues: { \":theme\": theme }\n  };\n\n  let resp = yield cloudwatch.trackExecTime(\n    \"DynamoDBScanLatency\", \n    () => dynamodb.scan(req).promise()\n  );\n  return resp.Items;\n}\n\nconst handler = co.wrap(function* (event, context, cb) {\n  let req = JSON.parse(event.body);\n  log.debug(`request body is valid JSON`, { requestBody: event.body });\n\n  let restaurants = yield findRestaurantsByTheme(req.theme, defaultResults);\n  log.debug(`found ${restaurants.length} restaurants`);\n\n  cloudwatch.incrCount(\"RestaurantsFound\", restaurants.length);\n\n  let response = {\n    statusCode: 200,\n    body: JSON.stringify(restaurants)\n  }\n\n  cb(null, response);\n});\n\nmodule.exports.handler = wrapper(handler);"
  },
  {
    "path": "lib/aws4.js",
    "content": "var aws4 = exports,    \n    url = require('url'),\n    querystring = require('querystring'),\n    crypto = require('crypto'),\n    lru = require('./lru'),\n    credentialsCache = lru(1000),\n    awscred = require('./awscred')\n\n// http://docs.amazonwebservices.com/general/latest/gr/signature-version-4.html\n\nfunction hmac(key, string, encoding) {\n  return crypto.createHmac('sha256', key).update(string, 'utf8').digest(encoding)\n}\n\nfunction hash(string, encoding) {\n  return crypto.createHash('sha256').update(string, 'utf8').digest(encoding)\n}\n\n// This function assumes the string has already been percent encoded\nfunction encodeRfc3986(urlEncodedString) {\n  return urlEncodedString.replace(/[!'()*]/g, function(c) {\n    return '%' + c.charCodeAt(0).toString(16).toUpperCase()\n  })\n}\n\n// request: { path | body, [host], [method], [headers], [service], [region] }\n// credentials: { accessKeyId, secretAccessKey, [sessionToken] }\nfunction RequestSigner(request, credentials) {\n\n  if (typeof request === 'string') request = url.parse(request)\n\n  var headers = request.headers = (request.headers || {}),\n      hostParts = this.matchHost(request.hostname || request.host || headers.Host || headers.host)\n\n  this.request = request\n  this.credentials = credentials || this.defaultCredentials()\n\n  this.service = request.service || hostParts[0] || ''\n  this.region = request.region || hostParts[1] || 'us-east-1'\n\n  // SES uses a different domain from the service name\n  if (this.service === 'email') this.service = 'ses'\n\n  if (!request.method && request.body)\n    request.method = 'POST'\n\n  if (!headers.Host && !headers.host) {\n    headers.Host = request.hostname || request.host || this.createHost()\n\n    // If a port is specified explicitly, use it as is\n    if (request.port)\n      headers.Host += ':' + request.port\n  }\n  if (!request.hostname && !request.host)\n    request.hostname = headers.Host || headers.host\n\n  this.isCodeCommitGit = this.service === 'codecommit' && request.method === 'GIT'\n}\n\nRequestSigner.prototype.matchHost = function(host) {\n  var match = (host || '').match(/([^\\.]+)\\.(?:([^\\.]*)\\.)?amazonaws\\.com$/)\n  var hostParts = (match || []).slice(1, 3)\n\n  // ES's hostParts are sometimes the other way round, if the value that is expected\n  // to be region equals ‘es’ switch them back\n  // e.g. search-cluster-name-aaaa00aaaa0aaa0aaaaaaa0aaa.us-east-1.es.amazonaws.com\n  if (hostParts[1] === 'es')\n    hostParts = hostParts.reverse()\n\n  return hostParts\n}\n\n// http://docs.aws.amazon.com/general/latest/gr/rande.html\nRequestSigner.prototype.isSingleRegion = function() {\n  // Special case for S3 and SimpleDB in us-east-1\n  if (['s3', 'sdb'].indexOf(this.service) >= 0 && this.region === 'us-east-1') return true\n\n  return ['cloudfront', 'ls', 'route53', 'iam', 'importexport', 'sts']\n    .indexOf(this.service) >= 0\n}\n\nRequestSigner.prototype.createHost = function() {\n  var region = this.isSingleRegion() ? '' :\n        (this.service === 's3' && this.region !== 'us-east-1' ? '-' : '.') + this.region,\n      service = this.service === 'ses' ? 'email' : this.service\n  return service + region + '.amazonaws.com'\n}\n\nRequestSigner.prototype.prepareRequest = function() {\n  this.parsePath()\n\n  var request = this.request, headers = request.headers, query\n\n  if (request.signQuery) {\n\n    this.parsedPath.query = query = this.parsedPath.query || {}\n\n    if (this.credentials.sessionToken)\n      query['X-Amz-Security-Token'] = this.credentials.sessionToken\n\n    if (this.service === 's3' && !query['X-Amz-Expires'])\n      query['X-Amz-Expires'] = 86400\n\n    if (query['X-Amz-Date'])\n      this.datetime = query['X-Amz-Date']\n    else\n      query['X-Amz-Date'] = this.getDateTime()\n\n    query['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'\n    query['X-Amz-Credential'] = this.credentials.accessKeyId + '/' + this.credentialString()\n    query['X-Amz-SignedHeaders'] = this.signedHeaders()\n\n  } else {\n\n    if (!request.doNotModifyHeaders && !this.isCodeCommitGit) {\n      if (request.body && !headers['Content-Type'] && !headers['content-type'])\n        headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'\n\n      if (request.body && !headers['Content-Length'] && !headers['content-length'])\n        headers['Content-Length'] = Buffer.byteLength(request.body)\n\n      if (this.credentials.sessionToken && !headers['X-Amz-Security-Token'] && !headers['x-amz-security-token'])\n        headers['X-Amz-Security-Token'] = this.credentials.sessionToken\n\n      if (this.service === 's3' && !headers['X-Amz-Content-Sha256'] && !headers['x-amz-content-sha256'])\n        headers['X-Amz-Content-Sha256'] = hash(this.request.body || '', 'hex')\n\n      if (headers['X-Amz-Date'] || headers['x-amz-date'])\n        this.datetime = headers['X-Amz-Date'] || headers['x-amz-date']\n      else\n        headers['X-Amz-Date'] = this.getDateTime()\n    }\n\n    delete headers.Authorization\n    delete headers.authorization\n  }\n}\n\nRequestSigner.prototype.sign = function() {\n  if (!this.parsedPath) this.prepareRequest()\n\n  if (this.request.signQuery) {\n    this.parsedPath.query['X-Amz-Signature'] = this.signature()\n  } else {\n    this.request.headers.Authorization = this.authHeader()\n  }\n\n  this.request.path = this.formatPath()\n\n  return this.request\n}\n\nRequestSigner.prototype.getDateTime = function() {\n  if (!this.datetime) {\n    var headers = this.request.headers,\n      date = new Date(headers.Date || headers.date || new Date)\n\n    this.datetime = date.toISOString().replace(/[:\\-]|\\.\\d{3}/g, '')\n\n    // Remove the trailing 'Z' on the timestamp string for CodeCommit git access\n    if (this.isCodeCommitGit) this.datetime = this.datetime.slice(0, -1)\n  }\n  return this.datetime\n}\n\nRequestSigner.prototype.getDate = function() {\n  return this.getDateTime().substr(0, 8)\n}\n\nRequestSigner.prototype.authHeader = function() {\n  return [\n    'AWS4-HMAC-SHA256 Credential=' + this.credentials.accessKeyId + '/' + this.credentialString(),\n    'SignedHeaders=' + this.signedHeaders(),\n    'Signature=' + this.signature(),\n  ].join(', ')\n}\n\nRequestSigner.prototype.signature = function() {\n  var date = this.getDate(),\n      cacheKey = [this.credentials.secretAccessKey, date, this.region, this.service].join(),\n      kDate, kRegion, kService, kCredentials = credentialsCache.get(cacheKey)\n  if (!kCredentials) {\n    kDate = hmac('AWS4' + this.credentials.secretAccessKey, date)\n    kRegion = hmac(kDate, this.region)\n    kService = hmac(kRegion, this.service)\n    kCredentials = hmac(kService, 'aws4_request')\n    credentialsCache.set(cacheKey, kCredentials)\n  }\n  return hmac(kCredentials, this.stringToSign(), 'hex')\n}\n\nRequestSigner.prototype.stringToSign = function() {\n  return [\n    'AWS4-HMAC-SHA256',\n    this.getDateTime(),\n    this.credentialString(),\n    hash(this.canonicalString(), 'hex'),\n  ].join('\\n')\n}\n\nRequestSigner.prototype.canonicalString = function() {\n  if (!this.parsedPath) this.prepareRequest()\n\n  var pathStr = this.parsedPath.path,\n      query = this.parsedPath.query,\n      headers = this.request.headers,\n      queryStr = '',\n      normalizePath = this.service !== 's3',\n      decodePath = this.service === 's3' || this.request.doNotEncodePath,\n      decodeSlashesInPath = this.service === 's3',\n      firstValOnly = this.service === 's3',\n      bodyHash\n\n  if (this.service === 's3' && this.request.signQuery) {\n    bodyHash = 'UNSIGNED-PAYLOAD'\n  } else if (this.isCodeCommitGit) {\n    bodyHash = ''\n  } else {\n    bodyHash = headers['X-Amz-Content-Sha256'] || headers['x-amz-content-sha256'] ||\n      hash(this.request.body || '', 'hex')\n  }\n\n  if (query) {\n    queryStr = encodeRfc3986(querystring.stringify(Object.keys(query).sort().reduce(function(obj, key) {\n      if (!key) return obj\n      obj[key] = !Array.isArray(query[key]) ? query[key] :\n        (firstValOnly ? query[key][0] : query[key].slice().sort())\n      return obj\n    }, {})))\n  }\n  if (pathStr !== '/') {\n    if (normalizePath) pathStr = pathStr.replace(/\\/{2,}/g, '/')\n    pathStr = pathStr.split('/').reduce(function(path, piece) {\n      if (normalizePath && piece === '..') {\n        path.pop()\n      } else if (!normalizePath || piece !== '.') {\n        if (decodePath) piece = querystring.unescape(piece)\n        path.push(encodeRfc3986(querystring.escape(piece)))\n      }\n      return path\n    }, []).join('/')\n    if (pathStr[0] !== '/') pathStr = '/' + pathStr\n    if (decodeSlashesInPath) pathStr = pathStr.replace(/%2F/g, '/')\n  }\n\n  return [\n    this.request.method || 'GET',\n    pathStr,\n    queryStr,\n    this.canonicalHeaders() + '\\n',\n    this.signedHeaders(),\n    bodyHash,\n  ].join('\\n')\n}\n\nRequestSigner.prototype.canonicalHeaders = function() {\n  var headers = this.request.headers\n  function trimAll(header) {\n    return header.toString().trim().replace(/\\s+/g, ' ')\n  }\n  return Object.keys(headers)\n    .sort(function(a, b) { return a.toLowerCase() < b.toLowerCase() ? -1 : 1 })\n    .map(function(key) { return key.toLowerCase() + ':' + trimAll(headers[key]) })\n    .join('\\n')\n}\n\nRequestSigner.prototype.signedHeaders = function() {\n  return Object.keys(this.request.headers)\n    .map(function(key) { return key.toLowerCase() })\n    .sort()\n    .join(';')\n}\n\nRequestSigner.prototype.credentialString = function() {\n  return [\n    this.getDate(),\n    this.region,\n    this.service,\n    'aws4_request',\n  ].join('/')\n}\n\nRequestSigner.prototype.defaultCredentials = function() {\n  var env = process.env\n\n  return {\n    accessKeyId: env.AWS_ACCESS_KEY_ID || env.AWS_ACCESS_KEY,\n    secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY,\n    sessionToken: env.AWS_SESSION_TOKEN,\n  }\n}\n\nRequestSigner.prototype.parsePath = function() {\n  var path = this.request.path || '/',\n      queryIx = path.indexOf('?'),\n      query = null\n\n  if (queryIx >= 0) {\n    query = querystring.parse(path.slice(queryIx + 1))\n    path = path.slice(0, queryIx)\n  }\n\n  // S3 doesn't always encode characters > 127 correctly and\n  // all services don't encode characters > 255 correctly\n  // So if there are non-reserved chars (and it's not already all % encoded), just encode them all\n  if (/[^0-9A-Za-z!'()*\\-._~%/]/.test(path)) {\n    path = path.split('/').map(function(piece) {\n      return querystring.escape(querystring.unescape(piece))\n    }).join('/')\n  }\n\n  this.parsedPath = {\n    path: path,\n    query: query,\n  }\n}\n\nRequestSigner.prototype.formatPath = function() {\n  var path = this.parsedPath.path,\n      query = this.parsedPath.query\n\n  if (!query) return path\n\n  // Services don't support empty query string keys\n  if (query[''] != null) delete query['']\n\n  return path + '?' + encodeRfc3986(querystring.stringify(query))\n}\n\naws4.RequestSigner = RequestSigner\n\naws4.sign = function(request, credentials) {\n  return new RequestSigner(request, credentials).sign()\n}\n\nvar isInitialized = false;\naws4.init = function() {\n  return new Promise(function(resolve, reject) {\n    if (isInitialized) {\n      return resolve()\n    }\n\n    if (process.env.AWS_ACCESS_KEY_ID) {\n      isInitialized = true;\n      return resolve()\n    } else {    \n      console.error(\"initializing AWS credentials...\")\n  \n      awscred.load(function(error, result) {\n        if (error) {\n          reject(error)\n        } else {\n          let cred = result.credentials;\n          process.env.AWS_ACCESS_KEY_ID = cred.accessKeyId;\n          process.env.AWS_SECRET_ACCESS_KEY = cred.secretAccessKey;\n    \n          if (cred.sessionToken) {\n            process.env.AWS_SESSION_TOKEN = cred.sessionToken;\n          }\n  \n          resolve()\n          isInitialized = true\n        }\n      })\n    }\n  })  \n}"
  },
  {
    "path": "lib/awscred.js",
    "content": "var fs = require('fs'),\n    path = require('path'),\n    http = require('http'),\n    env = process.env\n\nexports.credentialsCallChain = [\n  loadCredentialsFromEnv,\n  loadCredentialsFromIniFile,\n  loadCredentialsFromEc2Metadata,\n  loadCredentialsFromEcs,\n]\n\nexports.regionCallChain = [\n  loadRegionFromEnv,\n  loadRegionFromIniFile,\n]\n\nexports.load = exports.loadCredentialsAndRegion = loadCredentialsAndRegion\nexports.loadCredentials = loadCredentials\nexports.loadRegion = loadRegion\nexports.loadRegionSync = loadRegionSync\nexports.loadCredentialsFromEnv = loadCredentialsFromEnv\nexports.loadRegionFromEnv = loadRegionFromEnv\nexports.loadRegionFromEnvSync = loadRegionFromEnvSync\nexports.loadCredentialsFromIniFile = loadCredentialsFromIniFile\nexports.loadRegionFromIniFile = loadRegionFromIniFile\nexports.loadRegionFromIniFileSync = loadRegionFromIniFileSync\nexports.loadCredentialsFromEc2Metadata = loadCredentialsFromEc2Metadata\nexports.loadProfileFromIniFile = loadProfileFromIniFile\nexports.loadProfileFromIniFileSync = loadProfileFromIniFileSync\nexports.merge = merge\n\nfunction loadCredentialsAndRegion(options, cb) {\n  if (!cb) { cb = options; options = {} }\n  cb = once(cb)\n\n  var out = {}, callsRemaining = 2\n\n  function checkDone(propName) {\n    return function(err, data) {\n      if (err) return cb(err)\n      out[propName] = data\n      if (!--callsRemaining) return cb(null, out)\n    }\n  }\n\n  loadCredentials(options, checkDone('credentials'))\n\n  loadRegion(options, checkDone('region'))\n}\n\nfunction loadCredentials(options, cb) {\n  if (!cb) { cb = options; options = {} }\n  var credentialsCallChain = options.credentialsCallChain || exports.credentialsCallChain\n\n  function nextCall(i) {\n    credentialsCallChain[i](options, function(err, credentials) {\n      if (err) return cb(err)\n\n      if (credentials.accessKeyId && credentials.secretAccessKey)\n        return cb(null, credentials)\n\n      if (i >= credentialsCallChain.length - 1)\n        return cb(null, {})\n\n      nextCall(i + 1)\n    })\n  }\n  nextCall(0)\n}\n\nfunction loadRegion(options, cb) {\n  if (!cb) { cb = options; options = {} }\n  var regionCallChain = options.regionCallChain || exports.regionCallChain\n\n  function nextCall(i) {\n    regionCallChain[i](options, function(err, region) {\n      if (err) return cb(err)\n\n      if (region)\n        return cb(null, region)\n\n      if (i >= regionCallChain.length - 1)\n        return cb(null, 'us-east-1')\n\n      nextCall(i + 1)\n    })\n  }\n  nextCall(0)\n}\n\nfunction loadRegionSync(options) {\n  return loadRegionFromEnvSync(options) || loadRegionFromIniFileSync(options)\n}\n\nfunction loadCredentialsFromEnv(options, cb) {\n  if (!cb) { cb = options; options = {} }\n\n  cb(null, {\n    accessKeyId: env.AWS_ACCESS_KEY_ID || env.AMAZON_ACCESS_KEY_ID || env.AWS_ACCESS_KEY,\n    secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AMAZON_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY,\n    sessionToken: env.AWS_SESSION_TOKEN || env.AMAZON_SESSION_TOKEN,\n  })\n}\n\nfunction loadRegionFromEnv(options, cb) {\n  if (!cb) { cb = options; options = {} }\n\n  cb(null, loadRegionFromEnvSync())\n}\n\nfunction loadRegionFromEnvSync() {\n  return env.AWS_REGION || env.AMAZON_REGION || env.AWS_DEFAULT_REGION\n}\n\nfunction loadCredentialsFromIniFile(options, cb) {\n  if (!cb) { cb = options; options = {} }\n\n  loadProfileFromIniFile(options, 'credentials', function(err, profile) {\n    if (err) return cb(err)\n    cb(null, {\n      accessKeyId: profile.aws_access_key_id,\n      secretAccessKey: profile.aws_secret_access_key,\n      sessionToken: profile.aws_session_token,\n    })\n  })\n}\n\nfunction loadRegionFromIniFile(options, cb) {\n  if (!cb) { cb = options; options = {} }\n\n  loadProfileFromIniFile(options, 'config', function(err, profile) {\n    if (err) return cb(err)\n    cb(null, profile.region)\n  })\n}\n\nfunction loadRegionFromIniFileSync(options) {\n  return loadProfileFromIniFileSync(options || {}, 'config').region\n}\n\nvar TIMEOUT_CODES = ['ECONNRESET', 'ETIMEDOUT', 'EHOSTUNREACH', 'Unknown system errno 64']\nvar ec2Callbacks = []\nvar ecsCallbacks = []\n\nfunction loadCredentialsFromEcs(options, cb) {\n  if (!cb) { cb = options; options = {} }\n\n  ecsCallbacks.push(cb)\n  if (ecsCallbacks.length > 1) return // only want one caller at a time\n\n  cb = function(err, credentials) {\n    ecsCallbacks.forEach(function(cb) { cb(err, credentials) })\n    ecsCallbacks = []\n  }\n\n  if (options.timeout == null) options.timeout = 5000\n  options.host = '169.254.170.2'\n  options.path = process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI\n\n  return request(options, function(err, res, data) {    \n    if (err && ~TIMEOUT_CODES.indexOf(err.code)) return cb(null, {})\n    if (err) return cb(err)\n\n    if (res.statusCode != 200)\n      return cb(new Error('Failed to fetch IAM role: ' + res.statusCode + ' ' + data))\n\n    try { data = JSON.parse(data) } catch (e) { }\n\n    if (res.statusCode != 200)\n      return cb(new Error('Failed to fetch IAM credentials: ' + res.statusCode + ' ' + data))    \n\n    cb(null, {\n      accessKeyId: data.AccessKeyId,\n      secretAccessKey: data.SecretAccessKey,\n      sessionToken: data.Token,\n      expiration: new Date(data.Expiration),\n    })\n  })\n}\n\nfunction loadCredentialsFromEc2Metadata(options, cb) {\n  if (!cb) { cb = options; options = {} }\n\n  ec2Callbacks.push(cb)\n  if (ec2Callbacks.length > 1) return // only want one caller at a time\n\n  cb = function(err, credentials) {\n    ec2Callbacks.forEach(function(cb) { cb(err, credentials) })\n    ec2Callbacks = []\n  }\n\n  if (options.timeout == null) options.timeout = 5000\n  options.host = '169.254.169.254'\n  options.path = '/latest/meta-data/iam/security-credentials/'\n\n  return request(options, function(err, res, data) {\n    if (err && ~TIMEOUT_CODES.indexOf(err.code)) return cb(null, {})\n    if (err) return cb(err)\n\n    if (res.statusCode != 200)\n      return cb(new Error('Failed to fetch IAM role: ' + res.statusCode + ' ' + data))\n\n    options.path += data.split('\\n')[0]\n    request(options, function(err, res, data) {\n      if (err) return cb(err)\n\n      try { data = JSON.parse(data) } catch (e) { }\n\n      if (res.statusCode != 200 || data.Code != 'Success')\n        return cb(new Error('Failed to fetch IAM credentials: ' + res.statusCode + ' ' + data))\n\n      cb(null, {\n        accessKeyId: data.AccessKeyId,\n        secretAccessKey: data.SecretAccessKey,\n        sessionToken: data.Token,\n        expiration: new Date(data.Expiration),\n      })\n    })\n  })\n}\n\nfunction loadProfileFromIniFile(options, defaultFilename, cb) {\n  var filename = options.filename || path.join(resolveHome(), '.aws', defaultFilename),\n      profile = options.profile || resolveProfile()\n\n  fs.readFile(filename, 'utf8', function(err, data) {\n    if (err && err.code == 'ENOENT') return cb(null, {})\n    if (err) return cb(err)\n    cb(null, parseAwsIni(data)[profile] || {})\n  })\n}\n\nfunction loadProfileFromIniFileSync(options, defaultFilename) {\n  var filename = options.filename || path.join(resolveHome(), '.aws', defaultFilename),\n      profile = options.profile || resolveProfile(),\n      data\n\n  try {\n    data = fs.readFileSync(filename, 'utf8')\n  } catch (err) {\n    if (err.code == 'ENOENT') return {}\n    throw err\n  }\n\n  return parseAwsIni(data)[profile] || {}\n}\n\nfunction merge(obj, options, cb) {\n  if (!cb) { cb = options; options = {} }\n\n  var needRegion = !obj.region\n  var needCreds = !obj.credentials || !obj.credentials.accessKeyId || !obj.credentials.secretAccessKey\n\n  function loadCreds(cb) {\n    if (needRegion && needCreds) {\n      return loadCredentialsAndRegion(options, cb)\n    } else if (needRegion) {\n      return loadRegion(options, function(err, region) { cb(err, {region: region}) })\n    } else if (needCreds) {\n      return loadCredentials(options, function(err, credentials) { cb(err, {credentials: credentials}) })\n    }\n    cb(null, {})\n  }\n\n  loadCreds(function(err, creds) {\n    if (err) return cb(err)\n\n    if (creds.region) obj.region = creds.region\n    if (creds.credentials) {\n      if (!obj.credentials) {\n        obj.credentials = creds.credentials\n      } else {\n        Object.keys(creds.credentials).forEach(function(key) {\n          if (!obj.credentials[key]) obj.credentials[key] = creds.credentials[key]\n        })\n      }\n    }\n\n    cb()\n  })\n}\n\nfunction resolveProfile() {\n  return env.AWS_PROFILE || env.AMAZON_PROFILE || 'default'\n}\n\nfunction resolveHome() {\n  return env.HOME || env.USERPROFILE || ((env.HOMEDRIVE || 'C:') + env.HOMEPATH)\n}\n\n// Fairly strict INI parser – will only deal with alpha keys, must be within sections\nfunction parseAwsIni(ini) {\n  var section,\n      out = Object.create(null),\n      re = /^\\[([^\\]]+)\\]\\s*$|^([a-z_]+)\\s*=\\s*(.+?)\\s*$/,\n      lines = ini.split(/\\r?\\n/)\n\n  lines.forEach(function(line) {\n    var match = line.match(re)\n    if (!match) return\n    if (match[1]) {\n      section = match[1]\n      if (out[section] == null) out[section] = Object.create(null)\n    } else if (section) {\n      out[section][match[2]] = match[3]\n    }\n  })\n\n  return out\n}\n\nfunction request(options, cb) {\n  cb = once(cb)\n\n  var req = http.request(options, function(res) {\n    var data = ''\n    res.setEncoding('utf8')\n    res.on('error', cb)\n    res.on('data', function(chunk) { data += chunk })\n    res.on('end', function() { cb(null, res, data) })\n  }).on('error', cb)\n\n  if (options.timeout != null) {\n    req.setTimeout(options.timeout)\n    req.on('timeout', function() { req.abort() })\n  }\n\n  req.end()\n}\n\nfunction once(cb) {\n  var called = false\n  return function() {\n    if (called) return\n    called = true\n    cb.apply(this, arguments)\n  }\n}"
  },
  {
    "path": "lib/cloudwatch.js",
    "content": "'use strict';\n\nconst co         = require('co');\nconst AWSXRay    = require('aws-xray-sdk');\nconst AWS        = AWSXRay.captureAWS(require('aws-sdk'));\nconst log        = require('./log');\nconst cloudwatch = new AWS.CloudWatch();\n\nconst namespace = 'big-mouth';\nconst async     = (process.env.async_metrics || 'false') === 'true';\n\n// the Lambda execution environment defines a number of env variables:\n//    https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html\n// and the serverless framework also defines a STAGE env variable too\nconst dimensions = \n  [\n    { Name: 'Function', Value: process.env.AWS_LAMBDA_FUNCTION_NAME\t},\n    { Name: 'Version', Value: process.env.AWS_LAMBDA_FUNCTION_VERSION\t},\n    { Name: 'Stage', Value: process.env.STAGE\t}\n  ]\n  .filter(dim => dim.Value);\n\nlet countMetrics = {};\nlet timeMetrics  = {};\n\nfunction getCountMetricData(name, value) {\n  return {\n    MetricName : name,\n    Dimensions : dimensions,\n    Unit       : 'Count',\n    Value      : value\n  };\n}\n\nfunction getTimeMetricData(name, statsValues) {\n  return {\n    MetricName      : name,\n    Dimensions      : dimensions,\n    Unit            : 'Milliseconds',\n    StatisticValues : statsValues\n  };\n}\n\nfunction getCountMetricDatum() {\n  let keys = Object.keys(countMetrics);\n  if (keys.length === 0) {\n    return [];\n  }\n\n  let metricDatum = keys.map(key => getCountMetricData(key, countMetrics[key]));\n  countMetrics = {}; // zero out the recorded count metrics\n  return metricDatum;\n}\n\nfunction getTimeMetricDatum() {\n  let keys = Object.keys(timeMetrics);\n  if (keys.length === 0) {\n    return [];\n  }\n\n  let metricDatum = keys.map(key => getTimeMetricData(key, timeMetrics[key]));\n  timeMetrics = {}; // zero out the recorded time metrics\n  return metricDatum;\n}\n\nlet flush = co.wrap(function* () {\n  let countDatum = getCountMetricDatum();\n  let timeDatum  = getTimeMetricDatum();\n  let allDatum   = countDatum.concat(timeDatum);\n\n  if (allDatum.length == 0) { return; }\n\n  let metricNames = allDatum.map(x => x.MetricName).join(',');\n  log.debug(`flushing [${allDatum.length}] metrics to CloudWatch: ${metricNames}`);\n\n  var params = {\n    MetricData: allDatum,\n    Namespace: namespace\n  };\n\n  try {\n    yield cloudwatch.putMetricData(params).promise();\n    log.debug(`flushed [${allDatum.length}] metrics to CloudWatch: ${metricNames}`);\n  } catch (err) {\n    log.warn(`cloudn't flush [${allDatum.length}] CloudWatch metrics`, null, err);\n  }  \n});\n\nfunction clear() {\n  countMetrics = {};\n  timeMetrics = {};\n}\n\nfunction incrCount(metricName, count) {\n  count = count || 1;\n\n  if (async) {\n    console.log(`MONITORING|${count}|count|${metricName}|${namespace}`);\n  } else {\n    if (countMetrics[metricName]) {\n      countMetrics[metricName] += count;\n    } else {\n      countMetrics[metricName] = count;\n    }\n  }\n}\n\nfunction recordTimeInMillis(metricName, ms) {\n  if (!ms) {\n    return;\n  }\n\n  log.debug(`new execution time for [${metricName}] : ${ms} milliseconds`);\n\n  if (async) {\n    console.log(`MONITORING|${ms}|milliseconds|${metricName}|${namespace}`);\n  } else {\n    if (timeMetrics[metricName]) {\n      let metric = timeMetrics[metricName];\n      metric.Sum         += ms;\n      metric.Maximum     = Math.max(metric.Maximum, ms);\n      metric.Minimum     = Math.min(metric.Minimum, ms);\n      metric.SampleCount += 1;\n    } else {\n      let statsValues = {\n        Maximum     : ms,\n        Minimum     : ms,\n        SampleCount : 1,\n        Sum         : ms\n      };\n      timeMetrics[metricName] = statsValues;\n    }\n  }\n}\n\nfunction trackExecTime(metricName, f) {\n  if (!f || typeof f !== \"function\") {\n    throw new Error('cloudWatch.trackExecTime requires a function, eg. () => 42');\n  }\n\n  if (!metricName) {\n    throw new Error('cloudWatch.trackExecTime requires a metric name, eg. \"CloudSearch-latency\"');\n  }\n\n  let start = new Date().getTime(), end;\n  let res = f();\n  \n  // anything with a 'then' function can be considered a Promise...\n  // http://stackoverflow.com/a/27746324/55074\n  if (!res.hasOwnProperty('then')) {\n    end = new Date().getTime();\n    recordTimeInMillis(metricName, end-start);\n    return res;\n  } else {\n    return res.then(x => {\n      end = new Date().getTime();\n      recordTimeInMillis(metricName, end-start);\n      return x;\n    });\n  }\n}\n\nmodule.exports = {\n  flush,\n  clear,\n  incrCount,\n  trackExecTime,\n  recordTimeInMillis\n};"
  },
  {
    "path": "lib/correlation-ids.js",
    "content": "'use strict';\n\nlet clearAll = () => global.CONTEXT = undefined;\n\nlet replaceAllWith = ctx => global.CONTEXT = ctx;\n\nlet set = (key, value) => {\n  if (!key.startsWith(\"x-correlation-\")) {\n    key = \"x-correlation-\" + key;\n  }\n\n  if (!global.CONTEXT) {\n    global.CONTEXT = {};\n  }\n\n  global.CONTEXT[key] = value;\n};\n\nlet get = () => global.CONTEXT || {};\n\nmodule.exports = {\n  clearAll: clearAll,\n  replaceAllWith: replaceAllWith,\n  set: set,\n  get: get\n};"
  },
  {
    "path": "lib/http.js",
    "content": "'use strict';\n\nconst correlationIds = require('./correlation-ids');\nconst http = require('superagent-promise')(require('superagent'), Promise);\n\nfunction getRequest (options) {\n  let uri    = options.uri;\n  let method = options.method || '';\n\n  switch (method.toLowerCase()) {\n    case '':\n    case 'get':\n      return http.get(uri);\n    case 'head':\n      return http.head(uri);\n    case 'post':\n      return http.post(uri);\n    case 'put':\n      return http.put(uri);\n    case 'delete':\n      return http.del(uri);\n    default:\n      throw new Error(`unsupported method : ${method.toLowerCase()}`);\n  }\n}\n\nfunction setHeaders (request, headers) {\n  let headerNames = Object.keys(headers);\n  headerNames.forEach(h => request = request.set(h, headers[h]));\n\n  return request;\n}\n\nfunction setQueryStrings (request, qs) {\n  if (!qs) {\n    return request;\n  }\n  \n  return request.query(qs); \n}\n\nfunction setBody (request, body) {\n  if (!body) {\n    return request;\n  }\n\n  return request.send(body);\n}\n\n// options: { \n//    uri     : string\n//    method  : GET (default) | POST | PUT | HEAD\n//    headers : object\n//    qs      : object\n//    body    : object\n//  }\nlet Req = (options) => {\n  if (!options) {\n    throw new Error('no HTTP request options is provided');\n  }\n\n  if (!options.uri) {\n    throw new Error('no HTTP uri is specified');\n  }\n\n  const context = correlationIds.get();\n\n  // copy the provided headers last so it overrides the values from the context\n  let headers = Object.assign({}, context, options.headers);\n\n  let request = getRequest(options);\n\n  request = setHeaders(request, headers);\n  request = setQueryStrings(request, options.qs);\n  request = setBody(request, options.body);\n\n  return request\n    .catch(e => {\n      if (e.response && e.response.error) {\n        throw e.response.error;\n      }\n      \n      throw e;\n    });\n};\n\nmodule.exports = Req;"
  },
  {
    "path": "lib/kinesis.js",
    "content": "'use strict';\n\nconst _              = require('lodash');\nconst AWSXRay        = require('aws-xray-sdk');\nconst AWS            = AWSXRay.captureAWS(require('aws-sdk'));\nconst Kinesis        = new AWS.Kinesis();\nconst log            = require('./log');\nconst correlationIds = require('./correlation-ids');\n\nfunction tryJsonParse(data) {\n  if (!_.isString(data)) {\n    return null;\n  }\n\n  try {\n    return JSON.parse(data);\n  } catch (err) {\n    log.warn('only JSON string data can be modified to insert correlation IDs');\n    return null;\n  }\n}\n\nfunction addCorrelationIds(data) {\n  // only do with with JSON string data\n  const payload = tryJsonParse(data);\n  if (!payload) {\n    return data;\n  }\n\n  const context = correlationIds.get();  \n  const newData = Object.assign({ __context__: context }, payload);\n  return JSON.stringify(newData);\n}\n\nfunction putRecord(params, cb) {\n  const newData = addCorrelationIds(params.Data);\n  params = Object.assign({}, params, { Data: newData });\n\n  return Kinesis.putRecord(params, cb);\n};\n\nconst client = Object.assign({}, Kinesis, { putRecord });\n\nmodule.exports = client;"
  },
  {
    "path": "lib/log.js",
    "content": "'use strict';\n\nconst correlationIds = require('./correlation-ids');\n\nconst LogLevels = {\n  DEBUG : 0,\n  INFO  : 1,\n  WARN  : 2,\n  ERROR : 3\n};\n\n// most of these are available through the Node.js execution environment for Lambda\n// see https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html\nconst DEFAULT_CONTEXT = {\n  awsRegion: process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION,\n  functionName: process.env.AWS_LAMBDA_FUNCTION_NAME,\n  functionVersion: process.env.AWS_LAMBDA_FUNCTION_VERSION,\n  functionMemorySize: process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE,\n  stage: process.env.ENVIRONMENT || process.env.STAGE\n};\n\nfunction getContext () {\n  // if there's a global variable for all the current request context then use it\n  const context = correlationIds.get();\n  if (context) {\n    // note: this is a shallow copy, which is ok as we're not going to mutate anything\n    return Object.assign({}, DEFAULT_CONTEXT, context);\n  }\n\n  return DEFAULT_CONTEXT;\n}\n\n// default to debug if not specified\nfunction logLevelName() {\n  return process.env.log_level || 'DEBUG';\n}\n\nfunction isEnabled (level) {\n  return level >= LogLevels[logLevelName()];\n}\n\nfunction appendError(params, err) {\n  if (!err) {\n    return params;\n  }\n\n  return Object.assign(\n    {},\n    params || {}, \n    { errorName: err.name, errorMessage: err.message, stackTrace: err.stack }\n  );\n}\n\nfunction log (levelName, message, params) {\n  if (!isEnabled(LogLevels[levelName])) {\n    return;\n  }\n\n  let context = getContext();\n  let logMsg = Object.assign({}, context, params);\n  logMsg.level = levelName;\n  logMsg.message = message;\n\n  console.log(JSON.stringify(logMsg));\n}\n\nmodule.exports.debug = (msg, params) => log('DEBUG', msg, params);\nmodule.exports.info  = (msg, params) => log('INFO',  msg, params);\nmodule.exports.warn  = (msg, params, error) => log('WARN',  msg, appendError(params, error));\nmodule.exports.error = (msg, params, error) => log('ERROR', msg, appendError(params, error));\n\nmodule.exports.enableDebug = () => {\n  const oldLevel = process.env.log_level;\n  process.env.log_level = 'DEBUG';\n\n  // return a function to perform the rollback\n  return () => {\n    process.env.log_level = oldLevel;\n  }\n};"
  },
  {
    "path": "lib/lru.js",
    "content": "module.exports = function(size) {\n  return new LruCache(size)\n}\n\nfunction LruCache(size) {\n  this.capacity = size | 0\n  this.map = Object.create(null)\n  this.list = new DoublyLinkedList()\n}\n\nLruCache.prototype.get = function(key) {\n  var node = this.map[key]\n  if (node == null) return undefined\n  this.used(node)\n  return node.val\n}\n\nLruCache.prototype.set = function(key, val) {\n  var node = this.map[key]\n  if (node != null) {\n    node.val = val\n  } else {\n    if (!this.capacity) this.prune()\n    if (!this.capacity) return false\n    node = new DoublyLinkedNode(key, val)\n    this.map[key] = node\n    this.capacity--\n  }\n  this.used(node)\n  return true\n}\n\nLruCache.prototype.used = function(node) {\n  this.list.moveToFront(node)\n}\n\nLruCache.prototype.prune = function() {\n  var node = this.list.pop()\n  if (node != null) {\n    delete this.map[node.key]\n    this.capacity++\n  }\n}\n\n\nfunction DoublyLinkedList() {\n  this.firstNode = null\n  this.lastNode = null\n}\n\nDoublyLinkedList.prototype.moveToFront = function(node) {\n  if (this.firstNode == node) return\n\n  this.remove(node)\n\n  if (this.firstNode == null) {\n    this.firstNode = node\n    this.lastNode = node\n    node.prev = null\n    node.next = null\n  } else {\n    node.prev = null\n    node.next = this.firstNode\n    node.next.prev = node\n    this.firstNode = node\n  }\n}\n\nDoublyLinkedList.prototype.pop = function() {\n  var lastNode = this.lastNode\n  if (lastNode != null) {\n    this.remove(lastNode)\n  }\n  return lastNode\n}\n\nDoublyLinkedList.prototype.remove = function(node) {\n  if (this.firstNode == node) {\n    this.firstNode = node.next\n  } else if (node.prev != null) {\n    node.prev.next = node.next\n  }\n  if (this.lastNode == node) {\n    this.lastNode = node.prev\n  } else if (node.next != null) {\n    node.next.prev = node.prev\n  }\n}\n\n\nfunction DoublyLinkedNode(key, val) {\n  this.key = key\n  this.val = val\n  this.prev = null\n  this.next = null\n}"
  },
  {
    "path": "lib/notify.js",
    "content": "'use strict';\n\nconst _          = require('lodash');\nconst co         = require('co');\nconst sns        = require('./sns');\nconst kinesis    = require('./kinesis');\nconst chance     = require('chance').Chance();\nconst log        = require('./log');\nconst cloudwatch = require('./cloudwatch');\n\nconst streamName         = process.env.order_events_stream;\nconst restaurantTopicArn = process.env.restaurant_notification_topic;\nconst userTopicArn       = process.env.user_notification_topic;\n\nlet notifyRestaurantOfOrder = co.wrap(function* (order) {\n  try {\n    if (chance.bool({likelihood: 75})) { // 75% chance of failure\n      throw new Error(\"boom\");\n    }\n\n    let snsReq = {\n      Message: JSON.stringify(order),\n      TopicArn: restaurantTopicArn\n    };\n    yield cloudwatch.trackExecTime(\n      \"SnsPublishLatency\",\n      () => sns.publish(snsReq).promise()\n    );\n\n    let logContext = {\n      orderId: order.orderId,\n      restaurantName: order.restaurantName,\n      userEmail: order.userEmail\n    };\n    log.debug('notified restaurant of new order', logContext);\n\n    let data = _.clone(order);\n    data.eventType = 'restaurant_notified';\n\n    let kinesisReq = {\n      Data: JSON.stringify(data), // the SDK would base64 encode this for us\n      PartitionKey: order.orderId,\n      StreamName: streamName\n    };\n    yield cloudwatch.trackExecTime(\n      \"KinesisPutRecordLatency\",\n      () => kinesis.putRecord(kinesisReq).promise()\n    );\n    log.debug('published event into Kinesis', { eventName: 'restaurant_notified' });\n\n    cloudwatch.incrCount('NotifyRestaurantSuccess');\n  } catch (err) {\n    cloudwatch.incrCount('NotifyRestaurantFailed');\n    throw err;\n  }\n});\n\nlet notifyUserOfOrderAccepted = co.wrap(function* (order) {\n  try {\n    if (chance.bool({likelihood: 75})) { // 75% chance of failure\n      throw new Error(\"boom\");\n    }\n  \n    let snsReq = {\n      Message: JSON.stringify(order),\n      TopicArn: userTopicArn\n    };\n    yield cloudwatch.trackExecTime(\n      \"SnsPublishLatency\", \n      () => sns.publish(snsReq).promise()\n    );\n  \n    let logContext = {\n      orderId: order.orderId,\n      restaurantName: order.restaurantName,\n      userEmail: order.userEmail\n    };\n    log.debug('notified user of accepted order', logContext);\n  \n    let data = _.clone(order);\n    data.eventType = 'user_notified';\n  \n    let kinesisReq = {\n      Data: JSON.stringify(data), // the SDK would base64 encode this for us\n      PartitionKey: order.orderId,\n      StreamName: streamName\n    };\n    yield cloudwatch.trackExecTime(\n      \"KinesisPutRecordLatency\",\n      () => kinesis.putRecord(kinesisReq).promise()\n    );\n    log.debug(`published event into Kinesis`, { eventName: 'user_notified' });\n  \n    cloudwatch.incrCount('NotifyUserSuccess');\n  } catch (err) {\n    cloudwatch.incrCount('NotifyUserFailed');\n    throw err;\n  }\n});\n\nmodule.exports = {\n  restaurantOfOrder: notifyRestaurantOfOrder,\n  userOfOrderAccepted: notifyUserOfOrderAccepted\n};"
  },
  {
    "path": "lib/retry.js",
    "content": "'use strict';\n\nconst co         = require('co');\nconst sns        = require('./sns');\nconst log        = require('./log');\nconst cloudwatch = require('./cloudwatch');\n\nconst restaurantRetryTopicArn = process.env.restaurant_notification_retry_topic;\nconst userRetryTopicArn       = process.env.user_notification_retry_topic;\n\nlet retryRestaurantNotification = co.wrap(function* (order) {\n  let snsReq = {\n    Message: JSON.stringify(order),\n    TopicArn: restaurantRetryTopicArn\n  };\n  yield cloudwatch.trackExecTime(\n    \"SnsPublishLatency\",\n    () => sns.publish(snsReq).promise()\n  );\n\n  let logContext = {\n    orderId: order.orderId,\n    restaurantName: order.restaurantName,\n    userEmail: order.userEmail\n  };\n  log.debug('queued restaurant notification for retry', logContext);\n\n  cloudwatch.incrCount(\"NotifyRestaurantQueued\");\n});\n\nlet retryUserNotification = co.wrap(function* (order) {\n  let snsReq = {\n    Message: JSON.stringify(order),\n    TopicArn: userRetryTopicArn\n  };\n  yield cloudwatch.trackExecTime(\n    \"SnsPublishLatency\",\n    () => sns.publish(snsReq).promise()\n  );\n\n  let logContext = {\n    orderId: order.orderId,\n    restaurantName: order.restaurantName,\n    userEmail: order.userEmail\n  };\n  log.debug('queued user notification for retry', logContext);\n\n  cloudwatch.incrCount(\"NotifyUserQueued\");\n});\n\nmodule.exports = {\n  restaurantNotification: retryRestaurantNotification,\n  userNotification: retryUserNotification\n};"
  },
  {
    "path": "lib/sns.js",
    "content": "'use strict';\n\nconst AWSXRay        = require('aws-xray-sdk');\nconst AWS            = AWSXRay.captureAWS(require('aws-sdk'));\nconst SNS            = new AWS.SNS();\nconst correlationIds = require('./correlation-ids');\n\nfunction addCorrelationIds(messageAttributes) {\n  let attributes = {};\n  let context = correlationIds.get();\n  for (let key in context) {\n    attributes[key] = {\n      DataType: 'String',\n      StringValue: context[key]\n    };\n  }\n\n  // use `attribtues` as base so if the user's message attributes would override\n  // our correlation IDs\n  return Object.assign(attributes, messageAttributes || {});\n}\n\nfunction publish(params, cb) {\n  const newMessageAttributes = addCorrelationIds(params.MessageAttributes);\n  params = Object.assign(params, { MessageAttributes: newMessageAttributes });\n\n  return SNS.publish(params, cb);\n};\n\nconst client = Object.assign({}, SNS, { publish });\n\nmodule.exports = client;"
  },
  {
    "path": "middleware/capture-correlation-ids.js",
    "content": "'use strict';\n\nconst correlationIds = require('../lib/correlation-ids');\nconst log = require('../lib/log');\n\nfunction captureHttp(headers, awsRequestId, sampleDebugLogRate) {\n  if (!headers) {\n   log.warn(`Request ${awsRequestId} is missing headers`);\n   return;\n  }\n\n  let context = { awsRequestId };\n  for (const header in headers) {\n    if (header.toLowerCase().startsWith('x-correlation-')) {\n      context[header] = headers[header];\n    }\n  }\n \n  if (!context['x-correlation-id']) {\n    context['x-correlation-id'] = awsRequestId;\n  }\n\n  // forward the original User-Agent on\n  if (headers['User-Agent']) {\n    context['User-Agent'] = headers['User-Agent'];\n  }\n\n  if (headers['Debug-Log-Enabled']) {\n    context['Debug-Log-Enabled'] = headers['Debug-Log-Enabled'];\n  } else {\n    context['Debug-Log-Enabled'] = Math.random() < sampleDebugLogRate ? 'true' : 'false';\n  }\n\n  correlationIds.replaceAllWith(context);\n}\n\nfunction parsePayload (record) {\n  let json = new Buffer(record.kinesis.data, 'base64').toString('utf8');\n  return JSON.parse(json);  \n}\n\nfunction captureKinesis(event, context, sampleDebugLogRate) {\n  const awsRequestId = context.awsRequestId;\n  const events = event\n    .Records\n    .map(parsePayload)\n    .map(record => {\n      // the wrapped kinesis client would put the correlation IDs as part of \n      // the payload as a special __context__ property\n      let recordContext = record.__context__ || {};\n      recordContext.awsRequestId = awsRequestId;\n\n      delete record.__context__;\n\n      if (!recordContext['x-correlation-id']) {\n        recordContext['x-correlation-id'] = awsRequestId;\n      }\n\n      if (!recordContext['Debug-Log-Enabled']) {\n        recordContext['Debug-Log-Enabled'] = Math.random() < sampleDebugLogRate ? 'true' : 'false';\n      }\n\n      let debugLog = recordContext['Debug-Log-Enabled'] === 'true';\n\n      let oldContext = undefined;\n      let debugLogRollback = undefined;\n\n      // lets you add more correlation IDs for just this record\n      record.addToScope = (key, value) => {\n        if (!key.startsWith(\"x-correlation-\")) {\n          key = \"x-correlation-\" + key;\n        }\n\n        recordContext[key] = value;\n        correlationIds.set(key, value);\n      }\n\n      record.scopeToThis = () => {\n        if (!oldContext) {\n          oldContext = correlationIds.get();\n          correlationIds.replaceAllWith(recordContext);\n        }\n\n        if (debugLog) {\n          debugLogRollback = log.enableDebug();\n        }\n      };\n\n      record.unscope = () => {\n        if (oldContext) {\n          correlationIds.replaceAllWith(oldContext);\n        }\n\n        if (debugLogRollback) {\n          debugLogRollback();\n        }\n      }\n\n      return record;\n    });\n\n  context.parsedKinesisEvents = events;\n\n  correlationIds.replaceAllWith({ awsRequestId });\n}\n\nfunction captureSns(records, awsRequestId, sampleDebugLogRate) {\n  let context = { awsRequestId };\n\n  const snsRecord = records[0].Sns;\n  const msgAttributes = snsRecord.MessageAttributes;\n  \n  for (var msgAttribute in msgAttributes) {\n    if (msgAttribute.toLowerCase().startsWith('x-correlation-')) {\n      context[msgAttribute] = msgAttributes[msgAttribute].Value;\n    }\n\n    if (msgAttribute === 'User-Agent') {\n      context['User-Agent'] = msgAttributes['User-Agent'].Value;\n    }\n\n    if (msgAttribute === 'Debug-Log-Enabled') {\n      context['Debug-Log-Enabled'] = msgAttributes['Debug-Log-Enabled'].Value;\n    }\n  }\n \n  if (!context['x-correlation-id']) {\n    context['x-correlation-id'] = awsRequestId;\n  }\n\n  if (!context['Debug-Log-Enabled']) {\n    context['Debug-Log-Enabled'] = Math.random() < sampleDebugLogRate ? 'true' : 'false';\n  }\n\n  correlationIds.replaceAllWith(context);\n}\n\nfunction isApiGatewayEvent(event) {\n  return event.hasOwnProperty('httpMethod')\n}\n\nfunction isKinesisEvent(event) {\n  if (!event.hasOwnProperty('Records')) {\n    return false;\n  }\n  \n  if (!Array.isArray(event.Records)) {\n    return false;\n  }\n\n  return event.Records[0].eventSource === 'aws:kinesis';\n}\n\nfunction isSnsEvent(event) {\n  if (!event.hasOwnProperty('Records')) {\n    return false;\n  }\n  \n  if (!Array.isArray(event.Records)) {\n    return false;\n  }\n\n  return event.Records[0].EventSource === 'aws:sns';\n}\n\nmodule.exports = (config) => {\n  const sampleDebugLogRate = config.sampleDebugLogRate || 0.01;\n\n  return {\n    before: (handler, next) => {      \n      correlationIds.clearAll();\n\n      if (isApiGatewayEvent(handler.event)) {\n        captureHttp(handler.event.headers, handler.context.awsRequestId, sampleDebugLogRate);\n      } else if (isKinesisEvent(handler.event)) {\n        captureKinesis(handler.event, handler.context, sampleDebugLogRate);\n      } else if (isSnsEvent(handler.event)) {\n        captureSns(handler.event.Records, handler.context.awsRequestId, sampleDebugLogRate);\n      }\n\n      next()\n    }\n  };\n};"
  },
  {
    "path": "middleware/flush-metrics.js",
    "content": "'use strict';\n\nconst log        = require('../lib/log');\nconst cloudwatch = require('../lib/cloudwatch');\n\nmodule.exports = {\n  after: (handler, next) => {\n    cloudwatch.flush().then(_ => next());\n  },\n  onError: (handler, next) => {\n    cloudwatch.flush().then(_ => next(handler.error));\n  }\n};"
  },
  {
    "path": "middleware/function-shield.js",
    "content": "'use strict';\n\nconst FuncShield = require('@puresec/function-shield');\n\nmodule.exports = () => {\n  return {\n    before: (handler, next) => {\n      FuncShield.configure({\n        policy: {\n          outbound_connectivity: \"block\",\n          read_write_tmp: \"block\", \n          create_child_process: \"block\"\n        },\n        token: process.env.FUNCTION_SHIELD_TOKEN\n      });\n\n      next();\n    }\n  };\n};"
  },
  {
    "path": "middleware/sample-logging.js",
    "content": "'use strict';\n\nconst correlationIds = require('../lib/correlation-ids');\nconst log = require('../lib/log');\n\n// config should be { sampleRate: double } where sampleRate is between 0.0-1.0\nmodule.exports = (config) => {\n  let rollback = undefined;\n\n  const isDebugEnabled = () => {\n    const context = correlationIds.get();\n    if (context['Debug-Log-Enabled'] === 'true') {\n      return true;\n    }\n\n    return config.sampleRate && Math.random() <= config.sampleRate;\n  }\n\n  return {\n    before: (handler, next) => {\n      if (isDebugEnabled()) {\n        rollback = log.enableDebug();\n      }\n\n      next();\n    },\n    after: (handler, next) => {\n      if (rollback) {\n        rollback();\n      }\n\n      next();\n    },\n    onError: (handler, next) => {\n      let awsRequestId = handler.context.awsRequestId;\n      let invocationEvent = JSON.stringify(handler.event);\n      log.error('invocation failed', { awsRequestId, invocationEvent }, handler.error);\n\n      next(handler.error);\n    }\n  };\n};"
  },
  {
    "path": "middleware/wrapper.js",
    "content": "'use strict';\n\nconst middy = require('middy');\nconst sampleLogging = require('./sample-logging');\nconst captureCorrelationIds = require('./capture-correlation-ids');\nconst functionShield = require('./function-shield');\n\nmodule.exports = f => {\n  return middy(f)\n    .use(captureCorrelationIds({ sampleDebugLogRate: 0.01 }))\n    .use(sampleLogging({ sampleRate: 0.01 }))\n    .use(functionShield());\n};"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"big-mouth\",\n    \"version\": \"1.0.0\",\n    \"description\": \"\",\n    \"main\": \"index.js\",\n    \"scripts\": {\n        \"integration-test\": \"env TEST_MODE=handler ./node_modules/.bin/mocha tests/test_cases --reporter spec --retries 3 --timeout 10000\",\n        \"acceptance-test\": \"env TEST_MODE=http TEST_ROOT=https://i3c5h755j0.execute-api.us-east-1.amazonaws.com/dev ./node_modules/.bin/mocha tests/test_cases --reporter spec --retries 3 --timeout 10000\"\n    },\n    \"author\": \"\",\n    \"license\": \"ISC\",\n    \"dependencies\": {\n        \"@puresec/function-shield\": \"^1.0.7\",\n        \"aws-xray-sdk\": \"^1.2.0\",\n        \"bluebird\": \"^3.5.1\",\n        \"chance\": \"^1.0.13\",\n        \"co\": \"^4.6.0\",\n        \"middy\": \"^0.17.1\",\n        \"mustache\": \"^2.3.0\",\n        \"superagent\": \"^3.8.1\",\n        \"superagent-promise\": \"^1.1.0\"\n    },\n    \"devDependencies\": {\n        \"aws-sdk\": \"^2.302.0\",\n        \"chai\": \"^4.1.2\",\n        \"cheerio\": \"^1.0.0-rc.2\",\n        \"lodash\": \"^4.17.10\",\n        \"mocha\": \"^4.0.1\",\n        \"serverless\": \"^1.30.1\",\n        \"serverless-iam-roles-per-function\": \"^0.1.5\",\n        \"serverless-plugin-aws-alerts\": \"^1.2.4\",\n        \"serverless-plugin-canary-deployments\": \"^0.4.3\",\n        \"serverless-plugin-tracing\": \"^2.0.0\",\n        \"serverless-pseudo-parameters\": \"^1.2.5\",\n        \"serverless-sam\": \"0.0.3\"\n    }\n}\n"
  },
  {
    "path": "seed-restaurants.js",
    "content": "'use strict';\n\nconst co = require('co');\nconst AWS = require('aws-sdk');\nAWS.config.region = 'us-east-1';\nconst dynamodb = new AWS.DynamoDB.DocumentClient();\n\nlet restaurants = [\n  { \n    name: \"Fangtasia\", \n    image: \"https://d2qt42rcwzspd6.cloudfront.net/manning/fangtasia.png\", \n    themes: [\"true blood\"] \n  },\n  { \n    name: \"Shoney's\", \n    image: \"https://d2qt42rcwzspd6.cloudfront.net/manning/shoney's.png\", \n    themes: [\"cartoon\", \"rick and morty\"] \n  },\n  { \n    name: \"Freddy's BBQ Joint\", \n    image: \"https://d2qt42rcwzspd6.cloudfront.net/manning/freddy's+bbq+joint.png\", \n    themes: [\"netflix\", \"house of cards\"] \n  },\n  { \n    name: \"Pizza Planet\", \n    image: \"https://d2qt42rcwzspd6.cloudfront.net/manning/pizza+planet.png\", \n    themes: [\"netflix\", \"toy story\"] \n  },\n  { \n    name: \"Leaky Cauldron\", \n    image: \"https://d2qt42rcwzspd6.cloudfront.net/manning/leaky+cauldron.png\", \n    themes: [\"movie\", \"harry potter\"] \n  },\n  { \n    name: \"Lil' Bits\", \n    image: \"https://d2qt42rcwzspd6.cloudfront.net/manning/lil+bits.png\", \n    themes: [\"cartoon\", \"rick and morty\"] \n  },\n  { \n    name: \"Fancy Eats\", \n    image: \"https://d2qt42rcwzspd6.cloudfront.net/manning/fancy+eats.png\", \n    themes: [\"cartoon\", \"rick and morty\"] \n  },\n  { \n    name: \"Don Cuco\", \n    image: \"https://d2qt42rcwzspd6.cloudfront.net/manning/don%20cuco.png\", \n    themes: [\"cartoon\", \"rick and morty\"] \n  },\n];\n\nlet putReqs = restaurants.map(x => ({\n  PutRequest: {\n    Item: x\n  }\n}));\n\nlet req = {\n  RequestItems: {\n    'restaurants': putReqs\n  }\n};\ndynamodb.batchWrite(req).promise().then(() => console.log(\"all done\"));"
  },
  {
    "path": "serverless.yml",
    "content": "service: big-mouth\n\nplugins:\n  - serverless-pseudo-parameters\n  - serverless-sam\n  - serverless-iam-roles-per-function\n  - serverless-plugin-tracing\n  - serverless-plugin-canary-deployments\n  - serverless-plugin-aws-alerts\n\ncustom: \n  stage: ${opt:stage, self:provider.stage}\n  region: ${opt:region}\n  logLevel:\n    prod: WARN\n    default: DEBUG\n  serverless-iam-roles-per-function:\n    defaultInherit: true\n  alerts:\n    stages:\n      - dev\n      - staging\n      - production\n    dashboards: false\n    alarms:\n      - functionThrottles\n      - functionErrors\n\nprovider:\n  name: aws\n  runtime: nodejs6.10\n  tracing: true\n  environment:\n    log_level: ${self:custom.logLevel.${self:custom.stage}, self:custom.logLevel.default}\n    STAGE: ${self:custom.stage}\n    FUNCTION_SHIELD_TOKEN: ${ssm:/bigmouth/${self:custom.stage}/function_shield_token~true}\n  iamRoleStatements:\n    - Effect: Allow\n      Action: cloudwatch:PutMetricData\n      Resource: '*'\n    - Effect: Allow\n      Action:\n        - 'xray:PutTraceSegments'\n        - 'xray:PutTelemetryRecords'\n      Resource: '*'\n    - Effect: Allow\n      Action: codedeploy:*\n      Resource: '*'\n\nfunctions:\n  get-index:\n    handler: functions/get-index.handler\n    events:\n      - http:\n          path: /\n          method: get\n    environment:      \n      async_metrics: true\n    deploymentSettings:\n      type: Linear10PercentEvery1Minute\n      alias: Live\n      alarms:\n        - GetDashindexFunctionErrorsAlarm\n    #iamRoleStatementsInherit: true #optionally inherit shared permissions\n    iamRoleStatements:\n      - Effect: Allow\n        Action: execute-api:Invoke\n        Resource: arn:aws:execute-api:#{AWS::Region}:#{AWS::AccountId}:*/*/GET/restaurants\n      - Effect: Allow\n        Action: ssm:GetParameters*\n        Resource: arn:aws:ssm:#{AWS::Region}:#{AWS::AccountId}:parameter/bigmouth/${self:custom.stage}/*\n      - Effect: Allow\n        Action: secretsmanager:GetSecretValue\n        Resource: arn:aws:secretsmanager:#{AWS::Region}:#{AWS::AccountId}:secret:/bigmouth/${self:custom.stage}/*\n\n  get-restaurants:\n    handler: functions/get-restaurants.handler\n    events:\n      - http:\n          path: /restaurants/\n          method: get\n          authorizer: aws_iam\n    environment:\n      restaurants_table: restaurants\n      async_metrics: true\n    iamRoleStatements:\n      - Effect: Allow\n        Action: dynamodb:scan\n        Resource: arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/restaurants\n\n  search-restaurants:\n    handler: functions/search-restaurants.handler\n    events:\n      - http:\n          path: /restaurants/search\n          method: post\n          authorizer:\n            arn: arn:aws:cognito-idp:#{AWS::Region}:#{AWS::AccountId}:userpool/us-east-1_DfuAwa0vB\n    environment:\n      restaurants_table: restaurants\n      async_metrics: true\n    iamRoleStatements:\n      - Effect: Allow\n        Action: dynamodb:scan\n        Resource: arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/restaurants\n\n  place-order:\n    handler: functions/place-order.handler\n    events:\n      - http:\n          path: /orders\n          method: post\n          authorizer:\n            arn: arn:aws:cognito-idp:#{AWS::Region}:#{AWS::AccountId}:userpool/us-east-1_DfuAwa0vB\n    environment:\n      order_events_stream: order-events\n      async_metrics: true\n    iamRoleStatements:\n      - Effect: Allow\n        Action: kinesis:PutRecord\n        Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events\n\n  notify-restaurant:\n    handler: functions/notify-restaurant.handler\n    events:\n      - stream:\n          arn: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events\n    environment:\n      order_events_stream: order-events\n      restaurant_notification_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification\n      restaurant_notification_retry_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification-retry\n    iamRoleStatements:\n      - Effect: Allow\n        Action: kinesis:PutRecord\n        Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events\n      - Effect: Allow\n        Action: sns:Publish\n        Resource:\n          - arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification\n          - arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification-retry\n      \n  retry-notify-restaurant:\n    handler: functions/retry-notify-restaurant.handler\n    events:\n      - sns: restaurant-notification-retry\n    environment:\n      order_events_stream: order-events\n      restaurant_notification_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification\n    onError: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification-dlq\n    iamRoleStatements:\n      - Effect: Allow\n        Action: kinesis:PutRecord\n        Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events\n      - Effect: Allow\n        Action: sns:Publish\n        Resource:\n          - arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification\n    \n  accept-order:\n    handler: functions/accept-order.handler\n    events:\n      - http:\n          path: /orders/accept\n          method: post\n    environment:\n      order_events_stream: order-events\n      async_metrics: true\n    iamRoleStatements:\n      - Effect: Allow\n        Action: kinesis:PutRecord\n        Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events\n\n  notify-user:\n    handler: functions/notify-user.handler\n    events:\n      - stream:\n          arn: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events\n    environment:\n      order_events_stream: order-events\n      user_notification_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification\n      user_notification_retry_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification-retry\n    iamRoleStatements:\n      - Effect: Allow\n        Action: kinesis:PutRecord\n        Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events\n      - Effect: Allow\n        Action: sns:Publish\n        Resource:\n          - arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification\n          - arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification-retry\n\n  retry-notify-user:\n    handler: functions/retry-notify-user.handler\n    events:\n      - sns: user-notification-retry\n    environment:\n      order_events_stream: order-events\n      user_notification_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification\n    onError: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification-dlq\n    iamRoleStatements:\n      - Effect: Allow\n        Action: kinesis:PutRecord\n        Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events\n      - Effect: Allow\n        Action: sns:Publish\n        Resource:\n          - arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification\n\n  fulfill-order:\n    handler: functions/fulfill-order.handler\n    events:\n      - http:\n          path: /orders/complete\n          method: post\n    environment:\n      order_events_stream: order-events\n      async_metrics: true\n    iamRoleStatements:\n      - Effect: Allow\n        Action: kinesis:PutRecord\n        Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events\n\n  auto-create-api-alarms:\n    handler: functions/create-alarms.handler  \n    events:\n      - cloudwatchEvent:\n          event:\n            source:\n              - aws.apigateway\n            detail-type:\n              - AWS API Call via CloudTrail\n            detail:\n              eventSource:\n                - apigateway.amazonaws.com\n              eventName:\n                - CreateDeployment\n    environment:\n      alarm_actions: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:NotifyMe\n      ok_actions: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:NotifyMe\n    iamRoleStatements:\n      - Effect: Allow\n        Action: apigateway:GET\n        Resource: \n          - arn:aws:apigateway:#{AWS::Region}::/restapis/*\n          - arn:aws:apigateway:#{AWS::Region}::/restapis/*/stages/${self:custom.stage}\n      - Effect: Allow\n        Action: apigateway:PATCH\n        Resource: arn:aws:apigateway:#{AWS::Region}::/restapis/*/stages/${self:custom.stage}\n      - Effect: Allow\n        Action: cloudwatch:PutMetricAlarm\n        Resource: \"*\"\n\nresources:\n  Resources:\n    restaurantsTable:\n      Type: AWS::DynamoDB::Table\n      Properties:\n        TableName: restaurants\n        AttributeDefinitions:\n          - AttributeName: name\n            AttributeType: S\n        KeySchema:\n          - AttributeName: name\n            KeyType: HASH\n        ProvisionedThroughput:\n          ReadCapacityUnits: 1\n          WriteCapacityUnits: 1\n\n    orderEventsStream:\n      Type: AWS::Kinesis::Stream\n      Properties: \n        Name: order-events\n        ShardCount: 1\n\n    restaurantNotificationTopic:\n      Type: AWS::SNS::Topic\n      Properties: \n        DisplayName: restaurant-notification\n        TopicName: restaurant-notification\n\n    userNotificationTopic:\n      Type: AWS::SNS::Topic\n      Properties: \n        DisplayName: user-notification\n        TopicName: user-notification\n\n    restaurantNotificationDLQTopic:\n      Type: AWS::SNS::Topic\n      Properties: \n        DisplayName: restaurant-notification-dlq\n        TopicName: restaurant-notification-dlq\n\n    userNotificationDLQTopic:\n      Type: AWS::SNS::Topic\n      Properties: \n        DisplayName: user-notification-dlq\n        TopicName: user-notification-dlq"
  },
  {
    "path": "static/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\">\n    <title>Big Mouth</title>\n\n    <script src=\"https://sdk.amazonaws.com/js/aws-sdk-2.149.0.min.js\"></script>\n    <script src=\"https://d2qt42rcwzspd6.cloudfront.net/manning/aws-cognito-sdk.min.js\"></script>\n    <script src=\"https://d2qt42rcwzspd6.cloudfront.net/manning/amazon-cognito-identity.min.js\"></script>\n    <script src=\"https://code.jquery.com/jquery-3.2.1.min.js\" \n            integrity=\"sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=\"\n            crossorigin=\"anonymous\"></script>\n    <script src=\"https://code.jquery.com/ui/1.12.1/jquery-ui.min.js\" \n            integrity=\"sha384-Dziy8F2VlJQLMShA6FHWNul/veM9bCkRUaLqr199K94ntO5QUrLJBEbYegdSkkqX\" \n            crossorigin=\"anonymous\"></script>\n    <link rel=\"stylesheet\" href=\"https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css\">\n\n    <style>\n      .fullscreenDiv {\n        background-color: #05bafd;\n        width: 100%;\n        height: auto;\n        bottom: 0px;\n        top: 0px;\n        left: 0;\n        position: absolute;        \n      }\n      .restaurantsDiv {\n        background-color: #ffffff;\n        width: 100%;\n        height: auto;\n      }\n      .dayOfWeek {\n        font-family: Arial, Helvetica, sans-serif;\n        font-size: 32px;\n        padding: 10px;\n        height: auto;\n        display: flex;\n        justify-content: center;\n      }\n      .column-container {\n        padding: 0;\n        margin: 0;        \n        list-style: none;\n        display: flex;\n        flex-flow: column;\n        flex-wrap: wrap;\n        justify-content: center;\n      }\n      .row-container {\n        padding: 5px;\n        margin: 5px;\n        list-style: none;\n        display: flex;\n        flex-flow: row;\n        flex-wrap: wrap;\n        justify-content: center;\n      }\n      .item {\n        padding: 5px;\n        height: auto;\n        margin-top: 10px;\n        display: flex;\n        flex-flow: row;\n        flex-wrap: wrap;\n        justify-content: center;\n      }\n      .restaurant {\n        background-color: #00a8f7;\n        border-radius: 10px;\n        padding: 5px;\n        height: auto;\n        width: auto;\n        margin-left: 40px;\n        margin-right: 40px;\n        margin-top: 15px;\n        margin-bottom: 0px;\n        display: flex;\n        justify-content: center;\n      }\n      .restaurant-name {\n        font-size: 24px;\n        font-family:Arial, Helvetica, sans-serif;\n        color: #ffffff;\n        padding: 10px;\n        margin: 0px;\n      }\n      .restaurant-image {\n        padding-top: 0px;\n        margin-top: 0px;\n      }\n      .row-container-left {\n        list-style: none;\n        display: flex;\n        flex-flow: row;\n        justify-content: flex-start;\n      }\n      .menu-text {\n        font-family: Arial, Helvetica, sans-serif;\n        font-size: 24px;\n        font-weight: bold;\n        color: white;\n      }\n      .text-trail-space {\n        margin-right: 10px;\n      }\n      .hidden {\n        display: none;\n      }\n\n      lable, button, input {\n        display:block;\n        font-family: Arial, Helvetica, sans-serif;\n        font-size: 18px;\n      }\n      \n      fieldset { \n        padding:0; \n        border:0; \n        margin-top:25px; \n      }\n\n    </style>\n\n    <script>\n      const AWS_REGION = '{{awsRegion}}';\n      const COGNITO_USER_POOL_ID = '{{cognitoUserPoolId}}';\n      const CLIENT_ID = '{{cognitoClientId}}';\n      const SEARCH_URL = '{{& searchUrl}}';\n      const PLACE_ORDER_URL = '{{& placeOrderUrl}}';\n\n      var regDialog, regForm;\n      var verifyDialog;\n      var regCompleteDialog;\n      var signInDialog;\n      var userPool, cognitoUser;\n      var idToken;\n\n      function toggleSignOut (enable) {\n        enable === true ? $('#sign-out').show() : $('#sign-out').hide();\n      }\n\n      function toggleSignIn (enable) {\n        enable === true ? $('#sign-in').show() : $('#sign-in').hide();\n      }\n\n      function toggleRegister (enable) {\n        enable === true ? $('#register').show() : $('#register').hide();\n      }\n\n      function init() {\n        AWS.config.region = AWS_REGION;\n        AWSCognito.config.region = AWS_REGION;\n\n        var data = { \n          UserPoolId : COGNITO_USER_POOL_ID, \n          ClientId : CLIENT_ID\n        };\n        userPool = new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(data);\n        cognitoUser = userPool.getCurrentUser();\n\n        if (cognitoUser != null) {          \n          cognitoUser.getSession(function(err, session) {\n            if (err) {\n                alert(err);\n                return;\n            }\n\n            idToken = session.idToken.jwtToken;\n            console.log('idToken: ' + idToken);\n            console.log('session validity: ' + session.isValid());\n          });\n\n          toggleSignOut(true);\n          toggleSignIn(false);\n          toggleRegister(false);\n        } else {\n          toggleSignOut(false);\n          toggleSignIn(true);\n          toggleRegister(true);\n        }\n      }\n\n      function addUser() {\n        var firstName = $(\"#first-name\")[0].value;\n        var lastName = $(\"#last-name\")[0].value;\n        var username = $(\"#username\")[0].value;\n        var password = $(\"#password\")[0].value;\n        var email = $(\"#email\")[0].value;\n\n        var attributeList = [\n          new AWSCognito.CognitoIdentityServiceProvider.CognitoUserAttribute({ \n            Name : 'email', Value : email\n          }),\n          new AWSCognito.CognitoIdentityServiceProvider.CognitoUserAttribute({ \n            Name : 'given_name', Value : firstName\n          }),\n          new AWSCognito.CognitoIdentityServiceProvider.CognitoUserAttribute({ \n            Name : 'family_name', Value : lastName\n          }),\n        ];\n\n        userPool.signUp(username, password, attributeList, null, function(err, result){\n          if (err) {\n            alert(err);\n            return;\n          }\n          cognitoUser = result.user;\n          console.log('user name is ' + cognitoUser.getUsername());\n\n          regDialog.dialog(\"close\");\n          verifyDialog.dialog(\"open\");\n        });\n      }\n\n      function confirmUser() {\n        var verificationCode = $(\"#verification-code\")[0].value;\n        cognitoUser.confirmRegistration(verificationCode, true, function(err, result) {\n          if (err) {\n            alert(err);\n            return;\n          }\n          console.log('verification call result: ' + result);\n\n          verifyDialog.dialog(\"close\");\n          regCompleteDialog.dialog(\"open\");\n        });\n      }\n\n      function authenticateUser() {\n        var username = $(\"#sign-in-username\")[0].value;\n        var password = $(\"#sign-in-password\")[0].value;\n\n        var authenticationData = {\n          Username : username,\n          Password : password,\n        };\n        var authenticationDetails = new AWSCognito.CognitoIdentityServiceProvider.AuthenticationDetails(authenticationData);\n        var userData = {\n          Username : username,\n          Pool : userPool\n        };\n        var cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);\n\n        cognitoUser.authenticateUser(authenticationDetails, {\n          onSuccess: function (result) {\n            console.log('access token : ' + result.getAccessToken().getJwtToken());\n            /*Use the idToken for Logins Map when Federating User Pools with Cognito Identity or when passing through an Authorization Header to an API Gateway Authorizer*/\n            idToken = result.idToken.jwtToken;\n            console.log('idToken : ' + idToken);\n\n            signInDialog.dialog(\"close\");\n            toggleRegister(false);\n            toggleSignIn(false);\n            toggleSignOut(true);\n          },\n\n          onFailure: function(err) {\n            alert(err);\n          }\n        });\n      }\n\n      function signOut() {\n        if (cognitoUser != null) {\n          cognitoUser.signOut();\n          toggleRegister(true);\n          toggleSignIn(true);\n          toggleSignOut(false);\n        }\n      }\n\n      function searchRestaurants() {\n        var theme = $(\"#theme\")[0].value;\n\n        var xhr = new XMLHttpRequest();\n        xhr.open('POST', SEARCH_URL, true);\n        xhr.setRequestHeader(\"Content-Type\", \"application/json\");\n        xhr.setRequestHeader(\"Authorization\", idToken);\n        xhr.send(JSON.stringify({ theme }));\n        \n        xhr.onreadystatechange = function (e) {\n          if (xhr.readyState === 4 && xhr.status === 200) {\n            var restaurants = JSON.parse(xhr.responseText);\n            var restaurantsList = $(\"#restaurantsUl\");\n            restaurantsList.empty();\n\n            for (var restaurant of restaurants) {\n              restaurantsList.append(`\n              <li class=\"restaurant\">\n                <ul class=\"column-container\" onclick='placeOrder(\"${restaurant.name}\")'>\n                    <li class=\"item restaurant-name\">${restaurant.name}</li>\n                    <li class=\"item restaurant-image\">\n                      <img src=\"${restaurant.image}\">\n                    </li>\n                </ul>\n              </li>\n              `);\n            }\n\n          } else if (xhr.readyState === 4) {\n            alert(xhr.responseText);\n          }\n        };\n      }\n\n      function placeOrder(restaurantName) {\n        var xhr = new XMLHttpRequest();\n        xhr.open('POST', PLACE_ORDER_URL, true);\n        xhr.setRequestHeader(\"Content-Type\", \"application/json\");\n        xhr.setRequestHeader(\"Authorization\", idToken);\n        xhr.send(JSON.stringify({ restaurantName }));\n\n        xhr.onreadystatechange = function (e) {\n          if (xhr.readyState === 4 && xhr.status === 200) {\n            alert(\"your order has been placed, we'll let you know once it's been accepted by the restaurant!\");\n          } else if (xhr.readyState === 4) {\n            alert(xhr.responseText);\n          }\n        };\n      }\n\n      $(document).ready(function() {\n        regDialog = $(\"#reg-dialog-form\").dialog({\n          autoOpen: false,\n          modal: true,\n          buttons: {\n            \"Create an account\": addUser,\n            Cancel: function() {\n              regDialog.dialog(\"close\");\n            }\n          },\n          close: function() {\n            regForm[0].reset();\n          }\n        });\n\n        regForm = regDialog.find(\"form\").on(\"submit\", function(event) {\n          event.preventDefault();\n          addUser();\n        });\n        \n        $(\"#register\").on(\"click\", function() {\n          regDialog.dialog(\"open\");\n        });\n\n        verifyDialog = $(\"#verify-dialog-form\").dialog({\n          autoOpen: false,\n          modal: true,\n          buttons: {\n            \"Confirm registration\": confirmUser,\n            Cancel: function() {\n              verifyDialog.dialog(\"close\");\n            }\n          },\n          close: function() {\n            $(this).dialog(\"close\");\n          }\n        });\n\n        regCompleteDialog = $(\"#registered-message\").dialog({\n          autoOpen: false,\n          modal: true,\n          buttons: {\n            Ok: function() {\n              $(this).dialog(\"close\");\n            }\n          }\n        });\n\n        signInDialog = $(\"#sign-in-form\").dialog({\n          autoOpen: false,\n          modal: true,\n          buttons: {\n            \"Sign in\": authenticateUser,\n            Cancel: function() {\n              signInDialog.dialog(\"close\");\n            }\n          },\n          close: function() {\n            $(this).dialog(\"close\");\n          }\n        });\n\n        $(\"#sign-in\").on(\"click\", function() {\n          signInDialog.dialog(\"open\");\n        });\n\n        $(\"#sign-out\").on(\"click\", function() {\n          signOut();\n        })\n\n        init();\n      });\n\n    </script>\n  </head>\n\n  <body>\n    <div class=\"fullscreenDiv\">\n      <ul class=\"column-container\">\n        <li>\n          <ul class=\"row-container-left\">\n            <li id=\"register\" class=\"item text-trail-space hidden\">\n              <a class=\"menu-text\" href=\"#\">Register</a>\n            </li>\n            <li id=\"sign-in\" class=\"item menu-text text-trail-space hidden\">\n              <a class=\"menu-text\" href=\"#\">Sign in</a>\n            </li>\n            <li id=\"sign-out\" class=\"item menu-text text-trail-space hidden\">\n              <a class=\"menu-text\" href=\"#\">Sign out</a>\n            </li>\n          </ul>\n        </li>\n        <li class=\"item\">\n          <img id=\"logo\" src=\"https://d2qt42rcwzspd6.cloudfront.net/manning/big-mouth.png\">\n        </li>\n        <li class=\"item\">\n          <input id=\"theme\" type=\"text\" size=\"50\" placeholder=\"enter a theme, eg. rick and morty\"/>\n          <button onclick=\"searchRestaurants()\">Find Restaurants</button>\n        </li>\n        <li>\n          <div class=\"restaurantsDiv column-container\">\n            <b class=\"dayOfWeek\">{{dayOfWeek}}</b>\n            <ul id=\"restaurantsUl\" class=\"row-container\">\n              {{#restaurants}}\n              <li class=\"restaurant\">\n                <ul class=\"column-container\" onclick='placeOrder(\"{{name}}\")'>\n                    <li class=\"item restaurant-name\">{{name}}</li>\n                    <li class=\"item restaurant-image\">\n                      <img src=\"{{image}}\">\n                    </li>\n                </ul>\n              </li>\n              {{/restaurants}}\n            </ul>\n          </div>\n        </li>\n      </ul>\n    </div>\n\n    <div id=\"reg-dialog-form\" title=\"Register\">       \n      <form>\n        <fieldset>\n          <label for=\"first-name\">First Name</label>\n          <input type=\"text\" id=\"first-name\" class=\"text ui-widget-content ui-corner-all\">\n          <label for=\"last-name\">Last Name</label>\n          <input type=\"text\" id=\"last-name\" class=\"text ui-widget-content ui-corner-all\">\n          <label for=\"email\">Email</label>\n          <input type=\"text\" name=\"email\" id=\"email\" class=\"text ui-widget-content ui-corner-all\">\n          <label for=\"username\">Username</label>\n          <input type=\"text\" name=\"username\" id=\"username\" class=\"text ui-widget-content ui-corner-all\">\n          <label for=\"password\">Password</label>\n          <input type=\"password\" name=\"password\" id=\"password\" class=\"text ui-widget-content ui-corner-all\">\n        </fieldset>\n      </form>\n    </div>\n\n    <div id=\"verify-dialog-form\" title=\"Verify\">\n      <form>\n        <fieldset>\n            <label for=\"verification-code\">Verification Code</label>\n            <input type=\"text\" id=\"verification-code\" class=\"text ui-widget-content ui-corner-all\">\n        </fieldset>\n      </form>\n    </div>\n\n    <div id=\"registered-message\" title=\"Registration complete!\">\n      <p>\n        <span class=\"ui-icon ui-icon-circle-check\" style=\"float:left; margin:0 7px 50px 0;\"></span>\n        You are now registered!\n      </p>\n    </div>\n\n    <div id=\"sign-in-form\" title=\"Sign in\">\n      <form>\n          <fieldset>            \n            <label for=\"sign-in-username\">Username</label>\n            <input type=\"text\" id=\"sign-in-username\" class=\"text ui-widget-content ui-corner-all\">\n            <label for=\"sign-in-password\">Password</label>\n            <input type=\"password\" id=\"sign-in-password\" class=\"text ui-widget-content ui-corner-all\">\n          </fieldset>\n        </form>\n    </div>\n\n  </body>\n\n</html>"
  },
  {
    "path": "template.yml",
    "content": "AWSTemplateFormatVersion: '2010-09-09'\nTransform: 'AWS::Serverless-2016-10-31'\nDescription: 'SAM template for Serverless framework service: '\nResources:\n  restaurantsTable:\n    Type: 'AWS::DynamoDB::Table'\n    Properties:\n      TableName: restaurants\n      AttributeDefinitions:\n        - AttributeName: name\n          AttributeType: S\n      KeySchema:\n        - AttributeName: name\n          KeyType: HASH\n      ProvisionedThroughput:\n        ReadCapacityUnits: 1\n        WriteCapacityUnits: 1\n  GetIndex:\n    Type: 'AWS::Serverless::Function'\n    Properties:\n      Handler: functions/get-index.handler\n      Runtime: nodejs6.10\n      CodeUri: >-\n        /Users/yancui/SourceCode/Personal/manning-aws-lambda-operational-patterns-and-practices/.serverless/big-mouth.zip\n      MemorySize: 128\n      Timeout: 3\n      Policies:\n        - Effect: Allow\n          Action: 'dynamodb:scan'\n          Resource: 'arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/restaurants'\n      Environment:\n        Variables:\n          restaurants_api: https://i3c5h755j0.execute-api.us-east-1.amazonaws.com/dev/restaurants\n          cognito_user_pool_id: us-east-1_DfuAwa0vB\n          cognito_client_id: 1tf8pb1s1n53p7u608e7ic5ih8\n      Events:\n        Event1:\n          Type: Api\n          Properties:\n            Path: /\n            Method: get\n  GetRestaurants:\n    Type: 'AWS::Serverless::Function'\n    Properties:\n      Handler: functions/get-restaurants.handler\n      Runtime: nodejs6.10\n      CodeUri: >-\n        /Users/yancui/SourceCode/Personal/manning-aws-lambda-operational-patterns-and-practices/.serverless/big-mouth.zip\n      MemorySize: 128\n      Timeout: 3\n      Policies:\n        - Effect: Allow\n          Action: 'dynamodb:scan'\n          Resource: 'arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/restaurants'\n      Environment:\n        Variables:\n          restaurants_table: restaurants\n      Events:\n        Event1:\n          Type: Api\n          Properties:\n            Path: /restaurants/\n            Method: get\n  Aws_iamResourcePolicy:\n    Type: 'AWS::Lambda::Permission'\n    Properties:\n      Action: 'lambda:InvokeFunction'\n      FunctionName:\n        'Fn::GetAtt':\n          - Aws_iam\n          - Arn\n      Principal: apigateway.amazonaws.com\n      SourceAccount:\n        Ref: 'AWS::AccountId'\n  SearchRestaurants:\n    Type: 'AWS::Serverless::Function'\n    Properties:\n      Handler: functions/search-restaurants.handler\n      Runtime: nodejs6.10\n      CodeUri: >-\n        /Users/yancui/SourceCode/Personal/manning-aws-lambda-operational-patterns-and-practices/.serverless/big-mouth.zip\n      MemorySize: 128\n      Timeout: 3\n      Policies:\n        - Effect: Allow\n          Action: 'dynamodb:scan'\n          Resource: 'arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/restaurants'\n      Environment:\n        Variables:\n          restaurants_table: restaurants\n      Events:\n        Event1:\n          Type: Api\n          Properties:\n            Path: /restaurants/search\n            Method: post\n"
  },
  {
    "path": "tests/steps/given.js",
    "content": "'use strict';\n\nconst AWS     = require('aws-sdk');\nAWS.config.region = 'us-east-1';\nconst cognito = new AWS.CognitoIdentityServiceProvider();\nconst chance  = require('chance').Chance();\n\nlet random_password = () => {\n  // needs number, special char, upper and lower case\n  return `${chance.string({ length: 8})}B!gM0uth`;\n}\n\nlet an_authenticated_user = function* () {\n  let userpoolId = process.env.cognito_user_pool_id;\n  let clientId = process.env.cognito_server_client_id;\n\n  let firstName = chance.first();\n  let lastName  = chance.last();\n  let username  = `test-${firstName}-${lastName}-${chance.string({length: 8})}`;\n  let password  = random_password();\n  let email     = `${firstName}-${lastName}@big-mouth.com`;\n\n  let createReq = {\n    UserPoolId        : userpoolId,\n    Username          : username,\n    MessageAction     : 'SUPPRESS',\n    TemporaryPassword : password,\n    UserAttributes    : [\n      { Name: \"given_name\",  Value: firstName },\n      { Name: \"family_name\", Value: lastName },\n      { Name: \"email\",       Value: email }\n    ]\n  };\n  yield cognito.adminCreateUser(createReq).promise();\n\n  console.log(`[${username}] - user is created`);\n  \n  let req = {\n    AuthFlow        : 'ADMIN_NO_SRP_AUTH',\n    UserPoolId      : userpoolId,\n    ClientId        : clientId,\n    AuthParameters  : {\n      USERNAME: username,    \n      PASSWORD: password\n    }\n  };\n  let resp = yield cognito.adminInitiateAuth(req).promise();\n\n  console.log(`[${username}] - initialised auth flow`);\n\n  let challengeReq = {\n    UserPoolId          : userpoolId,\n    ClientId            : clientId,\n    ChallengeName       : resp.ChallengeName,\n    Session             : resp.Session,\n    ChallengeResponses  : {\n      USERNAME: username,\n      NEW_PASSWORD: random_password()\n    }\n  };\n  let challengeResp = yield cognito.adminRespondToAuthChallenge(challengeReq).promise();\n  \n  console.log(`[${username}] - responded to auth challenge`);\n\n  return {\n    username,\n    firstName,\n    lastName,\n    idToken: challengeResp.AuthenticationResult.IdToken\n  };\n};\n\nmodule.exports = {\n  an_authenticated_user\n};"
  },
  {
    "path": "tests/steps/init.js",
    "content": "'use strict';\n\nconst _ = require('lodash');\nconst co = require('co');\nconst Promise = require('bluebird');\nconst aws4 = require('../../lib/aws4');\nconst AWS = require('aws-sdk');\nAWS.config.region = 'us-east-1';\nconst SSM = new AWS.SSM();\n\nlet initialized = false;\n\nconst getParameters = co.wrap(function* (keys) {\n  const prefix = '/bigmouth/dev/';\n  const req = {\n    Names: keys.map(key => `${prefix}${key}`)\n  }\n  const resp = yield SSM.getParameters(req).promise();\n  return _.reduce(resp.Parameters, function(obj, param) {\n    obj[param.Name.substr(prefix.length)] = param.Value\n    return obj;\n  }, {})\n});\n\nlet init = co.wrap(function* () {\n  if (initialized) {\n    return;\n  }\n\n  const params = yield getParameters([\n    'cognito_client_id',\n    'cognito_user_pool_id',\n    'restaurants_api'\n  ]);\n\n  process.env.restaurants_api = params.restaurants_api;\n  process.env.restaurants_table = \"restaurants\";\n  process.env.AWS_REGION = \"us-east-1\";\n  process.env.cognito_client_id = params.cognito_client_id;\n  process.env.cognito_user_pool_id = params.cognito_user_pool_id;\n  process.env.cognito_server_client_id = \"niv7esuaibla0tj5q36b6mvnr\";\n  process.env.AWS_XRAY_CONTEXT_MISSING = \"LOG_ERROR\";\n  process.env.STAGE = 'dev';\n\n  yield aws4.init();\n\n  initialized = true;\n});\n\nmodule.exports.init = init;"
  },
  {
    "path": "tests/steps/tearDown.js",
    "content": "'use strict';\n\nconst co      = require('co');\nconst AWS     = require('aws-sdk');\nAWS.config.region = 'us-east-1';\nconst cognito = new AWS.CognitoIdentityServiceProvider();\n\nlet an_authenticated_user = function* (user) {\n  let req = {\n    UserPoolId: process.env.cognito_user_pool_id,\n    Username: user.username\n  };\n  yield cognito.adminDeleteUser(req).promise();\n  \n  console.log(`[${user.username}] - user deleted`);\n};\n\nmodule.exports = {\n  an_authenticated_user\n};"
  },
  {
    "path": "tests/steps/when.js",
    "content": "'use strict';\n\nconst APP_ROOT = '../../';\n\nconst _       = require('lodash');\nconst co      = require('co');\nconst Promise = require(\"bluebird\");\nconst http    = require('superagent-promise')(require('superagent'), Promise);\nconst aws4    = require('../../lib/aws4');\nconst URL     = require('url');\nconst mode    = process.env.TEST_MODE;\n\nlet respondFrom = function (httpRes) {\n  let contentType = _.get(httpRes, 'headers.content-type', 'application/json');\n  let body = \n    contentType === 'application/json'\n      ? httpRes.body\n      : httpRes.text;\n\n  return { \n    statusCode: httpRes.status,\n    body: body,\n    headers: httpRes.headers\n  };\n}\n\nlet signHttpRequest = (url, httpReq) => {\n  let urlData = URL.parse(url);\n  let opts = {\n    host: urlData.hostname, \n    path: urlData.pathname\n  };\n\n  aws4.sign(opts);\n\n  httpReq\n    .set('Host', opts.headers['Host'])\n    .set('X-Amz-Date', opts.headers['X-Amz-Date'])\n    .set('Authorization', opts.headers['Authorization']);\n\n  if (opts.headers['X-Amz-Security-Token']) {\n    httpReq.set('X-Amz-Security-Token', opts.headers['X-Amz-Security-Token']);\n  }\n}\n\nlet viaHttp = co.wrap(function* (relPath, method, opts) {\n  let root = process.env.TEST_ROOT;\n  let url = `${root}/${relPath}`;\n  console.log(`invoking via HTTP ${method} ${url}`);\n\n  try {\n    let httpReq = http(method, url);\n\n    let body = _.get(opts, \"body\");\n    if (body) {      \n      httpReq.send(body);\n    }\n\n    if (_.get(opts, \"iam_auth\", false) === true) {\n      signHttpRequest(url, httpReq);\n    }\n\n    let authHeader = _.get(opts, \"auth\");\n    if (authHeader) {\n      httpReq.set('Authorization', authHeader);\n    }\n    \n    let res = yield httpReq;\n    return respondFrom(res);\n  } catch (err) {\n    if (err.status) {\n      return {\n        statusCode: err.status,\n        headers: err.response.headers\n      };\n    } else {\n      throw err;\n    }\n  }\n})\n\nlet viaHandler = (event, functionName) => {  \n  let handler = require(`${APP_ROOT}/functions/${functionName}`).handler;\n  console.log(`invoking via handler function ${functionName}`);\n\n  return new Promise((resolve, reject) => {\n    let context = {};\n    let callback = function (err, response) {\n      if (err) {\n        reject(err);\n      } else {\n        let contentType = _.get(response, 'headers.content-type', 'application/json');\n        if (response.body && contentType === 'application/json') {\n          response.body = JSON.parse(response.body);\n        }\n\n        resolve(response);\n      }\n    };\n\n    handler(event, context, callback);\n  });\n}\n\nlet we_invoke_get_index = co.wrap(function* () {\n  let res = \n    mode === 'handler' \n      ? yield viaHandler({}, 'get-index')\n      : yield viaHttp('', 'GET');\n\n  return res;\n});\n\nlet we_invoke_get_restaurants = co.wrap(function* () {\n  let res =\n    mode === 'handler' \n      ? yield viaHandler({}, 'get-restaurants')\n      : yield viaHttp('restaurants', 'GET', { iam_auth: true });\n\n  return res;\n});\n\nlet we_invoke_search_restaurants = co.wrap(function* (user, theme) {\n  let body = JSON.stringify({ theme });\n  let auth = user.idToken;\n\n  let res = \n    mode === 'handler'\n      ? viaHandler({ body }, 'search-restaurants')\n      : viaHttp('restaurants/search', 'POST', { body, auth })\n\n  return res;\n});\n\nmodule.exports = {\n  we_invoke_get_index,\n  we_invoke_get_restaurants,\n  we_invoke_search_restaurants\n};"
  },
  {
    "path": "tests/test_cases/get-index.js",
    "content": "'use strict';\n\nconst co = require('co');\nconst expect = require('chai').expect;\nconst when = require('../steps/when');\nconst init = require('../steps/init').init;\nconst cheerio = require('cheerio');\n\ndescribe(`When we invoke the GET / endpoint`, co.wrap(function* () {\n  before(co.wrap(function* () {\n    yield init();\n  }));\n\n  it(`Should return the index page with 8 restaurants`, co.wrap(function* () {\n    let res = yield when.we_invoke_get_index();\n\n    expect(res.statusCode).to.equal(200);\n    expect(res.headers['content-type']).to.equal('text/html; charset=UTF-8');\n    expect(res.body).to.not.be.null;\n\n    let $ = cheerio.load(res.body);\n    let restaurants = $('.restaurant', '#restaurantsUl');\n    expect(restaurants.length).to.equal(8);\n    \n  }));\n  \n}));"
  },
  {
    "path": "tests/test_cases/get-restaurants.js",
    "content": "'use strict';\n\nconst co = require('co');\nconst expect = require('chai').expect;\nconst init = require('../steps/init').init;\nconst when = require('../steps/when');\n\ndescribe(`When we invoke the GET /restaurants endpoint`, co.wrap(function* () {\n  before(co.wrap(function* () {\n    yield init();\n  }));\n\n  it(`Should return an array of 8 restaurants`, co.wrap(function* () {\n    let res = yield when.we_invoke_get_restaurants();\n\n    expect(res.statusCode).to.equal(200);\n    expect(res.body).to.have.lengthOf(8);\n\n    for (let restaurant of res.body) {\n      expect(restaurant).to.have.property('name');\n      expect(restaurant).to.have.property('image');\n    }\n  }));\n}));"
  },
  {
    "path": "tests/test_cases/search-restaurants.js",
    "content": "'use strict';\n\nconst co = require('co');\nconst expect = require('chai').expect;\nconst init = require('../steps/init').init;\nconst when = require('../steps/when');\nconst given = require('../steps/given');\nconst tearDown = require('../steps/tearDown');\n\ndescribe(`Given an authenticated user`, co.wrap(function* () {\n  let user;\n  before(co.wrap(function* () {\n    yield init();\n    user = yield given.an_authenticated_user();\n  }));\n\n  after(co.wrap(function* () {\n    yield tearDown.an_authenticated_user(user);\n  }));\n\n  describe(`When we invoke the POST /restaurants/search endpoint with theme 'cartoon'`, co.wrap(function* () {\n    it(`Should return an array of 4 restaurants`, co.wrap(function* () {\n      let res = yield when.we_invoke_search_restaurants(user, 'cartoon');\n  \n      expect(res.statusCode).to.equal(200);\n      expect(res.body).to.have.lengthOf(4);\n  \n      for (let restaurant of res.body) {\n        expect(restaurant).to.have.property('name');\n        expect(restaurant).to.have.property('image');\n      }\n    }));\n  }));  \n}));\n\n"
  }
]