master eab98a4e1824 cached
50 files
118.3 KB
33.0k tokens
75 symbols
1 requests
Download .txt
Repository: theburningmonk/manning-aws-lambda-in-motion
Branch: master
Commit: eab98a4e1824
Files: 50
Total size: 118.3 KB

Directory structure:
gitextract_0qv7lc5k/

├── .gitignore
├── .vscode/
│   └── launch.json
├── README.md
├── build.sh
├── buildspec.yml
├── examples/
│   ├── create-alarm.json
│   ├── get-index.json
│   ├── notify-restaurant.json
│   ├── place-order.json
│   ├── retry-notify-restaurant.json
│   └── search-restaurants.json
├── functions/
│   ├── accept-order.js
│   ├── create-alarms.js
│   ├── fulfill-order.js
│   ├── get-index.js
│   ├── get-restaurants.js
│   ├── notify-restaurant.js
│   ├── notify-user.js
│   ├── place-order.js
│   ├── retry-notify-restaurant.js
│   ├── retry-notify-user.js
│   └── search-restaurants.js
├── lib/
│   ├── aws4.js
│   ├── awscred.js
│   ├── cloudwatch.js
│   ├── correlation-ids.js
│   ├── http.js
│   ├── kinesis.js
│   ├── log.js
│   ├── lru.js
│   ├── notify.js
│   ├── retry.js
│   └── sns.js
├── middleware/
│   ├── capture-correlation-ids.js
│   ├── flush-metrics.js
│   ├── function-shield.js
│   ├── sample-logging.js
│   └── wrapper.js
├── package.json
├── seed-restaurants.js
├── serverless.yml
├── static/
│   └── index.html
├── template.yml
└── tests/
    ├── steps/
    │   ├── given.js
    │   ├── init.js
    │   ├── tearDown.js
    │   └── when.js
    └── test_cases/
        ├── get-index.js
        ├── get-restaurants.js
        └── search-restaurants.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# package directories
node_modules
jspm_packages

# Serverless directories
.serverless

================================================
FILE: .vscode/launch.json
================================================
{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [  
    {
      "name": "Attach to SAM Local",
      "type": "node",
      "request": "attach",
      "address": "localhost",
      "port": 5858,
      "localRoot": "${workspaceRoot}",
      "remoteRoot": "/var/task"
    },
    {
      "type": "node",
      "request": "launch",
      "name": "get-index",
      "program": "${workspaceFolder}/node_modules/.bin/sls",
      "args": [
        "invoke",
        "local",
        "-f",
        "get-index",
        "-p",
        "examples/get-index.json"
      ],
      "env": {
        "restaurants_api": "https://8kbasri6v6.execute-api.us-east-1.amazonaws.com/dev/restaurants",
        "cognito_user_pool_id": "us-east-1_DfuAwa0vB",
        "cognito_client_id": "49lunjf7j7vsgmq9lhtfn4q7ma",
        "SLS_DEBUG": "*",
        "AWS_XRAY_CONTEXT_MISSING":"LOG_ERROR"
      }
    },
    {
      "type": "node",
      "request": "launch",
      "name": "get-restaurants",
      "program": "${workspaceFolder}/node_modules/.bin/sls",
      "args": [
        "invoke",
        "local",
        "-f",
        "get-restaurants",
        "-d",
        "{}"
      ],
      "env": {
        "restaurants_table": "restaurants",
        "SLS_DEBUG": "*",
        "AWS_XRAY_CONTEXT_MISSING":"LOG_ERROR"
      }
    },
    {
      "type": "node",
      "request": "launch",
      "name": "search-restaurants",
      "program": "${workspaceFolder}/node_modules/.bin/sls",
      "args": [
        "invoke",
        "local",
        "-f",
        "search-restaurants",
        "-p",
        "examples/search-restaurants.json"
      ],
      "env": {
        "restaurants_table": "restaurants",
        "SLS_DEBUG": "*",
        "AWS_XRAY_CONTEXT_MISSING":"LOG_ERROR"
      }
    },
    {
      "type": "node",
      "request": "launch",
      "name": "create-alarm",
      "program": "${workspaceFolder}/node_modules/.bin/sls",
      "args": [
        "invoke",
        "local",
        "-f",
        "auto-create-api-alarms",
        "-p",
        "examples/create-alarm.json"
      ]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "notify-restaurant",
      "program": "${workspaceFolder}/node_modules/.bin/sls",
      "args": [
        "invoke",
        "local",
        "-f",
        "notify-restaurant",
        "-p",
        "examples/notify-restaurant.json"
      ],
      "env": {
        "AWS_XRAY_CONTEXT_MISSING":"LOG_ERROR"
      }
    },
    {
      "type": "node",
      "request": "launch",
      "name": "place-order",
      "program": "${workspaceFolder}/node_modules/.bin/sls",
      "args": [
        "invoke",
        "local",
        "-f",
        "place-order",
        "-p",
        "examples/place-order.json"
      ],
      "env": {
        "AWS_XRAY_CONTEXT_MISSING":"LOG_ERROR"
      }
    },
    {
      "type": "node",
      "request": "launch",
      "name": "integration tests",
      "program": "${workspaceFolder}/node_modules/.bin/mocha",
      "env": {
        "TEST_MODE": "handler"
      },
      "args": [
        "tests/test_cases",
        "--reporter",
        "spec"
      ]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "acceptance tests",
      "program": "${workspaceFolder}/node_modules/.bin/mocha",
      "env": {
        "TEST_MODE": "http",
        "TEST_ROOT": "https://8kbasri6v6.execute-api.us-east-1.amazonaws.com/dev/"
      },
      "args": [
        "tests/test_cases",
        "-t",
        "2000",
        "--reporter",
        "spec"
      ]
    },    
  ]
}

================================================
FILE: README.md
================================================
# manning-aws-lambda-operational-patterns-and-practices
Code for the Manning video course "AWS Lambda: Operational Patterns and Practices"


================================================
FILE: build.sh
================================================
#!/bin/bash
set -e
set -o pipefail

instruction()
{
  echo "usage: ./build.sh deploy <env>"
  echo ""
  echo "env: eg. int, staging, prod, ..."
  echo ""
  echo "for example: ./deploy.sh int"
}

if [ $# -eq 0 ]; then
  instruction
  exit 1
elif [ "$1" = "int-test" ] && [ $# -eq 1 ]; then
  npm install

  npm run integration-test
elif [ "$1" = "acceptance-test" ] && [ $# -eq 1 ]; then
  npm install

  npm run acceptance-test
elif [ "$1" = "deploy" ] && [ $# -eq 3 ]; then
  STAGE=$2
  REGION=$3

  npm install
  'node_modules/.bin/sls' deploy -s $STAGE -r $REGION
else
  instruction
  exit 1
fi

================================================
FILE: buildspec.yml
================================================
version: 0.2

phases:
  build:
    commands:
      - chmod +x build.sh
      - ./build.sh int-test
      - ./build.sh deploy dev
      - ./build.sh acceptance-test

================================================
FILE: examples/create-alarm.json
================================================
{
  "version": "0",
  "id": "dee9a69c-8166-1ad7-41d4-1dad201e29f6",
  "detail-type": "AWS API Call via CloudTrail",
  "source": "aws.apigateway",
  "account": "374852340823",
  "time": "2018-04-09T00:17:47Z",
  "region": "us-east-1",
  "resources": [],
  "detail": {
      "eventVersion": "1.05",
      "userIdentity": {
          "type": "IAMUser",
          "principalId": "AIDAIRMUZZEGPO27IPFYW",
          "arn": "arn:aws:iam::374852340823:user/yan.cui",
          "accountId": "374852340823",
          "accessKeyId": "ASIAJNZDKN26DXPZFYQA",
          "userName": "yan.cui",
          "sessionContext": {
              "attributes": {
                  "mfaAuthenticated": "false",
                  "creationDate": "2018-04-09T00:17:30Z"
              }
          },
          "invokedBy": "cloudformation.amazonaws.com"
      },
      "eventTime": "2018-04-09T00:17:47Z",
      "eventSource": "apigateway.amazonaws.com",
      "eventName": "CreateDeployment",
      "awsRegion": "us-east-1",
      "sourceIPAddress": "cloudformation.amazonaws.com",
      "userAgent": "cloudformation.amazonaws.com",
      "requestParameters": {
          "restApiId": "8kbasri6v6",
          "createDeploymentInput": {
              "stageName": "dev"
          },
          "template": false
      },
      "responseElements": {
          "id": "cj2y0f",
          "createdDate": "Apr 9, 2018 12:17:47 AM",
          "deploymentUpdate": {
              "restApiId": "8kbasri6v6",
              "deploymentId": "cj2y0f",
              "template": false
          },
          "deploymentStages": {
              "deploymentId": "cj2y0f",
              "restApiId": "8kbasri6v6",
              "template": false,
              "templateSkipList": [
                  "position"
              ]
          },
          "deploymentDelete": {
              "deploymentId": "cj2y0f",
              "restApiId": "8kbasri6v6",
              "template": false
          },
          "self": {
              "deploymentId": "cj2y0f",
              "restApiId": "8kbasri6v6",
              "template": false
          }
      },
      "requestID": "6e25bd56-3b8b-11e8-a351-e5e3d3161fe7",
      "eventID": "a150d941-7a54-4572-97b2-0614a81fd25b",
      "readOnly": false,
      "eventType": "AwsApiCall"
  }
}

================================================
FILE: examples/get-index.json
================================================
{
  "body": null,
  "resource": "/",
  "requestContext": {
    "resourceId": "123456",
    "apiId": "1234567890",
    "resourcePath": "/",
    "httpMethod": "POST",
    "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
    "accountId": "123456789012",
    "identity": {
      "apiKey": null,
      "userArn": null,
      "cognitoAuthenticationType": null,
      "caller": null,
      "userAgent": "Custom User Agent String",
      "user": null,
      "cognitoIdentityPoolId": null,
      "cognitoIdentityId": null,
      "cognitoAuthenticationProvider": null,
      "sourceIp": "127.0.0.1",
      "accountId": null
    },
    "authorizer": {
      "claims": {
        "sub": "fef3b4c8-cb16-4eb0-9d7c-201ac4cb0905",
        "email_verified": "true",
        "iss": "https:\/\/cognito-idp.us-east-1.amazonaws.com\/us-east-1_DfuAwa0vB",
        "cognito:username": "theburningmonk",
        "given_name": "Yan",
        "aud": "1tf8pb1s1n53p7u608e7ic5ih8",
        "event_id": "e727e68e-1424-11e8-be95-8774da63618a",
        "token_use": "id",
        "auth_time": "1518986085",
        "exp": "Sun Feb 18 21:34:45 UTC 2018",
        "iat": "Sun Feb 18 20:34:45 UTC 2018",
        "family_name": "Cui",
        "email": "theburningmonk@gmail.com"
      }
    },
    "stage": "prod"
  },
  "queryStringParameters": {
    "foo": "bar"
  },
  "headers": {
    "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
    "Accept-Language": "en-US,en;q=0.8",
    "CloudFront-Is-Desktop-Viewer": "true",
    "CloudFront-Is-SmartTV-Viewer": "false",
    "CloudFront-Is-Mobile-Viewer": "false",
    "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
    "CloudFront-Viewer-Country": "US",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Upgrade-Insecure-Requests": "1",
    "X-Forwarded-Port": "443",
    "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
    "X-Forwarded-Proto": "https",
    "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
    "CloudFront-Is-Tablet-Viewer": "false",
    "Cache-Control": "max-age=0",
    "User-Agent": "Custom User Agent String",
    "CloudFront-Forwarded-Proto": "https",
    "Accept-Encoding": "gzip, deflate, sdch"
  },
  "pathParameters": {
  },
  "httpMethod": "GET",
  "stageVariables": {
    "baz": "qux"
  },
  "path": "/"
}

================================================
FILE: examples/notify-restaurant.json
================================================
{
  "Records":[
    {
      "kinesis": {
        "kinesisSchemaVersion":"1.0",
        "partitionKey":"e4a5eae6-19f7-5284-9cb5-f97d94518e34",
        "sequenceNumber":"49584583598311915935008171659389880270421244691116720130",
        "data":"eyJvcmRlcklkIjoiZTRhNWVhZTYtMTlmNy01Mjg0LTljYjUtZjk3ZDk0NTE4ZTM0IiwidXNlckVtYWlsIjoidGhlYnVybmluZ21vbmtAZ21haWwuY29tIiwicmVzdGF1cmFudE5hbWUiOiJGYW5ndGFzaWEiLCJldmVudFR5cGUiOiJvcmRlcl9wbGFjZWQifQ==",
        "approximateArrivalTimestamp":1527507873.753
      },
      "eventSource":"aws:kinesis",
      "eventVersion":"1.0",
      "eventID":"shardId-000000000000:49584583598311915935008171659389880270421244691116720130",
      "eventName":"aws:kinesis:record",
      "invokeIdentityArn":"arn:aws:iam::374852340823:role/big-mouth-dev-notify-restaurant-us-east-1-lambdaRole",
      "awsRegion":"us-east-1",
      "eventSourceARN":"arn:aws:kinesis:us-east-1:374852340823:stream/order-events"
    }
  ]
}

================================================
FILE: examples/place-order.json
================================================
{
  "resource": "\/orders",
  "path": "\/orders",
  "httpMethod": "POST",
  "headers": {
    "Accept": "*\/*",
    "Accept-Encoding": "gzip, deflate, br",
    "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",
    "Authorization": "eyJraWQiOiJJTnFEZWM5a2crdGx3WnNISEhabjY3dWRyXC9Hc3BrSXE4QTMwNlwvV054VTg9IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJmZWYzYjRjOC1jYjE2LTRlYjAtOWQ3Yy0yMDFhYzRjYjA5MDUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tXC91cy1lYXN0LTFfRGZ1QXdhMHZCIiwiY29nbml0bzp1c2VybmFtZSI6InRoZWJ1cm5pbmdtb25rIiwiZ2l2ZW5fbmFtZSI6IllhbiIsImF1ZCI6IjF0ZjhwYjFzMW41M3A3dTYwOGU3aWM1aWg4IiwiZXZlbnRfaWQiOiJhMTA0YmZmMi02MjBkLTExZTgtODI4My1mNzgxOGJkMzhhNWEiLCJ0b2tlbl91c2UiOiJpZCIsImF1dGhfdGltZSI6MTUyNzQ2NzEzMSwiZXhwIjoxNTI4NjgxNzA4LCJpYXQiOjE1Mjg2NzgxMDgsImZhbWlseV9uYW1lIjoiQ3VpIiwiZW1haWwiOiJ0aGVidXJuaW5nbW9ua0BnbWFpbC5jb20ifQ.ejKf8d6f9jTU72mTmpSkuj_oHIqf_ZA9dUk3_92Y-NvmkEf3AVGJWYdLm2_MoAZwYrzHNQD5cxqIN973pgem73bI9SH0pJqZcbIZ7hS0ieGPkfn_LuMnf4_M3XMPEkGxTcGObl8batYJPeW5ohPE-rfmDjOjUVZxbzemIFY6Pf9ulZTvpVLg6VweR9637GcVktnc8pEKIggG33asqj693MEr_0xX-ioBMWb7NouSD9lz_c-XgiP13LkcNeWuC1DqACRlb8IxB7EzVfFhb-80TTXdYhVKMCnH7e1y5icipdp_lfm1aV9NlhnrWSrVctTWUSnesOmELImFrDQWlH_slg",
    "CloudFront-Forwarded-Proto": "https",
    "CloudFront-Is-Desktop-Viewer": "true",
    "CloudFront-Is-Mobile-Viewer": "false",
    "CloudFront-Is-SmartTV-Viewer": "false",
    "CloudFront-Is-Tablet-Viewer": "false",
    "CloudFront-Viewer-Country": "GB",
    "content-type": "application\/json",
    "Host": "8kbasri6v6.execute-api.us-east-1.amazonaws.com",
    "origin": "https:\/\/8kbasri6v6.execute-api.us-east-1.amazonaws.com",
    "Referer": "https:\/\/8kbasri6v6.execute-api.us-east-1.amazonaws.com\/dev\/",
    "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",
    "Via": "2.0 37b010671d329179b4de819b0a4d4f15.cloudfront.net (CloudFront)",
    "X-Amz-Cf-Id": "7cCnqSDjyqrFSTv3lxs-2EjD4gZfu-MowCk4xWkKmqXlDA6RxJZOCQ==",
    "X-Amzn-Trace-Id": "Root=1-5b1dc99e-a55a324c20164c503b94e858",
    "X-Forwarded-For": "88.98.204.234, 52.46.38.90",
    "X-Forwarded-Port": "443",
    "X-Forwarded-Proto": "https"
  },
  "queryStringParameters": null,
  "pathParameters": null,
  "stageVariables": null,
  "requestContext": {
    "resourceId": "7wew6z",
    "authorizer": {
      "claims": {
        "sub": "fef3b4c8-cb16-4eb0-9d7c-201ac4cb0905",
        "email_verified": "true",
        "iss": "https:\/\/cognito-idp.us-east-1.amazonaws.com\/us-east-1_DfuAwa0vB",
        "cognito:username": "theburningmonk",
        "given_name": "Yan",
        "aud": "1tf8pb1s1n53p7u608e7ic5ih8",
        "event_id": "a104bff2-620d-11e8-8283-f7818bd38a5a",
        "token_use": "id",
        "auth_time": "1527467131",
        "exp": "Mon Jun 11 01:48:28 UTC 2018",
        "iat": "Mon Jun 11 00:48:28 UTC 2018",
        "family_name": "Cui",
        "email": "theburningmonk@gmail.com"
      }
    },
    "resourcePath": "\/orders",
    "httpMethod": "POST",
    "extendedRequestId": "ISxwwGuhoAMFg-Q=",
    "requestTime": "11\/Jun\/2018:01:00:14 +0000",
    "path": "\/dev\/orders",
    "accountId": "374852340823",
    "protocol": "HTTP\/1.1",
    "stage": "dev",
    "requestTimeEpoch": 1528678814455,
    "requestId": "cc9e10de-6d12-11e8-ad56-9338b9713eb7",
    "identity": {
      "cognitoIdentityPoolId": null,
      "accountId": null,
      "cognitoIdentityId": null,
      "caller": null,
      "sourceIp": "88.98.204.234",
      "accessKey": null,
      "cognitoAuthenticationType": null,
      "cognitoAuthenticationProvider": null,
      "userArn": null,
      "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",
      "user": null
    },
    "apiId": "8kbasri6v6"
  },
  "body": "{\"restaurantName\":\"Fangtasia\"}",
  "isBase64Encoded": false
}

================================================
FILE: examples/retry-notify-restaurant.json
================================================
{
  "Records": [
    {
      "EventVersion": "1.0",
      "EventSubscriptionArn": "arn:aws:sns:EXAMPLE",
      "EventSource": "aws:sns",
      "Sns": {
        "SignatureVersion": "1",
        "Timestamp": "1970-01-01T00:00:00.000Z",
        "Signature": "EXAMPLE",
        "SigningCertUrl": "EXAMPLE",
        "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
        "Message": "{\"orderId\":\"875ae3c4-c21c-59e6-9057-23694b75f5a2\",\"userEmail\":\"theburningmonk@gmail.com\",\"restaurantName\":\"Fangtasia\"}",
        "MessageAttributes": {          
        },
        "Type": "Notification",
        "UnsubscribeUrl": "EXAMPLE",
        "TopicArn": "arn:aws:sns:EXAMPLE",
        "Subject": "TestInvoke"
      }
    }
  ]
}

================================================
FILE: examples/search-restaurants.json
================================================
{
  "body": "{\"theme\":\"cartoon\"}",
  "resource": "/{proxy+}",
  "requestContext": {
    "resourceId": "123456",
    "apiId": "1234567890",
    "resourcePath": "/{proxy+}",
    "httpMethod": "POST",
    "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
    "accountId": "123456789012",
    "identity": {
      "apiKey": null,
      "userArn": null,
      "cognitoAuthenticationType": null,
      "caller": null,
      "userAgent": "Custom User Agent String",
      "user": null,
      "cognitoIdentityPoolId": null,
      "cognitoIdentityId": null,
      "cognitoAuthenticationProvider": null,
      "sourceIp": "127.0.0.1",
      "accountId": null
    },
    "authorizer": {
      "claims": {
        "sub": "fef3b4c8-cb16-4eb0-9d7c-201ac4cb0905",
        "email_verified": "true",
        "iss": "https:\/\/cognito-idp.us-east-1.amazonaws.com\/us-east-1_DfuAwa0vB",
        "cognito:username": "theburningmonk",
        "given_name": "Yan",
        "aud": "1tf8pb1s1n53p7u608e7ic5ih8",
        "event_id": "e727e68e-1424-11e8-be95-8774da63618a",
        "token_use": "id",
        "auth_time": "1518986085",
        "exp": "Sun Feb 18 21:34:45 UTC 2018",
        "iat": "Sun Feb 18 20:34:45 UTC 2018",
        "family_name": "Cui",
        "email": "theburningmonk@gmail.com"
      }
    },
    "stage": "prod"
  },
  "queryStringParameters": {
    "foo": "bar"
  },
  "headers": {
    "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
    "Accept-Language": "en-US,en;q=0.8",
    "CloudFront-Is-Desktop-Viewer": "true",
    "CloudFront-Is-SmartTV-Viewer": "false",
    "CloudFront-Is-Mobile-Viewer": "false",
    "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
    "CloudFront-Viewer-Country": "US",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Upgrade-Insecure-Requests": "1",
    "X-Forwarded-Port": "443",
    "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
    "X-Forwarded-Proto": "https",
    "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
    "CloudFront-Is-Tablet-Viewer": "false",
    "Cache-Control": "max-age=0",
    "User-Agent": "Custom User Agent String",
    "CloudFront-Forwarded-Proto": "https",
    "Accept-Encoding": "gzip, deflate, sdch"
  },
  "pathParameters": {
    "proxy": "path/to/resource"
  },
  "httpMethod": "POST",
  "stageVariables": {
    "baz": "qux"
  },
  "path": "/path/to/resource"
}

================================================
FILE: functions/accept-order.js
================================================
'use strict';

const co         = require('co');
const kinesis    = require('../lib/kinesis');
const log        = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
const wrapper    = require('../middleware/wrapper');

const streamName = process.env.order_events_stream;

const handler = co.wrap(function* (event, context, cb) {
  let req = JSON.parse(event.body);
  log.debug(`request body is valid JSON`, { requestBody: event.body });

  let restaurantName = req.restaurantName;
  let orderId = req.orderId;
  let userEmail = req.userEmail;

  correlationIds.set('order-id', orderId);
  correlationIds.set('restaurant-name', restaurantName);
  correlationIds.set('user-email', userEmail);

  log.debug('restaurant accepted order', { orderId, restaurantName, userEmail });

  let data = {
    orderId,
    userEmail,
    restaurantName,
    eventType: 'order_accepted'
  }

  let kinesisReq = {
    Data: JSON.stringify(data), // the SDK would base64 encode this for us
    PartitionKey: orderId,
    StreamName: streamName
  };

  yield cloudwatch.trackExecTime(
    "KinesisPutRecordLatency",
    () => kinesis.putRecord(kinesisReq).promise()
  );

  log.debug(`published event into Kinesis`, { eventName: 'order_accepted' });

  let response = {
    statusCode: 200,
    body: JSON.stringify({ orderId })
  }

  cb(null, response);
});

module.exports.handler = wrapper(handler);

================================================
FILE: functions/create-alarms.js
================================================
'use strict';

const _          = require('lodash');
const co         = require('co');
const AWS        = require('aws-sdk');
const apigateway = new AWS.APIGateway();
const cloudwatch = new AWS.CloudWatch();
const log        = require('../lib/log');

const alarmActions = (process.env.alarm_actions || '').split(',');
const okAction     = (process.env.ok_actions || '').split(',');

let enableDetailedMetrics = co.wrap(function* (restApiId, stageName) {
  let getResp = yield apigateway.getStage({ restApiId, stageName }).promise();
  log.debug('get stage settings', getResp.methodSettings);

  let isDetailedMetricsEnabled = _.get(getResp, 'methodSettings.*/*.metricsEnabled', false);
  if (isDetailedMetricsEnabled) {
    log.debug('detailed metrics already enabled', { restApiId, stageName });
  } else {
    let updateReq = {
      restApiId,
      stageName,
      patchOperations: [
        {
          path: "/*/*/metrics/enabled",
          value: "true",
          op: "replace"
        }
      ]
    };
    yield apigateway.updateStage(updateReq).promise();
    log.debug('enabled detailed metrics', { restApiId, stageName });
  }
});

let getRestEndpoints = co.wrap(function* (restApiId) {
  let resp = yield apigateway.getResources({ restApiId }).promise();
  log.debug('got REST resources', { restApiId });

  let resourceMethods = resp.items.map(x => {
    let methods = _.keys(x.resourceMethods);
    return methods.map(method => ({ resource: x.path, method }));
  });

  return _.flattenDeep(resourceMethods);
});

let getRestApiName = co.wrap(function* (restApiId) {
  let resp = yield apigateway.getRestApi({ restApiId }).promise();
  log.debug('got REST api', { restApiId });

  return resp.name;
});

let createAlarmsForEndpoints = co.wrap(function* (restApiId, stageName) {
  let apiName = yield getRestApiName(restApiId);
  log.debug(`API name is ${apiName}`, { restApiId, stageName });

  let restEndpoints = yield getRestEndpoints(restApiId);
  log.debug('got REST endpoints', { restApiId, stageName, restEndpoints });

  for (let endpoint of restEndpoints) {
    let putReq = {
      AlarmName: `API [${apiName}] stage [${stageName}] ${endpoint.method} ${endpoint.resource} : p99 > 1s`,
      MetricName: 'Latency',
      Dimensions: [
        { Name: 'ApiName',  Value: apiName },
        { Name: 'Resource', Value: endpoint.resource },
        { Name: 'Method',   Value: endpoint.method },
        { Name: 'Stage',    Value: stageName }
      ],
      Namespace: 'AWS/ApiGateway',
      Threshold: 1000,      // 1s
      ComparisonOperator: 'GreaterThanThreshold',      
      Period: 60,           // per min
      EvaluationPeriods: 5, 
      DatapointsToAlarm: 5, // 5 consecutive mins to trigger alarm
      ExtendedStatistic: 'p99',
      ActionsEnabled: true,
      AlarmActions: alarmActions,
      AlarmDescription: `auto-generated by Lambda [${process.env.AWS_LAMBDA_FUNCTION_NAME}]`,
      OKActions: okAction,
      Unit: 'Milliseconds'
    };
    yield cloudwatch.putMetricAlarm(putReq).promise();
  }

  log.debug('auto-created latency ALARMS for REST endpoints', { restApiId, stageName, restEndpoints });
});

module.exports.handler = co.wrap(function* (event, context, cb) {
  let restApiId = event.detail.requestParameters.restApiId;
  let stageName = event.detail.requestParameters.createDeploymentInput.stageName;

  yield enableDetailedMetrics(restApiId, stageName);

  yield createAlarmsForEndpoints(restApiId, stageName);

  cb(null, 'ok');
});

================================================
FILE: functions/fulfill-order.js
================================================
'use strict';

const co         = require('co');
const kinesis    = require('../lib/kinesis');
const log        = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
const wrapper    = require('../middleware/wrapper');

const streamName = process.env.order_events_stream;

const handler = co.wrap(function* (event, context, cb) {
  let body = JSON.parse(event.body);
  log.debug(`request body is valid JSON`, { requestBody: event.body });

  let restaurantName = body.restaurantName;
  let orderId = body.orderId;
  let userEmail = body.userEmail;

  correlationIds.set('order-id', orderId);
  correlationIds.set('restaurant-name', restaurantName);
  correlationIds.set('user-email', userEmail);

  log.debug('restaurant has fulfilled order', { orderId, restaurantName, userEmail });

  let data = {
    orderId,
    userEmail,
    restaurantName,
    eventType: 'order_fulfilled'
  }

  let kinesisReq = {
    Data: JSON.stringify(data), // the SDK would base64 encode this for us
    PartitionKey: orderId,
    StreamName: streamName
  };

  yield cloudwatch.trackExecTime(
    "KinesisPutRecordLatency", 
    () => kinesis.putRecord(kinesisReq).promise()
  );

  log.debug(`published event into Kinesis`, { eventName: 'order_fulfilled' });

  let response = {
    statusCode: 200,
    body: JSON.stringify({ orderId })
  }

  cb(null, response);
});

module.exports.handler = wrapper(handler);

================================================
FILE: functions/get-index.js
================================================
'use strict';

const co         = require("co");
const Promise    = require("bluebird");
const fs         = Promise.promisifyAll(require("fs"));
const Mustache   = require('mustache');
const http       = require('../lib/http');
const URL        = require('url');
const aws4       = require('../lib/aws4');
const log        = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
const AWSXRay    = require('aws-xray-sdk');
const wrapper    = require('../middleware/wrapper');
const { ssm, secretsManager } = require('middy/middlewares');

const STAGE     = process.env.STAGE;
const awsRegion = process.env.AWS_REGION;

const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

var html;

function* loadHtml() {
  if (!html) {
    html = yield fs.readFileAsync('static/index.html', 'utf-8');
  }

  return html;
}

function* getRestaurants(restaurantsApiUrl) {
  let url = URL.parse(restaurantsApiUrl);
  let opts = {
    host: url.hostname,
    path: url.pathname
  };

  aws4.sign(opts);

  let httpReq = http({
    uri: restaurantsApiUrl,
    headers: opts.headers
  });
  
  return new Promise((resolve, reject) => {
    let f = co.wrap(function* (subsegment) {
      if (subsegment) {
        subsegment.addMetadata('url', restaurantsApiUrl);
      }

      try {
        let body = (yield httpReq).body;
        if (subsegment) {
          subsegment.close();
        }
        resolve(body);
      } catch (err) {
        if (subsegment) {
          subsegment.close(err);
        }
        reject(err);
      }
    });

    // the current sub/segment
    let segment = AWSXRay.getSegment();

    AWSXRay.captureAsyncFunc("getting restaurants", f, segment);
  });
}

const handler = co.wrap(function* (event, context, callback) {
  yield aws4.init();

  let template = yield loadHtml();
  log.debug("loaded HTML template");

  let restaurants = yield cloudwatch.trackExecTime(
    "GetRestaurantsLatency",
    () => getRestaurants(context.restaurants_api)
  );
  log.debug(`loaded ${restaurants.length} restaurants`);

  let dayOfWeek = days[new Date().getDay()];
  let view = {
    dayOfWeek, 
    restaurants,
    awsRegion,
    cognitoUserPoolId: context.cognito.user_pool_id,
    cognitoClientId: context.cognito.client_id,
    searchUrl: `${context.restaurants_api}/search`,
    placeOrderUrl: `${context.orders_api}`
  };
  let html = Mustache.render(template, view);
  log.debug(`rendered HTML [${html.length} bytes]`);

  // uncomment this to cause function to err
  // yield http({ uri: 'https://theburningmonk.com' });

  cloudwatch.incrCount('RestaurantsReturned', restaurants.length);

  const response = {
    statusCode: 200,
    body: html,
    headers: {
      'content-type': 'text/html; charset=UTF-8'
    }
  };

  callback(null, response);
});

module.exports.handler = wrapper(handler)
  .use(ssm({
    cache: true,
    cacheExpiryInMillis: 3 * 60 * 1000, // 3 mins
    setToContext: true,
    names: {
      restaurants_api: `/bigmouth/${STAGE}/restaurants_api`,
      orders_api: `/bigmouth/${STAGE}/orders_api`
    }
  }))
  .use(secretsManager({
    cache: true,
    cacheExpiryInMillis: 3 * 60 * 1000, // 3 mins
    secrets: {
      cognito: `/bigmouth/${STAGE}/cognito`
    }
  }));

================================================
FILE: functions/get-restaurants.js
================================================
'use strict';

const co         = require('co');
const AWSXRay    = require('aws-xray-sdk');
const AWS        = AWSXRay.captureAWS(require('aws-sdk'));
const dynamodb   = new AWS.DynamoDB.DocumentClient();
const log        = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
const wrapper    = require('../middleware/wrapper');

const defaultResults = process.env.defaultResults || 8;
const tableName      = process.env.restaurants_table;

function* getRestaurants(count) {
  let req = {
    TableName: tableName,
    Limit: count
  };

  let resp = yield cloudwatch.trackExecTime(
    "DynamoDBScanLatency",
    () => dynamodb.scan(req).promise()
  );
  return resp.Items;
}

const handler = co.wrap(function* (event, context, cb) {
  let restaurants = yield getRestaurants(defaultResults);
  log.debug(`loaded ${restaurants.length} restaurants`);

  cloudwatch.incrCount("RestaurantsReturned", restaurants.length);

  let response = {
    statusCode: 200,
    body: JSON.stringify(restaurants)
  }

  cb(null, response);
});

module.exports.handler = wrapper(handler);

================================================
FILE: functions/notify-restaurant.js
================================================
'use strict';

const co     = require('co');
const notify = require('../lib/notify');
const retry  = require('../lib/retry');
const log    = require('../lib/log');
const wrapper = require('../middleware/wrapper');
const flushMetrics  = require('../middleware/flush-metrics');

const handler = co.wrap(function* (event, context, cb) {
  let events = context.parsedKinesisEvents;
  let orderPlaced = events.filter(r => r.eventType === 'order_placed');
  log.debug(`found ${orderPlaced.length} 'order_placed' events`);

  for (let order of orderPlaced) {
    order.scopeToThis();

    try {
      yield notify.restaurantOfOrder(order);
    } catch (err) {
      yield retry.restaurantNotification(order);

      let logContext = {
        orderId: order.orderId,
        restaurantName: order.restaurantName,
        userEmail: order.userEmail
      };
      log.warn('failed to notify restaurant of new order', logContext, err);
    }

    order.unscope();
  }
  
  cb(null, "all done");
});

module.exports.handler = wrapper(handler)
  .use(flushMetrics);

================================================
FILE: functions/notify-user.js
================================================
'use strict';

const co     = require('co');
const notify = require('../lib/notify');
const retry  = require('../lib/retry');
const log    = require('../lib/log');
const wrapper = require('../middleware/wrapper');

const flushMetrics  = require('../middleware/flush-metrics');

const handler = co.wrap(function* (event, context, cb) {
  let events = context.parsedKinesisEvents;
  let orderAccepted = events.filter(r => r.eventType === 'order_accepted');
  log.debug(`found ${orderAccepted.length} 'order_accepted' events`);

  for (let order of orderAccepted) {
    order.scopeToThis();

    try {
      yield notify.userOfOrderAccepted(order);
    } catch (err) {
      yield retry.userNotification(order);

      let logContext = {
        orderId: order.orderId,
        restaurantName: order.restaurantName,
        userEmail: order.userEmail
      };
      log.warn('failed to notify user of accepted order', logContext, err);
    }

    order.unscope();
  }
  
  cb(null, "all done");
});

module.exports.handler = wrapper(handler)
  .use(flushMetrics);

================================================
FILE: functions/place-order.js
================================================
'use strict';

const _              = require('lodash');
const co             = require('co');
const kinesis        = require('../lib/kinesis');
const chance         = require('chance').Chance();
const log            = require('../lib/log');
const cloudwatch     = require('../lib/cloudwatch');
const correlationIds = require('../lib/correlation-ids');
const wrapper        = require('../middleware/wrapper');

const streamName = process.env.order_events_stream;

const UNAUTHORIZED = {
  statusCode: 401,
  body: "unauthorized"
}

const handler = co.wrap(function* (event, context, cb) {
  let req = JSON.parse(event.body);
  log.debug(`request body is valid JSON`, { requestBody: event.body });

  let userEmail = _.get(event, 'requestContext.authorizer.claims.email');
  if (!userEmail) {
    cb(null, UNAUTHORIZED);
    log.error('unauthorized request, user email is not provided');

    return;
  }

  let restaurantName = req.restaurantName;
  let orderId = chance.guid();

  correlationIds.set('order-id', orderId);
  correlationIds.set('restaurant-name', restaurantName);
  correlationIds.set('user-email', userEmail);

  log.debug(`placing order...`, { orderId, restaurantName, userEmail });

  let data = {
    orderId,
    userEmail,
    restaurantName,
    eventType: 'order_placed'
  }

  let kinesisReq = {
    Data: JSON.stringify(data), // the SDK would base64 encode this for us
    PartitionKey: orderId,
    StreamName: streamName
  };

  yield cloudwatch.trackExecTime(
    "KinesisPutRecordLatency",
    () => kinesis.putRecord(kinesisReq).promise()
  );

  log.debug(`published event into Kinesis`, { eventName: 'order_placed' });

  let response = {
    statusCode: 200,
    body: JSON.stringify({ orderId })
  }

  cb(null, response);
});

module.exports.handler = wrapper(handler);

================================================
FILE: functions/retry-notify-restaurant.js
================================================
'use strict';

const co         = require('co');
const notify     = require('../lib/notify');
const log        = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
const wrapper    = require('../middleware/wrapper');

const flushMetrics  = require('../middleware/flush-metrics');

const handler = co.wrap(function* (event, context, cb) {
  let order = JSON.parse(event.Records[0].Sns.Message);
  order.retried = true;

  let logContext = {
    orderId: order.orderId,
    restaurantName: order.restaurantName,
    userEmail: order.userEmail,
    retry: true
  };

  try {
    yield notify.restaurantOfOrder(order);
    cb(null, "all done");
  } catch (err) {
    log.warn('failed to notify restaurant of new order', logContext, err);
    
    cb(err);
  } finally {
    cloudwatch.incrCount("NotifyRestaurantRetried");
  }
});

module.exports.handler = wrapper(handler)
  .use(flushMetrics);

================================================
FILE: functions/retry-notify-user.js
================================================
'use strict';

const co         = require('co');
const notify     = require('../lib/notify');
const log        = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
const wrapper    = require('../middleware/wrapper');

const flushMetrics = require('../middleware/flush-metrics');

const handler = co.wrap(function* (event, context, cb) {
  let order = JSON.parse(event.Records[0].Sns.Message);
  order.retried = true;

  let logContext = {
    orderId: order.orderId,
    restaurantName: order.restaurantName,
    userEmail: order.userEmail,
    retry: true
  };

  try {
    yield notify.userOfOrderAccepted(order);
    cb(null, "all done");
  } catch (err) {
    log.warn('failed to notify user of accepted order', logContext, err);

    cb(err);
  } finally {
    cloudwatch.incrCount("NotifyUserRetried");
  }
});

module.exports.handler = wrapper(handler)
  .use(flushMetrics);

================================================
FILE: functions/search-restaurants.js
================================================
'use strict';

const co         = require('co');
const AWSXRay    = require('aws-xray-sdk');
const AWS        = AWSXRay.captureAWS(require('aws-sdk'));
const dynamodb   = new AWS.DynamoDB.DocumentClient();
const log        = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');
const wrapper    = require('../middleware/wrapper');

const defaultResults = process.env.defaultResults || 8;
const tableName      = process.env.restaurants_table;

function* findRestaurantsByTheme(theme, count) {
  let req = {
    TableName: tableName,
    Limit: count,
    FilterExpression: "contains(themes, :theme)",
    ExpressionAttributeValues: { ":theme": theme }
  };

  let resp = yield cloudwatch.trackExecTime(
    "DynamoDBScanLatency", 
    () => dynamodb.scan(req).promise()
  );
  return resp.Items;
}

const handler = co.wrap(function* (event, context, cb) {
  let req = JSON.parse(event.body);
  log.debug(`request body is valid JSON`, { requestBody: event.body });

  let restaurants = yield findRestaurantsByTheme(req.theme, defaultResults);
  log.debug(`found ${restaurants.length} restaurants`);

  cloudwatch.incrCount("RestaurantsFound", restaurants.length);

  let response = {
    statusCode: 200,
    body: JSON.stringify(restaurants)
  }

  cb(null, response);
});

module.exports.handler = wrapper(handler);

================================================
FILE: lib/aws4.js
================================================
var aws4 = exports,    
    url = require('url'),
    querystring = require('querystring'),
    crypto = require('crypto'),
    lru = require('./lru'),
    credentialsCache = lru(1000),
    awscred = require('./awscred')

// http://docs.amazonwebservices.com/general/latest/gr/signature-version-4.html

function hmac(key, string, encoding) {
  return crypto.createHmac('sha256', key).update(string, 'utf8').digest(encoding)
}

function hash(string, encoding) {
  return crypto.createHash('sha256').update(string, 'utf8').digest(encoding)
}

// This function assumes the string has already been percent encoded
function encodeRfc3986(urlEncodedString) {
  return urlEncodedString.replace(/[!'()*]/g, function(c) {
    return '%' + c.charCodeAt(0).toString(16).toUpperCase()
  })
}

// request: { path | body, [host], [method], [headers], [service], [region] }
// credentials: { accessKeyId, secretAccessKey, [sessionToken] }
function RequestSigner(request, credentials) {

  if (typeof request === 'string') request = url.parse(request)

  var headers = request.headers = (request.headers || {}),
      hostParts = this.matchHost(request.hostname || request.host || headers.Host || headers.host)

  this.request = request
  this.credentials = credentials || this.defaultCredentials()

  this.service = request.service || hostParts[0] || ''
  this.region = request.region || hostParts[1] || 'us-east-1'

  // SES uses a different domain from the service name
  if (this.service === 'email') this.service = 'ses'

  if (!request.method && request.body)
    request.method = 'POST'

  if (!headers.Host && !headers.host) {
    headers.Host = request.hostname || request.host || this.createHost()

    // If a port is specified explicitly, use it as is
    if (request.port)
      headers.Host += ':' + request.port
  }
  if (!request.hostname && !request.host)
    request.hostname = headers.Host || headers.host

  this.isCodeCommitGit = this.service === 'codecommit' && request.method === 'GIT'
}

RequestSigner.prototype.matchHost = function(host) {
  var match = (host || '').match(/([^\.]+)\.(?:([^\.]*)\.)?amazonaws\.com$/)
  var hostParts = (match || []).slice(1, 3)

  // ES's hostParts are sometimes the other way round, if the value that is expected
  // to be region equals ‘es’ switch them back
  // e.g. search-cluster-name-aaaa00aaaa0aaa0aaaaaaa0aaa.us-east-1.es.amazonaws.com
  if (hostParts[1] === 'es')
    hostParts = hostParts.reverse()

  return hostParts
}

// http://docs.aws.amazon.com/general/latest/gr/rande.html
RequestSigner.prototype.isSingleRegion = function() {
  // Special case for S3 and SimpleDB in us-east-1
  if (['s3', 'sdb'].indexOf(this.service) >= 0 && this.region === 'us-east-1') return true

  return ['cloudfront', 'ls', 'route53', 'iam', 'importexport', 'sts']
    .indexOf(this.service) >= 0
}

RequestSigner.prototype.createHost = function() {
  var region = this.isSingleRegion() ? '' :
        (this.service === 's3' && this.region !== 'us-east-1' ? '-' : '.') + this.region,
      service = this.service === 'ses' ? 'email' : this.service
  return service + region + '.amazonaws.com'
}

RequestSigner.prototype.prepareRequest = function() {
  this.parsePath()

  var request = this.request, headers = request.headers, query

  if (request.signQuery) {

    this.parsedPath.query = query = this.parsedPath.query || {}

    if (this.credentials.sessionToken)
      query['X-Amz-Security-Token'] = this.credentials.sessionToken

    if (this.service === 's3' && !query['X-Amz-Expires'])
      query['X-Amz-Expires'] = 86400

    if (query['X-Amz-Date'])
      this.datetime = query['X-Amz-Date']
    else
      query['X-Amz-Date'] = this.getDateTime()

    query['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'
    query['X-Amz-Credential'] = this.credentials.accessKeyId + '/' + this.credentialString()
    query['X-Amz-SignedHeaders'] = this.signedHeaders()

  } else {

    if (!request.doNotModifyHeaders && !this.isCodeCommitGit) {
      if (request.body && !headers['Content-Type'] && !headers['content-type'])
        headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'

      if (request.body && !headers['Content-Length'] && !headers['content-length'])
        headers['Content-Length'] = Buffer.byteLength(request.body)

      if (this.credentials.sessionToken && !headers['X-Amz-Security-Token'] && !headers['x-amz-security-token'])
        headers['X-Amz-Security-Token'] = this.credentials.sessionToken

      if (this.service === 's3' && !headers['X-Amz-Content-Sha256'] && !headers['x-amz-content-sha256'])
        headers['X-Amz-Content-Sha256'] = hash(this.request.body || '', 'hex')

      if (headers['X-Amz-Date'] || headers['x-amz-date'])
        this.datetime = headers['X-Amz-Date'] || headers['x-amz-date']
      else
        headers['X-Amz-Date'] = this.getDateTime()
    }

    delete headers.Authorization
    delete headers.authorization
  }
}

RequestSigner.prototype.sign = function() {
  if (!this.parsedPath) this.prepareRequest()

  if (this.request.signQuery) {
    this.parsedPath.query['X-Amz-Signature'] = this.signature()
  } else {
    this.request.headers.Authorization = this.authHeader()
  }

  this.request.path = this.formatPath()

  return this.request
}

RequestSigner.prototype.getDateTime = function() {
  if (!this.datetime) {
    var headers = this.request.headers,
      date = new Date(headers.Date || headers.date || new Date)

    this.datetime = date.toISOString().replace(/[:\-]|\.\d{3}/g, '')

    // Remove the trailing 'Z' on the timestamp string for CodeCommit git access
    if (this.isCodeCommitGit) this.datetime = this.datetime.slice(0, -1)
  }
  return this.datetime
}

RequestSigner.prototype.getDate = function() {
  return this.getDateTime().substr(0, 8)
}

RequestSigner.prototype.authHeader = function() {
  return [
    'AWS4-HMAC-SHA256 Credential=' + this.credentials.accessKeyId + '/' + this.credentialString(),
    'SignedHeaders=' + this.signedHeaders(),
    'Signature=' + this.signature(),
  ].join(', ')
}

RequestSigner.prototype.signature = function() {
  var date = this.getDate(),
      cacheKey = [this.credentials.secretAccessKey, date, this.region, this.service].join(),
      kDate, kRegion, kService, kCredentials = credentialsCache.get(cacheKey)
  if (!kCredentials) {
    kDate = hmac('AWS4' + this.credentials.secretAccessKey, date)
    kRegion = hmac(kDate, this.region)
    kService = hmac(kRegion, this.service)
    kCredentials = hmac(kService, 'aws4_request')
    credentialsCache.set(cacheKey, kCredentials)
  }
  return hmac(kCredentials, this.stringToSign(), 'hex')
}

RequestSigner.prototype.stringToSign = function() {
  return [
    'AWS4-HMAC-SHA256',
    this.getDateTime(),
    this.credentialString(),
    hash(this.canonicalString(), 'hex'),
  ].join('\n')
}

RequestSigner.prototype.canonicalString = function() {
  if (!this.parsedPath) this.prepareRequest()

  var pathStr = this.parsedPath.path,
      query = this.parsedPath.query,
      headers = this.request.headers,
      queryStr = '',
      normalizePath = this.service !== 's3',
      decodePath = this.service === 's3' || this.request.doNotEncodePath,
      decodeSlashesInPath = this.service === 's3',
      firstValOnly = this.service === 's3',
      bodyHash

  if (this.service === 's3' && this.request.signQuery) {
    bodyHash = 'UNSIGNED-PAYLOAD'
  } else if (this.isCodeCommitGit) {
    bodyHash = ''
  } else {
    bodyHash = headers['X-Amz-Content-Sha256'] || headers['x-amz-content-sha256'] ||
      hash(this.request.body || '', 'hex')
  }

  if (query) {
    queryStr = encodeRfc3986(querystring.stringify(Object.keys(query).sort().reduce(function(obj, key) {
      if (!key) return obj
      obj[key] = !Array.isArray(query[key]) ? query[key] :
        (firstValOnly ? query[key][0] : query[key].slice().sort())
      return obj
    }, {})))
  }
  if (pathStr !== '/') {
    if (normalizePath) pathStr = pathStr.replace(/\/{2,}/g, '/')
    pathStr = pathStr.split('/').reduce(function(path, piece) {
      if (normalizePath && piece === '..') {
        path.pop()
      } else if (!normalizePath || piece !== '.') {
        if (decodePath) piece = querystring.unescape(piece)
        path.push(encodeRfc3986(querystring.escape(piece)))
      }
      return path
    }, []).join('/')
    if (pathStr[0] !== '/') pathStr = '/' + pathStr
    if (decodeSlashesInPath) pathStr = pathStr.replace(/%2F/g, '/')
  }

  return [
    this.request.method || 'GET',
    pathStr,
    queryStr,
    this.canonicalHeaders() + '\n',
    this.signedHeaders(),
    bodyHash,
  ].join('\n')
}

RequestSigner.prototype.canonicalHeaders = function() {
  var headers = this.request.headers
  function trimAll(header) {
    return header.toString().trim().replace(/\s+/g, ' ')
  }
  return Object.keys(headers)
    .sort(function(a, b) { return a.toLowerCase() < b.toLowerCase() ? -1 : 1 })
    .map(function(key) { return key.toLowerCase() + ':' + trimAll(headers[key]) })
    .join('\n')
}

RequestSigner.prototype.signedHeaders = function() {
  return Object.keys(this.request.headers)
    .map(function(key) { return key.toLowerCase() })
    .sort()
    .join(';')
}

RequestSigner.prototype.credentialString = function() {
  return [
    this.getDate(),
    this.region,
    this.service,
    'aws4_request',
  ].join('/')
}

RequestSigner.prototype.defaultCredentials = function() {
  var env = process.env

  return {
    accessKeyId: env.AWS_ACCESS_KEY_ID || env.AWS_ACCESS_KEY,
    secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY,
    sessionToken: env.AWS_SESSION_TOKEN,
  }
}

RequestSigner.prototype.parsePath = function() {
  var path = this.request.path || '/',
      queryIx = path.indexOf('?'),
      query = null

  if (queryIx >= 0) {
    query = querystring.parse(path.slice(queryIx + 1))
    path = path.slice(0, queryIx)
  }

  // S3 doesn't always encode characters > 127 correctly and
  // all services don't encode characters > 255 correctly
  // So if there are non-reserved chars (and it's not already all % encoded), just encode them all
  if (/[^0-9A-Za-z!'()*\-._~%/]/.test(path)) {
    path = path.split('/').map(function(piece) {
      return querystring.escape(querystring.unescape(piece))
    }).join('/')
  }

  this.parsedPath = {
    path: path,
    query: query,
  }
}

RequestSigner.prototype.formatPath = function() {
  var path = this.parsedPath.path,
      query = this.parsedPath.query

  if (!query) return path

  // Services don't support empty query string keys
  if (query[''] != null) delete query['']

  return path + '?' + encodeRfc3986(querystring.stringify(query))
}

aws4.RequestSigner = RequestSigner

aws4.sign = function(request, credentials) {
  return new RequestSigner(request, credentials).sign()
}

var isInitialized = false;
aws4.init = function() {
  return new Promise(function(resolve, reject) {
    if (isInitialized) {
      return resolve()
    }

    if (process.env.AWS_ACCESS_KEY_ID) {
      isInitialized = true;
      return resolve()
    } else {    
      console.error("initializing AWS credentials...")
  
      awscred.load(function(error, result) {
        if (error) {
          reject(error)
        } else {
          let cred = result.credentials;
          process.env.AWS_ACCESS_KEY_ID = cred.accessKeyId;
          process.env.AWS_SECRET_ACCESS_KEY = cred.secretAccessKey;
    
          if (cred.sessionToken) {
            process.env.AWS_SESSION_TOKEN = cred.sessionToken;
          }
  
          resolve()
          isInitialized = true
        }
      })
    }
  })  
}

================================================
FILE: lib/awscred.js
================================================
var fs = require('fs'),
    path = require('path'),
    http = require('http'),
    env = process.env

exports.credentialsCallChain = [
  loadCredentialsFromEnv,
  loadCredentialsFromIniFile,
  loadCredentialsFromEc2Metadata,
  loadCredentialsFromEcs,
]

exports.regionCallChain = [
  loadRegionFromEnv,
  loadRegionFromIniFile,
]

exports.load = exports.loadCredentialsAndRegion = loadCredentialsAndRegion
exports.loadCredentials = loadCredentials
exports.loadRegion = loadRegion
exports.loadRegionSync = loadRegionSync
exports.loadCredentialsFromEnv = loadCredentialsFromEnv
exports.loadRegionFromEnv = loadRegionFromEnv
exports.loadRegionFromEnvSync = loadRegionFromEnvSync
exports.loadCredentialsFromIniFile = loadCredentialsFromIniFile
exports.loadRegionFromIniFile = loadRegionFromIniFile
exports.loadRegionFromIniFileSync = loadRegionFromIniFileSync
exports.loadCredentialsFromEc2Metadata = loadCredentialsFromEc2Metadata
exports.loadProfileFromIniFile = loadProfileFromIniFile
exports.loadProfileFromIniFileSync = loadProfileFromIniFileSync
exports.merge = merge

function loadCredentialsAndRegion(options, cb) {
  if (!cb) { cb = options; options = {} }
  cb = once(cb)

  var out = {}, callsRemaining = 2

  function checkDone(propName) {
    return function(err, data) {
      if (err) return cb(err)
      out[propName] = data
      if (!--callsRemaining) return cb(null, out)
    }
  }

  loadCredentials(options, checkDone('credentials'))

  loadRegion(options, checkDone('region'))
}

function loadCredentials(options, cb) {
  if (!cb) { cb = options; options = {} }
  var credentialsCallChain = options.credentialsCallChain || exports.credentialsCallChain

  function nextCall(i) {
    credentialsCallChain[i](options, function(err, credentials) {
      if (err) return cb(err)

      if (credentials.accessKeyId && credentials.secretAccessKey)
        return cb(null, credentials)

      if (i >= credentialsCallChain.length - 1)
        return cb(null, {})

      nextCall(i + 1)
    })
  }
  nextCall(0)
}

function loadRegion(options, cb) {
  if (!cb) { cb = options; options = {} }
  var regionCallChain = options.regionCallChain || exports.regionCallChain

  function nextCall(i) {
    regionCallChain[i](options, function(err, region) {
      if (err) return cb(err)

      if (region)
        return cb(null, region)

      if (i >= regionCallChain.length - 1)
        return cb(null, 'us-east-1')

      nextCall(i + 1)
    })
  }
  nextCall(0)
}

function loadRegionSync(options) {
  return loadRegionFromEnvSync(options) || loadRegionFromIniFileSync(options)
}

function loadCredentialsFromEnv(options, cb) {
  if (!cb) { cb = options; options = {} }

  cb(null, {
    accessKeyId: env.AWS_ACCESS_KEY_ID || env.AMAZON_ACCESS_KEY_ID || env.AWS_ACCESS_KEY,
    secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AMAZON_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY,
    sessionToken: env.AWS_SESSION_TOKEN || env.AMAZON_SESSION_TOKEN,
  })
}

function loadRegionFromEnv(options, cb) {
  if (!cb) { cb = options; options = {} }

  cb(null, loadRegionFromEnvSync())
}

function loadRegionFromEnvSync() {
  return env.AWS_REGION || env.AMAZON_REGION || env.AWS_DEFAULT_REGION
}

function loadCredentialsFromIniFile(options, cb) {
  if (!cb) { cb = options; options = {} }

  loadProfileFromIniFile(options, 'credentials', function(err, profile) {
    if (err) return cb(err)
    cb(null, {
      accessKeyId: profile.aws_access_key_id,
      secretAccessKey: profile.aws_secret_access_key,
      sessionToken: profile.aws_session_token,
    })
  })
}

function loadRegionFromIniFile(options, cb) {
  if (!cb) { cb = options; options = {} }

  loadProfileFromIniFile(options, 'config', function(err, profile) {
    if (err) return cb(err)
    cb(null, profile.region)
  })
}

function loadRegionFromIniFileSync(options) {
  return loadProfileFromIniFileSync(options || {}, 'config').region
}

var TIMEOUT_CODES = ['ECONNRESET', 'ETIMEDOUT', 'EHOSTUNREACH', 'Unknown system errno 64']
var ec2Callbacks = []
var ecsCallbacks = []

function loadCredentialsFromEcs(options, cb) {
  if (!cb) { cb = options; options = {} }

  ecsCallbacks.push(cb)
  if (ecsCallbacks.length > 1) return // only want one caller at a time

  cb = function(err, credentials) {
    ecsCallbacks.forEach(function(cb) { cb(err, credentials) })
    ecsCallbacks = []
  }

  if (options.timeout == null) options.timeout = 5000
  options.host = '169.254.170.2'
  options.path = process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI

  return request(options, function(err, res, data) {    
    if (err && ~TIMEOUT_CODES.indexOf(err.code)) return cb(null, {})
    if (err) return cb(err)

    if (res.statusCode != 200)
      return cb(new Error('Failed to fetch IAM role: ' + res.statusCode + ' ' + data))

    try { data = JSON.parse(data) } catch (e) { }

    if (res.statusCode != 200)
      return cb(new Error('Failed to fetch IAM credentials: ' + res.statusCode + ' ' + data))    

    cb(null, {
      accessKeyId: data.AccessKeyId,
      secretAccessKey: data.SecretAccessKey,
      sessionToken: data.Token,
      expiration: new Date(data.Expiration),
    })
  })
}

function loadCredentialsFromEc2Metadata(options, cb) {
  if (!cb) { cb = options; options = {} }

  ec2Callbacks.push(cb)
  if (ec2Callbacks.length > 1) return // only want one caller at a time

  cb = function(err, credentials) {
    ec2Callbacks.forEach(function(cb) { cb(err, credentials) })
    ec2Callbacks = []
  }

  if (options.timeout == null) options.timeout = 5000
  options.host = '169.254.169.254'
  options.path = '/latest/meta-data/iam/security-credentials/'

  return request(options, function(err, res, data) {
    if (err && ~TIMEOUT_CODES.indexOf(err.code)) return cb(null, {})
    if (err) return cb(err)

    if (res.statusCode != 200)
      return cb(new Error('Failed to fetch IAM role: ' + res.statusCode + ' ' + data))

    options.path += data.split('\n')[0]
    request(options, function(err, res, data) {
      if (err) return cb(err)

      try { data = JSON.parse(data) } catch (e) { }

      if (res.statusCode != 200 || data.Code != 'Success')
        return cb(new Error('Failed to fetch IAM credentials: ' + res.statusCode + ' ' + data))

      cb(null, {
        accessKeyId: data.AccessKeyId,
        secretAccessKey: data.SecretAccessKey,
        sessionToken: data.Token,
        expiration: new Date(data.Expiration),
      })
    })
  })
}

function loadProfileFromIniFile(options, defaultFilename, cb) {
  var filename = options.filename || path.join(resolveHome(), '.aws', defaultFilename),
      profile = options.profile || resolveProfile()

  fs.readFile(filename, 'utf8', function(err, data) {
    if (err && err.code == 'ENOENT') return cb(null, {})
    if (err) return cb(err)
    cb(null, parseAwsIni(data)[profile] || {})
  })
}

function loadProfileFromIniFileSync(options, defaultFilename) {
  var filename = options.filename || path.join(resolveHome(), '.aws', defaultFilename),
      profile = options.profile || resolveProfile(),
      data

  try {
    data = fs.readFileSync(filename, 'utf8')
  } catch (err) {
    if (err.code == 'ENOENT') return {}
    throw err
  }

  return parseAwsIni(data)[profile] || {}
}

function merge(obj, options, cb) {
  if (!cb) { cb = options; options = {} }

  var needRegion = !obj.region
  var needCreds = !obj.credentials || !obj.credentials.accessKeyId || !obj.credentials.secretAccessKey

  function loadCreds(cb) {
    if (needRegion && needCreds) {
      return loadCredentialsAndRegion(options, cb)
    } else if (needRegion) {
      return loadRegion(options, function(err, region) { cb(err, {region: region}) })
    } else if (needCreds) {
      return loadCredentials(options, function(err, credentials) { cb(err, {credentials: credentials}) })
    }
    cb(null, {})
  }

  loadCreds(function(err, creds) {
    if (err) return cb(err)

    if (creds.region) obj.region = creds.region
    if (creds.credentials) {
      if (!obj.credentials) {
        obj.credentials = creds.credentials
      } else {
        Object.keys(creds.credentials).forEach(function(key) {
          if (!obj.credentials[key]) obj.credentials[key] = creds.credentials[key]
        })
      }
    }

    cb()
  })
}

function resolveProfile() {
  return env.AWS_PROFILE || env.AMAZON_PROFILE || 'default'
}

function resolveHome() {
  return env.HOME || env.USERPROFILE || ((env.HOMEDRIVE || 'C:') + env.HOMEPATH)
}

// Fairly strict INI parser – will only deal with alpha keys, must be within sections
function parseAwsIni(ini) {
  var section,
      out = Object.create(null),
      re = /^\[([^\]]+)\]\s*$|^([a-z_]+)\s*=\s*(.+?)\s*$/,
      lines = ini.split(/\r?\n/)

  lines.forEach(function(line) {
    var match = line.match(re)
    if (!match) return
    if (match[1]) {
      section = match[1]
      if (out[section] == null) out[section] = Object.create(null)
    } else if (section) {
      out[section][match[2]] = match[3]
    }
  })

  return out
}

function request(options, cb) {
  cb = once(cb)

  var req = http.request(options, function(res) {
    var data = ''
    res.setEncoding('utf8')
    res.on('error', cb)
    res.on('data', function(chunk) { data += chunk })
    res.on('end', function() { cb(null, res, data) })
  }).on('error', cb)

  if (options.timeout != null) {
    req.setTimeout(options.timeout)
    req.on('timeout', function() { req.abort() })
  }

  req.end()
}

function once(cb) {
  var called = false
  return function() {
    if (called) return
    called = true
    cb.apply(this, arguments)
  }
}

================================================
FILE: lib/cloudwatch.js
================================================
'use strict';

const co         = require('co');
const AWSXRay    = require('aws-xray-sdk');
const AWS        = AWSXRay.captureAWS(require('aws-sdk'));
const log        = require('./log');
const cloudwatch = new AWS.CloudWatch();

const namespace = 'big-mouth';
const async     = (process.env.async_metrics || 'false') === 'true';

// the Lambda execution environment defines a number of env variables:
//    https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html
// and the serverless framework also defines a STAGE env variable too
const dimensions = 
  [
    { Name: 'Function', Value: process.env.AWS_LAMBDA_FUNCTION_NAME	},
    { Name: 'Version', Value: process.env.AWS_LAMBDA_FUNCTION_VERSION	},
    { Name: 'Stage', Value: process.env.STAGE	}
  ]
  .filter(dim => dim.Value);

let countMetrics = {};
let timeMetrics  = {};

function getCountMetricData(name, value) {
  return {
    MetricName : name,
    Dimensions : dimensions,
    Unit       : 'Count',
    Value      : value
  };
}

function getTimeMetricData(name, statsValues) {
  return {
    MetricName      : name,
    Dimensions      : dimensions,
    Unit            : 'Milliseconds',
    StatisticValues : statsValues
  };
}

function getCountMetricDatum() {
  let keys = Object.keys(countMetrics);
  if (keys.length === 0) {
    return [];
  }

  let metricDatum = keys.map(key => getCountMetricData(key, countMetrics[key]));
  countMetrics = {}; // zero out the recorded count metrics
  return metricDatum;
}

function getTimeMetricDatum() {
  let keys = Object.keys(timeMetrics);
  if (keys.length === 0) {
    return [];
  }

  let metricDatum = keys.map(key => getTimeMetricData(key, timeMetrics[key]));
  timeMetrics = {}; // zero out the recorded time metrics
  return metricDatum;
}

let flush = co.wrap(function* () {
  let countDatum = getCountMetricDatum();
  let timeDatum  = getTimeMetricDatum();
  let allDatum   = countDatum.concat(timeDatum);

  if (allDatum.length == 0) { return; }

  let metricNames = allDatum.map(x => x.MetricName).join(',');
  log.debug(`flushing [${allDatum.length}] metrics to CloudWatch: ${metricNames}`);

  var params = {
    MetricData: allDatum,
    Namespace: namespace
  };

  try {
    yield cloudwatch.putMetricData(params).promise();
    log.debug(`flushed [${allDatum.length}] metrics to CloudWatch: ${metricNames}`);
  } catch (err) {
    log.warn(`cloudn't flush [${allDatum.length}] CloudWatch metrics`, null, err);
  }  
});

function clear() {
  countMetrics = {};
  timeMetrics = {};
}

function incrCount(metricName, count) {
  count = count || 1;

  if (async) {
    console.log(`MONITORING|${count}|count|${metricName}|${namespace}`);
  } else {
    if (countMetrics[metricName]) {
      countMetrics[metricName] += count;
    } else {
      countMetrics[metricName] = count;
    }
  }
}

function recordTimeInMillis(metricName, ms) {
  if (!ms) {
    return;
  }

  log.debug(`new execution time for [${metricName}] : ${ms} milliseconds`);

  if (async) {
    console.log(`MONITORING|${ms}|milliseconds|${metricName}|${namespace}`);
  } else {
    if (timeMetrics[metricName]) {
      let metric = timeMetrics[metricName];
      metric.Sum         += ms;
      metric.Maximum     = Math.max(metric.Maximum, ms);
      metric.Minimum     = Math.min(metric.Minimum, ms);
      metric.SampleCount += 1;
    } else {
      let statsValues = {
        Maximum     : ms,
        Minimum     : ms,
        SampleCount : 1,
        Sum         : ms
      };
      timeMetrics[metricName] = statsValues;
    }
  }
}

function trackExecTime(metricName, f) {
  if (!f || typeof f !== "function") {
    throw new Error('cloudWatch.trackExecTime requires a function, eg. () => 42');
  }

  if (!metricName) {
    throw new Error('cloudWatch.trackExecTime requires a metric name, eg. "CloudSearch-latency"');
  }

  let start = new Date().getTime(), end;
  let res = f();
  
  // anything with a 'then' function can be considered a Promise...
  // http://stackoverflow.com/a/27746324/55074
  if (!res.hasOwnProperty('then')) {
    end = new Date().getTime();
    recordTimeInMillis(metricName, end-start);
    return res;
  } else {
    return res.then(x => {
      end = new Date().getTime();
      recordTimeInMillis(metricName, end-start);
      return x;
    });
  }
}

module.exports = {
  flush,
  clear,
  incrCount,
  trackExecTime,
  recordTimeInMillis
};

================================================
FILE: lib/correlation-ids.js
================================================
'use strict';

let clearAll = () => global.CONTEXT = undefined;

let replaceAllWith = ctx => global.CONTEXT = ctx;

let set = (key, value) => {
  if (!key.startsWith("x-correlation-")) {
    key = "x-correlation-" + key;
  }

  if (!global.CONTEXT) {
    global.CONTEXT = {};
  }

  global.CONTEXT[key] = value;
};

let get = () => global.CONTEXT || {};

module.exports = {
  clearAll: clearAll,
  replaceAllWith: replaceAllWith,
  set: set,
  get: get
};

================================================
FILE: lib/http.js
================================================
'use strict';

const correlationIds = require('./correlation-ids');
const http = require('superagent-promise')(require('superagent'), Promise);

function getRequest (options) {
  let uri    = options.uri;
  let method = options.method || '';

  switch (method.toLowerCase()) {
    case '':
    case 'get':
      return http.get(uri);
    case 'head':
      return http.head(uri);
    case 'post':
      return http.post(uri);
    case 'put':
      return http.put(uri);
    case 'delete':
      return http.del(uri);
    default:
      throw new Error(`unsupported method : ${method.toLowerCase()}`);
  }
}

function setHeaders (request, headers) {
  let headerNames = Object.keys(headers);
  headerNames.forEach(h => request = request.set(h, headers[h]));

  return request;
}

function setQueryStrings (request, qs) {
  if (!qs) {
    return request;
  }
  
  return request.query(qs); 
}

function setBody (request, body) {
  if (!body) {
    return request;
  }

  return request.send(body);
}

// options: { 
//    uri     : string
//    method  : GET (default) | POST | PUT | HEAD
//    headers : object
//    qs      : object
//    body    : object
//  }
let Req = (options) => {
  if (!options) {
    throw new Error('no HTTP request options is provided');
  }

  if (!options.uri) {
    throw new Error('no HTTP uri is specified');
  }

  const context = correlationIds.get();

  // copy the provided headers last so it overrides the values from the context
  let headers = Object.assign({}, context, options.headers);

  let request = getRequest(options);

  request = setHeaders(request, headers);
  request = setQueryStrings(request, options.qs);
  request = setBody(request, options.body);

  return request
    .catch(e => {
      if (e.response && e.response.error) {
        throw e.response.error;
      }
      
      throw e;
    });
};

module.exports = Req;

================================================
FILE: lib/kinesis.js
================================================
'use strict';

const _              = require('lodash');
const AWSXRay        = require('aws-xray-sdk');
const AWS            = AWSXRay.captureAWS(require('aws-sdk'));
const Kinesis        = new AWS.Kinesis();
const log            = require('./log');
const correlationIds = require('./correlation-ids');

function tryJsonParse(data) {
  if (!_.isString(data)) {
    return null;
  }

  try {
    return JSON.parse(data);
  } catch (err) {
    log.warn('only JSON string data can be modified to insert correlation IDs');
    return null;
  }
}

function addCorrelationIds(data) {
  // only do with with JSON string data
  const payload = tryJsonParse(data);
  if (!payload) {
    return data;
  }

  const context = correlationIds.get();  
  const newData = Object.assign({ __context__: context }, payload);
  return JSON.stringify(newData);
}

function putRecord(params, cb) {
  const newData = addCorrelationIds(params.Data);
  params = Object.assign({}, params, { Data: newData });

  return Kinesis.putRecord(params, cb);
};

const client = Object.assign({}, Kinesis, { putRecord });

module.exports = client;

================================================
FILE: lib/log.js
================================================
'use strict';

const correlationIds = require('./correlation-ids');

const LogLevels = {
  DEBUG : 0,
  INFO  : 1,
  WARN  : 2,
  ERROR : 3
};

// most of these are available through the Node.js execution environment for Lambda
// see https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html
const DEFAULT_CONTEXT = {
  awsRegion: process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION,
  functionName: process.env.AWS_LAMBDA_FUNCTION_NAME,
  functionVersion: process.env.AWS_LAMBDA_FUNCTION_VERSION,
  functionMemorySize: process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE,
  stage: process.env.ENVIRONMENT || process.env.STAGE
};

function getContext () {
  // if there's a global variable for all the current request context then use it
  const context = correlationIds.get();
  if (context) {
    // note: this is a shallow copy, which is ok as we're not going to mutate anything
    return Object.assign({}, DEFAULT_CONTEXT, context);
  }

  return DEFAULT_CONTEXT;
}

// default to debug if not specified
function logLevelName() {
  return process.env.log_level || 'DEBUG';
}

function isEnabled (level) {
  return level >= LogLevels[logLevelName()];
}

function appendError(params, err) {
  if (!err) {
    return params;
  }

  return Object.assign(
    {},
    params || {}, 
    { errorName: err.name, errorMessage: err.message, stackTrace: err.stack }
  );
}

function log (levelName, message, params) {
  if (!isEnabled(LogLevels[levelName])) {
    return;
  }

  let context = getContext();
  let logMsg = Object.assign({}, context, params);
  logMsg.level = levelName;
  logMsg.message = message;

  console.log(JSON.stringify(logMsg));
}

module.exports.debug = (msg, params) => log('DEBUG', msg, params);
module.exports.info  = (msg, params) => log('INFO',  msg, params);
module.exports.warn  = (msg, params, error) => log('WARN',  msg, appendError(params, error));
module.exports.error = (msg, params, error) => log('ERROR', msg, appendError(params, error));

module.exports.enableDebug = () => {
  const oldLevel = process.env.log_level;
  process.env.log_level = 'DEBUG';

  // return a function to perform the rollback
  return () => {
    process.env.log_level = oldLevel;
  }
};

================================================
FILE: lib/lru.js
================================================
module.exports = function(size) {
  return new LruCache(size)
}

function LruCache(size) {
  this.capacity = size | 0
  this.map = Object.create(null)
  this.list = new DoublyLinkedList()
}

LruCache.prototype.get = function(key) {
  var node = this.map[key]
  if (node == null) return undefined
  this.used(node)
  return node.val
}

LruCache.prototype.set = function(key, val) {
  var node = this.map[key]
  if (node != null) {
    node.val = val
  } else {
    if (!this.capacity) this.prune()
    if (!this.capacity) return false
    node = new DoublyLinkedNode(key, val)
    this.map[key] = node
    this.capacity--
  }
  this.used(node)
  return true
}

LruCache.prototype.used = function(node) {
  this.list.moveToFront(node)
}

LruCache.prototype.prune = function() {
  var node = this.list.pop()
  if (node != null) {
    delete this.map[node.key]
    this.capacity++
  }
}


function DoublyLinkedList() {
  this.firstNode = null
  this.lastNode = null
}

DoublyLinkedList.prototype.moveToFront = function(node) {
  if (this.firstNode == node) return

  this.remove(node)

  if (this.firstNode == null) {
    this.firstNode = node
    this.lastNode = node
    node.prev = null
    node.next = null
  } else {
    node.prev = null
    node.next = this.firstNode
    node.next.prev = node
    this.firstNode = node
  }
}

DoublyLinkedList.prototype.pop = function() {
  var lastNode = this.lastNode
  if (lastNode != null) {
    this.remove(lastNode)
  }
  return lastNode
}

DoublyLinkedList.prototype.remove = function(node) {
  if (this.firstNode == node) {
    this.firstNode = node.next
  } else if (node.prev != null) {
    node.prev.next = node.next
  }
  if (this.lastNode == node) {
    this.lastNode = node.prev
  } else if (node.next != null) {
    node.next.prev = node.prev
  }
}


function DoublyLinkedNode(key, val) {
  this.key = key
  this.val = val
  this.prev = null
  this.next = null
}

================================================
FILE: lib/notify.js
================================================
'use strict';

const _          = require('lodash');
const co         = require('co');
const sns        = require('./sns');
const kinesis    = require('./kinesis');
const chance     = require('chance').Chance();
const log        = require('./log');
const cloudwatch = require('./cloudwatch');

const streamName         = process.env.order_events_stream;
const restaurantTopicArn = process.env.restaurant_notification_topic;
const userTopicArn       = process.env.user_notification_topic;

let notifyRestaurantOfOrder = co.wrap(function* (order) {
  try {
    if (chance.bool({likelihood: 75})) { // 75% chance of failure
      throw new Error("boom");
    }

    let snsReq = {
      Message: JSON.stringify(order),
      TopicArn: restaurantTopicArn
    };
    yield cloudwatch.trackExecTime(
      "SnsPublishLatency",
      () => sns.publish(snsReq).promise()
    );

    let logContext = {
      orderId: order.orderId,
      restaurantName: order.restaurantName,
      userEmail: order.userEmail
    };
    log.debug('notified restaurant of new order', logContext);

    let data = _.clone(order);
    data.eventType = 'restaurant_notified';

    let kinesisReq = {
      Data: JSON.stringify(data), // the SDK would base64 encode this for us
      PartitionKey: order.orderId,
      StreamName: streamName
    };
    yield cloudwatch.trackExecTime(
      "KinesisPutRecordLatency",
      () => kinesis.putRecord(kinesisReq).promise()
    );
    log.debug('published event into Kinesis', { eventName: 'restaurant_notified' });

    cloudwatch.incrCount('NotifyRestaurantSuccess');
  } catch (err) {
    cloudwatch.incrCount('NotifyRestaurantFailed');
    throw err;
  }
});

let notifyUserOfOrderAccepted = co.wrap(function* (order) {
  try {
    if (chance.bool({likelihood: 75})) { // 75% chance of failure
      throw new Error("boom");
    }
  
    let snsReq = {
      Message: JSON.stringify(order),
      TopicArn: userTopicArn
    };
    yield cloudwatch.trackExecTime(
      "SnsPublishLatency", 
      () => sns.publish(snsReq).promise()
    );
  
    let logContext = {
      orderId: order.orderId,
      restaurantName: order.restaurantName,
      userEmail: order.userEmail
    };
    log.debug('notified user of accepted order', logContext);
  
    let data = _.clone(order);
    data.eventType = 'user_notified';
  
    let kinesisReq = {
      Data: JSON.stringify(data), // the SDK would base64 encode this for us
      PartitionKey: order.orderId,
      StreamName: streamName
    };
    yield cloudwatch.trackExecTime(
      "KinesisPutRecordLatency",
      () => kinesis.putRecord(kinesisReq).promise()
    );
    log.debug(`published event into Kinesis`, { eventName: 'user_notified' });
  
    cloudwatch.incrCount('NotifyUserSuccess');
  } catch (err) {
    cloudwatch.incrCount('NotifyUserFailed');
    throw err;
  }
});

module.exports = {
  restaurantOfOrder: notifyRestaurantOfOrder,
  userOfOrderAccepted: notifyUserOfOrderAccepted
};

================================================
FILE: lib/retry.js
================================================
'use strict';

const co         = require('co');
const sns        = require('./sns');
const log        = require('./log');
const cloudwatch = require('./cloudwatch');

const restaurantRetryTopicArn = process.env.restaurant_notification_retry_topic;
const userRetryTopicArn       = process.env.user_notification_retry_topic;

let retryRestaurantNotification = co.wrap(function* (order) {
  let snsReq = {
    Message: JSON.stringify(order),
    TopicArn: restaurantRetryTopicArn
  };
  yield cloudwatch.trackExecTime(
    "SnsPublishLatency",
    () => sns.publish(snsReq).promise()
  );

  let logContext = {
    orderId: order.orderId,
    restaurantName: order.restaurantName,
    userEmail: order.userEmail
  };
  log.debug('queued restaurant notification for retry', logContext);

  cloudwatch.incrCount("NotifyRestaurantQueued");
});

let retryUserNotification = co.wrap(function* (order) {
  let snsReq = {
    Message: JSON.stringify(order),
    TopicArn: userRetryTopicArn
  };
  yield cloudwatch.trackExecTime(
    "SnsPublishLatency",
    () => sns.publish(snsReq).promise()
  );

  let logContext = {
    orderId: order.orderId,
    restaurantName: order.restaurantName,
    userEmail: order.userEmail
  };
  log.debug('queued user notification for retry', logContext);

  cloudwatch.incrCount("NotifyUserQueued");
});

module.exports = {
  restaurantNotification: retryRestaurantNotification,
  userNotification: retryUserNotification
};

================================================
FILE: lib/sns.js
================================================
'use strict';

const AWSXRay        = require('aws-xray-sdk');
const AWS            = AWSXRay.captureAWS(require('aws-sdk'));
const SNS            = new AWS.SNS();
const correlationIds = require('./correlation-ids');

function addCorrelationIds(messageAttributes) {
  let attributes = {};
  let context = correlationIds.get();
  for (let key in context) {
    attributes[key] = {
      DataType: 'String',
      StringValue: context[key]
    };
  }

  // use `attribtues` as base so if the user's message attributes would override
  // our correlation IDs
  return Object.assign(attributes, messageAttributes || {});
}

function publish(params, cb) {
  const newMessageAttributes = addCorrelationIds(params.MessageAttributes);
  params = Object.assign(params, { MessageAttributes: newMessageAttributes });

  return SNS.publish(params, cb);
};

const client = Object.assign({}, SNS, { publish });

module.exports = client;

================================================
FILE: middleware/capture-correlation-ids.js
================================================
'use strict';

const correlationIds = require('../lib/correlation-ids');
const log = require('../lib/log');

function captureHttp(headers, awsRequestId, sampleDebugLogRate) {
  if (!headers) {
   log.warn(`Request ${awsRequestId} is missing headers`);
   return;
  }

  let context = { awsRequestId };
  for (const header in headers) {
    if (header.toLowerCase().startsWith('x-correlation-')) {
      context[header] = headers[header];
    }
  }
 
  if (!context['x-correlation-id']) {
    context['x-correlation-id'] = awsRequestId;
  }

  // forward the original User-Agent on
  if (headers['User-Agent']) {
    context['User-Agent'] = headers['User-Agent'];
  }

  if (headers['Debug-Log-Enabled']) {
    context['Debug-Log-Enabled'] = headers['Debug-Log-Enabled'];
  } else {
    context['Debug-Log-Enabled'] = Math.random() < sampleDebugLogRate ? 'true' : 'false';
  }

  correlationIds.replaceAllWith(context);
}

function parsePayload (record) {
  let json = new Buffer(record.kinesis.data, 'base64').toString('utf8');
  return JSON.parse(json);  
}

function captureKinesis(event, context, sampleDebugLogRate) {
  const awsRequestId = context.awsRequestId;
  const events = event
    .Records
    .map(parsePayload)
    .map(record => {
      // the wrapped kinesis client would put the correlation IDs as part of 
      // the payload as a special __context__ property
      let recordContext = record.__context__ || {};
      recordContext.awsRequestId = awsRequestId;

      delete record.__context__;

      if (!recordContext['x-correlation-id']) {
        recordContext['x-correlation-id'] = awsRequestId;
      }

      if (!recordContext['Debug-Log-Enabled']) {
        recordContext['Debug-Log-Enabled'] = Math.random() < sampleDebugLogRate ? 'true' : 'false';
      }

      let debugLog = recordContext['Debug-Log-Enabled'] === 'true';

      let oldContext = undefined;
      let debugLogRollback = undefined;

      // lets you add more correlation IDs for just this record
      record.addToScope = (key, value) => {
        if (!key.startsWith("x-correlation-")) {
          key = "x-correlation-" + key;
        }

        recordContext[key] = value;
        correlationIds.set(key, value);
      }

      record.scopeToThis = () => {
        if (!oldContext) {
          oldContext = correlationIds.get();
          correlationIds.replaceAllWith(recordContext);
        }

        if (debugLog) {
          debugLogRollback = log.enableDebug();
        }
      };

      record.unscope = () => {
        if (oldContext) {
          correlationIds.replaceAllWith(oldContext);
        }

        if (debugLogRollback) {
          debugLogRollback();
        }
      }

      return record;
    });

  context.parsedKinesisEvents = events;

  correlationIds.replaceAllWith({ awsRequestId });
}

function captureSns(records, awsRequestId, sampleDebugLogRate) {
  let context = { awsRequestId };

  const snsRecord = records[0].Sns;
  const msgAttributes = snsRecord.MessageAttributes;
  
  for (var msgAttribute in msgAttributes) {
    if (msgAttribute.toLowerCase().startsWith('x-correlation-')) {
      context[msgAttribute] = msgAttributes[msgAttribute].Value;
    }

    if (msgAttribute === 'User-Agent') {
      context['User-Agent'] = msgAttributes['User-Agent'].Value;
    }

    if (msgAttribute === 'Debug-Log-Enabled') {
      context['Debug-Log-Enabled'] = msgAttributes['Debug-Log-Enabled'].Value;
    }
  }
 
  if (!context['x-correlation-id']) {
    context['x-correlation-id'] = awsRequestId;
  }

  if (!context['Debug-Log-Enabled']) {
    context['Debug-Log-Enabled'] = Math.random() < sampleDebugLogRate ? 'true' : 'false';
  }

  correlationIds.replaceAllWith(context);
}

function isApiGatewayEvent(event) {
  return event.hasOwnProperty('httpMethod')
}

function isKinesisEvent(event) {
  if (!event.hasOwnProperty('Records')) {
    return false;
  }
  
  if (!Array.isArray(event.Records)) {
    return false;
  }

  return event.Records[0].eventSource === 'aws:kinesis';
}

function isSnsEvent(event) {
  if (!event.hasOwnProperty('Records')) {
    return false;
  }
  
  if (!Array.isArray(event.Records)) {
    return false;
  }

  return event.Records[0].EventSource === 'aws:sns';
}

module.exports = (config) => {
  const sampleDebugLogRate = config.sampleDebugLogRate || 0.01;

  return {
    before: (handler, next) => {      
      correlationIds.clearAll();

      if (isApiGatewayEvent(handler.event)) {
        captureHttp(handler.event.headers, handler.context.awsRequestId, sampleDebugLogRate);
      } else if (isKinesisEvent(handler.event)) {
        captureKinesis(handler.event, handler.context, sampleDebugLogRate);
      } else if (isSnsEvent(handler.event)) {
        captureSns(handler.event.Records, handler.context.awsRequestId, sampleDebugLogRate);
      }

      next()
    }
  };
};

================================================
FILE: middleware/flush-metrics.js
================================================
'use strict';

const log        = require('../lib/log');
const cloudwatch = require('../lib/cloudwatch');

module.exports = {
  after: (handler, next) => {
    cloudwatch.flush().then(_ => next());
  },
  onError: (handler, next) => {
    cloudwatch.flush().then(_ => next(handler.error));
  }
};

================================================
FILE: middleware/function-shield.js
================================================
'use strict';

const FuncShield = require('@puresec/function-shield');

module.exports = () => {
  return {
    before: (handler, next) => {
      FuncShield.configure({
        policy: {
          outbound_connectivity: "block",
          read_write_tmp: "block", 
          create_child_process: "block"
        },
        token: process.env.FUNCTION_SHIELD_TOKEN
      });

      next();
    }
  };
};

================================================
FILE: middleware/sample-logging.js
================================================
'use strict';

const correlationIds = require('../lib/correlation-ids');
const log = require('../lib/log');

// config should be { sampleRate: double } where sampleRate is between 0.0-1.0
module.exports = (config) => {
  let rollback = undefined;

  const isDebugEnabled = () => {
    const context = correlationIds.get();
    if (context['Debug-Log-Enabled'] === 'true') {
      return true;
    }

    return config.sampleRate && Math.random() <= config.sampleRate;
  }

  return {
    before: (handler, next) => {
      if (isDebugEnabled()) {
        rollback = log.enableDebug();
      }

      next();
    },
    after: (handler, next) => {
      if (rollback) {
        rollback();
      }

      next();
    },
    onError: (handler, next) => {
      let awsRequestId = handler.context.awsRequestId;
      let invocationEvent = JSON.stringify(handler.event);
      log.error('invocation failed', { awsRequestId, invocationEvent }, handler.error);

      next(handler.error);
    }
  };
};

================================================
FILE: middleware/wrapper.js
================================================
'use strict';

const middy = require('middy');
const sampleLogging = require('./sample-logging');
const captureCorrelationIds = require('./capture-correlation-ids');
const functionShield = require('./function-shield');

module.exports = f => {
  return middy(f)
    .use(captureCorrelationIds({ sampleDebugLogRate: 0.01 }))
    .use(sampleLogging({ sampleRate: 0.01 }))
    .use(functionShield());
};

================================================
FILE: package.json
================================================
{
    "name": "big-mouth",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "integration-test": "env TEST_MODE=handler ./node_modules/.bin/mocha tests/test_cases --reporter spec --retries 3 --timeout 10000",
        "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"
    },
    "author": "",
    "license": "ISC",
    "dependencies": {
        "@puresec/function-shield": "^1.0.7",
        "aws-xray-sdk": "^1.2.0",
        "bluebird": "^3.5.1",
        "chance": "^1.0.13",
        "co": "^4.6.0",
        "middy": "^0.17.1",
        "mustache": "^2.3.0",
        "superagent": "^3.8.1",
        "superagent-promise": "^1.1.0"
    },
    "devDependencies": {
        "aws-sdk": "^2.302.0",
        "chai": "^4.1.2",
        "cheerio": "^1.0.0-rc.2",
        "lodash": "^4.17.10",
        "mocha": "^4.0.1",
        "serverless": "^1.30.1",
        "serverless-iam-roles-per-function": "^0.1.5",
        "serverless-plugin-aws-alerts": "^1.2.4",
        "serverless-plugin-canary-deployments": "^0.4.3",
        "serverless-plugin-tracing": "^2.0.0",
        "serverless-pseudo-parameters": "^1.2.5",
        "serverless-sam": "0.0.3"
    }
}


================================================
FILE: seed-restaurants.js
================================================
'use strict';

const co = require('co');
const AWS = require('aws-sdk');
AWS.config.region = 'us-east-1';
const dynamodb = new AWS.DynamoDB.DocumentClient();

let restaurants = [
  { 
    name: "Fangtasia", 
    image: "https://d2qt42rcwzspd6.cloudfront.net/manning/fangtasia.png", 
    themes: ["true blood"] 
  },
  { 
    name: "Shoney's", 
    image: "https://d2qt42rcwzspd6.cloudfront.net/manning/shoney's.png", 
    themes: ["cartoon", "rick and morty"] 
  },
  { 
    name: "Freddy's BBQ Joint", 
    image: "https://d2qt42rcwzspd6.cloudfront.net/manning/freddy's+bbq+joint.png", 
    themes: ["netflix", "house of cards"] 
  },
  { 
    name: "Pizza Planet", 
    image: "https://d2qt42rcwzspd6.cloudfront.net/manning/pizza+planet.png", 
    themes: ["netflix", "toy story"] 
  },
  { 
    name: "Leaky Cauldron", 
    image: "https://d2qt42rcwzspd6.cloudfront.net/manning/leaky+cauldron.png", 
    themes: ["movie", "harry potter"] 
  },
  { 
    name: "Lil' Bits", 
    image: "https://d2qt42rcwzspd6.cloudfront.net/manning/lil+bits.png", 
    themes: ["cartoon", "rick and morty"] 
  },
  { 
    name: "Fancy Eats", 
    image: "https://d2qt42rcwzspd6.cloudfront.net/manning/fancy+eats.png", 
    themes: ["cartoon", "rick and morty"] 
  },
  { 
    name: "Don Cuco", 
    image: "https://d2qt42rcwzspd6.cloudfront.net/manning/don%20cuco.png", 
    themes: ["cartoon", "rick and morty"] 
  },
];

let putReqs = restaurants.map(x => ({
  PutRequest: {
    Item: x
  }
}));

let req = {
  RequestItems: {
    'restaurants': putReqs
  }
};
dynamodb.batchWrite(req).promise().then(() => console.log("all done"));

================================================
FILE: serverless.yml
================================================
service: big-mouth

plugins:
  - serverless-pseudo-parameters
  - serverless-sam
  - serverless-iam-roles-per-function
  - serverless-plugin-tracing
  - serverless-plugin-canary-deployments
  - serverless-plugin-aws-alerts

custom: 
  stage: ${opt:stage, self:provider.stage}
  region: ${opt:region}
  logLevel:
    prod: WARN
    default: DEBUG
  serverless-iam-roles-per-function:
    defaultInherit: true
  alerts:
    stages:
      - dev
      - staging
      - production
    dashboards: false
    alarms:
      - functionThrottles
      - functionErrors

provider:
  name: aws
  runtime: nodejs6.10
  tracing: true
  environment:
    log_level: ${self:custom.logLevel.${self:custom.stage}, self:custom.logLevel.default}
    STAGE: ${self:custom.stage}
    FUNCTION_SHIELD_TOKEN: ${ssm:/bigmouth/${self:custom.stage}/function_shield_token~true}
  iamRoleStatements:
    - Effect: Allow
      Action: cloudwatch:PutMetricData
      Resource: '*'
    - Effect: Allow
      Action:
        - 'xray:PutTraceSegments'
        - 'xray:PutTelemetryRecords'
      Resource: '*'
    - Effect: Allow
      Action: codedeploy:*
      Resource: '*'

functions:
  get-index:
    handler: functions/get-index.handler
    events:
      - http:
          path: /
          method: get
    environment:      
      async_metrics: true
    deploymentSettings:
      type: Linear10PercentEvery1Minute
      alias: Live
      alarms:
        - GetDashindexFunctionErrorsAlarm
    #iamRoleStatementsInherit: true #optionally inherit shared permissions
    iamRoleStatements:
      - Effect: Allow
        Action: execute-api:Invoke
        Resource: arn:aws:execute-api:#{AWS::Region}:#{AWS::AccountId}:*/*/GET/restaurants
      - Effect: Allow
        Action: ssm:GetParameters*
        Resource: arn:aws:ssm:#{AWS::Region}:#{AWS::AccountId}:parameter/bigmouth/${self:custom.stage}/*
      - Effect: Allow
        Action: secretsmanager:GetSecretValue
        Resource: arn:aws:secretsmanager:#{AWS::Region}:#{AWS::AccountId}:secret:/bigmouth/${self:custom.stage}/*

  get-restaurants:
    handler: functions/get-restaurants.handler
    events:
      - http:
          path: /restaurants/
          method: get
          authorizer: aws_iam
    environment:
      restaurants_table: restaurants
      async_metrics: true
    iamRoleStatements:
      - Effect: Allow
        Action: dynamodb:scan
        Resource: arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/restaurants

  search-restaurants:
    handler: functions/search-restaurants.handler
    events:
      - http:
          path: /restaurants/search
          method: post
          authorizer:
            arn: arn:aws:cognito-idp:#{AWS::Region}:#{AWS::AccountId}:userpool/us-east-1_DfuAwa0vB
    environment:
      restaurants_table: restaurants
      async_metrics: true
    iamRoleStatements:
      - Effect: Allow
        Action: dynamodb:scan
        Resource: arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/restaurants

  place-order:
    handler: functions/place-order.handler
    events:
      - http:
          path: /orders
          method: post
          authorizer:
            arn: arn:aws:cognito-idp:#{AWS::Region}:#{AWS::AccountId}:userpool/us-east-1_DfuAwa0vB
    environment:
      order_events_stream: order-events
      async_metrics: true
    iamRoleStatements:
      - Effect: Allow
        Action: kinesis:PutRecord
        Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events

  notify-restaurant:
    handler: functions/notify-restaurant.handler
    events:
      - stream:
          arn: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events
    environment:
      order_events_stream: order-events
      restaurant_notification_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification
      restaurant_notification_retry_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification-retry
    iamRoleStatements:
      - Effect: Allow
        Action: kinesis:PutRecord
        Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events
      - Effect: Allow
        Action: sns:Publish
        Resource:
          - arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification
          - arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification-retry
      
  retry-notify-restaurant:
    handler: functions/retry-notify-restaurant.handler
    events:
      - sns: restaurant-notification-retry
    environment:
      order_events_stream: order-events
      restaurant_notification_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification
    onError: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification-dlq
    iamRoleStatements:
      - Effect: Allow
        Action: kinesis:PutRecord
        Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events
      - Effect: Allow
        Action: sns:Publish
        Resource:
          - arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:restaurant-notification
    
  accept-order:
    handler: functions/accept-order.handler
    events:
      - http:
          path: /orders/accept
          method: post
    environment:
      order_events_stream: order-events
      async_metrics: true
    iamRoleStatements:
      - Effect: Allow
        Action: kinesis:PutRecord
        Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events

  notify-user:
    handler: functions/notify-user.handler
    events:
      - stream:
          arn: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events
    environment:
      order_events_stream: order-events
      user_notification_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification
      user_notification_retry_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification-retry
    iamRoleStatements:
      - Effect: Allow
        Action: kinesis:PutRecord
        Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events
      - Effect: Allow
        Action: sns:Publish
        Resource:
          - arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification
          - arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification-retry

  retry-notify-user:
    handler: functions/retry-notify-user.handler
    events:
      - sns: user-notification-retry
    environment:
      order_events_stream: order-events
      user_notification_topic: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification
    onError: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification-dlq
    iamRoleStatements:
      - Effect: Allow
        Action: kinesis:PutRecord
        Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events
      - Effect: Allow
        Action: sns:Publish
        Resource:
          - arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:user-notification

  fulfill-order:
    handler: functions/fulfill-order.handler
    events:
      - http:
          path: /orders/complete
          method: post
    environment:
      order_events_stream: order-events
      async_metrics: true
    iamRoleStatements:
      - Effect: Allow
        Action: kinesis:PutRecord
        Resource: arn:aws:kinesis:#{AWS::Region}:#{AWS::AccountId}:stream/order-events

  auto-create-api-alarms:
    handler: functions/create-alarms.handler  
    events:
      - cloudwatchEvent:
          event:
            source:
              - aws.apigateway
            detail-type:
              - AWS API Call via CloudTrail
            detail:
              eventSource:
                - apigateway.amazonaws.com
              eventName:
                - CreateDeployment
    environment:
      alarm_actions: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:NotifyMe
      ok_actions: arn:aws:sns:#{AWS::Region}:#{AWS::AccountId}:NotifyMe
    iamRoleStatements:
      - Effect: Allow
        Action: apigateway:GET
        Resource: 
          - arn:aws:apigateway:#{AWS::Region}::/restapis/*
          - arn:aws:apigateway:#{AWS::Region}::/restapis/*/stages/${self:custom.stage}
      - Effect: Allow
        Action: apigateway:PATCH
        Resource: arn:aws:apigateway:#{AWS::Region}::/restapis/*/stages/${self:custom.stage}
      - Effect: Allow
        Action: cloudwatch:PutMetricAlarm
        Resource: "*"

resources:
  Resources:
    restaurantsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: restaurants
        AttributeDefinitions:
          - AttributeName: name
            AttributeType: S
        KeySchema:
          - AttributeName: name
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

    orderEventsStream:
      Type: AWS::Kinesis::Stream
      Properties: 
        Name: order-events
        ShardCount: 1

    restaurantNotificationTopic:
      Type: AWS::SNS::Topic
      Properties: 
        DisplayName: restaurant-notification
        TopicName: restaurant-notification

    userNotificationTopic:
      Type: AWS::SNS::Topic
      Properties: 
        DisplayName: user-notification
        TopicName: user-notification

    restaurantNotificationDLQTopic:
      Type: AWS::SNS::Topic
      Properties: 
        DisplayName: restaurant-notification-dlq
        TopicName: restaurant-notification-dlq

    userNotificationDLQTopic:
      Type: AWS::SNS::Topic
      Properties: 
        DisplayName: user-notification-dlq
        TopicName: user-notification-dlq

================================================
FILE: static/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Big Mouth</title>

    <script src="https://sdk.amazonaws.com/js/aws-sdk-2.149.0.min.js"></script>
    <script src="https://d2qt42rcwzspd6.cloudfront.net/manning/aws-cognito-sdk.min.js"></script>
    <script src="https://d2qt42rcwzspd6.cloudfront.net/manning/amazon-cognito-identity.min.js"></script>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js" 
            integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
            crossorigin="anonymous"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js" 
            integrity="sha384-Dziy8F2VlJQLMShA6FHWNul/veM9bCkRUaLqr199K94ntO5QUrLJBEbYegdSkkqX" 
            crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">

    <style>
      .fullscreenDiv {
        background-color: #05bafd;
        width: 100%;
        height: auto;
        bottom: 0px;
        top: 0px;
        left: 0;
        position: absolute;        
      }
      .restaurantsDiv {
        background-color: #ffffff;
        width: 100%;
        height: auto;
      }
      .dayOfWeek {
        font-family: Arial, Helvetica, sans-serif;
        font-size: 32px;
        padding: 10px;
        height: auto;
        display: flex;
        justify-content: center;
      }
      .column-container {
        padding: 0;
        margin: 0;        
        list-style: none;
        display: flex;
        flex-flow: column;
        flex-wrap: wrap;
        justify-content: center;
      }
      .row-container {
        padding: 5px;
        margin: 5px;
        list-style: none;
        display: flex;
        flex-flow: row;
        flex-wrap: wrap;
        justify-content: center;
      }
      .item {
        padding: 5px;
        height: auto;
        margin-top: 10px;
        display: flex;
        flex-flow: row;
        flex-wrap: wrap;
        justify-content: center;
      }
      .restaurant {
        background-color: #00a8f7;
        border-radius: 10px;
        padding: 5px;
        height: auto;
        width: auto;
        margin-left: 40px;
        margin-right: 40px;
        margin-top: 15px;
        margin-bottom: 0px;
        display: flex;
        justify-content: center;
      }
      .restaurant-name {
        font-size: 24px;
        font-family:Arial, Helvetica, sans-serif;
        color: #ffffff;
        padding: 10px;
        margin: 0px;
      }
      .restaurant-image {
        padding-top: 0px;
        margin-top: 0px;
      }
      .row-container-left {
        list-style: none;
        display: flex;
        flex-flow: row;
        justify-content: flex-start;
      }
      .menu-text {
        font-family: Arial, Helvetica, sans-serif;
        font-size: 24px;
        font-weight: bold;
        color: white;
      }
      .text-trail-space {
        margin-right: 10px;
      }
      .hidden {
        display: none;
      }

      lable, button, input {
        display:block;
        font-family: Arial, Helvetica, sans-serif;
        font-size: 18px;
      }
      
      fieldset { 
        padding:0; 
        border:0; 
        margin-top:25px; 
      }

    </style>

    <script>
      const AWS_REGION = '{{awsRegion}}';
      const COGNITO_USER_POOL_ID = '{{cognitoUserPoolId}}';
      const CLIENT_ID = '{{cognitoClientId}}';
      const SEARCH_URL = '{{& searchUrl}}';
      const PLACE_ORDER_URL = '{{& placeOrderUrl}}';

      var regDialog, regForm;
      var verifyDialog;
      var regCompleteDialog;
      var signInDialog;
      var userPool, cognitoUser;
      var idToken;

      function toggleSignOut (enable) {
        enable === true ? $('#sign-out').show() : $('#sign-out').hide();
      }

      function toggleSignIn (enable) {
        enable === true ? $('#sign-in').show() : $('#sign-in').hide();
      }

      function toggleRegister (enable) {
        enable === true ? $('#register').show() : $('#register').hide();
      }

      function init() {
        AWS.config.region = AWS_REGION;
        AWSCognito.config.region = AWS_REGION;

        var data = { 
          UserPoolId : COGNITO_USER_POOL_ID, 
          ClientId : CLIENT_ID
        };
        userPool = new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(data);
        cognitoUser = userPool.getCurrentUser();

        if (cognitoUser != null) {          
          cognitoUser.getSession(function(err, session) {
            if (err) {
                alert(err);
                return;
            }

            idToken = session.idToken.jwtToken;
            console.log('idToken: ' + idToken);
            console.log('session validity: ' + session.isValid());
          });

          toggleSignOut(true);
          toggleSignIn(false);
          toggleRegister(false);
        } else {
          toggleSignOut(false);
          toggleSignIn(true);
          toggleRegister(true);
        }
      }

      function addUser() {
        var firstName = $("#first-name")[0].value;
        var lastName = $("#last-name")[0].value;
        var username = $("#username")[0].value;
        var password = $("#password")[0].value;
        var email = $("#email")[0].value;

        var attributeList = [
          new AWSCognito.CognitoIdentityServiceProvider.CognitoUserAttribute({ 
            Name : 'email', Value : email
          }),
          new AWSCognito.CognitoIdentityServiceProvider.CognitoUserAttribute({ 
            Name : 'given_name', Value : firstName
          }),
          new AWSCognito.CognitoIdentityServiceProvider.CognitoUserAttribute({ 
            Name : 'family_name', Value : lastName
          }),
        ];

        userPool.signUp(username, password, attributeList, null, function(err, result){
          if (err) {
            alert(err);
            return;
          }
          cognitoUser = result.user;
          console.log('user name is ' + cognitoUser.getUsername());

          regDialog.dialog("close");
          verifyDialog.dialog("open");
        });
      }

      function confirmUser() {
        var verificationCode = $("#verification-code")[0].value;
        cognitoUser.confirmRegistration(verificationCode, true, function(err, result) {
          if (err) {
            alert(err);
            return;
          }
          console.log('verification call result: ' + result);

          verifyDialog.dialog("close");
          regCompleteDialog.dialog("open");
        });
      }

      function authenticateUser() {
        var username = $("#sign-in-username")[0].value;
        var password = $("#sign-in-password")[0].value;

        var authenticationData = {
          Username : username,
          Password : password,
        };
        var authenticationDetails = new AWSCognito.CognitoIdentityServiceProvider.AuthenticationDetails(authenticationData);
        var userData = {
          Username : username,
          Pool : userPool
        };
        var cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);

        cognitoUser.authenticateUser(authenticationDetails, {
          onSuccess: function (result) {
            console.log('access token : ' + result.getAccessToken().getJwtToken());
            /*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*/
            idToken = result.idToken.jwtToken;
            console.log('idToken : ' + idToken);

            signInDialog.dialog("close");
            toggleRegister(false);
            toggleSignIn(false);
            toggleSignOut(true);
          },

          onFailure: function(err) {
            alert(err);
          }
        });
      }

      function signOut() {
        if (cognitoUser != null) {
          cognitoUser.signOut();
          toggleRegister(true);
          toggleSignIn(true);
          toggleSignOut(false);
        }
      }

      function searchRestaurants() {
        var theme = $("#theme")[0].value;

        var xhr = new XMLHttpRequest();
        xhr.open('POST', SEARCH_URL, true);
        xhr.setRequestHeader("Content-Type", "application/json");
        xhr.setRequestHeader("Authorization", idToken);
        xhr.send(JSON.stringify({ theme }));
        
        xhr.onreadystatechange = function (e) {
          if (xhr.readyState === 4 && xhr.status === 200) {
            var restaurants = JSON.parse(xhr.responseText);
            var restaurantsList = $("#restaurantsUl");
            restaurantsList.empty();

            for (var restaurant of restaurants) {
              restaurantsList.append(`
              <li class="restaurant">
                <ul class="column-container" onclick='placeOrder("${restaurant.name}")'>
                    <li class="item restaurant-name">${restaurant.name}</li>
                    <li class="item restaurant-image">
                      <img src="${restaurant.image}">
                    </li>
                </ul>
              </li>
              `);
            }

          } else if (xhr.readyState === 4) {
            alert(xhr.responseText);
          }
        };
      }

      function placeOrder(restaurantName) {
        var xhr = new XMLHttpRequest();
        xhr.open('POST', PLACE_ORDER_URL, true);
        xhr.setRequestHeader("Content-Type", "application/json");
        xhr.setRequestHeader("Authorization", idToken);
        xhr.send(JSON.stringify({ restaurantName }));

        xhr.onreadystatechange = function (e) {
          if (xhr.readyState === 4 && xhr.status === 200) {
            alert("your order has been placed, we'll let you know once it's been accepted by the restaurant!");
          } else if (xhr.readyState === 4) {
            alert(xhr.responseText);
          }
        };
      }

      $(document).ready(function() {
        regDialog = $("#reg-dialog-form").dialog({
          autoOpen: false,
          modal: true,
          buttons: {
            "Create an account": addUser,
            Cancel: function() {
              regDialog.dialog("close");
            }
          },
          close: function() {
            regForm[0].reset();
          }
        });

        regForm = regDialog.find("form").on("submit", function(event) {
          event.preventDefault();
          addUser();
        });
        
        $("#register").on("click", function() {
          regDialog.dialog("open");
        });

        verifyDialog = $("#verify-dialog-form").dialog({
          autoOpen: false,
          modal: true,
          buttons: {
            "Confirm registration": confirmUser,
            Cancel: function() {
              verifyDialog.dialog("close");
            }
          },
          close: function() {
            $(this).dialog("close");
          }
        });

        regCompleteDialog = $("#registered-message").dialog({
          autoOpen: false,
          modal: true,
          buttons: {
            Ok: function() {
              $(this).dialog("close");
            }
          }
        });

        signInDialog = $("#sign-in-form").dialog({
          autoOpen: false,
          modal: true,
          buttons: {
            "Sign in": authenticateUser,
            Cancel: function() {
              signInDialog.dialog("close");
            }
          },
          close: function() {
            $(this).dialog("close");
          }
        });

        $("#sign-in").on("click", function() {
          signInDialog.dialog("open");
        });

        $("#sign-out").on("click", function() {
          signOut();
        })

        init();
      });

    </script>
  </head>

  <body>
    <div class="fullscreenDiv">
      <ul class="column-container">
        <li>
          <ul class="row-container-left">
            <li id="register" class="item text-trail-space hidden">
              <a class="menu-text" href="#">Register</a>
            </li>
            <li id="sign-in" class="item menu-text text-trail-space hidden">
              <a class="menu-text" href="#">Sign in</a>
            </li>
            <li id="sign-out" class="item menu-text text-trail-space hidden">
              <a class="menu-text" href="#">Sign out</a>
            </li>
          </ul>
        </li>
        <li class="item">
          <img id="logo" src="https://d2qt42rcwzspd6.cloudfront.net/manning/big-mouth.png">
        </li>
        <li class="item">
          <input id="theme" type="text" size="50" placeholder="enter a theme, eg. rick and morty"/>
          <button onclick="searchRestaurants()">Find Restaurants</button>
        </li>
        <li>
          <div class="restaurantsDiv column-container">
            <b class="dayOfWeek">{{dayOfWeek}}</b>
            <ul id="restaurantsUl" class="row-container">
              {{#restaurants}}
              <li class="restaurant">
                <ul class="column-container" onclick='placeOrder("{{name}}")'>
                    <li class="item restaurant-name">{{name}}</li>
                    <li class="item restaurant-image">
                      <img src="{{image}}">
                    </li>
                </ul>
              </li>
              {{/restaurants}}
            </ul>
          </div>
        </li>
      </ul>
    </div>

    <div id="reg-dialog-form" title="Register">       
      <form>
        <fieldset>
          <label for="first-name">First Name</label>
          <input type="text" id="first-name" class="text ui-widget-content ui-corner-all">
          <label for="last-name">Last Name</label>
          <input type="text" id="last-name" class="text ui-widget-content ui-corner-all">
          <label for="email">Email</label>
          <input type="text" name="email" id="email" class="text ui-widget-content ui-corner-all">
          <label for="username">Username</label>
          <input type="text" name="username" id="username" class="text ui-widget-content ui-corner-all">
          <label for="password">Password</label>
          <input type="password" name="password" id="password" class="text ui-widget-content ui-corner-all">
        </fieldset>
      </form>
    </div>

    <div id="verify-dialog-form" title="Verify">
      <form>
        <fieldset>
            <label for="verification-code">Verification Code</label>
            <input type="text" id="verification-code" class="text ui-widget-content ui-corner-all">
        </fieldset>
      </form>
    </div>

    <div id="registered-message" title="Registration complete!">
      <p>
        <span class="ui-icon ui-icon-circle-check" style="float:left; margin:0 7px 50px 0;"></span>
        You are now registered!
      </p>
    </div>

    <div id="sign-in-form" title="Sign in">
      <form>
          <fieldset>            
            <label for="sign-in-username">Username</label>
            <input type="text" id="sign-in-username" class="text ui-widget-content ui-corner-all">
            <label for="sign-in-password">Password</label>
            <input type="password" id="sign-in-password" class="text ui-widget-content ui-corner-all">
          </fieldset>
        </form>
    </div>

  </body>

</html>

================================================
FILE: template.yml
================================================
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: 'SAM template for Serverless framework service: '
Resources:
  restaurantsTable:
    Type: 'AWS::DynamoDB::Table'
    Properties:
      TableName: restaurants
      AttributeDefinitions:
        - AttributeName: name
          AttributeType: S
      KeySchema:
        - AttributeName: name
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1
  GetIndex:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: functions/get-index.handler
      Runtime: nodejs6.10
      CodeUri: >-
        /Users/yancui/SourceCode/Personal/manning-aws-lambda-operational-patterns-and-practices/.serverless/big-mouth.zip
      MemorySize: 128
      Timeout: 3
      Policies:
        - Effect: Allow
          Action: 'dynamodb:scan'
          Resource: 'arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/restaurants'
      Environment:
        Variables:
          restaurants_api: https://i3c5h755j0.execute-api.us-east-1.amazonaws.com/dev/restaurants
          cognito_user_pool_id: us-east-1_DfuAwa0vB
          cognito_client_id: 1tf8pb1s1n53p7u608e7ic5ih8
      Events:
        Event1:
          Type: Api
          Properties:
            Path: /
            Method: get
  GetRestaurants:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: functions/get-restaurants.handler
      Runtime: nodejs6.10
      CodeUri: >-
        /Users/yancui/SourceCode/Personal/manning-aws-lambda-operational-patterns-and-practices/.serverless/big-mouth.zip
      MemorySize: 128
      Timeout: 3
      Policies:
        - Effect: Allow
          Action: 'dynamodb:scan'
          Resource: 'arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/restaurants'
      Environment:
        Variables:
          restaurants_table: restaurants
      Events:
        Event1:
          Type: Api
          Properties:
            Path: /restaurants/
            Method: get
  Aws_iamResourcePolicy:
    Type: 'AWS::Lambda::Permission'
    Properties:
      Action: 'lambda:InvokeFunction'
      FunctionName:
        'Fn::GetAtt':
          - Aws_iam
          - Arn
      Principal: apigateway.amazonaws.com
      SourceAccount:
        Ref: 'AWS::AccountId'
  SearchRestaurants:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: functions/search-restaurants.handler
      Runtime: nodejs6.10
      CodeUri: >-
        /Users/yancui/SourceCode/Personal/manning-aws-lambda-operational-patterns-and-practices/.serverless/big-mouth.zip
      MemorySize: 128
      Timeout: 3
      Policies:
        - Effect: Allow
          Action: 'dynamodb:scan'
          Resource: 'arn:aws:dynamodb:#{AWS::Region}:#{AWS::AccountId}:table/restaurants'
      Environment:
        Variables:
          restaurants_table: restaurants
      Events:
        Event1:
          Type: Api
          Properties:
            Path: /restaurants/search
            Method: post


================================================
FILE: tests/steps/given.js
================================================
'use strict';

const AWS     = require('aws-sdk');
AWS.config.region = 'us-east-1';
const cognito = new AWS.CognitoIdentityServiceProvider();
const chance  = require('chance').Chance();

let random_password = () => {
  // needs number, special char, upper and lower case
  return `${chance.string({ length: 8})}B!gM0uth`;
}

let an_authenticated_user = function* () {
  let userpoolId = process.env.cognito_user_pool_id;
  let clientId = process.env.cognito_server_client_id;

  let firstName = chance.first();
  let lastName  = chance.last();
  let username  = `test-${firstName}-${lastName}-${chance.string({length: 8})}`;
  let password  = random_password();
  let email     = `${firstName}-${lastName}@big-mouth.com`;

  let createReq = {
    UserPoolId        : userpoolId,
    Username          : username,
    MessageAction     : 'SUPPRESS',
    TemporaryPassword : password,
    UserAttributes    : [
      { Name: "given_name",  Value: firstName },
      { Name: "family_name", Value: lastName },
      { Name: "email",       Value: email }
    ]
  };
  yield cognito.adminCreateUser(createReq).promise();

  console.log(`[${username}] - user is created`);
  
  let req = {
    AuthFlow        : 'ADMIN_NO_SRP_AUTH',
    UserPoolId      : userpoolId,
    ClientId        : clientId,
    AuthParameters  : {
      USERNAME: username,    
      PASSWORD: password
    }
  };
  let resp = yield cognito.adminInitiateAuth(req).promise();

  console.log(`[${username}] - initialised auth flow`);

  let challengeReq = {
    UserPoolId          : userpoolId,
    ClientId            : clientId,
    ChallengeName       : resp.ChallengeName,
    Session             : resp.Session,
    ChallengeResponses  : {
      USERNAME: username,
      NEW_PASSWORD: random_password()
    }
  };
  let challengeResp = yield cognito.adminRespondToAuthChallenge(challengeReq).promise();
  
  console.log(`[${username}] - responded to auth challenge`);

  return {
    username,
    firstName,
    lastName,
    idToken: challengeResp.AuthenticationResult.IdToken
  };
};

module.exports = {
  an_authenticated_user
};

================================================
FILE: tests/steps/init.js
================================================
'use strict';

const _ = require('lodash');
const co = require('co');
const Promise = require('bluebird');
const aws4 = require('../../lib/aws4');
const AWS = require('aws-sdk');
AWS.config.region = 'us-east-1';
const SSM = new AWS.SSM();

let initialized = false;

const getParameters = co.wrap(function* (keys) {
  const prefix = '/bigmouth/dev/';
  const req = {
    Names: keys.map(key => `${prefix}${key}`)
  }
  const resp = yield SSM.getParameters(req).promise();
  return _.reduce(resp.Parameters, function(obj, param) {
    obj[param.Name.substr(prefix.length)] = param.Value
    return obj;
  }, {})
});

let init = co.wrap(function* () {
  if (initialized) {
    return;
  }

  const params = yield getParameters([
    'cognito_client_id',
    'cognito_user_pool_id',
    'restaurants_api'
  ]);

  process.env.restaurants_api = params.restaurants_api;
  process.env.restaurants_table = "restaurants";
  process.env.AWS_REGION = "us-east-1";
  process.env.cognito_client_id = params.cognito_client_id;
  process.env.cognito_user_pool_id = params.cognito_user_pool_id;
  process.env.cognito_server_client_id = "niv7esuaibla0tj5q36b6mvnr";
  process.env.AWS_XRAY_CONTEXT_MISSING = "LOG_ERROR";
  process.env.STAGE = 'dev';

  yield aws4.init();

  initialized = true;
});

module.exports.init = init;

================================================
FILE: tests/steps/tearDown.js
================================================
'use strict';

const co      = require('co');
const AWS     = require('aws-sdk');
AWS.config.region = 'us-east-1';
const cognito = new AWS.CognitoIdentityServiceProvider();

let an_authenticated_user = function* (user) {
  let req = {
    UserPoolId: process.env.cognito_user_pool_id,
    Username: user.username
  };
  yield cognito.adminDeleteUser(req).promise();
  
  console.log(`[${user.username}] - user deleted`);
};

module.exports = {
  an_authenticated_user
};

================================================
FILE: tests/steps/when.js
================================================
'use strict';

const APP_ROOT = '../../';

const _       = require('lodash');
const co      = require('co');
const Promise = require("bluebird");
const http    = require('superagent-promise')(require('superagent'), Promise);
const aws4    = require('../../lib/aws4');
const URL     = require('url');
const mode    = process.env.TEST_MODE;

let respondFrom = function (httpRes) {
  let contentType = _.get(httpRes, 'headers.content-type', 'application/json');
  let body = 
    contentType === 'application/json'
      ? httpRes.body
      : httpRes.text;

  return { 
    statusCode: httpRes.status,
    body: body,
    headers: httpRes.headers
  };
}

let signHttpRequest = (url, httpReq) => {
  let urlData = URL.parse(url);
  let opts = {
    host: urlData.hostname, 
    path: urlData.pathname
  };

  aws4.sign(opts);

  httpReq
    .set('Host', opts.headers['Host'])
    .set('X-Amz-Date', opts.headers['X-Amz-Date'])
    .set('Authorization', opts.headers['Authorization']);

  if (opts.headers['X-Amz-Security-Token']) {
    httpReq.set('X-Amz-Security-Token', opts.headers['X-Amz-Security-Token']);
  }
}

let viaHttp = co.wrap(function* (relPath, method, opts) {
  let root = process.env.TEST_ROOT;
  let url = `${root}/${relPath}`;
  console.log(`invoking via HTTP ${method} ${url}`);

  try {
    let httpReq = http(method, url);

    let body = _.get(opts, "body");
    if (body) {      
      httpReq.send(body);
    }

    if (_.get(opts, "iam_auth", false) === true) {
      signHttpRequest(url, httpReq);
    }

    let authHeader = _.get(opts, "auth");
    if (authHeader) {
      httpReq.set('Authorization', authHeader);
    }
    
    let res = yield httpReq;
    return respondFrom(res);
  } catch (err) {
    if (err.status) {
      return {
        statusCode: err.status,
        headers: err.response.headers
      };
    } else {
      throw err;
    }
  }
})

let viaHandler = (event, functionName) => {  
  let handler = require(`${APP_ROOT}/functions/${functionName}`).handler;
  console.log(`invoking via handler function ${functionName}`);

  return new Promise((resolve, reject) => {
    let context = {};
    let callback = function (err, response) {
      if (err) {
        reject(err);
      } else {
        let contentType = _.get(response, 'headers.content-type', 'application/json');
        if (response.body && contentType === 'application/json') {
          response.body = JSON.parse(response.body);
        }

        resolve(response);
      }
    };

    handler(event, context, callback);
  });
}

let we_invoke_get_index = co.wrap(function* () {
  let res = 
    mode === 'handler' 
      ? yield viaHandler({}, 'get-index')
      : yield viaHttp('', 'GET');

  return res;
});

let we_invoke_get_restaurants = co.wrap(function* () {
  let res =
    mode === 'handler' 
      ? yield viaHandler({}, 'get-restaurants')
      : yield viaHttp('restaurants', 'GET', { iam_auth: true });

  return res;
});

let we_invoke_search_restaurants = co.wrap(function* (user, theme) {
  let body = JSON.stringify({ theme });
  let auth = user.idToken;

  let res = 
    mode === 'handler'
      ? viaHandler({ body }, 'search-restaurants')
      : viaHttp('restaurants/search', 'POST', { body, auth })

  return res;
});

module.exports = {
  we_invoke_get_index,
  we_invoke_get_restaurants,
  we_invoke_search_restaurants
};

================================================
FILE: tests/test_cases/get-index.js
================================================
'use strict';

const co = require('co');
const expect = require('chai').expect;
const when = require('../steps/when');
const init = require('../steps/init').init;
const cheerio = require('cheerio');

describe(`When we invoke the GET / endpoint`, co.wrap(function* () {
  before(co.wrap(function* () {
    yield init();
  }));

  it(`Should return the index page with 8 restaurants`, co.wrap(function* () {
    let res = yield when.we_invoke_get_index();

    expect(res.statusCode).to.equal(200);
    expect(res.headers['content-type']).to.equal('text/html; charset=UTF-8');
    expect(res.body).to.not.be.null;

    let $ = cheerio.load(res.body);
    let restaurants = $('.restaurant', '#restaurantsUl');
    expect(restaurants.length).to.equal(8);
    
  }));
  
}));

================================================
FILE: tests/test_cases/get-restaurants.js
================================================
'use strict';

const co = require('co');
const expect = require('chai').expect;
const init = require('../steps/init').init;
const when = require('../steps/when');

describe(`When we invoke the GET /restaurants endpoint`, co.wrap(function* () {
  before(co.wrap(function* () {
    yield init();
  }));

  it(`Should return an array of 8 restaurants`, co.wrap(function* () {
    let res = yield when.we_invoke_get_restaurants();

    expect(res.statusCode).to.equal(200);
    expect(res.body).to.have.lengthOf(8);

    for (let restaurant of res.body) {
      expect(restaurant).to.have.property('name');
      expect(restaurant).to.have.property('image');
    }
  }));
}));

================================================
FILE: tests/test_cases/search-restaurants.js
================================================
'use strict';

const co = require('co');
const expect = require('chai').expect;
const init = require('../steps/init').init;
const when = require('../steps/when');
const given = require('../steps/given');
const tearDown = require('../steps/tearDown');

describe(`Given an authenticated user`, co.wrap(function* () {
  let user;
  before(co.wrap(function* () {
    yield init();
    user = yield given.an_authenticated_user();
  }));

  after(co.wrap(function* () {
    yield tearDown.an_authenticated_user(user);
  }));

  describe(`When we invoke the POST /restaurants/search endpoint with theme 'cartoon'`, co.wrap(function* () {
    it(`Should return an array of 4 restaurants`, co.wrap(function* () {
      let res = yield when.we_invoke_search_restaurants(user, 'cartoon');
  
      expect(res.statusCode).to.equal(200);
      expect(res.body).to.have.lengthOf(4);
  
      for (let restaurant of res.body) {
        expect(restaurant).to.have.property('name');
        expect(restaurant).to.have.property('image');
      }
    }));
  }));  
}));

Download .txt
gitextract_0qv7lc5k/

├── .gitignore
├── .vscode/
│   └── launch.json
├── README.md
├── build.sh
├── buildspec.yml
├── examples/
│   ├── create-alarm.json
│   ├── get-index.json
│   ├── notify-restaurant.json
│   ├── place-order.json
│   ├── retry-notify-restaurant.json
│   └── search-restaurants.json
├── functions/
│   ├── accept-order.js
│   ├── create-alarms.js
│   ├── fulfill-order.js
│   ├── get-index.js
│   ├── get-restaurants.js
│   ├── notify-restaurant.js
│   ├── notify-user.js
│   ├── place-order.js
│   ├── retry-notify-restaurant.js
│   ├── retry-notify-user.js
│   └── search-restaurants.js
├── lib/
│   ├── aws4.js
│   ├── awscred.js
│   ├── cloudwatch.js
│   ├── correlation-ids.js
│   ├── http.js
│   ├── kinesis.js
│   ├── log.js
│   ├── lru.js
│   ├── notify.js
│   ├── retry.js
│   └── sns.js
├── middleware/
│   ├── capture-correlation-ids.js
│   ├── flush-metrics.js
│   ├── function-shield.js
│   ├── sample-logging.js
│   └── wrapper.js
├── package.json
├── seed-restaurants.js
├── serverless.yml
├── static/
│   └── index.html
├── template.yml
└── tests/
    ├── steps/
    │   ├── given.js
    │   ├── init.js
    │   ├── tearDown.js
    │   └── when.js
    └── test_cases/
        ├── get-index.js
        ├── get-restaurants.js
        └── search-restaurants.js
Download .txt
SYMBOL INDEX (75 symbols across 19 files)

FILE: functions/create-alarms.js
  constant AWS (line 5) | const AWS        = require('aws-sdk');

FILE: functions/get-index.js
  constant URL (line 8) | const URL        = require('url');
  constant STAGE (line 16) | const STAGE     = process.env.STAGE;

FILE: functions/get-restaurants.js
  constant AWS (line 5) | const AWS        = AWSXRay.captureAWS(require('aws-sdk'));

FILE: functions/place-order.js
  constant UNAUTHORIZED (line 14) | const UNAUTHORIZED = {

FILE: functions/search-restaurants.js
  constant AWS (line 5) | const AWS        = AWSXRay.captureAWS(require('aws-sdk'));

FILE: lib/aws4.js
  function hmac (line 11) | function hmac(key, string, encoding) {
  function hash (line 15) | function hash(string, encoding) {
  function encodeRfc3986 (line 20) | function encodeRfc3986(urlEncodedString) {
  function RequestSigner (line 28) | function RequestSigner(request, credentials) {
  function trimAll (line 258) | function trimAll(header) {

FILE: lib/awscred.js
  function loadCredentialsAndRegion (line 33) | function loadCredentialsAndRegion(options, cb) {
  function loadCredentials (line 52) | function loadCredentials(options, cb) {
  function loadRegion (line 72) | function loadRegion(options, cb) {
  function loadRegionSync (line 92) | function loadRegionSync(options) {
  function loadCredentialsFromEnv (line 96) | function loadCredentialsFromEnv(options, cb) {
  function loadRegionFromEnv (line 106) | function loadRegionFromEnv(options, cb) {
  function loadRegionFromEnvSync (line 112) | function loadRegionFromEnvSync() {
  function loadCredentialsFromIniFile (line 116) | function loadCredentialsFromIniFile(options, cb) {
  function loadRegionFromIniFile (line 129) | function loadRegionFromIniFile(options, cb) {
  function loadRegionFromIniFileSync (line 138) | function loadRegionFromIniFileSync(options) {
  function loadCredentialsFromEcs (line 146) | function loadCredentialsFromEcs(options, cb) {
  function loadCredentialsFromEc2Metadata (line 182) | function loadCredentialsFromEc2Metadata(options, cb) {
  function loadProfileFromIniFile (line 223) | function loadProfileFromIniFile(options, defaultFilename, cb) {
  function loadProfileFromIniFileSync (line 234) | function loadProfileFromIniFileSync(options, defaultFilename) {
  function merge (line 249) | function merge(obj, options, cb) {
  function resolveProfile (line 284) | function resolveProfile() {
  function resolveHome (line 288) | function resolveHome() {
  function parseAwsIni (line 293) | function parseAwsIni(ini) {
  function request (line 313) | function request(options, cb) {
  function once (line 332) | function once(cb) {

FILE: lib/cloudwatch.js
  constant AWS (line 5) | const AWS        = AWSXRay.captureAWS(require('aws-sdk'));
  function getCountMetricData (line 26) | function getCountMetricData(name, value) {
  function getTimeMetricData (line 35) | function getTimeMetricData(name, statsValues) {
  function getCountMetricDatum (line 44) | function getCountMetricDatum() {
  function getTimeMetricDatum (line 55) | function getTimeMetricDatum() {
  function clear (line 89) | function clear() {
  function incrCount (line 94) | function incrCount(metricName, count) {
  function recordTimeInMillis (line 108) | function recordTimeInMillis(metricName, ms) {
  function trackExecTime (line 136) | function trackExecTime(metricName, f) {

FILE: lib/http.js
  function getRequest (line 6) | function getRequest (options) {
  function setHeaders (line 27) | function setHeaders (request, headers) {
  function setQueryStrings (line 34) | function setQueryStrings (request, qs) {
  function setBody (line 42) | function setBody (request, body) {

FILE: lib/kinesis.js
  constant AWS (line 5) | const AWS            = AWSXRay.captureAWS(require('aws-sdk'));
  function tryJsonParse (line 10) | function tryJsonParse(data) {
  function addCorrelationIds (line 23) | function addCorrelationIds(data) {
  function putRecord (line 35) | function putRecord(params, cb) {

FILE: lib/log.js
  constant DEFAULT_CONTEXT (line 14) | const DEFAULT_CONTEXT = {
  function getContext (line 22) | function getContext () {
  function logLevelName (line 34) | function logLevelName() {
  function isEnabled (line 38) | function isEnabled (level) {
  function appendError (line 42) | function appendError(params, err) {
  function log (line 54) | function log (levelName, message, params) {

FILE: lib/lru.js
  function LruCache (line 5) | function LruCache(size) {
  function DoublyLinkedList (line 46) | function DoublyLinkedList() {
  function DoublyLinkedNode (line 91) | function DoublyLinkedNode(key, val) {

FILE: lib/sns.js
  constant AWS (line 4) | const AWS            = AWSXRay.captureAWS(require('aws-sdk'));
  constant SNS (line 5) | const SNS            = new AWS.SNS();
  function addCorrelationIds (line 8) | function addCorrelationIds(messageAttributes) {
  function publish (line 23) | function publish(params, cb) {

FILE: middleware/capture-correlation-ids.js
  function captureHttp (line 6) | function captureHttp(headers, awsRequestId, sampleDebugLogRate) {
  function parsePayload (line 37) | function parsePayload (record) {
  function captureKinesis (line 42) | function captureKinesis(event, context, sampleDebugLogRate) {
  function captureSns (line 107) | function captureSns(records, awsRequestId, sampleDebugLogRate) {
  function isApiGatewayEvent (line 138) | function isApiGatewayEvent(event) {
  function isKinesisEvent (line 142) | function isKinesisEvent(event) {
  function isSnsEvent (line 154) | function isSnsEvent(event) {

FILE: seed-restaurants.js
  constant AWS (line 4) | const AWS = require('aws-sdk');

FILE: tests/steps/given.js
  constant AWS (line 3) | const AWS     = require('aws-sdk');

FILE: tests/steps/init.js
  constant AWS (line 7) | const AWS = require('aws-sdk');
  constant SSM (line 9) | const SSM = new AWS.SSM();

FILE: tests/steps/tearDown.js
  constant AWS (line 4) | const AWS     = require('aws-sdk');

FILE: tests/steps/when.js
  constant APP_ROOT (line 3) | const APP_ROOT = '../../';
  constant URL (line 10) | const URL     = require('url');
Condensed preview — 50 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (130K chars).
[
  {
    "path": ".gitignore",
    "chars": 86,
    "preview": "# package directories\nnode_modules\njspm_packages\n\n# Serverless directories\n.serverless"
  },
  {
    "path": ".vscode/launch.json",
    "chars": 3745,
    "preview": "{\n  // Use IntelliSense to learn about possible attributes.\n  // Hover to view descriptions of existing attributes.\n  //"
  },
  {
    "path": "README.md",
    "chars": 139,
    "preview": "# manning-aws-lambda-operational-patterns-and-practices\nCode for the Manning video course \"AWS Lambda: Operational Patte"
  },
  {
    "path": "build.sh",
    "chars": 597,
    "preview": "#!/bin/bash\nset -e\nset -o pipefail\n\ninstruction()\n{\n  echo \"usage: ./build.sh deploy <env>\"\n  echo \"\"\n  echo \"env: eg. i"
  },
  {
    "path": "buildspec.yml",
    "chars": 163,
    "preview": "version: 0.2\n\nphases:\n  build:\n    commands:\n      - chmod +x build.sh\n      - ./build.sh int-test\n      - ./build.sh de"
  },
  {
    "path": "examples/create-alarm.json",
    "chars": 2287,
    "preview": "{\n  \"version\": \"0\",\n  \"id\": \"dee9a69c-8166-1ad7-41d4-1dad201e29f6\",\n  \"detail-type\": \"AWS API Call via CloudTrail\",\n  \"s"
  },
  {
    "path": "examples/get-index.json",
    "chars": 2354,
    "preview": "{\n  \"body\": null,\n  \"resource\": \"/\",\n  \"requestContext\": {\n    \"resourceId\": \"123456\",\n    \"apiId\": \"1234567890\",\n    \"r"
  },
  {
    "path": "examples/notify-restaurant.json",
    "chars": 943,
    "preview": "{\n  \"Records\":[\n    {\n      \"kinesis\": {\n        \"kinesisSchemaVersion\":\"1.0\",\n        \"partitionKey\":\"e4a5eae6-19f7-528"
  },
  {
    "path": "examples/place-order.json",
    "chars": 3979,
    "preview": "{\n  \"resource\": \"\\/orders\",\n  \"path\": \"\\/orders\",\n  \"httpMethod\": \"POST\",\n  \"headers\": {\n    \"Accept\": \"*\\/*\",\n    \"Acce"
  },
  {
    "path": "examples/retry-notify-restaurant.json",
    "chars": 732,
    "preview": "{\n  \"Records\": [\n    {\n      \"EventVersion\": \"1.0\",\n      \"EventSubscriptionArn\": \"arn:aws:sns:EXAMPLE\",\n      \"EventSou"
  },
  {
    "path": "examples/search-restaurants.json",
    "chars": 2440,
    "preview": "{\n  \"body\": \"{\\\"theme\\\":\\\"cartoon\\\"}\",\n  \"resource\": \"/{proxy+}\",\n  \"requestContext\": {\n    \"resourceId\": \"123456\",\n    "
  },
  {
    "path": "functions/accept-order.js",
    "chars": 1404,
    "preview": "'use strict';\n\nconst co         = require('co');\nconst kinesis    = require('../lib/kinesis');\nconst log        = requir"
  },
  {
    "path": "functions/create-alarms.js",
    "chars": 3492,
    "preview": "'use strict';\n\nconst _          = require('lodash');\nconst co         = require('co');\nconst AWS        = require('aws-s"
  },
  {
    "path": "functions/fulfill-order.js",
    "chars": 1416,
    "preview": "'use strict';\n\nconst co         = require('co');\nconst kinesis    = require('../lib/kinesis');\nconst log        = requir"
  },
  {
    "path": "functions/get-index.js",
    "chars": 3269,
    "preview": "'use strict';\n\nconst co         = require(\"co\");\nconst Promise    = require(\"bluebird\");\nconst fs         = Promise.prom"
  },
  {
    "path": "functions/get-restaurants.js",
    "chars": 1092,
    "preview": "'use strict';\n\nconst co         = require('co');\nconst AWSXRay    = require('aws-xray-sdk');\nconst AWS        = AWSXRay."
  },
  {
    "path": "functions/notify-restaurant.js",
    "chars": 1054,
    "preview": "'use strict';\n\nconst co     = require('co');\nconst notify = require('../lib/notify');\nconst retry  = require('../lib/ret"
  },
  {
    "path": "functions/notify-user.js",
    "chars": 1060,
    "preview": "'use strict';\n\nconst co     = require('co');\nconst notify = require('../lib/notify');\nconst retry  = require('../lib/ret"
  },
  {
    "path": "functions/place-order.js",
    "chars": 1806,
    "preview": "'use strict';\n\nconst _              = require('lodash');\nconst co             = require('co');\nconst kinesis        = re"
  },
  {
    "path": "functions/retry-notify-restaurant.js",
    "chars": 912,
    "preview": "'use strict';\n\nconst co         = require('co');\nconst notify     = require('../lib/notify');\nconst log        = require"
  },
  {
    "path": "functions/retry-notify-user.js",
    "chars": 902,
    "preview": "'use strict';\n\nconst co         = require('co');\nconst notify     = require('../lib/notify');\nconst log        = require"
  },
  {
    "path": "functions/search-restaurants.js",
    "chars": 1334,
    "preview": "'use strict';\n\nconst co         = require('co');\nconst AWSXRay    = require('aws-xray-sdk');\nconst AWS        = AWSXRay."
  },
  {
    "path": "lib/aws4.js",
    "chars": 11598,
    "preview": "var aws4 = exports,    \n    url = require('url'),\n    querystring = require('querystring'),\n    crypto = require('crypto"
  },
  {
    "path": "lib/awscred.js",
    "chars": 9576,
    "preview": "var fs = require('fs'),\n    path = require('path'),\n    http = require('http'),\n    env = process.env\n\nexports.credentia"
  },
  {
    "path": "lib/cloudwatch.js",
    "chars": 4403,
    "preview": "'use strict';\n\nconst co         = require('co');\nconst AWSXRay    = require('aws-xray-sdk');\nconst AWS        = AWSXRay."
  },
  {
    "path": "lib/correlation-ids.js",
    "chars": 455,
    "preview": "'use strict';\n\nlet clearAll = () => global.CONTEXT = undefined;\n\nlet replaceAllWith = ctx => global.CONTEXT = ctx;\n\nlet "
  },
  {
    "path": "lib/http.js",
    "chars": 1878,
    "preview": "'use strict';\n\nconst correlationIds = require('./correlation-ids');\nconst http = require('superagent-promise')(require('"
  },
  {
    "path": "lib/kinesis.js",
    "chars": 1112,
    "preview": "'use strict';\n\nconst _              = require('lodash');\nconst AWSXRay        = require('aws-xray-sdk');\nconst AWS      "
  },
  {
    "path": "lib/log.js",
    "chars": 2215,
    "preview": "'use strict';\n\nconst correlationIds = require('./correlation-ids');\n\nconst LogLevels = {\n  DEBUG : 0,\n  INFO  : 1,\n  WAR"
  },
  {
    "path": "lib/lru.js",
    "chars": 1913,
    "preview": "module.exports = function(size) {\n  return new LruCache(size)\n}\n\nfunction LruCache(size) {\n  this.capacity = size | 0\n  "
  },
  {
    "path": "lib/notify.js",
    "chars": 2969,
    "preview": "'use strict';\n\nconst _          = require('lodash');\nconst co         = require('co');\nconst sns        = require('./sns"
  },
  {
    "path": "lib/retry.js",
    "chars": 1449,
    "preview": "'use strict';\n\nconst co         = require('co');\nconst sns        = require('./sns');\nconst log        = require('./log'"
  },
  {
    "path": "lib/sns.js",
    "chars": 922,
    "preview": "'use strict';\n\nconst AWSXRay        = require('aws-xray-sdk');\nconst AWS            = AWSXRay.captureAWS(require('aws-sd"
  },
  {
    "path": "middleware/capture-correlation-ids.js",
    "chars": 4855,
    "preview": "'use strict';\n\nconst correlationIds = require('../lib/correlation-ids');\nconst log = require('../lib/log');\n\nfunction ca"
  },
  {
    "path": "middleware/flush-metrics.js",
    "chars": 296,
    "preview": "'use strict';\n\nconst log        = require('../lib/log');\nconst cloudwatch = require('../lib/cloudwatch');\n\nmodule.export"
  },
  {
    "path": "middleware/function-shield.js",
    "chars": 404,
    "preview": "'use strict';\n\nconst FuncShield = require('@puresec/function-shield');\n\nmodule.exports = () => {\n  return {\n    before: "
  },
  {
    "path": "middleware/sample-logging.js",
    "chars": 996,
    "preview": "'use strict';\n\nconst correlationIds = require('../lib/correlation-ids');\nconst log = require('../lib/log');\n\n// config s"
  },
  {
    "path": "middleware/wrapper.js",
    "chars": 400,
    "preview": "'use strict';\n\nconst middy = require('middy');\nconst sampleLogging = require('./sample-logging');\nconst captureCorrelati"
  },
  {
    "path": "package.json",
    "chars": 1335,
    "preview": "{\n    \"name\": \"big-mouth\",\n    \"version\": \"1.0.0\",\n    \"description\": \"\",\n    \"main\": \"index.js\",\n    \"scripts\": {\n     "
  },
  {
    "path": "seed-restaurants.js",
    "chars": 1619,
    "preview": "'use strict';\n\nconst co = require('co');\nconst AWS = require('aws-sdk');\nAWS.config.region = 'us-east-1';\nconst dynamodb"
  },
  {
    "path": "serverless.yml",
    "chars": 9543,
    "preview": "service: big-mouth\n\nplugins:\n  - serverless-pseudo-parameters\n  - serverless-sam\n  - serverless-iam-roles-per-function\n "
  },
  {
    "path": "static/index.html",
    "chars": 15202,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\">\n    <title>Big Mouth</title>\n\n    <script src=\"https://sdk.am"
  },
  {
    "path": "template.yml",
    "chars": 3026,
    "preview": "AWSTemplateFormatVersion: '2010-09-09'\nTransform: 'AWS::Serverless-2016-10-31'\nDescription: 'SAM template for Serverless"
  },
  {
    "path": "tests/steps/given.js",
    "chars": 2106,
    "preview": "'use strict';\n\nconst AWS     = require('aws-sdk');\nAWS.config.region = 'us-east-1';\nconst cognito = new AWS.CognitoIdent"
  },
  {
    "path": "tests/steps/init.js",
    "chars": 1309,
    "preview": "'use strict';\n\nconst _ = require('lodash');\nconst co = require('co');\nconst Promise = require('bluebird');\nconst aws4 = "
  },
  {
    "path": "tests/steps/tearDown.js",
    "chars": 470,
    "preview": "'use strict';\n\nconst co      = require('co');\nconst AWS     = require('aws-sdk');\nAWS.config.region = 'us-east-1';\nconst"
  },
  {
    "path": "tests/steps/when.js",
    "chars": 3363,
    "preview": "'use strict';\n\nconst APP_ROOT = '../../';\n\nconst _       = require('lodash');\nconst co      = require('co');\nconst Promi"
  },
  {
    "path": "tests/test_cases/get-index.js",
    "chars": 770,
    "preview": "'use strict';\n\nconst co = require('co');\nconst expect = require('chai').expect;\nconst when = require('../steps/when');\nc"
  },
  {
    "path": "tests/test_cases/get-restaurants.js",
    "chars": 672,
    "preview": "'use strict';\n\nconst co = require('co');\nconst expect = require('chai').expect;\nconst init = require('../steps/init').in"
  },
  {
    "path": "tests/test_cases/search-restaurants.js",
    "chars": 1052,
    "preview": "'use strict';\n\nconst co = require('co');\nconst expect = require('chai').expect;\nconst init = require('../steps/init').in"
  }
]

About this extraction

This page contains the full source code of the theburningmonk/manning-aws-lambda-in-motion GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 50 files (118.3 KB), approximately 33.0k tokens, and a symbol index with 75 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!