@sitblueprint/website-construct 0.1.0 → 0.1.1

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/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,43 @@
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.1] - 2025-09-06
11
+
12
+ ### Added
13
+
14
+ - `DomainConfig` for custom domains (domain + subdomain + ACM certificate).
15
+ - Route 53 `ARecord` creation for CloudFront distribution.
16
+ - Support for custom 404 pages (`notFoundResponsePagePath`).
17
+ - CloudFormation outputs:
18
+ - CloudFront Distribution URL
19
+ - S3 Website URL
20
+ - Custom domain URL (if configured)
21
+
22
+ ### Changed
23
+
24
+ - CloudFront distribution defaults:
25
+ - Redirect all traffic to HTTPS.
26
+ - Cache 404 responses for 30 minutes.
27
+ - Price class limited to `PRICE_CLASS_100` for lower cost.
28
+
29
+ ### Security
30
+
31
+ - Enforced access restrictions with CloudFront Origin Access Identity (OAI).
32
+ - Buckets encrypted with **S3 Managed Encryption** by default.
33
+
34
+ ---
35
+
36
+ ## [v0.1.0] - 2025-08-15
37
+
38
+ ### Added
39
+
40
+ - Initial release of Website.
41
+ - Static website hosting via S3.
42
+ - CloudFront distribution with default caching.
43
+ - Basic index and error page support.
package/lib/index.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { Construct } from "constructs";
2
+ export interface DomainConfig {
3
+ /** The root domain name (e.g., example.com).
4
+ * There must be an associated hosted zone in Route 53 for this domain.
5
+ */
6
+ domainName: string;
7
+ /** The subdomain name */
8
+ subdomainName: string;
9
+ /** The ARN of the SSL certificate to use for the domain. */
10
+ certificateArn: string;
11
+ }
12
+ export interface WebsiteProps {
13
+ /** The name of the S3 bucket that will host the website content. */
14
+ bucketName: string;
15
+ /** The path to the index document that will be served as the default page. */
16
+ indexFile: string;
17
+ /** The path to the error document that will be served when an error occurs. */
18
+ errorFile: string;
19
+ /** Optional configuration for custom domain setup. */
20
+ domainConfig?: DomainConfig;
21
+ /** Optional path to a custom 404 page. If not specified, the error file will be used. */
22
+ notFoundResponsePagePath?: string;
23
+ }
24
+ export declare class Website extends Construct {
25
+ constructor(scope: Construct, id: string, props: WebsiteProps);
26
+ private _getFullDomainName;
27
+ private _getCertificate;
28
+ }
package/lib/index.js ADDED
@@ -0,0 +1,109 @@
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
+ constructor(scope, id, props) {
37
+ super(scope, id);
38
+ const webBucket = new s3.Bucket(this, props.bucketName, {
39
+ websiteIndexDocument: props.indexFile,
40
+ websiteErrorDocument: props.errorFile,
41
+ removalPolicy: cdk.RemovalPolicy.DESTROY,
42
+ encryption: s3.BucketEncryption.S3_MANAGED,
43
+ });
44
+ const oai = new cloudfont.OriginAccessIdentity(this, `${props.bucketName}-OAI`);
45
+ webBucket.addToResourcePolicy(new iam.PolicyStatement({
46
+ actions: ["s3:GetObject"],
47
+ resources: [webBucket.arnForObjects("*")],
48
+ principals: [
49
+ new iam.CanonicalUserPrincipal(oai.cloudFrontOriginAccessIdentityS3CanonicalUserId),
50
+ ],
51
+ }));
52
+ const distribution = new cloudfont.Distribution(this, `${props.bucketName}-distribution`, {
53
+ defaultBehavior: {
54
+ origin: new origins.S3StaticWebsiteOrigin(webBucket),
55
+ viewerProtocolPolicy: cloudfont.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
56
+ },
57
+ errorResponses: [
58
+ {
59
+ httpStatus: 404,
60
+ responseHttpStatus: 404,
61
+ responsePagePath: props.notFoundResponsePagePath || `/404.html`,
62
+ ttl: cdk.Duration.minutes(30),
63
+ },
64
+ ],
65
+ priceClass: cloudfont.PriceClass.PRICE_CLASS_100,
66
+ ...(props.domainConfig
67
+ ? {
68
+ domainNames: [this._getFullDomainName(props.domainConfig)],
69
+ certificate: this._getCertificate(props.domainConfig.certificateArn),
70
+ }
71
+ : {}),
72
+ });
73
+ if (props.domainConfig) {
74
+ const hostedZone = route53.HostedZone.fromLookup(this, "HostedZone", {
75
+ domainName: props.domainConfig.domainName,
76
+ });
77
+ const domainARecord = new route53.ARecord(this, "DomainARecord", {
78
+ zone: hostedZone,
79
+ recordName: this._getFullDomainName(props.domainConfig),
80
+ target: cdk.aws_route53.RecordTarget.fromAlias(new cdk.aws_route53_targets.CloudFrontTarget(distribution)),
81
+ });
82
+ domainARecord.node.addDependency(distribution);
83
+ }
84
+ new cdk.CfnOutput(this, "cloudfront-website-url", {
85
+ value: distribution.distributionDomainName,
86
+ description: "CloudFront Distribution Domain Name",
87
+ });
88
+ new cdk.CfnOutput(this, "s3-website-url", {
89
+ value: webBucket.bucketWebsiteUrl,
90
+ description: "S3 Bucket Website URL",
91
+ });
92
+ if (props.domainConfig) {
93
+ new cdk.CfnOutput(this, "website-url", {
94
+ value: distribution.domainName,
95
+ description: "Website URL",
96
+ });
97
+ }
98
+ }
99
+ _getFullDomainName(domainConfig) {
100
+ return domainConfig.subdomainName
101
+ ? `${domainConfig.subdomainName}.${domainConfig.domainName}`
102
+ : domainConfig.domainName;
103
+ }
104
+ _getCertificate(arn) {
105
+ return certificatemanager.Certificate.fromCertificateArn(this, `website-cert`, arn);
106
+ }
107
+ }
108
+ exports.Website = Website;
109
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQUFBLDJDQUF1QztBQUN2Qyx1REFBeUM7QUFDekMsc0VBQXdEO0FBQ3hELDRFQUE4RDtBQUM5RCx1RkFBeUU7QUFDekUsaURBQW1DO0FBQ25DLHlEQUEyQztBQUMzQyxpRUFBbUQ7QUE4Qm5ELE1BQWEsT0FBUSxTQUFRLHNCQUFTO0lBQ3BDLFlBQVksS0FBZ0IsRUFBRSxFQUFVLEVBQUUsS0FBbUI7UUFDM0QsS0FBSyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsQ0FBQztRQUNqQixNQUFNLFNBQVMsR0FBRyxJQUFJLEVBQUUsQ0FBQyxNQUFNLENBQUMsSUFBSSxFQUFFLEtBQUssQ0FBQyxVQUFVLEVBQUU7WUFDdEQsb0JBQW9CLEVBQUUsS0FBSyxDQUFDLFNBQVM7WUFDckMsb0JBQW9CLEVBQUUsS0FBSyxDQUFDLFNBQVM7WUFDckMsYUFBYSxFQUFFLEdBQUcsQ0FBQyxhQUFhLENBQUMsT0FBTztZQUN4QyxVQUFVLEVBQUUsRUFBRSxDQUFDLGdCQUFnQixDQUFDLFVBQVU7U0FDM0MsQ0FBQyxDQUFDO1FBQ0gsTUFBTSxHQUFHLEdBQUcsSUFBSSxTQUFTLENBQUMsb0JBQW9CLENBQzVDLElBQUksRUFDSixHQUFHLEtBQUssQ0FBQyxVQUFVLE1BQU0sQ0FDMUIsQ0FBQztRQUNGLFNBQVMsQ0FBQyxtQkFBbUIsQ0FDM0IsSUFBSSxHQUFHLENBQUMsZUFBZSxDQUFDO1lBQ3RCLE9BQU8sRUFBRSxDQUFDLGNBQWMsQ0FBQztZQUN6QixTQUFTLEVBQUUsQ0FBQyxTQUFTLENBQUMsYUFBYSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQ3pDLFVBQVUsRUFBRTtnQkFDVixJQUFJLEdBQUcsQ0FBQyxzQkFBc0IsQ0FDNUIsR0FBRyxDQUFDLCtDQUErQyxDQUNwRDthQUNGO1NBQ0YsQ0FBQyxDQUNILENB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QUFrQixDQUN0RCxJQUFJLEVBQ0osY0FBYyxFQUNkLEdBQUcsQ0FDSixDQUFDO0lBQ0osQ0FBQztDQUNGO0FBbkdELDBCQW1HQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IENvbnN0cnVjdCB9IGZyb20gXCJjb25zdHJ1Y3RzXCI7XG5pbXBvcnQgKiBhcyBzMyBmcm9tIFwiYXdzLWNkay1saWIvYXdzLXMzXCI7XG5pbXBvcnQgKiBhcyBjbG91ZGZvbnQgZnJvbSBcImF3cy1jZGstbGliL2F3cy1jbG91ZGZyb250XCI7XG5pbXBvcnQgKiBhcyBvcmlnaW5zIGZyb20gXCJhd3MtY2RrLWxpYi9hd3MtY2xvdWRmcm9udC1vcmlnaW5zXCI7XG5pbXBvcnQgKiBhcyBjZXJ0aWZpY2F0ZW1hbmFnZXIgZnJvbSBcImF3cy1jZGstbGliL2F3cy1jZXJ0aWZpY2F0ZW1hbmFnZXJcIjtcbmltcG9ydCAqIGFzIGNkayBmcm9tIFwiYXdzLWNkay1saWJcIjtcbmltcG9ydCAqIGFzIGlhbSBmcm9tIFwiYXdzLWNkay1saWIvYXdzLWlhbVwiO1xuaW1wb3J0ICogYXMgcm91dGU1MyBmcm9tIFwiYXdzLWNkay1saWIvYXdzLXJvdXRlNTNcIjtcblxuZXhwb3J0IGludGVyZmFjZSBEb21haW5Db25maWcge1xuICAvKiogVGhlIHJvb3QgZG9tYWluIG5hbWUgKGUuZy4sIGV4YW1wbGUuY29tKS5cbiAgICogVGhlcmUgbXVzdCBiZSBhbiBhc3NvY2lhdGVkIGhvc3RlZCB6b25lIGluIFJvdXRlIDUzIGZvciB0aGlzIGRvbWFpbi5cbiAgICovXG4gIGRvbWFpbk5hbWU6IHN0cmluZztcbiAgLyoqIFRoZSBzdWJkb21haW4gbmFtZSAqL1xuICBzdWJkb21haW5OYW1lOiBzdHJpbmc7XG4gIC8qKiBUaGUgQVJOIG9mIHRoZSBTU0wgY2VydGlmaWNhdGUgdG8gdXNlIGZvciB0aGUgZG9tYWluLiAqL1xuICBjZXJ0aWZpY2F0ZUFybjogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFdlYnNpdGVQcm9wcyB7XG4gIC8qKiBUaGUgbmFtZSBvZiB0aGUgUzMgYnVja2V0IHRoYXQgd2lsbCBob3N0IHRoZSB3ZWJzaXRlIGNvbnRlbnQuICovXG4gIGJ1Y2tldE5hbWU6IHN0cmluZztcblxuICAvKiogVGhlIHBhdGggdG8gdGhlIGluZGV4IGRvY3VtZW50IHRoYXQgd2lsbCBiZSBzZXJ2ZWQgYXMgdGhlIGRlZmF1bHQgcGFnZS4gKi9cbiAgaW5kZXhGaWxlOiBzdHJpbmc7XG5cbiAgLyoqIFRoZSBwYXRoIHRvIHRoZSBlcnJvciBkb2N1bWVudCB0aGF0IHdpbGwgYmUgc2VydmVkIHdoZW4gYW4gZXJyb3Igb2NjdXJzLiAqL1xuICBlcnJvckZpbGU6IHN0cmluZztcblxuICAvKiogT3B0aW9uYWwgY29uZmlndXJhdGlvbiBmb3IgY3VzdG9tIGRvbWFpbiBzZXR1cC4gKi9cbiAgZG9tYWluQ29uZmlnPzogRG9tYWluQ29uZmlnO1xuXG4gIC8qKiBPcHRpb25hbCBwYXRoIHRvIGEgY3VzdG9tIDQwNCBwYWdlLiBJZiBub3Qgc3BlY2lmaWVkLCB0aGUgZXJyb3IgZmlsZSB3aWxsIGJlIHVzZWQuICovXG4gIG5vdEZvdW5kUmVzcG9uc2VQYWdlUGF0aD86IHN0cmluZztcbn1cblxuZXhwb3J0IGNsYXNzIFdlYnNpdGUgZXh0ZW5kcyBDb25zdHJ1Y3Qge1xuICBjb25zdHJ1Y3RvcihzY29wZTogQ29uc3RydWN0LCBpZDogc3RyaW5nLCBwcm9wczogV2Vic2l0ZVByb3BzKSB7XG4gICAgc3VwZXIoc2NvcGUsIGlkKTtcbiAgICBjb25zdCB3ZWJCdWNrZXQgPSBuZXcgczMuQnVja2V0KHRoaXMsIHByb3BzLmJ1Y2tldE5hbWUsIHtcbiAgICAgIHdlYnNpdGVJbmRleERvY3VtZW50OiBwcm9wcy5pbmRleEZpbGUsXG4gICAgICB3ZWJzaXRlRXJyb3JEb2N1bWVudDogcHJvcHMuZXJyb3JGaWxlLFxuICAgICAgcmVtb3ZhbFBvbGljeTogY2RrLlJlbW92YWxQb2xpY3kuREVTVFJPWSxcbiAgICAgIGVuY3J5cHRpb246IHMzLkJ1Y2tldEVuY3J5cHRpb24uUzNfTUFOQUdFRCxcbiAgICB9KTtcbiAgICBjb25zdCBvYWkgPSBuZXcgY2xvdWRmb250Lk9yaWdpbkFjY2Vzc0lkZW50aXR5KFxuICAgICAgdGhpcyxcbiAgICAgIGAke3Byb3BzLmJ1Y2tldE5hbWV9LU9BSWAsXG4gICAgKTtcbiAgICB3ZWJCdWNrZXQuYWRkVG9SZXNvdXJjZVBvbGljeShcbiAgICAgIG5ldyBpYW0uUG9saWN5U3RhdGVtZW50KHtcbiAgICAgICAgYWN0aW9uczogW1wiczM6R2V0T2JqZWN0XCJdLFxuICAgICAgICByZXNvdXJjZXM6IFt3ZWJCdWNrZXQuYXJuRm9yT2JqZWN0cyhcIipcIildLFxuICAgICAgICBwcmluY2lwYWxzOiBbXG4gICAgICAgICAgbmV3IGlhbS5DYW5vbmljYWxVc2VyUHJpbmNpcGFsKFxuICAgICAgICAgICAgb2FpLmNsb3VkRnJvbnRPcmlnaW5BY2Nlc3NJZGVudGl0eVMzQ2Fub25pY2FsVXNlcklkLFxuICAgICAgICAgICksXG4gICAgICAgIF0sXG4gICAgICB9KSxcbiAgICApO1xuXG4gICAgY29uc3QgZGlzdHJpYnV0aW9uID0gbmV3IGNsb3VkZm9udC5EaXN0cmlidXRpb24oXG4gICAgICB0aGlzLFxuICAgICAgYCR7cHJvcHMuYnVja2V0TmFtZX0tZGlzdHJpYnV0aW9uYCxcbiAgICAgIHtcbiAgICAgICAgZGVmYXVsdEJlaGF2aW9yOiB7XG4gICAgICAgICAgb3JpZ2luOiBuZXcgb3JpZ2lucy5TM1N0YXRpY1dlYnNpdGVPcmlnaW4od2ViQnVja2V0KSxcbiAgICAgICAgICB2aWV3ZXJQcm90b2NvbFBvbGljeTpcbiAgICAgICAgICAgIGNsb3VkZm9udC5WaWV3ZXJQcm90b2NvbFBvbGljeS5SRURJUkVDVF9UT19IVFRQUyxcbiAgICAgICAgfSxcbiAgICAgICAgZXJyb3JSZXNwb25zZXM6IFtcbiAgICAgICAgICB7XG4gICAgICAgICAgICBodHRwU3RhdHVzOiA0MDQsXG4gICAgICAgICAgICByZXNwb25zZUh0dHBTdGF0dXM6IDQwNCxcbiAgICAgICAgICAgIHJlc3BvbnNlUGFnZVBhdGg6IHByb3BzLm5vdEZvdW5kUmVzcG9uc2VQYWdlUGF0aCB8fCBgLzQwNC5odG1sYCxcbiAgICAgICAgICAgIHR0bDogY2RrLkR1cmF0aW9uLm1pbnV0ZXMoMzApLFxuICAgICAgICAgIH0sXG4gICAgICAgIF0sXG4gICAgICAgIHByaWNlQ2xhc3M6IGNsb3VkZm9udC5QcmljZUNsYXNzLlBSSUNFX0NMQVNTXzEwMCxcbiAgICAgICAgLi4uKHByb3BzLmRvbWFpbkNvbmZpZ1xuICAgICAgICAgID8ge1xuICAgICAgICAgICAgICBkb21haW5OYW1lczogW3RoaXMuX2dldEZ1bGxEb21haW5OYW1lKHByb3BzLmRvbWFpbkNvbmZpZyldLFxuICAgICAgICAgICAgICBjZXJ0aWZpY2F0ZTogdGhpcy5fZ2V0Q2VydGlmaWNhdGUoXG4gICAgICAgICAgICAgICAgcHJvcHMuZG9tYWluQ29uZmlnLmNlcnRpZmljYXRlQXJuLFxuICAgICAgICAgICAgICApLFxuICAgICAgICAgICAgfVxuICAgICAgICAgIDoge30pLFxuICAgICAgfSxcbiAgICApO1xuXG4gICAgaWYgKHByb3BzLmRvbWFpbkNvbmZpZykge1xuICAgICAgY29uc3QgaG9zdGVkWm9uZSA9IHJvdXRlNTMuSG9zdGVkWm9uZS5mcm9tTG9va3VwKHRoaXMsIFwiSG9zdGVkWm9uZVwiLCB7XG4gICAgICAgIGRvbWFpbk5hbWU6IHByb3BzLmRvbWFpbkNvbmZpZy5kb21haW5OYW1lLFxuICAgICAgfSk7XG4gICAgICBjb25zdCBkb21haW5BUmVjb3JkID0gbmV3IHJvdXRlNTMuQVJlY29yZCh0aGlzLCBcIkRvbWFpbkFSZWNvcmRcIiwge1xuICAgICAgICB6b25lOiBob3N0ZWRab25lLFxuICAgICAgICByZWNvcmROYW1lOiB0aGlzLl9nZXRGdWxsRG9tYWluTmFtZShwcm9wcy5kb21haW5Db25maWcpLFxuICAgICAgICB0YXJnZXQ6IGNkay5hd3Nfcm91dGU1My5SZWNvcmRUYXJnZXQuZnJvbUFsaWFzKFxuICAgICAgICAgIG5ldyBjZGsuYXdzX3JvdXRlNTNfdGFyZ2V0cy5DbG91ZEZyb250VGFyZ2V0KGRpc3RyaWJ1dGlvbiksXG4gICAgICAgICksXG4gICAgICB9KTtcbiAgICAgIGRvbWFpbkFSZWNvcmQubm9kZS5hZGREZXBlbmRlbmN5KGRpc3RyaWJ1dGlvbik7XG4gICAgfVxuXG4gICAgbmV3IGNkay5DZm5PdXRwdXQodGhpcywgXCJjbG91ZGZyb250LXdlYnNpdGUtdXJsXCIsIHtcbiAgICAgIHZhbHVlOiBkaXN0cmlidXRpb24uZGlzdHJpYnV0aW9uRG9tYWluTmFtZSxcbiAgICAgIGRlc2NyaXB0aW9uOiBcIkNsb3VkRnJvbnQgRGlzdHJpYnV0aW9uIERvbWFpbiBOYW1lXCIsXG4gICAgfSk7XG5cbiAgICBuZXcgY2RrLkNmbk91dHB1dCh0aGlzLCBcInMzLXdlYnNpdGUtdXJsXCIsIHtcbiAgICAgIHZhbHVlOiB3ZWJCdWNrZXQuYnVja2V0V2Vic2l0ZVVybCxcbiAgICAgIGRlc2NyaXB0aW9uOiBcIlMzIEJ1Y2tldCBXZWJzaXRlIFVSTFwiLFxuICAgIH0pO1xuXG4gICAgaWYgKHByb3BzLmRvbWFpbkNvbmZpZykge1xuICAgICAgbmV3IGNkay5DZm5PdXRwdXQodGhpcywgXCJ3ZWJzaXRlLXVybFwiLCB7XG4gICAgICAgIHZhbHVlOiBkaXN0cmlidXRpb24uZG9tYWluTmFtZSxcbiAgICAgICAgZGVzY3JpcHRpb246IFwiV2Vic2l0ZSBVUkxcIixcbiAgICAgIH0pO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgX2dldEZ1bGxEb21haW5OYW1lKGRvbWFpbkNvbmZpZzogRG9tYWluQ29uZmlnKTogc3RyaW5nIHtcbiAgICByZXR1cm4gZG9tYWluQ29uZmlnLnN1YmRvbWFpbk5hbWVcbiAgICAgID8gYCR7ZG9tYWluQ29uZmlnLnN1YmRvbWFpbk5hbWV9LiR7ZG9tYWluQ29uZmlnLmRvbWFpbk5hbWV9YFxuICAgICAgOiBkb21haW5Db25maWcuZG9tYWluTmFtZTtcbiAgfVxuXG4gIHByaXZhdGUgX2dldENlcnRpZmljYXRlKGFybjogc3RyaW5nKTogY2VydGlmaWNhdGVtYW5hZ2VyLklDZXJ0aWZpY2F0ZSB7XG4gICAgcmV0dXJuIGNlcnRpZmljYXRlbWFuYWdlci5DZXJ0aWZpY2F0ZS5mcm9tQ2VydGlmaWNhdGVBcm4oXG4gICAgICB0aGlzLFxuICAgICAgYHdlYnNpdGUtY2VydGAsXG4gICAgICBhcm4sXG4gICAgKTtcbiAgfVxufVxuIl19
package/lib/index.ts ADDED
@@ -0,0 +1,137 @@
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
+ constructor(scope: Construct, id: string, props: WebsiteProps) {
40
+ super(scope, id);
41
+ const webBucket = new s3.Bucket(this, props.bucketName, {
42
+ websiteIndexDocument: props.indexFile,
43
+ websiteErrorDocument: props.errorFile,
44
+ removalPolicy: cdk.RemovalPolicy.DESTROY,
45
+ encryption: s3.BucketEncryption.S3_MANAGED,
46
+ });
47
+ const oai = new cloudfont.OriginAccessIdentity(
48
+ this,
49
+ `${props.bucketName}-OAI`,
50
+ );
51
+ webBucket.addToResourcePolicy(
52
+ new iam.PolicyStatement({
53
+ actions: ["s3:GetObject"],
54
+ resources: [webBucket.arnForObjects("*")],
55
+ principals: [
56
+ new iam.CanonicalUserPrincipal(
57
+ oai.cloudFrontOriginAccessIdentityS3CanonicalUserId,
58
+ ),
59
+ ],
60
+ }),
61
+ );
62
+
63
+ const distribution = new cloudfont.Distribution(
64
+ this,
65
+ `${props.bucketName}-distribution`,
66
+ {
67
+ defaultBehavior: {
68
+ origin: new origins.S3StaticWebsiteOrigin(webBucket),
69
+ viewerProtocolPolicy:
70
+ cloudfont.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
71
+ },
72
+ errorResponses: [
73
+ {
74
+ httpStatus: 404,
75
+ responseHttpStatus: 404,
76
+ responsePagePath: props.notFoundResponsePagePath || `/404.html`,
77
+ ttl: cdk.Duration.minutes(30),
78
+ },
79
+ ],
80
+ priceClass: cloudfont.PriceClass.PRICE_CLASS_100,
81
+ ...(props.domainConfig
82
+ ? {
83
+ domainNames: [this._getFullDomainName(props.domainConfig)],
84
+ certificate: this._getCertificate(
85
+ props.domainConfig.certificateArn,
86
+ ),
87
+ }
88
+ : {}),
89
+ },
90
+ );
91
+
92
+ if (props.domainConfig) {
93
+ const hostedZone = route53.HostedZone.fromLookup(this, "HostedZone", {
94
+ domainName: props.domainConfig.domainName,
95
+ });
96
+ const domainARecord = new route53.ARecord(this, "DomainARecord", {
97
+ zone: hostedZone,
98
+ recordName: this._getFullDomainName(props.domainConfig),
99
+ target: cdk.aws_route53.RecordTarget.fromAlias(
100
+ new cdk.aws_route53_targets.CloudFrontTarget(distribution),
101
+ ),
102
+ });
103
+ domainARecord.node.addDependency(distribution);
104
+ }
105
+
106
+ new cdk.CfnOutput(this, "cloudfront-website-url", {
107
+ value: distribution.distributionDomainName,
108
+ description: "CloudFront Distribution Domain Name",
109
+ });
110
+
111
+ new cdk.CfnOutput(this, "s3-website-url", {
112
+ value: webBucket.bucketWebsiteUrl,
113
+ description: "S3 Bucket Website URL",
114
+ });
115
+
116
+ if (props.domainConfig) {
117
+ new cdk.CfnOutput(this, "website-url", {
118
+ value: distribution.domainName,
119
+ description: "Website URL",
120
+ });
121
+ }
122
+ }
123
+
124
+ private _getFullDomainName(domainConfig: DomainConfig): string {
125
+ return domainConfig.subdomainName
126
+ ? `${domainConfig.subdomainName}.${domainConfig.domainName}`
127
+ : domainConfig.domainName;
128
+ }
129
+
130
+ private _getCertificate(arn: string): certificatemanager.ICertificate {
131
+ return certificatemanager.Certificate.fromCertificateArn(
132
+ this,
133
+ `website-cert`,
134
+ arn,
135
+ );
136
+ }
137
+ }
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.1",
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