@anthusai/papyrus 1.0.0-next.4 → 1.0.0-next.41
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 +28 -70
- package/amplify/data/resource.ts +29 -70
- package/amplify/data/schema.js +33 -58
- package/amplify/data/schema.ts +34 -58
- 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 +45 -3
- 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/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/resolver-waves.js +26 -0
- package/amplify/resolver-waves.ts +34 -0
- package/amplify/site-backend-config.ts +36 -1
- package/amplify/site-backend-names.js +103 -0
- package/amplify/site-backend-names.ts +166 -0
- package/amplify/site-backend.js +93 -170
- package/amplify/site-backend.ts +126 -196
- package/amplify/storage/resource.js +3 -0
- package/amplify/storage/resource.ts +3 -0
- package/amplify/storage-backups/construct.js +31 -0
- package/amplify/storage-backups/construct.ts +47 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/api/media/[...path]/route.ts +5 -16
- 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 +21 -0
- package/app/layout.tsx +16 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +6 -5
- package/app/newsroom/[section]/page.tsx +4 -3
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/newsroom/layout.tsx +4 -9
- package/app/page.tsx +18 -5
- package/app/robots.ts +2 -5
- package/bin/papyrus-app.mjs +12 -2
- package/bin/papyrus-infra.mjs +29 -3
- package/components/amplify-client-provider.tsx +4 -22
- 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-taxonomy-client.ts +51 -0
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +67 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-client-shell.tsx +12 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +3 -1
- package/components/newsroom-references-view.tsx +83 -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/reference-cyclotron-status.tsx +109 -0
- package/components/reference-relevance-review.tsx +75 -0
- package/components/settings-page.tsx +2 -1
- package/components/topic-steering-workspace.tsx +54 -64
- 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 +228 -50
- package/infra/amplify-app-shell.ts +256 -53
- package/infra/build-specs.js +15 -5
- package/infra/build-specs.ts +18 -7
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +1 -0
- package/infra/site-config.js +236 -11
- package/infra/site-config.ts +276 -15
- package/lib/amplify-outputs-path.ts +5 -1
- 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 +42 -93
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-source-context.ts +12 -2
- package/lib/content-types.ts +4 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +61 -9
- package/lib/media-route-response.ts +45 -0
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-base-path.ts +12 -3
- package/lib/newsroom-category-drilldown.ts +3 -2
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-forum-routes.ts +6 -4
- package/lib/newsroom-index-filters.ts +14 -10
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +3 -0
- package/lib/oauth-redirect-priority.ts +40 -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 +13 -0
- 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-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/site-brand.ts +51 -0
- package/lib/staging-gated-path.ts +23 -0
- package/lib/staging-preview-object.ts +114 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +77 -0
- package/middleware.ts +23 -18
- package/package.json +45 -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 +45 -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 +19 -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
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
|
-
import { ArnFormat, CfnOutput, Stack, StackProps } from "aws-cdk-lib";
|
|
2
|
+
import { ArnFormat, CfnOutput, Duration, Fn, Stack, StackProps } from "aws-cdk-lib";
|
|
3
3
|
import * as iam from "aws-cdk-lib/aws-iam";
|
|
4
4
|
import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
5
|
+
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider";
|
|
5
6
|
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs";
|
|
6
|
-
import { AmplifyAppShellSiteConfig, resolveStagingDomainName } from "./site-config";
|
|
7
|
+
import { AmplifyAppShellSiteConfig, ReaderCustomHeaderRule, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
|
|
7
8
|
|
|
8
9
|
function environmentVariables(variables: Record<string, string>): amplify.CfnBranch.EnvironmentVariableProperty[] {
|
|
9
10
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
10
11
|
}
|
|
11
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
|
+
|
|
12
25
|
/**
|
|
13
26
|
* Provisions the Amplify *app shell* of a Papyrus publication from its
|
|
14
27
|
* site.json:
|
|
@@ -16,7 +29,19 @@ function environmentVariables(variables: Record<string, string>): amplify.CfnBra
|
|
|
16
29
|
* - CMS app (WEB_COMPUTE) with branches `main` (production, owns the backend)
|
|
17
30
|
* and `staging` (frontend only, reads the production backend)
|
|
18
31
|
* - for `markus-static` sites, a reader app (WEB) with branch `main`
|
|
19
|
-
* -
|
|
32
|
+
* - a domain association per branch/app when a domain is configured (without
|
|
33
|
+
* one, no AWS::Amplify::Domain is created and the default amplifyapp.com
|
|
34
|
+
* URL is used), IAM service and compute roles
|
|
35
|
+
* - `cms.staging: false` omits the staging branch and its domain
|
|
36
|
+
*
|
|
37
|
+
* Google sign-in is always on: the branches never set PAPYRUS_DISABLE_GOOGLE_OAUTH,
|
|
38
|
+
and `cms.cognitoDomainPrefix` becomes the hosted-UI domain
|
|
39
|
+
(PAPYRUS_COGNITO_DOMAIN_PREFIX) only when `cms.applyCognitoDomainPrefix` is true
|
|
40
|
+
(PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX); otherwise Amplify's generated domain is kept. The secrets GOOGLE_CLIENT_ID and
|
|
41
|
+
GOOGLE_CLIENT_SECRET are Amplify backend secrets set in the console, never here.
|
|
42
|
+
The stack also creates the `<siteId>-papyrus-authoring` role. The CLI signs
|
|
43
|
+
* AppSync requests (SigV4) with credentials for that role (SSO, OIDC or any
|
|
44
|
+
* principal in the account that may assume it); no token is stored anywhere.
|
|
20
45
|
*
|
|
21
46
|
* No repository and no access token are configured: the apps are manual-deploy
|
|
22
47
|
* until Amplify's GitHub App is connected once in the console
|
|
@@ -31,7 +56,9 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
31
56
|
super(scope, id, props);
|
|
32
57
|
|
|
33
58
|
const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
|
|
59
|
+
const stagingEnabled = isStagingEnabled(config);
|
|
34
60
|
const stagingDomainName = resolveStagingDomainName(config);
|
|
61
|
+
const cmsHostName = resolveCmsHostName(config);
|
|
35
62
|
|
|
36
63
|
const cmsServiceRole = new iam.Role(this, "AmplifyServiceRole", {
|
|
37
64
|
assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
|
|
@@ -48,6 +75,19 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
48
75
|
],
|
|
49
76
|
});
|
|
50
77
|
|
|
78
|
+
const revalidateSecretParameterName = config.frontend === "pretext" ? resolveRevalidateSecretParameterName(config) : undefined;
|
|
79
|
+
if (revalidateSecretParameterName) {
|
|
80
|
+
computeRole.addToPolicy(
|
|
81
|
+
new iam.PolicyStatement({
|
|
82
|
+
sid: "ReadRevalidateSecret",
|
|
83
|
+
actions: ["ssm:GetParameter"],
|
|
84
|
+
resources: [
|
|
85
|
+
this.formatArn({ service: "ssm", resource: "parameter", resourceName: revalidateSecretParameterName.slice(1), arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
86
|
+
],
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
51
91
|
const cmsApp = new amplify.CfnApp(this, "App", {
|
|
52
92
|
name: cmsAppName,
|
|
53
93
|
description: `Papyrus newsroom CMS for ${config.siteId} (WEB_COMPUTE)`,
|
|
@@ -56,13 +96,35 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
56
96
|
computeRoleArn: computeRole.roleArn,
|
|
57
97
|
buildSpec: cmsProductionBuildSpec(config),
|
|
58
98
|
jobConfig: config.cms.buildComputeType ? { buildComputeType: config.cms.buildComputeType } : undefined,
|
|
59
|
-
customRules: [
|
|
99
|
+
customRules: [
|
|
100
|
+
...(config.cms.redirects ?? []).map((redirect) => ({
|
|
101
|
+
source: `https://${redirect.source}`,
|
|
102
|
+
target: `https://${resolveCmsHostName(config)}`,
|
|
103
|
+
status: String(redirect.status),
|
|
104
|
+
})),
|
|
105
|
+
{ source: "/<*>", target: "/index.html", status: "404-200" },
|
|
106
|
+
],
|
|
60
107
|
});
|
|
61
108
|
|
|
62
109
|
const stagingMode: Record<string, string> = config.frontend === "markus-static"
|
|
63
110
|
? { PAPYRUS_STAGING_PREVIEW: "static" }
|
|
64
111
|
: { PAPYRUS_CONTENT_SOURCE: "drafts" };
|
|
65
112
|
|
|
113
|
+
const productionOrigin = cmsHostName
|
|
114
|
+
? `https://${cmsHostName}/`
|
|
115
|
+
: Fn.join("", ["https://main.", cmsApp.attrDefaultDomain, "/"]);
|
|
116
|
+
const stagingOrigin = stagingDomainName
|
|
117
|
+
? `https://${stagingDomainName}/`
|
|
118
|
+
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain, "/"]);
|
|
119
|
+
const sharedRootDomain = config.cms.domainPrefix !== undefined;
|
|
120
|
+
const defaultOriginsToAllow = [
|
|
121
|
+
...(config.cms.domainName ? [] : [productionOrigin]),
|
|
122
|
+
...(stagingEnabled && !stagingDomainName ? [stagingOrigin] : []),
|
|
123
|
+
];
|
|
124
|
+
const oauthRedirectUrls = defaultOriginsToAllow.length === 0
|
|
125
|
+
? config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS
|
|
126
|
+
: Fn.join(",", [config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS, ...defaultOriginsToAllow]);
|
|
127
|
+
|
|
66
128
|
const productionBranch = new amplify.CfnBranch(this, "Branch", {
|
|
67
129
|
appId: cmsApp.attrAppId,
|
|
68
130
|
branchName: "main",
|
|
@@ -71,74 +133,204 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
71
133
|
enablePerformanceMode: false,
|
|
72
134
|
environmentVariables: environmentVariables({
|
|
73
135
|
...config.cms.environment,
|
|
136
|
+
PAPYRUS_COGNITO_DOMAIN_PREFIX: config.cms.cognitoDomainPrefix,
|
|
137
|
+
...(config.cms.applyCognitoDomainPrefix ? { PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX: "true" } : {}),
|
|
138
|
+
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
74
139
|
PAPYRUS_SITE_BRAND: config.brand,
|
|
75
140
|
SITE_ENV: "production",
|
|
76
141
|
PAPYRUS_CONTENT_SOURCE: "published",
|
|
142
|
+
...(revalidateSecretParameterName ? { PAPYRUS_REVALIDATE_SECRET_PARAMETER: revalidateSecretParameterName } : {}),
|
|
77
143
|
}),
|
|
78
144
|
});
|
|
79
145
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
146
|
+
let stagingBranch: amplify.CfnBranch | undefined;
|
|
147
|
+
if (stagingEnabled) {
|
|
148
|
+
stagingBranch = new amplify.CfnBranch(this, "StagingBranch", {
|
|
149
|
+
appId: cmsApp.attrAppId,
|
|
150
|
+
branchName: "staging",
|
|
151
|
+
stage: "BETA",
|
|
152
|
+
enableAutoBuild: true,
|
|
153
|
+
enablePerformanceMode: false,
|
|
154
|
+
buildSpec: cmsStagingBuildSpec(config),
|
|
155
|
+
environmentVariables: environmentVariables({
|
|
156
|
+
...config.cms.environment,
|
|
157
|
+
PAPYRUS_COGNITO_DOMAIN_PREFIX: config.cms.cognitoDomainPrefix,
|
|
158
|
+
...(config.cms.applyCognitoDomainPrefix ? { PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX: "true" } : {}),
|
|
159
|
+
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
160
|
+
PAPYRUS_SITE_BRAND: config.brand,
|
|
161
|
+
SITE_ENV: "staging",
|
|
162
|
+
...stagingMode,
|
|
163
|
+
NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
|
|
164
|
+
? `https://${stagingDomainName}`
|
|
165
|
+
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]),
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
95
168
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
169
|
+
if (stagingDomainName && !sharedRootDomain) {
|
|
170
|
+
const stagingDomain = new amplify.CfnDomain(this, "StagingDomain", {
|
|
171
|
+
appId: cmsApp.attrAppId,
|
|
172
|
+
domainName: stagingDomainName,
|
|
173
|
+
subDomainSettings: [{ branchName: "staging", prefix: "" }],
|
|
174
|
+
});
|
|
175
|
+
stagingDomain.addResourceDependency(stagingBranch);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
102
178
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
179
|
+
if (config.cms.domainName) {
|
|
180
|
+
const subDomainSettings = sharedRootDomain
|
|
181
|
+
? [
|
|
182
|
+
{ branchName: "main", prefix: config.cms.domainPrefix as string },
|
|
183
|
+
...(config.cms.redirects ?? []).map((redirect) => ({
|
|
184
|
+
branchName: "main",
|
|
185
|
+
prefix: resolveRedirectSubDomainPrefix(config.cms.domainName as string, redirect.source),
|
|
186
|
+
})),
|
|
187
|
+
...(stagingBranch ? [{ branchName: "staging", prefix: config.cms.stagingDomainPrefix as string }] : []),
|
|
188
|
+
]
|
|
189
|
+
: [{ branchName: "main", prefix: "" }];
|
|
190
|
+
const productionDomain = new amplify.CfnDomain(this, "Domain", {
|
|
191
|
+
appId: cmsApp.attrAppId,
|
|
192
|
+
domainName: config.cms.domainName,
|
|
193
|
+
subDomainSettings,
|
|
194
|
+
});
|
|
195
|
+
productionDomain.addResourceDependency(productionBranch);
|
|
196
|
+
if (sharedRootDomain && stagingBranch) {
|
|
197
|
+
productionDomain.addResourceDependency(stagingBranch);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
109
200
|
|
|
110
201
|
this.cmsAppId = cmsApp.attrAppId;
|
|
111
202
|
new CfnOutput(this, "CmsAppId", { value: cmsApp.attrAppId, description: `Amplify app id for ${cmsAppName}` });
|
|
112
|
-
new CfnOutput(this, "CmsOrigin", { value:
|
|
113
|
-
|
|
203
|
+
new CfnOutput(this, "CmsOrigin", { value: productionOrigin });
|
|
204
|
+
if (stagingEnabled) {
|
|
205
|
+
new CfnOutput(this, "StagingOrigin", { value: stagingOrigin });
|
|
206
|
+
}
|
|
114
207
|
|
|
115
208
|
if (config.reader) {
|
|
116
|
-
this.readerAppId = this.addReaderApp(config, config.reader
|
|
209
|
+
this.readerAppId = this.addReaderApp(config, config.reader);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const amplifyAppIds = [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])];
|
|
213
|
+
this.addAuthoringRole(config, cmsApp.attrAppId, amplifyAppIds);
|
|
214
|
+
|
|
215
|
+
if (config.github) {
|
|
216
|
+
this.addGithubCiRole(config, config.github, amplifyAppIds);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private authoringStatements(config: AmplifyAppShellSiteConfig, cmsAppId: string, amplifyAppIds: string[]): iam.PolicyStatement[] {
|
|
221
|
+
const mediaBucketArn = `arn:${this.partition}:s3:::amplify-${cmsAppId}-*`;
|
|
222
|
+
return [
|
|
223
|
+
new iam.PolicyStatement({
|
|
224
|
+
sid: "SignAppSyncAuthoringRequests",
|
|
225
|
+
actions: ["appsync:GraphQL"],
|
|
226
|
+
resources: ["Query", "Mutation"].map((typeName) =>
|
|
227
|
+
this.formatArn({ service: "appsync", resource: "apis", resourceName: `*/types/${typeName}/fields/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
228
|
+
),
|
|
229
|
+
}),
|
|
230
|
+
new iam.PolicyStatement({
|
|
231
|
+
sid: "ReadWriteMediaAndPreview",
|
|
232
|
+
actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
|
|
233
|
+
resources: [`${mediaBucketArn}/media/*`, `${mediaBucketArn}/${resolveStoragePreviewPrefix(config)}*`],
|
|
234
|
+
}),
|
|
235
|
+
new iam.PolicyStatement({
|
|
236
|
+
sid: "ListMediaAndPreview",
|
|
237
|
+
actions: ["s3:ListBucket"],
|
|
238
|
+
resources: [mediaBucketArn],
|
|
239
|
+
conditions: { StringLike: { "s3:prefix": ["media/*", `${resolveStoragePreviewPrefix(config)}*`] } },
|
|
240
|
+
}),
|
|
241
|
+
new iam.PolicyStatement({
|
|
242
|
+
sid: "StartAndInspectAmplifyJobs",
|
|
243
|
+
actions: ["amplify:StartJob", "amplify:ListJobs", "amplify:GetJob", "amplify:ListBranches"],
|
|
244
|
+
resources: amplifyAppIds.flatMap((appId) => [
|
|
245
|
+
this.formatArn({ service: "amplify", resource: "apps", resourceName: appId, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
246
|
+
this.formatArn({ service: "amplify", resource: "apps", resourceName: `${appId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
247
|
+
]),
|
|
248
|
+
}),
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private addAuthoringRole(config: AmplifyAppShellSiteConfig, cmsAppId: string, amplifyAppIds: string[]): void {
|
|
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);
|
|
117
260
|
}
|
|
261
|
+
new CfnOutput(this, "PapyrusAuthoringRoleArn", {
|
|
262
|
+
value: authoringRole.roleArn,
|
|
263
|
+
description: `Role the Papyrus CLI assumes for ${config.siteId} (profile role_arn)`,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private addGithubCiRole(
|
|
268
|
+
config: AmplifyAppShellSiteConfig,
|
|
269
|
+
github: NonNullable<AmplifyAppShellSiteConfig["github"]>,
|
|
270
|
+
amplifyAppIds: string[],
|
|
271
|
+
): void {
|
|
272
|
+
const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(
|
|
273
|
+
this,
|
|
274
|
+
"GithubOidcProvider",
|
|
275
|
+
this.formatArn({
|
|
276
|
+
service: "iam",
|
|
277
|
+
region: "",
|
|
278
|
+
resource: "oidc-provider",
|
|
279
|
+
resourceName: GITHUB_OIDC_PROVIDER_HOST,
|
|
280
|
+
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const ciRole = new iam.Role(this, "GithubCiRole", {
|
|
285
|
+
roleName: `${config.siteId}-github-ci`,
|
|
286
|
+
description: `GitHub Actions role for ${github.owner}/${github.repo} (OIDC, least privilege, no admin policy)`,
|
|
287
|
+
maxSessionDuration: Duration.hours(1),
|
|
288
|
+
assumedBy: new iam.OpenIdConnectPrincipal(provider, {
|
|
289
|
+
StringEquals: { [`${GITHUB_OIDC_PROVIDER_HOST}:aud`]: "sts.amazonaws.com" },
|
|
290
|
+
StringLike: {
|
|
291
|
+
[`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: github.branches.map(
|
|
292
|
+
(branch) => `repo:${github.owner}/${github.repo}:ref:refs/heads/${branch}`,
|
|
293
|
+
),
|
|
294
|
+
},
|
|
295
|
+
}),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
for (const statement of this.authoringStatements(config, this.cmsAppId, amplifyAppIds)) {
|
|
299
|
+
ciRole.addToPolicy(statement);
|
|
300
|
+
}
|
|
301
|
+
ciRole.addToPolicy(
|
|
302
|
+
new iam.PolicyStatement({
|
|
303
|
+
sid: "WhoAmI",
|
|
304
|
+
actions: ["sts:GetCallerIdentity"],
|
|
305
|
+
resources: ["*"],
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
if (github.ciCanDeployInfra) {
|
|
309
|
+
ciRole.addToPolicy(
|
|
310
|
+
new iam.PolicyStatement({
|
|
311
|
+
sid: "DeployThisSitesAppShellStack",
|
|
312
|
+
actions: ["cloudformation:*"],
|
|
313
|
+
resources: [
|
|
314
|
+
this.formatArn({ service: "cloudformation", resource: "stack", resourceName: `${resolveStackName(config)}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
315
|
+
],
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
new CfnOutput(this, "GithubCiRoleArn", { value: ciRole.roleArn, description: `Role for GitHub Actions in ${github.owner}/${github.repo}` });
|
|
118
321
|
}
|
|
119
322
|
|
|
120
323
|
private addReaderApp(
|
|
121
324
|
config: AmplifyAppShellSiteConfig,
|
|
122
325
|
reader: NonNullable<AmplifyAppShellSiteConfig["reader"]>,
|
|
123
|
-
cmsAppId: string,
|
|
124
326
|
): string {
|
|
125
327
|
const readerAppName = reader.appName ?? `${config.siteId}-reader`;
|
|
126
328
|
const readerBranchName = reader.branchName ?? "main";
|
|
127
329
|
|
|
128
330
|
const readerServiceRole = new iam.Role(this, "ReaderServiceRole", {
|
|
129
331
|
assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
|
|
130
|
-
description: `Static reader build role for Amplify app ${readerAppName} (reads
|
|
332
|
+
description: `Static reader build role for Amplify app ${readerAppName} (reads published media only; content is read as a Cognito guest)`,
|
|
131
333
|
});
|
|
132
|
-
readerServiceRole.addToPolicy(
|
|
133
|
-
new iam.PolicyStatement({
|
|
134
|
-
sid: "ReadSiteSecretsForJwtMinting",
|
|
135
|
-
actions: ["ssm:GetParameter", "ssm:GetParameters"],
|
|
136
|
-
resources: [
|
|
137
|
-
this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/${cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
138
|
-
this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/shared/${cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
139
|
-
],
|
|
140
|
-
}),
|
|
141
|
-
);
|
|
142
334
|
readerServiceRole.addToPolicy(
|
|
143
335
|
new iam.PolicyStatement({
|
|
144
336
|
sid: "ReadMediaForExport",
|
|
@@ -153,6 +345,11 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
153
345
|
platform: "WEB",
|
|
154
346
|
iamServiceRole: readerServiceRole.roleArn,
|
|
155
347
|
buildSpec: readerBuildSpec(config),
|
|
348
|
+
customRules: [
|
|
349
|
+
...(reader.redirects ?? []),
|
|
350
|
+
{ source: "/<*>", target: "/404.html", status: "404-200" },
|
|
351
|
+
],
|
|
352
|
+
customHeaders: readerCustomHeaders(reader.customHeaders),
|
|
156
353
|
});
|
|
157
354
|
|
|
158
355
|
const readerBranch = new amplify.CfnBranch(this, "ReaderBranch", {
|
|
@@ -167,15 +364,21 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
167
364
|
}),
|
|
168
365
|
});
|
|
169
366
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
367
|
+
if (reader.domainName) {
|
|
368
|
+
const readerDomain = new amplify.CfnDomain(this, "ReaderDomain", {
|
|
369
|
+
appId: readerApp.attrAppId,
|
|
370
|
+
domainName: reader.domainName,
|
|
371
|
+
subDomainSettings: (reader.domainPrefixes ?? [""]).map((prefix) => ({ branchName: readerBranchName, prefix })),
|
|
372
|
+
});
|
|
373
|
+
readerDomain.addResourceDependency(readerBranch);
|
|
374
|
+
}
|
|
176
375
|
|
|
177
376
|
new CfnOutput(this, "ReaderAppId", { value: readerApp.attrAppId, description: `Amplify app id for ${readerAppName}` });
|
|
178
|
-
new CfnOutput(this, "ReaderOrigin", {
|
|
377
|
+
new CfnOutput(this, "ReaderOrigin", {
|
|
378
|
+
value: reader.domainName
|
|
379
|
+
? `https://${reader.domainName}/`
|
|
380
|
+
: Fn.join("", [`https://${readerBranchName}.`, readerApp.attrDefaultDomain, "/"]),
|
|
381
|
+
});
|
|
179
382
|
return readerApp.attrAppId;
|
|
180
383
|
}
|
|
181
384
|
}
|
package/infra/build-specs.js
CHANGED
|
@@ -14,6 +14,8 @@ function commandLines(commands, indent) {
|
|
|
14
14
|
}
|
|
15
15
|
const NPM_CLEAN_INSTALL = "npm ci --cache .npm --prefer-offline";
|
|
16
16
|
const FRONTEND_ENVIRONMENT_CAPTURE = "env | grep -e '^PAPYRUS_' -e '^SITE_ENV=' -e '^NEXT_PUBLIC_' >> .env.production";
|
|
17
|
+
const CHECKOUT_AS_PAPYRUS_ROOT = "PAPYRUS_ROOT=\"$PWD\"";
|
|
18
|
+
const RUNTIME_OUTPUTS_COPY = "cp amplify_outputs.json .next/amplify_outputs.json";
|
|
17
19
|
function pythonProvisioningCommands(config) {
|
|
18
20
|
return [
|
|
19
21
|
"export UV_CACHE_DIR=\"$PWD/.uv-cache\"",
|
|
@@ -50,7 +52,11 @@ export function cmsProductionBuildSpec(config) {
|
|
|
50
52
|
"if [ -z \"${PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI:-}\" ]; then export PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD=true; fi",
|
|
51
53
|
"npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID",
|
|
52
54
|
], " "),
|
|
53
|
-
...frontendSection([], [
|
|
55
|
+
...frontendSection([], [
|
|
56
|
+
FRONTEND_ENVIRONMENT_CAPTURE,
|
|
57
|
+
"npx papyrus-app sync && npm run build",
|
|
58
|
+
...(config.frontend === "pretext" ? [RUNTIME_OUTPUTS_COPY] : []),
|
|
59
|
+
], ".next", [".next/cache/**/*", ".npm/**/*"]),
|
|
54
60
|
].join("\n") + "\n";
|
|
55
61
|
}
|
|
56
62
|
export function cmsStagingBuildSpec(config) {
|
|
@@ -58,12 +64,16 @@ export function cmsStagingBuildSpec(config) {
|
|
|
58
64
|
const cachePaths = [".next/cache/**/*", ".npm/**/*"];
|
|
59
65
|
if (config.frontend === "markus-static") {
|
|
60
66
|
const readerConfig = reader(config);
|
|
61
|
-
preBuild.push(...pythonProvisioningCommands(config),
|
|
67
|
+
preBuild.push(...pythonProvisioningCommands(config), `${CHECKOUT_AS_PAPYRUS_ROOT} papyrus ops content export-published --drafts --out content-export --clean`, readerConfig.buildCommand, `${CHECKOUT_AS_PAPYRUS_ROOT} papyrus ops content upload-preview --dir ${readerConfig.baseDirectory} --prefix ${resolveStoragePreviewPrefix(config)}`);
|
|
62
68
|
cachePaths.push(".uv-cache/**/*");
|
|
63
69
|
}
|
|
64
70
|
return [
|
|
65
71
|
"version: 1",
|
|
66
|
-
...frontendSection(preBuild, [
|
|
72
|
+
...frontendSection(preBuild, [
|
|
73
|
+
FRONTEND_ENVIRONMENT_CAPTURE,
|
|
74
|
+
"npx papyrus-app sync && npm run build",
|
|
75
|
+
RUNTIME_OUTPUTS_COPY,
|
|
76
|
+
], ".next", cachePaths),
|
|
67
77
|
].join("\n") + "\n";
|
|
68
78
|
}
|
|
69
79
|
export function readerBuildSpec(config) {
|
|
@@ -72,8 +82,8 @@ export function readerBuildSpec(config) {
|
|
|
72
82
|
"version: 1",
|
|
73
83
|
...frontendSection([
|
|
74
84
|
...pythonProvisioningCommands(config),
|
|
75
|
-
|
|
76
|
-
"papyrus content export-published --out content-export --clean",
|
|
85
|
+
...(readerConfig.preBuildCommands ?? []),
|
|
86
|
+
...(readerConfig.skipContentExport ? [] : ["papyrus ops content export-published --auth guest --out content-export --clean"]),
|
|
77
87
|
], [readerConfig.buildCommand], readerConfig.baseDirectory, [".uv-cache/**/*"]),
|
|
78
88
|
].join("\n") + "\n";
|
|
79
89
|
}
|
package/infra/build-specs.ts
CHANGED
|
@@ -22,6 +22,10 @@ const NPM_CLEAN_INSTALL = "npm ci --cache .npm --prefer-offline";
|
|
|
22
22
|
const FRONTEND_ENVIRONMENT_CAPTURE =
|
|
23
23
|
"env | grep -e '^PAPYRUS_' -e '^SITE_ENV=' -e '^NEXT_PUBLIC_' >> .env.production";
|
|
24
24
|
|
|
25
|
+
const CHECKOUT_AS_PAPYRUS_ROOT = "PAPYRUS_ROOT=\"$PWD\"";
|
|
26
|
+
|
|
27
|
+
const RUNTIME_OUTPUTS_COPY = "cp amplify_outputs.json .next/amplify_outputs.json";
|
|
28
|
+
|
|
25
29
|
function pythonProvisioningCommands(config: AmplifyAppShellSiteConfig): string[] {
|
|
26
30
|
return [
|
|
27
31
|
"export UV_CACHE_DIR=\"$PWD/.uv-cache\"",
|
|
@@ -76,7 +80,11 @@ export function cmsProductionBuildSpec(config: AmplifyAppShellSiteConfig): strin
|
|
|
76
80
|
),
|
|
77
81
|
...frontendSection(
|
|
78
82
|
[],
|
|
79
|
-
[
|
|
83
|
+
[
|
|
84
|
+
FRONTEND_ENVIRONMENT_CAPTURE,
|
|
85
|
+
"npx papyrus-app sync && npm run build",
|
|
86
|
+
...(config.frontend === "pretext" ? [RUNTIME_OUTPUTS_COPY] : []),
|
|
87
|
+
],
|
|
80
88
|
".next",
|
|
81
89
|
[".next/cache/**/*", ".npm/**/*"],
|
|
82
90
|
),
|
|
@@ -90,10 +98,9 @@ export function cmsStagingBuildSpec(config: AmplifyAppShellSiteConfig): string {
|
|
|
90
98
|
const readerConfig = reader(config);
|
|
91
99
|
preBuild.push(
|
|
92
100
|
...pythonProvisioningCommands(config),
|
|
93
|
-
|
|
94
|
-
"papyrus content export-published --drafts --out content-export --clean",
|
|
101
|
+
`${CHECKOUT_AS_PAPYRUS_ROOT} papyrus ops content export-published --drafts --out content-export --clean`,
|
|
95
102
|
readerConfig.buildCommand,
|
|
96
|
-
|
|
103
|
+
`${CHECKOUT_AS_PAPYRUS_ROOT} papyrus ops content upload-preview --dir ${readerConfig.baseDirectory} --prefix ${resolveStoragePreviewPrefix(config)}`,
|
|
97
104
|
);
|
|
98
105
|
cachePaths.push(".uv-cache/**/*");
|
|
99
106
|
}
|
|
@@ -101,7 +108,11 @@ export function cmsStagingBuildSpec(config: AmplifyAppShellSiteConfig): string {
|
|
|
101
108
|
"version: 1",
|
|
102
109
|
...frontendSection(
|
|
103
110
|
preBuild,
|
|
104
|
-
[
|
|
111
|
+
[
|
|
112
|
+
FRONTEND_ENVIRONMENT_CAPTURE,
|
|
113
|
+
"npx papyrus-app sync && npm run build",
|
|
114
|
+
RUNTIME_OUTPUTS_COPY,
|
|
115
|
+
],
|
|
105
116
|
".next",
|
|
106
117
|
cachePaths,
|
|
107
118
|
),
|
|
@@ -115,8 +126,8 @@ export function readerBuildSpec(config: AmplifyAppShellSiteConfig): string {
|
|
|
115
126
|
...frontendSection(
|
|
116
127
|
[
|
|
117
128
|
...pythonProvisioningCommands(config),
|
|
118
|
-
|
|
119
|
-
"papyrus content export-published --out content-export --clean",
|
|
129
|
+
...(readerConfig.preBuildCommands ?? []),
|
|
130
|
+
...(readerConfig.skipContentExport ? [] : ["papyrus ops content export-published --auth guest --out content-export --clean"]),
|
|
120
131
|
],
|
|
121
132
|
[readerConfig.buildCommand],
|
|
122
133
|
readerConfig.baseDirectory,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { CfnOutput, Stack } from "aws-cdk-lib";
|
|
2
|
+
import * as iam from "aws-cdk-lib/aws-iam";
|
|
3
|
+
export const GITHUB_OIDC_PROVIDER_HOST = "token.actions.githubusercontent.com";
|
|
4
|
+
/**
|
|
5
|
+
* Account-global singleton: AWS allows one IAM OIDC provider per issuer URL per
|
|
6
|
+
* account, so this never belongs in a per-site stack. Deploy it once per
|
|
7
|
+
* account, and only after `aws iam list-open-id-connect-providers` shows no
|
|
8
|
+
* provider for token.actions.githubusercontent.com. When one already exists,
|
|
9
|
+
* do not deploy this stack: per-site CI roles only reference the provider by
|
|
10
|
+
* its ARN (arn:aws:iam::<account>:oidc-provider/token.actions.githubusercontent.com)
|
|
11
|
+
* and work against an existing provider unchanged.
|
|
12
|
+
*/
|
|
13
|
+
export class GithubOidcProviderStack extends Stack {
|
|
14
|
+
providerArn;
|
|
15
|
+
constructor(scope, id, props) {
|
|
16
|
+
super(scope, id, props);
|
|
17
|
+
const provider = new iam.OpenIdConnectProvider(this, "GithubOidc", {
|
|
18
|
+
url: `https://${GITHUB_OIDC_PROVIDER_HOST}`,
|
|
19
|
+
clientIds: ["sts.amazonaws.com"],
|
|
20
|
+
});
|
|
21
|
+
this.providerArn = provider.openIdConnectProviderArn;
|
|
22
|
+
new CfnOutput(this, "GithubOidcProviderArn", { value: provider.openIdConnectProviderArn });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
import { CfnOutput, Stack, StackProps } from "aws-cdk-lib";
|
|
3
|
+
import * as iam from "aws-cdk-lib/aws-iam";
|
|
4
|
+
|
|
5
|
+
export const GITHUB_OIDC_PROVIDER_HOST = "token.actions.githubusercontent.com";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Account-global singleton: AWS allows one IAM OIDC provider per issuer URL per
|
|
9
|
+
* account, so this never belongs in a per-site stack. Deploy it once per
|
|
10
|
+
* account, and only after `aws iam list-open-id-connect-providers` shows no
|
|
11
|
+
* provider for token.actions.githubusercontent.com. When one already exists,
|
|
12
|
+
* do not deploy this stack: per-site CI roles only reference the provider by
|
|
13
|
+
* its ARN (arn:aws:iam::<account>:oidc-provider/token.actions.githubusercontent.com)
|
|
14
|
+
* and work against an existing provider unchanged.
|
|
15
|
+
*/
|
|
16
|
+
export class GithubOidcProviderStack extends Stack {
|
|
17
|
+
public readonly providerArn: string;
|
|
18
|
+
|
|
19
|
+
constructor(scope: Construct, id: string, props?: StackProps) {
|
|
20
|
+
super(scope, id, props);
|
|
21
|
+
|
|
22
|
+
const provider = new iam.OpenIdConnectProvider(this, "GithubOidc", {
|
|
23
|
+
url: `https://${GITHUB_OIDC_PROVIDER_HOST}`,
|
|
24
|
+
clientIds: ["sts.amazonaws.com"],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
this.providerArn = provider.openIdConnectProviderArn;
|
|
28
|
+
new CfnOutput(this, "GithubOidcProviderArn", { value: provider.openIdConnectProviderArn });
|
|
29
|
+
}
|
|
30
|
+
}
|
package/infra/index.js
CHANGED
|
@@ -10,5 +10,6 @@ try {
|
|
|
10
10
|
);
|
|
11
11
|
}
|
|
12
12
|
export const { AmplifyAppShellStack } = await import("./amplify-app-shell.js");
|
|
13
|
+
export const { GithubOidcProviderStack } = await import("./github-oidc-provider.js");
|
|
13
14
|
export { parseSiteConfig } from "./site-config.js";
|
|
14
15
|
export { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec, buildSpecFor } from "./build-specs.js";
|