@anthusai/papyrus 1.0.0-next.40 → 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.
@@ -3,17 +3,31 @@ 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
  }
10
+ function readerCustomHeaders(rules) {
11
+ if (!rules)
12
+ return undefined;
13
+ const lines = ["customHeaders:"];
14
+ for (const rule of rules) {
15
+ lines.push(` - pattern: ${JSON.stringify(rule.pattern)}`, " headers:");
16
+ for (const [key, value] of Object.entries(rule.headers)) {
17
+ lines.push(` - key: ${JSON.stringify(key)}`, ` value: ${JSON.stringify(value)}`);
18
+ }
19
+ }
20
+ return lines.join("\n") + "\n";
21
+ }
10
22
  /**
11
23
  * Provisions the Amplify *app shell* of a Papyrus publication from its
12
24
  * site.json:
13
25
  *
14
26
  * - CMS app (WEB_COMPUTE) with branches `main` (production, owns the backend)
15
27
  * and `staging` (frontend only, reads the production backend)
16
- * - for `markus-static` sites, a reader app (WEB) with branch `main`
28
+ * - for `markus-static` sites, a reader app (WEB) with branch `main`; a
29
+ * `markus-static` site may omit `cms` to get the reader app alone (no CMS
30
+ * app, no CMS roles, no authoring role)
17
31
  * - a domain association per branch/app when a domain is configured (without
18
32
  * one, no AWS::Amplify::Domain is created and the default amplifyapp.com
19
33
  * URL is used), IAM service and compute roles
@@ -38,6 +52,12 @@ export class AmplifyAppShellStack extends Stack {
38
52
  readerAppId;
39
53
  constructor(scope, id, config, props) {
40
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
+ }
41
61
  const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
42
62
  const stagingEnabled = isStagingEnabled(config);
43
63
  const stagingDomainName = resolveStagingDomainName(config);
@@ -180,7 +200,7 @@ export class AmplifyAppShellStack extends Stack {
180
200
  const amplifyAppIds = [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])];
181
201
  this.addAuthoringRole(config, cmsApp.attrAppId, amplifyAppIds);
182
202
  if (config.github) {
183
- this.addGithubCiRole(config, config.github, amplifyAppIds);
203
+ this.addGithubCiRole(config, config.github, cmsApp.attrAppId, amplifyAppIds);
184
204
  }
185
205
  }
186
206
  authoringStatements(config, cmsAppId, amplifyAppIds) {
@@ -226,7 +246,7 @@ export class AmplifyAppShellStack extends Stack {
226
246
  description: `Role the Papyrus CLI assumes for ${config.siteId} (profile role_arn)`,
227
247
  });
228
248
  }
229
- addGithubCiRole(config, github, amplifyAppIds) {
249
+ addGithubCiRole(config, github, cmsAppId, amplifyAppIds) {
230
250
  const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(this, "GithubOidcProvider", this.formatArn({
231
251
  service: "iam",
232
252
  region: "",
@@ -245,7 +265,7 @@ export class AmplifyAppShellStack extends Stack {
245
265
  },
246
266
  }),
247
267
  });
248
- for (const statement of this.authoringStatements(config, this.cmsAppId, amplifyAppIds)) {
268
+ for (const statement of this.authoringStatements(config, cmsAppId, amplifyAppIds)) {
249
269
  ciRole.addToPolicy(statement);
250
270
  }
251
271
  ciRole.addToPolicy(new iam.PolicyStatement({
@@ -282,7 +302,11 @@ export class AmplifyAppShellStack extends Stack {
282
302
  platform: "WEB",
283
303
  iamServiceRole: readerServiceRole.roleArn,
284
304
  buildSpec: readerBuildSpec(config),
285
- customRules: [{ source: "/<*>", target: "/404.html", status: "404-200" }],
305
+ customRules: [
306
+ ...(reader.redirects ?? []),
307
+ { source: "/<*>", target: "/404.html", status: "404-200" },
308
+ ],
309
+ customHeaders: readerCustomHeaders(reader.customHeaders),
286
310
  });
287
311
  const readerBranch = new amplify.CfnBranch(this, "ReaderBranch", {
288
312
  appId: readerApp.attrAppId,
@@ -299,7 +323,7 @@ export class AmplifyAppShellStack extends Stack {
299
323
  const readerDomain = new amplify.CfnDomain(this, "ReaderDomain", {
300
324
  appId: readerApp.attrAppId,
301
325
  domainName: reader.domainName,
302
- subDomainSettings: [{ branchName: readerBranchName, prefix: "" }],
326
+ subDomainSettings: (reader.domainPrefixes ?? [""]).map((prefix) => ({ branchName: readerBranchName, prefix })),
303
327
  });
304
328
  readerDomain.addResourceDependency(readerBranch);
305
329
  }
@@ -4,19 +4,33 @@ import * as iam from "aws-cdk-lib/aws-iam";
4
4
  import * as amplify from "aws-cdk-lib/aws-amplify";
5
5
  import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider";
6
6
  import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs";
7
- import { AmplifyAppShellSiteConfig, isStagingEnabled, 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 }));
11
11
  }
12
12
 
13
+ function readerCustomHeaders(rules: ReaderCustomHeaderRule[] | undefined): string | undefined {
14
+ if (!rules) return undefined;
15
+ const lines = ["customHeaders:"];
16
+ for (const rule of rules) {
17
+ lines.push(` - pattern: ${JSON.stringify(rule.pattern)}`, " headers:");
18
+ for (const [key, value] of Object.entries(rule.headers)) {
19
+ lines.push(` - key: ${JSON.stringify(key)}`, ` value: ${JSON.stringify(value)}`);
20
+ }
21
+ }
22
+ return lines.join("\n") + "\n";
23
+ }
24
+
13
25
  /**
14
26
  * Provisions the Amplify *app shell* of a Papyrus publication from its
15
27
  * site.json:
16
28
  *
17
29
  * - CMS app (WEB_COMPUTE) with branches `main` (production, owns the backend)
18
30
  * and `staging` (frontend only, reads the production backend)
19
- * - for `markus-static` sites, a reader app (WEB) with branch `main`
31
+ * - for `markus-static` sites, a reader app (WEB) with branch `main`; a
32
+ * `markus-static` site may omit `cms` to get the reader app alone (no CMS
33
+ * app, no CMS roles, no authoring role)
20
34
  * - a domain association per branch/app when a domain is configured (without
21
35
  * one, no AWS::Amplify::Domain is created and the default amplifyapp.com
22
36
  * URL is used), IAM service and compute roles
@@ -37,12 +51,18 @@ The stack also creates the `<siteId>-papyrus-authoring` role. The CLI signs
37
51
  * app by `ampx pipeline-deploy` from the production build.
38
52
  */
39
53
  export class AmplifyAppShellStack extends Stack {
40
- public readonly cmsAppId: string;
54
+ public readonly cmsAppId?: string;
41
55
  public readonly readerAppId?: string;
42
56
 
43
57
  constructor(scope: Construct, id: string, config: AmplifyAppShellSiteConfig, props?: StackProps) {
44
58
  super(scope, id, props);
45
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
+
46
66
  const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
47
67
  const stagingEnabled = isStagingEnabled(config);
48
68
  const stagingDomainName = resolveStagingDomainName(config);
@@ -201,7 +221,7 @@ export class AmplifyAppShellStack extends Stack {
201
221
  this.addAuthoringRole(config, cmsApp.attrAppId, amplifyAppIds);
202
222
 
203
223
  if (config.github) {
204
- this.addGithubCiRole(config, config.github, amplifyAppIds);
224
+ this.addGithubCiRole(config, config.github, cmsApp.attrAppId, amplifyAppIds);
205
225
  }
206
226
  }
207
227
 
@@ -255,6 +275,7 @@ export class AmplifyAppShellStack extends Stack {
255
275
  private addGithubCiRole(
256
276
  config: AmplifyAppShellSiteConfig,
257
277
  github: NonNullable<AmplifyAppShellSiteConfig["github"]>,
278
+ cmsAppId: string,
258
279
  amplifyAppIds: string[],
259
280
  ): void {
260
281
  const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(
@@ -283,7 +304,7 @@ export class AmplifyAppShellStack extends Stack {
283
304
  }),
284
305
  });
285
306
 
286
- for (const statement of this.authoringStatements(config, this.cmsAppId, amplifyAppIds)) {
307
+ for (const statement of this.authoringStatements(config, cmsAppId, amplifyAppIds)) {
287
308
  ciRole.addToPolicy(statement);
288
309
  }
289
310
  ciRole.addToPolicy(
@@ -333,7 +354,11 @@ export class AmplifyAppShellStack extends Stack {
333
354
  platform: "WEB",
334
355
  iamServiceRole: readerServiceRole.roleArn,
335
356
  buildSpec: readerBuildSpec(config),
336
- customRules: [{ source: "/<*>", target: "/404.html", status: "404-200" }],
357
+ customRules: [
358
+ ...(reader.redirects ?? []),
359
+ { source: "/<*>", target: "/404.html", status: "404-200" },
360
+ ],
361
+ customHeaders: readerCustomHeaders(reader.customHeaders),
337
362
  });
338
363
 
339
364
  const readerBranch = new amplify.CfnBranch(this, "ReaderBranch", {
@@ -352,7 +377,7 @@ export class AmplifyAppShellStack extends Stack {
352
377
  const readerDomain = new amplify.CfnDomain(this, "ReaderDomain", {
353
378
  appId: readerApp.attrAppId,
354
379
  domainName: reader.domainName,
355
- subDomainSettings: [{ branchName: readerBranchName, prefix: "" }],
380
+ subDomainSettings: (reader.domainPrefixes ?? [""]).map((prefix) => ({ branchName: readerBranchName, prefix })),
356
381
  });
357
382
  readerDomain.addResourceDependency(readerBranch);
358
383
  }
@@ -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)
@@ -82,11 +82,14 @@ export function readerBuildSpec(config) {
82
82
  "version: 1",
83
83
  ...frontendSection([
84
84
  ...pythonProvisioningCommands(config),
85
- "papyrus ops content export-published --auth guest --out content-export --clean",
85
+ ...(readerConfig.preBuildCommands ?? []),
86
+ ...(readerConfig.skipContentExport ? [] : ["papyrus ops content export-published --auth guest --out content-export --clean"]),
86
87
  ], [readerConfig.buildCommand], readerConfig.baseDirectory, [".uv-cache/**/*"]),
87
88
  ].join("\n") + "\n";
88
89
  }
89
90
  export function buildSpecFor(config, app) {
91
+ if (app !== "reader" && !hasCmsApp(config))
92
+ throw new Error(`site ${config.siteId} has no cms app`);
90
93
  if (app === "cms-production")
91
94
  return cmsProductionBuildSpec(config);
92
95
  if (app === "cms-staging")
@@ -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
 
@@ -126,7 +126,8 @@ export function readerBuildSpec(config: AmplifyAppShellSiteConfig): string {
126
126
  ...frontendSection(
127
127
  [
128
128
  ...pythonProvisioningCommands(config),
129
- "papyrus ops content export-published --auth guest --out content-export --clean",
129
+ ...(readerConfig.preBuildCommands ?? []),
130
+ ...(readerConfig.skipContentExport ? [] : ["papyrus ops content export-published --auth guest --out content-export --clean"]),
130
131
  ],
131
132
  [readerConfig.buildCommand],
132
133
  readerConfig.baseDirectory,
@@ -136,6 +137,7 @@ export function readerBuildSpec(config: AmplifyAppShellSiteConfig): string {
136
137
  }
137
138
 
138
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`);
139
141
  if (app === "cms-production") return cmsProductionBuildSpec(config);
140
142
  if (app === "cms-staging") return cmsStagingBuildSpec(config);
141
143
  return readerBuildSpec(config);
@@ -1,3 +1,7 @@
1
+ export const READER_REDIRECT_STATUSES = ["301", "302", "200", "404", "404-200"];
2
+ export function hasCmsApp(config) {
3
+ return config.cms !== undefined;
4
+ }
1
5
  export const DEFAULT_CI_BRANCHES = ["main", "staging"];
2
6
  const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
3
7
  const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
@@ -78,6 +82,63 @@ function parseRedirects(value) {
78
82
  return { source, status };
79
83
  });
80
84
  }
85
+ function parseReaderDomainPrefixes(value) {
86
+ if (value === undefined)
87
+ return undefined;
88
+ if (!Array.isArray(value) || value.length === 0)
89
+ fail("reader.domainPrefixes", "must be a non-empty array");
90
+ const prefixes = value.map((entry, index) => {
91
+ const field = `reader.domainPrefixes[${index}]`;
92
+ if (entry === "")
93
+ return "";
94
+ return requireString(entry, field, DOMAIN_PREFIX_PATTERN, 'single DNS label, or "" for the apex');
95
+ });
96
+ if (new Set(prefixes).size !== prefixes.length)
97
+ fail("reader.domainPrefixes", "must not repeat a prefix");
98
+ return prefixes;
99
+ }
100
+ function parseReaderRedirects(value) {
101
+ if (value === undefined)
102
+ return undefined;
103
+ if (!Array.isArray(value) || value.length === 0)
104
+ fail("reader.redirects", "must be a non-empty array");
105
+ return value.map((entry, index) => {
106
+ const field = `reader.redirects[${index}]`;
107
+ const record = requireObject(entry, field);
108
+ rejectUnknownKeys(record, ["source", "target", "status"], field);
109
+ const status = record.status ?? "301";
110
+ if (typeof status !== "string" || !READER_REDIRECT_STATUSES.includes(status)) {
111
+ fail(`${field}.status`, `must be one of ${READER_REDIRECT_STATUSES.join(", ")}, got ${JSON.stringify(status)}`);
112
+ }
113
+ return {
114
+ source: requireString(record.source, `${field}.source`),
115
+ target: requireString(record.target, `${field}.target`),
116
+ status: status,
117
+ };
118
+ });
119
+ }
120
+ function parseReaderCustomHeaders(value) {
121
+ if (value === undefined)
122
+ return undefined;
123
+ if (!Array.isArray(value) || value.length === 0)
124
+ fail("reader.customHeaders", "must be a non-empty array");
125
+ return value.map((entry, index) => {
126
+ const field = `reader.customHeaders[${index}]`;
127
+ const record = requireObject(entry, field);
128
+ rejectUnknownKeys(record, ["pattern", "headers"], field);
129
+ const headers = requireStringRecord(record.headers, `${field}.headers`);
130
+ if (Object.keys(headers).length === 0)
131
+ fail(`${field}.headers`, "must not be empty");
132
+ return { pattern: requireString(record.pattern, `${field}.pattern`), headers };
133
+ });
134
+ }
135
+ function parseReaderPreBuildCommands(value) {
136
+ if (value === undefined)
137
+ return undefined;
138
+ if (!Array.isArray(value) || value.length === 0)
139
+ fail("reader.preBuildCommands", "must be a non-empty array of commands");
140
+ return value.map((command, index) => requireString(command, `reader.preBuildCommands[${index}]`));
141
+ }
81
142
  export function resolveRedirectSubDomainPrefix(rootDomain, source) {
82
143
  return source === rootDomain ? "" : source.slice(0, -rootDomain.length - 1);
83
144
  }
@@ -108,21 +169,8 @@ export function resolveStackName(config) {
108
169
  export function resolveStoragePreviewPrefix(config) {
109
170
  return config.storagePreviewPrefix ?? "preview/";
110
171
  }
111
- export function parseSiteConfig(raw) {
112
- const record = requireObject(raw, "site");
113
- rejectUnknownKeys(record, ["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"], "site");
114
- const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
115
- const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
116
- const brand = requireString(record.brand, "brand");
117
- const frontend = requireString(record.frontend, "frontend");
118
- if (frontend !== "pretext" && frontend !== "markus-static") {
119
- fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
120
- }
121
- const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
122
- const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
123
- const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
124
- const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
125
- const cmsRecord = requireObject(record.cms, "cms");
172
+ function parseCms(value) {
173
+ const cmsRecord = requireObject(value, "cms");
126
174
  rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects"], "cms");
127
175
  const buildComputeType = cmsRecord.buildComputeType;
128
176
  if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
@@ -198,12 +246,50 @@ export function parseSiteConfig(raw) {
198
246
  fail("cms.redirects", "must not repeat a source");
199
247
  }
200
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);
201
284
  let reader;
202
285
  if (record.reader !== undefined) {
203
286
  if (frontend !== "markus-static")
204
287
  fail("reader", `is only allowed when frontend is "markus-static"`);
205
288
  const readerRecord = requireObject(record.reader, "reader");
206
- rejectUnknownKeys(readerRecord, ["appName", "domainName", "branchName", "buildCommand", "baseDirectory", "environment"], "reader");
289
+ rejectUnknownKeys(readerRecord, ["appName", "domainName", "domainPrefixes", "branchName", "buildCommand", "baseDirectory", "environment", "redirects", "customHeaders", "preBuildCommands", "skipContentExport"], "reader");
290
+ if (readerRecord.skipContentExport !== undefined && typeof readerRecord.skipContentExport !== "boolean") {
291
+ fail("reader.skipContentExport", `must be a boolean, got ${JSON.stringify(readerRecord.skipContentExport)}`);
292
+ }
207
293
  reader = {
208
294
  appName: optionalString(readerRecord.appName, "reader.appName", APP_NAME_PATTERN),
209
295
  domainName: optionalString(readerRecord.domainName, "reader.domainName", HOST_NAME_PATTERN, "host name"),
@@ -211,13 +297,22 @@ export function parseSiteConfig(raw) {
211
297
  buildCommand: requireString(readerRecord.buildCommand, "reader.buildCommand"),
212
298
  baseDirectory: requireString(readerRecord.baseDirectory, "reader.baseDirectory"),
213
299
  environment: readerRecord.environment === undefined ? undefined : requireStringRecord(readerRecord.environment, "reader.environment"),
300
+ domainPrefixes: parseReaderDomainPrefixes(readerRecord.domainPrefixes),
301
+ redirects: parseReaderRedirects(readerRecord.redirects),
302
+ customHeaders: parseReaderCustomHeaders(readerRecord.customHeaders),
303
+ preBuildCommands: parseReaderPreBuildCommands(readerRecord.preBuildCommands),
304
+ skipContentExport: readerRecord.skipContentExport,
214
305
  };
306
+ if (reader.domainPrefixes && !reader.domainName)
307
+ fail("reader.domainPrefixes", "requires reader.domainName");
215
308
  }
216
309
  else if (frontend === "markus-static") {
217
310
  fail("reader", `is required when frontend is "markus-static"`);
218
311
  }
219
312
  let github;
220
313
  if (record.github !== undefined) {
314
+ if (cms === undefined)
315
+ fail("github", "requires cms (the CI role is scoped to the CMS app)");
221
316
  const githubRecord = requireObject(record.github, "github");
222
317
  rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
223
318
  const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
@@ -225,7 +320,7 @@ export function parseSiteConfig(raw) {
225
320
  if (repository !== `https://github.com/${owner}/${repo}`) {
226
321
  fail("github", `owner/repo must match repository ${repository}`);
227
322
  }
228
- let branches = cms.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
323
+ let branches = cms?.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
229
324
  if (githubRecord.branches !== undefined) {
230
325
  if (!Array.isArray(githubRecord.branches) || githubRecord.branches.length === 0) {
231
326
  fail("github.branches", "must be a non-empty array of branch names");
@@ -242,7 +337,7 @@ export function parseSiteConfig(raw) {
242
337
  fail("github.ciCanDeployInfra", "must be a boolean");
243
338
  github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra };
244
339
  }
245
- const hasDomain = cms.domainName !== undefined || reader?.domainName !== undefined;
340
+ const hasDomain = cms?.domainName !== undefined || reader?.domainName !== undefined;
246
341
  if (hasDomain && hostedZoneId === undefined) {
247
342
  fail("hostedZoneId", "is required when cms.domainName or reader.domainName is set");
248
343
  }
@@ -262,21 +357,7 @@ export function parseSiteConfig(raw) {
262
357
  storagePreviewPrefix,
263
358
  github,
264
359
  };
265
- const redirectValue = cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS;
266
- if (redirectValue === undefined)
267
- fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
268
- const redirectUrls = redirectValue.split(",").map((url) => url.trim());
269
- const stagingDomainName = resolveStagingDomainName(config);
270
- const listedOrigins = new Set(redirectUrls.map(originOfRedirectUrl));
271
- const requiredOrigins = [
272
- ...(resolveCmsHostName(config) ? [`https://${resolveCmsHostName(config)}`] : []),
273
- ...(stagingDomainName ? [`https://${stagingDomainName}`] : []),
274
- originOfRedirectUrl(LOCAL_DEVELOPMENT_ORIGIN),
275
- ];
276
- for (const origin of requiredOrigins) {
277
- if (!listedOrigins.has(origin)) {
278
- fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}/ (or just the origin: the backend also registers its newsroom path)`);
279
- }
280
- }
360
+ if (hasCmsApp(config))
361
+ validateOauthRedirectUrls(config);
281
362
  return config;
282
363
  }
@@ -3,6 +3,35 @@ export type CmsRedirect = {
3
3
  status: 301 | 302;
4
4
  };
5
5
 
6
+ export type ReaderRedirectStatus = "301" | "302" | "200" | "404" | "404-200";
7
+
8
+ export type ReaderRedirect = {
9
+ source: string;
10
+ target: string;
11
+ status: ReaderRedirectStatus;
12
+ };
13
+
14
+ export type ReaderCustomHeaderRule = {
15
+ pattern: string;
16
+ headers: Record<string, string>;
17
+ };
18
+
19
+ export const READER_REDIRECT_STATUSES: ReaderRedirectStatus[] = ["301", "302", "200", "404", "404-200"];
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
+
6
35
  export type AmplifyAppShellSiteConfig = {
7
36
  siteId: string;
8
37
  repository: string;
@@ -10,26 +39,19 @@ export type AmplifyAppShellSiteConfig = {
10
39
  frontend: "pretext" | "markus-static";
11
40
  hostedZoneId?: string;
12
41
  stackName?: string;
13
- cms: {
14
- appName?: string;
15
- domainName?: string;
16
- domainPrefix?: string;
17
- staging?: boolean;
18
- cognitoDomainPrefix: string;
19
- applyCognitoDomainPrefix?: boolean;
20
- buildComputeType?: "STANDARD" | "STANDARD_8GB";
21
- environment: Record<string, string>;
22
- stagingDomainName?: string;
23
- stagingDomainPrefix?: string;
24
- redirects?: CmsRedirect[];
25
- };
42
+ cms?: CmsConfig;
26
43
  reader?: {
27
44
  appName?: string;
28
45
  domainName?: string;
46
+ domainPrefixes?: string[];
29
47
  branchName?: string;
30
48
  buildCommand: string;
31
49
  baseDirectory: string;
32
50
  environment?: Record<string, string>;
51
+ redirects?: ReaderRedirect[];
52
+ customHeaders?: ReaderCustomHeaderRule[];
53
+ preBuildCommands?: string[];
54
+ skipContentExport?: boolean;
33
55
  };
34
56
  papyrusVersion: string;
35
57
  storagePreviewPrefix?: string;
@@ -41,6 +63,12 @@ export type AmplifyAppShellSiteConfig = {
41
63
  };
42
64
  };
43
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
+
44
72
  export const DEFAULT_CI_BRANCHES = ["main", "staging"];
45
73
 
46
74
  const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
@@ -126,20 +154,70 @@ function parseRedirects(value: unknown): CmsRedirect[] | undefined {
126
154
  });
127
155
  }
128
156
 
157
+ function parseReaderDomainPrefixes(value: unknown): string[] | undefined {
158
+ if (value === undefined) return undefined;
159
+ if (!Array.isArray(value) || value.length === 0) fail("reader.domainPrefixes", "must be a non-empty array");
160
+ const prefixes = (value as unknown[]).map((entry, index) => {
161
+ const field = `reader.domainPrefixes[${index}]`;
162
+ if (entry === "") return "";
163
+ return requireString(entry, field, DOMAIN_PREFIX_PATTERN, 'single DNS label, or "" for the apex');
164
+ });
165
+ if (new Set(prefixes).size !== prefixes.length) fail("reader.domainPrefixes", "must not repeat a prefix");
166
+ return prefixes;
167
+ }
168
+
169
+ function parseReaderRedirects(value: unknown): ReaderRedirect[] | undefined {
170
+ if (value === undefined) return undefined;
171
+ if (!Array.isArray(value) || value.length === 0) fail("reader.redirects", "must be a non-empty array");
172
+ return (value as unknown[]).map((entry, index) => {
173
+ const field = `reader.redirects[${index}]`;
174
+ const record = requireObject(entry, field);
175
+ rejectUnknownKeys(record, ["source", "target", "status"], field);
176
+ const status = record.status ?? "301";
177
+ if (typeof status !== "string" || !READER_REDIRECT_STATUSES.includes(status as ReaderRedirectStatus)) {
178
+ fail(`${field}.status`, `must be one of ${READER_REDIRECT_STATUSES.join(", ")}, got ${JSON.stringify(status)}`);
179
+ }
180
+ return {
181
+ source: requireString(record.source, `${field}.source`),
182
+ target: requireString(record.target, `${field}.target`),
183
+ status: status as ReaderRedirectStatus,
184
+ };
185
+ });
186
+ }
187
+
188
+ function parseReaderCustomHeaders(value: unknown): ReaderCustomHeaderRule[] | undefined {
189
+ if (value === undefined) return undefined;
190
+ if (!Array.isArray(value) || value.length === 0) fail("reader.customHeaders", "must be a non-empty array");
191
+ return (value as unknown[]).map((entry, index) => {
192
+ const field = `reader.customHeaders[${index}]`;
193
+ const record = requireObject(entry, field);
194
+ rejectUnknownKeys(record, ["pattern", "headers"], field);
195
+ const headers = requireStringRecord(record.headers, `${field}.headers`);
196
+ if (Object.keys(headers).length === 0) fail(`${field}.headers`, "must not be empty");
197
+ return { pattern: requireString(record.pattern, `${field}.pattern`), headers };
198
+ });
199
+ }
200
+
201
+ function parseReaderPreBuildCommands(value: unknown): string[] | undefined {
202
+ if (value === undefined) return undefined;
203
+ if (!Array.isArray(value) || value.length === 0) fail("reader.preBuildCommands", "must be a non-empty array of commands");
204
+ return (value as unknown[]).map((command, index) => requireString(command, `reader.preBuildCommands[${index}]`));
205
+ }
206
+
129
207
  export function resolveRedirectSubDomainPrefix(rootDomain: string, source: string): string {
130
208
  return source === rootDomain ? "" : source.slice(0, -rootDomain.length - 1);
131
209
  }
132
210
 
133
- export function isStagingEnabled(config: Pick<AmplifyAppShellSiteConfig, "cms">): boolean {
211
+ export function isStagingEnabled(config: Pick<CmsSiteConfig, "cms">): boolean {
134
212
  return config.cms.staging !== false;
135
213
  }
136
214
 
137
- export function resolveCmsHostName(config: Pick<AmplifyAppShellSiteConfig, "cms">): string | undefined {
215
+ export function resolveCmsHostName(config: Pick<CmsSiteConfig, "cms">): string | undefined {
138
216
  if (!config.cms.domainName) return undefined;
139
217
  return config.cms.domainPrefix ? `${config.cms.domainPrefix}.${config.cms.domainName}` : config.cms.domainName;
140
218
  }
141
219
 
142
- export function resolveStagingDomainName(config: Pick<AmplifyAppShellSiteConfig, "cms">): string | undefined {
220
+ export function resolveStagingDomainName(config: Pick<CmsSiteConfig, "cms">): string | undefined {
143
221
  if (!isStagingEnabled(config)) return undefined;
144
222
  if (config.cms.domainPrefix) {
145
223
  return config.cms.stagingDomainPrefix ? `${config.cms.stagingDomainPrefix}.${config.cms.domainName}` : undefined;
@@ -158,27 +236,8 @@ export function resolveStoragePreviewPrefix(config: Pick<AmplifyAppShellSiteConf
158
236
  return config.storagePreviewPrefix ?? "preview/";
159
237
  }
160
238
 
161
- export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
162
- const record = requireObject(raw, "site");
163
- rejectUnknownKeys(
164
- record,
165
- ["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
166
- "site",
167
- );
168
-
169
- const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
170
- const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
171
- const brand = requireString(record.brand, "brand");
172
- const frontend = requireString(record.frontend, "frontend");
173
- if (frontend !== "pretext" && frontend !== "markus-static") {
174
- fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
175
- }
176
- const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
177
- const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
178
- const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
179
- const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
180
-
181
- const cmsRecord = requireObject(record.cms, "cms");
239
+ function parseCms(value: unknown): CmsConfig {
240
+ const cmsRecord = requireObject(value, "cms");
182
241
  rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects"], "cms");
183
242
  const buildComputeType = cmsRecord.buildComputeType;
184
243
  if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
@@ -190,7 +249,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
190
249
  if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
191
250
  fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
192
251
  }
193
- const cms: AmplifyAppShellSiteConfig["cms"] = {
252
+ const cms: CmsConfig = {
194
253
  appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
195
254
  domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
196
255
  domainPrefix: optionalString(cmsRecord.domainPrefix, "cms.domainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
@@ -260,11 +319,57 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
260
319
  }
261
320
  }
262
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
+
263
365
  let reader: AmplifyAppShellSiteConfig["reader"];
264
366
  if (record.reader !== undefined) {
265
367
  if (frontend !== "markus-static") fail("reader", `is only allowed when frontend is "markus-static"`);
266
368
  const readerRecord = requireObject(record.reader, "reader");
267
- rejectUnknownKeys(readerRecord, ["appName", "domainName", "branchName", "buildCommand", "baseDirectory", "environment"], "reader");
369
+ rejectUnknownKeys(readerRecord, ["appName", "domainName", "domainPrefixes", "branchName", "buildCommand", "baseDirectory", "environment", "redirects", "customHeaders", "preBuildCommands", "skipContentExport"], "reader");
370
+ if (readerRecord.skipContentExport !== undefined && typeof readerRecord.skipContentExport !== "boolean") {
371
+ fail("reader.skipContentExport", `must be a boolean, got ${JSON.stringify(readerRecord.skipContentExport)}`);
372
+ }
268
373
  reader = {
269
374
  appName: optionalString(readerRecord.appName, "reader.appName", APP_NAME_PATTERN),
270
375
  domainName: optionalString(readerRecord.domainName, "reader.domainName", HOST_NAME_PATTERN, "host name"),
@@ -272,13 +377,20 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
272
377
  buildCommand: requireString(readerRecord.buildCommand, "reader.buildCommand"),
273
378
  baseDirectory: requireString(readerRecord.baseDirectory, "reader.baseDirectory"),
274
379
  environment: readerRecord.environment === undefined ? undefined : requireStringRecord(readerRecord.environment, "reader.environment"),
380
+ domainPrefixes: parseReaderDomainPrefixes(readerRecord.domainPrefixes),
381
+ redirects: parseReaderRedirects(readerRecord.redirects),
382
+ customHeaders: parseReaderCustomHeaders(readerRecord.customHeaders),
383
+ preBuildCommands: parseReaderPreBuildCommands(readerRecord.preBuildCommands),
384
+ skipContentExport: readerRecord.skipContentExport as boolean | undefined,
275
385
  };
386
+ if (reader.domainPrefixes && !reader.domainName) fail("reader.domainPrefixes", "requires reader.domainName");
276
387
  } else if (frontend === "markus-static") {
277
388
  fail("reader", `is required when frontend is "markus-static"`);
278
389
  }
279
390
 
280
391
  let github: AmplifyAppShellSiteConfig["github"];
281
392
  if (record.github !== undefined) {
393
+ if (cms === undefined) fail("github", "requires cms (the CI role is scoped to the CMS app)");
282
394
  const githubRecord = requireObject(record.github, "github");
283
395
  rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
284
396
  const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
@@ -286,7 +398,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
286
398
  if (repository !== `https://github.com/${owner}/${repo}`) {
287
399
  fail("github", `owner/repo must match repository ${repository}`);
288
400
  }
289
- let branches = cms.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
401
+ let branches = cms?.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
290
402
  if (githubRecord.branches !== undefined) {
291
403
  if (!Array.isArray(githubRecord.branches) || githubRecord.branches.length === 0) {
292
404
  fail("github.branches", "must be a non-empty array of branch names");
@@ -303,7 +415,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
303
415
  github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra as boolean };
304
416
  }
305
417
 
306
- const hasDomain = cms.domainName !== undefined || reader?.domainName !== undefined;
418
+ const hasDomain = cms?.domainName !== undefined || reader?.domainName !== undefined;
307
419
  if (hasDomain && hostedZoneId === undefined) {
308
420
  fail("hostedZoneId", "is required when cms.domainName or reader.domainName is set");
309
421
  }
@@ -325,21 +437,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
325
437
  github,
326
438
  };
327
439
 
328
- const redirectValue = cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS;
329
- if (redirectValue === undefined) fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
330
- const redirectUrls = redirectValue.split(",").map((url) => url.trim());
331
- const stagingDomainName = resolveStagingDomainName(config);
332
- const listedOrigins = new Set(redirectUrls.map(originOfRedirectUrl));
333
- const requiredOrigins = [
334
- ...(resolveCmsHostName(config) ? [`https://${resolveCmsHostName(config)}`] : []),
335
- ...(stagingDomainName ? [`https://${stagingDomainName}`] : []),
336
- originOfRedirectUrl(LOCAL_DEVELOPMENT_ORIGIN),
337
- ];
338
- for (const origin of requiredOrigins) {
339
- if (!listedOrigins.has(origin)) {
340
- fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}/ (or just the origin: the backend also registers its newsroom path)`);
341
- }
342
- }
440
+ if (hasCmsApp(config)) validateOauthRedirectUrls(config);
343
441
 
344
442
  return config;
345
443
  }
package/package.json CHANGED
@@ -302,7 +302,7 @@
302
302
  "optional": true
303
303
  }
304
304
  },
305
- "version": "1.0.0-next.40",
305
+ "version": "1.0.0-next.42",
306
306
  "dependencies": {
307
307
  "@aws-amplify/adapter-nextjs": "^1.7.3",
308
308
  "@aws-sdk/client-cognito-identity-provider": "3.1045.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-next.40",
2
+ "version": "1.0.0-next.42",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",