@anthusai/papyrus 1.0.0-next.41 → 1.0.0-next.42
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/infra/amplify-app-shell.js +13 -5
- package/infra/amplify-app-shell.ts +14 -5
- package/infra/build-specs.js +3 -1
- package/infra/build-specs.ts +2 -1
- package/infra/site-config.js +46 -33
- package/infra/site-config.ts +74 -55
- package/package.json +1 -1
- package/routes.manifest.json +1 -1
|
@@ -3,7 +3,7 @@ import * as iam from "aws-cdk-lib/aws-iam";
|
|
|
3
3
|
import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
4
4
|
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider.js";
|
|
5
5
|
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs.js";
|
|
6
|
-
import { isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
|
|
6
|
+
import { hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
|
|
7
7
|
function environmentVariables(variables) {
|
|
8
8
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
9
9
|
}
|
|
@@ -25,7 +25,9 @@ function readerCustomHeaders(rules) {
|
|
|
25
25
|
*
|
|
26
26
|
* - CMS app (WEB_COMPUTE) with branches `main` (production, owns the backend)
|
|
27
27
|
* and `staging` (frontend only, reads the production backend)
|
|
28
|
-
* - for `markus-static` sites, a reader app (WEB) with branch `main
|
|
28
|
+
* - for `markus-static` sites, a reader app (WEB) with branch `main`; a
|
|
29
|
+
* `markus-static` site may omit `cms` to get the reader app alone (no CMS
|
|
30
|
+
* app, no CMS roles, no authoring role)
|
|
29
31
|
* - a domain association per branch/app when a domain is configured (without
|
|
30
32
|
* one, no AWS::Amplify::Domain is created and the default amplifyapp.com
|
|
31
33
|
* URL is used), IAM service and compute roles
|
|
@@ -50,6 +52,12 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
50
52
|
readerAppId;
|
|
51
53
|
constructor(scope, id, config, props) {
|
|
52
54
|
super(scope, id, props);
|
|
55
|
+
if (!hasCmsApp(config)) {
|
|
56
|
+
if (!config.reader)
|
|
57
|
+
throw new Error(`site ${config.siteId} has neither a cms section nor a reader`);
|
|
58
|
+
this.readerAppId = this.addReaderApp(config, config.reader);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
53
61
|
const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
|
|
54
62
|
const stagingEnabled = isStagingEnabled(config);
|
|
55
63
|
const stagingDomainName = resolveStagingDomainName(config);
|
|
@@ -192,7 +200,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
192
200
|
const amplifyAppIds = [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])];
|
|
193
201
|
this.addAuthoringRole(config, cmsApp.attrAppId, amplifyAppIds);
|
|
194
202
|
if (config.github) {
|
|
195
|
-
this.addGithubCiRole(config, config.github, amplifyAppIds);
|
|
203
|
+
this.addGithubCiRole(config, config.github, cmsApp.attrAppId, amplifyAppIds);
|
|
196
204
|
}
|
|
197
205
|
}
|
|
198
206
|
authoringStatements(config, cmsAppId, amplifyAppIds) {
|
|
@@ -238,7 +246,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
238
246
|
description: `Role the Papyrus CLI assumes for ${config.siteId} (profile role_arn)`,
|
|
239
247
|
});
|
|
240
248
|
}
|
|
241
|
-
addGithubCiRole(config, github, amplifyAppIds) {
|
|
249
|
+
addGithubCiRole(config, github, cmsAppId, amplifyAppIds) {
|
|
242
250
|
const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(this, "GithubOidcProvider", this.formatArn({
|
|
243
251
|
service: "iam",
|
|
244
252
|
region: "",
|
|
@@ -257,7 +265,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
257
265
|
},
|
|
258
266
|
}),
|
|
259
267
|
});
|
|
260
|
-
for (const statement of this.authoringStatements(config,
|
|
268
|
+
for (const statement of this.authoringStatements(config, cmsAppId, amplifyAppIds)) {
|
|
261
269
|
ciRole.addToPolicy(statement);
|
|
262
270
|
}
|
|
263
271
|
ciRole.addToPolicy(new iam.PolicyStatement({
|
|
@@ -4,7 +4,7 @@ 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, ReaderCustomHeaderRule, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
|
|
7
|
+
import { AmplifyAppShellSiteConfig, ReaderCustomHeaderRule, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
|
|
8
8
|
|
|
9
9
|
function environmentVariables(variables: Record<string, string>): amplify.CfnBranch.EnvironmentVariableProperty[] {
|
|
10
10
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
@@ -28,7 +28,9 @@ function readerCustomHeaders(rules: ReaderCustomHeaderRule[] | undefined): strin
|
|
|
28
28
|
*
|
|
29
29
|
* - CMS app (WEB_COMPUTE) with branches `main` (production, owns the backend)
|
|
30
30
|
* and `staging` (frontend only, reads the production backend)
|
|
31
|
-
* - for `markus-static` sites, a reader app (WEB) with branch `main
|
|
31
|
+
* - for `markus-static` sites, a reader app (WEB) with branch `main`; a
|
|
32
|
+
* `markus-static` site may omit `cms` to get the reader app alone (no CMS
|
|
33
|
+
* app, no CMS roles, no authoring role)
|
|
32
34
|
* - a domain association per branch/app when a domain is configured (without
|
|
33
35
|
* one, no AWS::Amplify::Domain is created and the default amplifyapp.com
|
|
34
36
|
* URL is used), IAM service and compute roles
|
|
@@ -49,12 +51,18 @@ The stack also creates the `<siteId>-papyrus-authoring` role. The CLI signs
|
|
|
49
51
|
* app by `ampx pipeline-deploy` from the production build.
|
|
50
52
|
*/
|
|
51
53
|
export class AmplifyAppShellStack extends Stack {
|
|
52
|
-
public readonly cmsAppId
|
|
54
|
+
public readonly cmsAppId?: string;
|
|
53
55
|
public readonly readerAppId?: string;
|
|
54
56
|
|
|
55
57
|
constructor(scope: Construct, id: string, config: AmplifyAppShellSiteConfig, props?: StackProps) {
|
|
56
58
|
super(scope, id, props);
|
|
57
59
|
|
|
60
|
+
if (!hasCmsApp(config)) {
|
|
61
|
+
if (!config.reader) throw new Error(`site ${config.siteId} has neither a cms section nor a reader`);
|
|
62
|
+
this.readerAppId = this.addReaderApp(config, config.reader);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
58
66
|
const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
|
|
59
67
|
const stagingEnabled = isStagingEnabled(config);
|
|
60
68
|
const stagingDomainName = resolveStagingDomainName(config);
|
|
@@ -213,7 +221,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
213
221
|
this.addAuthoringRole(config, cmsApp.attrAppId, amplifyAppIds);
|
|
214
222
|
|
|
215
223
|
if (config.github) {
|
|
216
|
-
this.addGithubCiRole(config, config.github, amplifyAppIds);
|
|
224
|
+
this.addGithubCiRole(config, config.github, cmsApp.attrAppId, amplifyAppIds);
|
|
217
225
|
}
|
|
218
226
|
}
|
|
219
227
|
|
|
@@ -267,6 +275,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
267
275
|
private addGithubCiRole(
|
|
268
276
|
config: AmplifyAppShellSiteConfig,
|
|
269
277
|
github: NonNullable<AmplifyAppShellSiteConfig["github"]>,
|
|
278
|
+
cmsAppId: string,
|
|
270
279
|
amplifyAppIds: string[],
|
|
271
280
|
): void {
|
|
272
281
|
const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(
|
|
@@ -295,7 +304,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
295
304
|
}),
|
|
296
305
|
});
|
|
297
306
|
|
|
298
|
-
for (const statement of this.authoringStatements(config,
|
|
307
|
+
for (const statement of this.authoringStatements(config, cmsAppId, amplifyAppIds)) {
|
|
299
308
|
ciRole.addToPolicy(statement);
|
|
300
309
|
}
|
|
301
310
|
ciRole.addToPolicy(
|
package/infra/build-specs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveStoragePreviewPrefix } from "./site-config.js";
|
|
1
|
+
import { hasCmsApp, resolveStoragePreviewPrefix } from "./site-config.js";
|
|
2
2
|
export function papyrusVersionToPep440(version) {
|
|
3
3
|
const match = /^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z-]+\.(\d+))?$/.exec(version);
|
|
4
4
|
if (!match)
|
|
@@ -88,6 +88,8 @@ export function readerBuildSpec(config) {
|
|
|
88
88
|
].join("\n") + "\n";
|
|
89
89
|
}
|
|
90
90
|
export function buildSpecFor(config, app) {
|
|
91
|
+
if (app !== "reader" && !hasCmsApp(config))
|
|
92
|
+
throw new Error(`site ${config.siteId} has no cms app`);
|
|
91
93
|
if (app === "cms-production")
|
|
92
94
|
return cmsProductionBuildSpec(config);
|
|
93
95
|
if (app === "cms-staging")
|
package/infra/build-specs.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AmplifyAppShellSiteConfig, resolveStoragePreviewPrefix } from "./site-config";
|
|
1
|
+
import { AmplifyAppShellSiteConfig, hasCmsApp, resolveStoragePreviewPrefix } from "./site-config";
|
|
2
2
|
|
|
3
3
|
export type BuildSpecAppName = "cms-production" | "cms-staging" | "reader";
|
|
4
4
|
|
|
@@ -137,6 +137,7 @@ export function readerBuildSpec(config: AmplifyAppShellSiteConfig): string {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
export function buildSpecFor(config: AmplifyAppShellSiteConfig, app: BuildSpecAppName): string {
|
|
140
|
+
if (app !== "reader" && !hasCmsApp(config)) throw new Error(`site ${config.siteId} has no cms app`);
|
|
140
141
|
if (app === "cms-production") return cmsProductionBuildSpec(config);
|
|
141
142
|
if (app === "cms-staging") return cmsStagingBuildSpec(config);
|
|
142
143
|
return readerBuildSpec(config);
|
package/infra/site-config.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export const READER_REDIRECT_STATUSES = ["301", "302", "200", "404", "404-200"];
|
|
2
|
+
export function hasCmsApp(config) {
|
|
3
|
+
return config.cms !== undefined;
|
|
4
|
+
}
|
|
2
5
|
export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
3
6
|
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
4
7
|
const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
@@ -166,21 +169,8 @@ export function resolveStackName(config) {
|
|
|
166
169
|
export function resolveStoragePreviewPrefix(config) {
|
|
167
170
|
return config.storagePreviewPrefix ?? "preview/";
|
|
168
171
|
}
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
rejectUnknownKeys(record, ["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"], "site");
|
|
172
|
-
const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
|
|
173
|
-
const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
|
|
174
|
-
const brand = requireString(record.brand, "brand");
|
|
175
|
-
const frontend = requireString(record.frontend, "frontend");
|
|
176
|
-
if (frontend !== "pretext" && frontend !== "markus-static") {
|
|
177
|
-
fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
|
|
178
|
-
}
|
|
179
|
-
const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
|
|
180
|
-
const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
|
|
181
|
-
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
182
|
-
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
183
|
-
const cmsRecord = requireObject(record.cms, "cms");
|
|
172
|
+
function parseCms(value) {
|
|
173
|
+
const cmsRecord = requireObject(value, "cms");
|
|
184
174
|
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects"], "cms");
|
|
185
175
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
186
176
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
@@ -256,6 +246,41 @@ export function parseSiteConfig(raw) {
|
|
|
256
246
|
fail("cms.redirects", "must not repeat a source");
|
|
257
247
|
}
|
|
258
248
|
}
|
|
249
|
+
return cms;
|
|
250
|
+
}
|
|
251
|
+
function validateOauthRedirectUrls(config) {
|
|
252
|
+
const redirectValue = config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS;
|
|
253
|
+
if (redirectValue === undefined)
|
|
254
|
+
fail("config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
|
|
255
|
+
const redirectUrls = redirectValue.split(",").map((url) => url.trim());
|
|
256
|
+
const stagingDomainName = resolveStagingDomainName(config);
|
|
257
|
+
const listedOrigins = new Set(redirectUrls.map(originOfRedirectUrl));
|
|
258
|
+
const requiredOrigins = [
|
|
259
|
+
...(resolveCmsHostName(config) ? [`https://${resolveCmsHostName(config)}`] : []),
|
|
260
|
+
...(stagingDomainName ? [`https://${stagingDomainName}`] : []),
|
|
261
|
+
originOfRedirectUrl(LOCAL_DEVELOPMENT_ORIGIN),
|
|
262
|
+
];
|
|
263
|
+
for (const origin of requiredOrigins) {
|
|
264
|
+
if (!listedOrigins.has(origin)) {
|
|
265
|
+
fail("config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}/ (or just the origin: the backend also registers its newsroom path)`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
export function parseSiteConfig(raw) {
|
|
270
|
+
const record = requireObject(raw, "site");
|
|
271
|
+
rejectUnknownKeys(record, ["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"], "site");
|
|
272
|
+
const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
|
|
273
|
+
const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
|
|
274
|
+
const brand = requireString(record.brand, "brand");
|
|
275
|
+
const frontend = requireString(record.frontend, "frontend");
|
|
276
|
+
if (frontend !== "pretext" && frontend !== "markus-static") {
|
|
277
|
+
fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
|
|
278
|
+
}
|
|
279
|
+
const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
|
|
280
|
+
const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
|
|
281
|
+
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
282
|
+
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
283
|
+
const cms = record.cms === undefined && frontend === "markus-static" ? undefined : parseCms(record.cms);
|
|
259
284
|
let reader;
|
|
260
285
|
if (record.reader !== undefined) {
|
|
261
286
|
if (frontend !== "markus-static")
|
|
@@ -286,6 +311,8 @@ export function parseSiteConfig(raw) {
|
|
|
286
311
|
}
|
|
287
312
|
let github;
|
|
288
313
|
if (record.github !== undefined) {
|
|
314
|
+
if (cms === undefined)
|
|
315
|
+
fail("github", "requires cms (the CI role is scoped to the CMS app)");
|
|
289
316
|
const githubRecord = requireObject(record.github, "github");
|
|
290
317
|
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
|
|
291
318
|
const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
|
|
@@ -293,7 +320,7 @@ export function parseSiteConfig(raw) {
|
|
|
293
320
|
if (repository !== `https://github.com/${owner}/${repo}`) {
|
|
294
321
|
fail("github", `owner/repo must match repository ${repository}`);
|
|
295
322
|
}
|
|
296
|
-
let branches = cms
|
|
323
|
+
let branches = cms?.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
|
|
297
324
|
if (githubRecord.branches !== undefined) {
|
|
298
325
|
if (!Array.isArray(githubRecord.branches) || githubRecord.branches.length === 0) {
|
|
299
326
|
fail("github.branches", "must be a non-empty array of branch names");
|
|
@@ -310,7 +337,7 @@ export function parseSiteConfig(raw) {
|
|
|
310
337
|
fail("github.ciCanDeployInfra", "must be a boolean");
|
|
311
338
|
github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra };
|
|
312
339
|
}
|
|
313
|
-
const hasDomain = cms
|
|
340
|
+
const hasDomain = cms?.domainName !== undefined || reader?.domainName !== undefined;
|
|
314
341
|
if (hasDomain && hostedZoneId === undefined) {
|
|
315
342
|
fail("hostedZoneId", "is required when cms.domainName or reader.domainName is set");
|
|
316
343
|
}
|
|
@@ -330,21 +357,7 @@ export function parseSiteConfig(raw) {
|
|
|
330
357
|
storagePreviewPrefix,
|
|
331
358
|
github,
|
|
332
359
|
};
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
|
|
336
|
-
const redirectUrls = redirectValue.split(",").map((url) => url.trim());
|
|
337
|
-
const stagingDomainName = resolveStagingDomainName(config);
|
|
338
|
-
const listedOrigins = new Set(redirectUrls.map(originOfRedirectUrl));
|
|
339
|
-
const requiredOrigins = [
|
|
340
|
-
...(resolveCmsHostName(config) ? [`https://${resolveCmsHostName(config)}`] : []),
|
|
341
|
-
...(stagingDomainName ? [`https://${stagingDomainName}`] : []),
|
|
342
|
-
originOfRedirectUrl(LOCAL_DEVELOPMENT_ORIGIN),
|
|
343
|
-
];
|
|
344
|
-
for (const origin of requiredOrigins) {
|
|
345
|
-
if (!listedOrigins.has(origin)) {
|
|
346
|
-
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}/ (or just the origin: the backend also registers its newsroom path)`);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
360
|
+
if (hasCmsApp(config))
|
|
361
|
+
validateOauthRedirectUrls(config);
|
|
349
362
|
return config;
|
|
350
363
|
}
|
package/infra/site-config.ts
CHANGED
|
@@ -18,6 +18,20 @@ export type ReaderCustomHeaderRule = {
|
|
|
18
18
|
|
|
19
19
|
export const READER_REDIRECT_STATUSES: ReaderRedirectStatus[] = ["301", "302", "200", "404", "404-200"];
|
|
20
20
|
|
|
21
|
+
export type CmsConfig = {
|
|
22
|
+
appName?: string;
|
|
23
|
+
domainName?: string;
|
|
24
|
+
domainPrefix?: string;
|
|
25
|
+
staging?: boolean;
|
|
26
|
+
cognitoDomainPrefix: string;
|
|
27
|
+
applyCognitoDomainPrefix?: boolean;
|
|
28
|
+
buildComputeType?: "STANDARD" | "STANDARD_8GB";
|
|
29
|
+
environment: Record<string, string>;
|
|
30
|
+
stagingDomainName?: string;
|
|
31
|
+
stagingDomainPrefix?: string;
|
|
32
|
+
redirects?: CmsRedirect[];
|
|
33
|
+
};
|
|
34
|
+
|
|
21
35
|
export type AmplifyAppShellSiteConfig = {
|
|
22
36
|
siteId: string;
|
|
23
37
|
repository: string;
|
|
@@ -25,19 +39,7 @@ export type AmplifyAppShellSiteConfig = {
|
|
|
25
39
|
frontend: "pretext" | "markus-static";
|
|
26
40
|
hostedZoneId?: string;
|
|
27
41
|
stackName?: string;
|
|
28
|
-
cms
|
|
29
|
-
appName?: string;
|
|
30
|
-
domainName?: string;
|
|
31
|
-
domainPrefix?: string;
|
|
32
|
-
staging?: boolean;
|
|
33
|
-
cognitoDomainPrefix: string;
|
|
34
|
-
applyCognitoDomainPrefix?: boolean;
|
|
35
|
-
buildComputeType?: "STANDARD" | "STANDARD_8GB";
|
|
36
|
-
environment: Record<string, string>;
|
|
37
|
-
stagingDomainName?: string;
|
|
38
|
-
stagingDomainPrefix?: string;
|
|
39
|
-
redirects?: CmsRedirect[];
|
|
40
|
-
};
|
|
42
|
+
cms?: CmsConfig;
|
|
41
43
|
reader?: {
|
|
42
44
|
appName?: string;
|
|
43
45
|
domainName?: string;
|
|
@@ -61,6 +63,12 @@ export type AmplifyAppShellSiteConfig = {
|
|
|
61
63
|
};
|
|
62
64
|
};
|
|
63
65
|
|
|
66
|
+
export type CmsSiteConfig = AmplifyAppShellSiteConfig & { cms: CmsConfig };
|
|
67
|
+
|
|
68
|
+
export function hasCmsApp(config: AmplifyAppShellSiteConfig): config is CmsSiteConfig {
|
|
69
|
+
return config.cms !== undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
64
72
|
export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
65
73
|
|
|
66
74
|
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
@@ -200,16 +208,16 @@ export function resolveRedirectSubDomainPrefix(rootDomain: string, source: strin
|
|
|
200
208
|
return source === rootDomain ? "" : source.slice(0, -rootDomain.length - 1);
|
|
201
209
|
}
|
|
202
210
|
|
|
203
|
-
export function isStagingEnabled(config: Pick<
|
|
211
|
+
export function isStagingEnabled(config: Pick<CmsSiteConfig, "cms">): boolean {
|
|
204
212
|
return config.cms.staging !== false;
|
|
205
213
|
}
|
|
206
214
|
|
|
207
|
-
export function resolveCmsHostName(config: Pick<
|
|
215
|
+
export function resolveCmsHostName(config: Pick<CmsSiteConfig, "cms">): string | undefined {
|
|
208
216
|
if (!config.cms.domainName) return undefined;
|
|
209
217
|
return config.cms.domainPrefix ? `${config.cms.domainPrefix}.${config.cms.domainName}` : config.cms.domainName;
|
|
210
218
|
}
|
|
211
219
|
|
|
212
|
-
export function resolveStagingDomainName(config: Pick<
|
|
220
|
+
export function resolveStagingDomainName(config: Pick<CmsSiteConfig, "cms">): string | undefined {
|
|
213
221
|
if (!isStagingEnabled(config)) return undefined;
|
|
214
222
|
if (config.cms.domainPrefix) {
|
|
215
223
|
return config.cms.stagingDomainPrefix ? `${config.cms.stagingDomainPrefix}.${config.cms.domainName}` : undefined;
|
|
@@ -228,27 +236,8 @@ export function resolveStoragePreviewPrefix(config: Pick<AmplifyAppShellSiteConf
|
|
|
228
236
|
return config.storagePreviewPrefix ?? "preview/";
|
|
229
237
|
}
|
|
230
238
|
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
rejectUnknownKeys(
|
|
234
|
-
record,
|
|
235
|
-
["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
|
|
236
|
-
"site",
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
|
|
240
|
-
const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
|
|
241
|
-
const brand = requireString(record.brand, "brand");
|
|
242
|
-
const frontend = requireString(record.frontend, "frontend");
|
|
243
|
-
if (frontend !== "pretext" && frontend !== "markus-static") {
|
|
244
|
-
fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
|
|
245
|
-
}
|
|
246
|
-
const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
|
|
247
|
-
const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
|
|
248
|
-
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
249
|
-
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
250
|
-
|
|
251
|
-
const cmsRecord = requireObject(record.cms, "cms");
|
|
239
|
+
function parseCms(value: unknown): CmsConfig {
|
|
240
|
+
const cmsRecord = requireObject(value, "cms");
|
|
252
241
|
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects"], "cms");
|
|
253
242
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
254
243
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
@@ -260,7 +249,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
260
249
|
if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
|
|
261
250
|
fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
|
|
262
251
|
}
|
|
263
|
-
const cms:
|
|
252
|
+
const cms: CmsConfig = {
|
|
264
253
|
appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
|
|
265
254
|
domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
|
|
266
255
|
domainPrefix: optionalString(cmsRecord.domainPrefix, "cms.domainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
@@ -330,6 +319,49 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
330
319
|
}
|
|
331
320
|
}
|
|
332
321
|
|
|
322
|
+
return cms;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function validateOauthRedirectUrls(config: CmsSiteConfig): void {
|
|
326
|
+
const redirectValue = config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS;
|
|
327
|
+
if (redirectValue === undefined) fail("config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
|
|
328
|
+
const redirectUrls = redirectValue.split(",").map((url) => url.trim());
|
|
329
|
+
const stagingDomainName = resolveStagingDomainName(config);
|
|
330
|
+
const listedOrigins = new Set(redirectUrls.map(originOfRedirectUrl));
|
|
331
|
+
const requiredOrigins = [
|
|
332
|
+
...(resolveCmsHostName(config) ? [`https://${resolveCmsHostName(config)}`] : []),
|
|
333
|
+
...(stagingDomainName ? [`https://${stagingDomainName}`] : []),
|
|
334
|
+
originOfRedirectUrl(LOCAL_DEVELOPMENT_ORIGIN),
|
|
335
|
+
];
|
|
336
|
+
for (const origin of requiredOrigins) {
|
|
337
|
+
if (!listedOrigins.has(origin)) {
|
|
338
|
+
fail("config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}/ (or just the origin: the backend also registers its newsroom path)`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
344
|
+
const record = requireObject(raw, "site");
|
|
345
|
+
rejectUnknownKeys(
|
|
346
|
+
record,
|
|
347
|
+
["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
|
|
348
|
+
"site",
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
|
|
352
|
+
const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
|
|
353
|
+
const brand = requireString(record.brand, "brand");
|
|
354
|
+
const frontend = requireString(record.frontend, "frontend");
|
|
355
|
+
if (frontend !== "pretext" && frontend !== "markus-static") {
|
|
356
|
+
fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
|
|
357
|
+
}
|
|
358
|
+
const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
|
|
359
|
+
const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
|
|
360
|
+
const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
|
|
361
|
+
const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
|
|
362
|
+
|
|
363
|
+
const cms = record.cms === undefined && frontend === "markus-static" ? undefined : parseCms(record.cms);
|
|
364
|
+
|
|
333
365
|
let reader: AmplifyAppShellSiteConfig["reader"];
|
|
334
366
|
if (record.reader !== undefined) {
|
|
335
367
|
if (frontend !== "markus-static") fail("reader", `is only allowed when frontend is "markus-static"`);
|
|
@@ -358,6 +390,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
358
390
|
|
|
359
391
|
let github: AmplifyAppShellSiteConfig["github"];
|
|
360
392
|
if (record.github !== undefined) {
|
|
393
|
+
if (cms === undefined) fail("github", "requires cms (the CI role is scoped to the CMS app)");
|
|
361
394
|
const githubRecord = requireObject(record.github, "github");
|
|
362
395
|
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
|
|
363
396
|
const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
|
|
@@ -365,7 +398,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
365
398
|
if (repository !== `https://github.com/${owner}/${repo}`) {
|
|
366
399
|
fail("github", `owner/repo must match repository ${repository}`);
|
|
367
400
|
}
|
|
368
|
-
let branches = cms
|
|
401
|
+
let branches = cms?.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
|
|
369
402
|
if (githubRecord.branches !== undefined) {
|
|
370
403
|
if (!Array.isArray(githubRecord.branches) || githubRecord.branches.length === 0) {
|
|
371
404
|
fail("github.branches", "must be a non-empty array of branch names");
|
|
@@ -382,7 +415,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
382
415
|
github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra as boolean };
|
|
383
416
|
}
|
|
384
417
|
|
|
385
|
-
const hasDomain = cms
|
|
418
|
+
const hasDomain = cms?.domainName !== undefined || reader?.domainName !== undefined;
|
|
386
419
|
if (hasDomain && hostedZoneId === undefined) {
|
|
387
420
|
fail("hostedZoneId", "is required when cms.domainName or reader.domainName is set");
|
|
388
421
|
}
|
|
@@ -404,21 +437,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
404
437
|
github,
|
|
405
438
|
};
|
|
406
439
|
|
|
407
|
-
|
|
408
|
-
if (redirectValue === undefined) fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
|
|
409
|
-
const redirectUrls = redirectValue.split(",").map((url) => url.trim());
|
|
410
|
-
const stagingDomainName = resolveStagingDomainName(config);
|
|
411
|
-
const listedOrigins = new Set(redirectUrls.map(originOfRedirectUrl));
|
|
412
|
-
const requiredOrigins = [
|
|
413
|
-
...(resolveCmsHostName(config) ? [`https://${resolveCmsHostName(config)}`] : []),
|
|
414
|
-
...(stagingDomainName ? [`https://${stagingDomainName}`] : []),
|
|
415
|
-
originOfRedirectUrl(LOCAL_DEVELOPMENT_ORIGIN),
|
|
416
|
-
];
|
|
417
|
-
for (const origin of requiredOrigins) {
|
|
418
|
-
if (!listedOrigins.has(origin)) {
|
|
419
|
-
fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}/ (or just the origin: the backend also registers its newsroom path)`);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
440
|
+
if (hasCmsApp(config)) validateOauthRedirectUrls(config);
|
|
422
441
|
|
|
423
442
|
return config;
|
|
424
443
|
}
|
package/package.json
CHANGED
package/routes.manifest.json
CHANGED