@sitblueprint/website-construct 0.1.0 → 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Stevens Blueprint
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -9,8 +9,8 @@ A reusable [AWS CDK](https://docs.aws.amazon.com/cdk/) construct to deploy a web
9
9
 
10
10
  ## Installation
11
11
 
12
- ```
13
- npm install website-construct
12
+ ```bash
13
+ npm i @sitblueprint/website-construct
14
14
  ```
15
15
 
16
16
  ## Usage
@@ -20,7 +20,7 @@ export class MyWebsiteStack extends cdk.Stack {
20
20
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
21
21
  super(scope, id, props);
22
22
 
23
- new WebsiteConstruct(this, "MyWebsite", {
23
+ new Website(this, "MyWebsite", {
24
24
  bucketName: "my-static-site-bucket",
25
25
  indexFile: "index.html",
26
26
  errorFile: "error.html",
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ---
9
+
10
+ ## [v0.1.2] - 2025-09-06
11
+
12
+ ### Changed
13
+
14
+ - Made S3 website bucket a public member, so that it is accessible from outside the construct
15
+
16
+ ## [v0.1.1] - 2025-09-06
17
+
18
+ ### Added
19
+
20
+ - `DomainConfig` for custom domains (domain + subdomain + ACM certificate).
21
+ - Route 53 `ARecord` creation for CloudFront distribution.
22
+ - Support for custom 404 pages (`notFoundResponsePagePath`).
23
+ - CloudFormation outputs:
24
+ - CloudFront Distribution URL
25
+ - S3 Website URL
26
+ - Custom domain URL (if configured)
27
+
28
+ ### Changed
29
+
30
+ - CloudFront distribution defaults:
31
+ - Redirect all traffic to HTTPS.
32
+ - Cache 404 responses for 30 minutes.
33
+ - Price class limited to `PRICE_CLASS_100` for lower cost.
34
+
35
+ ### Security
36
+
37
+ - Enforced access restrictions with CloudFront Origin Access Identity (OAI).
38
+ - Buckets encrypted with **S3 Managed Encryption** by default.
39
+
40
+ ---
41
+
42
+ ## [v0.1.0] - 2025-08-15
43
+
44
+ ### Added
45
+
46
+ - Initial release of Website.
47
+ - Static website hosting via S3.
48
+ - CloudFront distribution with default caching.
49
+ - Basic index and error page support.
package/lib/index.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { Construct } from "constructs";
2
+ import * as s3 from "aws-cdk-lib/aws-s3";
3
+ export interface DomainConfig {
4
+ /** The root domain name (e.g., example.com).
5
+ * There must be an associated hosted zone in Route 53 for this domain.
6
+ */
7
+ domainName: string;
8
+ /** The subdomain name */
9
+ subdomainName: string;
10
+ /** The ARN of the SSL certificate to use for the domain. */
11
+ certificateArn: string;
12
+ }
13
+ export interface WebsiteProps {
14
+ /** The name of the S3 bucket that will host the website content. */
15
+ bucketName: string;
16
+ /** The path to the index document that will be served as the default page. */
17
+ indexFile: string;
18
+ /** The path to the error document that will be served when an error occurs. */
19
+ errorFile: string;
20
+ /** Optional configuration for custom domain setup. */
21
+ domainConfig?: DomainConfig;
22
+ /** Optional path to a custom 404 page. If not specified, the error file will be used. */
23
+ notFoundResponsePagePath?: string;
24
+ }
25
+ export declare class Website extends Construct {
26
+ readonly bucket: s3.Bucket;
27
+ constructor(scope: Construct, id: string, props: WebsiteProps);
28
+ private _getFullDomainName;
29
+ private _getCertificate;
30
+ }
package/lib/index.js ADDED
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.Website = void 0;
27
+ const constructs_1 = require("constructs");
28
+ const s3 = __importStar(require("aws-cdk-lib/aws-s3"));
29
+ const cloudfont = __importStar(require("aws-cdk-lib/aws-cloudfront"));
30
+ const origins = __importStar(require("aws-cdk-lib/aws-cloudfront-origins"));
31
+ const certificatemanager = __importStar(require("aws-cdk-lib/aws-certificatemanager"));
32
+ const cdk = __importStar(require("aws-cdk-lib"));
33
+ const iam = __importStar(require("aws-cdk-lib/aws-iam"));
34
+ const route53 = __importStar(require("aws-cdk-lib/aws-route53"));
35
+ class Website extends constructs_1.Construct {
36
+ bucket;
37
+ constructor(scope, id, props) {
38
+ super(scope, id);
39
+ this.bucket = new s3.Bucket(this, props.bucketName, {
40
+ websiteIndexDocument: props.indexFile,
41
+ websiteErrorDocument: props.errorFile,
42
+ removalPolicy: cdk.RemovalPolicy.DESTROY,
43
+ encryption: s3.BucketEncryption.S3_MANAGED,
44
+ });
45
+ const oai = new cloudfont.OriginAccessIdentity(this, `${props.bucketName}-OAI`);
46
+ this.bucket.addToResourcePolicy(new iam.PolicyStatement({
47
+ actions: ["s3:GetObject"],
48
+ resources: [this.bucket.arnForObjects("*")],
49
+ principals: [
50
+ new iam.CanonicalUserPrincipal(oai.cloudFrontOriginAccessIdentityS3CanonicalUserId),
51
+ ],
52
+ }));
53
+ const distribution = new cloudfont.Distribution(this, `${props.bucketName}-distribution`, {
54
+ defaultBehavior: {
55
+ origin: new origins.S3StaticWebsiteOrigin(this.bucket),
56
+ viewerProtocolPolicy: cloudfont.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
57
+ },
58
+ errorResponses: [
59
+ {
60
+ httpStatus: 404,
61
+ responseHttpStatus: 404,
62
+ responsePagePath: props.notFoundResponsePagePath || `/404.html`,
63
+ ttl: cdk.Duration.minutes(30),
64
+ },
65
+ ],
66
+ priceClass: cloudfont.PriceClass.PRICE_CLASS_100,
67
+ ...(props.domainConfig
68
+ ? {
69
+ domainNames: [this._getFullDomainName(props.domainConfig)],
70
+ certificate: this._getCertificate(props.domainConfig.certificateArn),
71
+ }
72
+ : {}),
73
+ });
74
+ if (props.domainConfig) {
75
+ const hostedZone = route53.HostedZone.fromLookup(this, "HostedZone", {
76
+ domainName: props.domainConfig.domainName,
77
+ });
78
+ const domainARecord = new route53.ARecord(this, "DomainARecord", {
79
+ zone: hostedZone,
80
+ recordName: this._getFullDomainName(props.domainConfig),
81
+ target: cdk.aws_route53.RecordTarget.fromAlias(new cdk.aws_route53_targets.CloudFrontTarget(distribution)),
82
+ });
83
+ domainARecord.node.addDependency(distribution);
84
+ }
85
+ new cdk.CfnOutput(this, "cloudfront-website-url", {
86
+ value: distribution.distributionDomainName,
87
+ description: "CloudFront Distribution Domain Name",
88
+ });
89
+ new cdk.CfnOutput(this, "s3-website-url", {
90
+ value: this.bucket.bucketWebsiteUrl,
91
+ description: "S3 Bucket Website URL",
92
+ });
93
+ if (props.domainConfig) {
94
+ new cdk.CfnOutput(this, "website-url", {
95
+ value: distribution.domainName,
96
+ description: "Website URL",
97
+ });
98
+ }
99
+ }
100
+ _getFullDomainName(domainConfig) {
101
+ return domainConfig.subdomainName
102
+ ? `${domainConfig.subdomainName}.${domainConfig.domainName}`
103
+ : domainConfig.domainName;
104
+ }
105
+ _getCertificate(arn) {
106
+ return certificatemanager.Certificate.fromCertificateArn(this, `website-cert`, arn);
107
+ }
108
+ }
109
+ exports.Website = Website;
110
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQUFBLDJDQUF1QztBQUN2Qyx1REFBeUM7QUFDekMsc0VBQXdEO0FBQ3hELDRFQUE4RDtBQUM5RCx1RkFBeUU7QUFDekUsaURBQW1DO0FBQ25DLHlEQUEyQztBQUMzQyxpRUFBbUQ7QUE4Qm5ELE1BQWEsT0FBUSxTQUFRLHNCQUFTO0lBQ3BCLE1BQU0sQ0FBWTtJQUVsQyxZQUFZLEtBQWdCLEVBQUUsRUFBVSxFQUFFLEtBQW1CO1FBQzNELEtBQUssQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUM7UUFDakIsSUFBSSxDQUFDLE1BQU0sR0FBRyxJQUFJLEVBQUUsQ0FBQyxNQUFNLENBQUMsSUFBSSxFQUFFLEtBQUssQ0FBQyxVQUFVLEVBQUU7WUFDbEQsb0JBQW9CLEVBQUUsS0FBSyxDQUFDLFNBQVM7WUFDckMsb0JBQW9CLEVBQUUsS0FBSyxDQUFDLFNBQVM7WUFDckMsYUFBYSxFQUFFLEdBQUcsQ0FBQyxhQUFhLENBQUMsT0FBTztZQUN4QyxVQUFVLEVBQUUsRUFBRSxDQUFDLGdCQUFnQixDQUFDLFVBQVU7U0FDM0MsQ0FBQyxDQUFDO1FBQ0gsTUFBTSxHQUFHLEdBQUcsSUFBSSxTQUFTLENBQUMsb0JBQW9CLENBQzVDLElBQUksRUFDSixHQUFHLEtBQUssQ0FBQyxVQUFVLE1BQU0sQ0FDMUIsQ0FBQztRQUNGLElBQUksQ0FBQyxNQUFNLENBQUMsbUJBQW1CLENBQzdCLElBQUksR0FBRyxDQUFDLGVBQWUsQ0FBQztZQUN0QixPQUFPLEVBQUUsQ0FBQyxjQUFjLENBQUM7WUFDekIsU0FBUyxFQUFFLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsR0FBRyxDQUFDLENBQUM7WUFDM0MsVUFBVSxFQUFFO2dCQUNWLElBQUksR0FBRyxDQUFDLHNCQUFzQixDQUM1QixHQUFH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LENBQUM7SUFFTyxlQUFlLENBQUMsR0FBVztRQUNqQyxPQUFPLGtCQUFrQixDQUFDLFdBQVcsQ0FBQyxrQkFBa0IsQ0FDdEQsSUFBSSxFQUNKLGNBQWMsRUFDZCxHQUFHLENBQ0osQ0FBQztJQUNKLENBQUM7Q0FDRjtBQXJHRCwwQkFxR0MiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBDb25zdHJ1Y3QgfSBmcm9tIFwiY29uc3RydWN0c1wiO1xuaW1wb3J0ICogYXMgczMgZnJvbSBcImF3cy1jZGstbGliL2F3cy1zM1wiO1xuaW1wb3J0ICogYXMgY2xvdWRmb250IGZyb20gXCJhd3MtY2RrLWxpYi9hd3MtY2xvdWRmcm9udFwiO1xuaW1wb3J0ICogYXMgb3JpZ2lucyBmcm9tIFwiYXdzLWNkay1saWIvYXdzLWNsb3VkZnJvbnQtb3JpZ2luc1wiO1xuaW1wb3J0ICogYXMgY2VydGlmaWNhdGVtYW5hZ2VyIGZyb20gXCJhd3MtY2RrLWxpYi9hd3MtY2VydGlmaWNhdGVtYW5hZ2VyXCI7XG5pbXBvcnQgKiBhcyBjZGsgZnJvbSBcImF3cy1jZGstbGliXCI7XG5pbXBvcnQgKiBhcyBpYW0gZnJvbSBcImF3cy1jZGstbGliL2F3cy1pYW1cIjtcbmltcG9ydCAqIGFzIHJvdXRlNTMgZnJvbSBcImF3cy1jZGstbGliL2F3cy1yb3V0ZTUzXCI7XG5cbmV4cG9ydCBpbnRlcmZhY2UgRG9tYWluQ29uZmlnIHtcbiAgLyoqIFRoZSByb290IGRvbWFpbiBuYW1lIChlLmcuLCBleGFtcGxlLmNvbSkuXG4gICAqIFRoZXJlIG11c3QgYmUgYW4gYXNzb2NpYXRlZCBob3N0ZWQgem9uZSBpbiBSb3V0ZSA1MyBmb3IgdGhpcyBkb21haW4uXG4gICAqL1xuICBkb21haW5OYW1lOiBzdHJpbmc7XG4gIC8qKiBUaGUgc3ViZG9tYWluIG5hbWUgKi9cbiAgc3ViZG9tYWluTmFtZTogc3RyaW5nO1xuICAvKiogVGhlIEFSTiBvZiB0aGUgU1NMIGNlcnRpZmljYXRlIHRvIHVzZSBmb3IgdGhlIGRvbWFpbi4gKi9cbiAgY2VydGlmaWNhdGVBcm46IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBXZWJzaXRlUHJvcHMge1xuICAvKiogVGhlIG5hbWUgb2YgdGhlIFMzIGJ1Y2tldCB0aGF0IHdpbGwgaG9zdCB0aGUgd2Vic2l0ZSBjb250ZW50LiAqL1xuICBidWNrZXROYW1lOiBzdHJpbmc7XG5cbiAgLyoqIFRoZSBwYXRoIHRvIHRoZSBpbmRleCBkb2N1bWVudCB0aGF0IHdpbGwgYmUgc2VydmVkIGFzIHRoZSBkZWZhdWx0IHBhZ2UuICovXG4gIGluZGV4RmlsZTogc3RyaW5nO1xuXG4gIC8qKiBUaGUgcGF0aCB0byB0aGUgZXJyb3IgZG9jdW1lbnQgdGhhdCB3aWxsIGJlIHNlcnZlZCB3aGVuIGFuIGVycm9yIG9jY3Vycy4gKi9cbiAgZXJyb3JGaWxlOiBzdHJpbmc7XG5cbiAgLyoqIE9wdGlvbmFsIGNvbmZpZ3VyYXRpb24gZm9yIGN1c3RvbSBkb21haW4gc2V0dXAuICovXG4gIGRvbWFpbkNvbmZpZz86IERvbWFpbkNvbmZpZztcblxuICAvKiogT3B0aW9uYWwgcGF0aCB0byBhIGN1c3RvbSA0MDQgcGFnZS4gSWYgbm90IHNwZWNpZmllZCwgdGhlIGVycm9yIGZpbGUgd2lsbCBiZSB1c2VkLiAqL1xuICBub3RGb3VuZFJlc3BvbnNlUGFnZVBhdGg/OiBzdHJpbmc7XG59XG5cbmV4cG9ydCBjbGFzcyBXZWJzaXRlIGV4dGVuZHMgQ29uc3RydWN0IHtcbiAgcHVibGljIHJlYWRvbmx5IGJ1Y2tldDogczMuQnVja2V0O1xuXG4gIGNvbnN0cnVjdG9yKHNjb3BlOiBDb25zdHJ1Y3QsIGlkOiBzdHJpbmcsIHByb3BzOiBXZWJzaXRlUHJvcHMpIHtcbiAgICBzdXBlcihzY29wZSwgaWQpO1xuICAgIHRoaXMuYnVja2V0ID0gbmV3IHMzLkJ1Y2tldCh0aGlzLCBwcm9wcy5idWNrZXROYW1lLCB7XG4gICAgICB3ZWJzaXRlSW5kZXhEb2N1bWVudDogcHJvcHMuaW5kZXhGaWxlLFxuICAgICAgd2Vic2l0ZUVycm9yRG9jdW1lbnQ6IHByb3BzLmVycm9yRmlsZSxcbiAgICAgIHJlbW92YWxQb2xpY3k6IGNkay5SZW1vdmFsUG9saWN5LkRFU1RST1ksXG4gICAgICBlbmNyeXB0aW9uOiBzMy5CdWNrZXRFbmNyeXB0aW9uLlMzX01BTkFHRUQsXG4gICAgfSk7XG4gICAgY29uc3Qgb2FpID0gbmV3IGNsb3VkZm9udC5PcmlnaW5BY2Nlc3NJZGVudGl0eShcbiAgICAgIHRoaXMsXG4gICAgICBgJHtwcm9wcy5idWNrZXROYW1lfS1PQUlgLFxuICAgICk7XG4gICAgdGhpcy5idWNrZXQuYWRkVG9SZXNvdXJjZVBvbGljeShcbiAgICAgIG5ldyBpYW0uUG9saWN5U3RhdGVtZW50KHtcbiAgICAgICAgYWN0aW9uczogW1wiczM6R2V0T2JqZWN0XCJdLFxuICAgICAgICByZXNvdXJjZXM6IFt0aGlzLmJ1Y2tldC5hcm5Gb3JPYmplY3RzKFwiKlwiKV0sXG4gICAgICAgIHByaW5jaXBhbHM6IFtcbiAgICAgICAgICBuZXcgaWFtLkNhbm9uaWNhbFVzZXJQcmluY2lwYWwoXG4gICAgICAgICAgICBvYWkuY2xvdWRGcm9udE9yaWdpbkFjY2Vzc0lkZW50aXR5UzNDYW5vbmljYWxVc2VySWQsXG4gICAgICAgICAgKSxcbiAgICAgICAgXSxcbiAgICAgIH0pLFxuICAgICk7XG5cbiAgICBjb25zdCBkaXN0cmlidXRpb24gPSBuZXcgY2xvdWRmb250LkRpc3RyaWJ1dGlvbihcbiAgICAgIHRoaXMsXG4gICAgICBgJHtwcm9wcy5idWNrZXROYW1lfS1kaXN0cmlidXRpb25gLFxuICAgICAge1xuICAgICAgICBkZWZhdWx0QmVoYXZpb3I6IHtcbiAgICAgICAgICBvcmlnaW46IG5ldyBvcmlnaW5zLlMzU3RhdGljV2Vic2l0ZU9yaWdpbih0aGlzLmJ1Y2tldCksXG4gICAgICAgICAgdmlld2VyUHJvdG9jb2xQb2xpY3k6XG4gICAgICAgICAgICBjbG91ZGZvbnQuVmlld2VyUHJvdG9jb2xQb2xpY3kuUkVESVJFQ1RfVE9fSFRUUFMsXG4gICAgICAgIH0sXG4gICAgICAgIGVycm9yUmVzcG9uc2VzOiBbXG4gICAgICAgICAge1xuICAgICAgICAgICAgaHR0cFN0YXR1czogNDA0LFxuICAgICAgICAgICAgcmVzcG9uc2VIdHRwU3RhdHVzOiA0MDQsXG4gICAgICAgICAgICByZXNwb25zZVBhZ2VQYXRoOiBwcm9wcy5ub3RGb3VuZFJlc3BvbnNlUGFnZVBhdGggfHwgYC80MDQuaHRtbGAsXG4gICAgICAgICAgICB0dGw6IGNkay5EdXJhdGlvbi5taW51dGVzKDMwKSxcbiAgICAgICAgICB9LFxuICAgICAgICBdLFxuICAgICAgICBwcmljZUNsYXNzOiBjbG91ZGZvbnQuUHJpY2VDbGFzcy5QUklDRV9DTEFTU18xMDAsXG4gICAgICAgIC4uLihwcm9wcy5kb21haW5Db25maWdcbiAgICAgICAgICA/IHtcbiAgICAgICAgICAgICAgZG9tYWluTmFtZXM6IFt0aGlzLl9nZXRGdWxsRG9tYWluTmFtZShwcm9wcy5kb21haW5Db25maWcpXSxcbiAgICAgICAgICAgICAgY2VydGlmaWNhdGU6IHRoaXMuX2dldENlcnRpZmljYXRlKFxuICAgICAgICAgICAgICAgIHByb3BzLmRvbWFpbkNvbmZpZy5jZXJ0aWZpY2F0ZUFybixcbiAgICAgICAgICAgICAgKSxcbiAgICAgICAgICAgIH1cbiAgICAgICAgICA6IHt9KSxcbiAgICAgIH0sXG4gICAgKTtcblxuICAgIGlmIChwcm9wcy5kb21haW5Db25maWcpIHtcbiAgICAgIGNvbnN0IGhvc3RlZFpvbmUgPSByb3V0ZTUzLkhvc3RlZFpvbmUuZnJvbUxvb2t1cCh0aGlzLCBcIkhvc3RlZFpvbmVcIiwge1xuICAgICAgICBkb21haW5OYW1lOiBwcm9wcy5kb21haW5Db25maWcuZG9tYWluTmFtZSxcbiAgICAgIH0pO1xuICAgICAgY29uc3QgZG9tYWluQVJlY29yZCA9IG5ldyByb3V0ZTUzLkFSZWNvcmQodGhpcywgXCJEb21haW5BUmVjb3JkXCIsIHtcbiAgICAgICAgem9uZTogaG9zdGVkWm9uZSxcbiAgICAgICAgcmVjb3JkTmFtZTogdGhpcy5fZ2V0RnVsbERvbWFpbk5hbWUocHJvcHMuZG9tYWluQ29uZmlnKSxcbiAgICAgICAgdGFyZ2V0OiBjZGsuYXdzX3JvdXRlNTMuUmVjb3JkVGFyZ2V0LmZyb21BbGlhcyhcbiAgICAgICAgICBuZXcgY2RrLmF3c19yb3V0ZTUzX3RhcmdldHMuQ2xvdWRGcm9udFRhcmdldChkaXN0cmlidXRpb24pLFxuICAgICAgICApLFxuICAgICAgfSk7XG4gICAgICBkb21haW5BUmVjb3JkLm5vZGUuYWRkRGVwZW5kZW5jeShkaXN0cmlidXRpb24pO1xuICAgIH1cblxuICAgIG5ldyBjZGsuQ2ZuT3V0cHV0KHRoaXMsIFwiY2xvdWRmcm9udC13ZWJzaXRlLXVybFwiLCB7XG4gICAgICB2YWx1ZTogZGlzdHJpYnV0aW9uLmRpc3RyaWJ1dGlvbkRvbWFpbk5hbWUsXG4gICAgICBkZXNjcmlwdGlvbjogXCJDbG91ZEZyb250IERpc3RyaWJ1dGlvbiBEb21haW4gTmFtZVwiLFxuICAgIH0pO1xuXG4gICAgbmV3IGNkay5DZm5PdXRwdXQodGhpcywgXCJzMy13ZWJzaXRlLXVybFwiLCB7XG4gICAgICB2YWx1ZTogdGhpcy5idWNrZXQuYnVja2V0V2Vic2l0ZVVybCxcbiAgICAgIGRlc2NyaXB0aW9uOiBcIlMzIEJ1Y2tldCBXZWJzaXRlIFVSTFwiLFxuICAgIH0pO1xuXG4gICAgaWYgKHByb3BzLmRvbWFpbkNvbmZpZykge1xuICAgICAgbmV3IGNkay5DZm5PdXRwdXQodGhpcywgXCJ3ZWJzaXRlLXVybFwiLCB7XG4gICAgICAgIHZhbHVlOiBkaXN0cmlidXRpb24uZG9tYWluTmFtZSxcbiAgICAgICAgZGVzY3JpcHRpb246IFwiV2Vic2l0ZSBVUkxcIixcbiAgICAgIH0pO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgX2dldEZ1bGxEb21haW5OYW1lKGRvbWFpbkNvbmZpZzogRG9tYWluQ29uZmlnKTogc3RyaW5nIHtcbiAgICByZXR1cm4gZG9tYWluQ29uZmlnLnN1YmRvbWFpbk5hbWVcbiAgICAgID8gYCR7ZG9tYWluQ29uZmlnLnN1YmRvbWFpbk5hbWV9LiR7ZG9tYWluQ29uZmlnLmRvbWFpbk5hbWV9YFxuICAgICAgOiBkb21haW5Db25maWcuZG9tYWluTmFtZTtcbiAgfVxuXG4gIHByaXZhdGUgX2dldENlcnRpZmljYXRlKGFybjogc3RyaW5nKTogY2VydGlmaWNhdGVtYW5hZ2VyLklDZXJ0aWZpY2F0ZSB7XG4gICAgcmV0dXJuIGNlcnRpZmljYXRlbWFuYWdlci5DZXJ0aWZpY2F0ZS5mcm9tQ2VydGlmaWNhdGVBcm4oXG4gICAgICB0aGlzLFxuICAgICAgYHdlYnNpdGUtY2VydGAsXG4gICAgICBhcm4sXG4gICAgKTtcbiAgfVxufVxuIl19
package/lib/index.ts ADDED
@@ -0,0 +1,139 @@
1
+ import { Construct } from "constructs";
2
+ import * as s3 from "aws-cdk-lib/aws-s3";
3
+ import * as cloudfont from "aws-cdk-lib/aws-cloudfront";
4
+ import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
5
+ import * as certificatemanager from "aws-cdk-lib/aws-certificatemanager";
6
+ import * as cdk from "aws-cdk-lib";
7
+ import * as iam from "aws-cdk-lib/aws-iam";
8
+ import * as route53 from "aws-cdk-lib/aws-route53";
9
+
10
+ export interface DomainConfig {
11
+ /** The root domain name (e.g., example.com).
12
+ * There must be an associated hosted zone in Route 53 for this domain.
13
+ */
14
+ domainName: string;
15
+ /** The subdomain name */
16
+ subdomainName: string;
17
+ /** The ARN of the SSL certificate to use for the domain. */
18
+ certificateArn: string;
19
+ }
20
+
21
+ export interface WebsiteProps {
22
+ /** The name of the S3 bucket that will host the website content. */
23
+ bucketName: string;
24
+
25
+ /** The path to the index document that will be served as the default page. */
26
+ indexFile: string;
27
+
28
+ /** The path to the error document that will be served when an error occurs. */
29
+ errorFile: string;
30
+
31
+ /** Optional configuration for custom domain setup. */
32
+ domainConfig?: DomainConfig;
33
+
34
+ /** Optional path to a custom 404 page. If not specified, the error file will be used. */
35
+ notFoundResponsePagePath?: string;
36
+ }
37
+
38
+ export class Website extends Construct {
39
+ public readonly bucket: s3.Bucket;
40
+
41
+ constructor(scope: Construct, id: string, props: WebsiteProps) {
42
+ super(scope, id);
43
+ this.bucket = new s3.Bucket(this, props.bucketName, {
44
+ websiteIndexDocument: props.indexFile,
45
+ websiteErrorDocument: props.errorFile,
46
+ removalPolicy: cdk.RemovalPolicy.DESTROY,
47
+ encryption: s3.BucketEncryption.S3_MANAGED,
48
+ });
49
+ const oai = new cloudfont.OriginAccessIdentity(
50
+ this,
51
+ `${props.bucketName}-OAI`,
52
+ );
53
+ this.bucket.addToResourcePolicy(
54
+ new iam.PolicyStatement({
55
+ actions: ["s3:GetObject"],
56
+ resources: [this.bucket.arnForObjects("*")],
57
+ principals: [
58
+ new iam.CanonicalUserPrincipal(
59
+ oai.cloudFrontOriginAccessIdentityS3CanonicalUserId,
60
+ ),
61
+ ],
62
+ }),
63
+ );
64
+
65
+ const distribution = new cloudfont.Distribution(
66
+ this,
67
+ `${props.bucketName}-distribution`,
68
+ {
69
+ defaultBehavior: {
70
+ origin: new origins.S3StaticWebsiteOrigin(this.bucket),
71
+ viewerProtocolPolicy:
72
+ cloudfont.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
73
+ },
74
+ errorResponses: [
75
+ {
76
+ httpStatus: 404,
77
+ responseHttpStatus: 404,
78
+ responsePagePath: props.notFoundResponsePagePath || `/404.html`,
79
+ ttl: cdk.Duration.minutes(30),
80
+ },
81
+ ],
82
+ priceClass: cloudfont.PriceClass.PRICE_CLASS_100,
83
+ ...(props.domainConfig
84
+ ? {
85
+ domainNames: [this._getFullDomainName(props.domainConfig)],
86
+ certificate: this._getCertificate(
87
+ props.domainConfig.certificateArn,
88
+ ),
89
+ }
90
+ : {}),
91
+ },
92
+ );
93
+
94
+ if (props.domainConfig) {
95
+ const hostedZone = route53.HostedZone.fromLookup(this, "HostedZone", {
96
+ domainName: props.domainConfig.domainName,
97
+ });
98
+ const domainARecord = new route53.ARecord(this, "DomainARecord", {
99
+ zone: hostedZone,
100
+ recordName: this._getFullDomainName(props.domainConfig),
101
+ target: cdk.aws_route53.RecordTarget.fromAlias(
102
+ new cdk.aws_route53_targets.CloudFrontTarget(distribution),
103
+ ),
104
+ });
105
+ domainARecord.node.addDependency(distribution);
106
+ }
107
+
108
+ new cdk.CfnOutput(this, "cloudfront-website-url", {
109
+ value: distribution.distributionDomainName,
110
+ description: "CloudFront Distribution Domain Name",
111
+ });
112
+
113
+ new cdk.CfnOutput(this, "s3-website-url", {
114
+ value: this.bucket.bucketWebsiteUrl,
115
+ description: "S3 Bucket Website URL",
116
+ });
117
+
118
+ if (props.domainConfig) {
119
+ new cdk.CfnOutput(this, "website-url", {
120
+ value: distribution.domainName,
121
+ description: "Website URL",
122
+ });
123
+ }
124
+ }
125
+
126
+ private _getFullDomainName(domainConfig: DomainConfig): string {
127
+ return domainConfig.subdomainName
128
+ ? `${domainConfig.subdomainName}.${domainConfig.domainName}`
129
+ : domainConfig.domainName;
130
+ }
131
+
132
+ private _getCertificate(arn: string): certificatemanager.ICertificate {
133
+ return certificatemanager.Certificate.fromCertificateArn(
134
+ this,
135
+ `website-cert`,
136
+ arn,
137
+ );
138
+ }
139
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sitblueprint/website-construct",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A reusable AWS CDK construct for deploying static websites with optional custom domain support.",
5
5
  "author": "Miguel Merlin <mmerlin@stevens.edu>",
6
6
  "license": "MIT",
@@ -43,4 +43,4 @@
43
43
  "aws-cdk-lib": "2.208.0",
44
44
  "constructs": "^10.0.0"
45
45
  }
46
- }
46
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ const assertions_1 = require("aws-cdk-lib/assertions");
27
+ const cdk = __importStar(require("aws-cdk-lib"));
28
+ const lib_1 = require("../lib");
29
+ describe("Website", () => {
30
+ let app;
31
+ let stack;
32
+ beforeEach(() => {
33
+ app = new cdk.App();
34
+ stack = new cdk.Stack(app, "TestStack", {
35
+ env: { account: "123456789012", region: "us-east-1" },
36
+ });
37
+ stack.node.setContext("hosted-zone:account=123456789012:domainName=example.com:region=us-east-1", {
38
+ Id: "/hostedzone/Z123456789012",
39
+ Name: "example.com.",
40
+ });
41
+ });
42
+ describe("Basic functionality", () => {
43
+ test("creates S3 bucket with basic configuration", () => {
44
+ const props = {
45
+ bucketName: "test-website-bucket",
46
+ indexFile: "index.html",
47
+ errorFile: "error.html",
48
+ };
49
+ new lib_1.Website(stack, "TestWebsite", props);
50
+ const template = assertions_1.Template.fromStack(stack);
51
+ template.hasResourceProperties("AWS::S3::Bucket", {
52
+ WebsiteConfiguration: {
53
+ IndexDocument: "index.html",
54
+ ErrorDocument: "error.html",
55
+ },
56
+ BucketEncryption: {
57
+ ServerSideEncryptionConfiguration: [
58
+ {
59
+ ServerSideEncryptionByDefault: {
60
+ SSEAlgorithm: "AES256",
61
+ },
62
+ },
63
+ ],
64
+ },
65
+ });
66
+ });
67
+ test("creates CloudFront Origin Access Identity", () => {
68
+ const props = {
69
+ bucketName: "test-website-bucket",
70
+ indexFile: "index.html",
71
+ errorFile: "error.html",
72
+ };
73
+ new lib_1.Website(stack, "TestWebsite", props);
74
+ const template = assertions_1.Template.fromStack(stack);
75
+ template.hasResourceProperties("AWS::CloudFront::CloudFrontOriginAccessIdentity", {
76
+ CloudFrontOriginAccessIdentityConfig: {
77
+ Comment: assertions_1.Match.anyValue(),
78
+ },
79
+ });
80
+ });
81
+ test("creates CloudFront distribution with correct configuration", () => {
82
+ const props = {
83
+ bucketName: "test-website-bucket",
84
+ indexFile: "index.html",
85
+ errorFile: "error.html",
86
+ };
87
+ new lib_1.Website(stack, "TestWebsite", props);
88
+ const template = assertions_1.Template.fromStack(stack);
89
+ template.hasResourceProperties("AWS::CloudFront::Distribution", {
90
+ DistributionConfig: {
91
+ DefaultCacheBehavior: {
92
+ ViewerProtocolPolicy: "redirect-to-https",
93
+ },
94
+ CustomErrorResponses: [
95
+ {
96
+ ErrorCode: 404,
97
+ ResponseCode: 404,
98
+ ResponsePagePath: "/404.html",
99
+ ErrorCachingMinTTL: 1800,
100
+ },
101
+ ],
102
+ PriceClass: "PriceClass_100",
103
+ Enabled: true,
104
+ },
105
+ });
106
+ });
107
+ });
108
+ describe("Custom error page configuration", () => {
109
+ test("uses custom not found response page path", () => {
110
+ const props = {
111
+ bucketName: "test-website-bucket",
112
+ indexFile: "index.html",
113
+ errorFile: "error.html",
114
+ notFoundResponsePagePath: "/custom-404.html",
115
+ };
116
+ new lib_1.Website(stack, "TestWebsite", props);
117
+ const template = assertions_1.Template.fromStack(stack);
118
+ template.hasResourceProperties("AWS::CloudFront::Distribution", {
119
+ DistributionConfig: {
120
+ CustomErrorResponses: [
121
+ {
122
+ ErrorCode: 404,
123
+ ResponseCode: 404,
124
+ ResponsePagePath: "/custom-404.html",
125
+ ErrorCachingMinTTL: 1800,
126
+ },
127
+ ],
128
+ },
129
+ });
130
+ });
131
+ test("uses default 404.html when notFoundResponsePagePath is not provided", () => {
132
+ const props = {
133
+ bucketName: "test-website-bucket",
134
+ indexFile: "index.html",
135
+ errorFile: "error.html",
136
+ };
137
+ new lib_1.Website(stack, "TestWebsite", props);
138
+ const template = assertions_1.Template.fromStack(stack);
139
+ template.hasResourceProperties("AWS::CloudFront::Distribution", {
140
+ DistributionConfig: {
141
+ CustomErrorResponses: [
142
+ {
143
+ ErrorCode: 404,
144
+ ResponseCode: 404,
145
+ ResponsePagePath: "/404.html",
146
+ ErrorCachingMinTTL: 1800,
147
+ },
148
+ ],
149
+ },
150
+ });
151
+ });
152
+ });
153
+ describe("Domain configuration", () => {
154
+ const domainConfig = {
155
+ domainName: "example.com",
156
+ subdomainName: "www",
157
+ certificateArn: "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
158
+ };
159
+ test("configures CloudFront distribution with custom domain", () => {
160
+ const props = {
161
+ bucketName: "test-website-bucket",
162
+ indexFile: "index.html",
163
+ errorFile: "error.html",
164
+ domainConfig,
165
+ };
166
+ new lib_1.Website(stack, "TestWebsite", props);
167
+ const template = assertions_1.Template.fromStack(stack);
168
+ template.hasResourceProperties("AWS::CloudFront::Distribution", {
169
+ DistributionConfig: {
170
+ Aliases: ["www.example.com"],
171
+ ViewerCertificate: {
172
+ AcmCertificateArn: domainConfig.certificateArn,
173
+ SslSupportMethod: "sni-only",
174
+ },
175
+ },
176
+ });
177
+ });
178
+ test("creates Route53 A record when domain config is provided", () => {
179
+ const props = {
180
+ bucketName: "test-website-bucket",
181
+ indexFile: "index.html",
182
+ errorFile: "error.html",
183
+ domainConfig,
184
+ };
185
+ new lib_1.Website(stack, "TestWebsite", props);
186
+ const template = assertions_1.Template.fromStack(stack);
187
+ template.hasResourceProperties("AWS::Route53::RecordSet", {
188
+ Type: "A",
189
+ Name: "www.example.com.",
190
+ AliasTarget: {
191
+ DNSName: assertions_1.Match.anyValue(),
192
+ HostedZoneId: assertions_1.Match.anyValue(),
193
+ },
194
+ });
195
+ });
196
+ test("handles domain without subdomain", () => {
197
+ const domainConfigWithoutSub = {
198
+ domainName: "example.com",
199
+ subdomainName: "",
200
+ certificateArn: "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
201
+ };
202
+ const props = {
203
+ bucketName: "test-website-bucket",
204
+ indexFile: "index.html",
205
+ errorFile: "error.html",
206
+ domainConfig: domainConfigWithoutSub,
207
+ };
208
+ new lib_1.Website(stack, "TestWebsite", props);
209
+ const template = assertions_1.Template.fromStack(stack);
210
+ template.hasResourceProperties("AWS::CloudFront::Distribution", {
211
+ DistributionConfig: {
212
+ Aliases: ["example.com"],
213
+ },
214
+ });
215
+ template.hasResourceProperties("AWS::Route53::RecordSet", {
216
+ Type: "A",
217
+ Name: "example.com.",
218
+ });
219
+ });
220
+ });
221
+ describe("Private methods", () => {
222
+ test("_getFullDomainName returns correct domain with subdomain", () => {
223
+ const props = {
224
+ bucketName: "test-bucket",
225
+ indexFile: "index.html",
226
+ errorFile: "error.html",
227
+ };
228
+ const construct = new lib_1.Website(stack, "TestWebsite", props);
229
+ // Access private method through bracket notation for testing
230
+ const getFullDomainName = construct._getFullDomainName;
231
+ const domainConfig = {
232
+ domainName: "example.com",
233
+ subdomainName: "blog",
234
+ certificateArn: "arn:test",
235
+ };
236
+ const result = getFullDomainName(domainConfig);
237
+ expect(result).toBe("blog.example.com");
238
+ });
239
+ test("_getFullDomainName returns root domain when subdomain is empty", () => {
240
+ const props = {
241
+ bucketName: "test-bucket",
242
+ indexFile: "index.html",
243
+ errorFile: "error.html",
244
+ };
245
+ const construct = new lib_1.Website(stack, "TestWebsite", props);
246
+ const getFullDomainName = construct._getFullDomainName;
247
+ const domainConfig = {
248
+ domainName: "example.com",
249
+ subdomainName: "",
250
+ certificateArn: "arn:test",
251
+ };
252
+ const result = getFullDomainName(domainConfig);
253
+ expect(result).toBe("example.com");
254
+ });
255
+ });
256
+ describe("Edge cases", () => {
257
+ test("handles minimal configuration", () => {
258
+ const props = {
259
+ bucketName: "minimal-bucket",
260
+ indexFile: "index.html",
261
+ errorFile: "error.html",
262
+ };
263
+ expect(() => {
264
+ new lib_1.Website(stack, "TestWebsite", props);
265
+ }).not.toThrow();
266
+ const template = assertions_1.Template.fromStack(stack);
267
+ template.resourceCountIs("AWS::S3::Bucket", 1);
268
+ template.resourceCountIs("AWS::CloudFront::Distribution", 1);
269
+ template.resourceCountIs("AWS::CloudFront::CloudFrontOriginAccessIdentity", 1);
270
+ });
271
+ test("does not create Route53 resources when domain config is not provided", () => {
272
+ const props = {
273
+ bucketName: "test-bucket",
274
+ indexFile: "index.html",
275
+ errorFile: "error.html",
276
+ };
277
+ new lib_1.Website(stack, "TestWebsite", props);
278
+ const template = assertions_1.Template.fromStack(stack);
279
+ template.resourceCountIs("AWS::Route53::RecordSet", 0);
280
+ });
281
+ });
282
+ });
283
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2Vic2l0ZS1jb25zdHJ1Y3QudGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIndlYnNpdGUtY29uc3RydWN0LnRlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQUFBLHVEQUF5RDtBQUN6RCxpREFBbUM7QUFDbkMsZ0NBQTZEO0FBRTdELFFBQVEsQ0FBQyxTQUFTLEVBQUUsR0FBRyxFQUFFO0lBQ3ZCLElBQUksR0FBWSxDQUFDO0lBQ2pCLElBQUksS0FBZ0IsQ0FBQztJQUVyQixVQUFVLENBQUMsR0FBRyxFQUFFO1FBQ2QsR0FBRyxHQUFHLElBQUksR0FBRyxDQUFDLEdBQUcsRUFBRSxDQUFDO1FBQ3BCLEtBQUssR0FBRyxJQUFJLEdBQUcsQ0FBQyxLQUFLLENBQUMsR0FBRyxFQUFFLFdBQVcsRUFBRTtZQUN0QyxHQUFHLEVBQUUsRUFBRSxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sRUFBRSxXQUFXLEVBQUU7U0FDdEQsQ0FBQyxDQUFDO1FBQ0gsS0FBSyxDQUFDLElBQUksQ0FBQyxVQUFVLENBQ25CLDBFQUEwRSxFQUMxRTtZQUNFLEVBQUUsRUFBRSwyQkFBMkI7WUFDL0IsSUFBSSxFQUFFLGNBQWM7U0FDckIsQ0FDRixDQUFDO0lBQ0osQ0FBQyxDQUFDLENBQUM7SUFFSCxRQUFRLENBQUMscUJBQXFCLEVBQUUsR0FBRyxFQUFFO1FBQ25DLElBQUksQ0FBQyw0Q0FBNEMsRUFBRSxHQUFHLEVBQUU7WUFDdEQsTUFBTSxLQUFLLEdBQWlCO2dCQUMxQixVQUFVLEVBQUUscUJBQXFCO2dCQUNqQyxTQUFTLEVBQUUsWUFBWTtnQkFDdkIsU0FBUyxFQUFFLFlBQVk7YUFDeEIsQ0FBQztZQUVGLElBQUksYUFBTyxDQUFDLEtBQUssRUFBRSxhQUFhLEVBQUUsS0FBSyxDQUFDLENBQUM7WUFFekMsTUFBTSxRQUFRLEdBQUcscUJBQVEsQ0FBQyxTQUFTLENBQUMsS0FBSyxDQUFDLENBQUM7WUFFM0Ms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Q0FBQyxpQkFBaUIsRUFBRSxDQUFDLENBQUMsQ0FBQztZQUMvQyxRQUFRLENBQUMsZUFBZSxDQUFDLCtCQUErQixFQUFFLENBQUMsQ0FBQyxDQUFDO1lBQzdELFFBQVEsQ0FBQyxlQUFlLENBQ3RCLGlEQUFpRCxFQUNqRCxDQUFDLENBQ0YsQ0FBQztRQUNKLENBQUMsQ0FBQyxDQUFDO1FBRUgsSUFBSSxDQUFDLHNFQUFzRSxFQUFFLEdBQUcsRUFBRTtZQUNoRixNQUFNLEtBQUssR0FBaUI7Z0JBQzFCLFVBQVUsRUFBRSxhQUFhO2dCQUN6QixTQUFTLEVBQUUsWUFBWTtnQkFDdkIsU0FBUyxFQUFFLFlBQVk7YUFDeEIsQ0FBQztZQUVGLElBQUksYUFBTyxDQUFDLEtBQUssRUFBRSxhQUFhLEVBQUUsS0FBSyxDQUFDLENBQUM7WUFFekMsTUFBTSxRQUFRLEdBQUcscUJBQVEsQ0FBQyxTQUFTLENBQUMsS0FBSyxDQUFDLENBQUM7WUFDM0MsUUFBUSxDQUFDLGVBQWUsQ0FBQyx5QkFBeUIsRUFBRSxDQUFDLENBQUMsQ0FBQztRQUN6RCxDQUFDLENBQUMsQ0FBQztJQUNMLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyxDQUFDLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBUZW1wbGF0ZSwgTWF0Y2ggfSBmcm9tIFwiYXdzLWNkay1saWIvYXNzZXJ0aW9uc1wiO1xuaW1wb3J0ICogYXMgY2RrIGZyb20gXCJhd3MtY2RrLWxpYlwiO1xuaW1wb3J0IHsgV2Vic2l0ZSwgV2Vic2l0ZVByb3BzLCBEb21haW5Db25maWcgfSBmcm9tIFwiLi4vbGliXCI7XG5cbmRlc2NyaWJlKFwiV2Vic2l0ZVwiLCAoKSA9PiB7XG4gIGxldCBhcHA6IGNkay5BcHA7XG4gIGxldCBzdGFjazogY2RrLlN0YWNrO1xuXG4gIGJlZm9yZUVhY2goKCkgPT4ge1xuICAgIGFwcCA9IG5ldyBjZGsuQXBwKCk7XG4gICAgc3RhY2sgPSBuZXcgY2RrLlN0YWNrKGFwcCwgXCJUZXN0U3RhY2tcIiwge1xuICAgICAgZW52OiB7IGFjY291bnQ6IFwiMTIzNDU2Nzg5MDEyXCIsIHJlZ2lvbjogXCJ1cy1lYXN0LTFcIiB9LFxuICAgIH0pO1xuICAgIHN0YWNrLm5vZGUuc2V0Q29udGV4dChcbiAgICAgIFwiaG9zdGVkLXpvbmU6YWNjb3VudD0xMjM0NTY3ODkwMTI6ZG9tYWluTmFtZT1leGFtcGxlLmNvbTpyZWdpb249dXMtZWFzdC0xXCIsXG4gICAgICB7XG4gICAgICAgIElkOiBcIi9ob3N0ZWR6b25lL1oxMjM0NTY3ODkwMTJcIixcbiAgICAgICAgTmFtZTogXCJleGFtcGxlLmNvbS5cIixcbiAgICAgIH0sXG4gICAgKTtcbiAgfSk7XG5cbiAgZGVzY3JpYmUoXCJCYXNpYyBmdW5jdGlvbmFsaXR5XCIsICgpID0+IHtcbiAgICB0ZXN0KFwiY3JlYXRlcyBTMyBidWNrZXQgd2l0aCBiYXNpYyBjb25maWd1cmF0aW9uXCIsICgpID0+IHtcbiAgICAgIGNvbnN0IHByb3BzOiBXZWJzaXRlUHJvcHMgPSB7XG4gICAgICAgIGJ1Y2tldE5hbWU6IFwidGVzdC13ZWJzaXRlLWJ1Y2tldFwiLFxuICAgICAgICBpbmRleEZpbGU6IFwiaW5kZXguaHRtbFwiLFxuICAgICAgICBlcnJvckZpbGU6IFwiZXJyb3IuaHRtbFwiLFxuICAgICAgfTtcblxuICAgICAgbmV3IFdlYnNpdGUoc3RhY2ssIFwiVGVzdFdlYnNpdGVcIiwgcHJvcHMpO1xuXG4gICAgICBjb25zdCB0ZW1wbGF0ZSA9IFRlbXBsYXRlLmZyb21TdGFjayhzdGFjayk7XG5cbiAgICAgIHRlbXBsYXRlLmhhc1Jlc291cmNlUHJvcGVydGllcyhcIkFXUzo6UzM6OkJ1Y2tldFwiLCB7XG4gICAgICAgIFdlYnNpdGVDb25maWd1cmF0aW9uOiB7XG4gICAgICAgICAgSW5kZXhEb2N1bWVudDogXCJpbmRleC5odG1sXCIsXG4gICAgICAgICAgRXJyb3JEb2N1bWVudDogXCJlcnJvci5odG1sXCIsXG4gICAgICAgIH0sXG4gICAgICAgIEJ1Y2tldEVuY3J5cHRpb246IHtcbiAgICAgICAgICBTZXJ2ZXJTaWRlRW5jcnlwdGlvbkNvbmZpZ3VyYXRpb246IFtcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgU2VydmVyU2lkZUVuY3J5cHRpb25CeURlZmF1bHQ6IHtcbiAgICAgICAgICAgICAgICBTU0VBbGdvcml0aG06IFwiQUVTMjU2XCIsXG4gICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICB9LFxuICAgICAgICAgIF0sXG4gICAgICAgIH0sXG4gICAgICB9KTtcbiAgICB9KTtcblxuICAgIHRlc3QoXCJjcmVhdGVzIENsb3VkRnJvbnQgT3JpZ2luIEFjY2VzcyBJZGVudGl0eVwiLCAoKSA9PiB7XG4gICAgICBjb25zdCBwcm9wczogV2Vic2l0ZVByb3BzID0ge1xuICAgICAgICBidWNrZXROYW1lOiBcInRlc3Qtd2Vic2l0ZS1idWNrZXRcIixcbiAgICAgICAgaW5kZXhGaWxlOiBcImluZGV4Lmh0bWxcIixcbiAgICAgICAgZXJyb3JGaWxlOiBcImVycm9yLmh0bWxcIixcbiAgICAgIH07XG5cbiAgICAgIG5ldyBXZWJzaXRlKHN0YWNrLCBcIlRlc3RXZWJzaXRlXCIsIHByb3BzKTtcblxuICAgICAgY29uc3QgdGVtcGxhdGUgPSBUZW1wbGF0ZS5mcm9tU3RhY2soc3RhY2spO1xuXG4gICAgICB0ZW1wbGF0ZS5oYXNSZXNvdXJjZVByb3BlcnRpZXMoXG4gICAgICAgIFwiQVdTOjpDbG91ZEZyb250OjpDbG91ZEZyb250T3JpZ2luQWNjZXNzSWRlbnRpdHlcIixcbiAgICAgICAge1xuICAgICAgICAgIENsb3VkRnJvbnRPcmlnaW5BY2Nlc3NJZGVudGl0eUNvbmZpZzoge1xuICAgICAgICAgICAgQ29tbWVudDogTWF0Y2guYW55VmFsdWUoKSxcbiAgICAgICAgICB9LFxuICAgICAgICB9LFxuICAgICAgKTtcbiAgICB9KTtcblxuICAgIHRlc3QoXCJjcmVhdGVzIENsb3VkRnJvbnQgZGlzdHJpYnV0aW9uIHdpdGggY29ycmVjdCBjb25maWd1cmF0aW9uXCIsICgpID0+IHtcbiAgICAgIGNvbnN0IHByb3BzOiBXZWJzaXRlUHJvcHMgPSB7XG4gICAgICAgIGJ1Y2tldE5hbWU6IFwidGVzdC13ZWJzaXRlLWJ1Y2tldFwiLFxuICAgICAgICBpbmRleEZpbGU6IFwiaW5kZXguaHRtbFwiLFxuICAgICAgICBlcnJvckZpbGU6IFwiZXJyb3IuaHRtbFwiLFxuICAgICAgfTtcblxuICAgICAgbmV3IFdlYnNpdGUoc3RhY2ssIFwiVGVzdFdlYnNpdGVcIiwgcHJvcHMpO1xuXG4gICAgICBjb25zdCB0ZW1wbGF0ZSA9IFRlbXBsYXRlLmZyb21TdGFjayhzdGFjayk7XG5cbiAgICAgIHRlbXBsYXRlLmhhc1Jlc291cmNlUHJvcGVydGllcyhcIkFXUzo6Q2xvdWRGcm9udDo6RGlzdHJpYnV0aW9uXCIsIHtcbiAgICAgICAgRGlzdHJpYnV0aW9uQ29uZmlnOiB7XG4gICAgICAgICAgRGVmYXVsdENhY2hlQmVoYXZpb3I6IHtcbiAgICAgICAgICAgIFZpZXdlclByb3RvY29sUG9saWN5OiBcInJlZGlyZWN0LXRvLWh0dHBzXCIsXG4gICAgICAgICAgfSxcbiAgICAgICAgICBDdXN0b21FcnJvclJlc3BvbnNlczogW1xuICAgICAgICAgICAge1xuICAgICAgICAgICAgICBFcnJvckNvZGU6IDQwNCxcbiAgICAgICAgICAgICAgUmVzcG9uc2VDb2RlOiA0MDQsXG4gICAgICAgICAgICAgIFJlc3BvbnNlUGFnZVBhdGg6IFwiLzQwNC5odG1sXCIsXG4gICAgICAgICAgICAgIEVycm9yQ2FjaGluZ01pblRUTDogMTgwMCxcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgXSxcbiAgICAgICAgICBQcmljZUNsYXNzOiBcIlByaWNlQ2xhc3NfMTAwXCIsXG4gICAgICAgICAgRW5hYmxlZDogdHJ1ZSxcbiAgICAgICAgfSxcbiAgICAgIH0pO1xuICAgIH0pO1xuICB9KTtcblxuICBkZXNjcmliZShcIkN1c3RvbSBlcnJvciBwYWdlIGNvbmZpZ3VyYXRpb25cIiwgKCkgPT4ge1xuICAgIHRlc3QoXCJ1c2VzIGN1c3RvbSBub3QgZm91bmQgcmVzcG9uc2UgcGFnZSBwYXRoXCIsICgpID0+IHtcbiAgICAgIGNvbnN0IHByb3BzOiBXZWJzaXRlUHJvcHMgPSB7XG4gICAgICAgIGJ1Y2tldE5hbWU6IFwidGVzdC13ZWJzaXRlLWJ1Y2tldFwiLFxuICAgICAgICBpbmRleEZpbGU6IFwiaW5kZXguaHRtbFwiLFxuICAgICAgICBlcnJvckZpbGU6IFwiZXJyb3IuaHRtbFwiLFxuICAgICAgICBub3RGb3VuZFJlc3BvbnNlUGFnZVBhdGg6IFwiL2N1c3RvbS00MDQuaHRtbFwiLFxuICAgICAgfTtcblxuICAgICAgbmV3IFdlYnNpdGUoc3RhY2ssIFwiVGVzdFdlYnNpdGVcIiwgcHJvcHMpO1xuXG4gICAgICBjb25zdCB0ZW1wbGF0ZSA9IFRlbXBsYXRlLmZyb21TdGFjayhzdGFjayk7XG5cbiAgICAgIHRlbXBsYXRlLmhhc1Jlc291cmNlUHJvcGVydGllcyhcIkFXUzo6Q2xvdWRGcm9udDo6RGlzdHJpYnV0aW9uXCIsIHtcbiAgICAgICAgRGlzdHJpYnV0aW9uQ29uZmlnOiB7XG4gICAgICAgICAgQ3VzdG9tRXJyb3JSZXNwb25zZXM6IFtcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgRXJyb3JDb2RlOiA0MDQsXG4gICAgICAgICAgICAgIFJlc3BvbnNlQ29kZTogNDA0LFxuICAgICAgICAgICAgICBSZXNwb25zZVBhZ2VQYXRoOiBcIi9jdXN0b20tNDA0Lmh0bWxcIixcbiAgICAgICAgICAgICAgRXJyb3JDYWNoaW5nTWluVFRMOiAxODAwLFxuICAgICAgICAgICAgfSxcbiAgICAgICAgICBdLFxuICAgICAgICB9LFxuICAgICAgfSk7XG4gICAgfSk7XG5cbiAgICB0ZXN0KFwidXNlcyBkZWZhdWx0IDQwNC5odG1sIHdoZW4gbm90Rm91bmRSZXNwb25zZVBhZ2VQYXRoIGlzIG5vdCBwcm92aWRlZFwiLCAoKSA9PiB7XG4gICAgICBjb25zdCBwcm9wczogV2Vic2l0ZVByb3BzID0ge1xuICAgICAgICBidWNrZXROYW1lOiBcInRlc3Qtd2Vic2l0ZS1idWNrZXRcIixcbiAgICAgICAgaW5kZXhGaWxlOiBcImluZGV4Lmh0bWxcIixcbiAgICAgICAgZXJyb3JGaWxlOiBcImVycm9yLmh0bWxcIixcbiAgICAgIH07XG5cbiAgICAgIG5ldyBXZWJzaXRlKHN0YWNrLCBcIlRlc3RXZWJzaXRlXCIsIHByb3BzKTtcblxuICAgICAgY29uc3QgdGVtcGxhdGUgPSBUZW1wbGF0ZS5mcm9tU3RhY2soc3RhY2spO1xuXG4gICAgICB0ZW1wbGF0ZS5oYXNSZXNvdXJjZVByb3BlcnRpZXMoXCJBV1M6OkNsb3VkRnJvbnQ6OkRpc3RyaWJ1dGlvblwiLCB7XG4gICAgICAgIERpc3RyaWJ1dGlvbkNvbmZpZzoge1xuICAgICAgICAgIEN1c3RvbUVycm9yUmVzcG9uc2VzOiBbXG4gICAgICAgICAgICB7XG4gICAgICAgICAgICAgIEVycm9yQ29kZTogNDA0LFxuICAgICAgICAgICAgICBSZXNwb25zZUNvZGU6IDQwNCxcbiAgICAgICAgICAgICAgUmVzcG9uc2VQYWdlUGF0aDogXCIvNDA0Lmh0bWxcIixcbiAgICAgICAgICAgICAgRXJyb3JDYWNoaW5nTWluVFRMOiAxODAwLFxuICAgICAgICAgICAgfSxcbiAgICAgICAgICBdLFxuICAgICAgICB9LFxuICAgICAgfSk7XG4gICAgfSk7XG4gIH0pO1xuXG4gIGRlc2NyaWJlKFwiRG9tYWluIGNvbmZpZ3VyYXRpb25cIiwgKCkgPT4ge1xuICAgIGNvbnN0IGRvbWFpbkNvbmZpZzogRG9tYWluQ29uZmlnID0ge1xuICAgICAgZG9tYWluTmFtZTogXCJleGFtcGxlLmNvbVwiLFxuICAgICAgc3ViZG9tYWluTmFtZTogXCJ3d3dcIixcbiAgICAgIGNlcnRpZmljYXRlQXJuOlxuICAgICAgICBcImFybjphd3M6YWNtOnVzLWVhc3QtMToxMjM0NTY3ODkwMTI6Y2VydGlmaWNhdGUvMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MDEyXCIsXG4gICAgfTtcblxuICAgIHRlc3QoXCJjb25maWd1cmVzIENsb3VkRnJvbnQgZGlzdHJpYnV0aW9uIHdpdGggY3VzdG9tIGRvbWFpblwiLCAoKSA9PiB7XG4gICAgICBjb25zdCBwcm9wczogV2Vic2l0ZVByb3BzID0ge1xuICAgICAgICBidWNrZXROYW1lOiBcInRlc3Qtd2Vic2l0ZS1idWNrZXRcIixcbiAgICAgICAgaW5kZXhGaWxlOiBcImluZGV4Lmh0bWxcIixcbiAgICAgICAgZXJyb3JGaWxlOiBcImVycm9yLmh0bWxcIixcbiAgICAgICAgZG9tYWluQ29uZmlnLFxuICAgICAgfTtcblxuICAgICAgbmV3IFdlYnNpdGUoc3RhY2ssIFwiVGVzdFdlYnNpdGVcIiwgcHJvcHMpO1xuXG4gICAgICBjb25zdCB0ZW1wbGF0ZSA9IFRlbXBsYXRlLmZyb21TdGFjayhzdGFjayk7XG5cbiAgICAgIHRlbXBsYXRlLmhhc1Jlc291cmNlUHJvcGVydGllcyhcIkFXUzo6Q2xvdWRGcm9udDo6RGlzdHJpYnV0aW9uXCIsIHtcbiAgICAgICAgRGlzdHJpYnV0aW9uQ29uZmlnOiB7XG4gICAgICAgICAgQWxpYXNlczogW1wid3d3LmV4YW1wbGUuY29tXCJdLFxuICAgICAgICAgIFZpZXdlckNlcnRpZmljYXRlOiB7XG4gICAgICAgICAgICBBY21DZXJ0aWZpY2F0ZUFybjogZG9tYWluQ29uZmlnLmNlcnRpZmljYXRlQXJuLFxuICAgICAgICAgICAgU3NsU3VwcG9ydE1ldGhvZDogXCJzbmktb25seVwiLFxuICAgICAgICAgIH0sXG4gICAgICAgIH0sXG4gICAgICB9KTtcbiAgICB9KTtcblxuICAgIHRlc3QoXCJjcmVhdGVzIFJvdXRlNTMgQSByZWNvcmQgd2hlbiBkb21haW4gY29uZmlnIGlzIHByb3ZpZGVkXCIsICgpID0+IHtcbiAgICAgIGNvbnN0IHByb3BzOiBXZWJzaXRlUHJvcHMgPSB7XG4gICAgICAgIGJ1Y2tldE5hbWU6IFwidGVzdC13ZWJzaXRlLWJ1Y2tldFwiLFxuICAgICAgICBpbmRleEZpbGU6IFwiaW5kZXguaHRtbFwiLFxuICAgICAgICBlcnJvckZpbGU6IFwiZXJyb3IuaHRtbFwiLFxuICAgICAgICBkb21haW5Db25maWcsXG4gICAgICB9O1xuXG4gICAgICBuZXcgV2Vic2l0ZShzdGFjaywgXCJUZXN0V2Vic2l0ZVwiLCBwcm9wcyk7XG5cbiAgICAgIGNvbnN0IHRlbXBsYXRlID0gVGVtcGxhdGUuZnJvbVN0YWNrKHN0YWNrKTtcblxuICAgICAgdGVtcGxhdGUuaGFzUmVzb3VyY2VQcm9wZXJ0aWVzKFwiQVdTOjpSb3V0ZTUzOjpSZWNvcmRTZXRcIiwge1xuICAgICAgICBUeXBlOiBcIkFcIixcbiAgICAgICAgTmFtZTogXCJ3d3cuZXhhbXBsZS5jb20uXCIsXG4gICAgICAgIEFsaWFzVGFyZ2V0OiB7XG4gICAgICAgICAgRE5TTmFtZTogTWF0Y2guYW55VmFsdWUoKSxcbiAgICAgICAgICBIb3N0ZWRab25lSWQ6IE1hdGNoLmFueVZhbHVlKCksXG4gICAgICAgIH0sXG4gICAgICB9KTtcbiAgICB9KTtcblxuICAgIHRlc3QoXCJoYW5kbGVzIGRvbWFpbiB3aXRob3V0IHN1YmRvbWFpblwiLCAoKSA9PiB7XG4gICAgICBjb25zdCBkb21haW5Db25maWdXaXRob3V0U3ViOiBEb21haW5Db25maWcgPSB7XG4gICAgICAgIGRvbWFpbk5hbWU6IFwiZXhhbXBsZS5jb21cIixcbiAgICAgICAgc3ViZG9tYWluTmFtZTogXCJcIixcbiAgICAgICAgY2VydGlmaWNhdGVBcm46XG4gICAgICAgICAgXCJhcm46YXdzOmFjbTp1cy1lYXN0LTE6MTIzNDU2Nzg5MDEyOmNlcnRpZmljYXRlLzEyMzQ1Njc4LTEyMzQtMTIzNC0xMjM0LTEyMzQ1Njc4OTAxMlwiLFxuICAgICAgfTtcblxuICAgICAgY29uc3QgcHJvcHM6IFdlYnNpdGVQcm9wcyA9IHtcbiAgICAgICAgYnVja2V0TmFtZTogXCJ0ZXN0LXdlYnNpdGUtYnVja2V0XCIsXG4gICAgICAgIGluZGV4RmlsZTogXCJpbmRleC5odG1sXCIsXG4gICAgICAgIGVycm9yRmlsZTogXCJlcnJvci5odG1sXCIsXG4gICAgICAgIGRvbWFpbkNvbmZpZzogZG9tYWluQ29uZmlnV2l0aG91dFN1YixcbiAgICAgIH07XG5cbiAgICAgIG5ldyBXZWJzaXRlKHN0YWNrLCBcIlRlc3RXZWJzaXRlXCIsIHByb3BzKTtcblxuICAgICAgY29uc3QgdGVtcGxhdGUgPSBUZW1wbGF0ZS5mcm9tU3RhY2soc3RhY2spO1xuXG4gICAgICB0ZW1wbGF0ZS5oYXNSZXNvdXJjZVByb3BlcnRpZXMoXCJBV1M6OkNsb3VkRnJvbnQ6OkRpc3RyaWJ1dGlvblwiLCB7XG4gICAgICAgIERpc3RyaWJ1dGlvbkNvbmZpZzoge1xuICAgICAgICAgIEFsaWFzZXM6IFtcImV4YW1wbGUuY29tXCJdLFxuICAgICAgICB9LFxuICAgICAgfSk7XG5cbiAgICAgIHRlbXBsYXRlLmhhc1Jlc291cmNlUHJvcGVydGllcyhcIkFXUzo6Um91dGU1Mzo6UmVjb3JkU2V0XCIsIHtcbiAgICAgICAgVHlwZTogXCJBXCIsXG4gICAgICAgIE5hbWU6IFwiZXhhbXBsZS5jb20uXCIsXG4gICAgICB9KTtcbiAgICB9KTtcbiAgfSk7XG5cbiAgZGVzY3JpYmUoXCJQcml2YXRlIG1ldGhvZHNcIiwgKCkgPT4ge1xuICAgIHRlc3QoXCJfZ2V0RnVsbERvbWFpbk5hbWUgcmV0dXJucyBjb3JyZWN0IGRvbWFpbiB3aXRoIHN1YmRvbWFpblwiLCAoKSA9PiB7XG4gICAgICBjb25zdCBwcm9wczogV2Vic2l0ZVByb3BzID0ge1xuICAgICAgICBidWNrZXROYW1lOiBcInRlc3QtYnVja2V0XCIsXG4gICAgICAgIGluZGV4RmlsZTogXCJpbmRleC5odG1sXCIsXG4gICAgICAgIGVycm9yRmlsZTogXCJlcnJvci5odG1sXCIsXG4gICAgICB9O1xuXG4gICAgICBjb25zdCBjb25zdHJ1Y3QgPSBuZXcgV2Vic2l0ZShzdGFjaywgXCJUZXN0V2Vic2l0ZVwiLCBwcm9wcyk7XG5cbiAgICAgIC8vIEFjY2VzcyBwcml2YXRlIG1ldGhvZCB0aHJvdWdoIGJyYWNrZXQgbm90YXRpb24gZm9yIHRlc3RpbmdcbiAgICAgIGNvbnN0IGdldEZ1bGxEb21haW5OYW1lID0gKGNvbnN0cnVjdCBhcyBhbnkpLl9nZXRGdWxsRG9tYWluTmFtZTtcblxuICAgICAgY29uc3QgZG9tYWluQ29uZmlnOiBEb21haW5Db25maWcgPSB7XG4gICAgICAgIGRvbWFpbk5hbWU6IFwiZXhhbXBsZS5jb21cIixcbiAgICAgICAgc3ViZG9tYWluTmFtZTogXCJibG9nXCIsXG4gICAgICAgIGNlcnRpZmljYXRlQXJuOiBcImFybjp0ZXN0XCIsXG4gICAgICB9O1xuXG4gICAgICBjb25zdCByZXN1bHQgPSBnZXRGdWxsRG9tYWluTmFtZShkb21haW5Db25maWcpO1xuICAgICAgZXhwZWN0KHJlc3VsdCkudG9CZShcImJsb2cuZXhhbXBsZS5jb21cIik7XG4gICAgfSk7XG5cbiAgICB0ZXN0KFwiX2dldEZ1bGxEb21haW5OYW1lIHJldHVybnMgcm9vdCBkb21haW4gd2hlbiBzdWJkb21haW4gaXMgZW1wdHlcIiwgKCkgPT4ge1xuICAgICAgY29uc3QgcHJvcHM6IFdlYnNpdGVQcm9wcyA9IHtcbiAgICAgICAgYnVja2V0TmFtZTogXCJ0ZXN0LWJ1Y2tldFwiLFxuICAgICAgICBpbmRleEZpbGU6IFwiaW5kZXguaHRtbFwiLFxuICAgICAgICBlcnJvckZpbGU6IFwiZXJyb3IuaHRtbFwiLFxuICAgICAgfTtcblxuICAgICAgY29uc3QgY29uc3RydWN0ID0gbmV3IFdlYnNpdGUoc3RhY2ssIFwiVGVzdFdlYnNpdGVcIiwgcHJvcHMpO1xuXG4gICAgICBjb25zdCBnZXRGdWxsRG9tYWluTmFtZSA9IChjb25zdHJ1Y3QgYXMgYW55KS5fZ2V0RnVsbERvbWFpbk5hbWU7XG5cbiAgICAgIGNvbnN0IGRvbWFpbkNvbmZpZzogRG9tYWluQ29uZmlnID0ge1xuICAgICAgICBkb21haW5OYW1lOiBcImV4YW1wbGUuY29tXCIsXG4gICAgICAgIHN1YmRvbWFpbk5hbWU6IFwiXCIsXG4gICAgICAgIGNlcnRpZmljYXRlQXJuOiBcImFybjp0ZXN0XCIsXG4gICAgICB9O1xuXG4gICAgICBjb25zdCByZXN1bHQgPSBnZXRGdWxsRG9tYWluTmFtZShkb21haW5Db25maWcpO1xuICAgICAgZXhwZWN0KHJlc3VsdCkudG9CZShcImV4YW1wbGUuY29tXCIpO1xuICAgIH0pO1xuICB9KTtcblxuICBkZXNjcmliZShcIkVkZ2UgY2FzZXNcIiwgKCkgPT4ge1xuICAgIHRlc3QoXCJoYW5kbGVzIG1pbmltYWwgY29uZmlndXJhdGlvblwiLCAoKSA9PiB7XG4gICAgICBjb25zdCBwcm9wczogV2Vic2l0ZVByb3BzID0ge1xuICAgICAgICBidWNrZXROYW1lOiBcIm1pbmltYWwtYnVja2V0XCIsXG4gICAgICAgIGluZGV4RmlsZTogXCJpbmRleC5odG1sXCIsXG4gICAgICAgIGVycm9yRmlsZTogXCJlcnJvci5odG1sXCIsXG4gICAgICB9O1xuXG4gICAgICBleHBlY3QoKCkgPT4ge1xuICAgICAgICBuZXcgV2Vic2l0ZShzdGFjaywgXCJUZXN0V2Vic2l0ZVwiLCBwcm9wcyk7XG4gICAgICB9KS5ub3QudG9UaHJvdygpO1xuXG4gICAgICBjb25zdCB0ZW1wbGF0ZSA9IFRlbXBsYXRlLmZyb21TdGFjayhzdGFjayk7XG4gICAgICB0ZW1wbGF0ZS5yZXNvdXJjZUNvdW50SXMoXCJBV1M6OlMzOjpCdWNrZXRcIiwgMSk7XG4gICAgICB0ZW1wbGF0ZS5yZXNvdXJjZUNvdW50SXMoXCJBV1M6OkNsb3VkRnJvbnQ6OkRpc3RyaWJ1dGlvblwiLCAxKTtcbiAgICAgIHRlbXBsYXRlLnJlc291cmNlQ291bnRJcyhcbiAgICAgICAgXCJBV1M6OkNsb3VkRnJvbnQ6OkNsb3VkRnJvbnRPcmlnaW5BY2Nlc3NJZGVudGl0eVwiLFxuICAgICAgICAxLFxuICAgICAgKTtcbiAgICB9KTtcblxuICAgIHRlc3QoXCJkb2VzIG5vdCBjcmVhdGUgUm91dGU1MyByZXNvdXJjZXMgd2hlbiBkb21haW4gY29uZmlnIGlzIG5vdCBwcm92aWRlZFwiLCAoKSA9PiB7XG4gICAgICBjb25zdCBwcm9wczogV2Vic2l0ZVByb3BzID0ge1xuICAgICAgICBidWNrZXROYW1lOiBcInRlc3QtYnVja2V0XCIsXG4gICAgICAgIGluZGV4RmlsZTogXCJpbmRleC5odG1sXCIsXG4gICAgICAgIGVycm9yRmlsZTogXCJlcnJvci5odG1sXCIsXG4gICAgICB9O1xuXG4gICAgICBuZXcgV2Vic2l0ZShzdGFjaywgXCJUZXN0V2Vic2l0ZVwiLCBwcm9wcyk7XG5cbiAgICAgIGNvbnN0IHRlbXBsYXRlID0gVGVtcGxhdGUuZnJvbVN0YWNrKHN0YWNrKTtcbiAgICAgIHRlbXBsYXRlLnJlc291cmNlQ291bnRJcyhcIkFXUzo6Um91dGU1Mzo6UmVjb3JkU2V0XCIsIDApO1xuICAgIH0pO1xuICB9KTtcbn0pO1xuIl19
@@ -0,0 +1,321 @@
1
+ import { Template, Match } from "aws-cdk-lib/assertions";
2
+ import * as cdk from "aws-cdk-lib";
3
+ import { Website, WebsiteProps, DomainConfig } from "../lib";
4
+
5
+ describe("Website", () => {
6
+ let app: cdk.App;
7
+ let stack: cdk.Stack;
8
+
9
+ beforeEach(() => {
10
+ app = new cdk.App();
11
+ stack = new cdk.Stack(app, "TestStack", {
12
+ env: { account: "123456789012", region: "us-east-1" },
13
+ });
14
+ stack.node.setContext(
15
+ "hosted-zone:account=123456789012:domainName=example.com:region=us-east-1",
16
+ {
17
+ Id: "/hostedzone/Z123456789012",
18
+ Name: "example.com.",
19
+ },
20
+ );
21
+ });
22
+
23
+ describe("Basic functionality", () => {
24
+ test("creates S3 bucket with basic configuration", () => {
25
+ const props: WebsiteProps = {
26
+ bucketName: "test-website-bucket",
27
+ indexFile: "index.html",
28
+ errorFile: "error.html",
29
+ };
30
+
31
+ new Website(stack, "TestWebsite", props);
32
+
33
+ const template = Template.fromStack(stack);
34
+
35
+ template.hasResourceProperties("AWS::S3::Bucket", {
36
+ WebsiteConfiguration: {
37
+ IndexDocument: "index.html",
38
+ ErrorDocument: "error.html",
39
+ },
40
+ BucketEncryption: {
41
+ ServerSideEncryptionConfiguration: [
42
+ {
43
+ ServerSideEncryptionByDefault: {
44
+ SSEAlgorithm: "AES256",
45
+ },
46
+ },
47
+ ],
48
+ },
49
+ });
50
+ });
51
+
52
+ test("creates CloudFront Origin Access Identity", () => {
53
+ const props: WebsiteProps = {
54
+ bucketName: "test-website-bucket",
55
+ indexFile: "index.html",
56
+ errorFile: "error.html",
57
+ };
58
+
59
+ new Website(stack, "TestWebsite", props);
60
+
61
+ const template = Template.fromStack(stack);
62
+
63
+ template.hasResourceProperties(
64
+ "AWS::CloudFront::CloudFrontOriginAccessIdentity",
65
+ {
66
+ CloudFrontOriginAccessIdentityConfig: {
67
+ Comment: Match.anyValue(),
68
+ },
69
+ },
70
+ );
71
+ });
72
+
73
+ test("creates CloudFront distribution with correct configuration", () => {
74
+ const props: WebsiteProps = {
75
+ bucketName: "test-website-bucket",
76
+ indexFile: "index.html",
77
+ errorFile: "error.html",
78
+ };
79
+
80
+ new Website(stack, "TestWebsite", props);
81
+
82
+ const template = Template.fromStack(stack);
83
+
84
+ template.hasResourceProperties("AWS::CloudFront::Distribution", {
85
+ DistributionConfig: {
86
+ DefaultCacheBehavior: {
87
+ ViewerProtocolPolicy: "redirect-to-https",
88
+ },
89
+ CustomErrorResponses: [
90
+ {
91
+ ErrorCode: 404,
92
+ ResponseCode: 404,
93
+ ResponsePagePath: "/404.html",
94
+ ErrorCachingMinTTL: 1800,
95
+ },
96
+ ],
97
+ PriceClass: "PriceClass_100",
98
+ Enabled: true,
99
+ },
100
+ });
101
+ });
102
+ });
103
+
104
+ describe("Custom error page configuration", () => {
105
+ test("uses custom not found response page path", () => {
106
+ const props: WebsiteProps = {
107
+ bucketName: "test-website-bucket",
108
+ indexFile: "index.html",
109
+ errorFile: "error.html",
110
+ notFoundResponsePagePath: "/custom-404.html",
111
+ };
112
+
113
+ new Website(stack, "TestWebsite", props);
114
+
115
+ const template = Template.fromStack(stack);
116
+
117
+ template.hasResourceProperties("AWS::CloudFront::Distribution", {
118
+ DistributionConfig: {
119
+ CustomErrorResponses: [
120
+ {
121
+ ErrorCode: 404,
122
+ ResponseCode: 404,
123
+ ResponsePagePath: "/custom-404.html",
124
+ ErrorCachingMinTTL: 1800,
125
+ },
126
+ ],
127
+ },
128
+ });
129
+ });
130
+
131
+ test("uses default 404.html when notFoundResponsePagePath is not provided", () => {
132
+ const props: WebsiteProps = {
133
+ bucketName: "test-website-bucket",
134
+ indexFile: "index.html",
135
+ errorFile: "error.html",
136
+ };
137
+
138
+ new Website(stack, "TestWebsite", props);
139
+
140
+ const template = Template.fromStack(stack);
141
+
142
+ template.hasResourceProperties("AWS::CloudFront::Distribution", {
143
+ DistributionConfig: {
144
+ CustomErrorResponses: [
145
+ {
146
+ ErrorCode: 404,
147
+ ResponseCode: 404,
148
+ ResponsePagePath: "/404.html",
149
+ ErrorCachingMinTTL: 1800,
150
+ },
151
+ ],
152
+ },
153
+ });
154
+ });
155
+ });
156
+
157
+ describe("Domain configuration", () => {
158
+ const domainConfig: DomainConfig = {
159
+ domainName: "example.com",
160
+ subdomainName: "www",
161
+ certificateArn:
162
+ "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
163
+ };
164
+
165
+ test("configures CloudFront distribution with custom domain", () => {
166
+ const props: WebsiteProps = {
167
+ bucketName: "test-website-bucket",
168
+ indexFile: "index.html",
169
+ errorFile: "error.html",
170
+ domainConfig,
171
+ };
172
+
173
+ new Website(stack, "TestWebsite", props);
174
+
175
+ const template = Template.fromStack(stack);
176
+
177
+ template.hasResourceProperties("AWS::CloudFront::Distribution", {
178
+ DistributionConfig: {
179
+ Aliases: ["www.example.com"],
180
+ ViewerCertificate: {
181
+ AcmCertificateArn: domainConfig.certificateArn,
182
+ SslSupportMethod: "sni-only",
183
+ },
184
+ },
185
+ });
186
+ });
187
+
188
+ test("creates Route53 A record when domain config is provided", () => {
189
+ const props: WebsiteProps = {
190
+ bucketName: "test-website-bucket",
191
+ indexFile: "index.html",
192
+ errorFile: "error.html",
193
+ domainConfig,
194
+ };
195
+
196
+ new Website(stack, "TestWebsite", props);
197
+
198
+ const template = Template.fromStack(stack);
199
+
200
+ template.hasResourceProperties("AWS::Route53::RecordSet", {
201
+ Type: "A",
202
+ Name: "www.example.com.",
203
+ AliasTarget: {
204
+ DNSName: Match.anyValue(),
205
+ HostedZoneId: Match.anyValue(),
206
+ },
207
+ });
208
+ });
209
+
210
+ test("handles domain without subdomain", () => {
211
+ const domainConfigWithoutSub: DomainConfig = {
212
+ domainName: "example.com",
213
+ subdomainName: "",
214
+ certificateArn:
215
+ "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
216
+ };
217
+
218
+ const props: WebsiteProps = {
219
+ bucketName: "test-website-bucket",
220
+ indexFile: "index.html",
221
+ errorFile: "error.html",
222
+ domainConfig: domainConfigWithoutSub,
223
+ };
224
+
225
+ new Website(stack, "TestWebsite", props);
226
+
227
+ const template = Template.fromStack(stack);
228
+
229
+ template.hasResourceProperties("AWS::CloudFront::Distribution", {
230
+ DistributionConfig: {
231
+ Aliases: ["example.com"],
232
+ },
233
+ });
234
+
235
+ template.hasResourceProperties("AWS::Route53::RecordSet", {
236
+ Type: "A",
237
+ Name: "example.com.",
238
+ });
239
+ });
240
+ });
241
+
242
+ describe("Private methods", () => {
243
+ test("_getFullDomainName returns correct domain with subdomain", () => {
244
+ const props: WebsiteProps = {
245
+ bucketName: "test-bucket",
246
+ indexFile: "index.html",
247
+ errorFile: "error.html",
248
+ };
249
+
250
+ const construct = new Website(stack, "TestWebsite", props);
251
+
252
+ // Access private method through bracket notation for testing
253
+ const getFullDomainName = (construct as any)._getFullDomainName;
254
+
255
+ const domainConfig: DomainConfig = {
256
+ domainName: "example.com",
257
+ subdomainName: "blog",
258
+ certificateArn: "arn:test",
259
+ };
260
+
261
+ const result = getFullDomainName(domainConfig);
262
+ expect(result).toBe("blog.example.com");
263
+ });
264
+
265
+ test("_getFullDomainName returns root domain when subdomain is empty", () => {
266
+ const props: WebsiteProps = {
267
+ bucketName: "test-bucket",
268
+ indexFile: "index.html",
269
+ errorFile: "error.html",
270
+ };
271
+
272
+ const construct = new Website(stack, "TestWebsite", props);
273
+
274
+ const getFullDomainName = (construct as any)._getFullDomainName;
275
+
276
+ const domainConfig: DomainConfig = {
277
+ domainName: "example.com",
278
+ subdomainName: "",
279
+ certificateArn: "arn:test",
280
+ };
281
+
282
+ const result = getFullDomainName(domainConfig);
283
+ expect(result).toBe("example.com");
284
+ });
285
+ });
286
+
287
+ describe("Edge cases", () => {
288
+ test("handles minimal configuration", () => {
289
+ const props: WebsiteProps = {
290
+ bucketName: "minimal-bucket",
291
+ indexFile: "index.html",
292
+ errorFile: "error.html",
293
+ };
294
+
295
+ expect(() => {
296
+ new Website(stack, "TestWebsite", props);
297
+ }).not.toThrow();
298
+
299
+ const template = Template.fromStack(stack);
300
+ template.resourceCountIs("AWS::S3::Bucket", 1);
301
+ template.resourceCountIs("AWS::CloudFront::Distribution", 1);
302
+ template.resourceCountIs(
303
+ "AWS::CloudFront::CloudFrontOriginAccessIdentity",
304
+ 1,
305
+ );
306
+ });
307
+
308
+ test("does not create Route53 resources when domain config is not provided", () => {
309
+ const props: WebsiteProps = {
310
+ bucketName: "test-bucket",
311
+ indexFile: "index.html",
312
+ errorFile: "error.html",
313
+ };
314
+
315
+ new Website(stack, "TestWebsite", props);
316
+
317
+ const template = Template.fromStack(stack);
318
+ template.resourceCountIs("AWS::Route53::RecordSet", 0);
319
+ });
320
+ });
321
+ });
@@ -1,18 +0,0 @@
1
- name: prettier
2
-
3
- on:
4
- push:
5
- pull_request:
6
-
7
- jobs:
8
- build:
9
- runs-on: ubuntu-latest
10
-
11
- steps:
12
- - name: Clone repository
13
- uses: actions/checkout@v4
14
-
15
- - name: Run prettier -check
16
- run: |
17
- npm ci
18
- npm run format:check
@@ -1,24 +0,0 @@
1
- name: Run Tests
2
- on:
3
- push:
4
- pull_request:
5
- jobs:
6
- test:
7
- runs-on: ubuntu-latest
8
- steps:
9
- - name: Checkout repository
10
- uses: actions/checkout@v4
11
-
12
- - name: Set up Node.js
13
- uses: actions/setup-node@v4
14
- with:
15
- node-version: "20"
16
- cache: "npm"
17
-
18
- - name: Install dependencies
19
- run: npm ci
20
-
21
- - name: Run Jest tests
22
- env:
23
- CI: true
24
- run: npm test