@deployfoundation/foundation-deploy 0.2.16 → 0.2.18
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/dist/bin/app.js +2 -2
- package/dist/bin/foundation-deploy.js +7 -15
- package/dist/bin/release-account.js +2 -2
- package/dist/{chunk-pygj3x43.js → chunk-fjbc0ydk.js} +1 -1
- package/dist/{chunk-9rasrppa.js → chunk-k2rxb77g.js} +6 -0
- package/dist/{chunk-rq4dc5ns.js → chunk-rhg6yfht.js} +70 -39
- package/dist/src/index.js +2 -2
- package/package.json +1 -1
- package/src/deploy/pages-domain.ts +5 -4
- package/src/deploy/setup.ts +2 -6
- package/src/deploy/slack-manifest.ts +3 -8
- package/src/stacks/pages-edge-function.ts +47 -32
- package/src/stacks/pages-stack.ts +66 -39
package/dist/bin/app.js
CHANGED
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
FoundationPages,
|
|
9
9
|
FoundationPipeline,
|
|
10
10
|
NewsletterStack
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-rhg6yfht.js";
|
|
12
12
|
import {
|
|
13
13
|
adminsFor,
|
|
14
14
|
instanceNames,
|
|
15
15
|
loadInstanceFile,
|
|
16
16
|
provisionsIntegration
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-k2rxb77g.js";
|
|
18
18
|
|
|
19
19
|
// bin/app.ts
|
|
20
20
|
import { execFileSync } from "node:child_process";
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
stackExists,
|
|
30
30
|
stackOutput,
|
|
31
31
|
toolVersion
|
|
32
|
-
} from "../chunk-
|
|
32
|
+
} from "../chunk-fjbc0ydk.js";
|
|
33
33
|
import {
|
|
34
34
|
BASE_SLACK_EVENTS,
|
|
35
35
|
CAPABILITY_IDS,
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
requiredSlackScopes,
|
|
46
46
|
skillsKey,
|
|
47
47
|
slackCommandPrefix
|
|
48
|
-
} from "../chunk-
|
|
48
|
+
} from "../chunk-k2rxb77g.js";
|
|
49
49
|
|
|
50
50
|
// src/deploy/config-sync.ts
|
|
51
51
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync } from "node:fs";
|
|
@@ -793,7 +793,7 @@ function pagesDomainReport(state, baseUrl) {
|
|
|
793
793
|
if (state.mode === "none" || state.domain === undefined)
|
|
794
794
|
return baseUrl === undefined ? [] : [` pages are served at ${baseUrl}`];
|
|
795
795
|
if (state.delegated)
|
|
796
|
-
return [` pages are served at https
|
|
796
|
+
return [` pages are served at https://<pageId>.${state.domain} (sign-in on ${state.domain})`];
|
|
797
797
|
if (state.nameServers.length === 0)
|
|
798
798
|
return [
|
|
799
799
|
` ${state.domain}: the pages zone is created by this deploy; its NS records follow on the next run.`
|
|
@@ -801,8 +801,8 @@ function pagesDomainReport(state, baseUrl) {
|
|
|
801
801
|
return [
|
|
802
802
|
` ☐ add these NS records for ${state.domain} at your DNS provider:`,
|
|
803
803
|
...state.nameServers.map((server) => ` ${state.domain}. NS ${server}.`),
|
|
804
|
-
|
|
805
|
-
" Deploy again once they do: that deploy issues the certificate and
|
|
804
|
+
" until they resolve, pages are not served and page_publish says so.",
|
|
805
|
+
" Deploy again once they do: that deploy issues the certificate and turns pages on."
|
|
806
806
|
];
|
|
807
807
|
}
|
|
808
808
|
|
|
@@ -1592,12 +1592,7 @@ function slackManifestFor(instance, env) {
|
|
|
1592
1592
|
pages: instance.integrations.pages
|
|
1593
1593
|
}, {
|
|
1594
1594
|
...env,
|
|
1595
|
-
...instance.integrations.pages && instance.pages.domain !== undefined ? {
|
|
1596
|
-
PAGES_AUTH_REDIRECT_URL: [
|
|
1597
|
-
`https://${instance.pages.domain}/_auth/callback`,
|
|
1598
|
-
...env.PAGES_AUTH_REDIRECT_URL === undefined ? [] : [env.PAGES_AUTH_REDIRECT_URL]
|
|
1599
|
-
].join(",")
|
|
1600
|
-
} : {}
|
|
1595
|
+
...instance.integrations.pages && instance.pages.domain !== undefined ? { PAGES_AUTH_REDIRECT_URL: `https://${instance.pages.domain}/_auth/callback` } : {}
|
|
1601
1596
|
});
|
|
1602
1597
|
}
|
|
1603
1598
|
|
|
@@ -1829,10 +1824,7 @@ async function reportSlackManifest(ctx) {
|
|
|
1829
1824
|
COMMANDS_REQUEST_URL: await output(ctx, ctx.names.api, "CommandsUrl"),
|
|
1830
1825
|
INTERACTIVE_REQUEST_URL: await output(ctx, ctx.names.api, "InteractiveUrl"),
|
|
1831
1826
|
...ctx.instance.integrations.pages ? {
|
|
1832
|
-
PAGES_AUTH_REDIRECT_URL:
|
|
1833
|
-
await output(ctx, ctx.names.pages, "PagesAuthRedirectUrl"),
|
|
1834
|
-
await output(ctx, ctx.names.pages, "PagesDefaultAuthRedirectUrl")
|
|
1835
|
-
].join(",")
|
|
1827
|
+
PAGES_AUTH_REDIRECT_URL: await output(ctx, ctx.names.pages, "PagesAuthRedirectUrl")
|
|
1836
1828
|
} : {}
|
|
1837
1829
|
});
|
|
1838
1830
|
console.log(` manifest: ${JSON.stringify(manifest)}`);
|
|
@@ -747,6 +747,12 @@ var RawInstance = z2.object({
|
|
|
747
747
|
newsletter: RawNewsletter,
|
|
748
748
|
pages: RawPages
|
|
749
749
|
}).strict().superRefine((doc, ctx) => {
|
|
750
|
+
if (doc.integrations.pages && doc.pages.domain === undefined)
|
|
751
|
+
ctx.addIssue({
|
|
752
|
+
code: z2.ZodIssueCode.custom,
|
|
753
|
+
path: ["pages", "domain"],
|
|
754
|
+
message: "integrations.pages needs pages.domain: each page is served from its own subdomain of it (docs/pages.md)"
|
|
755
|
+
});
|
|
750
756
|
if (doc.pages.domain !== undefined && !doc.integrations.pages)
|
|
751
757
|
ctx.addIssue({
|
|
752
758
|
code: z2.ZodIssueCode.custom,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
scheduledJobBindings,
|
|
20
20
|
slackCommandPrefixes,
|
|
21
21
|
webhookJobBindings
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-k2rxb77g.js";
|
|
23
23
|
|
|
24
24
|
// src/stacks/agent-stack.ts
|
|
25
25
|
import * as cdk from "aws-cdk-lib";
|
|
@@ -2755,10 +2755,11 @@ var PAGES_VIEWER_REQUEST_SOURCE = `import cf from 'cloudfront';
|
|
|
2755
2755
|
import crypto from 'crypto';
|
|
2756
2756
|
|
|
2757
2757
|
var kvs = cf.kvs('__KVS_ID__');
|
|
2758
|
-
var
|
|
2758
|
+
var DOMAIN = '__DOMAIN__';
|
|
2759
|
+
var COOKIE = '__Host-fpa';
|
|
2759
2760
|
var KEY_NAME = '${PAGES_EDGE_KEY_NAME}';
|
|
2760
2761
|
var HOST_HEADER = '${PAGES_HOST_HEADER}';
|
|
2761
|
-
var
|
|
2762
|
+
var PAGE_HOST_RE = /^([a-z0-9]{24})\\.(.+)$/;
|
|
2762
2763
|
var VERSION_RE = /^\\/v([1-9][0-9]{0,5})(\\/.*)?$/;
|
|
2763
2764
|
var EXPORT_RE = /^\\/exports\\/v([1-9][0-9]{0,5})\\/([^\\/]+)$/;
|
|
2764
2765
|
var UNSAFE_RE = /(^|\\/)\\.\\.?(\\/|$)|\\/\\/|%2e|%2f|%5c|\\\\/i;
|
|
@@ -2785,6 +2786,11 @@ function redirect(location) {
|
|
|
2785
2786
|
};
|
|
2786
2787
|
}
|
|
2787
2788
|
|
|
2789
|
+
function toLambda(request, host) {
|
|
2790
|
+
request.headers[HOST_HEADER] = { value: host };
|
|
2791
|
+
return request;
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2788
2794
|
function equal(a, b) {
|
|
2789
2795
|
if (a.length !== b.length) return false;
|
|
2790
2796
|
var diff = 0;
|
|
@@ -2811,10 +2817,10 @@ function cookieValues(request) {
|
|
|
2811
2817
|
return values;
|
|
2812
2818
|
}
|
|
2813
2819
|
|
|
2814
|
-
function objectPath(pageId,
|
|
2820
|
+
function objectPath(pageId, uri) {
|
|
2815
2821
|
var prefix = '/pages/' + pageId;
|
|
2816
|
-
if (
|
|
2817
|
-
var version = VERSION_RE.exec(
|
|
2822
|
+
if (uri === '/') return prefix + '/latest.html';
|
|
2823
|
+
var version = VERSION_RE.exec(uri);
|
|
2818
2824
|
if (version) {
|
|
2819
2825
|
var sub = version[2];
|
|
2820
2826
|
if (sub.charAt(sub.length - 1) === '/') {
|
|
@@ -2824,7 +2830,7 @@ function objectPath(pageId, rest) {
|
|
|
2824
2830
|
}
|
|
2825
2831
|
return prefix + '/v' + version[1] + '/build' + sub;
|
|
2826
2832
|
}
|
|
2827
|
-
var download = EXPORT_RE.exec(
|
|
2833
|
+
var download = EXPORT_RE.exec(uri);
|
|
2828
2834
|
if (download) return prefix + '/v' + download[1] + '/exports/' + download[2];
|
|
2829
2835
|
return null;
|
|
2830
2836
|
}
|
|
@@ -2832,18 +2838,20 @@ function objectPath(pageId, rest) {
|
|
|
2832
2838
|
async function handler(event) {
|
|
2833
2839
|
var request = event.request;
|
|
2834
2840
|
var uri = request.uri;
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2841
|
+
var host = request.headers.host ? request.headers.host.value.toLowerCase() : '';
|
|
2842
|
+
|
|
2843
|
+
if (host === DOMAIN) {
|
|
2844
|
+
if (uri === '/_auth/start' || uri === '/_auth/callback') return toLambda(request, host);
|
|
2845
|
+
return reply(404, 'Not Found', 'Not found');
|
|
2838
2846
|
}
|
|
2839
|
-
var page =
|
|
2840
|
-
if (!page ||
|
|
2847
|
+
var page = PAGE_HOST_RE.exec(host);
|
|
2848
|
+
if (!page || page[2] !== DOMAIN) return reply(404, 'Not Found', 'Not found');
|
|
2841
2849
|
var pageId = page[1];
|
|
2842
|
-
|
|
2843
|
-
if (
|
|
2844
|
-
var version = VERSION_RE.exec(
|
|
2850
|
+
if (uri === '/_auth/redeem') return toLambda(request, host);
|
|
2851
|
+
if (UNSAFE_RE.test(uri)) return reply(404, 'Not Found', 'Not found');
|
|
2852
|
+
var version = VERSION_RE.exec(uri);
|
|
2845
2853
|
if (version && version[2] === undefined) return redirect(uri + '/');
|
|
2846
|
-
var target = objectPath(pageId,
|
|
2854
|
+
var target = objectPath(pageId, uri);
|
|
2847
2855
|
if (target === null) return reply(404, 'Not Found', 'Not found');
|
|
2848
2856
|
|
|
2849
2857
|
var key;
|
|
@@ -2860,20 +2868,22 @@ async function handler(event) {
|
|
|
2860
2868
|
return request;
|
|
2861
2869
|
}
|
|
2862
2870
|
}
|
|
2863
|
-
return redirect('/_auth/start?page=' + pageId + '&next=' + encodeURIComponent(uri));
|
|
2871
|
+
return redirect('https://' + DOMAIN + '/_auth/start?page=' + pageId + '&next=' + encodeURIComponent(uri));
|
|
2864
2872
|
}
|
|
2865
2873
|
`;
|
|
2866
|
-
function pagesViewerRequestCode(kvsId) {
|
|
2867
|
-
|
|
2874
|
+
function pagesViewerRequestCode(kvsId, domain) {
|
|
2875
|
+
if (!/^(?:[a-z0-9-]+\.)+[a-z]{2,63}$/.test(domain))
|
|
2876
|
+
throw new Error("invalid pages domain");
|
|
2877
|
+
return PAGES_VIEWER_REQUEST_SOURCE.replace("__KVS_ID__", kvsId).replace("__DOMAIN__", domain);
|
|
2868
2878
|
}
|
|
2869
2879
|
|
|
2870
2880
|
// src/stacks/pages-stack.ts
|
|
2871
2881
|
function pagesDomainPlan(instance, delegated) {
|
|
2872
2882
|
const { domain, hostedZoneId } = instance.pages;
|
|
2873
2883
|
if (domain === undefined)
|
|
2874
|
-
|
|
2884
|
+
throw new Error("hosted pages need pages.domain");
|
|
2875
2885
|
const ownZone = hostedZoneId === undefined;
|
|
2876
|
-
return
|
|
2886
|
+
return { domain, attached: !ownZone || delegated, ownZone };
|
|
2877
2887
|
}
|
|
2878
2888
|
var PAGES_CONTENT_SECURITY_POLICY = [
|
|
2879
2889
|
"default-src 'self'",
|
|
@@ -2904,7 +2914,8 @@ class FoundationPages extends cdk7.Stack {
|
|
|
2904
2914
|
const names = instanceNames(instance);
|
|
2905
2915
|
const display = instance.displayName;
|
|
2906
2916
|
const plan = pagesDomainPlan(instance, props.domainDelegated === true);
|
|
2907
|
-
const
|
|
2917
|
+
const pagesDomain = plan.domain;
|
|
2918
|
+
const domain = plan.attached ? pagesDomain : undefined;
|
|
2908
2919
|
this.bucket = new s32.Bucket(this, "PagesBucket", {
|
|
2909
2920
|
blockPublicAccess: s32.BlockPublicAccess.BLOCK_ALL,
|
|
2910
2921
|
encryption: s32.BucketEncryption.S3_MANAGED,
|
|
@@ -2965,6 +2976,12 @@ class FoundationPages extends cdk7.Stack {
|
|
|
2965
2976
|
actions: ["dynamodb:GetItem"],
|
|
2966
2977
|
resources: [this.table.tableArn]
|
|
2967
2978
|
}));
|
|
2979
|
+
authRole.addToPolicy(new iam8.PolicyStatement({
|
|
2980
|
+
sid: "PagesAuthClaimCode",
|
|
2981
|
+
actions: ["dynamodb:PutItem"],
|
|
2982
|
+
resources: [this.table.tableArn],
|
|
2983
|
+
conditions: { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": ["CODE#*"] } }
|
|
2984
|
+
}));
|
|
2968
2985
|
authRole.addToPolicy(new iam8.PolicyStatement({
|
|
2969
2986
|
sid: "PagesTableKey",
|
|
2970
2987
|
actions: ["kms:Decrypt", "kms:DescribeKey"],
|
|
@@ -2983,7 +3000,7 @@ class FoundationPages extends cdk7.Stack {
|
|
|
2983
3000
|
FOUNDATION_SLACK_TEAM_ID: instance.slack.teamId,
|
|
2984
3001
|
FOUNDATION_PAGES_TABLE_NAME: this.table.tableName,
|
|
2985
3002
|
FOUNDATION_DISPLAY_NAME: display,
|
|
2986
|
-
|
|
3003
|
+
FOUNDATION_PAGES_DOMAIN: pagesDomain
|
|
2987
3004
|
}
|
|
2988
3005
|
});
|
|
2989
3006
|
const authUrl = authFunction.addFunctionUrl({ authType: lambda3.FunctionUrlAuthType.AWS_IAM });
|
|
@@ -3028,7 +3045,7 @@ class FoundationPages extends cdk7.Stack {
|
|
|
3028
3045
|
runtime: cloudfront.FunctionRuntime.JS_2_0,
|
|
3029
3046
|
keyValueStore: edgeKeys,
|
|
3030
3047
|
comment: `${display} pages: cookie check and path mapping`,
|
|
3031
|
-
code: cloudfront.FunctionCode.fromInline(pagesViewerRequestCode(edgeKeys.keyValueStoreId))
|
|
3048
|
+
code: cloudfront.FunctionCode.fromInline(pagesViewerRequestCode(edgeKeys.keyValueStoreId, pagesDomain))
|
|
3032
3049
|
});
|
|
3033
3050
|
const headers = new cloudfront.ResponseHeadersPolicy(this, "PagesHeaders", {
|
|
3034
3051
|
comment: `${display} pages security headers`,
|
|
@@ -3050,16 +3067,21 @@ class FoundationPages extends cdk7.Stack {
|
|
|
3050
3067
|
}
|
|
3051
3068
|
},
|
|
3052
3069
|
customHeadersBehavior: {
|
|
3053
|
-
customHeaders: [
|
|
3070
|
+
customHeaders: [
|
|
3071
|
+
{ header: "X-Robots-Tag", value: "noindex, nofollow", override: true },
|
|
3072
|
+
{ header: "Cross-Origin-Resource-Policy", value: "same-origin", override: true },
|
|
3073
|
+
{ header: "Cross-Origin-Opener-Policy", value: "same-origin", override: true },
|
|
3074
|
+
{ header: "Origin-Agent-Cluster", value: "?1", override: true }
|
|
3075
|
+
]
|
|
3054
3076
|
}
|
|
3055
3077
|
});
|
|
3056
|
-
const zoneDomain =
|
|
3057
|
-
const zone =
|
|
3078
|
+
const zoneDomain = pagesDomain;
|
|
3079
|
+
const zone = instance.pages.hostedZoneId !== undefined ? route532.HostedZone.fromHostedZoneId(this, "PagesParentZone", instance.pages.hostedZoneId) : new route532.PublicHostedZone(this, "PagesZone", {
|
|
3058
3080
|
zoneName: zoneDomain,
|
|
3059
3081
|
comment: `${display} hosted pages (delegated from the company's DNS)`
|
|
3060
3082
|
});
|
|
3061
3083
|
let certificate;
|
|
3062
|
-
if (domain !== undefined
|
|
3084
|
+
if (domain !== undefined) {
|
|
3063
3085
|
const certificateRole = new iam8.Role(this, "PagesCertificateRole", {
|
|
3064
3086
|
assumedBy: new iam8.ServicePrincipal("lambda.amazonaws.com"),
|
|
3065
3087
|
description: `${display} issues the pages domain certificate in us-east-1`,
|
|
@@ -3093,7 +3115,11 @@ class FoundationPages extends cdk7.Stack {
|
|
|
3093
3115
|
const issued = new cdk7.CustomResource(this, "PagesCertificate", {
|
|
3094
3116
|
serviceToken: certificateFunction.functionArn,
|
|
3095
3117
|
resourceType: "Custom::FoundationPagesCertificate",
|
|
3096
|
-
properties: {
|
|
3118
|
+
properties: {
|
|
3119
|
+
DomainName: domain,
|
|
3120
|
+
SubjectAlternativeNames: [`*.${domain}`],
|
|
3121
|
+
HostedZoneId: zone.hostedZoneId
|
|
3122
|
+
}
|
|
3097
3123
|
});
|
|
3098
3124
|
certificate = acm2.Certificate.fromCertificateArn(this, "PagesCertificateArn", issued.getAttString("CertificateArn"));
|
|
3099
3125
|
}
|
|
@@ -3102,7 +3128,7 @@ class FoundationPages extends cdk7.Stack {
|
|
|
3102
3128
|
];
|
|
3103
3129
|
this.distribution = new cloudfront.Distribution(this, "Distribution", {
|
|
3104
3130
|
comment: `${display} pages`,
|
|
3105
|
-
...domain === undefined || certificate === undefined ? {} : { domainNames: [domain], certificate },
|
|
3131
|
+
...domain === undefined || certificate === undefined ? {} : { domainNames: [domain, `*.${domain}`], certificate },
|
|
3106
3132
|
defaultRootObject: "index.html",
|
|
3107
3133
|
minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
|
|
3108
3134
|
httpVersion: cloudfront.HttpVersion.HTTP2_AND_3,
|
|
@@ -3130,13 +3156,18 @@ class FoundationPages extends cdk7.Stack {
|
|
|
3130
3156
|
}
|
|
3131
3157
|
});
|
|
3132
3158
|
cdk7.Annotations.of(this.distribution).acknowledgeWarning("@aws-cdk/aws-cloudfront-origins:listBucketSecurityRisk", "The viewer function forwards only mapped object keys and 404s `/`; defaultRootObject is set too. LIST only turns a missing key into 404 instead of 403.");
|
|
3133
|
-
if (domain !== undefined
|
|
3159
|
+
if (domain !== undefined) {
|
|
3134
3160
|
const target = route532.RecordTarget.fromAlias(new route53Targets2.CloudFrontTarget(this.distribution));
|
|
3135
3161
|
new route532.ARecord(this, "PagesAliasA", { zone, recordName: `${domain}.`, target });
|
|
3136
3162
|
new route532.AaaaRecord(this, "PagesAliasAaaa", { zone, recordName: `${domain}.`, target });
|
|
3163
|
+
new route532.ARecord(this, "PagesWildcardA", { zone, recordName: `*.${domain}.`, target });
|
|
3164
|
+
new route532.AaaaRecord(this, "PagesWildcardAaaa", {
|
|
3165
|
+
zone,
|
|
3166
|
+
recordName: `*.${domain}.`,
|
|
3167
|
+
target
|
|
3168
|
+
});
|
|
3137
3169
|
}
|
|
3138
|
-
const
|
|
3139
|
-
const baseUrl = domain === undefined ? defaultUrl : `https://${domain}`;
|
|
3170
|
+
const signInUrl = `https://${pagesDomain}`;
|
|
3140
3171
|
const apiRole = new iam8.Role(this, "PagesApiRole", {
|
|
3141
3172
|
assumedBy: new iam8.ServicePrincipal("lambda.amazonaws.com"),
|
|
3142
3173
|
description: `${display} Pages API proxy (the only writer of the pages bucket and table)`,
|
|
@@ -3182,22 +3213,22 @@ class FoundationPages extends cdk7.Stack {
|
|
|
3182
3213
|
environment: {
|
|
3183
3214
|
FOUNDATION_PAGES_TABLE_NAME: this.table.tableName,
|
|
3184
3215
|
FOUNDATION_PAGES_BUCKET_NAME: this.bucket.bucketName,
|
|
3185
|
-
|
|
3216
|
+
...domain === undefined ? {} : { FOUNDATION_PAGES_DOMAIN: domain },
|
|
3186
3217
|
FOUNDATION_SLACK_TEAM_ID: instance.slack.teamId,
|
|
3187
3218
|
FOUNDATION_SLACK_SIGNING_SECRET_ID: props.secrets.signing.secretName
|
|
3188
3219
|
}
|
|
3189
3220
|
});
|
|
3190
3221
|
new cdk7.CfnOutput(this, "PagesApiFunctionArn", { value: this.apiFunction.functionArn });
|
|
3191
|
-
new cdk7.CfnOutput(this, "PagesBaseUrl", {
|
|
3222
|
+
new cdk7.CfnOutput(this, "PagesBaseUrl", {
|
|
3223
|
+
description: "The sign-in host; each page is https://<pageId>.<this domain>",
|
|
3224
|
+
value: signInUrl
|
|
3225
|
+
});
|
|
3192
3226
|
new cdk7.CfnOutput(this, "PagesDistributionDomainName", {
|
|
3193
3227
|
value: this.distribution.distributionDomainName
|
|
3194
3228
|
});
|
|
3195
3229
|
new cdk7.CfnOutput(this, "PagesAuthRedirectUrl", {
|
|
3196
3230
|
description: "Slack OAuth redirect URL for Sign in with Slack on the pages domain",
|
|
3197
|
-
value: `${
|
|
3198
|
-
});
|
|
3199
|
-
new cdk7.CfnOutput(this, "PagesDefaultAuthRedirectUrl", {
|
|
3200
|
-
value: `${defaultUrl}/_auth/callback`
|
|
3231
|
+
value: `${signInUrl}/_auth/callback`
|
|
3201
3232
|
});
|
|
3202
3233
|
new cdk7.CfnOutput(this, "PagesCustomDomainAttached", {
|
|
3203
3234
|
value: domain === undefined ? "false" : "true"
|
package/dist/src/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
FoundationPipeline,
|
|
11
11
|
NewsletterStack,
|
|
12
12
|
deployStatements
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-rhg6yfht.js";
|
|
14
14
|
import {
|
|
15
15
|
BUN_VERSION,
|
|
16
16
|
LAMBDA_ENTRY_POINTS,
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
skillsKey,
|
|
44
44
|
skipBundle,
|
|
45
45
|
verifyManifest
|
|
46
|
-
} from "../chunk-
|
|
46
|
+
} from "../chunk-k2rxb77g.js";
|
|
47
47
|
export {
|
|
48
48
|
BUILD_TIMEOUT,
|
|
49
49
|
BUN_VERSION,
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Route 53 zone for exactly that subdomain and outputs its name servers. The
|
|
6
6
|
* company adds those as NS records at its own DNS provider. Until they
|
|
7
7
|
* resolve, issuing the certificate would leave CloudFormation waiting on ACM
|
|
8
|
-
* validation, so
|
|
8
|
+
* validation, so pages are not served (and publishing reports it) until then.
|
|
9
9
|
* Before each `cdk deploy` this module looks the delegation up in public DNS
|
|
10
10
|
* and, once it matches the zone, passes `-c pagesDomainDelegated=true`: that
|
|
11
11
|
* deploy issues the certificate and attaches the domain.
|
|
@@ -92,7 +92,8 @@ export async function publicNs(name: string): Promise<string[]> {
|
|
|
92
92
|
export function pagesDomainReport(state: PagesDomainState, baseUrl: string | undefined): string[] {
|
|
93
93
|
if (state.mode === "none" || state.domain === undefined)
|
|
94
94
|
return baseUrl === undefined ? [] : [` pages are served at ${baseUrl}`];
|
|
95
|
-
if (state.delegated)
|
|
95
|
+
if (state.delegated)
|
|
96
|
+
return [` pages are served at https://<pageId>.${state.domain} (sign-in on ${state.domain})`];
|
|
96
97
|
if (state.nameServers.length === 0)
|
|
97
98
|
return [
|
|
98
99
|
` ${state.domain}: the pages zone is created by this deploy; its NS records follow on the next run.`,
|
|
@@ -100,7 +101,7 @@ export function pagesDomainReport(state: PagesDomainState, baseUrl: string | und
|
|
|
100
101
|
return [
|
|
101
102
|
` ☐ add these NS records for ${state.domain} at your DNS provider:`,
|
|
102
103
|
...state.nameServers.map((server) => ` ${state.domain}. NS ${server}.`),
|
|
103
|
-
|
|
104
|
-
" Deploy again once they do: that deploy issues the certificate and
|
|
104
|
+
" until they resolve, pages are not served and page_publish says so.",
|
|
105
|
+
" Deploy again once they do: that deploy issues the certificate and turns pages on.",
|
|
105
106
|
];
|
|
106
107
|
}
|
package/src/deploy/setup.ts
CHANGED
|
@@ -190,14 +190,10 @@ async function reportSlackManifest(ctx: AwsContext): Promise<void> {
|
|
|
190
190
|
EVENTS_REQUEST_URL: await output(ctx, ctx.names.api, "EventsUrl"),
|
|
191
191
|
COMMANDS_REQUEST_URL: await output(ctx, ctx.names.api, "CommandsUrl"),
|
|
192
192
|
INTERACTIVE_REQUEST_URL: await output(ctx, ctx.names.api, "InteractiveUrl"),
|
|
193
|
-
//
|
|
194
|
-
// name's, which links posted before a custom domain attached still use.
|
|
193
|
+
// One sign-in callback, on the pages domain itself.
|
|
195
194
|
...(ctx.instance.integrations.pages
|
|
196
195
|
? {
|
|
197
|
-
PAGES_AUTH_REDIRECT_URL:
|
|
198
|
-
await output(ctx, ctx.names.pages, "PagesAuthRedirectUrl"),
|
|
199
|
-
await output(ctx, ctx.names.pages, "PagesDefaultAuthRedirectUrl"),
|
|
200
|
-
].join(","),
|
|
196
|
+
PAGES_AUTH_REDIRECT_URL: await output(ctx, ctx.names.pages, "PagesAuthRedirectUrl"),
|
|
201
197
|
}
|
|
202
198
|
: {}),
|
|
203
199
|
});
|
|
@@ -138,15 +138,10 @@ export function slackManifestFor(instance: Instance, env: SlackManifestEnv): Sla
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
...env,
|
|
141
|
-
//
|
|
142
|
-
// registering it
|
|
141
|
+
// The one sign-in callback, on the pages domain, is known from the
|
|
142
|
+
// instance file alone; registering it before delegation is harmless.
|
|
143
143
|
...(instance.integrations.pages && instance.pages.domain !== undefined
|
|
144
|
-
? {
|
|
145
|
-
PAGES_AUTH_REDIRECT_URL: [
|
|
146
|
-
`https://${instance.pages.domain}/_auth/callback`,
|
|
147
|
-
...(env.PAGES_AUTH_REDIRECT_URL === undefined ? [] : [env.PAGES_AUTH_REDIRECT_URL]),
|
|
148
|
-
].join(","),
|
|
149
|
-
}
|
|
144
|
+
? { PAGES_AUTH_REDIRECT_URL: `https://${instance.pages.domain}/_auth/callback` }
|
|
150
145
|
: {}),
|
|
151
146
|
},
|
|
152
147
|
);
|
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The CloudFront viewer-request function in front of every hosted page.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Every page is its own origin, `<pageId>.<domain>`, so one page's script can
|
|
5
|
+
* never read another page, even for a viewer signed in to both. The function
|
|
6
|
+
* runs on each request, before the cache, in CloudFront's `cloudfront-js-2.0`
|
|
7
|
+
* runtime, and routes by Host:
|
|
6
8
|
*
|
|
7
|
-
* 1.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 2.
|
|
11
|
-
*
|
|
9
|
+
* 1. `<domain>` (the sign-in host) serves only `/_auth/start` and
|
|
10
|
+
* `/_auth/callback`, passed to the sign-in Lambda with the Host copied into
|
|
11
|
+
* `x-foundation-pages-host`. Everything else there is a 404.
|
|
12
|
+
* 2. `<pageId>.<domain>` serves that one page. `/_auth/redeem` passes to the
|
|
13
|
+
* Lambda (it sets the page host's own cookie). Anything else needs a valid
|
|
14
|
+
* `__Host-fpa` cookie whose page id is this host's: the HMAC the sign-in
|
|
15
|
+
* Lambda made with the key this function reads from the attached
|
|
12
16
|
* KeyValueStore (`@deployfoundation/foundation-core/page-cookie` is the
|
|
13
|
-
* signer; a test holds the two to one format). No cookie,
|
|
14
|
-
*
|
|
17
|
+
* signer; a test holds the two to one format). No cookie, or an expired or
|
|
18
|
+
* foreign one, is a 302 to `https://<domain>/_auth/start`.
|
|
15
19
|
* 3. A granted request is rewritten onto its S3 key (the layout in
|
|
16
20
|
* `packages/capability-pages/src/layout.ts`): a trailing `/` gets
|
|
17
21
|
* `index.html`, and an extensionless path is a single-page-app route that
|
|
18
22
|
* gets the version's root `index.html` (how an open-slide deck's `/s/<id>`
|
|
19
|
-
* route loads). `source/` and `uploads/` have no public path at all.
|
|
23
|
+
* route loads). `source/` and `uploads/` have no public path at all. The
|
|
24
|
+
* rewritten key carries the page id, so the cache can never serve one
|
|
25
|
+
* page's object on another page's host.
|
|
20
26
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
27
|
+
* Any other host (the distribution's `*.cloudfront.net` name included) is a
|
|
28
|
+
* 404. Dot segments and encoded separators are refused before rewriting.
|
|
23
29
|
*
|
|
24
30
|
* The runtime is not Node: `var`, plain functions, and only the `crypto`
|
|
25
|
-
* HMAC/hash subset. `__KVS_ID__`
|
|
31
|
+
* HMAC/hash subset. `__KVS_ID__` and `__DOMAIN__` are replaced at synth.
|
|
26
32
|
*/
|
|
27
33
|
|
|
28
34
|
/** The KeyValueStore key holding the cookie HMAC key; the seeding resource writes it. */
|
|
@@ -34,10 +40,11 @@ export const PAGES_VIEWER_REQUEST_SOURCE = `import cf from 'cloudfront';
|
|
|
34
40
|
import crypto from 'crypto';
|
|
35
41
|
|
|
36
42
|
var kvs = cf.kvs('__KVS_ID__');
|
|
37
|
-
var
|
|
43
|
+
var DOMAIN = '__DOMAIN__';
|
|
44
|
+
var COOKIE = '__Host-fpa';
|
|
38
45
|
var KEY_NAME = '${PAGES_EDGE_KEY_NAME}';
|
|
39
46
|
var HOST_HEADER = '${PAGES_HOST_HEADER}';
|
|
40
|
-
var
|
|
47
|
+
var PAGE_HOST_RE = /^([a-z0-9]{24})\\.(.+)$/;
|
|
41
48
|
var VERSION_RE = /^\\/v([1-9][0-9]{0,5})(\\/.*)?$/;
|
|
42
49
|
var EXPORT_RE = /^\\/exports\\/v([1-9][0-9]{0,5})\\/([^\\/]+)$/;
|
|
43
50
|
var UNSAFE_RE = /(^|\\/)\\.\\.?(\\/|$)|\\/\\/|%2e|%2f|%5c|\\\\/i;
|
|
@@ -64,6 +71,11 @@ function redirect(location) {
|
|
|
64
71
|
};
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
function toLambda(request, host) {
|
|
75
|
+
request.headers[HOST_HEADER] = { value: host };
|
|
76
|
+
return request;
|
|
77
|
+
}
|
|
78
|
+
|
|
67
79
|
function equal(a, b) {
|
|
68
80
|
if (a.length !== b.length) return false;
|
|
69
81
|
var diff = 0;
|
|
@@ -90,10 +102,10 @@ function cookieValues(request) {
|
|
|
90
102
|
return values;
|
|
91
103
|
}
|
|
92
104
|
|
|
93
|
-
function objectPath(pageId,
|
|
105
|
+
function objectPath(pageId, uri) {
|
|
94
106
|
var prefix = '/pages/' + pageId;
|
|
95
|
-
if (
|
|
96
|
-
var version = VERSION_RE.exec(
|
|
107
|
+
if (uri === '/') return prefix + '/latest.html';
|
|
108
|
+
var version = VERSION_RE.exec(uri);
|
|
97
109
|
if (version) {
|
|
98
110
|
var sub = version[2];
|
|
99
111
|
if (sub.charAt(sub.length - 1) === '/') {
|
|
@@ -103,7 +115,7 @@ function objectPath(pageId, rest) {
|
|
|
103
115
|
}
|
|
104
116
|
return prefix + '/v' + version[1] + '/build' + sub;
|
|
105
117
|
}
|
|
106
|
-
var download = EXPORT_RE.exec(
|
|
118
|
+
var download = EXPORT_RE.exec(uri);
|
|
107
119
|
if (download) return prefix + '/v' + download[1] + '/exports/' + download[2];
|
|
108
120
|
return null;
|
|
109
121
|
}
|
|
@@ -111,18 +123,20 @@ function objectPath(pageId, rest) {
|
|
|
111
123
|
async function handler(event) {
|
|
112
124
|
var request = event.request;
|
|
113
125
|
var uri = request.uri;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
var host = request.headers.host ? request.headers.host.value.toLowerCase() : '';
|
|
127
|
+
|
|
128
|
+
if (host === DOMAIN) {
|
|
129
|
+
if (uri === '/_auth/start' || uri === '/_auth/callback') return toLambda(request, host);
|
|
130
|
+
return reply(404, 'Not Found', 'Not found');
|
|
117
131
|
}
|
|
118
|
-
var page =
|
|
119
|
-
if (!page ||
|
|
132
|
+
var page = PAGE_HOST_RE.exec(host);
|
|
133
|
+
if (!page || page[2] !== DOMAIN) return reply(404, 'Not Found', 'Not found');
|
|
120
134
|
var pageId = page[1];
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
var version = VERSION_RE.exec(
|
|
135
|
+
if (uri === '/_auth/redeem') return toLambda(request, host);
|
|
136
|
+
if (UNSAFE_RE.test(uri)) return reply(404, 'Not Found', 'Not found');
|
|
137
|
+
var version = VERSION_RE.exec(uri);
|
|
124
138
|
if (version && version[2] === undefined) return redirect(uri + '/');
|
|
125
|
-
var target = objectPath(pageId,
|
|
139
|
+
var target = objectPath(pageId, uri);
|
|
126
140
|
if (target === null) return reply(404, 'Not Found', 'Not found');
|
|
127
141
|
|
|
128
142
|
var key;
|
|
@@ -139,11 +153,12 @@ async function handler(event) {
|
|
|
139
153
|
return request;
|
|
140
154
|
}
|
|
141
155
|
}
|
|
142
|
-
return redirect('/_auth/start?page=' + pageId + '&next=' + encodeURIComponent(uri));
|
|
156
|
+
return redirect('https://' + DOMAIN + '/_auth/start?page=' + pageId + '&next=' + encodeURIComponent(uri));
|
|
143
157
|
}
|
|
144
158
|
`;
|
|
145
159
|
|
|
146
|
-
/** The function source for one store; `kvsId` may be a CDK token. */
|
|
147
|
-
export function pagesViewerRequestCode(kvsId: string): string {
|
|
148
|
-
|
|
160
|
+
/** The function source for one store and pages domain; `kvsId` may be a CDK token. */
|
|
161
|
+
export function pagesViewerRequestCode(kvsId: string, domain: string): string {
|
|
162
|
+
if (!/^(?:[a-z0-9-]+\.)+[a-z]{2,63}$/.test(domain)) throw new Error("invalid pages domain");
|
|
163
|
+
return PAGES_VIEWER_REQUEST_SOURCE.replace("__KVS_ID__", kvsId).replace("__DOMAIN__", domain);
|
|
149
164
|
}
|
|
@@ -28,27 +28,28 @@ export interface FoundationPagesProps extends cdk.StackProps {
|
|
|
28
28
|
/**
|
|
29
29
|
* The deploy tool has seen the Foundation-made zone's NS delegation resolve
|
|
30
30
|
* (`-c pagesDomainDelegated=true`), so the certificate can be issued and the
|
|
31
|
-
* domain attached without the deploy hanging on validation.
|
|
32
|
-
*
|
|
31
|
+
* domain attached without the deploy hanging on validation. Implied by
|
|
32
|
+
* `pages.hostedZoneId`.
|
|
33
33
|
*/
|
|
34
34
|
domainDelegated?: boolean;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** How the stack serves pages for this instance and deploy. */
|
|
38
38
|
export interface PagesDomainPlan {
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
|
|
39
|
+
/** `pages.domain`: the sign-in host; each page is `<pageId>.<domain>`. */
|
|
40
|
+
domain: string;
|
|
41
|
+
/** The domain and its wildcard are attached to the distribution in this deploy. */
|
|
42
|
+
attached: boolean;
|
|
43
43
|
/** Foundation creates the zone (no `pages.hostedZoneId`). */
|
|
44
44
|
ownZone: boolean;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function pagesDomainPlan(instance: Instance, delegated: boolean): PagesDomainPlan {
|
|
48
48
|
const { domain, hostedZoneId } = instance.pages;
|
|
49
|
-
|
|
49
|
+
// The instance schema already requires it; per-page origins need a domain.
|
|
50
|
+
if (domain === undefined) throw new Error("hosted pages need pages.domain");
|
|
50
51
|
const ownZone = hostedZoneId === undefined;
|
|
51
|
-
return
|
|
52
|
+
return { domain, attached: !ownZone || delegated, ownZone };
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
/**
|
|
@@ -107,7 +108,9 @@ export class FoundationPages extends cdk.Stack {
|
|
|
107
108
|
const names = namesFor(instance);
|
|
108
109
|
const display = instance.displayName;
|
|
109
110
|
const plan = pagesDomainPlan(instance, props.domainDelegated === true);
|
|
110
|
-
const
|
|
111
|
+
const pagesDomain = plan.domain;
|
|
112
|
+
// Set only once the domain is served; until then the Pages API says "not ready".
|
|
113
|
+
const domain = plan.attached ? pagesDomain : undefined;
|
|
111
114
|
|
|
112
115
|
this.bucket = new s3.Bucket(this, "PagesBucket", {
|
|
113
116
|
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
@@ -185,6 +188,15 @@ export class FoundationPages extends cdk.Stack {
|
|
|
185
188
|
resources: [this.table.tableArn],
|
|
186
189
|
}),
|
|
187
190
|
);
|
|
191
|
+
// One-time sign-in handoff codes: the only rows it may write, and only new ones.
|
|
192
|
+
authRole.addToPolicy(
|
|
193
|
+
new iam.PolicyStatement({
|
|
194
|
+
sid: "PagesAuthClaimCode",
|
|
195
|
+
actions: ["dynamodb:PutItem"],
|
|
196
|
+
resources: [this.table.tableArn],
|
|
197
|
+
conditions: { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": ["CODE#*"] } },
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
188
200
|
authRole.addToPolicy(
|
|
189
201
|
new iam.PolicyStatement({
|
|
190
202
|
sid: "PagesTableKey",
|
|
@@ -205,7 +217,7 @@ export class FoundationPages extends cdk.Stack {
|
|
|
205
217
|
FOUNDATION_SLACK_TEAM_ID: instance.slack.teamId,
|
|
206
218
|
FOUNDATION_PAGES_TABLE_NAME: this.table.tableName,
|
|
207
219
|
FOUNDATION_DISPLAY_NAME: display,
|
|
208
|
-
|
|
220
|
+
FOUNDATION_PAGES_DOMAIN: pagesDomain,
|
|
209
221
|
},
|
|
210
222
|
});
|
|
211
223
|
// IAM auth: only CloudFront, signing through origin access control, can call it.
|
|
@@ -260,7 +272,9 @@ export class FoundationPages extends cdk.Stack {
|
|
|
260
272
|
runtime: cloudfront.FunctionRuntime.JS_2_0,
|
|
261
273
|
keyValueStore: edgeKeys,
|
|
262
274
|
comment: `${display} pages: cookie check and path mapping`,
|
|
263
|
-
code: cloudfront.FunctionCode.fromInline(
|
|
275
|
+
code: cloudfront.FunctionCode.fromInline(
|
|
276
|
+
pagesViewerRequestCode(edgeKeys.keyValueStoreId, pagesDomain),
|
|
277
|
+
),
|
|
264
278
|
});
|
|
265
279
|
const headers = new cloudfront.ResponseHeadersPolicy(this, "PagesHeaders", {
|
|
266
280
|
comment: `${display} pages security headers`,
|
|
@@ -282,7 +296,17 @@ export class FoundationPages extends cdk.Stack {
|
|
|
282
296
|
},
|
|
283
297
|
},
|
|
284
298
|
customHeadersBehavior: {
|
|
285
|
-
customHeaders: [
|
|
299
|
+
customHeaders: [
|
|
300
|
+
{ header: "X-Robots-Tag", value: "noindex, nofollow", override: true },
|
|
301
|
+
// Pages are sibling origins on one site. CORP stops page A pulling
|
|
302
|
+
// page B's scripts or data in as subresources (the viewer's Lax
|
|
303
|
+
// cookie would be sent, since the hosts are same-site); COOP cuts
|
|
304
|
+
// window references between pages; Origin-Agent-Cluster turns off
|
|
305
|
+
// `document.domain`, so two pages can never merge into one origin.
|
|
306
|
+
{ header: "Cross-Origin-Resource-Policy", value: "same-origin", override: true },
|
|
307
|
+
{ header: "Cross-Origin-Opener-Policy", value: "same-origin", override: true },
|
|
308
|
+
{ header: "Origin-Agent-Cluster", value: "?1", override: true },
|
|
309
|
+
],
|
|
286
310
|
},
|
|
287
311
|
});
|
|
288
312
|
// --- custom domain ---------------------------------------------------
|
|
@@ -290,22 +314,16 @@ export class FoundationPages extends cdk.Stack {
|
|
|
290
314
|
// company delegates one name (NS records at its DNS provider) and never
|
|
291
315
|
// hands over its main zone. With `pages.hostedZoneId` the parent zone is
|
|
292
316
|
// already in this account and records go straight into it.
|
|
293
|
-
const zoneDomain =
|
|
294
|
-
const zone: route53.IHostedZone
|
|
295
|
-
|
|
296
|
-
?
|
|
297
|
-
:
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
instance.pages.hostedZoneId,
|
|
302
|
-
)
|
|
303
|
-
: new route53.PublicHostedZone(this, "PagesZone", {
|
|
304
|
-
zoneName: zoneDomain,
|
|
305
|
-
comment: `${display} hosted pages (delegated from the company's DNS)`,
|
|
306
|
-
});
|
|
317
|
+
const zoneDomain = pagesDomain;
|
|
318
|
+
const zone: route53.IHostedZone =
|
|
319
|
+
instance.pages.hostedZoneId !== undefined
|
|
320
|
+
? route53.HostedZone.fromHostedZoneId(this, "PagesParentZone", instance.pages.hostedZoneId)
|
|
321
|
+
: new route53.PublicHostedZone(this, "PagesZone", {
|
|
322
|
+
zoneName: zoneDomain,
|
|
323
|
+
comment: `${display} hosted pages (delegated from the company's DNS)`,
|
|
324
|
+
});
|
|
307
325
|
let certificate: acm.ICertificate | undefined;
|
|
308
|
-
if (domain !== undefined
|
|
326
|
+
if (domain !== undefined) {
|
|
309
327
|
const certificateRole = new iam.Role(this, "PagesCertificateRole", {
|
|
310
328
|
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
|
|
311
329
|
description: `${display} issues the pages domain certificate in us-east-1`,
|
|
@@ -347,7 +365,12 @@ export class FoundationPages extends cdk.Stack {
|
|
|
347
365
|
const issued = new cdk.CustomResource(this, "PagesCertificate", {
|
|
348
366
|
serviceToken: certificateFunction.functionArn,
|
|
349
367
|
resourceType: "Custom::FoundationPagesCertificate",
|
|
350
|
-
|
|
368
|
+
// The sign-in host and every page host.
|
|
369
|
+
properties: {
|
|
370
|
+
DomainName: domain,
|
|
371
|
+
SubjectAlternativeNames: [`*.${domain}`],
|
|
372
|
+
HostedZoneId: zone.hostedZoneId,
|
|
373
|
+
},
|
|
351
374
|
});
|
|
352
375
|
certificate = acm.Certificate.fromCertificateArn(
|
|
353
376
|
this,
|
|
@@ -363,7 +386,7 @@ export class FoundationPages extends cdk.Stack {
|
|
|
363
386
|
comment: `${display} pages`,
|
|
364
387
|
...(domain === undefined || certificate === undefined
|
|
365
388
|
? {}
|
|
366
|
-
: { domainNames: [domain], certificate }),
|
|
389
|
+
: { domainNames: [domain, `*.${domain}`], certificate }),
|
|
367
390
|
// Never a bucket listing at `/`, even if the viewer function were detached.
|
|
368
391
|
defaultRootObject: "index.html",
|
|
369
392
|
minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
|
|
@@ -399,16 +422,21 @@ export class FoundationPages extends cdk.Stack {
|
|
|
399
422
|
"@aws-cdk/aws-cloudfront-origins:listBucketSecurityRisk",
|
|
400
423
|
"The viewer function forwards only mapped object keys and 404s `/`; defaultRootObject is set too. LIST only turns a missing key into 404 instead of 403.",
|
|
401
424
|
);
|
|
402
|
-
if (domain !== undefined
|
|
425
|
+
if (domain !== undefined) {
|
|
403
426
|
const target = route53.RecordTarget.fromAlias(
|
|
404
427
|
new route53Targets.CloudFrontTarget(this.distribution),
|
|
405
428
|
);
|
|
406
429
|
// Fully qualified, so the parent-zone case needs no zone name.
|
|
407
430
|
new route53.ARecord(this, "PagesAliasA", { zone, recordName: `${domain}.`, target });
|
|
408
431
|
new route53.AaaaRecord(this, "PagesAliasAaaa", { zone, recordName: `${domain}.`, target });
|
|
432
|
+
new route53.ARecord(this, "PagesWildcardA", { zone, recordName: `*.${domain}.`, target });
|
|
433
|
+
new route53.AaaaRecord(this, "PagesWildcardAaaa", {
|
|
434
|
+
zone,
|
|
435
|
+
recordName: `*.${domain}.`,
|
|
436
|
+
target,
|
|
437
|
+
});
|
|
409
438
|
}
|
|
410
|
-
const
|
|
411
|
-
const baseUrl = domain === undefined ? defaultUrl : `https://${domain}`;
|
|
439
|
+
const signInUrl = `https://${pagesDomain}`;
|
|
412
440
|
|
|
413
441
|
// --- Pages API proxy -------------------------------------------------
|
|
414
442
|
const apiRole = new iam.Role(this, "PagesApiRole", {
|
|
@@ -465,24 +493,23 @@ export class FoundationPages extends cdk.Stack {
|
|
|
465
493
|
environment: {
|
|
466
494
|
FOUNDATION_PAGES_TABLE_NAME: this.table.tableName,
|
|
467
495
|
FOUNDATION_PAGES_BUCKET_NAME: this.bucket.bucketName,
|
|
468
|
-
|
|
496
|
+
...(domain === undefined ? {} : { FOUNDATION_PAGES_DOMAIN: domain }),
|
|
469
497
|
FOUNDATION_SLACK_TEAM_ID: instance.slack.teamId,
|
|
470
498
|
FOUNDATION_SLACK_SIGNING_SECRET_ID: props.secrets.signing.secretName,
|
|
471
499
|
},
|
|
472
500
|
});
|
|
473
501
|
|
|
474
502
|
new cdk.CfnOutput(this, "PagesApiFunctionArn", { value: this.apiFunction.functionArn });
|
|
475
|
-
new cdk.CfnOutput(this, "PagesBaseUrl", {
|
|
503
|
+
new cdk.CfnOutput(this, "PagesBaseUrl", {
|
|
504
|
+
description: "The sign-in host; each page is https://<pageId>.<this domain>",
|
|
505
|
+
value: signInUrl,
|
|
506
|
+
});
|
|
476
507
|
new cdk.CfnOutput(this, "PagesDistributionDomainName", {
|
|
477
508
|
value: this.distribution.distributionDomainName,
|
|
478
509
|
});
|
|
479
510
|
new cdk.CfnOutput(this, "PagesAuthRedirectUrl", {
|
|
480
511
|
description: "Slack OAuth redirect URL for Sign in with Slack on the pages domain",
|
|
481
|
-
value: `${
|
|
482
|
-
});
|
|
483
|
-
// Registered as well, so links from before a custom domain attached keep signing in.
|
|
484
|
-
new cdk.CfnOutput(this, "PagesDefaultAuthRedirectUrl", {
|
|
485
|
-
value: `${defaultUrl}/_auth/callback`,
|
|
512
|
+
value: `${signInUrl}/_auth/callback`,
|
|
486
513
|
});
|
|
487
514
|
new cdk.CfnOutput(this, "PagesCustomDomainAttached", {
|
|
488
515
|
value: domain === undefined ? "false" : "true",
|