├── .DS_Store ├── .gitignore ├── .npmignore ├── README.md ├── bin └── cloudfront-image-proxy.ts ├── cdk.json ├── cost-estimation-medium.png ├── cost-estimation-original.png ├── jest.config.js ├── lib ├── cloudfront-image-proxy-stack.ts └── constructs │ ├── image-proxy.ts │ ├── lambda-at-edge.ts │ ├── origin-response │ ├── index.ts │ └── lambda │ │ ├── .gitignore │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ └── viewer-request │ ├── index.ts │ └── lambda │ ├── .gitignore │ ├── index.js │ ├── package-lock.json │ └── package.json ├── package.json ├── schema.png ├── test └── cloudfront-image-proxy.test.ts ├── tsconfig.json └── yarn.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skorfmann/cloudfront-image-proxy/2fa6b3a50e8d090607d543759e4b1ccbb38f3e5a/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | yarn-error.log 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | # Parcel build directories 11 | .cache 12 | .build 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CloudFront Image Proxy 2 | 3 | Make CloudFront resize images "on the fly" via lambda@edge, cache it and persists it in S3. Utilises [Sharp](https://github.com/lovell/sharp) for image transformations. 4 | 5 | ![Schema](./schema.png) 6 | 7 | Illustration & inspiration from [this blog post](https://aws.amazon.com/blogs/networking-and-content-delivery/resizing-images-with-amazon-cloudfront-lambdaedge-aws-cdn-blog/) 8 | 9 | ## Package & Deploy 10 | 11 | This makes use of the [AWS CDK](https://github.com/aws/aws-cdk) 12 | 13 | ``` 14 | yarn install 15 | cdk deploy 16 | ``` 17 | 18 | ## Usage 19 | 20 | Once deployed, upload an image (example.jpg) to the created S3 bucket in an `images` folder (`images/example.jpg`). Now you can request this image via the Cloudfront distribution domain `https://.cloudfront.net/images/example.jpg`, or generate other versions of it like that `https://.cloudfront.net/images/example.jpg?d=200x200`. 21 | 22 | At the moment, there's little sanity checking or error handling, and the only implemented image transformation option is the resizing. However, exposing more options of `sharp` is just a bunch of typing ;) Check the end of the Readme for more ideas. 23 | 24 | ## Cost Estimation 25 | 26 | It's not really straightforward to estimate the pricing. The [AWS Calculator](https://calculator.aws) is not helpful for the resources used here. A back of the napkin estimate for an example image taken from [unsplash](https://unsplash.com/photos/Z1tDa4nEUnM) (4899 × 3266, 1.8 MB): 27 | 28 | ![Cost Estimation Original](./cost-estimation-original.png) 29 | 30 | While data transfer from AWS origins to Cloudfront is free, data transfer to [Lambda@edge is not](https://aws.amazon.com/lambda/pricing/#Lambda.40Edge_Pricing) (unless in the same region). Therefore, the data transfer cost seems to be a significant part of the equation. I think it would make sense, to generate a few (small / medium / large) versions after the image upload, so that the data transfer is not that costly. See this example, where the same image got transformed to 1920x180 and just 250 KB: 31 | 32 | ![Cost Estimation Medium](./cost-estimation-medium.png) 33 | 34 | Interestingly enough, the processing time of the image conversion lambda isn't much different for both versions. The next improvement which could make sense, is either moving the Bucket in the region where most of the traffic is happening, or to create an API Gateway / Lambda combo to perform the actual image conversion wihin one region. 35 | 36 | ### SaaS Comparison 37 | 38 | Comparing this with a service like [imgix](https://www.imgix.com/): 39 | 40 | - 1000 original images access per month (all conversions included): 3 USD 41 | - 1 GB CDN data transfer: 0.08 USD 42 | 43 | Assuming we generate 10 versions for each original image, this Cloudfront proxy clocks in at around 0.85 USD for 10k generated images (inkl CDN traffic). Since CDN traffic cost for subsequently cached image requests is more or less the same for imigix and Cloudfront we can ignore this factor. 44 | 45 | The Cloudfront version runs at roughly 30% of the cost of imgix. However, since imgix is a SaaS offering, it's an unfair comparison and you should take engineering time and opertional burdens into account. Having said that, it's still interesting to see those numbers side by side. 46 | 47 | If you're interested in more details about the cost estimation, get in touch please. 48 | 49 | ## Lambda@Edge Gotchas 50 | 51 | - Functions have to reside in `us-east-1` 52 | - No environment variables 53 | - Viewer-* functions are limited to 5 seconds execution and 128 MB RAM [See here](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html#limits-lambda-at-edge) 54 | - Response-* functions mostly normal Lambda limits 55 | - Response-* response size is limited to 1 MB - (see for a potential solution for bigger images https://stackoverflow.com/questions/55064286/lambdaedge-gives-502-only-for-some-images) 56 | - CloudWatch logs appear at the closest option to the edge location of a request 57 | 58 | ## Further Ideas 59 | 60 | - [ ] Split lambda@edge functions into separate stack, so not all the resources have to be deployed to `us-east-1` 61 | - [ ] Unit / Integration tests 62 | - [ ] Cloudinary compatible URL param structure 63 | - [ ] Make the resizing more resilient (allow something like `x300` to scale one dimension dynamically) 64 | - [ ] Change File Structure in S3 to make it easy to delete files - something like images//300x200/ 65 | - [ ] Pre Generate low res images for large images to improve conversion speed (e.g. 6000x4000 could pre generate 3000x2000(high) / 1000x667(medium) / 500x226(small)) 66 | - [ ] Option for snap dimensions (401x303 => 400x300) 67 | - [ ] Expose more Sharp features 68 | - [ ] Client libraries 69 | - [ ] On-Demand external image source e.g. fetch original image from mydomain.com and persist it in S3 70 | - [ ] Add Kinsesis Firehose to aggregate all CloudWatch logs from functions across all edge locations 71 | - [ ] Multiple Tenants 72 | - [ ] Provide packages as [jsii packges](https://github.com/aws/jsii) for Python / C# / Java 73 | - [ ] Analytics / Dashbords / Reporting 74 | 75 | ## Similar projects 76 | 77 | ### Standalone Server 78 | 79 | - [Imaginary](https://github.com/h2non/imaginary) 80 | - [Imgproxy](https://github.com/imgproxy/imgproxy) 81 | - [Imageproxy](https://github.com/willnorris/imageproxy) 82 | 83 | ## AWS S3 Trigger 84 | 85 | - [Retinal](https://github.com/adieuadieu/retinal) 86 | 87 | ## AWS API Gateway 88 | 89 | - [imageResize](https://github.com/yapawa/imageResize) 90 | 91 | ## SaaS 92 | 93 | - [Cloudinary](https://cloudinary.com/) 94 | - [imgix](https://www.imgix.com/) 95 | -------------------------------------------------------------------------------- /bin/cloudfront-image-proxy.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from '@aws-cdk/core'; 4 | import { CloudfrontImageProxyStack } from '../lib/cloudfront-image-proxy-stack'; 5 | 6 | const app = new cdk.App(); 7 | // only valid option for lambda@edge is us-east-1 8 | new CloudfrontImageProxyStack(app, 'CloudfrontImageProxyStack', { env: { region: 'us-east-1'}}); 9 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node bin/cloudfront-image-proxy.ts", 3 | "context": { 4 | "@aws-cdk/core:enableStackNameDuplicates": "true", 5 | "aws-cdk:enableDiffNoFail": "true" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cost-estimation-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skorfmann/cloudfront-image-proxy/2fa6b3a50e8d090607d543759e4b1ccbb38f3e5a/cost-estimation-medium.png -------------------------------------------------------------------------------- /cost-estimation-original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skorfmann/cloudfront-image-proxy/2fa6b3a50e8d090607d543759e4b1ccbb38f3e5a/cost-estimation-original.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/test'], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /lib/cloudfront-image-proxy-stack.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as cdk from '@aws-cdk/core'; 3 | import * as s3 from '@aws-cdk/aws-s3' 4 | import { OriginResponse } from './constructs/origin-response' 5 | import { ViewerRequest } from './constructs/viewer-request' 6 | import { ImageProxy } from './constructs/image-proxy' 7 | 8 | export class CloudfrontImageProxyStack extends cdk.Stack { 9 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 10 | super(scope, id, props); 11 | 12 | const imageStore = new s3.Bucket(this, "ImageStore"); 13 | 14 | const originResponse = new OriginResponse(this, 'OriginResponse') 15 | imageStore.grantReadWrite(originResponse.lambdaAtEdge.handler) 16 | 17 | new ImageProxy(this, 'ImageProxy', { 18 | imageStore, 19 | lambdaAssociations: [ 20 | originResponse, 21 | new ViewerRequest(this, 'ViewerRequest'), 22 | ] 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/constructs/image-proxy.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as cdk from '@aws-cdk/core'; 3 | import * as cloudfront from '@aws-cdk/aws-cloudfront' 4 | import * as s3 from '@aws-cdk/aws-s3' 5 | import { LambdaAtEdgeHandler } from './lambda-at-edge' 6 | 7 | interface ImageProxyProps { 8 | imageStore: s3.Bucket; 9 | lambdaAssociations: LambdaAtEdgeHandler[]; 10 | } 11 | 12 | export class ImageProxy extends cdk.Construct { 13 | public readonly distribution: cloudfront.CloudFrontWebDistribution 14 | 15 | constructor(scope: cdk.Construct, id: string, props: ImageProxyProps) { 16 | super(scope, id); 17 | 18 | const { imageStore, lambdaAssociations } = props; 19 | 20 | // Origin access identity for cloudfront to access the bucket 21 | const identity = new cloudfront.OriginAccessIdentity(this, "Identity"); 22 | imageStore.grantRead(identity); 23 | 24 | // The CDN web distribution 25 | this.distribution = new cloudfront.CloudFrontWebDistribution(this, "Distribution", { 26 | loggingConfig: {}, 27 | originConfigs: [ 28 | { 29 | originHeaders: { 30 | // due to lack of ENV support in Lambda@edge, pass it as a header 31 | BUCKET_NAME: imageStore.bucketName 32 | }, 33 | s3OriginSource: { 34 | s3BucketSource: imageStore, 35 | originAccessIdentity: identity, 36 | }, 37 | behaviors: [ 38 | { 39 | defaultTtl: cdk.Duration.minutes(60), 40 | minTtl: cdk.Duration.minutes(60), 41 | maxTtl: cdk.Duration.days(365), 42 | forwardedValues: { 43 | queryString: true, 44 | queryStringCacheKeys: ['d'] 45 | }, 46 | isDefaultBehavior: true, 47 | lambdaFunctionAssociations: lambdaAssociations.map(association => (association.lambdaAtEdge.association)) 48 | } 49 | ] 50 | } 51 | ], 52 | }); 53 | 54 | new cdk.CfnOutput(this, 'ImageProxyUrl', { 55 | value: this.distribution.domainName 56 | }) 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /lib/constructs/lambda-at-edge.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from '@aws-cdk/core'; 2 | import * as lambda from '@aws-cdk/aws-lambda' 3 | import * as cloudfront from '@aws-cdk/aws-cloudfront' 4 | import * as iam from '@aws-cdk/aws-iam' 5 | 6 | export interface LambdaAtEdgeProps { 7 | eventType: cloudfront.LambdaEdgeEventType; 8 | handlerPath: string; 9 | memorySize?: number; 10 | timeout?: cdk.Duration; 11 | } 12 | 13 | export interface LambdaAtEdgeHandler { 14 | readonly lambdaAtEdge: LambdaAtEdge; 15 | } 16 | 17 | export class LambdaAtEdge extends cdk.Construct { 18 | public readonly association: cloudfront.LambdaFunctionAssociation 19 | public readonly handler: lambda.Function 20 | 21 | constructor(scope: cdk.Construct, id: string, props: LambdaAtEdgeProps) { 22 | super(scope, id); 23 | 24 | const { handlerPath, eventType, memorySize, timeout } = props; 25 | const runtime = lambda.Runtime.NODEJS_12_X 26 | 27 | this.handler = new lambda.Function(this, 'Handler', { 28 | code: lambda.Code.fromAsset(handlerPath, { 29 | bundling: { 30 | image: runtime.bundlingDockerImage, 31 | command: [ 32 | 'bash', '-c', [ 33 | `cp -R /asset-input/* /asset-output/`, 34 | `cd /asset-output`, 35 | `npm install` 36 | ].join(' && ') 37 | ], 38 | user: 'root' 39 | }, 40 | }), 41 | runtime, 42 | handler: "index.handler", 43 | memorySize: memorySize || 128, 44 | timeout: timeout || cdk.Duration.seconds(5), 45 | role: new iam.Role(this, 'AllowLambdaServiceToAssumeRole', { 46 | assumedBy: new iam.CompositePrincipal( 47 | new iam.ServicePrincipal('lambda.amazonaws.com'), 48 | new iam.ServicePrincipal('edgelambda.amazonaws.com'), 49 | ), 50 | managedPolicies: [iam.ManagedPolicy.fromManagedPolicyArn(this, 'Execution', 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole')] 51 | }) 52 | }); 53 | 54 | const hash = cdk.FileSystem.fingerprint(handlerPath, { 55 | exclude: ['node_modules'] 56 | }) 57 | 58 | const version = this.handler.addVersion(hash); 59 | 60 | this.association = { 61 | eventType, 62 | lambdaFunction: version 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /lib/constructs/origin-response/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as cdk from '@aws-cdk/core'; 3 | import * as cloudfront from '@aws-cdk/aws-cloudfront' 4 | import { LambdaAtEdge, LambdaAtEdgeHandler } from '../lambda-at-edge' 5 | import * as path from 'path' 6 | 7 | export class OriginResponse extends cdk.Construct implements LambdaAtEdgeHandler { 8 | public readonly lambdaAtEdge: LambdaAtEdge 9 | 10 | constructor(scope: cdk.Construct, id: string) { 11 | super(scope, id); 12 | 13 | this.lambdaAtEdge = new LambdaAtEdge(this, 'LambdaAtEdge', { 14 | eventType: cloudfront.LambdaEdgeEventType.ORIGIN_RESPONSE, 15 | handlerPath: path.join(__dirname, "lambda"), 16 | memorySize: 1024, 17 | timeout: cdk.Duration.seconds(15) 18 | }) 19 | } 20 | } -------------------------------------------------------------------------------- /lib/constructs/origin-response/lambda/.gitignore: -------------------------------------------------------------------------------- 1 | !*.js -------------------------------------------------------------------------------- /lib/constructs/origin-response/lambda/index.js: -------------------------------------------------------------------------------- 1 | const querystring = require("querystring"); 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | const AWS = require("aws-sdk"); 6 | const S3 = new AWS.S3({ 7 | signatureVersion: "v4" 8 | }); 9 | const Sharp = require("sharp"); 10 | 11 | exports.handler = (event, context, callback) => { 12 | let response = event.Records[0].cf.response; 13 | 14 | //check if image is not present 15 | if (response.status == 404) { 16 | let request = event.Records[0].cf.request; 17 | 18 | console.log('request', JSON.stringify(request)) 19 | 20 | const bucketName = request.headers.bucket_name[0].value 21 | 22 | let params = querystring.parse(request.querystring); 23 | 24 | // if there is no dimension attribute, just pass the response 25 | if (!params.d) { 26 | callback(null, response); 27 | return 28 | } 29 | 30 | // read the required path. Ex: uri /images/100x100/webp/image.jpg 31 | let path = request.uri; 32 | 33 | console.log({path}) 34 | // read the S3 key from the path variable. 35 | // Ex: path variable /images/100x100/webp/image.jpg 36 | let key = path.substring(1); 37 | console.log({ key }); 38 | 39 | // parse the prefix, width, height and image name 40 | // Ex: key=images/200x200/webp/image.jpg 41 | let prefix, originalKey, match, width, height, requiredFormat, imageName; 42 | let startIndex; 43 | 44 | try { 45 | match = key.match(/(.*)\/(\d+)x(\d+)\/(.*)\/(.*)/); 46 | console.log("match", JSON.stringify(match)) 47 | prefix = match[1]; 48 | width = parseInt(match[2], 10); 49 | height = parseInt(match[3], 10); 50 | 51 | // correction for jpg required for 'Sharp' 52 | requiredFormat = match[4] == "jpg" ? "jpeg" : match[4]; 53 | imageName = match[5]; 54 | originalKey = prefix + "/" + imageName; 55 | console.log('try', {originalKey}) 56 | } catch (err) { 57 | // no prefix exist for image.. 58 | console.log("no prefix present.."); 59 | match = key.match(/(\d+)x(\d+)\/(.*)\/(.*)/); 60 | width = parseInt(match[0], 10); 61 | height = parseInt(match[1], 10); 62 | 63 | // correction for jpg required for 'Sharp' 64 | requiredFormat = match[2] == "jpg" ? "jpeg" : match[3]; 65 | imageName = match[3]; 66 | originalKey = imageName; 67 | console.log("catch", { originalKey }); 68 | } 69 | 70 | console.log({bucketName}) 71 | 72 | // get the source image file 73 | S3.getObject({ Bucket: bucketName, Key: originalKey }) 74 | .promise() 75 | // perform the resize operation 76 | .then(data => Sharp(data.Body) 77 | .resize(width, height) 78 | .toFormat(requiredFormat) 79 | .toBuffer()) 80 | .then(buffer => { 81 | // save the resized object to S3 bucket with appropriate object key. 82 | S3.putObject({ 83 | Body: buffer, 84 | Bucket: bucketName, 85 | ContentType: "image/" + requiredFormat, 86 | CacheControl: "max-age=31536000", 87 | Key: key, 88 | StorageClass: "STANDARD" 89 | }) 90 | .promise() 91 | // even if there is exception in saving the object we send back the generated 92 | // image back to viewer below 93 | .catch(() => { 94 | console.log("Exception while writing resized image to bucket"); 95 | }); 96 | 97 | // generate a binary response with resized image 98 | // TODO: Check for image size and do a redirect (to self?) if it's larger than 1 MB 99 | response.status = 200; 100 | response.body = buffer.toString("base64"); 101 | response.bodyEncoding = "base64"; 102 | response.headers["content-type"] = [{ key: "Content-Type", value: "image/" + requiredFormat }]; 103 | callback(null, response); 104 | }) 105 | .catch(err => { 106 | console.log("Exception while reading source image :%j", err); 107 | }); 108 | } // end of if block checking response statusCode 109 | else { 110 | // allow the response to pass through 111 | callback(null, response); 112 | } 113 | }; 114 | 115 | 116 | -------------------------------------------------------------------------------- /lib/constructs/origin-response/lambda/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "origin-response", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ansi-regex": { 8 | "version": "2.1.1", 9 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 10 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 11 | }, 12 | "aproba": { 13 | "version": "1.2.0", 14 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", 15 | "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" 16 | }, 17 | "are-we-there-yet": { 18 | "version": "1.1.5", 19 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", 20 | "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", 21 | "requires": { 22 | "delegates": "^1.0.0", 23 | "readable-stream": "^2.0.6" 24 | } 25 | }, 26 | "base64-js": { 27 | "version": "1.3.1", 28 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 29 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" 30 | }, 31 | "bl": { 32 | "version": "4.0.2", 33 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", 34 | "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", 35 | "requires": { 36 | "buffer": "^5.5.0", 37 | "inherits": "^2.0.4", 38 | "readable-stream": "^3.4.0" 39 | }, 40 | "dependencies": { 41 | "readable-stream": { 42 | "version": "3.6.0", 43 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 44 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 45 | "requires": { 46 | "inherits": "^2.0.3", 47 | "string_decoder": "^1.1.1", 48 | "util-deprecate": "^1.0.1" 49 | } 50 | } 51 | } 52 | }, 53 | "buffer": { 54 | "version": "5.6.0", 55 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", 56 | "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", 57 | "requires": { 58 | "base64-js": "^1.0.2", 59 | "ieee754": "^1.1.4" 60 | } 61 | }, 62 | "chownr": { 63 | "version": "1.1.4", 64 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 65 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" 66 | }, 67 | "code-point-at": { 68 | "version": "1.1.0", 69 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 70 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" 71 | }, 72 | "color": { 73 | "version": "3.1.2", 74 | "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz", 75 | "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", 76 | "requires": { 77 | "color-convert": "^1.9.1", 78 | "color-string": "^1.5.2" 79 | } 80 | }, 81 | "color-convert": { 82 | "version": "1.9.3", 83 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 84 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 85 | "requires": { 86 | "color-name": "1.1.3" 87 | } 88 | }, 89 | "color-name": { 90 | "version": "1.1.3", 91 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 92 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 93 | }, 94 | "color-string": { 95 | "version": "1.5.3", 96 | "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", 97 | "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", 98 | "requires": { 99 | "color-name": "^1.0.0", 100 | "simple-swizzle": "^0.2.2" 101 | } 102 | }, 103 | "console-control-strings": { 104 | "version": "1.1.0", 105 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", 106 | "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" 107 | }, 108 | "core-util-is": { 109 | "version": "1.0.2", 110 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 111 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 112 | }, 113 | "decompress-response": { 114 | "version": "4.2.1", 115 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", 116 | "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", 117 | "requires": { 118 | "mimic-response": "^2.0.0" 119 | } 120 | }, 121 | "deep-extend": { 122 | "version": "0.6.0", 123 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 124 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 125 | }, 126 | "delegates": { 127 | "version": "1.0.0", 128 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 129 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 130 | }, 131 | "detect-libc": { 132 | "version": "1.0.3", 133 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", 134 | "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" 135 | }, 136 | "end-of-stream": { 137 | "version": "1.4.4", 138 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 139 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 140 | "requires": { 141 | "once": "^1.4.0" 142 | } 143 | }, 144 | "expand-template": { 145 | "version": "2.0.3", 146 | "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", 147 | "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" 148 | }, 149 | "fs-constants": { 150 | "version": "1.0.0", 151 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 152 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" 153 | }, 154 | "fs-minipass": { 155 | "version": "2.1.0", 156 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", 157 | "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", 158 | "requires": { 159 | "minipass": "^3.0.0" 160 | } 161 | }, 162 | "gauge": { 163 | "version": "2.7.4", 164 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", 165 | "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", 166 | "requires": { 167 | "aproba": "^1.0.3", 168 | "console-control-strings": "^1.0.0", 169 | "has-unicode": "^2.0.0", 170 | "object-assign": "^4.1.0", 171 | "signal-exit": "^3.0.0", 172 | "string-width": "^1.0.1", 173 | "strip-ansi": "^3.0.1", 174 | "wide-align": "^1.1.0" 175 | } 176 | }, 177 | "github-from-package": { 178 | "version": "0.0.0", 179 | "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", 180 | "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" 181 | }, 182 | "has-unicode": { 183 | "version": "2.0.1", 184 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", 185 | "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" 186 | }, 187 | "ieee754": { 188 | "version": "1.1.13", 189 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 190 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 191 | }, 192 | "inherits": { 193 | "version": "2.0.4", 194 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 195 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 196 | }, 197 | "ini": { 198 | "version": "1.3.5", 199 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", 200 | "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" 201 | }, 202 | "is-arrayish": { 203 | "version": "0.3.2", 204 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 205 | "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" 206 | }, 207 | "is-fullwidth-code-point": { 208 | "version": "1.0.0", 209 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 210 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 211 | "requires": { 212 | "number-is-nan": "^1.0.0" 213 | } 214 | }, 215 | "isarray": { 216 | "version": "1.0.0", 217 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 218 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 219 | }, 220 | "mimic-response": { 221 | "version": "2.1.0", 222 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", 223 | "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" 224 | }, 225 | "minimist": { 226 | "version": "1.2.5", 227 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 228 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 229 | }, 230 | "minipass": { 231 | "version": "3.1.3", 232 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", 233 | "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", 234 | "requires": { 235 | "yallist": "^4.0.0" 236 | } 237 | }, 238 | "minizlib": { 239 | "version": "2.1.0", 240 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.0.tgz", 241 | "integrity": "sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==", 242 | "requires": { 243 | "minipass": "^3.0.0", 244 | "yallist": "^4.0.0" 245 | } 246 | }, 247 | "mkdirp": { 248 | "version": "0.5.5", 249 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 250 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 251 | "requires": { 252 | "minimist": "^1.2.5" 253 | } 254 | }, 255 | "mkdirp-classic": { 256 | "version": "0.5.3", 257 | "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 258 | "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" 259 | }, 260 | "napi-build-utils": { 261 | "version": "1.0.2", 262 | "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", 263 | "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" 264 | }, 265 | "node-abi": { 266 | "version": "2.18.0", 267 | "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.18.0.tgz", 268 | "integrity": "sha512-yi05ZoiuNNEbyT/xXfSySZE+yVnQW6fxPZuFbLyS1s6b5Kw3HzV2PHOM4XR+nsjzkHxByK+2Wg+yCQbe35l8dw==", 269 | "requires": { 270 | "semver": "^5.4.1" 271 | }, 272 | "dependencies": { 273 | "semver": { 274 | "version": "5.7.1", 275 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 276 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 277 | } 278 | } 279 | }, 280 | "node-addon-api": { 281 | "version": "3.0.0", 282 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.0.tgz", 283 | "integrity": "sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg==" 284 | }, 285 | "noop-logger": { 286 | "version": "0.1.1", 287 | "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", 288 | "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" 289 | }, 290 | "npmlog": { 291 | "version": "4.1.2", 292 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", 293 | "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", 294 | "requires": { 295 | "are-we-there-yet": "~1.1.2", 296 | "console-control-strings": "~1.1.0", 297 | "gauge": "~2.7.3", 298 | "set-blocking": "~2.0.0" 299 | } 300 | }, 301 | "number-is-nan": { 302 | "version": "1.0.1", 303 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 304 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 305 | }, 306 | "object-assign": { 307 | "version": "4.1.1", 308 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 309 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 310 | }, 311 | "once": { 312 | "version": "1.4.0", 313 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 314 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 315 | "requires": { 316 | "wrappy": "1" 317 | } 318 | }, 319 | "prebuild-install": { 320 | "version": "5.3.4", 321 | "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.4.tgz", 322 | "integrity": "sha512-AkKN+pf4fSEihjapLEEj8n85YIw/tN6BQqkhzbDc0RvEZGdkpJBGMUYx66AAMcPG2KzmPQS7Cm16an4HVBRRMA==", 323 | "requires": { 324 | "detect-libc": "^1.0.3", 325 | "expand-template": "^2.0.3", 326 | "github-from-package": "0.0.0", 327 | "minimist": "^1.2.3", 328 | "mkdirp": "^0.5.1", 329 | "napi-build-utils": "^1.0.1", 330 | "node-abi": "^2.7.0", 331 | "noop-logger": "^0.1.1", 332 | "npmlog": "^4.0.1", 333 | "pump": "^3.0.0", 334 | "rc": "^1.2.7", 335 | "simple-get": "^3.0.3", 336 | "tar-fs": "^2.0.0", 337 | "tunnel-agent": "^0.6.0", 338 | "which-pm-runs": "^1.0.0" 339 | }, 340 | "dependencies": { 341 | "simple-get": { 342 | "version": "3.1.0", 343 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", 344 | "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", 345 | "requires": { 346 | "decompress-response": "^4.2.0", 347 | "once": "^1.3.1", 348 | "simple-concat": "^1.0.0" 349 | } 350 | } 351 | } 352 | }, 353 | "process-nextick-args": { 354 | "version": "2.0.1", 355 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 356 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 357 | }, 358 | "pump": { 359 | "version": "3.0.0", 360 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 361 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 362 | "requires": { 363 | "end-of-stream": "^1.1.0", 364 | "once": "^1.3.1" 365 | } 366 | }, 367 | "querystring": { 368 | "version": "0.2.0", 369 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 370 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 371 | }, 372 | "rc": { 373 | "version": "1.2.8", 374 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 375 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 376 | "requires": { 377 | "deep-extend": "^0.6.0", 378 | "ini": "~1.3.0", 379 | "minimist": "^1.2.0", 380 | "strip-json-comments": "~2.0.1" 381 | } 382 | }, 383 | "readable-stream": { 384 | "version": "2.3.7", 385 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 386 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 387 | "requires": { 388 | "core-util-is": "~1.0.0", 389 | "inherits": "~2.0.3", 390 | "isarray": "~1.0.0", 391 | "process-nextick-args": "~2.0.0", 392 | "safe-buffer": "~5.1.1", 393 | "string_decoder": "~1.1.1", 394 | "util-deprecate": "~1.0.1" 395 | } 396 | }, 397 | "safe-buffer": { 398 | "version": "5.1.2", 399 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 400 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 401 | }, 402 | "semver": { 403 | "version": "7.3.2", 404 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", 405 | "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" 406 | }, 407 | "set-blocking": { 408 | "version": "2.0.0", 409 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 410 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 411 | }, 412 | "sharp": { 413 | "version": "0.25.4", 414 | "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.25.4.tgz", 415 | "integrity": "sha512-umSzJJ1oBwIOfwFFt/fJ7JgCva9FvrEU2cbbm7u/3hSDZhXvkME8WE5qpaJqLIe2Har5msF5UG4CzYlEg5o3BQ==", 416 | "requires": { 417 | "color": "^3.1.2", 418 | "detect-libc": "^1.0.3", 419 | "node-addon-api": "^3.0.0", 420 | "npmlog": "^4.1.2", 421 | "prebuild-install": "^5.3.4", 422 | "semver": "^7.3.2", 423 | "simple-get": "^4.0.0", 424 | "tar": "^6.0.2", 425 | "tunnel-agent": "^0.6.0" 426 | } 427 | }, 428 | "signal-exit": { 429 | "version": "3.0.3", 430 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 431 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" 432 | }, 433 | "simple-concat": { 434 | "version": "1.0.0", 435 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", 436 | "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" 437 | }, 438 | "simple-get": { 439 | "version": "4.0.0", 440 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.0.tgz", 441 | "integrity": "sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ==", 442 | "requires": { 443 | "decompress-response": "^6.0.0", 444 | "once": "^1.3.1", 445 | "simple-concat": "^1.0.0" 446 | }, 447 | "dependencies": { 448 | "decompress-response": { 449 | "version": "6.0.0", 450 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 451 | "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 452 | "requires": { 453 | "mimic-response": "^3.1.0" 454 | } 455 | }, 456 | "mimic-response": { 457 | "version": "3.1.0", 458 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 459 | "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" 460 | } 461 | } 462 | }, 463 | "simple-swizzle": { 464 | "version": "0.2.2", 465 | "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 466 | "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", 467 | "requires": { 468 | "is-arrayish": "^0.3.1" 469 | } 470 | }, 471 | "string-width": { 472 | "version": "1.0.2", 473 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 474 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 475 | "requires": { 476 | "code-point-at": "^1.0.0", 477 | "is-fullwidth-code-point": "^1.0.0", 478 | "strip-ansi": "^3.0.0" 479 | } 480 | }, 481 | "string_decoder": { 482 | "version": "1.1.1", 483 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 484 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 485 | "requires": { 486 | "safe-buffer": "~5.1.0" 487 | } 488 | }, 489 | "strip-ansi": { 490 | "version": "3.0.1", 491 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 492 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 493 | "requires": { 494 | "ansi-regex": "^2.0.0" 495 | } 496 | }, 497 | "strip-json-comments": { 498 | "version": "2.0.1", 499 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 500 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" 501 | }, 502 | "tar": { 503 | "version": "6.0.2", 504 | "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.2.tgz", 505 | "integrity": "sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg==", 506 | "requires": { 507 | "chownr": "^2.0.0", 508 | "fs-minipass": "^2.0.0", 509 | "minipass": "^3.0.0", 510 | "minizlib": "^2.1.0", 511 | "mkdirp": "^1.0.3", 512 | "yallist": "^4.0.0" 513 | }, 514 | "dependencies": { 515 | "chownr": { 516 | "version": "2.0.0", 517 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", 518 | "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" 519 | }, 520 | "mkdirp": { 521 | "version": "1.0.4", 522 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", 523 | "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" 524 | } 525 | } 526 | }, 527 | "tar-fs": { 528 | "version": "2.1.0", 529 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.0.tgz", 530 | "integrity": "sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg==", 531 | "requires": { 532 | "chownr": "^1.1.1", 533 | "mkdirp-classic": "^0.5.2", 534 | "pump": "^3.0.0", 535 | "tar-stream": "^2.0.0" 536 | } 537 | }, 538 | "tar-stream": { 539 | "version": "2.1.2", 540 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz", 541 | "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==", 542 | "requires": { 543 | "bl": "^4.0.1", 544 | "end-of-stream": "^1.4.1", 545 | "fs-constants": "^1.0.0", 546 | "inherits": "^2.0.3", 547 | "readable-stream": "^3.1.1" 548 | }, 549 | "dependencies": { 550 | "readable-stream": { 551 | "version": "3.6.0", 552 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 553 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 554 | "requires": { 555 | "inherits": "^2.0.3", 556 | "string_decoder": "^1.1.1", 557 | "util-deprecate": "^1.0.1" 558 | } 559 | } 560 | } 561 | }, 562 | "tunnel-agent": { 563 | "version": "0.6.0", 564 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 565 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 566 | "requires": { 567 | "safe-buffer": "^5.0.1" 568 | } 569 | }, 570 | "util-deprecate": { 571 | "version": "1.0.2", 572 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 573 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 574 | }, 575 | "which-pm-runs": { 576 | "version": "1.0.0", 577 | "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", 578 | "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" 579 | }, 580 | "wide-align": { 581 | "version": "1.1.3", 582 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", 583 | "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", 584 | "requires": { 585 | "string-width": "^1.0.2 || 2" 586 | } 587 | }, 588 | "wrappy": { 589 | "version": "1.0.2", 590 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 591 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 592 | }, 593 | "yallist": { 594 | "version": "4.0.0", 595 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 596 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 597 | } 598 | } 599 | } 600 | -------------------------------------------------------------------------------- /lib/constructs/origin-response/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "origin-response", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": {}, 7 | "keywords": [], 8 | "author": "", 9 | "license": "ISC", 10 | "dependencies": { 11 | "querystring": "^0.2.0", 12 | "sharp": "^0.25.4" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/constructs/viewer-request/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as cdk from '@aws-cdk/core'; 3 | import * as cloudfront from '@aws-cdk/aws-cloudfront' 4 | import { LambdaAtEdge, LambdaAtEdgeHandler } from '../lambda-at-edge' 5 | import * as path from 'path' 6 | 7 | export class ViewerRequest extends cdk.Construct implements LambdaAtEdgeHandler { 8 | public readonly lambdaAtEdge: LambdaAtEdge 9 | 10 | constructor(scope: cdk.Construct, id: string) { 11 | super(scope, id); 12 | 13 | this.lambdaAtEdge = new LambdaAtEdge(this, 'LambdaAtEdge', { 14 | eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST, 15 | handlerPath: path.join(__dirname, "lambda"), 16 | memorySize: 128, 17 | // max 5 seconds 18 | timeout: cdk.Duration.seconds(1), 19 | }) 20 | } 21 | } -------------------------------------------------------------------------------- /lib/constructs/viewer-request/lambda/.gitignore: -------------------------------------------------------------------------------- 1 | !*.js -------------------------------------------------------------------------------- /lib/constructs/viewer-request/lambda/index.js: -------------------------------------------------------------------------------- 1 | const querystring = require("querystring"); 2 | 3 | const variables = { 4 | webpExtension: "webp" 5 | }; 6 | 7 | exports.handler = (event, context, callback) => { 8 | const request = event.Records[0].cf.request; 9 | const headers = request.headers; 10 | 11 | // parse the querystrings key-value pairs. In our case it would be d=100x100 12 | const params = querystring.parse(request.querystring); 13 | 14 | // fetch the uri of original image 15 | let fwdUri = request.uri; 16 | 17 | // if there is no dimension attribute, just pass the request 18 | if (!params.d) { 19 | callback(null, request); 20 | return; 21 | } 22 | // read the dimension parameter value = width x height and split it by 'x' 23 | const dimensionMatch = params.d.split("x"); 24 | 25 | // set the width and height parameters 26 | let width = dimensionMatch[0]; 27 | let height = dimensionMatch[1]; 28 | 29 | // parse the prefix, image name and extension from the uri. 30 | // In our case /images/image.jpg 31 | 32 | const match = fwdUri.match(/(.*)\/(.*)\.(.*)/); 33 | 34 | let prefix = match[1]; 35 | let imageName = match[2]; 36 | let extension = match[3]; 37 | 38 | // read the accept header to determine if webP is supported. 39 | let accept = headers["accept"] ? headers["accept"][0].value : ""; 40 | 41 | let url = []; 42 | // build the new uri to be forwarded upstream 43 | url.push(prefix); 44 | url.push(width + "x" + height); 45 | 46 | // check support for webp 47 | if (accept.includes(variables.webpExtension)) { 48 | url.push(variables.webpExtension); 49 | } else { 50 | url.push(extension); 51 | } 52 | url.push(imageName + "." + extension); 53 | 54 | fwdUri = url.join("/"); 55 | 56 | // final modified url is of format /images/200x200/webp/image.jpg 57 | request.uri = fwdUri; 58 | callback(null, request); 59 | }; -------------------------------------------------------------------------------- /lib/constructs/viewer-request/lambda/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "viewer-request", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "querystring": { 8 | "version": "0.2.0", 9 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 10 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/constructs/viewer-request/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "viewer-request", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "querystring": "^0.2.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudfront-image-proxy", 3 | "version": "0.1.0", 4 | "bin": { 5 | "cloudfront-image-proxy": "bin/cloudfront-image-proxy.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@aws-cdk/assert": "1.46.0", 15 | "@types/jest": "^25.2.1", 16 | "@types/node": "10.17.5", 17 | "aws-cdk": "1.46.0", 18 | "jest": "^25.5.0", 19 | "ts-jest": "^25.3.1", 20 | "ts-node": "^8.1.0", 21 | "typescript": "~3.7.2" 22 | }, 23 | "dependencies": { 24 | "@aws-cdk/aws-cloudfront": "^1.46.0", 25 | "@aws-cdk/aws-iam": "^1.46.0", 26 | "@aws-cdk/aws-lambda": "^1.46.0", 27 | "@aws-cdk/aws-s3": "^1.46.0", 28 | "@aws-cdk/core": "1.46.0", 29 | "source-map-support": "^0.5.16" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skorfmann/cloudfront-image-proxy/2fa6b3a50e8d090607d543759e4b1ccbb38f3e5a/schema.png -------------------------------------------------------------------------------- /test/cloudfront-image-proxy.test.ts: -------------------------------------------------------------------------------- 1 | import { expect as expectCDK, matchTemplate, MatchStyle } from '@aws-cdk/assert'; 2 | import * as cdk from '@aws-cdk/core'; 3 | import * as CloudfrontImageProxy from '../lib/cloudfront-image-proxy-stack'; 4 | 5 | test('Empty Stack', () => { 6 | const app = new cdk.App(); 7 | // WHEN 8 | const stack = new CloudfrontImageProxy.CloudfrontImageProxyStack(app, 'MyTestStack'); 9 | // THEN 10 | expectCDK(stack).to(matchTemplate({ 11 | "Resources": {} 12 | }, MatchStyle.EXACT)) 13 | }); 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["cdk.out"] 23 | } 24 | --------------------------------------------------------------------------------