@anthusai/papyrus 1.0.0-next.7 → 1.0.0-next.9
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/functions/content-actions/handler.py +1 -1
- package/bin/papyrus-infra.mjs +3 -3
- package/infra/amplify-app-shell.js +78 -51
- package/infra/amplify-app-shell.ts +78 -53
- package/infra/build-specs.js +1 -2
- package/infra/build-specs.ts +1 -2
- package/infra/site-config.js +39 -8
- package/infra/site-config.ts +47 -12
- package/lib/content-source-context.ts +12 -2
- package/lib/graphql-content-repository.ts +13 -4
- package/package.json +1 -1
- package/routes.manifest.json +1 -1
|
@@ -130,7 +130,7 @@ def request_staging_build_action() -> dict:
|
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
def handler(event, context):
|
|
133
|
-
field = event["
|
|
133
|
+
field = event["fieldName"]
|
|
134
134
|
arguments = parse_input(event["arguments"]["input"])
|
|
135
135
|
actor = actor_from_event(event)
|
|
136
136
|
try:
|
package/bin/papyrus-infra.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import fs from "node:fs";
|
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
13
|
import { buildSpecFor } from "../infra/build-specs.js";
|
|
14
|
-
import { parseSiteConfig } from "../infra/site-config.js";
|
|
14
|
+
import { parseSiteConfig, resolveStackName } from "../infra/site-config.js";
|
|
15
15
|
|
|
16
16
|
const USAGE = [
|
|
17
17
|
"Usage: papyrus-infra synth --site infra/site.json [--out cdk.out] [--account A] [--region R]",
|
|
@@ -89,7 +89,7 @@ const { App } = createRequire(path.join(process.cwd(), "noop.js"))("aws-cdk-lib"
|
|
|
89
89
|
const app = new App({ outdir: path.resolve(opt("out", "cdk.out")) });
|
|
90
90
|
new AmplifyAppShellStack(app, `AmplifyAppShell-${config.siteId}`, config, {
|
|
91
91
|
env: { account: opt("account", "335163751677"), region: opt("region", "us-east-1") },
|
|
92
|
-
stackName:
|
|
92
|
+
stackName: resolveStackName(config),
|
|
93
93
|
});
|
|
94
94
|
app.synth();
|
|
95
|
-
console.log(`synthesized
|
|
95
|
+
console.log(`synthesized ${resolveStackName(config)} -> ${path.resolve(opt("out", "cdk.out"))}`);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { ArnFormat, CfnOutput, Duration, Stack } from "aws-cdk-lib";
|
|
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
4
|
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider.js";
|
|
5
5
|
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs.js";
|
|
6
|
-
import { resolveStagingDomainName } from "./site-config.js";
|
|
6
|
+
import { isStagingEnabled, resolveStackName, resolveStagingDomainName } from "./site-config.js";
|
|
7
7
|
function environmentVariables(variables) {
|
|
8
8
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
9
9
|
}
|
|
@@ -14,7 +14,10 @@ function environmentVariables(variables) {
|
|
|
14
14
|
* - CMS app (WEB_COMPUTE) with branches `main` (production, owns the backend)
|
|
15
15
|
* and `staging` (frontend only, reads the production backend)
|
|
16
16
|
* - for `markus-static` sites, a reader app (WEB) with branch `main`
|
|
17
|
-
* -
|
|
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
|
|
18
21
|
*
|
|
19
22
|
* No repository and no access token are configured: the apps are manual-deploy
|
|
20
23
|
* until Amplify's GitHub App is connected once in the console
|
|
@@ -27,6 +30,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
27
30
|
constructor(scope, id, config, props) {
|
|
28
31
|
super(scope, id, props);
|
|
29
32
|
const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
|
|
33
|
+
const stagingEnabled = isStagingEnabled(config);
|
|
30
34
|
const stagingDomainName = resolveStagingDomainName(config);
|
|
31
35
|
const cmsServiceRole = new iam.Role(this, "AmplifyServiceRole", {
|
|
32
36
|
assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
|
|
@@ -54,6 +58,19 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
54
58
|
const stagingMode = config.frontend === "markus-static"
|
|
55
59
|
? { PAPYRUS_STAGING_PREVIEW: "static" }
|
|
56
60
|
: { PAPYRUS_CONTENT_SOURCE: "drafts" };
|
|
61
|
+
const productionOrigin = config.cms.domainName
|
|
62
|
+
? `https://${config.cms.domainName}/`
|
|
63
|
+
: Fn.join("", ["https://main.", cmsApp.attrDefaultDomain, "/"]);
|
|
64
|
+
const stagingOrigin = stagingDomainName
|
|
65
|
+
? `https://${stagingDomainName}/`
|
|
66
|
+
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain, "/"]);
|
|
67
|
+
const defaultOriginsToAllow = [
|
|
68
|
+
...(config.cms.domainName ? [] : [productionOrigin]),
|
|
69
|
+
...(stagingEnabled && !stagingDomainName ? [stagingOrigin] : []),
|
|
70
|
+
];
|
|
71
|
+
const oauthRedirectUrls = defaultOriginsToAllow.length === 0
|
|
72
|
+
? config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS
|
|
73
|
+
: Fn.join(",", [config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS, ...defaultOriginsToAllow]);
|
|
57
74
|
const productionBranch = new amplify.CfnBranch(this, "Branch", {
|
|
58
75
|
appId: cmsApp.attrAppId,
|
|
59
76
|
branchName: "main",
|
|
@@ -62,44 +79,56 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
62
79
|
enablePerformanceMode: false,
|
|
63
80
|
environmentVariables: environmentVariables({
|
|
64
81
|
...config.cms.environment,
|
|
82
|
+
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
65
83
|
PAPYRUS_SITE_BRAND: config.brand,
|
|
66
84
|
SITE_ENV: "production",
|
|
67
85
|
PAPYRUS_CONTENT_SOURCE: "published",
|
|
68
86
|
}),
|
|
69
87
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
if (stagingEnabled) {
|
|
89
|
+
const stagingBranch = new amplify.CfnBranch(this, "StagingBranch", {
|
|
90
|
+
appId: cmsApp.attrAppId,
|
|
91
|
+
branchName: "staging",
|
|
92
|
+
stage: "BETA",
|
|
93
|
+
enableAutoBuild: true,
|
|
94
|
+
enablePerformanceMode: false,
|
|
95
|
+
buildSpec: cmsStagingBuildSpec(config),
|
|
96
|
+
environmentVariables: environmentVariables({
|
|
97
|
+
...config.cms.environment,
|
|
98
|
+
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
99
|
+
PAPYRUS_SITE_BRAND: config.brand,
|
|
100
|
+
SITE_ENV: "staging",
|
|
101
|
+
...stagingMode,
|
|
102
|
+
NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
|
|
103
|
+
? `https://${stagingDomainName}`
|
|
104
|
+
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]),
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
if (stagingDomainName) {
|
|
108
|
+
const stagingDomain = new amplify.CfnDomain(this, "StagingDomain", {
|
|
109
|
+
appId: cmsApp.attrAppId,
|
|
110
|
+
domainName: stagingDomainName,
|
|
111
|
+
subDomainSettings: [{ branchName: "staging", prefix: "" }],
|
|
112
|
+
});
|
|
113
|
+
stagingDomain.addResourceDependency(stagingBranch);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (config.cms.domainName) {
|
|
117
|
+
const productionDomain = new amplify.CfnDomain(this, "Domain", {
|
|
118
|
+
appId: cmsApp.attrAppId,
|
|
119
|
+
domainName: config.cms.domainName,
|
|
120
|
+
subDomainSettings: [{ branchName: "main", prefix: "" }],
|
|
121
|
+
});
|
|
122
|
+
productionDomain.addResourceDependency(productionBranch);
|
|
123
|
+
}
|
|
97
124
|
this.cmsAppId = cmsApp.attrAppId;
|
|
98
125
|
new CfnOutput(this, "CmsAppId", { value: cmsApp.attrAppId, description: `Amplify app id for ${cmsAppName}` });
|
|
99
|
-
new CfnOutput(this, "CmsOrigin", { value:
|
|
100
|
-
|
|
126
|
+
new CfnOutput(this, "CmsOrigin", { value: productionOrigin });
|
|
127
|
+
if (stagingEnabled) {
|
|
128
|
+
new CfnOutput(this, "StagingOrigin", { value: stagingOrigin });
|
|
129
|
+
}
|
|
101
130
|
if (config.reader) {
|
|
102
|
-
this.readerAppId = this.addReaderApp(config, config.reader
|
|
131
|
+
this.readerAppId = this.addReaderApp(config, config.reader);
|
|
103
132
|
}
|
|
104
133
|
if (config.github) {
|
|
105
134
|
this.addGithubCiRole(config, config.github, [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])]);
|
|
@@ -150,27 +179,19 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
150
179
|
sid: "DeployThisSitesAppShellStack",
|
|
151
180
|
actions: ["cloudformation:*"],
|
|
152
181
|
resources: [
|
|
153
|
-
this.formatArn({ service: "cloudformation", resource: "stack", resourceName:
|
|
182
|
+
this.formatArn({ service: "cloudformation", resource: "stack", resourceName: `${resolveStackName(config)}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
154
183
|
],
|
|
155
184
|
}));
|
|
156
185
|
}
|
|
157
186
|
new CfnOutput(this, "GithubCiRoleArn", { value: ciRole.roleArn, description: `Role for GitHub Actions in ${github.owner}/${github.repo}` });
|
|
158
187
|
}
|
|
159
|
-
addReaderApp(config, reader
|
|
188
|
+
addReaderApp(config, reader) {
|
|
160
189
|
const readerAppName = reader.appName ?? `${config.siteId}-reader`;
|
|
161
190
|
const readerBranchName = reader.branchName ?? "main";
|
|
162
191
|
const readerServiceRole = new iam.Role(this, "ReaderServiceRole", {
|
|
163
192
|
assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
|
|
164
|
-
description: `Static reader build role for Amplify app ${readerAppName} (reads
|
|
193
|
+
description: `Static reader build role for Amplify app ${readerAppName} (reads published media only; content is read as a Cognito guest)`,
|
|
165
194
|
});
|
|
166
|
-
readerServiceRole.addToPolicy(new iam.PolicyStatement({
|
|
167
|
-
sid: "ReadSiteSecretsForJwtMinting",
|
|
168
|
-
actions: ["ssm:GetParameter", "ssm:GetParameters"],
|
|
169
|
-
resources: [
|
|
170
|
-
this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/${cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
171
|
-
this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/shared/${cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
172
|
-
],
|
|
173
|
-
}));
|
|
174
195
|
readerServiceRole.addToPolicy(new iam.PolicyStatement({
|
|
175
196
|
sid: "ReadMediaForExport",
|
|
176
197
|
actions: ["s3:GetObject"],
|
|
@@ -194,14 +215,20 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
194
215
|
SITE_ENV: "production",
|
|
195
216
|
}),
|
|
196
217
|
});
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
218
|
+
if (reader.domainName) {
|
|
219
|
+
const readerDomain = new amplify.CfnDomain(this, "ReaderDomain", {
|
|
220
|
+
appId: readerApp.attrAppId,
|
|
221
|
+
domainName: reader.domainName,
|
|
222
|
+
subDomainSettings: [{ branchName: readerBranchName, prefix: "" }],
|
|
223
|
+
});
|
|
224
|
+
readerDomain.addResourceDependency(readerBranch);
|
|
225
|
+
}
|
|
203
226
|
new CfnOutput(this, "ReaderAppId", { value: readerApp.attrAppId, description: `Amplify app id for ${readerAppName}` });
|
|
204
|
-
new CfnOutput(this, "ReaderOrigin", {
|
|
227
|
+
new CfnOutput(this, "ReaderOrigin", {
|
|
228
|
+
value: reader.domainName
|
|
229
|
+
? `https://${reader.domainName}/`
|
|
230
|
+
: Fn.join("", [`https://${readerBranchName}.`, readerApp.attrDefaultDomain, "/"]),
|
|
231
|
+
});
|
|
205
232
|
return readerApp.attrAppId;
|
|
206
233
|
}
|
|
207
234
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
|
-
import { ArnFormat, CfnOutput, Duration, 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
5
|
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider";
|
|
6
6
|
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs";
|
|
7
|
-
import { AmplifyAppShellSiteConfig, resolveStagingDomainName } from "./site-config";
|
|
7
|
+
import { AmplifyAppShellSiteConfig, isStagingEnabled, resolveStackName, resolveStagingDomainName } 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 }));
|
|
@@ -17,7 +17,10 @@ function environmentVariables(variables: Record<string, string>): amplify.CfnBra
|
|
|
17
17
|
* - CMS app (WEB_COMPUTE) with branches `main` (production, owns the backend)
|
|
18
18
|
* and `staging` (frontend only, reads the production backend)
|
|
19
19
|
* - for `markus-static` sites, a reader app (WEB) with branch `main`
|
|
20
|
-
* -
|
|
20
|
+
* - a domain association per branch/app when a domain is configured (without
|
|
21
|
+
* one, no AWS::Amplify::Domain is created and the default amplifyapp.com
|
|
22
|
+
* URL is used), IAM service and compute roles
|
|
23
|
+
* - `cms.staging: false` omits the staging branch and its domain
|
|
21
24
|
*
|
|
22
25
|
* No repository and no access token are configured: the apps are manual-deploy
|
|
23
26
|
* until Amplify's GitHub App is connected once in the console
|
|
@@ -32,6 +35,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
32
35
|
super(scope, id, props);
|
|
33
36
|
|
|
34
37
|
const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
|
|
38
|
+
const stagingEnabled = isStagingEnabled(config);
|
|
35
39
|
const stagingDomainName = resolveStagingDomainName(config);
|
|
36
40
|
|
|
37
41
|
const cmsServiceRole = new iam.Role(this, "AmplifyServiceRole", {
|
|
@@ -64,6 +68,20 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
64
68
|
? { PAPYRUS_STAGING_PREVIEW: "static" }
|
|
65
69
|
: { PAPYRUS_CONTENT_SOURCE: "drafts" };
|
|
66
70
|
|
|
71
|
+
const productionOrigin = config.cms.domainName
|
|
72
|
+
? `https://${config.cms.domainName}/`
|
|
73
|
+
: Fn.join("", ["https://main.", cmsApp.attrDefaultDomain, "/"]);
|
|
74
|
+
const stagingOrigin = stagingDomainName
|
|
75
|
+
? `https://${stagingDomainName}/`
|
|
76
|
+
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain, "/"]);
|
|
77
|
+
const defaultOriginsToAllow = [
|
|
78
|
+
...(config.cms.domainName ? [] : [productionOrigin]),
|
|
79
|
+
...(stagingEnabled && !stagingDomainName ? [stagingOrigin] : []),
|
|
80
|
+
];
|
|
81
|
+
const oauthRedirectUrls = defaultOriginsToAllow.length === 0
|
|
82
|
+
? config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS
|
|
83
|
+
: Fn.join(",", [config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS, ...defaultOriginsToAllow]);
|
|
84
|
+
|
|
67
85
|
const productionBranch = new amplify.CfnBranch(this, "Branch", {
|
|
68
86
|
appId: cmsApp.attrAppId,
|
|
69
87
|
branchName: "main",
|
|
@@ -72,49 +90,61 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
72
90
|
enablePerformanceMode: false,
|
|
73
91
|
environmentVariables: environmentVariables({
|
|
74
92
|
...config.cms.environment,
|
|
93
|
+
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
75
94
|
PAPYRUS_SITE_BRAND: config.brand,
|
|
76
95
|
SITE_ENV: "production",
|
|
77
96
|
PAPYRUS_CONTENT_SOURCE: "published",
|
|
78
97
|
}),
|
|
79
98
|
});
|
|
80
99
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
if (stagingEnabled) {
|
|
101
|
+
const stagingBranch = new amplify.CfnBranch(this, "StagingBranch", {
|
|
102
|
+
appId: cmsApp.attrAppId,
|
|
103
|
+
branchName: "staging",
|
|
104
|
+
stage: "BETA",
|
|
105
|
+
enableAutoBuild: true,
|
|
106
|
+
enablePerformanceMode: false,
|
|
107
|
+
buildSpec: cmsStagingBuildSpec(config),
|
|
108
|
+
environmentVariables: environmentVariables({
|
|
109
|
+
...config.cms.environment,
|
|
110
|
+
PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
|
|
111
|
+
PAPYRUS_SITE_BRAND: config.brand,
|
|
112
|
+
SITE_ENV: "staging",
|
|
113
|
+
...stagingMode,
|
|
114
|
+
NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
|
|
115
|
+
? `https://${stagingDomainName}`
|
|
116
|
+
: Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]),
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
96
119
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
if (stagingDomainName) {
|
|
121
|
+
const stagingDomain = new amplify.CfnDomain(this, "StagingDomain", {
|
|
122
|
+
appId: cmsApp.attrAppId,
|
|
123
|
+
domainName: stagingDomainName,
|
|
124
|
+
subDomainSettings: [{ branchName: "staging", prefix: "" }],
|
|
125
|
+
});
|
|
126
|
+
stagingDomain.addResourceDependency(stagingBranch);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
103
129
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
if (config.cms.domainName) {
|
|
131
|
+
const productionDomain = new amplify.CfnDomain(this, "Domain", {
|
|
132
|
+
appId: cmsApp.attrAppId,
|
|
133
|
+
domainName: config.cms.domainName,
|
|
134
|
+
subDomainSettings: [{ branchName: "main", prefix: "" }],
|
|
135
|
+
});
|
|
136
|
+
productionDomain.addResourceDependency(productionBranch);
|
|
137
|
+
}
|
|
110
138
|
|
|
111
139
|
this.cmsAppId = cmsApp.attrAppId;
|
|
112
140
|
new CfnOutput(this, "CmsAppId", { value: cmsApp.attrAppId, description: `Amplify app id for ${cmsAppName}` });
|
|
113
|
-
new CfnOutput(this, "CmsOrigin", { value:
|
|
114
|
-
|
|
141
|
+
new CfnOutput(this, "CmsOrigin", { value: productionOrigin });
|
|
142
|
+
if (stagingEnabled) {
|
|
143
|
+
new CfnOutput(this, "StagingOrigin", { value: stagingOrigin });
|
|
144
|
+
}
|
|
115
145
|
|
|
116
146
|
if (config.reader) {
|
|
117
|
-
this.readerAppId = this.addReaderApp(config, config.reader
|
|
147
|
+
this.readerAppId = this.addReaderApp(config, config.reader);
|
|
118
148
|
}
|
|
119
149
|
|
|
120
150
|
if (config.github) {
|
|
@@ -186,7 +216,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
186
216
|
sid: "DeployThisSitesAppShellStack",
|
|
187
217
|
actions: ["cloudformation:*"],
|
|
188
218
|
resources: [
|
|
189
|
-
this.formatArn({ service: "cloudformation", resource: "stack", resourceName:
|
|
219
|
+
this.formatArn({ service: "cloudformation", resource: "stack", resourceName: `${resolveStackName(config)}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
190
220
|
],
|
|
191
221
|
}),
|
|
192
222
|
);
|
|
@@ -198,25 +228,14 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
198
228
|
private addReaderApp(
|
|
199
229
|
config: AmplifyAppShellSiteConfig,
|
|
200
230
|
reader: NonNullable<AmplifyAppShellSiteConfig["reader"]>,
|
|
201
|
-
cmsAppId: string,
|
|
202
231
|
): string {
|
|
203
232
|
const readerAppName = reader.appName ?? `${config.siteId}-reader`;
|
|
204
233
|
const readerBranchName = reader.branchName ?? "main";
|
|
205
234
|
|
|
206
235
|
const readerServiceRole = new iam.Role(this, "ReaderServiceRole", {
|
|
207
236
|
assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
|
|
208
|
-
description: `Static reader build role for Amplify app ${readerAppName} (reads
|
|
237
|
+
description: `Static reader build role for Amplify app ${readerAppName} (reads published media only; content is read as a Cognito guest)`,
|
|
209
238
|
});
|
|
210
|
-
readerServiceRole.addToPolicy(
|
|
211
|
-
new iam.PolicyStatement({
|
|
212
|
-
sid: "ReadSiteSecretsForJwtMinting",
|
|
213
|
-
actions: ["ssm:GetParameter", "ssm:GetParameters"],
|
|
214
|
-
resources: [
|
|
215
|
-
this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/${cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
216
|
-
this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/shared/${cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
|
|
217
|
-
],
|
|
218
|
-
}),
|
|
219
|
-
);
|
|
220
239
|
readerServiceRole.addToPolicy(
|
|
221
240
|
new iam.PolicyStatement({
|
|
222
241
|
sid: "ReadMediaForExport",
|
|
@@ -245,15 +264,21 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
245
264
|
}),
|
|
246
265
|
});
|
|
247
266
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
267
|
+
if (reader.domainName) {
|
|
268
|
+
const readerDomain = new amplify.CfnDomain(this, "ReaderDomain", {
|
|
269
|
+
appId: readerApp.attrAppId,
|
|
270
|
+
domainName: reader.domainName,
|
|
271
|
+
subDomainSettings: [{ branchName: readerBranchName, prefix: "" }],
|
|
272
|
+
});
|
|
273
|
+
readerDomain.addResourceDependency(readerBranch);
|
|
274
|
+
}
|
|
254
275
|
|
|
255
276
|
new CfnOutput(this, "ReaderAppId", { value: readerApp.attrAppId, description: `Amplify app id for ${readerAppName}` });
|
|
256
|
-
new CfnOutput(this, "ReaderOrigin", {
|
|
277
|
+
new CfnOutput(this, "ReaderOrigin", {
|
|
278
|
+
value: reader.domainName
|
|
279
|
+
? `https://${reader.domainName}/`
|
|
280
|
+
: Fn.join("", [`https://${readerBranchName}.`, readerApp.attrDefaultDomain, "/"]),
|
|
281
|
+
});
|
|
257
282
|
return readerApp.attrAppId;
|
|
258
283
|
}
|
|
259
284
|
}
|
package/infra/build-specs.js
CHANGED
|
@@ -72,8 +72,7 @@ export function readerBuildSpec(config) {
|
|
|
72
72
|
"version: 1",
|
|
73
73
|
...frontendSection([
|
|
74
74
|
...pythonProvisioningCommands(config),
|
|
75
|
-
"papyrus
|
|
76
|
-
"papyrus content export-published --out content-export --clean",
|
|
75
|
+
"papyrus content export-published --auth guest --out content-export --clean",
|
|
77
76
|
], [readerConfig.buildCommand], readerConfig.baseDirectory, [".uv-cache/**/*"]),
|
|
78
77
|
].join("\n") + "\n";
|
|
79
78
|
}
|
package/infra/build-specs.ts
CHANGED
|
@@ -115,8 +115,7 @@ export function readerBuildSpec(config: AmplifyAppShellSiteConfig): string {
|
|
|
115
115
|
...frontendSection(
|
|
116
116
|
[
|
|
117
117
|
...pythonProvisioningCommands(config),
|
|
118
|
-
"papyrus
|
|
119
|
-
"papyrus content export-published --out content-export --clean",
|
|
118
|
+
"papyrus content export-published --auth guest --out content-export --clean",
|
|
120
119
|
],
|
|
121
120
|
[readerConfig.buildCommand],
|
|
122
121
|
readerConfig.baseDirectory,
|
package/infra/site-config.js
CHANGED
|
@@ -4,6 +4,7 @@ const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_
|
|
|
4
4
|
const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
|
|
5
5
|
const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
6
6
|
const APP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 _.-]*$/;
|
|
7
|
+
const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
|
|
7
8
|
const BRANCH_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9/_.-]*$/;
|
|
8
9
|
const PAPYRUS_VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+\.\d+)?$/;
|
|
9
10
|
const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
@@ -46,18 +47,28 @@ function rejectUnknownKeys(record, allowed, field) {
|
|
|
46
47
|
fail(`${field}.${key}`, `is not a known field (known: ${allowed.join(", ")})`);
|
|
47
48
|
}
|
|
48
49
|
}
|
|
50
|
+
export function isStagingEnabled(config) {
|
|
51
|
+
return config.cms.staging !== false;
|
|
52
|
+
}
|
|
49
53
|
export function resolveStagingDomainName(config) {
|
|
54
|
+
if (!isStagingEnabled(config))
|
|
55
|
+
return undefined;
|
|
50
56
|
if (config.cms.stagingDomainName)
|
|
51
57
|
return config.cms.stagingDomainName;
|
|
58
|
+
if (!config.cms.domainName)
|
|
59
|
+
return undefined;
|
|
52
60
|
const [, ...rest] = config.cms.domainName.split(".");
|
|
53
61
|
return ["staging", ...rest].join(".");
|
|
54
62
|
}
|
|
63
|
+
export function resolveStackName(config) {
|
|
64
|
+
return config.stackName ?? `amplify-app-shell-${config.siteId}`;
|
|
65
|
+
}
|
|
55
66
|
export function resolveStoragePreviewPrefix(config) {
|
|
56
67
|
return config.storagePreviewPrefix ?? "preview/";
|
|
57
68
|
}
|
|
58
69
|
export function parseSiteConfig(raw) {
|
|
59
70
|
const record = requireObject(raw, "site");
|
|
60
|
-
rejectUnknownKeys(record, ["siteId", "repository", "brand", "frontend", "hostedZoneId", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"], "site");
|
|
71
|
+
rejectUnknownKeys(record, ["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"], "site");
|
|
61
72
|
const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
|
|
62
73
|
const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
|
|
63
74
|
const brand = requireString(record.brand, "brand");
|
|
@@ -65,22 +76,33 @@ export function parseSiteConfig(raw) {
|
|
|
65
76
|
if (frontend !== "pretext" && frontend !== "markus-static") {
|
|
66
77
|
fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
|
|
67
78
|
}
|
|
68
|
-
const hostedZoneId =
|
|
79
|
+
const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
|
|
80
|
+
const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
|
|
69
81
|
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
70
82
|
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
71
83
|
const cmsRecord = requireObject(record.cms, "cms");
|
|
72
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "buildComputeType", "environment", "stagingDomainName"], "cms");
|
|
84
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "buildComputeType", "environment", "stagingDomainName"], "cms");
|
|
73
85
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
74
86
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
75
87
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
76
88
|
}
|
|
89
|
+
if (cmsRecord.staging !== undefined && typeof cmsRecord.staging !== "boolean") {
|
|
90
|
+
fail("cms.staging", `must be a boolean, got ${JSON.stringify(cmsRecord.staging)}`);
|
|
91
|
+
}
|
|
77
92
|
const cms = {
|
|
78
93
|
appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
|
|
79
|
-
domainName:
|
|
94
|
+
domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
|
|
95
|
+
staging: cmsRecord.staging,
|
|
80
96
|
buildComputeType: buildComputeType,
|
|
81
97
|
environment: requireStringRecord(cmsRecord.environment, "cms.environment"),
|
|
82
98
|
stagingDomainName: optionalString(cmsRecord.stagingDomainName, "cms.stagingDomainName", HOST_NAME_PATTERN, "host name"),
|
|
83
99
|
};
|
|
100
|
+
if (cms.staging === false && cms.stagingDomainName) {
|
|
101
|
+
fail("cms.stagingDomainName", "must not be set when cms.staging is false");
|
|
102
|
+
}
|
|
103
|
+
if (cms.stagingDomainName && !cms.domainName) {
|
|
104
|
+
fail("cms.stagingDomainName", "requires cms.domainName");
|
|
105
|
+
}
|
|
84
106
|
let reader;
|
|
85
107
|
if (record.reader !== undefined) {
|
|
86
108
|
if (frontend !== "markus-static")
|
|
@@ -89,7 +111,7 @@ export function parseSiteConfig(raw) {
|
|
|
89
111
|
rejectUnknownKeys(readerRecord, ["appName", "domainName", "branchName", "buildCommand", "baseDirectory", "environment"], "reader");
|
|
90
112
|
reader = {
|
|
91
113
|
appName: optionalString(readerRecord.appName, "reader.appName", APP_NAME_PATTERN),
|
|
92
|
-
domainName:
|
|
114
|
+
domainName: optionalString(readerRecord.domainName, "reader.domainName", HOST_NAME_PATTERN, "host name"),
|
|
93
115
|
branchName: optionalString(readerRecord.branchName, "reader.branchName", BRANCH_NAME_PATTERN),
|
|
94
116
|
buildCommand: requireString(readerRecord.buildCommand, "reader.buildCommand"),
|
|
95
117
|
baseDirectory: requireString(readerRecord.baseDirectory, "reader.baseDirectory"),
|
|
@@ -108,7 +130,7 @@ export function parseSiteConfig(raw) {
|
|
|
108
130
|
if (repository !== `https://github.com/${owner}/${repo}`) {
|
|
109
131
|
fail("github", `owner/repo must match repository ${repository}`);
|
|
110
132
|
}
|
|
111
|
-
let branches = DEFAULT_CI_BRANCHES;
|
|
133
|
+
let branches = cms.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
|
|
112
134
|
if (githubRecord.branches !== undefined) {
|
|
113
135
|
if (!Array.isArray(githubRecord.branches) || githubRecord.branches.length === 0) {
|
|
114
136
|
fail("github.branches", "must be a non-empty array of branch names");
|
|
@@ -125,12 +147,20 @@ export function parseSiteConfig(raw) {
|
|
|
125
147
|
fail("github.ciCanDeployInfra", "must be a boolean");
|
|
126
148
|
github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra };
|
|
127
149
|
}
|
|
150
|
+
const hasDomain = cms.domainName !== undefined || reader?.domainName !== undefined;
|
|
151
|
+
if (hasDomain && hostedZoneId === undefined) {
|
|
152
|
+
fail("hostedZoneId", "is required when cms.domainName or reader.domainName is set");
|
|
153
|
+
}
|
|
154
|
+
if (!hasDomain && hostedZoneId !== undefined) {
|
|
155
|
+
fail("hostedZoneId", "is only allowed when cms.domainName or reader.domainName is set");
|
|
156
|
+
}
|
|
128
157
|
const config = {
|
|
129
158
|
siteId,
|
|
130
159
|
repository,
|
|
131
160
|
brand,
|
|
132
161
|
frontend,
|
|
133
162
|
hostedZoneId,
|
|
163
|
+
stackName,
|
|
134
164
|
cms,
|
|
135
165
|
reader,
|
|
136
166
|
papyrusVersion,
|
|
@@ -141,9 +171,10 @@ export function parseSiteConfig(raw) {
|
|
|
141
171
|
if (redirectValue === undefined)
|
|
142
172
|
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
|
|
143
173
|
const redirectUrls = redirectValue.split(",").map((url) => url.trim());
|
|
174
|
+
const stagingDomainName = resolveStagingDomainName(config);
|
|
144
175
|
const requiredOrigins = [
|
|
145
|
-
`https://${cms.domainName}
|
|
146
|
-
`https://${
|
|
176
|
+
...(cms.domainName ? [`https://${cms.domainName}/`] : []),
|
|
177
|
+
...(stagingDomainName ? [`https://${stagingDomainName}/`] : []),
|
|
147
178
|
LOCAL_DEVELOPMENT_ORIGIN,
|
|
148
179
|
];
|
|
149
180
|
for (const origin of requiredOrigins) {
|
package/infra/site-config.ts
CHANGED
|
@@ -3,17 +3,19 @@ export type AmplifyAppShellSiteConfig = {
|
|
|
3
3
|
repository: string;
|
|
4
4
|
brand: string;
|
|
5
5
|
frontend: "pretext" | "markus-static";
|
|
6
|
-
hostedZoneId
|
|
6
|
+
hostedZoneId?: string;
|
|
7
|
+
stackName?: string;
|
|
7
8
|
cms: {
|
|
8
9
|
appName?: string;
|
|
9
|
-
domainName
|
|
10
|
+
domainName?: string;
|
|
11
|
+
staging?: boolean;
|
|
10
12
|
buildComputeType?: "STANDARD" | "STANDARD_8GB";
|
|
11
13
|
environment: Record<string, string>;
|
|
12
14
|
stagingDomainName?: string;
|
|
13
15
|
};
|
|
14
16
|
reader?: {
|
|
15
17
|
appName?: string;
|
|
16
|
-
domainName
|
|
18
|
+
domainName?: string;
|
|
17
19
|
branchName?: string;
|
|
18
20
|
buildCommand: string;
|
|
19
21
|
baseDirectory: string;
|
|
@@ -36,6 +38,7 @@ const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_
|
|
|
36
38
|
const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
|
|
37
39
|
const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
38
40
|
const APP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 _.-]*$/;
|
|
41
|
+
const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
|
|
39
42
|
const BRANCH_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9/_.-]*$/;
|
|
40
43
|
const PAPYRUS_VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+\.\d+)?$/;
|
|
41
44
|
const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
@@ -83,12 +86,22 @@ function rejectUnknownKeys(record: Record<string, unknown>, allowed: string[], f
|
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
export function
|
|
89
|
+
export function isStagingEnabled(config: Pick<AmplifyAppShellSiteConfig, "cms">): boolean {
|
|
90
|
+
return config.cms.staging !== false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resolveStagingDomainName(config: Pick<AmplifyAppShellSiteConfig, "cms">): string | undefined {
|
|
94
|
+
if (!isStagingEnabled(config)) return undefined;
|
|
87
95
|
if (config.cms.stagingDomainName) return config.cms.stagingDomainName;
|
|
96
|
+
if (!config.cms.domainName) return undefined;
|
|
88
97
|
const [, ...rest] = config.cms.domainName.split(".");
|
|
89
98
|
return ["staging", ...rest].join(".");
|
|
90
99
|
}
|
|
91
100
|
|
|
101
|
+
export function resolveStackName(config: Pick<AmplifyAppShellSiteConfig, "siteId" | "stackName">): string {
|
|
102
|
+
return config.stackName ?? `amplify-app-shell-${config.siteId}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
export function resolveStoragePreviewPrefix(config: Pick<AmplifyAppShellSiteConfig, "storagePreviewPrefix">): string {
|
|
93
106
|
return config.storagePreviewPrefix ?? "preview/";
|
|
94
107
|
}
|
|
@@ -97,7 +110,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
97
110
|
const record = requireObject(raw, "site");
|
|
98
111
|
rejectUnknownKeys(
|
|
99
112
|
record,
|
|
100
|
-
["siteId", "repository", "brand", "frontend", "hostedZoneId", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
|
|
113
|
+
["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
|
|
101
114
|
"site",
|
|
102
115
|
);
|
|
103
116
|
|
|
@@ -108,24 +121,36 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
108
121
|
if (frontend !== "pretext" && frontend !== "markus-static") {
|
|
109
122
|
fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
|
|
110
123
|
}
|
|
111
|
-
const hostedZoneId =
|
|
124
|
+
const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
|
|
125
|
+
const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
|
|
112
126
|
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
113
127
|
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
114
128
|
|
|
115
129
|
const cmsRecord = requireObject(record.cms, "cms");
|
|
116
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "buildComputeType", "environment", "stagingDomainName"], "cms");
|
|
130
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "buildComputeType", "environment", "stagingDomainName"], "cms");
|
|
117
131
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
118
132
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
119
133
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
120
134
|
}
|
|
135
|
+
if (cmsRecord.staging !== undefined && typeof cmsRecord.staging !== "boolean") {
|
|
136
|
+
fail("cms.staging", `must be a boolean, got ${JSON.stringify(cmsRecord.staging)}`);
|
|
137
|
+
}
|
|
121
138
|
const cms: AmplifyAppShellSiteConfig["cms"] = {
|
|
122
139
|
appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
|
|
123
|
-
domainName:
|
|
140
|
+
domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
|
|
141
|
+
staging: cmsRecord.staging as boolean | undefined,
|
|
124
142
|
buildComputeType: buildComputeType as "STANDARD" | "STANDARD_8GB" | undefined,
|
|
125
143
|
environment: requireStringRecord(cmsRecord.environment, "cms.environment"),
|
|
126
144
|
stagingDomainName: optionalString(cmsRecord.stagingDomainName, "cms.stagingDomainName", HOST_NAME_PATTERN, "host name"),
|
|
127
145
|
};
|
|
128
146
|
|
|
147
|
+
if (cms.staging === false && cms.stagingDomainName) {
|
|
148
|
+
fail("cms.stagingDomainName", "must not be set when cms.staging is false");
|
|
149
|
+
}
|
|
150
|
+
if (cms.stagingDomainName && !cms.domainName) {
|
|
151
|
+
fail("cms.stagingDomainName", "requires cms.domainName");
|
|
152
|
+
}
|
|
153
|
+
|
|
129
154
|
let reader: AmplifyAppShellSiteConfig["reader"];
|
|
130
155
|
if (record.reader !== undefined) {
|
|
131
156
|
if (frontend !== "markus-static") fail("reader", `is only allowed when frontend is "markus-static"`);
|
|
@@ -133,7 +158,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
133
158
|
rejectUnknownKeys(readerRecord, ["appName", "domainName", "branchName", "buildCommand", "baseDirectory", "environment"], "reader");
|
|
134
159
|
reader = {
|
|
135
160
|
appName: optionalString(readerRecord.appName, "reader.appName", APP_NAME_PATTERN),
|
|
136
|
-
domainName:
|
|
161
|
+
domainName: optionalString(readerRecord.domainName, "reader.domainName", HOST_NAME_PATTERN, "host name"),
|
|
137
162
|
branchName: optionalString(readerRecord.branchName, "reader.branchName", BRANCH_NAME_PATTERN),
|
|
138
163
|
buildCommand: requireString(readerRecord.buildCommand, "reader.buildCommand"),
|
|
139
164
|
baseDirectory: requireString(readerRecord.baseDirectory, "reader.baseDirectory"),
|
|
@@ -152,7 +177,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
152
177
|
if (repository !== `https://github.com/${owner}/${repo}`) {
|
|
153
178
|
fail("github", `owner/repo must match repository ${repository}`);
|
|
154
179
|
}
|
|
155
|
-
let branches = DEFAULT_CI_BRANCHES;
|
|
180
|
+
let branches = cms.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
|
|
156
181
|
if (githubRecord.branches !== undefined) {
|
|
157
182
|
if (!Array.isArray(githubRecord.branches) || githubRecord.branches.length === 0) {
|
|
158
183
|
fail("github.branches", "must be a non-empty array of branch names");
|
|
@@ -169,12 +194,21 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
169
194
|
github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra as boolean };
|
|
170
195
|
}
|
|
171
196
|
|
|
197
|
+
const hasDomain = cms.domainName !== undefined || reader?.domainName !== undefined;
|
|
198
|
+
if (hasDomain && hostedZoneId === undefined) {
|
|
199
|
+
fail("hostedZoneId", "is required when cms.domainName or reader.domainName is set");
|
|
200
|
+
}
|
|
201
|
+
if (!hasDomain && hostedZoneId !== undefined) {
|
|
202
|
+
fail("hostedZoneId", "is only allowed when cms.domainName or reader.domainName is set");
|
|
203
|
+
}
|
|
204
|
+
|
|
172
205
|
const config: AmplifyAppShellSiteConfig = {
|
|
173
206
|
siteId,
|
|
174
207
|
repository,
|
|
175
208
|
brand,
|
|
176
209
|
frontend,
|
|
177
210
|
hostedZoneId,
|
|
211
|
+
stackName,
|
|
178
212
|
cms,
|
|
179
213
|
reader,
|
|
180
214
|
papyrusVersion,
|
|
@@ -185,9 +219,10 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
185
219
|
const redirectValue = cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS;
|
|
186
220
|
if (redirectValue === undefined) fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
|
|
187
221
|
const redirectUrls = redirectValue.split(",").map((url) => url.trim());
|
|
222
|
+
const stagingDomainName = resolveStagingDomainName(config);
|
|
188
223
|
const requiredOrigins = [
|
|
189
|
-
`https://${cms.domainName}
|
|
190
|
-
`https://${
|
|
224
|
+
...(cms.domainName ? [`https://${cms.domainName}/`] : []),
|
|
225
|
+
...(stagingDomainName ? [`https://${stagingDomainName}/`] : []),
|
|
191
226
|
LOCAL_DEVELOPMENT_ORIGIN,
|
|
192
227
|
];
|
|
193
228
|
for (const origin of requiredOrigins) {
|
|
@@ -3,12 +3,22 @@ import { getContentSource, type ContentSource } from "./site-env";
|
|
|
3
3
|
|
|
4
4
|
export type ContentSourceContext = {
|
|
5
5
|
source: ContentSource;
|
|
6
|
+
/** Request-scoped data client (drafts mode: carries the visitor's session cookies). */
|
|
7
|
+
client?: unknown;
|
|
6
8
|
};
|
|
7
9
|
|
|
8
10
|
const contentSourceStorage = new AsyncLocalStorage<ContentSourceContext>();
|
|
9
11
|
|
|
10
|
-
export function runWithContentSource<T>(
|
|
11
|
-
|
|
12
|
+
export function runWithContentSource<T>(
|
|
13
|
+
source: ContentSource,
|
|
14
|
+
operation: () => Promise<T>,
|
|
15
|
+
client?: unknown,
|
|
16
|
+
): Promise<T> {
|
|
17
|
+
return contentSourceStorage.run({ source, client }, operation);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function currentRequestClient<T>(): T | undefined {
|
|
21
|
+
return contentSourceStorage.getStore()?.client as T | undefined;
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
export function currentContentSource(): ContentSource {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { generateServerClientUsingCookies } from "@aws-amplify/adapter-nextjs/api";
|
|
1
2
|
import { generateClient } from "aws-amplify/data";
|
|
2
3
|
import type { Schema } from "../amplify/data/resource";
|
|
3
4
|
import type { Article, ArticleImage, ArticleImageAsset, ArticleImageLayout, ArticleImageThemeVariants, ArticleVideoAsset, ArticleVideoThemeVariants } from "./articles";
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
} from "./publication-items";
|
|
18
19
|
import { BodyIrError, matchBodyImages, projectBodyIr, type BodyProjection, type BodyProjectionImage } from "./markus-body";
|
|
19
20
|
import { SITE_BRAND } from "./site-brand";
|
|
20
|
-
import { currentContentSource, runWithContentSource } from "./content-source-context";
|
|
21
|
+
import { currentContentSource, currentRequestClient, runWithContentSource } from "./content-source-context";
|
|
21
22
|
import { getContentSource } from "./site-env";
|
|
22
23
|
|
|
23
24
|
const DEFAULT_EDITION_SLUG = "current";
|
|
@@ -143,12 +144,18 @@ function isVisibleItemStatus(status: string): boolean {
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
async function withReaderGraphQLContext<T>(operation: () => Promise<T>): Promise<T> {
|
|
146
|
-
const { runWithAmplifyServerContext } = getAmplifyServerRuntime();
|
|
147
|
+
const { runWithAmplifyServerContext, config } = getAmplifyServerRuntime();
|
|
147
148
|
const source = getContentSource();
|
|
148
|
-
|
|
149
|
+
if (source === "drafts") {
|
|
150
|
+
// Drafts are read as the signed-in visitor: the client reads the session from the request cookies, and it is
|
|
151
|
+
// kept in the request's async context (never a module global) so one visitor's session cannot serve another.
|
|
152
|
+
const { cookies } = await import("next/headers");
|
|
153
|
+
const client = generateServerClientUsingCookies<Schema>({ config, cookies, authMode: "userPool" });
|
|
154
|
+
return runWithContentSource(source, operation, client);
|
|
155
|
+
}
|
|
149
156
|
return runWithContentSource(source, () =>
|
|
150
157
|
runWithAmplifyServerContext({
|
|
151
|
-
nextServerContext,
|
|
158
|
+
nextServerContext: null,
|
|
152
159
|
operation: async () => {
|
|
153
160
|
readerContextClient = generateClient<Schema>({ authMode: authMode() });
|
|
154
161
|
try {
|
|
@@ -241,6 +248,8 @@ export const graphqlContentRepository: ContentRepository = {
|
|
|
241
248
|
};
|
|
242
249
|
|
|
243
250
|
function getClient(): DataClient {
|
|
251
|
+
const requestClient = currentRequestClient<DataClient>();
|
|
252
|
+
if (requestClient) return requestClient;
|
|
244
253
|
if (readerContextClient) return readerContextClient;
|
|
245
254
|
|
|
246
255
|
if (!buildTimeClient) {
|
package/package.json
CHANGED
package/routes.manifest.json
CHANGED