@anthusai/papyrus 1.0.0-next.8 → 1.0.0

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.
Files changed (209) hide show
  1. package/amplify/auth/redirect-urls.js +54 -0
  2. package/amplify/auth/redirect-urls.ts +66 -0
  3. package/amplify/auth/resource.js +20 -5
  4. package/amplify/auth/resource.ts +27 -5
  5. package/amplify/data/resource.js +42 -80
  6. package/amplify/data/resource.ts +42 -80
  7. package/amplify/data/schema.js +45 -60
  8. package/amplify/data/schema.ts +47 -60
  9. package/amplify/functions/category-action/handler.ts +5 -3
  10. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
  11. package/amplify/functions/console-chat-responder/resource.js +0 -7
  12. package/amplify/functions/console-chat-responder/resource.ts +0 -8
  13. package/amplify/functions/console-chat-responder/src/main.rs +0 -9
  14. package/amplify/functions/content-actions/handler.py +143 -1
  15. package/amplify/functions/content-actions/requirements.txt +2 -1
  16. package/amplify/functions/content-actions/resource.js +6 -1
  17. package/amplify/functions/content-actions/resource.ts +9 -2
  18. package/amplify/functions/renditions/handler.py +120 -0
  19. package/amplify/functions/renditions/requirements.txt +4 -0
  20. package/amplify/functions/shared/python-bundle.js +42 -2
  21. package/amplify/functions/shared/python-bundle.ts +47 -2
  22. package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
  23. package/amplify/inbound-email/ses-intake.js +86 -0
  24. package/amplify/inbound-email/ses-intake.ts +110 -0
  25. package/amplify/plugins/registry.js +19 -0
  26. package/amplify/plugins/registry.ts +23 -0
  27. package/amplify/plugins/signup/function/handler.py +200 -0
  28. package/amplify/plugins/signup/function/resource.js +27 -0
  29. package/amplify/plugins/signup/function/resource.ts +35 -0
  30. package/amplify/plugins/signup/papyrus.plugin.js +59 -0
  31. package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
  32. package/amplify/plugins/signup/schema.js +40 -0
  33. package/amplify/plugins/signup/schema.ts +41 -0
  34. package/amplify/plugins/signup/waf.js +48 -0
  35. package/amplify/plugins/signup/waf.ts +51 -0
  36. package/amplify/plugins/types.js +1 -0
  37. package/amplify/plugins/types.ts +39 -0
  38. package/amplify/plugins/wire.js +24 -0
  39. package/amplify/plugins/wire.ts +26 -0
  40. package/amplify/publication/default-publication.js +18 -0
  41. package/amplify/publication/default-publication.ts +20 -0
  42. package/amplify/publication/invalidation-alarm.js +26 -0
  43. package/amplify/publication/invalidation-alarm.ts +30 -0
  44. package/amplify/publication/redirect-and-index.js +37 -0
  45. package/amplify/publication/renditions-function.js +74 -0
  46. package/amplify/publication/renditions-function.ts +112 -0
  47. package/amplify/publication/renditions-settings.js +51 -0
  48. package/amplify/publication/renditions-settings.ts +58 -0
  49. package/amplify/publication/reproject-on-deploy.js +73 -0
  50. package/amplify/publication/reproject-on-deploy.ts +78 -0
  51. package/amplify/publication/s3-projection-settings.js +67 -0
  52. package/amplify/publication/s3-projection-settings.ts +101 -0
  53. package/amplify/publication/s3-projection.js +151 -0
  54. package/amplify/publication/s3-projection.ts +201 -0
  55. package/amplify/resolver-waves.js +26 -0
  56. package/amplify/resolver-waves.ts +34 -0
  57. package/amplify/revalidate-invalidation-settings.js +30 -0
  58. package/amplify/revalidate-invalidation-settings.ts +35 -0
  59. package/amplify/revalidate-secret-policy.js +13 -0
  60. package/amplify/revalidate-secret-policy.ts +19 -0
  61. package/amplify/site-backend-config.js +65 -1
  62. package/amplify/site-backend-config.ts +153 -2
  63. package/amplify/site-backend-names.js +103 -0
  64. package/amplify/site-backend-names.ts +166 -0
  65. package/amplify/site-backend.js +143 -176
  66. package/amplify/site-backend.ts +181 -201
  67. package/amplify/storage-backups/construct.js +31 -0
  68. package/amplify/storage-backups/construct.ts +47 -0
  69. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
  70. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  71. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
  72. package/app/[year]/[month]/[day]/page.tsx +1 -1
  73. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
  74. package/app/api/archive/editions/route.ts +1 -1
  75. package/app/api/cyclotron/priming-proof/route.ts +11 -0
  76. package/app/api/media/[...path]/route.ts +5 -16
  77. package/app/api/revalidate/route.ts +15 -7
  78. package/app/archive/page.tsx +1 -1
  79. package/app/articles/[slug]/page.tsx +7 -5
  80. package/app/blog-rhythm.css +278 -0
  81. package/app/cyclotron/page.tsx +13 -0
  82. package/app/globals.css +2 -0
  83. package/app/layout.tsx +16 -0
  84. package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
  85. package/app/newsroom/[section]/page.tsx +6 -3
  86. package/app/newsroom/layout.tsx +4 -9
  87. package/app/newsroom/settings/page.tsx +6 -0
  88. package/app/newsroom/work/page.tsx +10 -0
  89. package/app/page.tsx +22 -8
  90. package/app/robots.ts +2 -5
  91. package/app/tailwind.css +1 -0
  92. package/bin/papyrus-infra.mjs +3 -3
  93. package/components/amplify-client-provider.tsx +5 -22
  94. package/components/archive-shell.tsx +2 -22
  95. package/components/article-video.tsx +152 -5
  96. package/components/blog-page-background.tsx +23 -6
  97. package/components/news-desk-page.tsx +4 -15
  98. package/components/news-desk-taxonomy-client.ts +90 -3
  99. package/components/newspaper.tsx +6 -2
  100. package/components/newsroom-access-gate.tsx +2 -1
  101. package/components/newsroom-board-view.tsx +99 -0
  102. package/components/newsroom-client-shell.tsx +12 -0
  103. package/components/newsroom-curation-provider.tsx +17 -0
  104. package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
  105. package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
  106. package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
  107. package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
  108. package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
  109. package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
  110. package/components/newsroom-newsprint-board.css +48 -0
  111. package/components/newsroom-ops-overview.tsx +28 -11
  112. package/components/newsroom-ops-shell.tsx +7 -1
  113. package/components/newsroom-organizer-shell.tsx +130 -0
  114. package/components/newsroom-organizer.css +63 -0
  115. package/components/newsroom-references-view.tsx +92 -8
  116. package/components/papyrus-console-shell.tsx +4 -2
  117. package/components/pictogram-figure.tsx +9 -2
  118. package/components/presentation-header.tsx +135 -0
  119. package/components/presentation-rhythm-hrule.tsx +17 -0
  120. package/components/priming-proof-page.tsx +92 -0
  121. package/components/publication-cyclotron-panel.tsx +57 -0
  122. package/components/reader-auth-control.tsx +8 -5
  123. package/components/reference-curation-controls.tsx +77 -0
  124. package/components/reference-cyclotron-status.tsx +109 -0
  125. package/components/reference-priming.tsx +18 -0
  126. package/components/reference-relevance-review.tsx +75 -0
  127. package/components/settings-page.tsx +2 -1
  128. package/components/signup-entries-panel.tsx +94 -0
  129. package/components/topic-steering-workspace.tsx +78 -6
  130. package/components/use-blog-header-obstacles.ts +81 -0
  131. package/components/use-brand-fonts-ready.ts +28 -0
  132. package/components/use-near-viewport.ts +30 -0
  133. package/components/use-rhythm-overlay.ts +29 -0
  134. package/infra/amplify-app-shell.js +219 -71
  135. package/infra/amplify-app-shell.ts +231 -77
  136. package/infra/build-specs.js +23 -8
  137. package/infra/build-specs.ts +27 -11
  138. package/infra/site-config.js +277 -34
  139. package/infra/site-config.ts +340 -43
  140. package/lib/amplify-outputs-path.ts +5 -1
  141. package/lib/amplify-server-runtime.ts +36 -1
  142. package/lib/approved-priming-proof-reports.ts +6 -0
  143. package/lib/blog-feature-solver.ts +459 -0
  144. package/lib/blog-rhythm.ts +134 -0
  145. package/lib/brand-fonts-watch.ts +43 -0
  146. package/lib/brand-fonts.ts +112 -0
  147. package/lib/cached-content-repository.ts +12 -0
  148. package/lib/category-repository.ts +43 -93
  149. package/lib/content-item-lifecycle.ts +55 -0
  150. package/lib/content-repository.ts +3 -0
  151. package/lib/content-types.ts +4 -0
  152. package/lib/curation-client.ts +61 -0
  153. package/lib/edition-routes.ts +31 -29
  154. package/lib/graphql-content-repository.ts +56 -22
  155. package/lib/media-route-response.ts +45 -0
  156. package/lib/newspaper-layout.ts +3 -2
  157. package/lib/newsroom-base-path.ts +12 -3
  158. package/lib/newsroom-board.ts +99 -0
  159. package/lib/newsroom-category-drilldown.ts +3 -2
  160. package/lib/newsroom-forum-routes.ts +6 -4
  161. package/lib/newsroom-index-filters.ts +14 -10
  162. package/lib/newsroom-nav.ts +3 -1
  163. package/lib/newsroom-organizer-nav.ts +37 -0
  164. package/lib/newsroom-section-routing.ts +20 -0
  165. package/lib/oauth-redirect-priority.ts +40 -0
  166. package/lib/papyrus-web-locations.ts +7 -6
  167. package/lib/plugin-settings.ts +11 -0
  168. package/lib/presentation-footer.ts +3 -2
  169. package/lib/pretext-layout.ts +12 -3
  170. package/lib/priming-proof-report.ts +89 -0
  171. package/lib/prospectus-site-identity.ts +303 -0
  172. package/lib/publication-cyclotron.ts +74 -0
  173. package/lib/rate-limit-retry.ts +55 -0
  174. package/lib/reader-base-path.ts +24 -0
  175. package/lib/reader-cache-lifetime.ts +5 -0
  176. package/lib/reader-static-params.ts +85 -4
  177. package/lib/reader-storage-url.ts +9 -12
  178. package/lib/reference-policy.ts +5 -0
  179. package/lib/relevance-decisions.ts +103 -0
  180. package/lib/relevance-status.ts +63 -0
  181. package/lib/renderer.ts +7 -0
  182. package/lib/revalidate-prewarm.ts +53 -0
  183. package/lib/revalidate-secret.ts +59 -0
  184. package/lib/robots-policy.ts +14 -0
  185. package/lib/root-route-redirect.ts +16 -0
  186. package/lib/semantic-graph.ts +7 -0
  187. package/lib/shared-credentials-provider.ts +61 -0
  188. package/lib/signup-csv.ts +24 -0
  189. package/lib/site-brand.ts +59 -0
  190. package/lib/staging-gated-path.ts +23 -0
  191. package/lib/staging-preview-object.ts +21 -5
  192. package/lib/test-editor-override.ts +4 -0
  193. package/lib/video-loading.ts +22 -0
  194. package/lib/video-script.ts +14 -0
  195. package/middleware.ts +11 -19
  196. package/package.json +64 -1
  197. package/renderers/pretext/article-page.tsx +39 -3
  198. package/renderers/pretext/presentation-shared.tsx +113 -0
  199. package/renderers/pretext/presentation-shell.tsx +15 -102
  200. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  201. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  202. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  203. package/routes.manifest.json +68 -12
  204. package/src/sync.mjs +30 -6
  205. package/src/with-papyrus.d.mts +9 -0
  206. package/src/with-papyrus.mjs +48 -3
  207. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  208. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  209. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
@@ -1,13 +1,50 @@
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_.-]+$/;
4
8
  const HOSTED_ZONE_ID_PATTERN = /^Z[A-Z0-9]+$/;
5
9
  const HOST_NAME_PATTERN = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
10
+ const DOMAIN_PREFIX_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
6
11
  const APP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 _.-]*$/;
12
+ const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
7
13
  const BRANCH_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9/_.-]*$/;
8
14
  const PAPYRUS_VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+\.\d+)?$/;
9
15
  const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
10
16
  const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
17
+ const COGNITO_DOMAIN_PREFIX_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
18
+ const COGNITO_RESERVED_WORDS = ["aws", "amazon", "cognito"];
19
+ const PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN = /^arn:aws:execute-api:[a-z0-9-]+:\d{12}:[a-z0-9]+\/api$/;
20
+ const TEMPLATE_MANAGED_ENVIRONMENT_KEYS = ["PAPYRUS_COGNITO_DOMAIN_PREFIX", "PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX", "PAPYRUS_DISABLE_GOOGLE_OAUTH", "PAPYRUS_REVALIDATE_SECRET_PARAMETER", "PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT"];
21
+ /** Routes Prospectus admits for a registered Papyrus site (Prospectus accounts/papyrus-sites.ts). */
22
+ export const PROSPECTUS_PAPYRUS_SITE_ROUTES = [
23
+ "GET/v1/identities/me",
24
+ "POST/v1/identities/google/challenge",
25
+ "POST/v1/identities/google/resolve",
26
+ "GET/v1/accounts",
27
+ "GET/v1/accounts/*",
28
+ "POST/v1/accounts/*/authorize",
29
+ "GET/v1/account-directory",
30
+ "GET/v1/account-directory/*",
31
+ ];
32
+ export function resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn) {
33
+ const [, , , region, , apiPath] = prospectusAccountsApiStageArn.split(":");
34
+ const [apiId, stageName] = apiPath.split("/");
35
+ return `https://${apiId}.execute-api.${region}.amazonaws.com/${stageName}/`;
36
+ }
37
+ function originOfRedirectUrl(value) {
38
+ try {
39
+ return new URL(value).origin;
40
+ }
41
+ catch {
42
+ return value;
43
+ }
44
+ }
45
+ export function resolveRevalidateSecretParameterName(config) {
46
+ return `/papyrus/${config.siteId}/revalidate-secret`;
47
+ }
11
48
  const LOCAL_DEVELOPMENT_ORIGIN = "http://localhost:3001/";
12
49
  function fail(field, problem) {
13
50
  throw new Error(`site.json: ${field} ${problem}`);
@@ -46,18 +83,221 @@ function rejectUnknownKeys(record, allowed, field) {
46
83
  fail(`${field}.${key}`, `is not a known field (known: ${allowed.join(", ")})`);
47
84
  }
48
85
  }
86
+ function parseRedirects(value) {
87
+ if (value === undefined)
88
+ return undefined;
89
+ if (!Array.isArray(value) || value.length === 0)
90
+ fail("cms.redirects", "must be a non-empty array");
91
+ return value.map((entry, index) => {
92
+ const field = `cms.redirects[${index}]`;
93
+ const record = requireObject(entry, field);
94
+ rejectUnknownKeys(record, ["source", "status"], field);
95
+ const source = requireString(record.source, `${field}.source`, HOST_NAME_PATTERN, "host name without scheme or path");
96
+ const status = record.status ?? 301;
97
+ if (status !== 301 && status !== 302)
98
+ fail(`${field}.status`, `must be 301 or 302, got ${JSON.stringify(status)}`);
99
+ return { source, status };
100
+ });
101
+ }
102
+ function parseReaderDomainPrefixes(value) {
103
+ if (value === undefined)
104
+ return undefined;
105
+ if (!Array.isArray(value) || value.length === 0)
106
+ fail("reader.domainPrefixes", "must be a non-empty array");
107
+ const prefixes = value.map((entry, index) => {
108
+ const field = `reader.domainPrefixes[${index}]`;
109
+ if (entry === "")
110
+ return "";
111
+ return requireString(entry, field, DOMAIN_PREFIX_PATTERN, 'single DNS label, or "" for the apex');
112
+ });
113
+ if (new Set(prefixes).size !== prefixes.length)
114
+ fail("reader.domainPrefixes", "must not repeat a prefix");
115
+ return prefixes;
116
+ }
117
+ function parseReaderRedirects(value) {
118
+ if (value === undefined)
119
+ return undefined;
120
+ if (!Array.isArray(value) || value.length === 0)
121
+ fail("reader.redirects", "must be a non-empty array");
122
+ return value.map((entry, index) => {
123
+ const field = `reader.redirects[${index}]`;
124
+ const record = requireObject(entry, field);
125
+ rejectUnknownKeys(record, ["source", "target", "status"], field);
126
+ const status = record.status ?? "301";
127
+ if (typeof status !== "string" || !READER_REDIRECT_STATUSES.includes(status)) {
128
+ fail(`${field}.status`, `must be one of ${READER_REDIRECT_STATUSES.join(", ")}, got ${JSON.stringify(status)}`);
129
+ }
130
+ return {
131
+ source: requireString(record.source, `${field}.source`),
132
+ target: requireString(record.target, `${field}.target`),
133
+ status: status,
134
+ };
135
+ });
136
+ }
137
+ function parseReaderCustomHeaders(value) {
138
+ if (value === undefined)
139
+ return undefined;
140
+ if (!Array.isArray(value) || value.length === 0)
141
+ fail("reader.customHeaders", "must be a non-empty array");
142
+ return value.map((entry, index) => {
143
+ const field = `reader.customHeaders[${index}]`;
144
+ const record = requireObject(entry, field);
145
+ rejectUnknownKeys(record, ["pattern", "headers"], field);
146
+ const headers = requireStringRecord(record.headers, `${field}.headers`);
147
+ if (Object.keys(headers).length === 0)
148
+ fail(`${field}.headers`, "must not be empty");
149
+ return { pattern: requireString(record.pattern, `${field}.pattern`), headers };
150
+ });
151
+ }
152
+ function parseReaderPreBuildCommands(value) {
153
+ if (value === undefined)
154
+ return undefined;
155
+ if (!Array.isArray(value) || value.length === 0)
156
+ fail("reader.preBuildCommands", "must be a non-empty array of commands");
157
+ return value.map((command, index) => requireString(command, `reader.preBuildCommands[${index}]`));
158
+ }
159
+ export function resolveRedirectSubDomainPrefix(rootDomain, source) {
160
+ return source === rootDomain ? "" : source.slice(0, -rootDomain.length - 1);
161
+ }
162
+ export function isStagingEnabled(config) {
163
+ return config.cms.staging !== false;
164
+ }
165
+ export function resolveCmsHostName(config) {
166
+ if (!config.cms.domainName)
167
+ return undefined;
168
+ return config.cms.domainPrefix ? `${config.cms.domainPrefix}.${config.cms.domainName}` : config.cms.domainName;
169
+ }
49
170
  export function resolveStagingDomainName(config) {
171
+ if (!isStagingEnabled(config))
172
+ return undefined;
173
+ if (config.cms.domainPrefix) {
174
+ return config.cms.stagingDomainPrefix ? `${config.cms.stagingDomainPrefix}.${config.cms.domainName}` : undefined;
175
+ }
50
176
  if (config.cms.stagingDomainName)
51
177
  return config.cms.stagingDomainName;
178
+ if (!config.cms.domainName)
179
+ return undefined;
52
180
  const [, ...rest] = config.cms.domainName.split(".");
53
181
  return ["staging", ...rest].join(".");
54
182
  }
183
+ export function resolveStackName(config) {
184
+ return config.stackName ?? `amplify-app-shell-${config.siteId}`;
185
+ }
55
186
  export function resolveStoragePreviewPrefix(config) {
56
187
  return config.storagePreviewPrefix ?? "preview/";
57
188
  }
189
+ function parseStagingPreview(value) {
190
+ if (value === undefined)
191
+ return undefined;
192
+ const record = requireObject(value, "cms.stagingPreview");
193
+ rejectUnknownKeys(record, ["buildCommand", "baseDirectory"], "cms.stagingPreview");
194
+ return {
195
+ buildCommand: requireString(record.buildCommand, "cms.stagingPreview.buildCommand"),
196
+ baseDirectory: requireString(record.baseDirectory, "cms.stagingPreview.baseDirectory"),
197
+ };
198
+ }
199
+ function parseCms(value) {
200
+ const cmsRecord = requireObject(value, "cms");
201
+ rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview", "prospectusAccountsApiStageArn"], "cms");
202
+ const buildComputeType = cmsRecord.buildComputeType;
203
+ if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
204
+ fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
205
+ }
206
+ if (cmsRecord.staging !== undefined && typeof cmsRecord.staging !== "boolean") {
207
+ fail("cms.staging", `must be a boolean, got ${JSON.stringify(cmsRecord.staging)}`);
208
+ }
209
+ if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
210
+ fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
211
+ }
212
+ const cms = {
213
+ appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
214
+ domainName: optionalString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
215
+ domainPrefix: optionalString(cmsRecord.domainPrefix, "cms.domainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
216
+ staging: cmsRecord.staging,
217
+ cognitoDomainPrefix: requireString(cmsRecord.cognitoDomainPrefix, "cms.cognitoDomainPrefix", COGNITO_DOMAIN_PREFIX_PATTERN, "lowercase letters, digits and hyphens; globally unique per region"),
218
+ applyCognitoDomainPrefix: cmsRecord.applyCognitoDomainPrefix,
219
+ buildComputeType: buildComputeType,
220
+ environment: requireStringRecord(cmsRecord.environment, "cms.environment"),
221
+ stagingDomainName: optionalString(cmsRecord.stagingDomainName, "cms.stagingDomainName", HOST_NAME_PATTERN, "host name"),
222
+ stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
223
+ redirects: parseRedirects(cmsRecord.redirects),
224
+ stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
225
+ prospectusAccountsApiStageArn: optionalString(cmsRecord.prospectusAccountsApiStageArn, "cms.prospectusAccountsApiStageArn", PROSPECTUS_ACCOUNTS_API_STAGE_ARN_PATTERN, "arn:aws:execute-api:<region>:<account>:<api id>/api"),
226
+ };
227
+ const reservedWord = COGNITO_RESERVED_WORDS.find((word) => cms.cognitoDomainPrefix.includes(word));
228
+ if (reservedWord) {
229
+ fail("cms.cognitoDomainPrefix", `must not contain "${reservedWord}" (Cognito reserves it): ${JSON.stringify(cms.cognitoDomainPrefix)}`);
230
+ }
231
+ for (const key of TEMPLATE_MANAGED_ENVIRONMENT_KEYS) {
232
+ if (key in cms.environment) {
233
+ fail(`cms.environment.${key}`, "is managed by the template: Google sign-in is always on, the Cognito domain comes from cms.cognitoDomainPrefix, the revalidation secret parameter is derived from siteId and the Prospectus endpoint comes from cms.prospectusAccountsApiStageArn");
234
+ }
235
+ }
236
+ if (cms.staging === false && cms.stagingDomainName) {
237
+ fail("cms.stagingDomainName", "must not be set when cms.staging is false");
238
+ }
239
+ if (cms.stagingDomainName && !cms.domainName) {
240
+ fail("cms.stagingDomainName", "requires cms.domainName");
241
+ }
242
+ if (cms.domainPrefix && !cms.domainName) {
243
+ fail("cms.domainPrefix", "requires cms.domainName (the root domain)");
244
+ }
245
+ if (cms.stagingDomainPrefix && !cms.domainPrefix) {
246
+ fail("cms.stagingDomainPrefix", "requires cms.domainPrefix");
247
+ }
248
+ if (cms.domainPrefix && cms.stagingDomainName) {
249
+ fail("cms.stagingDomainName", "must not be set with cms.domainPrefix: use cms.stagingDomainPrefix");
250
+ }
251
+ if (cms.staging === false && cms.stagingDomainPrefix) {
252
+ fail("cms.stagingDomainPrefix", "must not be set when cms.staging is false");
253
+ }
254
+ if (cms.domainPrefix && cms.staging !== false && !cms.stagingDomainPrefix) {
255
+ fail("cms.stagingDomainPrefix", "is required when cms.domainPrefix is set and staging is enabled: the staging host is never derived under a shared root domain");
256
+ }
257
+ if (cms.domainPrefix && cms.stagingDomainPrefix === cms.domainPrefix) {
258
+ fail("cms.stagingDomainPrefix", "must differ from cms.domainPrefix");
259
+ }
260
+ if (cms.redirects) {
261
+ if (!cms.domainPrefix)
262
+ fail("cms.redirects", "requires cms.domainPrefix (redirects target the primary host under a shared root domain)");
263
+ cms.redirects.forEach((redirect, index) => {
264
+ const field = `cms.redirects[${index}].source`;
265
+ const root = cms.domainName;
266
+ if (redirect.source !== root && !(redirect.source.endsWith(`.${root}`) && DOMAIN_PREFIX_PATTERN.test(redirect.source.slice(0, -root.length - 1)))) {
267
+ fail(field, `must be the root domain ${root} or a single label under it: ${JSON.stringify(redirect.source)}`);
268
+ }
269
+ if (redirect.source === resolveCmsHostName({ cms }))
270
+ fail(field, "must differ from the primary host");
271
+ if (redirect.source === resolveStagingDomainName({ cms }))
272
+ fail(field, "must differ from the staging host");
273
+ });
274
+ if (new Set(cms.redirects.map((redirect) => redirect.source)).size !== cms.redirects.length) {
275
+ fail("cms.redirects", "must not repeat a source");
276
+ }
277
+ }
278
+ return cms;
279
+ }
280
+ function validateOauthRedirectUrls(config) {
281
+ const redirectValue = config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS;
282
+ if (redirectValue === undefined)
283
+ fail("config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
284
+ const redirectUrls = redirectValue.split(",").map((url) => url.trim());
285
+ const stagingDomainName = resolveStagingDomainName(config);
286
+ const listedOrigins = new Set(redirectUrls.map(originOfRedirectUrl));
287
+ const requiredOrigins = [
288
+ ...(resolveCmsHostName(config) ? [`https://${resolveCmsHostName(config)}`] : []),
289
+ ...(stagingDomainName ? [`https://${stagingDomainName}`] : []),
290
+ originOfRedirectUrl(LOCAL_DEVELOPMENT_ORIGIN),
291
+ ];
292
+ for (const origin of requiredOrigins) {
293
+ if (!listedOrigins.has(origin)) {
294
+ fail("config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}/ (or just the origin: the backend also registers its newsroom path)`);
295
+ }
296
+ }
297
+ }
58
298
  export function parseSiteConfig(raw) {
59
299
  const record = requireObject(raw, "site");
60
- rejectUnknownKeys(record, ["siteId", "repository", "brand", "frontend", "hostedZoneId", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"], "site");
300
+ rejectUnknownKeys(record, ["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"], "site");
61
301
  const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
62
302
  const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
63
303
  const brand = requireString(record.brand, "brand");
@@ -65,42 +305,49 @@ export function parseSiteConfig(raw) {
65
305
  if (frontend !== "pretext" && frontend !== "markus-static") {
66
306
  fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
67
307
  }
68
- const hostedZoneId = requireString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
308
+ const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
309
+ const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
69
310
  const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
70
311
  const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
71
- const cmsRecord = requireObject(record.cms, "cms");
72
- rejectUnknownKeys(cmsRecord, ["appName", "domainName", "buildComputeType", "environment", "stagingDomainName"], "cms");
73
- const buildComputeType = cmsRecord.buildComputeType;
74
- if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
75
- fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
76
- }
77
- const cms = {
78
- appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
79
- domainName: requireString(cmsRecord.domainName, "cms.domainName", HOST_NAME_PATTERN, "host name"),
80
- buildComputeType: buildComputeType,
81
- environment: requireStringRecord(cmsRecord.environment, "cms.environment"),
82
- stagingDomainName: optionalString(cmsRecord.stagingDomainName, "cms.stagingDomainName", HOST_NAME_PATTERN, "host name"),
83
- };
312
+ const cms = record.cms === undefined && frontend === "markus-static" ? undefined : parseCms(record.cms);
84
313
  let reader;
85
314
  if (record.reader !== undefined) {
86
315
  if (frontend !== "markus-static")
87
316
  fail("reader", `is only allowed when frontend is "markus-static"`);
88
317
  const readerRecord = requireObject(record.reader, "reader");
89
- rejectUnknownKeys(readerRecord, ["appName", "domainName", "branchName", "buildCommand", "baseDirectory", "environment"], "reader");
318
+ rejectUnknownKeys(readerRecord, ["appName", "domainName", "domainPrefixes", "branchName", "buildCommand", "baseDirectory", "environment", "redirects", "customHeaders", "preBuildCommands", "skipContentExport"], "reader");
319
+ if (readerRecord.skipContentExport !== undefined && typeof readerRecord.skipContentExport !== "boolean") {
320
+ fail("reader.skipContentExport", `must be a boolean, got ${JSON.stringify(readerRecord.skipContentExport)}`);
321
+ }
90
322
  reader = {
91
323
  appName: optionalString(readerRecord.appName, "reader.appName", APP_NAME_PATTERN),
92
- domainName: requireString(readerRecord.domainName, "reader.domainName", HOST_NAME_PATTERN, "host name"),
324
+ domainName: optionalString(readerRecord.domainName, "reader.domainName", HOST_NAME_PATTERN, "host name"),
93
325
  branchName: optionalString(readerRecord.branchName, "reader.branchName", BRANCH_NAME_PATTERN),
94
326
  buildCommand: requireString(readerRecord.buildCommand, "reader.buildCommand"),
95
327
  baseDirectory: requireString(readerRecord.baseDirectory, "reader.baseDirectory"),
96
328
  environment: readerRecord.environment === undefined ? undefined : requireStringRecord(readerRecord.environment, "reader.environment"),
329
+ domainPrefixes: parseReaderDomainPrefixes(readerRecord.domainPrefixes),
330
+ redirects: parseReaderRedirects(readerRecord.redirects),
331
+ customHeaders: parseReaderCustomHeaders(readerRecord.customHeaders),
332
+ preBuildCommands: parseReaderPreBuildCommands(readerRecord.preBuildCommands),
333
+ skipContentExport: readerRecord.skipContentExport,
97
334
  };
335
+ if (reader.domainPrefixes && !reader.domainName)
336
+ fail("reader.domainPrefixes", "requires reader.domainName");
337
+ }
338
+ else if (frontend === "markus-static" && cms === undefined) {
339
+ fail("reader", `is required when frontend is "markus-static" and there is no cms section`);
340
+ }
341
+ if (cms?.stagingPreview !== undefined && frontend !== "markus-static") {
342
+ fail("cms.stagingPreview", `is only allowed when frontend is "markus-static"`);
98
343
  }
99
- else if (frontend === "markus-static") {
100
- fail("reader", `is required when frontend is "markus-static"`);
344
+ if (cms?.stagingPreview !== undefined && cms.staging === false) {
345
+ fail("cms.stagingPreview", "must not be set when cms.staging is false");
101
346
  }
102
347
  let github;
103
348
  if (record.github !== undefined) {
349
+ if (cms === undefined)
350
+ fail("github", "requires cms (the CI role is scoped to the CMS app)");
104
351
  const githubRecord = requireObject(record.github, "github");
105
352
  rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
106
353
  const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
@@ -108,7 +355,7 @@ export function parseSiteConfig(raw) {
108
355
  if (repository !== `https://github.com/${owner}/${repo}`) {
109
356
  fail("github", `owner/repo must match repository ${repository}`);
110
357
  }
111
- let branches = DEFAULT_CI_BRANCHES;
358
+ let branches = cms?.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
112
359
  if (githubRecord.branches !== undefined) {
113
360
  if (!Array.isArray(githubRecord.branches) || githubRecord.branches.length === 0) {
114
361
  fail("github.branches", "must be a non-empty array of branch names");
@@ -125,31 +372,27 @@ export function parseSiteConfig(raw) {
125
372
  fail("github.ciCanDeployInfra", "must be a boolean");
126
373
  github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra };
127
374
  }
375
+ const hasDomain = cms?.domainName !== undefined || reader?.domainName !== undefined;
376
+ if (hasDomain && hostedZoneId === undefined) {
377
+ fail("hostedZoneId", "is required when cms.domainName or reader.domainName is set");
378
+ }
379
+ if (!hasDomain && hostedZoneId !== undefined) {
380
+ fail("hostedZoneId", "is only allowed when cms.domainName or reader.domainName is set");
381
+ }
128
382
  const config = {
129
383
  siteId,
130
384
  repository,
131
385
  brand,
132
386
  frontend,
133
387
  hostedZoneId,
388
+ stackName,
134
389
  cms,
135
390
  reader,
136
391
  papyrusVersion,
137
392
  storagePreviewPrefix,
138
393
  github,
139
394
  };
140
- const redirectValue = cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS;
141
- if (redirectValue === undefined)
142
- fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
143
- const redirectUrls = redirectValue.split(",").map((url) => url.trim());
144
- const requiredOrigins = [
145
- `https://${cms.domainName}/`,
146
- `https://${resolveStagingDomainName(config)}/`,
147
- LOCAL_DEVELOPMENT_ORIGIN,
148
- ];
149
- for (const origin of requiredOrigins) {
150
- if (!redirectUrls.includes(origin)) {
151
- fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}`);
152
- }
153
- }
395
+ if (hasCmsApp(config))
396
+ validateOauthRedirectUrls(config);
154
397
  return config;
155
398
  }