@inizioevoke/aws-cdk-helpers 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.editorconfig ADDED
@@ -0,0 +1,12 @@
1
+ # EditorConfig is awesome: https://EditorConfig.org
2
+
3
+ # top-most EditorConfig file
4
+ root = true
5
+
6
+ [*]
7
+ indent_style = space
8
+ indent_size = 2
9
+ end_of_line = lf
10
+ charset = utf-8
11
+ trim_trailing_whitespace = false
12
+ insert_final_newline = false
@@ -0,0 +1,29 @@
1
+ import { RemovalPolicy } from "aws-cdk-lib";
2
+ import { Construct } from 'constructs';
3
+ import { Certificate, CertificateValidation } from "aws-cdk-lib/aws-certificatemanager";
4
+ import { IHostedZone } from "aws-cdk-lib/aws-route53";
5
+ import { tagResource } from "../lib/utils";
6
+
7
+ export interface SSLCertificateProps {
8
+ hostedZone: IHostedZone;
9
+ domainName: string;
10
+ destroy?: boolean;
11
+ tags?: Record<string, string>;
12
+ }
13
+
14
+ export class SSLCertificate extends Construct {
15
+ public readonly certificate: Certificate;
16
+
17
+ constructor(scope: Construct, id: string, props: SSLCertificateProps) {
18
+ super(scope, id);
19
+
20
+ this.certificate = new Certificate(this, `Certificate-${props.domainName}`, {
21
+ domainName: props.domainName,
22
+ validation: CertificateValidation.fromDns(props.hostedZone),
23
+ });
24
+ tagResource(this.certificate, props.tags);
25
+ if (typeof props.destroy == 'boolean') {
26
+ this.certificate.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,124 @@
1
+ import { resolve } from 'node:path';
2
+ import { readFileSync } from 'node:fs';
3
+ import { Buffer } from 'node:buffer';
4
+ import { RemovalPolicy } from "aws-cdk-lib";
5
+ import { Construct } from 'constructs';
6
+ import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
7
+ import { BehaviorOptions, Distribution, Function, FunctionCode, FunctionEventType, FunctionRuntime, IOrigin, PriceClass } from 'aws-cdk-lib/aws-cloudfront';
8
+ import { tagResource } from '../lib/utils';
9
+ import type { IResourceProps } from '../lib/types';
10
+
11
+ export interface CloudFrontDistributionProps extends IResourceProps {
12
+ domainName: string;
13
+ description?: string;
14
+ defaultBehavior: BehaviorOptions;
15
+ additionalBehaviors?: Record<string, BehaviorOptions>;
16
+ priceClass?: 'PRICE_CLASS_ALL' | 'PRICE_CLASS_100' | 'PRICE_CLASS_200' | 'NOT_PROD' | 'PROD';
17
+ certificate: ICertificate;
18
+ webAclId?: string;
19
+ }
20
+ export class CloudFrontDistribution extends Construct {
21
+ public readonly distribution: Distribution;
22
+
23
+ constructor(scope: Construct, id: string, props: CloudFrontDistributionProps) {
24
+ super(scope, id);
25
+
26
+ let priceClass = PriceClass.PRICE_CLASS_ALL;
27
+ switch (props.priceClass) {
28
+ case 'NOT_PROD':
29
+ case 'PRICE_CLASS_100':
30
+ priceClass = PriceClass.PRICE_CLASS_100;
31
+ break;
32
+ case 'PRICE_CLASS_200':
33
+ priceClass = PriceClass.PRICE_CLASS_200;
34
+ break;
35
+ }
36
+
37
+ this.distribution = new Distribution(this, `${id}|CloudFrontDistribution`, {
38
+ comment: props.description,
39
+ defaultBehavior: props.defaultBehavior,
40
+ additionalBehaviors: props.additionalBehaviors,
41
+ domainNames: [props.domainName],
42
+ certificate: props.certificate,
43
+ priceClass,
44
+ webAclId: props.webAclId
45
+ });
46
+ tagResource(this.distribution, props.tags);
47
+ if (typeof props.destroy == 'boolean') {
48
+ this.distribution.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
49
+ }
50
+ }
51
+ }
52
+
53
+ export type CloudFrontFunctionRedirectPaths = Record<string, string | [string, 301|302]>;
54
+
55
+ export interface CloudFrontFunctionRedirects {
56
+ trailingSlash?: boolean;
57
+ defaultDocument?: string;
58
+ redirectDefaultDocument?: boolean;
59
+ paths?: CloudFrontFunctionRedirectPaths;
60
+ }
61
+
62
+ export interface DefaultDocFunctionProps extends IResourceProps {
63
+ functionName?: string;
64
+ defaultDocument?: string;
65
+ redirects?: CloudFrontFunctionRedirects;
66
+ }
67
+
68
+ export interface BasicAuthDefaultDocFunctionProps extends DefaultDocFunctionProps {
69
+ user: string;
70
+ password: string;
71
+ }
72
+
73
+ export interface CloudFrontFunctionProps extends IResourceProps {
74
+ code: string;
75
+ functionName?: string;
76
+ runtime?: 'JS_1_0' | 'JS_2_0';
77
+ }
78
+ export class CloudFrontFunction extends Construct {
79
+ public readonly fn: Function;
80
+
81
+ constructor(scope: Construct, id: string, props: CloudFrontFunctionProps) {
82
+ super(scope, id);
83
+
84
+ this.fn = new Function(this, 'CloudFrontFunction', {
85
+ functionName: props.functionName,
86
+ runtime: FunctionRuntime[props.runtime ?? 'JS_2_0'],
87
+ code: FunctionCode.fromInline(props.code)
88
+ });
89
+ tagResource(this.fn, props.tags);
90
+
91
+ if (typeof props.destroy == 'boolean') {
92
+ this.fn.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
93
+ }
94
+ }
95
+
96
+ static createDefaultDoc(scope: Construct, id: string, props: DefaultDocFunctionProps) {
97
+ const code = readFileSync(resolve(__dirname, '../templates/cffn-default-doc.js'), 'utf8')
98
+ .replace('__DEFAULT_DOCUMENT__', props.defaultDocument ?? 'index.html')
99
+ .replace('__REDIRECTS__', props.redirects?.paths ? JSON.stringify(props.redirects?.paths ) : 'null')
100
+ .replace('__TRAILING_SLASH__', props.redirects?.trailingSlash?.toString() ?? 'null')
101
+ .replace('__REDIR_DEF_DOC__', props.redirects?.redirectDefaultDocument?.toString() ?? 'false');
102
+
103
+ return new CloudFrontFunction(scope, 'DefaultDocFunction', {
104
+ functionName: props.functionName,
105
+ runtime: 'JS_2_0',
106
+ code
107
+ });
108
+ }
109
+
110
+ static createBasicAuthDefaultDoc(scope: Construct, id: string, props: BasicAuthDefaultDocFunctionProps) {
111
+ const code = readFileSync(resolve(__dirname, '../templates/cffn-default-doc-basic-auth.js'), 'utf8')
112
+ .replace('__DEFAULT_DOCUMENT__', props.defaultDocument ?? 'index.html')
113
+ .replace('__CREDENTIALS__', Buffer.from(`${props.user}:${props.password}`).toString('base64'))
114
+ .replace('__REDIRECTS__', props.redirects?.paths ? JSON.stringify(props.redirects?.paths ) : 'null')
115
+ .replace('__TRAILING_SLASH__', props.redirects?.trailingSlash?.toString() ?? 'null')
116
+ .replace('__REDIR_DEF_DOC__', props.redirects?.redirectDefaultDocument?.toString() ?? 'false');
117
+
118
+ return new CloudFrontFunction(scope, 'BasicAuthDefaultDocFunction', {
119
+ functionName: props.functionName,
120
+ runtime: 'JS_2_0',
121
+ code
122
+ });
123
+ }
124
+ }
@@ -0,0 +1,102 @@
1
+ import { RemovalPolicy } from "aws-cdk-lib";
2
+ import { Construct } from 'constructs';
3
+ import { BuildEnvironmentVariableType, BuildSpec, ComputeType, IBuildImage, LinuxBuildImage, PipelineProject } from 'aws-cdk-lib/aws-codebuild';
4
+ import { Repository } from "aws-cdk-lib/aws-ecr";
5
+ import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
6
+ import { Bucket, IBucket } from 'aws-cdk-lib/aws-s3';
7
+
8
+ import { tagResource } from "../lib/utils";
9
+
10
+ export type EnvironmentVariables = Record<string, { value: any, type?: BuildEnvironmentVariableType }>;
11
+
12
+ export interface EcrBuildImage {
13
+ repository: string;
14
+ tag: string;
15
+ }
16
+ export interface CodeBuildPipelineProjectProps {
17
+ projectName?: string;
18
+ buildSpec?: string | object;
19
+ buildImage?: ('STANDARD_5_0' | 'STANDARD_6_0' | 'STANDARD_7_0') | IBuildImage;
20
+ ecrBuildImage?: EcrBuildImage,
21
+ computeType?: ('SMALL' | 'MEDIUM' | 'LARGE' | 'X_LARGE' | 'X2_LARGE') | string;
22
+ environmentVariables?: EnvironmentVariables;
23
+ cloudFrontDistributionArn?: string;
24
+ s3BucketName?: string;
25
+ destroy?: boolean;
26
+ tags?: Record<string, string>;
27
+ }
28
+
29
+ export class CodeBuildPipelineProject extends Construct {
30
+ public readonly project: PipelineProject;
31
+
32
+ constructor(scope: Construct, id: string, props: CodeBuildPipelineProjectProps) {
33
+ super(scope, id);
34
+
35
+ if (!props.environmentVariables) {
36
+ props.environmentVariables = {};
37
+ }
38
+ if (props.cloudFrontDistributionArn && !props.environmentVariables.CLOUDFRONT_DISTRIBUTION_ID) {
39
+ props.environmentVariables.CLOUDFRONT_DISTRIBUTION_ID = {
40
+ value: props.cloudFrontDistributionArn.split('/').pop()
41
+ };
42
+ }
43
+
44
+ if (props.s3BucketName && !props.environmentVariables.S3_BUCKET_NAME) {
45
+ props.environmentVariables.S3_BUCKET_NAME = {
46
+ value: props.s3BucketName
47
+ };
48
+ }
49
+
50
+ let buildImage: IBuildImage | undefined = undefined;
51
+ if (props.ecrBuildImage) {
52
+ const repository = props.ecrBuildImage.repository.startsWith('arn:aws') ?
53
+ Repository.fromRepositoryArn(this, 'EcrRepository', props.ecrBuildImage.repository) :
54
+ Repository.fromRepositoryName(this, 'EcrRepository', props.ecrBuildImage.repository);
55
+ buildImage = LinuxBuildImage.fromEcrRepository(repository, props.ecrBuildImage.tag);
56
+ } else if (props.buildImage) {
57
+ if (typeof props.buildImage == 'string') {
58
+ buildImage = LinuxBuildImage[props.buildImage ?? 'STANDARD_7_0']
59
+ } else {
60
+ buildImage = props.buildImage as IBuildImage;
61
+ }
62
+ }
63
+
64
+ this.project = new PipelineProject(this, 'CodeBuildPipelineProjectProps', {
65
+ projectName: props.projectName,
66
+ buildSpec: props.buildSpec ? typeof props.buildSpec == 'object' ? BuildSpec.fromObjectToYaml(props.buildSpec) : BuildSpec.fromSourceFilename(props.buildSpec ?? 'buildspec.yml') : undefined,
67
+ environment: {
68
+ buildImage: buildImage ?? LinuxBuildImage.STANDARD_7_0,
69
+ computeType: ComputeType[props.computeType as (keyof typeof ComputeType | undefined) ?? 'SMALL'],
70
+ environmentVariables: props.environmentVariables
71
+ }
72
+ });
73
+ tagResource(this.project, props.tags);
74
+ if (typeof props.destroy == 'boolean') {
75
+ this.project.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
76
+ }
77
+
78
+ // Add CloudWatch log permissions
79
+ this.project.addToRolePolicy(new PolicyStatement({
80
+ actions: [
81
+ 'logs:CreateLogGroup',
82
+ 'logs:CreateLogStream',
83
+ 'logs:PutLogEvents'
84
+ ],
85
+ resources: [this.project.projectArn]
86
+ }));
87
+
88
+ // Allow the project to create an invalidation
89
+ if (props.cloudFrontDistributionArn) {
90
+ this.project.addToRolePolicy(new PolicyStatement({
91
+ actions: ['cloudfront:CreateInvalidation'],
92
+ resources: [props.cloudFrontDistributionArn]
93
+ }));
94
+ }
95
+
96
+ // Allow the project to sync files to S3
97
+ if (props.s3BucketName) {
98
+ const s3Bucket = Bucket.fromBucketName(this, 'S3BucketByName', props.s3BucketName);
99
+ s3Bucket.grantReadWrite(this.project);
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,86 @@
1
+ import { RemovalPolicy, PhysicalName } from "aws-cdk-lib";
2
+ import { Construct } from 'constructs';
3
+ import { IProject } from "aws-cdk-lib/aws-codebuild";
4
+ import { Artifact, Pipeline } from "aws-cdk-lib/aws-codepipeline";
5
+ import { CodeBuildAction, CodeStarConnectionsSourceAction } from "aws-cdk-lib/aws-codepipeline-actions";
6
+ import { Bucket } from 'aws-cdk-lib/aws-s3';
7
+
8
+ import type { EnvironmentVariables } from './codebuild';
9
+ import { tagResource } from "../lib/utils";
10
+
11
+ export interface AddSourceStageParams {
12
+ stageName?: string;
13
+ codestarConnectionArn: string;
14
+ repoOwner: string;
15
+ repo: string;
16
+ branch: string;
17
+ }
18
+ export interface AddBuildStageParams {
19
+ stageName?: string;
20
+ codeBuildProject: IProject;
21
+ sourceArtifact: Artifact;
22
+ environmentVariables?: EnvironmentVariables;
23
+ }
24
+
25
+ export interface CodePipelineProjectProps {
26
+ pipelineName?: string;
27
+ destroy?: boolean;
28
+ tags?: Record<string, string>;
29
+ }
30
+ export class CodePipelineProject extends Construct {
31
+ public readonly s3Bucket: Bucket;
32
+ public readonly pipeline: Pipeline;
33
+
34
+ constructor(scope: Construct, id: string, props: CodePipelineProjectProps) {
35
+ super(scope, id);
36
+
37
+ this.s3Bucket = new Bucket(this, 'CodePipelineArtifactBucket', {
38
+ bucketName: PhysicalName.GENERATE_IF_NEEDED,
39
+ removalPolicy: props.destroy !== undefined ? props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN : undefined,
40
+ autoDeleteObjects: props.destroy === true ? true : undefined
41
+ });
42
+ tagResource(this.s3Bucket, props.tags);
43
+
44
+ this.pipeline = new Pipeline(this, 'CodePipelineProject', {
45
+ pipelineName: props.pipelineName,
46
+ artifactBucket: this.s3Bucket
47
+ });
48
+
49
+ tagResource(this.pipeline, props.tags);
50
+ if (typeof props.destroy == 'boolean') {
51
+ this.pipeline.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
52
+ }
53
+ }
54
+
55
+ addSourceStage(params: AddSourceStageParams): Artifact {
56
+ const artifact = new Artifact();
57
+ this.pipeline.addStage({
58
+ stageName: params.stageName ?? 'Source',
59
+ actions: [
60
+ new CodeStarConnectionsSourceAction({
61
+ actionName: params.stageName ?? 'Source',
62
+ output: artifact,
63
+ connectionArn: params.codestarConnectionArn,
64
+ owner: params.repoOwner,
65
+ repo: params.repo,
66
+ branch: params.branch
67
+ })
68
+ ]
69
+ });
70
+ return artifact;
71
+ }
72
+
73
+ addBuildStage(params: AddBuildStageParams) {
74
+ this.pipeline.addStage({
75
+ stageName: params.stageName ?? 'Build',
76
+ actions: [
77
+ new CodeBuildAction({
78
+ actionName: params.stageName ?? 'Build',
79
+ project: params.codeBuildProject,
80
+ input: params.sourceArtifact,
81
+ environmentVariables: params.environmentVariables
82
+ })
83
+ ]
84
+ });
85
+ }
86
+ }
@@ -0,0 +1,34 @@
1
+ import { IDistribution } from "aws-cdk-lib/aws-cloudfront";
2
+ import { ARecord, AaaaRecord, HostedZone, IHostedZone, RecordTarget } from "aws-cdk-lib/aws-route53";
3
+ import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets";
4
+ import { Construct } from "constructs";
5
+
6
+ export interface CloudFrontAliasProps {
7
+ hostedZone: IHostedZone;
8
+ domainName: string;
9
+ distribution: IDistribution;
10
+ }
11
+ export class CloudFrontAlias extends Construct {
12
+ public readonly aRecord: ARecord;
13
+ public readonly aaaaRecord: AaaaRecord;
14
+
15
+ constructor(scope: Construct, id: string, props: CloudFrontAliasProps) {
16
+ super(scope, id);
17
+
18
+ this.aRecord = new ARecord(this, `ARecord-${props.domainName}`, {
19
+ zone: props.hostedZone,
20
+ target: RecordTarget.fromAlias(new CloudFrontTarget(props.distribution)),
21
+ recordName: props.domainName
22
+ });
23
+
24
+ this.aaaaRecord = new AaaaRecord(this, `AaaaRecord-${props.domainName}`, {
25
+ zone: props.hostedZone,
26
+ target: RecordTarget.fromAlias(new CloudFrontTarget(props.distribution)),
27
+ recordName: props.domainName
28
+ });
29
+ }
30
+ }
31
+
32
+ export function getHostedZone(scope: Construct, id: string, domainName: string) {
33
+ return HostedZone.fromLookup(scope, `getHostedZone-${domainName}`, { domainName });
34
+ }
@@ -0,0 +1,26 @@
1
+ import { RemovalPolicy } from "aws-cdk-lib";
2
+ import { Construct } from 'constructs';
3
+ import { Bucket } from 'aws-cdk-lib/aws-s3';
4
+ import { tagResource } from '../lib/utils';
5
+
6
+ export interface S3BucketProps {
7
+ /** The name of the S3 bucket */
8
+ bucketName?: string;
9
+ destroy?: boolean;
10
+ tags?: Record<string, string>;
11
+ }
12
+
13
+ export class S3Bucket extends Construct {
14
+ public readonly bucket: Bucket;
15
+
16
+ constructor(scope: Construct, id: string, props: S3BucketProps) {
17
+ super(scope, id);
18
+
19
+ this.bucket = new Bucket(this, 'S3Bucket', {
20
+ bucketName: props.bucketName,
21
+ removalPolicy: props.destroy !== undefined ? props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN : undefined,
22
+ autoDeleteObjects: props.destroy === true ? true : undefined
23
+ });
24
+ tagResource(this.bucket, props.tags);
25
+ }
26
+ }
@@ -0,0 +1,113 @@
1
+ import { RemovalPolicy } from "aws-cdk-lib";
2
+ import { Construct } from 'constructs';
3
+ import { CfnWebACL } from 'aws-cdk-lib/aws-wafv2';
4
+ import { cleanResourceName, tagResource } from '../lib/utils';
5
+ import type { IResourceProps } from '../lib/types';
6
+
7
+ export type WebAclManagedRule =
8
+ 'AWSManagedRulesAmazonIpReputationList' |
9
+ 'AWSManagedRulesAnonymousIpList' |
10
+ 'AWSManagedRulesCommonRuleSet' |
11
+ 'AWSManagedRulesKnownBadInputsRuleSet' |
12
+ 'AWSManagedRulesSQLiRuleSet';
13
+
14
+ export interface CloudFrontWebAclProps extends IResourceProps {
15
+ name: string;
16
+ managedRules?: (WebAclManagedRule & string)[];
17
+ }
18
+ export class CloudFrontWebAcl extends Construct {
19
+ public readonly acl: CfnWebACL;
20
+
21
+ constructor(scope: Construct, id: string, props: CloudFrontWebAclProps) {
22
+ super(scope, id);
23
+
24
+ props.name = cleanResourceName(props.name);
25
+
26
+ if (!props.managedRules) {
27
+ props.managedRules = [
28
+ 'AWSManagedRulesAmazonIpReputationList',
29
+ 'AWSManagedRulesAnonymousIpList',
30
+ 'AWSManagedRulesCommonRuleSet',
31
+ 'AWSManagedRulesKnownBadInputsRuleSet'
32
+ ];
33
+ }
34
+
35
+ this.acl = new CfnWebACL(this, 'CloudFrontWebAcl', {
36
+ scope: 'CLOUDFRONT',
37
+ name: props.name,
38
+ defaultAction: { allow: {} },
39
+ visibilityConfig: {
40
+ cloudWatchMetricsEnabled: true,
41
+ sampledRequestsEnabled: true,
42
+ metricName: `${props.name}-metrics`
43
+ },
44
+ rules: transformManagedRules(props.managedRules)
45
+ });
46
+ tagResource(this.acl, props.tags);
47
+ if (typeof props.destroy == 'boolean') {
48
+ this.acl.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
49
+ }
50
+ }
51
+ }
52
+
53
+ export interface ApiGatewayWebAclProps extends IResourceProps {
54
+ name: string;
55
+ aclScope?: 'CLOUDFRONT' | 'REGIONAL';
56
+ managedRules?: (WebAclManagedRule & string)[];
57
+ }
58
+ export class ApiGatewayWebAcl extends Construct {
59
+ public readonly acl: CfnWebACL;
60
+ constructor(scope: Construct, id: string, props: ApiGatewayWebAclProps) {
61
+ super(scope, id);
62
+
63
+ props.name = cleanResourceName(props.name);
64
+
65
+ if (!props.managedRules) {
66
+ props.managedRules = [
67
+ 'AWSManagedRulesAmazonIpReputationList',
68
+ 'AWSManagedRulesAnonymousIpList',
69
+ 'AWSManagedRulesCommonRuleSet',
70
+ 'AWSManagedRulesKnownBadInputsRuleSet',
71
+ 'AWSManagedRulesSQLiRuleSet'
72
+ ];
73
+ }
74
+
75
+ this.acl = new CfnWebACL(this, 'ApiGatewayWebAcl', {
76
+ scope: props.aclScope ?? 'REGIONAL',
77
+ name: props.name,
78
+ defaultAction: { allow: {} },
79
+ visibilityConfig: {
80
+ cloudWatchMetricsEnabled: true,
81
+ sampledRequestsEnabled: true,
82
+ metricName: `${props.name}-metrics`
83
+ },
84
+ rules: transformManagedRules(props.managedRules)
85
+ });
86
+ tagResource(this.acl, props.tags);
87
+ if (typeof props.destroy == 'boolean') {
88
+ this.acl.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
89
+ }
90
+ }
91
+ }
92
+
93
+
94
+ function transformManagedRules(rules: (WebAclManagedRule | string)[]): CfnWebACL.RuleProperty[] {
95
+ return rules.map((ruleName, i) => {
96
+ return {
97
+ name: `AWS-${ruleName}`,
98
+ priority: i,
99
+ statement: {
100
+ managedRuleGroupStatement: {
101
+ name: ruleName,
102
+ vendorName: 'AWS'
103
+ }
104
+ },
105
+ overrideAction: { none: {} },
106
+ visibilityConfig: {
107
+ cloudWatchMetricsEnabled: true,
108
+ sampledRequestsEnabled: true,
109
+ metricName: `AWS-${ruleName}`
110
+ }
111
+ } as CfnWebACL.RuleProperty;
112
+ });
113
+ }
package/lib/types.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ export type DeepPartial<T> = T extends object ? {
2
+ [P in keyof T]?: DeepPartial<T[P]>;
3
+ } : T;
4
+
5
+ export type WithRequiredProperty<Type, Key extends keyof Type> = Omit<Type, Key> & {
6
+ [Property in Key]-?: Type[Property];
7
+ };
8
+
9
+ export interface IResourceProps {
10
+ destroy?: boolean;
11
+ tags?: Record<string, string>;
12
+ }
package/lib/utils.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { IConstruct } from 'constructs';
2
+ import { Tags } from 'aws-cdk-lib/core';
3
+
4
+ export function domainAsId(domain: string) {
5
+ return domain.replace(/\./g, '-');
6
+ }
7
+
8
+ export function cleanResourceName(name: string) {
9
+ return name.replace(/[^0-9a-z_\-]/gi, '');
10
+ }
11
+
12
+ export function tagResource(resource: IConstruct, tags?: Record<string, string>) {
13
+ if (tags) {
14
+ for (const [key, value] of Object.entries(tags)) {
15
+ Tags.of(resource).add(key, value);
16
+ }
17
+ }
18
+ }
19
+
20
+ export function mergeTags(...tags: (Record<string, string> | null | undefined)[]): Record<string, string> {
21
+ const _tags = tags.filter(t => t !== undefined && t !== null) ?? [];
22
+ return Object.assign({}, ..._tags);
23
+ }
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@inizioevoke/aws-cdk-helpers",
3
+ "version": "1.0.0",
4
+ "author": "",
5
+ "license": "ISC",
6
+ "description": "",
7
+ "devDependencies": {
8
+ "@types/node": "^25.3.0",
9
+ "aws-cdk-lib": "^2.238.0",
10
+ "constructs": "^10.5.1",
11
+ "typescript": "^5.9.3"
12
+ }
13
+ }
package/readme.md ADDED
@@ -0,0 +1,80 @@
1
+ # @inizioevoke/aws-cdk-helpers
2
+
3
+ ## Example App
4
+
5
+ ```typescript
6
+ import * as cdk from 'aws-cdk-lib/core';
7
+ import { CloudFrontFunctionRedirects } from '@inizioevoke/aws-cdk-helpers/constructs/cloudfront';
8
+ import { WebStaticServerlessStage } from '@inizioevoke/aws-cdk-helpers/stages/web-static-serverless-stage';
9
+ import { AddSourceStageParams } from '@inizioevoke/aws-cdk-helpers/constructs/codepipeline';
10
+
11
+ const app = new cdk.App();
12
+
13
+ const env = {
14
+ account: '000000000000',
15
+ region: 'us-east-1'
16
+ };
17
+
18
+ const tags: Record<string, string> = {
19
+ 'Account': 'Account Name',
20
+ 'Brand': 'Brand Name'
21
+ };
22
+
23
+ const repository: Omit<AddSourceStageParams, 'branch'> = {
24
+ codestarConnectionArn: 'ARN',
25
+ repoOwner: 'evokegroup',
26
+ repo: 'myproject-v1'
27
+ };
28
+
29
+ const redirects: CloudFrontFunctionRedirects = {
30
+ paths: {
31
+ '/hello': '/world',
32
+ '/redirect': ['/here', 302]
33
+ }
34
+ };
35
+
36
+ new WebStaticServerlessStage(app, 'dev', {
37
+ env,
38
+ tags,
39
+ stageName: 'myproject-v1-dev',
40
+ envType: 'NOT_PROD',
41
+ description: 'My Project DEV',
42
+ hostedZone: 'evodev.net',
43
+ domainName: 'myproject-v1.evodev.net',
44
+ basicAuth: {
45
+ user: 'user',
46
+ password: 'password'
47
+ },
48
+ redirects,
49
+ pipeline: {
50
+ sourceStage: {
51
+ ...repository,
52
+ branch: 'develop'
53
+ }
54
+ }
55
+ });
56
+
57
+ new WebStaticServerlessStage(app, 'prd', {
58
+ env,
59
+ tags,
60
+ stageName: 'myproject-v1-prd',
61
+ envType: 'PROD',
62
+ description: 'My Project PRD',
63
+ hostedZone: 'myproject.com',
64
+ domainName: 'www.myproject.com',
65
+ attachWebAcl: true,
66
+ redirects,
67
+ pipeline: {
68
+ sourceStage: {
69
+ ...repository,
70
+ branch: 'prod'
71
+ }
72
+ }
73
+ });
74
+ ```
75
+ ```
76
+ cdk deploy dev/*
77
+ ```
78
+ ```
79
+ cdk deploy prd/*
80
+ ```
package/samples/app.ts ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import * as cdk from 'aws-cdk-lib/core';
3
+ import { CloudFrontFunctionRedirects } from '../constructs/cloudfront'; // @inizioevoke/aws-cdk-helpers
4
+ import { WebStaticServerlessStage } from '../stages/web-static-serverless-stage'; // @inizioevoke/aws-cdk-helpers
5
+ import { AddSourceStageParams } from '../constructs/codepipeline'; // @inizioevoke/aws-cdk-helpers
6
+
7
+ const app = new cdk.App();
8
+
9
+ const env = {
10
+ account: '000000000000',
11
+ region: 'us-east-1'
12
+ };
13
+
14
+ const tags: Record<string, string> = {
15
+ 'Account': 'Account Name',
16
+ 'Brand': 'Brand Name'
17
+ };
18
+
19
+ const repository: Omit<AddSourceStageParams, 'branch'> = {
20
+ codestarConnectionArn: 'ARN',
21
+ repoOwner: 'evokegroup',
22
+ repo: 'myproject-v1'
23
+ };
24
+
25
+ const redirects: CloudFrontFunctionRedirects = {
26
+ paths: {
27
+ '/hello': '/world',
28
+ '/redirect': ['/here', 302]
29
+ }
30
+ };
31
+
32
+ new WebStaticServerlessStage(app, 'dev', {
33
+ env,
34
+ tags,
35
+ stageName: 'myproject-v1-dev',
36
+ envType: 'NOT_PROD',
37
+ description: 'My Project DEV',
38
+ hostedZone: 'evodev.net',
39
+ domainName: 'myproject-v1.evodev.net',
40
+ basicAuth: {
41
+ user: 'user',
42
+ password: 'password'
43
+ },
44
+ redirects,
45
+ pipeline: {
46
+ sourceStage: {
47
+ ...repository,
48
+ branch: 'develop'
49
+ }
50
+ }
51
+ });
52
+
53
+ new WebStaticServerlessStage(app, 'prd', {
54
+ env,
55
+ tags,
56
+ stageName: 'myproject-v1-prd',
57
+ envType: 'PROD',
58
+ description: 'My Project PRD',
59
+ hostedZone: 'myproject.com',
60
+ domainName: 'www.myproject.com',
61
+ attachWebAcl: true,
62
+ redirects,
63
+ pipeline: {
64
+ sourceStage: {
65
+ ...repository,
66
+ branch: 'prod'
67
+ }
68
+ }
69
+ });
@@ -0,0 +1,24 @@
1
+ version: 0.2
2
+
3
+ env:
4
+ variables:
5
+ # Environment variables
6
+ S3_BUCKET_NAME:
7
+ CLOUDFRONT_DISTRIBUTION_ID:
8
+
9
+ phases:
10
+ install:
11
+ runtime-versions:
12
+ nodejs: 24
13
+ commands:
14
+ - npm install
15
+ build:
16
+ commands:
17
+ - npm run build
18
+ post_build:
19
+ commands:
20
+ - '[ ${CODEBUILD_BUILD_SUCCEEDING:-0} -eq 1 ] || exit 1'
21
+ - echo "Syncing to S3"
22
+ - aws s3 sync dist s3://${S3_BUCKET_NAME}/ --delete
23
+ - echo "Invalidating CloudFront"
24
+ - aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_DISTRIBUTION_ID} --paths "/*"
@@ -0,0 +1,42 @@
1
+ import { Stack, StackProps } from 'aws-cdk-lib/core';
2
+ import { Construct } from 'constructs';
3
+ import { CodeBuildPipelineProject, CodeBuildPipelineProjectProps } from '../../constructs/codebuild';
4
+ import { CodePipelineProject, AddSourceStageParams, AddBuildStageParams } from '../../constructs/codepipeline';
5
+
6
+ export interface CodePipelineStackProps {
7
+ pipelineName?: string;
8
+ sourceStage: AddSourceStageParams;
9
+ buildStage?: Pick<AddBuildStageParams, 'stageName' | 'environmentVariables'>;
10
+ codeBuildProject?: Omit<CodeBuildPipelineProjectProps, 'destroy' | 'tags'>,
11
+ destroy?: boolean;
12
+ tags?: Record<string, string>;
13
+ }
14
+ export class CodePipelineStack extends Stack {
15
+ public readonly codeBuildPipelineProject: CodeBuildPipelineProject;
16
+ public readonly codePipelineProject: CodePipelineProject;
17
+
18
+ constructor(scope: Construct, id: string, props: CodePipelineStackProps & StackProps) {
19
+ super(scope, id, props);
20
+
21
+ this.codeBuildPipelineProject = new CodeBuildPipelineProject(this, 'CodeBuildPipelineProject', {
22
+ ...(props.codeBuildProject ?? {}),
23
+ destroy: props.destroy,
24
+ tags: props.tags
25
+ });
26
+
27
+ this.codePipelineProject = new CodePipelineProject(this, 'CodePipelineProject', {
28
+ pipelineName: props.pipelineName,
29
+ destroy: props.destroy,
30
+ tags: props.tags
31
+ });
32
+
33
+ const sourceArtifact = this.codePipelineProject.addSourceStage(props.sourceStage);
34
+
35
+ this.codePipelineProject.addBuildStage({
36
+ stageName: props.buildStage?.stageName,
37
+ codeBuildProject: this.codeBuildPipelineProject.project,
38
+ sourceArtifact: sourceArtifact,
39
+ environmentVariables: props.buildStage?.environmentVariables
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,119 @@
1
+ import { Stack, StackProps, PhysicalName } from 'aws-cdk-lib/core';
2
+ import { Construct } from 'constructs';
3
+
4
+ import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
5
+ import { BehaviorOptions, Distribution, Function, FunctionEventType } from 'aws-cdk-lib/aws-cloudfront';
6
+ import { ARecord, AaaaRecord } from "aws-cdk-lib/aws-route53";
7
+ import { Bucket } from "aws-cdk-lib/aws-s3";
8
+ import { CfnWebACL } from 'aws-cdk-lib/aws-wafv2';
9
+
10
+ import { S3BucketOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
11
+ import { SSLCertificate } from '../../constructs/certificatemanager';
12
+ import { CloudFrontFunctionRedirects, CloudFrontFunction, CloudFrontDistribution } from '../../constructs/cloudfront';
13
+ import { getHostedZone, CloudFrontAlias } from '../../constructs/route53';
14
+ import { S3Bucket } from '../../constructs/s3';
15
+ import { CloudFrontWebAcl } from '../../constructs/waf';
16
+ import { domainAsId } from '../../lib/utils';
17
+
18
+ export interface BasicAuthCredentials {
19
+ user: string;
20
+ password: string;
21
+ }
22
+
23
+ export interface WebGlobalStackProps {
24
+ envType: 'NOT_PROD' | 'PROD';
25
+ description?: string;
26
+ bucketName?: string;
27
+ hostedZone: string;
28
+ domainName: string;
29
+ basicAuth?: BasicAuthCredentials;
30
+ redirects?: CloudFrontFunctionRedirects;
31
+ attachWebAcl?: boolean;
32
+ destroy?: boolean;
33
+ }
34
+
35
+ export class WebGlobalStack extends Stack {
36
+ public readonly sslCertificate: Certificate;
37
+ public readonly s3Bucket: Bucket;
38
+ public readonly cloudFrontFunction: Function;
39
+ public readonly cloudFrontDistribution: Distribution;
40
+ public readonly webAcl: CfnWebACL | undefined;
41
+ public readonly aRecord: ARecord;
42
+ public readonly aaaaRecord: AaaaRecord;
43
+
44
+ constructor(scope: Construct, id: string, props: WebGlobalStackProps & StackProps) {
45
+ super(scope, id, props);
46
+
47
+ const hostedZone = getHostedZone(this, 'getHostedZone', props.hostedZone);
48
+
49
+ const _sslCert = new SSLCertificate(this, 'SSLCertificate', {
50
+ hostedZone: hostedZone,
51
+ domainName: props.domainName,
52
+ destroy: props.destroy
53
+ });
54
+ this.sslCertificate = _sslCert.certificate;
55
+
56
+ const _s3Bucket = new S3Bucket(this, 'S3Bucket', {
57
+ bucketName: props.bucketName ?? PhysicalName.GENERATE_IF_NEEDED,
58
+ destroy: props.destroy
59
+ });
60
+ this.s3Bucket = _s3Bucket.bucket;
61
+
62
+ const defaultBehavior: BehaviorOptions = {
63
+ origin: S3BucketOrigin.withOriginAccessControl(this.s3Bucket),
64
+ functionAssociations: []
65
+ };
66
+
67
+ let cffn: CloudFrontFunction;
68
+ if (props.basicAuth) {
69
+ cffn = CloudFrontFunction.createBasicAuthDefaultDoc(this, 'createBasicAuthDefaultDoc', {
70
+ user: props.basicAuth.user,
71
+ password: props.basicAuth.password,
72
+ redirects: props.redirects,
73
+ destroy: props.destroy
74
+ });
75
+ } else {
76
+ cffn = CloudFrontFunction.createDefaultDoc(this, 'createDefaultDoc', {
77
+ redirects: props.redirects,
78
+ destroy: props.destroy
79
+ });
80
+ }
81
+ this.cloudFrontFunction = cffn.fn;
82
+
83
+ defaultBehavior.functionAssociations!.push({
84
+ eventType: FunctionEventType.VIEWER_REQUEST,
85
+ function: this.cloudFrontFunction
86
+ });
87
+ defaultBehavior.functionAssociations!.push({
88
+ eventType: FunctionEventType.VIEWER_RESPONSE,
89
+ function: this.cloudFrontFunction
90
+ });
91
+
92
+
93
+ if (props.attachWebAcl !== false) {
94
+ const waf = new CloudFrontWebAcl(this, 'CloudFrontWebAcl', {
95
+ name: `${domainAsId(props.domainName)}`,
96
+ destroy: props.destroy
97
+ });
98
+ this.webAcl = waf.acl;
99
+ }
100
+
101
+ const cfDistribution = new CloudFrontDistribution(this, 'CloudFrontDistribution', {
102
+ domainName: props.domainName,
103
+ description: props.description ?? props.domainName,
104
+ priceClass: props.envType,
105
+ certificate: this.sslCertificate,
106
+ defaultBehavior,
107
+ webAclId: this.webAcl?.attrArn
108
+ });
109
+ this.cloudFrontDistribution = cfDistribution.distribution;
110
+
111
+ const cfAlias = new CloudFrontAlias(this, 'CloudFrontAlias', {
112
+ distribution: cfDistribution.distribution,
113
+ domainName: props.domainName,
114
+ hostedZone: hostedZone
115
+ });
116
+ this.aRecord = cfAlias.aRecord;
117
+ this.aaaaRecord = cfAlias.aaaaRecord;
118
+ }
119
+ }
@@ -0,0 +1,55 @@
1
+ import { Stage, StageProps } from 'aws-cdk-lib/core';
2
+ import { Construct } from 'constructs';
3
+
4
+ import { CodeBuildPipelineProjectProps } from '../constructs/codebuild';
5
+ import { WebGlobalStack, WebGlobalStackProps } from '../stacks/web-static-serverless/web-global-stack';
6
+ import { CodePipelineStack, CodePipelineStackProps } from '../stacks/web-static-serverless/codepipeline-stack';
7
+
8
+ export interface Str8rClmStageProps extends WebGlobalStackProps, StageProps {
9
+ pipeline?: Omit<CodePipelineStackProps, 'codeBuildProject'> & {
10
+ codeBuildProject?: Omit<CodeBuildPipelineProjectProps, 'cloudFrontDistributionArn' | 's3BucketName' | 'destroy' | 'tags'>
11
+ }
12
+ destroy?: boolean;
13
+ tags?: Record<string, string>;
14
+ }
15
+
16
+ export class Str8rClmStage extends Stage {
17
+ public readonly webStack: WebGlobalStack;
18
+ public readonly codePipelineStack: CodePipelineStack;
19
+
20
+ constructor(scope: Construct, id: string, props: Str8rClmStageProps) {
21
+ super(scope, id);
22
+
23
+ if (props.destroy === undefined) {
24
+ props.destroy = true;
25
+ }
26
+
27
+ this.webStack = new WebGlobalStack(this, 'web-global', {
28
+ ...props,
29
+ env: {
30
+ account: props.env?.account,
31
+ region: 'us-east-1'
32
+ },
33
+ stackName: `${props.stageName}-web-global-stack`,
34
+ destroy: props.destroy,
35
+ tags: props.tags
36
+ });
37
+
38
+ if (props.pipeline) {
39
+ props.pipeline.codeBuildProject = props.pipeline.codeBuildProject ?? {};
40
+ if (!props.pipeline.codeBuildProject.ecrBuildImage && !props.pipeline.codeBuildProject.buildImage) {
41
+ props.pipeline.codeBuildProject.ecrBuildImage = { repository: 'str8r-v3/codebuild', tag: 'veeva-clm' };
42
+ }
43
+ (props.pipeline.codeBuildProject as CodeBuildPipelineProjectProps).cloudFrontDistributionArn = this.webStack.cloudFrontDistribution.distributionArn;
44
+ (props.pipeline.codeBuildProject as CodeBuildPipelineProjectProps).s3BucketName = this.webStack.s3Bucket.bucketName;
45
+
46
+ this.codePipelineStack = new CodePipelineStack(this, 'codepipeline', {
47
+ ...props.pipeline,
48
+ env: props.env,
49
+ stackName: `${props.stageName}-codepipeline-stack`,
50
+ destroy: props.destroy,
51
+ tags: props.tags
52
+ });
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,55 @@
1
+ import { Stage, StageProps } from 'aws-cdk-lib/core';
2
+ import { Construct } from 'constructs';
3
+
4
+ import { CodeBuildPipelineProjectProps } from '../constructs/codebuild';
5
+ import { WebGlobalStack, WebGlobalStackProps } from '../stacks/web-static-serverless/web-global-stack';
6
+ // import { WebRegionalStack } from '../stacks/web-static-serverless/web-regional-stack';
7
+ import { CodePipelineStack, CodePipelineStackProps } from '../stacks/web-static-serverless/codepipeline-stack';
8
+
9
+ export interface WebStaticServerlessStageProps extends WebGlobalStackProps, StageProps {
10
+ pipeline?: Omit<CodePipelineStackProps, 'codeBuildProject'> & {
11
+ codeBuildProject?: Omit<CodeBuildPipelineProjectProps, 'cloudFrontDistributionArn' | 's3BucketName' | 'destroy' | 'tags'>
12
+ }
13
+ destroy?: boolean;
14
+ tags?: Record<string, string>;
15
+ }
16
+
17
+ export class WebStaticServerlessStage extends Stage {
18
+ // public readonly regionalStack: WebRegionalStack;
19
+ public readonly webStack: WebGlobalStack;
20
+ public readonly codePipelineStack: CodePipelineStack;
21
+
22
+ constructor(scope: Construct, id: string, props: WebStaticServerlessStageProps) {
23
+ super(scope, id);
24
+
25
+ if (props.destroy === undefined) {
26
+ props.destroy = true;
27
+ }
28
+
29
+ this.webStack = new WebGlobalStack(this, 'web-global', {
30
+ ...props,
31
+ env: {
32
+ account: props.env?.account,
33
+ region: 'us-east-1'
34
+ },
35
+ stackName: `${props.stageName}-web-global-stack`,
36
+ // s3BucketName: this.regionalStack.s3Bucket.bucketName
37
+ destroy: props.destroy,
38
+ tags: props.tags
39
+ });
40
+
41
+ if (props.pipeline) {
42
+ props.pipeline.codeBuildProject = props.pipeline.codeBuildProject ?? {};
43
+ (props.pipeline.codeBuildProject as CodeBuildPipelineProjectProps).cloudFrontDistributionArn = this.webStack.cloudFrontDistribution.distributionArn;
44
+ (props.pipeline.codeBuildProject as CodeBuildPipelineProjectProps).s3BucketName = this.webStack.s3Bucket.bucketName;
45
+
46
+ this.codePipelineStack = new CodePipelineStack(this, 'codepipeline', {
47
+ ...props.pipeline,
48
+ env: props.env,
49
+ stackName: `${props.stageName}-codepipeline-stack`,
50
+ destroy: props.destroy,
51
+ tags: props.tags
52
+ });
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Viewer Request - handle basic authorization, redirects, append index.html to origin
3
+ * Viewer Response - handle 301 redirect with request querystring
4
+ */
5
+
6
+ const DEFAULT_DOCUMENT = "__DEFAULT_DOCUMENT__";
7
+ const BASIC_AUTH = ["Basic __CREDENTIALS__"];
8
+
9
+ /** @type {Record<string, string | [string, 301|302]>} */
10
+ const redirects = __REDIRECTS__;
11
+
12
+ const useTrailingSlash = __TRAILING_SLASH__;
13
+ const shouldRedirDefDoc = __REDIR_DEF_DOC__;
14
+
15
+
16
+ function handler(event) {
17
+ switch (event.context.eventType) {
18
+ case "viewer-request":
19
+ return handleRequest(event);
20
+ case "viewer-response":
21
+ return handleResponse(event);
22
+ default:
23
+ return {
24
+ statusCode: 400,
25
+ statusDescription: "Bad request",
26
+ };
27
+ }
28
+ }
29
+
30
+ function handleRequest(event) {
31
+ if (authorized(event)) {
32
+ const request = event.request;
33
+ const uri = request.uri;
34
+
35
+ const redirect = redirects ? redirects[uri] : null;
36
+ if (redirect) {
37
+ const redir = Array.isArray(redirect) ? redirect : [redirect, 301];
38
+ return redirectResponse(event, redir[0], redir[1]);
39
+ }
40
+
41
+ if (/^(?:.+?)\.[^\.\/]+$/.test(uri)) {
42
+ // uri has a file extension
43
+ if (uri.endsWith(DEFAULT_DOCUMENT) && shouldRedirDefDoc === true) {
44
+ return redirectResponse(event, uri.slice(0, -1 * DEFAULT_DOCUMENT.length - (useTrailingSlash ? 0 : 1)));
45
+ } else {
46
+ return request;
47
+ }
48
+ } else if (useTrailingSlash === true && !uri.endsWith("/")) {
49
+ return redirectResponse(event, `${uri}/`);
50
+ } else if (uri !== '/' && useTrailingSlash === false && uri.endsWith("/")) {
51
+ return redirectResponse(event, uri.slice(0, -1));
52
+ } else {
53
+ request.uri = `${uri}${uri.endsWith("/") ? "" : "/"}${DEFAULT_DOCUMENT}`;
54
+ return request;
55
+ }
56
+ } else {
57
+ return {
58
+ statusCode: 401,
59
+ statusDescription: "Unauthorized",
60
+ headers: {
61
+ "www-authenticate": { value: "Basic" },
62
+ },
63
+ };
64
+ }
65
+ }
66
+
67
+ function authorized(event) {
68
+ let flag = false;
69
+ const authHeader = event.request.headers["authorization"];
70
+ if (authHeader && authHeader.value) {
71
+ BASIC_AUTH.every((val) => {
72
+ flag = authHeader.value === val;
73
+ return !flag;
74
+ });
75
+ }
76
+ return flag;
77
+ }
78
+
79
+ function handleResponse(event) {
80
+ switch (event.response.statusCode) {
81
+ case 301:
82
+ case 302:
83
+ return redirectResponse(event);
84
+ default:
85
+ return event.response;
86
+ }
87
+ }
88
+
89
+ function redirectResponse(event, location, statusCode) {
90
+ const response = event.response || {};
91
+ if (statusCode === 302) {
92
+ response.statusCode = 302;
93
+ response.statusDescription = "Found";
94
+ } else {
95
+ response.statusCode = 301;
96
+ response.statusDescription = "Moved Permanently";
97
+ }
98
+ const path = location || getLocationHeaderValue(response.headers) || '/';
99
+ const querystring = event.request.querystring;
100
+ response.headers = updateLocationHeader(response.headers, pathWithQS(path, querystring));
101
+ return response;
102
+ }
103
+
104
+ function getLocationHeaderValue(headers) {
105
+ return headers && headers["location"] ? headers["location"].value : undefined;
106
+ }
107
+
108
+ function updateLocationHeader(headers, location) {
109
+ headers = headers || {};
110
+ headers["location"] = {
111
+ value: location,
112
+ };
113
+ return headers;
114
+ }
115
+
116
+ function qsParamExists(path, key, value) {
117
+ return path.indexOf(`${key}=${value}`) != -1;
118
+ }
119
+
120
+ function pathWithQS(path, querystring) {
121
+ let qs = [];
122
+ if (querystring) {
123
+ qs = Object.keys(querystring)
124
+ .map(function (key) {
125
+ if (querystring[key].multiValue) {
126
+ return querystring[key].multiValue.map((multiKey) => {
127
+ return !qsParamExists(path, key, multiKey.value)
128
+ ? `${key}=${multiKey.value}`
129
+ : null;
130
+ });
131
+ } else {
132
+ return !qsParamExists(path, key, querystring[key].value)
133
+ ? `${key}=${querystring[key].value}`
134
+ : null;
135
+ }
136
+ })
137
+ .reduce((r, item) => {
138
+ (item instanceof Array ? item : [item]).forEach((i) => {
139
+ r.push(i);
140
+ });
141
+ return r;
142
+ }, [])
143
+ .filter((item) => {
144
+ return item !== null;
145
+ });
146
+ }
147
+ return `${path}${qs.length > 0 ? (path.indexOf("?") == -1 ? "?" : "&") : ""}${qs.join("&")}`;
148
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Viewer Request - redirects, append index.html to origin
3
+ * Viewer Response - handle 301 redirect with request querystring
4
+ */
5
+
6
+ const DEFAULT_DOCUMENT = "__DEFAULT_DOCUMENT__";
7
+
8
+ /** @type {Record<string, string | [string, 301|302]>} */
9
+ const redirects = __REDIRECTS__;
10
+
11
+ const useTrailingSlash = __TRAILING_SLASH__;
12
+ const shouldRedirDefDoc = __REDIR_DEF_DOC__;
13
+
14
+
15
+ function handler(event) {
16
+ switch (event.context.eventType) {
17
+ case "viewer-request":
18
+ return handleRequest(event);
19
+ case "viewer-response":
20
+ return handleResponse(event);
21
+ default:
22
+ return {
23
+ statusCode: 400,
24
+ statusDescription: "Bad request",
25
+ };
26
+ }
27
+ }
28
+
29
+ function handleRequest(event) {
30
+ const request = event.request;
31
+ const uri = request.uri;
32
+
33
+ const redirect = redirects ? redirects[uri] : null;
34
+ if (redirect) {
35
+ const redir = Array.isArray(redirect) ? redirect : [redirect, 301];
36
+ return redirectResponse(event, redir[0], redir[1]);
37
+ }
38
+
39
+ if (/^(?:.+?)\.[^\.\/]+$/.test(uri)) {
40
+ // uri has a file extension
41
+ if (uri.endsWith(DEFAULT_DOCUMENT) && shouldRedirDefDoc === true) {
42
+ return redirectResponse(event, uri.slice(0, -1 * DEFAULT_DOCUMENT.length - (useTrailingSlash ? 0 : 1)));
43
+ } else {
44
+ return request;
45
+ }
46
+ } else if (useTrailingSlash === true && !uri.endsWith("/")) {
47
+ return redirectResponse(event, `${uri}/`);
48
+ } else if (uri !== '/' && useTrailingSlash === false && uri.endsWith("/")) {
49
+ return redirectResponse(event, uri.slice(0, -1));
50
+ } else {
51
+ request.uri = `${uri}${uri.endsWith("/") ? "" : "/"}${DEFAULT_DOCUMENT}`;
52
+ return request;
53
+ }
54
+ }
55
+
56
+ function handleResponse(event) {
57
+ switch (event.response.statusCode) {
58
+ case 301:
59
+ case 302:
60
+ return redirectResponse(event);
61
+ default:
62
+ return event.response;
63
+ }
64
+ }
65
+
66
+ function redirectResponse(event, location, statusCode) {
67
+ const response = event.response || {};
68
+ if (statusCode === 302) {
69
+ response.statusCode = 302;
70
+ response.statusDescription = "Found";
71
+ } else {
72
+ response.statusCode = 301;
73
+ response.statusDescription = "Moved Permanently";
74
+ }
75
+ const path = location || getLocationHeaderValue(response.headers) || '/';
76
+ const querystring = event.request.querystring;
77
+ response.headers = updateLocationHeader(response.headers, pathWithQS(path, querystring));
78
+ return response;
79
+ }
80
+
81
+ function getLocationHeaderValue(headers) {
82
+ return headers && headers["location"] ? headers["location"].value : undefined;
83
+ }
84
+
85
+ function updateLocationHeader(headers, location) {
86
+ headers = headers || {};
87
+ headers["location"] = {
88
+ value: location,
89
+ };
90
+ return headers;
91
+ }
92
+
93
+ function qsParamExists(path, key, value) {
94
+ return path.indexOf(`${key}=${value}`) != -1;
95
+ }
96
+
97
+ function pathWithQS(path, querystring) {
98
+ let qs = [];
99
+ if (querystring) {
100
+ qs = Object.keys(querystring)
101
+ .map(function (key) {
102
+ if (querystring[key].multiValue) {
103
+ return querystring[key].multiValue.map((multiKey) => {
104
+ return !qsParamExists(path, key, multiKey.value)
105
+ ? `${key}=${multiKey.value}`
106
+ : null;
107
+ });
108
+ } else {
109
+ return !qsParamExists(path, key, querystring[key].value)
110
+ ? `${key}=${querystring[key].value}`
111
+ : null;
112
+ }
113
+ })
114
+ .reduce((r, item) => {
115
+ (item instanceof Array ? item : [item]).forEach((i) => {
116
+ r.push(i);
117
+ });
118
+ return r;
119
+ }, [])
120
+ .filter((item) => {
121
+ return item !== null;
122
+ });
123
+ }
124
+ return `${path}${qs.length > 0 ? (path.indexOf("?") == -1 ? "?" : "&") : ""}${qs.join("&")}`;
125
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": [
7
+ "es2022"
8
+ ],
9
+ "declaration": true,
10
+ "strict": true,
11
+ "noImplicitAny": true,
12
+ "strictNullChecks": true,
13
+ "noImplicitThis": true,
14
+ "alwaysStrict": true,
15
+ "noUnusedLocals": false,
16
+ "noUnusedParameters": false,
17
+ "noImplicitReturns": true,
18
+ "noFallthroughCasesInSwitch": false,
19
+ "inlineSourceMap": true,
20
+ "inlineSources": true,
21
+ "experimentalDecorators": true,
22
+ "strictPropertyInitialization": false,
23
+ "skipLibCheck": true,
24
+ "types": ["node"]
25
+ },
26
+ "exclude": [
27
+ "node_modules"
28
+ ]
29
+ }