@anthusai/papyrus 1.0.0-next.2 → 1.0.0-next.21
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/resource.js +17 -4
- package/amplify/auth/resource.ts +21 -4
- package/amplify/data/resource.js +52 -70
- package/amplify/data/resource.ts +57 -70
- package/amplify/data/schema.js +52 -58
- package/amplify/data/schema.ts +57 -58
- 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 +149 -0
- package/amplify/functions/content-actions/requirements.txt +4 -0
- package/amplify/functions/content-actions/resource.js +40 -0
- package/amplify/functions/content-actions/resource.ts +54 -0
- package/amplify/functions/shared/python-bundle.js +17 -2
- package/amplify/functions/shared/python-bundle.ts +23 -2
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/site-backend-config.ts +12 -1
- package/amplify/site-backend.js +51 -27
- package/amplify/site-backend.ts +66 -34
- package/amplify/storage/resource.js +3 -0
- package/amplify/storage/resource.ts +3 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +4 -2
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/api/revalidate/route.ts +8 -7
- package/app/articles/[slug]/page.tsx +6 -4
- package/app/blog-rhythm.css +278 -0
- package/app/globals.css +22 -4
- package/app/layout.tsx +24 -12
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/page.tsx +12 -4
- package/app/robots.ts +11 -0
- package/bin/papyrus-app.mjs +12 -2
- package/bin/papyrus-infra.mjs +74 -18
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +247 -3
- package/components/blog-page-background.tsx +23 -6
- package/components/markus-ir-preview.tsx +300 -0
- package/components/news-desk-page.tsx +1 -4
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +66 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +3 -1
- package/components/pictogram-figure.tsx +9 -2
- package/components/presentation-header.tsx +135 -0
- package/components/presentation-rhythm-hrule.tsx +17 -0
- package/components/settings-page.tsx +2 -1
- package/components/staging-banner.tsx +11 -0
- package/components/topic-steering-workspace.tsx +1 -59
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +28 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +285 -80
- package/infra/amplify-app-shell.ts +326 -113
- package/infra/build-specs.js +94 -0
- package/infra/build-specs.ts +141 -0
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +3 -0
- package/infra/site-config.js +273 -0
- package/infra/site-config.ts +336 -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 +14 -1
- package/lib/category-repository.ts +1 -93
- package/lib/content-actions-client.ts +98 -0
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-repository.ts +6 -0
- package/lib/content-source-context.ts +26 -0
- package/lib/content-types.ts +4 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/empty-theme.css +1 -0
- package/lib/graphql-content-repository.ts +239 -43
- package/lib/layout-scenarios.ts +8 -8
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +3 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +12 -3
- package/lib/reader-base-path.ts +24 -0
- package/lib/reader-static-params.ts +8 -0
- package/lib/renderer.ts +7 -0
- package/lib/revalidate-secret.ts +59 -0
- package/lib/root-route-redirect.ts +16 -0
- package/lib/site-brand-tenant.ts +3 -22
- package/lib/site-brand.ts +95 -42
- package/lib/site-env.ts +52 -0
- package/lib/staging-gate.ts +37 -0
- package/lib/staging-gated-path.ts +22 -0
- package/lib/staging-preview-object.ts +98 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +77 -0
- package/middleware.ts +56 -42
- package/package.json +40 -1
- package/renderers/pretext/article-page.tsx +40 -9
- package/renderers/pretext/presentation-shared.tsx +113 -0
- package/renderers/pretext/presentation-shell.tsx +18 -112
- 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 +37 -2
- package/scripts/videoml/build-browser-bundle.mjs +22 -0
- package/scripts/videoml/build-preview-bundle.mjs +50 -0
- package/scripts/videoml/bundle-common.mjs +129 -0
- package/scripts/videoml/preview/preview-entry.tsx +381 -0
- package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
- package/scripts/videoml/preview/preview-timing.ts +42 -0
- package/scripts/videoml/preview.html +77 -0
- package/scripts/videoml/standard-entry.tsx +46 -0
- package/src/sync.mjs +30 -6
- package/src/with-papyrus.mjs +15 -2
- 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
- package/publications/anth_us/brand.ts +0 -68
- package/publications/anth_us/theme.css +0 -66
- package/publications/pilobol_us/brand.ts +0 -37
- package/publications/pilobol_us/theme.css +0 -140
- package/publications/threat_intelligence/blog-defense/graph.ts +0 -636
- package/publications/threat_intelligence/blog-defense/layout.ts +0 -934
- package/publications/threat_intelligence/blog-defense/page-background.tsx +0 -590
- package/publications/threat_intelligence/brand.ts +0 -40
- package/publications/threat_intelligence/pictograms/art.tsx +0 -608
- package/publications/threat_intelligence/pictograms/figure.tsx +0 -82
- package/publications/threat_intelligence/pictograms/registry.ts +0 -30
- package/publications/threat_intelligence/pictograms/system.tsx +0 -305
- package/publications/threat_intelligence/seed/seed-edition-content.json +0 -629
- package/publications/threat_intelligence/theme.css +0 -1440
- /package/{publications/papyrus/theme.css → app/papyrus-theme.css} +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
export function useRhythmOverlay(): boolean {
|
|
6
|
+
const [showRhythmOverlay, setShowRhythmOverlay] = useState(false);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
10
|
+
if (isEditableEventTarget(event.target)) return;
|
|
11
|
+
if ((event.key === "=" || event.code === "Equal") && event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
12
|
+
event.preventDefault();
|
|
13
|
+
setShowRhythmOverlay((current) => !current);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
18
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
return showRhythmOverlay;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isEditableEventTarget(target: EventTarget | null): boolean {
|
|
25
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
26
|
+
const tagName = target.tagName.toLowerCase();
|
|
27
|
+
if (tagName === "input" || tagName === "textarea" || tagName === "select") return true;
|
|
28
|
+
return target.isContentEditable || target.closest("[contenteditable='true']") !== null;
|
|
29
|
+
}
|
|
@@ -1,109 +1,314 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ArnFormat, CfnOutput, Duration, Fn, Stack } from "aws-cdk-lib";
|
|
2
2
|
import * as iam from "aws-cdk-lib/aws-iam";
|
|
3
3
|
import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
4
|
+
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider.js";
|
|
5
|
+
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs.js";
|
|
6
|
+
import { isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
|
|
7
|
+
function environmentVariables(variables) {
|
|
8
|
+
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
9
|
+
}
|
|
4
10
|
/**
|
|
5
|
-
* Provisions the Amplify *app shell*
|
|
11
|
+
* Provisions the Amplify *app shell* of a Papyrus publication from its
|
|
12
|
+
* site.json:
|
|
6
13
|
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
14
|
+
* - CMS app (WEB_COMPUTE) with branches `main` (production, owns the backend)
|
|
15
|
+
* and `staging` (frontend only, reads the production backend)
|
|
16
|
+
* - for `markus-static` sites, a reader app (WEB) with branch `main`
|
|
17
|
+
* - a domain association per branch/app when a domain is configured (without
|
|
18
|
+
* one, no AWS::Amplify::Domain is created and the default amplifyapp.com
|
|
19
|
+
* URL is used), IAM service and compute roles
|
|
20
|
+
* - `cms.staging: false` omits the staging branch and its domain
|
|
11
21
|
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
22
|
+
* Google sign-in is always on: the branches never set PAPYRUS_DISABLE_GOOGLE_OAUTH,
|
|
23
|
+
and `cms.cognitoDomainPrefix` becomes the hosted-UI domain
|
|
24
|
+
(PAPYRUS_COGNITO_DOMAIN_PREFIX) only when `cms.applyCognitoDomainPrefix` is true
|
|
25
|
+
(PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX); otherwise Amplify's generated domain is kept. The secrets GOOGLE_CLIENT_ID and
|
|
26
|
+
GOOGLE_CLIENT_SECRET are Amplify backend secrets set in the console, never here.
|
|
27
|
+
The stack also creates the `<siteId>-papyrus-authoring` role. The CLI signs
|
|
28
|
+
* AppSync requests (SigV4) with credentials for that role (SSO, OIDC or any
|
|
29
|
+
* principal in the account that may assume it); no token is stored anywhere.
|
|
16
30
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
31
|
+
* No repository and no access token are configured: the apps are manual-deploy
|
|
32
|
+
* until Amplify's GitHub App is connected once in the console
|
|
33
|
+
* (docs/site-hosting.md). The Papyrus backend itself is created inside the CMS
|
|
34
|
+
* app by `ampx pipeline-deploy` from the production build.
|
|
19
35
|
*/
|
|
20
36
|
export class AmplifyAppShellStack extends Stack {
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
cmsAppId;
|
|
38
|
+
readerAppId;
|
|
23
39
|
constructor(scope, id, config, props) {
|
|
24
40
|
super(scope, id, props);
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
41
|
+
const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
|
|
42
|
+
const stagingEnabled = isStagingEnabled(config);
|
|
43
|
+
const stagingDomainName = resolveStagingDomainName(config);
|
|
44
|
+
const cmsHostName = resolveCmsHostName(config);
|
|
45
|
+
const cmsServiceRole = new iam.Role(this, "AmplifyServiceRole", {
|
|
29
46
|
assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
|
|
30
|
-
description: `Backend deploy service role for Amplify app ${
|
|
47
|
+
description: `Backend deploy and staging build service role for Amplify app ${cmsAppName} (AdministratorAccess: documented risk)`,
|
|
31
48
|
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess")],
|
|
32
49
|
});
|
|
33
|
-
// WEB_COMPUTE (Next.js SSR) requires a compute role for the rendering server.
|
|
34
50
|
const computeRole = new iam.Role(this, "AmplifyComputeRole", {
|
|
35
51
|
assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
|
|
36
|
-
description: `SSR compute role for Amplify app ${
|
|
52
|
+
description: `SSR compute role for Amplify app ${cmsAppName}`,
|
|
37
53
|
managedPolicies: [
|
|
38
54
|
iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"),
|
|
39
55
|
iam.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchLogsFullAccess"),
|
|
40
56
|
],
|
|
41
57
|
});
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
const revalidateSecretParameterName = config.frontend === "pretext" ? resolveRevalidateSecretParameterName(config) : undefined;
|
|
59
|
+
if (revalidateSecretParameterName) {
|
|
60
|
+
computeRole.addToPolicy(new iam.PolicyStatement({
|
|
61
|
+
sid: "ReadRevalidateSecret",
|
|
62
|
+
actions: ["ssm:GetParameter"],
|
|
63
|
+
resources: [
|
|
64
|
+
this.formatArn({ service: "ssm", resource: "parameter", resourceName: revalidateSecretParameterName.slice(1), arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
65
|
+
],
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
const cmsApp = new amplify.CfnApp(this, "App", {
|
|
69
|
+
name: cmsAppName,
|
|
70
|
+
description: `Papyrus newsroom CMS for ${config.siteId} (WEB_COMPUTE)`,
|
|
71
|
+
platform: "WEB_COMPUTE",
|
|
72
|
+
iamServiceRole: cmsServiceRole.roleArn,
|
|
49
73
|
computeRoleArn: computeRole.roleArn,
|
|
50
|
-
buildSpec:
|
|
51
|
-
|
|
74
|
+
buildSpec: cmsProductionBuildSpec(config),
|
|
75
|
+
jobConfig: config.cms.buildComputeType ? { buildComputeType: config.cms.buildComputeType } : undefined,
|
|
76
|
+
customRules: [
|
|
77
|
+
...(config.cms.redirects ?? []).map((redirect) => ({
|
|
78
|
+
source: `https://${redirect.source}`,
|
|
79
|
+
target: `https://${resolveCmsHostName(config)}`,
|
|
80
|
+
status: String(redirect.status),
|
|
81
|
+
})),
|
|
82
|
+
{ source: "/<*>", target: "/index.html", status: "404-200" },
|
|
83
|
+
],
|
|
52
84
|
});
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
const stagingMode = config.frontend === "markus-static"
|
|
86
|
+
? { PAPYRUS_STAGING_PREVIEW: "static" }
|
|
87
|
+
: { PAPYRUS_CONTENT_SOURCE: "drafts" };
|
|
88
|
+
const productionOrigin = cmsHostName
|
|
89
|
+
? `https://${cmsHostName}/`
|
|
90
|
+
: Fn.join("", ["https://main.", cmsApp.attrDefaultDomain, "/"]);
|
|
91
|
+
const stagingOrigin = stagingDomainName
|
|
92
|
+
? `https://${stagingDomainName}/`
|
|
93
|
+
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain, "/"]);
|
|
94
|
+
const sharedRootDomain = config.cms.domainPrefix !== undefined;
|
|
95
|
+
const defaultOriginsToAllow = [
|
|
96
|
+
...(config.cms.domainName ? [] : [productionOrigin]),
|
|
97
|
+
...(stagingEnabled && !stagingDomainName ? [stagingOrigin] : []),
|
|
98
|
+
];
|
|
99
|
+
const oauthRedirectUrls = defaultOriginsToAllow.length === 0
|
|
100
|
+
? config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS
|
|
101
|
+
: Fn.join(",", [config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS, ...defaultOriginsToAllow]);
|
|
102
|
+
const productionBranch = new amplify.CfnBranch(this, "Branch", {
|
|
103
|
+
appId: cmsApp.attrAppId,
|
|
104
|
+
branchName: "main",
|
|
58
105
|
stage: "PRODUCTION",
|
|
59
|
-
enableAutoBuild:
|
|
106
|
+
enableAutoBuild: true,
|
|
60
107
|
enablePerformanceMode: false,
|
|
61
|
-
environmentVariables:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
108
|
+
environmentVariables: environmentVariables({
|
|
109
|
+
...config.cms.environment,
|
|
110
|
+
PAPYRUS_COGNITO_DOMAIN_PREFIX: config.cms.cognitoDomainPrefix,
|
|
111
|
+
...(config.cms.applyCognitoDomainPrefix ? { PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX: "true" } : {}),
|
|
112
|
+
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
113
|
+
PAPYRUS_SITE_BRAND: config.brand,
|
|
114
|
+
SITE_ENV: "production",
|
|
115
|
+
PAPYRUS_CONTENT_SOURCE: "published",
|
|
116
|
+
...(revalidateSecretParameterName ? { PAPYRUS_REVALIDATE_SECRET_PARAMETER: revalidateSecretParameterName } : {}),
|
|
117
|
+
}),
|
|
65
118
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
119
|
+
let stagingBranch;
|
|
120
|
+
if (stagingEnabled) {
|
|
121
|
+
stagingBranch = new amplify.CfnBranch(this, "StagingBranch", {
|
|
122
|
+
appId: cmsApp.attrAppId,
|
|
123
|
+
branchName: "staging",
|
|
124
|
+
stage: "BETA",
|
|
125
|
+
enableAutoBuild: true,
|
|
126
|
+
enablePerformanceMode: false,
|
|
127
|
+
buildSpec: cmsStagingBuildSpec(config),
|
|
128
|
+
environmentVariables: environmentVariables({
|
|
129
|
+
...config.cms.environment,
|
|
130
|
+
PAPYRUS_COGNITO_DOMAIN_PREFIX: config.cms.cognitoDomainPrefix,
|
|
131
|
+
...(config.cms.applyCognitoDomainPrefix ? { PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX: "true" } : {}),
|
|
132
|
+
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
133
|
+
PAPYRUS_SITE_BRAND: config.brand,
|
|
134
|
+
SITE_ENV: "staging",
|
|
135
|
+
...stagingMode,
|
|
136
|
+
NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
|
|
137
|
+
? `https://${stagingDomainName}`
|
|
138
|
+
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]),
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
if (stagingDomainName && !sharedRootDomain) {
|
|
142
|
+
const stagingDomain = new amplify.CfnDomain(this, "StagingDomain", {
|
|
143
|
+
appId: cmsApp.attrAppId,
|
|
144
|
+
domainName: stagingDomainName,
|
|
145
|
+
subDomainSettings: [{ branchName: "staging", prefix: "" }],
|
|
146
|
+
});
|
|
147
|
+
stagingDomain.addResourceDependency(stagingBranch);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (config.cms.domainName) {
|
|
151
|
+
const subDomainSettings = sharedRootDomain
|
|
152
|
+
? [
|
|
153
|
+
{ branchName: "main", prefix: config.cms.domainPrefix },
|
|
154
|
+
...(config.cms.redirects ?? []).map((redirect) => ({
|
|
155
|
+
branchName: "main",
|
|
156
|
+
prefix: resolveRedirectSubDomainPrefix(config.cms.domainName, redirect.source),
|
|
157
|
+
})),
|
|
158
|
+
...(stagingBranch ? [{ branchName: "staging", prefix: config.cms.stagingDomainPrefix }] : []),
|
|
159
|
+
]
|
|
160
|
+
: [{ branchName: "main", prefix: "" }];
|
|
161
|
+
const productionDomain = new amplify.CfnDomain(this, "Domain", {
|
|
162
|
+
appId: cmsApp.attrAppId,
|
|
163
|
+
domainName: config.cms.domainName,
|
|
164
|
+
subDomainSettings,
|
|
165
|
+
});
|
|
166
|
+
productionDomain.addResourceDependency(productionBranch);
|
|
167
|
+
if (sharedRootDomain && stagingBranch) {
|
|
168
|
+
productionDomain.addResourceDependency(stagingBranch);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
this.cmsAppId = cmsApp.attrAppId;
|
|
172
|
+
new CfnOutput(this, "CmsAppId", { value: cmsApp.attrAppId, description: `Amplify app id for ${cmsAppName}` });
|
|
173
|
+
new CfnOutput(this, "CmsOrigin", { value: productionOrigin });
|
|
174
|
+
if (stagingEnabled) {
|
|
175
|
+
new CfnOutput(this, "StagingOrigin", { value: stagingOrigin });
|
|
176
|
+
}
|
|
177
|
+
if (config.reader) {
|
|
178
|
+
this.readerAppId = this.addReaderApp(config, config.reader);
|
|
179
|
+
}
|
|
180
|
+
const amplifyAppIds = [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])];
|
|
181
|
+
this.addAuthoringRole(config, cmsApp.attrAppId, amplifyAppIds);
|
|
182
|
+
if (config.github) {
|
|
183
|
+
this.addGithubCiRole(config, config.github, amplifyAppIds);
|
|
184
|
+
}
|
|
74
185
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
* Papyrus backend + Next.js frontend on every build. Inlined because Amplify
|
|
78
|
-
* needs a buildSpec on the app for the first deploy before the repo is cloned.
|
|
79
|
-
*/
|
|
80
|
-
buildSpec() {
|
|
186
|
+
authoringStatements(config, cmsAppId, amplifyAppIds) {
|
|
187
|
+
const mediaBucketArn = `arn:${this.partition}:s3:::amplify-${cmsAppId}-*`;
|
|
81
188
|
return [
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
189
|
+
new iam.PolicyStatement({
|
|
190
|
+
sid: "SignAppSyncAuthoringRequests",
|
|
191
|
+
actions: ["appsync:GraphQL"],
|
|
192
|
+
resources: ["Query", "Mutation"].map((typeName) => this.formatArn({ service: "appsync", resource: "apis", resourceName: `*/types/${typeName}/fields/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME })),
|
|
193
|
+
}),
|
|
194
|
+
new iam.PolicyStatement({
|
|
195
|
+
sid: "ReadWriteMediaAndPreview",
|
|
196
|
+
actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
|
|
197
|
+
resources: [`${mediaBucketArn}/media/*`, `${mediaBucketArn}/${resolveStoragePreviewPrefix(config)}*`],
|
|
198
|
+
}),
|
|
199
|
+
new iam.PolicyStatement({
|
|
200
|
+
sid: "ListMediaAndPreview",
|
|
201
|
+
actions: ["s3:ListBucket"],
|
|
202
|
+
resources: [mediaBucketArn],
|
|
203
|
+
conditions: { StringLike: { "s3:prefix": ["media/*", `${resolveStoragePreviewPrefix(config)}*`] } },
|
|
204
|
+
}),
|
|
205
|
+
new iam.PolicyStatement({
|
|
206
|
+
sid: "StartAndInspectAmplifyJobs",
|
|
207
|
+
actions: ["amplify:StartJob", "amplify:ListJobs", "amplify:GetJob", "amplify:ListBranches"],
|
|
208
|
+
resources: amplifyAppIds.flatMap((appId) => [
|
|
209
|
+
this.formatArn({ service: "amplify", resource: "apps", resourceName: appId, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
210
|
+
this.formatArn({ service: "amplify", resource: "apps", resourceName: `${appId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
211
|
+
]),
|
|
212
|
+
}),
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
addAuthoringRole(config, cmsAppId, amplifyAppIds) {
|
|
216
|
+
const authoringRole = new iam.Role(this, "PapyrusAuthoringRole", {
|
|
217
|
+
roleName: `${config.siteId}-papyrus-authoring`,
|
|
218
|
+
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.`,
|
|
219
|
+
assumedBy: new iam.AccountRootPrincipal(),
|
|
220
|
+
});
|
|
221
|
+
for (const statement of this.authoringStatements(config, cmsAppId, amplifyAppIds)) {
|
|
222
|
+
authoringRole.addToPolicy(statement);
|
|
223
|
+
}
|
|
224
|
+
new CfnOutput(this, "PapyrusAuthoringRoleArn", {
|
|
225
|
+
value: authoringRole.roleArn,
|
|
226
|
+
description: `Role the Papyrus CLI assumes for ${config.siteId} (profile role_arn)`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
addGithubCiRole(config, github, amplifyAppIds) {
|
|
230
|
+
const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(this, "GithubOidcProvider", this.formatArn({
|
|
231
|
+
service: "iam",
|
|
232
|
+
region: "",
|
|
233
|
+
resource: "oidc-provider",
|
|
234
|
+
resourceName: GITHUB_OIDC_PROVIDER_HOST,
|
|
235
|
+
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
|
|
236
|
+
}));
|
|
237
|
+
const ciRole = new iam.Role(this, "GithubCiRole", {
|
|
238
|
+
roleName: `${config.siteId}-github-ci`,
|
|
239
|
+
description: `GitHub Actions role for ${github.owner}/${github.repo} (OIDC, least privilege, no admin policy)`,
|
|
240
|
+
maxSessionDuration: Duration.hours(1),
|
|
241
|
+
assumedBy: new iam.OpenIdConnectPrincipal(provider, {
|
|
242
|
+
StringEquals: { [`${GITHUB_OIDC_PROVIDER_HOST}:aud`]: "sts.amazonaws.com" },
|
|
243
|
+
StringLike: {
|
|
244
|
+
[`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: github.branches.map((branch) => `repo:${github.owner}/${github.repo}:ref:refs/heads/${branch}`),
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
248
|
+
for (const statement of this.authoringStatements(config, this.cmsAppId, amplifyAppIds)) {
|
|
249
|
+
ciRole.addToPolicy(statement);
|
|
250
|
+
}
|
|
251
|
+
ciRole.addToPolicy(new iam.PolicyStatement({
|
|
252
|
+
sid: "WhoAmI",
|
|
253
|
+
actions: ["sts:GetCallerIdentity"],
|
|
254
|
+
resources: ["*"],
|
|
255
|
+
}));
|
|
256
|
+
if (github.ciCanDeployInfra) {
|
|
257
|
+
ciRole.addToPolicy(new iam.PolicyStatement({
|
|
258
|
+
sid: "DeployThisSitesAppShellStack",
|
|
259
|
+
actions: ["cloudformation:*"],
|
|
260
|
+
resources: [
|
|
261
|
+
this.formatArn({ service: "cloudformation", resource: "stack", resourceName: `${resolveStackName(config)}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
262
|
+
],
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
new CfnOutput(this, "GithubCiRoleArn", { value: ciRole.roleArn, description: `Role for GitHub Actions in ${github.owner}/${github.repo}` });
|
|
266
|
+
}
|
|
267
|
+
addReaderApp(config, reader) {
|
|
268
|
+
const readerAppName = reader.appName ?? `${config.siteId}-reader`;
|
|
269
|
+
const readerBranchName = reader.branchName ?? "main";
|
|
270
|
+
const readerServiceRole = new iam.Role(this, "ReaderServiceRole", {
|
|
271
|
+
assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
|
|
272
|
+
description: `Static reader build role for Amplify app ${readerAppName} (reads published media only; content is read as a Cognito guest)`,
|
|
273
|
+
});
|
|
274
|
+
readerServiceRole.addToPolicy(new iam.PolicyStatement({
|
|
275
|
+
sid: "ReadMediaForExport",
|
|
276
|
+
actions: ["s3:GetObject"],
|
|
277
|
+
resources: ["arn:aws:s3:::amplify-*/media/*"],
|
|
278
|
+
}));
|
|
279
|
+
const readerApp = new amplify.CfnApp(this, "ReaderApp", {
|
|
280
|
+
name: readerAppName,
|
|
281
|
+
description: `Papyrus static reader for ${config.siteId} (WEB)`,
|
|
282
|
+
platform: "WEB",
|
|
283
|
+
iamServiceRole: readerServiceRole.roleArn,
|
|
284
|
+
buildSpec: readerBuildSpec(config),
|
|
285
|
+
customRules: [{ source: "/<*>", target: "/404.html", status: "404-200" }],
|
|
286
|
+
});
|
|
287
|
+
const readerBranch = new amplify.CfnBranch(this, "ReaderBranch", {
|
|
288
|
+
appId: readerApp.attrAppId,
|
|
289
|
+
branchName: readerBranchName,
|
|
290
|
+
stage: "PRODUCTION",
|
|
291
|
+
enableAutoBuild: true,
|
|
292
|
+
environmentVariables: environmentVariables({
|
|
293
|
+
...(reader.environment ?? {}),
|
|
294
|
+
PAPYRUS_SITE_BRAND: config.brand,
|
|
295
|
+
SITE_ENV: "production",
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
298
|
+
if (reader.domainName) {
|
|
299
|
+
const readerDomain = new amplify.CfnDomain(this, "ReaderDomain", {
|
|
300
|
+
appId: readerApp.attrAppId,
|
|
301
|
+
domainName: reader.domainName,
|
|
302
|
+
subDomainSettings: [{ branchName: readerBranchName, prefix: "" }],
|
|
303
|
+
});
|
|
304
|
+
readerDomain.addResourceDependency(readerBranch);
|
|
305
|
+
}
|
|
306
|
+
new CfnOutput(this, "ReaderAppId", { value: readerApp.attrAppId, description: `Amplify app id for ${readerAppName}` });
|
|
307
|
+
new CfnOutput(this, "ReaderOrigin", {
|
|
308
|
+
value: reader.domainName
|
|
309
|
+
? `https://${reader.domainName}/`
|
|
310
|
+
: Fn.join("", [`https://${readerBranchName}.`, readerApp.attrDefaultDomain, "/"]),
|
|
311
|
+
});
|
|
312
|
+
return readerApp.attrAppId;
|
|
108
313
|
}
|
|
109
314
|
}
|