├── .gitignore ├── .gitmodules ├── .npmignore ├── LICENSE ├── README.md ├── bower.json ├── docs └── examples │ ├── bash │ ├── index.html │ └── index.js │ ├── nomocors │ ├── index.html │ └── index.js │ └── runcasperjs │ ├── bin │ └── phantomjs │ ├── index.html │ └── index.js ├── lib └── lambda-job.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | bower_components/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/examples/runcasperjs/bin/casperjs"] 2 | path = docs/examples/runcasperjs/bin/casperjs 3 | url = https://github.com/n1k0/casperjs.git 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Larry Gadea 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LambdaJob 2 | 3 | [![Version npm][version]](http://browsenpm.org/package/lambda-job)[![Version bower][bower]](https://github.com/lg/lambda-job) 4 | 5 | [version]: http://img.shields.io/npm/v/lambda-job.svg?style=flat-square 6 | [bower]: https://img.shields.io/bower/v/lambda-job.svg?style=flat-square 7 | 8 | **THIS PROJECT IS NO LONGER MAINTAINED -- Amazon has integrated this functionality straight into Lambda!** 9 | 10 | LambdaJob provides real-time invoking and completion detection of AWS Lambda functions from JavaScript on the clientside's web browser. This makes it easy to scale things like: scraping websites, thumbnail generation of images/videos, running custom binaries, etc. 11 | 12 | ## Features 13 | 14 | - Lambdas triggered by S3 Object creation 15 | - Real-time results via SQS long-polling 16 | - No back-end needed, just an AWS account and public IAM permissions 17 | - Low-latency and performance focus throughout 18 | - Maximum job throttling (to stay under the 25 Lambda limit) 19 | - Shared SQS queue per client 20 | - Helper for calling commandline applications on Lambda 21 | 22 | ## Usage 23 | 24 | From the web browser: 25 | 26 | ```javascript 27 | var lambdaJob = new LambdaJobClient(); 28 | lambdaJob.invoke("bash", {cmd: "/sbin/ifconfig"}, function (err, output) { 29 | console.log(output); 30 | }); 31 | ``` 32 | 33 | The Lambda job: 34 | 35 | ```javascript 36 | var LambdaJob = require('lambda-job'); 37 | var lambdaJob = new LambdaJob.LambdaJobWorker(AWS, jobReceived); 38 | exports.handler = lambdaJob.lambdaHandler; 39 | 40 | function jobReceived(params, errDataCallback) { 41 | console.log("Will run: bash -c " + params.cmd + "..."); 42 | lambdaJob.execHelper(params.cmd, function(err, consoleOutput) { 43 | errDataCallback(err, consoleOutput); 44 | } 45 | } 46 | ``` 47 | 48 | Outputs in the web browser: 49 | 50 | ``` 51 | vsb_20 Link encap:Ethernet HWaddr 26:3A:68:14:69:21 52 | inet addr:192.168.20.21 Bcast:0.0.0.0 Mask:255.255.255.0 53 | UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 54 | RX packets:54 errors:0 dropped:0 overruns:0 frame:0 55 | TX packets:52 errors:0 dropped:0 overruns:0 carrier:0 56 | collisions:0 txqueuelen:1000 57 | RX bytes:23982 (23.4 KiB) TX bytes:10948 (10.6 KiB) 58 | [...] 59 | ``` 60 | 61 | ## Options 62 | - `debugLog`: Turn debug logging on/off. Useful to turn on to see how LambdaJob works. *(default: false)* 63 | - `maxActiveJobs`: Throttle how many jobs can run at once. Note AWS has a hard limit of 25 active jobs at any one point. *(default: 25)* 64 | - `jobTimeout`: The maximum amount of time (in ms) that a job can run for. *(default: 30000)* 65 | - `s3BucketPrefix`: Since we create S3 objects for every desired job, the bucket they're inside of has this prefix. Note that S3 buckets are in a global namespace with other people. *(default: lambda-job-)* 66 | - `sqsQueuePrefix`: Like the s3BucketPrefix except for SQS queues. *(default: lambda-job-)* 67 | 68 | ## Prerequisites 69 | 70 | 1. Create an IAM user with the following permissions: 71 | 72 | { 73 | "Version": "2012-10-17", 74 | "Statement": [ 75 | { 76 | "Sid": "Stmt1418615018000", 77 | "Effect": "Allow", 78 | "Action": [ 79 | "s3:PutObject", 80 | "s3:PutObjectAcl", 81 | "s3:GetObject", 82 | "s3:DeleteObject", 83 | "sqs:CreateQueue", 84 | "sqs:DeleteQueue", 85 | "sqs:ReceiveMessage", 86 | "sqs:SendMessage", 87 | "sqs:DeleteMessage" 88 | ], 89 | "Resource": [ 90 | "arn:aws:s3:::lambda-job-*", 91 | "arn:aws:sqs:us-west-2:YOURACCOUNTNUM:lambda-job-*" 92 | ] 93 | } 94 | ] 95 | } 96 | 2. Create an S3 bucket named `lambda-job-LAMBDANAME` 97 | 3. Add CORS permissions on the S3 bucket. Here's a CORS config sample: 98 | 99 | 100 | 101 | 102 | * 103 | GET 104 | POST 105 | PUT 106 | DELETE 107 | HEAD 108 | 3000 109 | * 110 | 111 | 112 | 4. Create your Lambda and set this S3 bucket as your S3 source. It's recommended to use the full 1024MB of RAM since it seems things are faster this way. 113 | 6. In your Lambda, upload your files to AWS, making sure you included the npm modules you'll need in the archive. 114 | 7. Browse to index.html either where you've hosted it, or locally. 115 | 116 | ## Examples 117 | 118 | Before running any of these examples make sure you do the permissions related things above and also change the `CHANGEME` text in the `index.html` and `index.js` files to be your AWS keys. 119 | 120 | - *bash* shows how you can run custom binaries on Lambda. It also provides convenient access to explore the Lambda VM and what's available. 121 | - *nomocors* shows how you could use LambdaJob to proxy around CORS restrictions. Great for client-side automation. 122 | - *runcasperjs* shows how you can do web scriping from Lambda. 123 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-job", 3 | "version": "1.0.0", 4 | "homepage": "https://github.com/lg/lambda-job", 5 | "authors": [ 6 | "Larry Gadea " 7 | ], 8 | "description": "Use AWS Lambda to run scalable server-side code from the web browser", 9 | "main": "lib/lambda-job.js", 10 | "moduleType": [ 11 | "node" 12 | ], 13 | "keywords": [ 14 | "aws", 15 | "lambda" 16 | ], 17 | "license": "ISC", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "test", 23 | "tests" 24 | ], 25 | "dependencies": { 26 | "aws-sdk": "~2.1.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/examples/bash/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Command to run:
9 | 10 |
11 |
12 | Result: (recommended you also open the JS console)
13 |
14 | 15 | -------------------------------------------------------------------------------- /docs/examples/bash/index.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | AWS.config.update({accessKeyId: "CHANGETHIS", secretAccessKey: "CHANGETHIS", region: "us-west-2"}); 3 | 4 | var LambdaJob = require('lambda-job'); 5 | var lambdaJob = new LambdaJob.LambdaJobWorker(AWS, jobReceived); 6 | exports.handler = lambdaJob.lambdaHandler; 7 | 8 | function jobReceived(params, errDataCallback) { 9 | console.log("Will run: bash -c " + params.cmd + "..."); 10 | lambdaJob.execHelper(params.cmd, function(err, consoleOutput) { 11 | errDataCallback(err, consoleOutput); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /docs/examples/nomocors/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Website to retrieve:
9 |
10 | Parallel jobs:
11 |
12 |
13 |
14 | Result: (recommended you also open the JS console)
15 |
16 | 17 | -------------------------------------------------------------------------------- /docs/examples/nomocors/index.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | AWS.config.update({accessKeyId: "CHANGETHIS", secretAccessKey: "CHANGETHIS", region: "us-west-2"}); 3 | 4 | var lambdaJobWorker = require('lambda-job'); 5 | var lambdaJob = new lambdaJobWorker.LambdaJobWorker(AWS, jobReceived); 6 | exports.handler = lambdaJob.lambdaHandler; 7 | 8 | function jobReceived(params, errDataCallback) { 9 | var request = require('request'); 10 | request({url: params.url}, function (error, response, body) { 11 | if (!error && response.statusCode == 200) { 12 | errDataCallback(null, body); 13 | } else { 14 | errDataCallback(error, null); 15 | } 16 | }); 17 | } -------------------------------------------------------------------------------- /docs/examples/runcasperjs/bin/phantomjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lg/lambda-job/605e48878b79a4e4d55b1ee53f6b11c396861f51/docs/examples/runcasperjs/bin/phantomjs -------------------------------------------------------------------------------- /docs/examples/runcasperjs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | CasperJS/PhantomJS script to run:
9 |
19 |
20 |
21 | Result: (recommended you also open the JS console)
22 |
23 | 24 | -------------------------------------------------------------------------------- /docs/examples/runcasperjs/index.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | AWS.config.update({accessKeyId: "CHANGETHIS", secretAccessKey: "CHANGETHIS", region: "us-west-2"}); 3 | 4 | var LambdaJob = require('lambda-job'); 5 | var lambdaJob = new LambdaJob.LambdaJobWorker(AWS, jobReceived); 6 | exports.handler = lambdaJob.lambdaHandler; 7 | 8 | function jobReceived(params, errDataCallback) { 9 | var fs = require('fs'); 10 | 11 | fs.writeFile("/tmp/script.js", params.script, function(err) { 12 | if (err) return errDataCallback('Couldnt write script file locally!', null); 13 | 14 | lambdaJob.execHelper("PHANTOMJS_EXECUTABLE=bin/phantomjs bin/casperjs/bin/casperjs /tmp/script.js", function(err, consoleOutput) { 15 | errDataCallback(err, consoleOutput); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /lib/lambda-job.js: -------------------------------------------------------------------------------- 1 | // LambdaJob 1.0.0 by Larry Gadea (trivex@gmail.com) 2 | 3 | function LambdaJobClient(options) { 4 | var sqs = new AWS.SQS(); 5 | var s3 = new AWS.S3(); 6 | 7 | // If a job in in the queue, it can be either a) creating an sqs queue, b) waiting for another 8 | // job to create the sqs queue or c) been sent and waiting for a response. 9 | var jobs = []; // of jobInfo 10 | var sqsRecv = null; 11 | 12 | // Default options 13 | if (options == undefined) options = {} 14 | var defaults = { 15 | debugLog: false, 16 | maxActiveJobs: 25, 17 | jobTimeout: 30000, 18 | s3BucketPrefix: "lambda-job-", 19 | sqsQueuePrefix: "lambda-job-" 20 | } 21 | Object.keys(defaults).forEach(function(key) { 22 | options[key] = (options[key] == undefined) ? defaults[key] : options[key] 23 | }); 24 | 25 | function debugInfo(text) { 26 | if (options.debugLog) 27 | console.log(text); 28 | } 29 | 30 | function getRandomId() { 31 | return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); 32 | } 33 | 34 | function getQueueUrl() { 35 | return (jobs.length == 0) ? null : jobs[0].queueUrl; 36 | } 37 | 38 | function getTotalActiveJobs() { 39 | var activeJobs = 0; 40 | jobs.forEach(function (job) { 41 | if (job.queueUrl != null) 42 | activeJobs += 1; 43 | }); 44 | return activeJobs; 45 | } 46 | 47 | function createQueue() { 48 | var qid = getRandomId(); 49 | sqs.createQueue({QueueName: options.sqsQueuePrefix + qid}, function(err, data) { 50 | if (err) { 51 | console.error("Removing all jobs due to SQS queue creation failure: " + err); 52 | jobs.forEach(function(jobInfo) { 53 | jobInfo.errDataCallback("Failed to create an SQS queue to run job", null); 54 | }); 55 | jobs = []; 56 | return; 57 | } 58 | 59 | var queueUrl = data["QueueUrl"]; 60 | debugInfo("Created and listening on queue: " + queueUrl); 61 | 62 | // Start the job that was creating the queue 63 | jobs.forEach(function(jobInfo) { 64 | if (jobInfo.queueUrl == "creating") 65 | invokeJob(jobInfo, queueUrl); 66 | }); 67 | 68 | waitForJobResponses(queueUrl); 69 | 70 | // Incase any other jobs were added while the queue was being created 71 | invokeWaitingJobs(); 72 | }); 73 | } 74 | 75 | function invokeWaitingJobs() { 76 | jobs.forEach(function(jobInfo) { 77 | if (jobInfo.queueUrl != null) 78 | return; 79 | 80 | // Do not invoke job yet if we're at the maximum amount of active jobs 81 | if (getTotalActiveJobs() >= options.maxActiveJobs) 82 | return; 83 | 84 | invokeJob(jobInfo, getQueueUrl()); 85 | }); 86 | } 87 | 88 | function invokeJob(jobInfo, queueUrl) { 89 | jobInfo.queueUrl = queueUrl; 90 | jobInfo.timeoutTimer = setTimeout(function() { invokeTimeout(jobInfo.iid); }, options.jobTimeout); 91 | 92 | debugInfo("Invoking " + jobInfo.lambdaName + " (" + jobInfo.iid + ") onto " + jobInfo.queueUrl); 93 | s3.putObject({Bucket: options.s3BucketPrefix + jobInfo.lambdaName, Key: jobInfo.iid + "-request.job", Body: JSON.stringify(jobInfo)}, function(err, data) { 94 | if (err) { 95 | processJobResponse(jobInfo, "Failed to create S3 job: " + err, null); 96 | return; 97 | } 98 | }); 99 | } 100 | 101 | function jobIndexFromiid(iid) { 102 | for (var i = 0; i < jobs.length; i++) { 103 | if (jobs[i].iid == iid) { 104 | return i; 105 | } 106 | } 107 | return -1; 108 | } 109 | 110 | function processJobResponse(jobInfo, err, data) { 111 | var matchedJobsIndex = jobIndexFromiid(jobInfo.iid); 112 | if (matchedJobsIndex == -1) { 113 | // Job result likely arrived after it timedout locally 114 | debugInfo("Job response arrived too late: " + jobInfo.iid); 115 | return; 116 | } 117 | 118 | // Callback before removing the job such that if the callback creates new jobs 119 | // they will use the existing SQS queue 120 | var requestJobInfo = jobs[matchedJobsIndex]; 121 | requestJobInfo.errDataCallback(err, data); 122 | 123 | jobs.splice(matchedJobsIndex, 1); 124 | if (requestJobInfo.timeoutTimer) 125 | clearInterval(requestJobInfo.timeoutTimer); 126 | 127 | invokeWaitingJobs(); 128 | 129 | // We kill the SQS queue if there are no jobs in it. 130 | if (jobs.length == 0) { 131 | debugInfo("Removing SQS queue " + requestJobInfo.queueUrl); 132 | if (sqsRecv) 133 | sqsRecv.abort(); 134 | sqs.deleteQueue({QueueUrl: requestJobInfo.queueUrl}, function(err, data) {}); 135 | } 136 | 137 | // S3 request should be cleaned up too 138 | s3.deleteObject({Bucket: options.s3BucketPrefix + requestJobInfo.lambdaName, Key: requestJobInfo.iid + "-request.job"}, function(err, data) {}); 139 | } 140 | 141 | function waitForJobResponses(queueUrl) { 142 | var sqsRecv = sqs.receiveMessage({QueueUrl: queueUrl, WaitTimeSeconds: 20, MaxNumberOfMessages: 10}, function(err, data) { 143 | sqsRecv = null; 144 | 145 | if (err) { 146 | console.error("Failed to start receiving from SQS queue: " + err); 147 | debugInfo("Killing all jobs"); 148 | jobs.forEach(function(jobInfo) { 149 | processJobResponse(jobInfo, "Failed to listen on SQS Message queue: " + err, null); 150 | }); 151 | return; 152 | } 153 | 154 | data.Messages.forEach(function(message) { 155 | sqs.deleteMessage({QueueUrl: queueUrl, ReceiptHandle: message.ReceiptHandle}, function(err, data) {}); 156 | 157 | debugInfo("Response received:"); 158 | debugInfo(message); 159 | 160 | var responseJobInfo = JSON.parse(message.Body); 161 | processJobResponse(responseJobInfo, responseJobInfo.responseError, responseJobInfo.responseData); 162 | }); 163 | 164 | if (jobs.length > 0) 165 | waitForJobResponses(queueUrl); 166 | }); 167 | } 168 | 169 | function invokeTimeout(iid) { 170 | console.error("Timeout for job " + iid); 171 | 172 | var jobIndex = jobIndexFromiid(iid); 173 | if (jobIndex == -1) { 174 | console.error("Job not found? " + iid); 175 | return; 176 | } 177 | 178 | // Simulate a response, giving the error of having timed out 179 | var jobInfo = jobs[jobIndex]; 180 | processJobResponse(jobs[jobIndex], "Timed out waiting for Lambda response", null); 181 | } 182 | 183 | this.invoke = function(lambdaName, lambdaParams, errDataCallback) { 184 | var iid = getRandomId(); 185 | debugInfo("Will be invoking " + lambdaName + " (" + iid + ")"); 186 | 187 | var jobInfo = {iid: iid, lambdaName: lambdaName, params: lambdaParams, errDataCallback: errDataCallback}; 188 | jobInfo.queueUrl = null; 189 | jobs.push(jobInfo); 190 | 191 | // Do not invoke job yet if we're at the maximum amount of active jobs 192 | if (getTotalActiveJobs() >= options.maxActiveJobs) 193 | return; 194 | 195 | var queueUrl = getQueueUrl(); 196 | if (!queueUrl) { 197 | // The SQS queue hasn't already been created. It gets created on demand. 198 | // After the queue is created, it'll automatically scan the queue list for 199 | // pending or null queueUrls and do the invokes. 200 | jobInfo.queueUrl = "creating"; 201 | createQueue(); 202 | 203 | } else if (queueUrl == "creating") { 204 | // The SQS queue is still being created. As the job was pushed, it'll be 205 | // auto-invoked once the queue creation finishes. 206 | 207 | } else { 208 | // Jobs already running 209 | invokeJob(jobInfo, queueUrl); 210 | } 211 | } 212 | } 213 | 214 | ///////////////////////////// 215 | 216 | var LambdaJobWorker = function(AWS, jobReceivedCallback) { 217 | var util = require('util'); 218 | var jobInfo = null; 219 | 220 | this.lambdaHandler = function(event, context) { 221 | console.log("LambdaJob 1.0.0 by Larry Gadea (trivex@gmail.com)"); 222 | 223 | // Check this is a valid job that we can process and get the id. Since this lambda will 224 | // get called on ANY new objects in a bucket, it's possible the bucket is actually 225 | // multi-purpose and we should ignore the job 226 | var bucket, key, id; 227 | try { 228 | bucket = event.Records[0].s3.bucket.name; 229 | key = event.Records[0].s3.object.key; 230 | id = key.match(/(.+)\-request\.job/)[1]; 231 | } catch (e) { 232 | return console.error("Couldn't identify event as a LambdaJob: " + util.inspect(event, {showHidden: false, depth: null})); 233 | } 234 | console.log("Processing LambdaJob: " + id); 235 | 236 | // Though we have the ID, we need to get the actual job data from S3 237 | var s3 = new AWS.S3(); 238 | s3.getObject({Bucket: bucket, Key: key}, function(err, data) { 239 | if (err) return console.error("Failed to download job object: " + util.inspect(err, {showHidden: false, depth: null})); 240 | jobInfo = JSON.parse(data.Body.toString()); 241 | var queueUrl = jobInfo.queueUrl; 242 | 243 | // Call the user code with the job data. This is where the user would run things 244 | // like phantomjs, ffmpeg, imagemagick, etc. 245 | jobReceivedCallback(jobInfo.params, finishJob); 246 | }); 247 | } 248 | 249 | function finishJob(err, data) { 250 | console.log("Job has finished " + (err ? "with an error" : "successfully") + ": " + util.inspect(err || data, {showHidden: false, depth: null})); 251 | jobInfo.responseError = err; 252 | jobInfo.responseData = data; 253 | 254 | // No need to pass back the params since they could be large and will be useless 255 | delete jobInfo.params; 256 | 257 | // We use SQS to notify the client via SQS's push notification functionality. Note 258 | // that there's a max size of 256KB for data that's sent back. 259 | var sqs = new AWS.SQS(); 260 | sqs.sendMessage({QueueUrl: jobInfo.queueUrl, MessageBody: JSON.stringify(jobInfo)}, function(err, data) { 261 | if (err) { 262 | console.error("Failed to send completion SQS message: " + err); 263 | } else { 264 | console.log("Successfully sent response"); 265 | } 266 | 267 | console.log("LambdaJob completed!"); 268 | awsContext.done(null, null); 269 | }); 270 | } 271 | 272 | this.execHelper = function(cmd, errDataCallback) { 273 | var cp = require('child_process'); 274 | 275 | var output = ""; 276 | console.log("Running command: " + cmd); 277 | var process = cp.spawn('bash', ['-c', cmd], {}); 278 | 279 | process.stdout.on('data', function (data) { 280 | console.log("> " + data.toString()); 281 | output += data.toString(); 282 | }); 283 | process.stderr.on('data', function (data) { 284 | console.log("! " + data.toString()); 285 | output += data.toString(); 286 | }); 287 | process.on('close', function (code) { 288 | if (code != 0) { 289 | errDataCallback("Process returned code: " + code, output); 290 | } else { 291 | errDataCallback(null, output); 292 | } 293 | }); 294 | } 295 | } 296 | 297 | // To make this module not cause errors for the web browser 298 | if (typeof module !== 'undefined') 299 | module.exports.LambdaJobWorker = LambdaJobWorker; 300 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-job", 3 | "version": "1.0.0", 4 | "description": "Use AWS Lambda to run scalable server-side code from the web browser", 5 | "main": "./lib/lambda-job.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "aws", 11 | "lambda" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:lg/lambda-job.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/lg/lambda-job/issues" 19 | }, 20 | "author": "Larry Gadea", 21 | "license": "ISC", 22 | "dependencies": { 23 | "aws-sdk": "^2.1.4" 24 | } 25 | } 26 | --------------------------------------------------------------------------------