@anthusai/papyrus 1.0.0-next.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/amplify/auth/redirect-urls.js +54 -0
- package/amplify/auth/redirect-urls.ts +66 -0
- package/amplify/auth/resource.js +20 -5
- package/amplify/auth/resource.ts +27 -5
- package/amplify/data/resource.js +42 -80
- package/amplify/data/resource.ts +42 -80
- package/amplify/data/schema.js +45 -60
- package/amplify/data/schema.ts +47 -60
- package/amplify/functions/category-action/handler.ts +5 -3
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
- package/amplify/functions/console-chat-responder/resource.js +0 -7
- package/amplify/functions/console-chat-responder/resource.ts +0 -8
- package/amplify/functions/console-chat-responder/src/main.rs +0 -9
- package/amplify/functions/content-actions/handler.py +143 -1
- package/amplify/functions/content-actions/requirements.txt +2 -1
- package/amplify/functions/content-actions/resource.js +6 -1
- package/amplify/functions/content-actions/resource.ts +9 -2
- package/amplify/functions/renditions/handler.py +120 -0
- package/amplify/functions/renditions/requirements.txt +4 -0
- package/amplify/functions/shared/python-bundle.js +42 -2
- package/amplify/functions/shared/python-bundle.ts +47 -2
- package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
- package/amplify/inbound-email/ses-intake.js +86 -0
- package/amplify/inbound-email/ses-intake.ts +110 -0
- package/amplify/plugins/registry.js +19 -0
- package/amplify/plugins/registry.ts +23 -0
- package/amplify/plugins/signup/function/handler.py +200 -0
- package/amplify/plugins/signup/function/resource.js +27 -0
- package/amplify/plugins/signup/function/resource.ts +35 -0
- package/amplify/plugins/signup/papyrus.plugin.js +59 -0
- package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
- package/amplify/plugins/signup/schema.js +40 -0
- package/amplify/plugins/signup/schema.ts +41 -0
- package/amplify/plugins/signup/waf.js +48 -0
- package/amplify/plugins/signup/waf.ts +51 -0
- package/amplify/plugins/types.js +1 -0
- package/amplify/plugins/types.ts +39 -0
- package/amplify/plugins/wire.js +24 -0
- package/amplify/plugins/wire.ts +26 -0
- package/amplify/publication/default-publication.js +18 -0
- package/amplify/publication/default-publication.ts +20 -0
- package/amplify/publication/invalidation-alarm.js +26 -0
- package/amplify/publication/invalidation-alarm.ts +30 -0
- package/amplify/publication/redirect-and-index.js +37 -0
- package/amplify/publication/renditions-function.js +74 -0
- package/amplify/publication/renditions-function.ts +112 -0
- package/amplify/publication/renditions-settings.js +51 -0
- package/amplify/publication/renditions-settings.ts +58 -0
- package/amplify/publication/reproject-on-deploy.js +73 -0
- package/amplify/publication/reproject-on-deploy.ts +78 -0
- package/amplify/publication/s3-projection-settings.js +67 -0
- package/amplify/publication/s3-projection-settings.ts +101 -0
- package/amplify/publication/s3-projection.js +151 -0
- package/amplify/publication/s3-projection.ts +201 -0
- package/amplify/resolver-waves.js +26 -0
- package/amplify/resolver-waves.ts +34 -0
- package/amplify/revalidate-invalidation-settings.js +30 -0
- package/amplify/revalidate-invalidation-settings.ts +35 -0
- package/amplify/revalidate-secret-policy.js +13 -0
- package/amplify/revalidate-secret-policy.ts +19 -0
- package/amplify/site-backend-config.js +65 -1
- package/amplify/site-backend-config.ts +153 -2
- package/amplify/site-backend-names.js +103 -0
- package/amplify/site-backend-names.ts +166 -0
- package/amplify/site-backend.js +143 -176
- package/amplify/site-backend.ts +181 -201
- package/amplify/storage-backups/construct.js +31 -0
- package/amplify/storage-backups/construct.ts +47 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
- package/app/[year]/[month]/[day]/page.tsx +1 -1
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
- package/app/api/archive/editions/route.ts +1 -1
- package/app/api/cyclotron/priming-proof/route.ts +11 -0
- package/app/api/media/[...path]/route.ts +5 -16
- package/app/api/revalidate/route.ts +15 -7
- package/app/archive/page.tsx +1 -1
- package/app/articles/[slug]/page.tsx +7 -5
- package/app/blog-rhythm.css +278 -0
- package/app/cyclotron/page.tsx +13 -0
- package/app/globals.css +2 -0
- package/app/layout.tsx +16 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
- package/app/newsroom/[section]/page.tsx +6 -3
- package/app/newsroom/layout.tsx +4 -9
- package/app/newsroom/settings/page.tsx +6 -0
- package/app/newsroom/work/page.tsx +10 -0
- package/app/page.tsx +22 -8
- package/app/robots.ts +2 -5
- package/app/tailwind.css +1 -0
- package/components/amplify-client-provider.tsx +5 -22
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +152 -5
- package/components/blog-page-background.tsx +23 -6
- package/components/news-desk-page.tsx +4 -15
- package/components/news-desk-taxonomy-client.ts +90 -3
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +2 -1
- package/components/newsroom-board-view.tsx +99 -0
- package/components/newsroom-client-shell.tsx +12 -0
- package/components/newsroom-curation-provider.tsx +17 -0
- package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
- package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
- package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
- package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
- package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
- package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
- package/components/newsroom-newsprint-board.css +48 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +7 -1
- package/components/newsroom-organizer-shell.tsx +130 -0
- package/components/newsroom-organizer.css +63 -0
- package/components/newsroom-references-view.tsx +92 -8
- package/components/papyrus-console-shell.tsx +4 -2
- package/components/pictogram-figure.tsx +9 -2
- package/components/presentation-header.tsx +135 -0
- package/components/presentation-rhythm-hrule.tsx +17 -0
- package/components/priming-proof-page.tsx +92 -0
- package/components/publication-cyclotron-panel.tsx +57 -0
- package/components/reader-auth-control.tsx +8 -5
- package/components/reference-curation-controls.tsx +77 -0
- package/components/reference-cyclotron-status.tsx +109 -0
- package/components/reference-priming.tsx +18 -0
- package/components/reference-relevance-review.tsx +75 -0
- package/components/settings-page.tsx +2 -1
- package/components/signup-entries-panel.tsx +94 -0
- package/components/topic-steering-workspace.tsx +78 -6
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +28 -0
- package/components/use-near-viewport.ts +30 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +148 -27
- package/infra/amplify-app-shell.ts +160 -31
- package/infra/build-specs.js +23 -7
- package/infra/build-specs.ts +27 -10
- package/infra/site-config.js +248 -36
- package/infra/site-config.ts +313 -51
- package/lib/amplify-outputs-path.ts +5 -1
- package/lib/amplify-server-runtime.ts +36 -1
- package/lib/approved-priming-proof-reports.ts +6 -0
- package/lib/blog-feature-solver.ts +459 -0
- package/lib/blog-rhythm.ts +134 -0
- package/lib/brand-fonts-watch.ts +43 -0
- package/lib/brand-fonts.ts +112 -0
- package/lib/cached-content-repository.ts +12 -0
- package/lib/category-repository.ts +43 -93
- package/lib/content-item-lifecycle.ts +55 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-types.ts +4 -0
- package/lib/curation-client.ts +61 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +56 -22
- package/lib/media-route-response.ts +45 -0
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-base-path.ts +12 -3
- package/lib/newsroom-board.ts +99 -0
- package/lib/newsroom-category-drilldown.ts +3 -2
- package/lib/newsroom-forum-routes.ts +6 -4
- package/lib/newsroom-index-filters.ts +14 -10
- package/lib/newsroom-nav.ts +3 -1
- package/lib/newsroom-organizer-nav.ts +37 -0
- package/lib/newsroom-section-routing.ts +20 -0
- package/lib/oauth-redirect-priority.ts +40 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/plugin-settings.ts +11 -0
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +12 -3
- package/lib/priming-proof-report.ts +89 -0
- package/lib/prospectus-site-identity.ts +303 -0
- package/lib/publication-cyclotron.ts +74 -0
- package/lib/rate-limit-retry.ts +55 -0
- package/lib/reader-base-path.ts +24 -0
- package/lib/reader-cache-lifetime.ts +5 -0
- package/lib/reader-static-params.ts +85 -4
- package/lib/reader-storage-url.ts +9 -12
- package/lib/reference-policy.ts +5 -0
- package/lib/relevance-decisions.ts +103 -0
- package/lib/relevance-status.ts +63 -0
- package/lib/renderer.ts +7 -0
- package/lib/revalidate-prewarm.ts +53 -0
- package/lib/revalidate-secret.ts +59 -0
- package/lib/robots-policy.ts +14 -0
- package/lib/root-route-redirect.ts +16 -0
- package/lib/semantic-graph.ts +7 -0
- package/lib/shared-credentials-provider.ts +61 -0
- package/lib/signup-csv.ts +24 -0
- package/lib/site-brand.ts +59 -0
- package/lib/staging-gated-path.ts +23 -0
- package/lib/staging-preview-object.ts +21 -5
- package/lib/test-editor-override.ts +4 -0
- package/lib/video-loading.ts +22 -0
- package/lib/video-script.ts +14 -0
- package/middleware.ts +11 -19
- package/package.json +64 -1
- package/renderers/pretext/article-page.tsx +39 -3
- package/renderers/pretext/presentation-shared.tsx +113 -0
- package/renderers/pretext/presentation-shell.tsx +15 -102
- package/renderers/pretext/rhythm-article-page.tsx +165 -0
- package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
- package/renderers/pretext/rhythm-shell-style.ts +35 -0
- package/routes.manifest.json +68 -12
- package/src/sync.mjs +30 -6
- package/src/with-papyrus.d.mts +9 -0
- package/src/with-papyrus.mjs +48 -3
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
- package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
|
@@ -3,22 +3,45 @@ import * as iam from "aws-cdk-lib/aws-iam";
|
|
|
3
3
|
import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
4
4
|
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider.js";
|
|
5
5
|
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs.js";
|
|
6
|
-
import { isStagingEnabled, resolveStackName, resolveStagingDomainName } from "./site-config.js";
|
|
6
|
+
import { PROSPECTUS_PAPYRUS_SITE_ROUTES, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveProspectusAccountsEndpoint, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
|
|
7
7
|
function environmentVariables(variables) {
|
|
8
8
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
9
9
|
}
|
|
10
|
+
function readerCustomHeaders(rules) {
|
|
11
|
+
if (!rules)
|
|
12
|
+
return undefined;
|
|
13
|
+
const lines = ["customHeaders:"];
|
|
14
|
+
for (const rule of rules) {
|
|
15
|
+
lines.push(` - pattern: ${JSON.stringify(rule.pattern)}`, " headers:");
|
|
16
|
+
for (const [key, value] of Object.entries(rule.headers)) {
|
|
17
|
+
lines.push(` - key: ${JSON.stringify(key)}`, ` value: ${JSON.stringify(value)}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return lines.join("\n") + "\n";
|
|
21
|
+
}
|
|
10
22
|
/**
|
|
11
23
|
* Provisions the Amplify *app shell* of a Papyrus publication from its
|
|
12
24
|
* site.json:
|
|
13
25
|
*
|
|
14
26
|
* - CMS app (WEB_COMPUTE) with branches `main` (production, owns the backend)
|
|
15
27
|
* and `staging` (frontend only, reads the production backend)
|
|
16
|
-
* - for `markus-static` sites, a reader app (WEB) with branch `main
|
|
28
|
+
* - for `markus-static` sites, a reader app (WEB) with branch `main`; a
|
|
29
|
+
* `markus-static` site may omit `cms` to get the reader app alone (no CMS
|
|
30
|
+
* app, no CMS roles, no authoring role)
|
|
17
31
|
* - a domain association per branch/app when a domain is configured (without
|
|
18
32
|
* one, no AWS::Amplify::Domain is created and the default amplifyapp.com
|
|
19
33
|
* URL is used), IAM service and compute roles
|
|
20
34
|
* - `cms.staging: false` omits the staging branch and its domain
|
|
21
35
|
*
|
|
36
|
+
* Google sign-in is always on: the branches never set PAPYRUS_DISABLE_GOOGLE_OAUTH,
|
|
37
|
+
and `cms.cognitoDomainPrefix` becomes the hosted-UI domain
|
|
38
|
+
(PAPYRUS_COGNITO_DOMAIN_PREFIX) only when `cms.applyCognitoDomainPrefix` is true
|
|
39
|
+
(PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX); otherwise Amplify's generated domain is kept. The secrets GOOGLE_CLIENT_ID and
|
|
40
|
+
GOOGLE_CLIENT_SECRET are Amplify backend secrets set in the console, never here.
|
|
41
|
+
The stack also creates the `<siteId>-papyrus-authoring` role. The CLI signs
|
|
42
|
+
* AppSync requests (SigV4) with credentials for that role (SSO, OIDC or any
|
|
43
|
+
* principal in the account that may assume it); no token is stored anywhere.
|
|
44
|
+
*
|
|
22
45
|
* No repository and no access token are configured: the apps are manual-deploy
|
|
23
46
|
* until Amplify's GitHub App is connected once in the console
|
|
24
47
|
* (docs/site-hosting.md). The Papyrus backend itself is created inside the CMS
|
|
@@ -29,9 +52,16 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
29
52
|
readerAppId;
|
|
30
53
|
constructor(scope, id, config, props) {
|
|
31
54
|
super(scope, id, props);
|
|
55
|
+
if (!hasCmsApp(config)) {
|
|
56
|
+
if (!config.reader)
|
|
57
|
+
throw new Error(`site ${config.siteId} has neither a cms section nor a reader`);
|
|
58
|
+
this.readerAppId = this.addReaderApp(config, config.reader);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
32
61
|
const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
|
|
33
62
|
const stagingEnabled = isStagingEnabled(config);
|
|
34
63
|
const stagingDomainName = resolveStagingDomainName(config);
|
|
64
|
+
const cmsHostName = resolveCmsHostName(config);
|
|
35
65
|
const cmsServiceRole = new iam.Role(this, "AmplifyServiceRole", {
|
|
36
66
|
assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
|
|
37
67
|
description: `Backend deploy and staging build service role for Amplify app ${cmsAppName} (AdministratorAccess: documented risk)`,
|
|
@@ -45,6 +75,27 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
45
75
|
iam.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchLogsFullAccess"),
|
|
46
76
|
],
|
|
47
77
|
});
|
|
78
|
+
const revalidateSecretParameterName = config.frontend === "pretext" ? resolveRevalidateSecretParameterName(config) : undefined;
|
|
79
|
+
if (revalidateSecretParameterName) {
|
|
80
|
+
computeRole.addToPolicy(new iam.PolicyStatement({
|
|
81
|
+
sid: "ReadRevalidateSecret",
|
|
82
|
+
actions: ["ssm:GetParameter"],
|
|
83
|
+
resources: [
|
|
84
|
+
this.formatArn({ service: "ssm", resource: "parameter", resourceName: revalidateSecretParameterName.slice(1), arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
85
|
+
],
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
const prospectusAccountsApiStageArn = config.cms.prospectusAccountsApiStageArn;
|
|
89
|
+
const prospectusEnvironment = prospectusAccountsApiStageArn
|
|
90
|
+
? { PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT: resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn) }
|
|
91
|
+
: {};
|
|
92
|
+
if (prospectusAccountsApiStageArn) {
|
|
93
|
+
computeRole.addToPolicy(new iam.PolicyStatement({
|
|
94
|
+
sid: "InvokeProspectusAccountsAsRegisteredSite",
|
|
95
|
+
actions: ["execute-api:Invoke"],
|
|
96
|
+
resources: PROSPECTUS_PAPYRUS_SITE_ROUTES.map((route) => `${prospectusAccountsApiStageArn}/${route}`),
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
48
99
|
const cmsApp = new amplify.CfnApp(this, "App", {
|
|
49
100
|
name: cmsAppName,
|
|
50
101
|
description: `Papyrus newsroom CMS for ${config.siteId} (WEB_COMPUTE)`,
|
|
@@ -53,17 +104,25 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
53
104
|
computeRoleArn: computeRole.roleArn,
|
|
54
105
|
buildSpec: cmsProductionBuildSpec(config),
|
|
55
106
|
jobConfig: config.cms.buildComputeType ? { buildComputeType: config.cms.buildComputeType } : undefined,
|
|
56
|
-
customRules: [
|
|
107
|
+
customRules: [
|
|
108
|
+
...(config.cms.redirects ?? []).map((redirect) => ({
|
|
109
|
+
source: `https://${redirect.source}`,
|
|
110
|
+
target: `https://${resolveCmsHostName(config)}`,
|
|
111
|
+
status: String(redirect.status),
|
|
112
|
+
})),
|
|
113
|
+
{ source: "/<*>", target: "/index.html", status: "404-200" },
|
|
114
|
+
],
|
|
57
115
|
});
|
|
58
116
|
const stagingMode = config.frontend === "markus-static"
|
|
59
117
|
? { PAPYRUS_STAGING_PREVIEW: "static" }
|
|
60
118
|
: { PAPYRUS_CONTENT_SOURCE: "drafts" };
|
|
61
|
-
const productionOrigin =
|
|
62
|
-
? `https://${
|
|
119
|
+
const productionOrigin = cmsHostName
|
|
120
|
+
? `https://${cmsHostName}/`
|
|
63
121
|
: Fn.join("", ["https://main.", cmsApp.attrDefaultDomain, "/"]);
|
|
64
122
|
const stagingOrigin = stagingDomainName
|
|
65
123
|
? `https://${stagingDomainName}/`
|
|
66
124
|
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain, "/"]);
|
|
125
|
+
const sharedRootDomain = config.cms.domainPrefix !== undefined;
|
|
67
126
|
const defaultOriginsToAllow = [
|
|
68
127
|
...(config.cms.domainName ? [] : [productionOrigin]),
|
|
69
128
|
...(stagingEnabled && !stagingDomainName ? [stagingOrigin] : []),
|
|
@@ -79,14 +138,19 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
79
138
|
enablePerformanceMode: false,
|
|
80
139
|
environmentVariables: environmentVariables({
|
|
81
140
|
...config.cms.environment,
|
|
141
|
+
PAPYRUS_COGNITO_DOMAIN_PREFIX: config.cms.cognitoDomainPrefix,
|
|
142
|
+
...(config.cms.applyCognitoDomainPrefix ? { PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX: "true" } : {}),
|
|
82
143
|
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
83
144
|
PAPYRUS_SITE_BRAND: config.brand,
|
|
84
145
|
SITE_ENV: "production",
|
|
85
146
|
PAPYRUS_CONTENT_SOURCE: "published",
|
|
147
|
+
...(revalidateSecretParameterName ? { PAPYRUS_REVALIDATE_SECRET_PARAMETER: revalidateSecretParameterName } : {}),
|
|
148
|
+
...prospectusEnvironment,
|
|
86
149
|
}),
|
|
87
150
|
});
|
|
151
|
+
let stagingBranch;
|
|
88
152
|
if (stagingEnabled) {
|
|
89
|
-
|
|
153
|
+
stagingBranch = new amplify.CfnBranch(this, "StagingBranch", {
|
|
90
154
|
appId: cmsApp.attrAppId,
|
|
91
155
|
branchName: "staging",
|
|
92
156
|
stage: "BETA",
|
|
@@ -95,16 +159,19 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
95
159
|
buildSpec: cmsStagingBuildSpec(config),
|
|
96
160
|
environmentVariables: environmentVariables({
|
|
97
161
|
...config.cms.environment,
|
|
162
|
+
PAPYRUS_COGNITO_DOMAIN_PREFIX: config.cms.cognitoDomainPrefix,
|
|
163
|
+
...(config.cms.applyCognitoDomainPrefix ? { PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX: "true" } : {}),
|
|
98
164
|
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
99
165
|
PAPYRUS_SITE_BRAND: config.brand,
|
|
100
166
|
SITE_ENV: "staging",
|
|
101
167
|
...stagingMode,
|
|
168
|
+
...prospectusEnvironment,
|
|
102
169
|
NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
|
|
103
170
|
? `https://${stagingDomainName}`
|
|
104
171
|
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]),
|
|
105
172
|
}),
|
|
106
173
|
});
|
|
107
|
-
if (stagingDomainName) {
|
|
174
|
+
if (stagingDomainName && !sharedRootDomain) {
|
|
108
175
|
const stagingDomain = new amplify.CfnDomain(this, "StagingDomain", {
|
|
109
176
|
appId: cmsApp.attrAppId,
|
|
110
177
|
domainName: stagingDomainName,
|
|
@@ -114,27 +181,89 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
114
181
|
}
|
|
115
182
|
}
|
|
116
183
|
if (config.cms.domainName) {
|
|
184
|
+
const subDomainSettings = sharedRootDomain
|
|
185
|
+
? [
|
|
186
|
+
{ branchName: "main", prefix: config.cms.domainPrefix },
|
|
187
|
+
...(config.cms.redirects ?? []).map((redirect) => ({
|
|
188
|
+
branchName: "main",
|
|
189
|
+
prefix: resolveRedirectSubDomainPrefix(config.cms.domainName, redirect.source),
|
|
190
|
+
})),
|
|
191
|
+
...(stagingBranch ? [{ branchName: "staging", prefix: config.cms.stagingDomainPrefix }] : []),
|
|
192
|
+
]
|
|
193
|
+
: [{ branchName: "main", prefix: "" }];
|
|
117
194
|
const productionDomain = new amplify.CfnDomain(this, "Domain", {
|
|
118
195
|
appId: cmsApp.attrAppId,
|
|
119
196
|
domainName: config.cms.domainName,
|
|
120
|
-
subDomainSettings
|
|
197
|
+
subDomainSettings,
|
|
121
198
|
});
|
|
122
199
|
productionDomain.addResourceDependency(productionBranch);
|
|
200
|
+
if (sharedRootDomain && stagingBranch) {
|
|
201
|
+
productionDomain.addResourceDependency(stagingBranch);
|
|
202
|
+
}
|
|
123
203
|
}
|
|
124
204
|
this.cmsAppId = cmsApp.attrAppId;
|
|
125
205
|
new CfnOutput(this, "CmsAppId", { value: cmsApp.attrAppId, description: `Amplify app id for ${cmsAppName}` });
|
|
126
206
|
new CfnOutput(this, "CmsOrigin", { value: productionOrigin });
|
|
207
|
+
new CfnOutput(this, "CmsComputeRoleArn", {
|
|
208
|
+
value: computeRole.roleArn,
|
|
209
|
+
description: `SSR compute role for ${cmsAppName}; the roleArn of its Prospectus site registration`,
|
|
210
|
+
});
|
|
127
211
|
if (stagingEnabled) {
|
|
128
212
|
new CfnOutput(this, "StagingOrigin", { value: stagingOrigin });
|
|
129
213
|
}
|
|
130
214
|
if (config.reader) {
|
|
131
215
|
this.readerAppId = this.addReaderApp(config, config.reader);
|
|
132
216
|
}
|
|
217
|
+
const amplifyAppIds = [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])];
|
|
218
|
+
this.addAuthoringRole(config, cmsApp.attrAppId, amplifyAppIds);
|
|
133
219
|
if (config.github) {
|
|
134
|
-
this.addGithubCiRole(config, config.github,
|
|
220
|
+
this.addGithubCiRole(config, config.github, cmsApp.attrAppId, amplifyAppIds);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
authoringStatements(config, cmsAppId, amplifyAppIds) {
|
|
224
|
+
const mediaBucketArn = `arn:${this.partition}:s3:::amplify-${cmsAppId}-*`;
|
|
225
|
+
return [
|
|
226
|
+
new iam.PolicyStatement({
|
|
227
|
+
sid: "SignAppSyncAuthoringRequests",
|
|
228
|
+
actions: ["appsync:GraphQL"],
|
|
229
|
+
resources: ["Query", "Mutation"].map((typeName) => this.formatArn({ service: "appsync", resource: "apis", resourceName: `*/types/${typeName}/fields/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME })),
|
|
230
|
+
}),
|
|
231
|
+
new iam.PolicyStatement({
|
|
232
|
+
sid: "ReadWriteMediaAndPreview",
|
|
233
|
+
actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
|
|
234
|
+
resources: [`${mediaBucketArn}/media/*`, `${mediaBucketArn}/${resolveStoragePreviewPrefix(config)}*`],
|
|
235
|
+
}),
|
|
236
|
+
new iam.PolicyStatement({
|
|
237
|
+
sid: "ListMediaAndPreview",
|
|
238
|
+
actions: ["s3:ListBucket"],
|
|
239
|
+
resources: [mediaBucketArn],
|
|
240
|
+
conditions: { StringLike: { "s3:prefix": ["media/*", `${resolveStoragePreviewPrefix(config)}*`] } },
|
|
241
|
+
}),
|
|
242
|
+
new iam.PolicyStatement({
|
|
243
|
+
sid: "StartAndInspectAmplifyJobs",
|
|
244
|
+
actions: ["amplify:StartJob", "amplify:ListJobs", "amplify:GetJob", "amplify:ListBranches"],
|
|
245
|
+
resources: amplifyAppIds.flatMap((appId) => [
|
|
246
|
+
this.formatArn({ service: "amplify", resource: "apps", resourceName: appId, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
247
|
+
this.formatArn({ service: "amplify", resource: "apps", resourceName: `${appId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
248
|
+
]),
|
|
249
|
+
}),
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
addAuthoringRole(config, cmsAppId, amplifyAppIds) {
|
|
253
|
+
const authoringRole = new iam.Role(this, "PapyrusAuthoringRole", {
|
|
254
|
+
roleName: `${config.siteId}-papyrus-authoring`,
|
|
255
|
+
description: `Papyrus CLI authoring role for ${config.siteId}: AppSync (SigV4), media and preview storage, rebuild jobs. Assumable by principals in this account that are allowed sts:AssumeRole on it.`,
|
|
256
|
+
assumedBy: new iam.AccountRootPrincipal(),
|
|
257
|
+
});
|
|
258
|
+
for (const statement of this.authoringStatements(config, cmsAppId, amplifyAppIds)) {
|
|
259
|
+
authoringRole.addToPolicy(statement);
|
|
135
260
|
}
|
|
261
|
+
new CfnOutput(this, "PapyrusAuthoringRoleArn", {
|
|
262
|
+
value: authoringRole.roleArn,
|
|
263
|
+
description: `Role the Papyrus CLI assumes for ${config.siteId} (profile role_arn)`,
|
|
264
|
+
});
|
|
136
265
|
}
|
|
137
|
-
addGithubCiRole(config, github, amplifyAppIds) {
|
|
266
|
+
addGithubCiRole(config, github, cmsAppId, amplifyAppIds) {
|
|
138
267
|
const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(this, "GithubOidcProvider", this.formatArn({
|
|
139
268
|
service: "iam",
|
|
140
269
|
region: "",
|
|
@@ -153,22 +282,9 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
153
282
|
},
|
|
154
283
|
}),
|
|
155
284
|
});
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
resources: amplifyAppIds.flatMap((appId) => [
|
|
160
|
-
this.formatArn({ service: "amplify", resource: "apps", resourceName: appId, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
161
|
-
this.formatArn({ service: "amplify", resource: "apps", resourceName: `${appId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
162
|
-
]),
|
|
163
|
-
}));
|
|
164
|
-
ciRole.addToPolicy(new iam.PolicyStatement({
|
|
165
|
-
sid: "ReadSiteSecretsForJwtMinting",
|
|
166
|
-
actions: ["ssm:GetParameter", "ssm:GetParameters"],
|
|
167
|
-
resources: [
|
|
168
|
-
this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/${this.cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
169
|
-
this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/shared/${this.cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
170
|
-
],
|
|
171
|
-
}));
|
|
285
|
+
for (const statement of this.authoringStatements(config, cmsAppId, amplifyAppIds)) {
|
|
286
|
+
ciRole.addToPolicy(statement);
|
|
287
|
+
}
|
|
172
288
|
ciRole.addToPolicy(new iam.PolicyStatement({
|
|
173
289
|
sid: "WhoAmI",
|
|
174
290
|
actions: ["sts:GetCallerIdentity"],
|
|
@@ -203,6 +319,11 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
203
319
|
platform: "WEB",
|
|
204
320
|
iamServiceRole: readerServiceRole.roleArn,
|
|
205
321
|
buildSpec: readerBuildSpec(config),
|
|
322
|
+
customRules: [
|
|
323
|
+
...(reader.redirects ?? []),
|
|
324
|
+
{ source: "/<*>", target: "/404.html", status: "404-200" },
|
|
325
|
+
],
|
|
326
|
+
customHeaders: readerCustomHeaders(reader.customHeaders),
|
|
206
327
|
});
|
|
207
328
|
const readerBranch = new amplify.CfnBranch(this, "ReaderBranch", {
|
|
208
329
|
appId: readerApp.attrAppId,
|
|
@@ -219,7 +340,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
219
340
|
const readerDomain = new amplify.CfnDomain(this, "ReaderDomain", {
|
|
220
341
|
appId: readerApp.attrAppId,
|
|
221
342
|
domainName: reader.domainName,
|
|
222
|
-
subDomainSettings: [{ branchName: readerBranchName, prefix
|
|
343
|
+
subDomainSettings: (reader.domainPrefixes ?? [""]).map((prefix) => ({ branchName: readerBranchName, prefix })),
|
|
223
344
|
});
|
|
224
345
|
readerDomain.addResourceDependency(readerBranch);
|
|
225
346
|
}
|
|
@@ -4,39 +4,69 @@ import * as iam from "aws-cdk-lib/aws-iam";
|
|
|
4
4
|
import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
5
5
|
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider";
|
|
6
6
|
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs";
|
|
7
|
-
import { AmplifyAppShellSiteConfig, isStagingEnabled, resolveStackName, resolveStagingDomainName } from "./site-config";
|
|
7
|
+
import { AmplifyAppShellSiteConfig, PROSPECTUS_PAPYRUS_SITE_ROUTES, ReaderCustomHeaderRule, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveProspectusAccountsEndpoint, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
|
|
8
8
|
|
|
9
9
|
function environmentVariables(variables: Record<string, string>): amplify.CfnBranch.EnvironmentVariableProperty[] {
|
|
10
10
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function readerCustomHeaders(rules: ReaderCustomHeaderRule[] | undefined): string | undefined {
|
|
14
|
+
if (!rules) return undefined;
|
|
15
|
+
const lines = ["customHeaders:"];
|
|
16
|
+
for (const rule of rules) {
|
|
17
|
+
lines.push(` - pattern: ${JSON.stringify(rule.pattern)}`, " headers:");
|
|
18
|
+
for (const [key, value] of Object.entries(rule.headers)) {
|
|
19
|
+
lines.push(` - key: ${JSON.stringify(key)}`, ` value: ${JSON.stringify(value)}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return lines.join("\n") + "\n";
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
/**
|
|
14
26
|
* Provisions the Amplify *app shell* of a Papyrus publication from its
|
|
15
27
|
* site.json:
|
|
16
28
|
*
|
|
17
29
|
* - CMS app (WEB_COMPUTE) with branches `main` (production, owns the backend)
|
|
18
30
|
* and `staging` (frontend only, reads the production backend)
|
|
19
|
-
* - for `markus-static` sites, a reader app (WEB) with branch `main
|
|
31
|
+
* - for `markus-static` sites, a reader app (WEB) with branch `main`; a
|
|
32
|
+
* `markus-static` site may omit `cms` to get the reader app alone (no CMS
|
|
33
|
+
* app, no CMS roles, no authoring role)
|
|
20
34
|
* - a domain association per branch/app when a domain is configured (without
|
|
21
35
|
* one, no AWS::Amplify::Domain is created and the default amplifyapp.com
|
|
22
36
|
* URL is used), IAM service and compute roles
|
|
23
37
|
* - `cms.staging: false` omits the staging branch and its domain
|
|
24
38
|
*
|
|
39
|
+
* Google sign-in is always on: the branches never set PAPYRUS_DISABLE_GOOGLE_OAUTH,
|
|
40
|
+
and `cms.cognitoDomainPrefix` becomes the hosted-UI domain
|
|
41
|
+
(PAPYRUS_COGNITO_DOMAIN_PREFIX) only when `cms.applyCognitoDomainPrefix` is true
|
|
42
|
+
(PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX); otherwise Amplify's generated domain is kept. The secrets GOOGLE_CLIENT_ID and
|
|
43
|
+
GOOGLE_CLIENT_SECRET are Amplify backend secrets set in the console, never here.
|
|
44
|
+
The stack also creates the `<siteId>-papyrus-authoring` role. The CLI signs
|
|
45
|
+
* AppSync requests (SigV4) with credentials for that role (SSO, OIDC or any
|
|
46
|
+
* principal in the account that may assume it); no token is stored anywhere.
|
|
47
|
+
*
|
|
25
48
|
* No repository and no access token are configured: the apps are manual-deploy
|
|
26
49
|
* until Amplify's GitHub App is connected once in the console
|
|
27
50
|
* (docs/site-hosting.md). The Papyrus backend itself is created inside the CMS
|
|
28
51
|
* app by `ampx pipeline-deploy` from the production build.
|
|
29
52
|
*/
|
|
30
53
|
export class AmplifyAppShellStack extends Stack {
|
|
31
|
-
public readonly cmsAppId
|
|
54
|
+
public readonly cmsAppId?: string;
|
|
32
55
|
public readonly readerAppId?: string;
|
|
33
56
|
|
|
34
57
|
constructor(scope: Construct, id: string, config: AmplifyAppShellSiteConfig, props?: StackProps) {
|
|
35
58
|
super(scope, id, props);
|
|
36
59
|
|
|
60
|
+
if (!hasCmsApp(config)) {
|
|
61
|
+
if (!config.reader) throw new Error(`site ${config.siteId} has neither a cms section nor a reader`);
|
|
62
|
+
this.readerAppId = this.addReaderApp(config, config.reader);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
37
66
|
const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
|
|
38
67
|
const stagingEnabled = isStagingEnabled(config);
|
|
39
68
|
const stagingDomainName = resolveStagingDomainName(config);
|
|
69
|
+
const cmsHostName = resolveCmsHostName(config);
|
|
40
70
|
|
|
41
71
|
const cmsServiceRole = new iam.Role(this, "AmplifyServiceRole", {
|
|
42
72
|
assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
|
|
@@ -53,6 +83,33 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
53
83
|
],
|
|
54
84
|
});
|
|
55
85
|
|
|
86
|
+
const revalidateSecretParameterName = config.frontend === "pretext" ? resolveRevalidateSecretParameterName(config) : undefined;
|
|
87
|
+
if (revalidateSecretParameterName) {
|
|
88
|
+
computeRole.addToPolicy(
|
|
89
|
+
new iam.PolicyStatement({
|
|
90
|
+
sid: "ReadRevalidateSecret",
|
|
91
|
+
actions: ["ssm:GetParameter"],
|
|
92
|
+
resources: [
|
|
93
|
+
this.formatArn({ service: "ssm", resource: "parameter", resourceName: revalidateSecretParameterName.slice(1), arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
94
|
+
],
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const prospectusAccountsApiStageArn = config.cms.prospectusAccountsApiStageArn;
|
|
100
|
+
const prospectusEnvironment = prospectusAccountsApiStageArn
|
|
101
|
+
? { PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT: resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn) }
|
|
102
|
+
: {};
|
|
103
|
+
if (prospectusAccountsApiStageArn) {
|
|
104
|
+
computeRole.addToPolicy(
|
|
105
|
+
new iam.PolicyStatement({
|
|
106
|
+
sid: "InvokeProspectusAccountsAsRegisteredSite",
|
|
107
|
+
actions: ["execute-api:Invoke"],
|
|
108
|
+
resources: PROSPECTUS_PAPYRUS_SITE_ROUTES.map((route) => `${prospectusAccountsApiStageArn}/${route}`),
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
56
113
|
const cmsApp = new amplify.CfnApp(this, "App", {
|
|
57
114
|
name: cmsAppName,
|
|
58
115
|
description: `Papyrus newsroom CMS for ${config.siteId} (WEB_COMPUTE)`,
|
|
@@ -61,19 +118,27 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
61
118
|
computeRoleArn: computeRole.roleArn,
|
|
62
119
|
buildSpec: cmsProductionBuildSpec(config),
|
|
63
120
|
jobConfig: config.cms.buildComputeType ? { buildComputeType: config.cms.buildComputeType } : undefined,
|
|
64
|
-
customRules: [
|
|
121
|
+
customRules: [
|
|
122
|
+
...(config.cms.redirects ?? []).map((redirect) => ({
|
|
123
|
+
source: `https://${redirect.source}`,
|
|
124
|
+
target: `https://${resolveCmsHostName(config)}`,
|
|
125
|
+
status: String(redirect.status),
|
|
126
|
+
})),
|
|
127
|
+
{ source: "/<*>", target: "/index.html", status: "404-200" },
|
|
128
|
+
],
|
|
65
129
|
});
|
|
66
130
|
|
|
67
131
|
const stagingMode: Record<string, string> = config.frontend === "markus-static"
|
|
68
132
|
? { PAPYRUS_STAGING_PREVIEW: "static" }
|
|
69
133
|
: { PAPYRUS_CONTENT_SOURCE: "drafts" };
|
|
70
134
|
|
|
71
|
-
const productionOrigin =
|
|
72
|
-
? `https://${
|
|
135
|
+
const productionOrigin = cmsHostName
|
|
136
|
+
? `https://${cmsHostName}/`
|
|
73
137
|
: Fn.join("", ["https://main.", cmsApp.attrDefaultDomain, "/"]);
|
|
74
138
|
const stagingOrigin = stagingDomainName
|
|
75
139
|
? `https://${stagingDomainName}/`
|
|
76
140
|
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain, "/"]);
|
|
141
|
+
const sharedRootDomain = config.cms.domainPrefix !== undefined;
|
|
77
142
|
const defaultOriginsToAllow = [
|
|
78
143
|
...(config.cms.domainName ? [] : [productionOrigin]),
|
|
79
144
|
...(stagingEnabled && !stagingDomainName ? [stagingOrigin] : []),
|
|
@@ -90,15 +155,20 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
90
155
|
enablePerformanceMode: false,
|
|
91
156
|
environmentVariables: environmentVariables({
|
|
92
157
|
...config.cms.environment,
|
|
158
|
+
PAPYRUS_COGNITO_DOMAIN_PREFIX: config.cms.cognitoDomainPrefix,
|
|
159
|
+
...(config.cms.applyCognitoDomainPrefix ? { PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX: "true" } : {}),
|
|
93
160
|
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
94
161
|
PAPYRUS_SITE_BRAND: config.brand,
|
|
95
162
|
SITE_ENV: "production",
|
|
96
163
|
PAPYRUS_CONTENT_SOURCE: "published",
|
|
164
|
+
...(revalidateSecretParameterName ? { PAPYRUS_REVALIDATE_SECRET_PARAMETER: revalidateSecretParameterName } : {}),
|
|
165
|
+
...prospectusEnvironment,
|
|
97
166
|
}),
|
|
98
167
|
});
|
|
99
168
|
|
|
169
|
+
let stagingBranch: amplify.CfnBranch | undefined;
|
|
100
170
|
if (stagingEnabled) {
|
|
101
|
-
|
|
171
|
+
stagingBranch = new amplify.CfnBranch(this, "StagingBranch", {
|
|
102
172
|
appId: cmsApp.attrAppId,
|
|
103
173
|
branchName: "staging",
|
|
104
174
|
stage: "BETA",
|
|
@@ -107,17 +177,20 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
107
177
|
buildSpec: cmsStagingBuildSpec(config),
|
|
108
178
|
environmentVariables: environmentVariables({
|
|
109
179
|
...config.cms.environment,
|
|
180
|
+
PAPYRUS_COGNITO_DOMAIN_PREFIX: config.cms.cognitoDomainPrefix,
|
|
181
|
+
...(config.cms.applyCognitoDomainPrefix ? { PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX: "true" } : {}),
|
|
110
182
|
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
111
183
|
PAPYRUS_SITE_BRAND: config.brand,
|
|
112
184
|
SITE_ENV: "staging",
|
|
113
185
|
...stagingMode,
|
|
186
|
+
...prospectusEnvironment,
|
|
114
187
|
NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
|
|
115
188
|
? `https://${stagingDomainName}`
|
|
116
189
|
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]),
|
|
117
190
|
}),
|
|
118
191
|
});
|
|
119
192
|
|
|
120
|
-
if (stagingDomainName) {
|
|
193
|
+
if (stagingDomainName && !sharedRootDomain) {
|
|
121
194
|
const stagingDomain = new amplify.CfnDomain(this, "StagingDomain", {
|
|
122
195
|
appId: cmsApp.attrAppId,
|
|
123
196
|
domainName: stagingDomainName,
|
|
@@ -128,17 +201,34 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
128
201
|
}
|
|
129
202
|
|
|
130
203
|
if (config.cms.domainName) {
|
|
204
|
+
const subDomainSettings = sharedRootDomain
|
|
205
|
+
? [
|
|
206
|
+
{ branchName: "main", prefix: config.cms.domainPrefix as string },
|
|
207
|
+
...(config.cms.redirects ?? []).map((redirect) => ({
|
|
208
|
+
branchName: "main",
|
|
209
|
+
prefix: resolveRedirectSubDomainPrefix(config.cms.domainName as string, redirect.source),
|
|
210
|
+
})),
|
|
211
|
+
...(stagingBranch ? [{ branchName: "staging", prefix: config.cms.stagingDomainPrefix as string }] : []),
|
|
212
|
+
]
|
|
213
|
+
: [{ branchName: "main", prefix: "" }];
|
|
131
214
|
const productionDomain = new amplify.CfnDomain(this, "Domain", {
|
|
132
215
|
appId: cmsApp.attrAppId,
|
|
133
216
|
domainName: config.cms.domainName,
|
|
134
|
-
subDomainSettings
|
|
217
|
+
subDomainSettings,
|
|
135
218
|
});
|
|
136
219
|
productionDomain.addResourceDependency(productionBranch);
|
|
220
|
+
if (sharedRootDomain && stagingBranch) {
|
|
221
|
+
productionDomain.addResourceDependency(stagingBranch);
|
|
222
|
+
}
|
|
137
223
|
}
|
|
138
224
|
|
|
139
225
|
this.cmsAppId = cmsApp.attrAppId;
|
|
140
226
|
new CfnOutput(this, "CmsAppId", { value: cmsApp.attrAppId, description: `Amplify app id for ${cmsAppName}` });
|
|
141
227
|
new CfnOutput(this, "CmsOrigin", { value: productionOrigin });
|
|
228
|
+
new CfnOutput(this, "CmsComputeRoleArn", {
|
|
229
|
+
value: computeRole.roleArn,
|
|
230
|
+
description: `SSR compute role for ${cmsAppName}; the roleArn of its Prospectus site registration`,
|
|
231
|
+
});
|
|
142
232
|
if (stagingEnabled) {
|
|
143
233
|
new CfnOutput(this, "StagingOrigin", { value: stagingOrigin });
|
|
144
234
|
}
|
|
@@ -147,14 +237,65 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
147
237
|
this.readerAppId = this.addReaderApp(config, config.reader);
|
|
148
238
|
}
|
|
149
239
|
|
|
240
|
+
const amplifyAppIds = [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])];
|
|
241
|
+
this.addAuthoringRole(config, cmsApp.attrAppId, amplifyAppIds);
|
|
242
|
+
|
|
150
243
|
if (config.github) {
|
|
151
|
-
this.addGithubCiRole(config, config.github,
|
|
244
|
+
this.addGithubCiRole(config, config.github, cmsApp.attrAppId, amplifyAppIds);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private authoringStatements(config: AmplifyAppShellSiteConfig, cmsAppId: string, amplifyAppIds: string[]): iam.PolicyStatement[] {
|
|
249
|
+
const mediaBucketArn = `arn:${this.partition}:s3:::amplify-${cmsAppId}-*`;
|
|
250
|
+
return [
|
|
251
|
+
new iam.PolicyStatement({
|
|
252
|
+
sid: "SignAppSyncAuthoringRequests",
|
|
253
|
+
actions: ["appsync:GraphQL"],
|
|
254
|
+
resources: ["Query", "Mutation"].map((typeName) =>
|
|
255
|
+
this.formatArn({ service: "appsync", resource: "apis", resourceName: `*/types/${typeName}/fields/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
256
|
+
),
|
|
257
|
+
}),
|
|
258
|
+
new iam.PolicyStatement({
|
|
259
|
+
sid: "ReadWriteMediaAndPreview",
|
|
260
|
+
actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
|
|
261
|
+
resources: [`${mediaBucketArn}/media/*`, `${mediaBucketArn}/${resolveStoragePreviewPrefix(config)}*`],
|
|
262
|
+
}),
|
|
263
|
+
new iam.PolicyStatement({
|
|
264
|
+
sid: "ListMediaAndPreview",
|
|
265
|
+
actions: ["s3:ListBucket"],
|
|
266
|
+
resources: [mediaBucketArn],
|
|
267
|
+
conditions: { StringLike: { "s3:prefix": ["media/*", `${resolveStoragePreviewPrefix(config)}*`] } },
|
|
268
|
+
}),
|
|
269
|
+
new iam.PolicyStatement({
|
|
270
|
+
sid: "StartAndInspectAmplifyJobs",
|
|
271
|
+
actions: ["amplify:StartJob", "amplify:ListJobs", "amplify:GetJob", "amplify:ListBranches"],
|
|
272
|
+
resources: amplifyAppIds.flatMap((appId) => [
|
|
273
|
+
this.formatArn({ service: "amplify", resource: "apps", resourceName: appId, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
274
|
+
this.formatArn({ service: "amplify", resource: "apps", resourceName: `${appId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
275
|
+
]),
|
|
276
|
+
}),
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private addAuthoringRole(config: AmplifyAppShellSiteConfig, cmsAppId: string, amplifyAppIds: string[]): void {
|
|
281
|
+
const authoringRole = new iam.Role(this, "PapyrusAuthoringRole", {
|
|
282
|
+
roleName: `${config.siteId}-papyrus-authoring`,
|
|
283
|
+
description: `Papyrus CLI authoring role for ${config.siteId}: AppSync (SigV4), media and preview storage, rebuild jobs. Assumable by principals in this account that are allowed sts:AssumeRole on it.`,
|
|
284
|
+
assumedBy: new iam.AccountRootPrincipal(),
|
|
285
|
+
});
|
|
286
|
+
for (const statement of this.authoringStatements(config, cmsAppId, amplifyAppIds)) {
|
|
287
|
+
authoringRole.addToPolicy(statement);
|
|
152
288
|
}
|
|
289
|
+
new CfnOutput(this, "PapyrusAuthoringRoleArn", {
|
|
290
|
+
value: authoringRole.roleArn,
|
|
291
|
+
description: `Role the Papyrus CLI assumes for ${config.siteId} (profile role_arn)`,
|
|
292
|
+
});
|
|
153
293
|
}
|
|
154
294
|
|
|
155
295
|
private addGithubCiRole(
|
|
156
296
|
config: AmplifyAppShellSiteConfig,
|
|
157
297
|
github: NonNullable<AmplifyAppShellSiteConfig["github"]>,
|
|
298
|
+
cmsAppId: string,
|
|
158
299
|
amplifyAppIds: string[],
|
|
159
300
|
): void {
|
|
160
301
|
const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(
|
|
@@ -183,26 +324,9 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
183
324
|
}),
|
|
184
325
|
});
|
|
185
326
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
actions: ["amplify:StartJob", "amplify:ListJobs", "amplify:GetJob", "amplify:ListBranches"],
|
|
190
|
-
resources: amplifyAppIds.flatMap((appId) => [
|
|
191
|
-
this.formatArn({ service: "amplify", resource: "apps", resourceName: appId, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
192
|
-
this.formatArn({ service: "amplify", resource: "apps", resourceName: `${appId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
193
|
-
]),
|
|
194
|
-
}),
|
|
195
|
-
);
|
|
196
|
-
ciRole.addToPolicy(
|
|
197
|
-
new iam.PolicyStatement({
|
|
198
|
-
sid: "ReadSiteSecretsForJwtMinting",
|
|
199
|
-
actions: ["ssm:GetParameter", "ssm:GetParameters"],
|
|
200
|
-
resources: [
|
|
201
|
-
this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/${this.cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
202
|
-
this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/shared/${this.cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
203
|
-
],
|
|
204
|
-
}),
|
|
205
|
-
);
|
|
327
|
+
for (const statement of this.authoringStatements(config, cmsAppId, amplifyAppIds)) {
|
|
328
|
+
ciRole.addToPolicy(statement);
|
|
329
|
+
}
|
|
206
330
|
ciRole.addToPolicy(
|
|
207
331
|
new iam.PolicyStatement({
|
|
208
332
|
sid: "WhoAmI",
|
|
@@ -250,6 +374,11 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
250
374
|
platform: "WEB",
|
|
251
375
|
iamServiceRole: readerServiceRole.roleArn,
|
|
252
376
|
buildSpec: readerBuildSpec(config),
|
|
377
|
+
customRules: [
|
|
378
|
+
...(reader.redirects ?? []),
|
|
379
|
+
{ source: "/<*>", target: "/404.html", status: "404-200" },
|
|
380
|
+
],
|
|
381
|
+
customHeaders: readerCustomHeaders(reader.customHeaders),
|
|
253
382
|
});
|
|
254
383
|
|
|
255
384
|
const readerBranch = new amplify.CfnBranch(this, "ReaderBranch", {
|
|
@@ -268,7 +397,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
268
397
|
const readerDomain = new amplify.CfnDomain(this, "ReaderDomain", {
|
|
269
398
|
appId: readerApp.attrAppId,
|
|
270
399
|
domainName: reader.domainName,
|
|
271
|
-
subDomainSettings: [{ branchName: readerBranchName, prefix
|
|
400
|
+
subDomainSettings: (reader.domainPrefixes ?? [""]).map((prefix) => ({ branchName: readerBranchName, prefix })),
|
|
272
401
|
});
|
|
273
402
|
readerDomain.addResourceDependency(readerBranch);
|
|
274
403
|
}
|