@anthusai/papyrus 1.0.0-next.9 → 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 (208) 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/components/amplify-client-provider.tsx +5 -22
  93. package/components/archive-shell.tsx +2 -22
  94. package/components/article-video.tsx +152 -5
  95. package/components/blog-page-background.tsx +23 -6
  96. package/components/news-desk-page.tsx +4 -15
  97. package/components/news-desk-taxonomy-client.ts +90 -3
  98. package/components/newspaper.tsx +6 -2
  99. package/components/newsroom-access-gate.tsx +2 -1
  100. package/components/newsroom-board-view.tsx +99 -0
  101. package/components/newsroom-client-shell.tsx +12 -0
  102. package/components/newsroom-curation-provider.tsx +17 -0
  103. package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
  104. package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
  105. package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
  106. package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
  107. package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
  108. package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
  109. package/components/newsroom-newsprint-board.css +48 -0
  110. package/components/newsroom-ops-overview.tsx +28 -11
  111. package/components/newsroom-ops-shell.tsx +7 -1
  112. package/components/newsroom-organizer-shell.tsx +130 -0
  113. package/components/newsroom-organizer.css +63 -0
  114. package/components/newsroom-references-view.tsx +92 -8
  115. package/components/papyrus-console-shell.tsx +4 -2
  116. package/components/pictogram-figure.tsx +9 -2
  117. package/components/presentation-header.tsx +135 -0
  118. package/components/presentation-rhythm-hrule.tsx +17 -0
  119. package/components/priming-proof-page.tsx +92 -0
  120. package/components/publication-cyclotron-panel.tsx +57 -0
  121. package/components/reader-auth-control.tsx +8 -5
  122. package/components/reference-curation-controls.tsx +77 -0
  123. package/components/reference-cyclotron-status.tsx +109 -0
  124. package/components/reference-priming.tsx +18 -0
  125. package/components/reference-relevance-review.tsx +75 -0
  126. package/components/settings-page.tsx +2 -1
  127. package/components/signup-entries-panel.tsx +94 -0
  128. package/components/topic-steering-workspace.tsx +78 -6
  129. package/components/use-blog-header-obstacles.ts +81 -0
  130. package/components/use-brand-fonts-ready.ts +28 -0
  131. package/components/use-near-viewport.ts +30 -0
  132. package/components/use-rhythm-overlay.ts +29 -0
  133. package/infra/amplify-app-shell.js +148 -27
  134. package/infra/amplify-app-shell.ts +160 -31
  135. package/infra/build-specs.js +23 -7
  136. package/infra/build-specs.ts +27 -10
  137. package/infra/site-config.js +248 -36
  138. package/infra/site-config.ts +313 -51
  139. package/lib/amplify-outputs-path.ts +5 -1
  140. package/lib/amplify-server-runtime.ts +36 -1
  141. package/lib/approved-priming-proof-reports.ts +6 -0
  142. package/lib/blog-feature-solver.ts +459 -0
  143. package/lib/blog-rhythm.ts +134 -0
  144. package/lib/brand-fonts-watch.ts +43 -0
  145. package/lib/brand-fonts.ts +112 -0
  146. package/lib/cached-content-repository.ts +12 -0
  147. package/lib/category-repository.ts +43 -93
  148. package/lib/content-item-lifecycle.ts +55 -0
  149. package/lib/content-repository.ts +3 -0
  150. package/lib/content-types.ts +4 -0
  151. package/lib/curation-client.ts +61 -0
  152. package/lib/edition-routes.ts +31 -29
  153. package/lib/graphql-content-repository.ts +56 -22
  154. package/lib/media-route-response.ts +45 -0
  155. package/lib/newspaper-layout.ts +3 -2
  156. package/lib/newsroom-base-path.ts +12 -3
  157. package/lib/newsroom-board.ts +99 -0
  158. package/lib/newsroom-category-drilldown.ts +3 -2
  159. package/lib/newsroom-forum-routes.ts +6 -4
  160. package/lib/newsroom-index-filters.ts +14 -10
  161. package/lib/newsroom-nav.ts +3 -1
  162. package/lib/newsroom-organizer-nav.ts +37 -0
  163. package/lib/newsroom-section-routing.ts +20 -0
  164. package/lib/oauth-redirect-priority.ts +40 -0
  165. package/lib/papyrus-web-locations.ts +7 -6
  166. package/lib/plugin-settings.ts +11 -0
  167. package/lib/presentation-footer.ts +3 -2
  168. package/lib/pretext-layout.ts +12 -3
  169. package/lib/priming-proof-report.ts +89 -0
  170. package/lib/prospectus-site-identity.ts +303 -0
  171. package/lib/publication-cyclotron.ts +74 -0
  172. package/lib/rate-limit-retry.ts +55 -0
  173. package/lib/reader-base-path.ts +24 -0
  174. package/lib/reader-cache-lifetime.ts +5 -0
  175. package/lib/reader-static-params.ts +85 -4
  176. package/lib/reader-storage-url.ts +9 -12
  177. package/lib/reference-policy.ts +5 -0
  178. package/lib/relevance-decisions.ts +103 -0
  179. package/lib/relevance-status.ts +63 -0
  180. package/lib/renderer.ts +7 -0
  181. package/lib/revalidate-prewarm.ts +53 -0
  182. package/lib/revalidate-secret.ts +59 -0
  183. package/lib/robots-policy.ts +14 -0
  184. package/lib/root-route-redirect.ts +16 -0
  185. package/lib/semantic-graph.ts +7 -0
  186. package/lib/shared-credentials-provider.ts +61 -0
  187. package/lib/signup-csv.ts +24 -0
  188. package/lib/site-brand.ts +59 -0
  189. package/lib/staging-gated-path.ts +23 -0
  190. package/lib/staging-preview-object.ts +21 -5
  191. package/lib/test-editor-override.ts +4 -0
  192. package/lib/video-loading.ts +22 -0
  193. package/lib/video-script.ts +14 -0
  194. package/middleware.ts +11 -19
  195. package/package.json +64 -1
  196. package/renderers/pretext/article-page.tsx +39 -3
  197. package/renderers/pretext/presentation-shared.tsx +113 -0
  198. package/renderers/pretext/presentation-shell.tsx +15 -102
  199. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  200. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  201. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  202. package/routes.manifest.json +68 -12
  203. package/src/sync.mjs +30 -6
  204. package/src/with-papyrus.d.mts +9 -0
  205. package/src/with-papyrus.mjs +48 -3
  206. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  207. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  208. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
@@ -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)
@@ -14,6 +14,8 @@ function commandLines(commands, indent) {
14
14
  }
15
15
  const NPM_CLEAN_INSTALL = "npm ci --cache .npm --prefer-offline";
16
16
  const FRONTEND_ENVIRONMENT_CAPTURE = "env | grep -e '^PAPYRUS_' -e '^SITE_ENV=' -e '^NEXT_PUBLIC_' >> .env.production";
17
+ const CHECKOUT_AS_PAPYRUS_ROOT = "PAPYRUS_ROOT=\"$PWD\"";
18
+ const RUNTIME_OUTPUTS_COPY = "cp amplify_outputs.json .next/amplify_outputs.json";
17
19
  function pythonProvisioningCommands(config) {
18
20
  return [
19
21
  "export UV_CACHE_DIR=\"$PWD/.uv-cache\"",
@@ -30,6 +32,9 @@ function reader(config) {
30
32
  throw new Error(`site ${config.siteId} has no reader app (frontend ${config.frontend})`);
31
33
  return config.reader;
32
34
  }
35
+ function stagingPreviewFor(config) {
36
+ return config.cms?.stagingPreview ?? config.reader;
37
+ }
33
38
  function frontendSection(preBuildCommands, buildCommands, artifactsDirectory, cachePaths) {
34
39
  const lines = ["frontend:", " phases:"];
35
40
  if (preBuildCommands.length > 0) {
@@ -50,20 +55,28 @@ export function cmsProductionBuildSpec(config) {
50
55
  "if [ -z \"${PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI:-}\" ]; then export PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD=true; fi",
51
56
  "npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID",
52
57
  ], " "),
53
- ...frontendSection([], [FRONTEND_ENVIRONMENT_CAPTURE, "npx papyrus-app sync && npm run build"], ".next", [".next/cache/**/*", ".npm/**/*"]),
58
+ ...frontendSection([], [
59
+ FRONTEND_ENVIRONMENT_CAPTURE,
60
+ "npx papyrus-app sync && npm run build",
61
+ ...(config.frontend === "pretext" ? [RUNTIME_OUTPUTS_COPY] : []),
62
+ ], ".next", [".next/cache/**/*", ".npm/**/*"]),
54
63
  ].join("\n") + "\n";
55
64
  }
56
65
  export function cmsStagingBuildSpec(config) {
57
66
  const preBuild = [NPM_CLEAN_INSTALL, "npx ampx generate outputs --branch main --app-id $AWS_APP_ID"];
58
67
  const cachePaths = [".next/cache/**/*", ".npm/**/*"];
59
- if (config.frontend === "markus-static") {
60
- const readerConfig = reader(config);
61
- preBuild.push(...pythonProvisioningCommands(config), "papyrus auth refresh-jwt --write-env .env", "papyrus content export-published --drafts --out content-export --clean", readerConfig.buildCommand, `papyrus content upload-preview --dir ${readerConfig.baseDirectory} --prefix ${resolveStoragePreviewPrefix(config)}`);
68
+ const preview = config.frontend === "markus-static" ? stagingPreviewFor(config) : undefined;
69
+ if (preview !== undefined) {
70
+ preBuild.push(...pythonProvisioningCommands(config), `${CHECKOUT_AS_PAPYRUS_ROOT} papyrus ops content export-published --drafts --out content-export --clean`, preview.buildCommand, `${CHECKOUT_AS_PAPYRUS_ROOT} papyrus ops content upload-preview --dir ${preview.baseDirectory} --prefix ${resolveStoragePreviewPrefix(config)}`);
62
71
  cachePaths.push(".uv-cache/**/*");
63
72
  }
64
73
  return [
65
74
  "version: 1",
66
- ...frontendSection(preBuild, [FRONTEND_ENVIRONMENT_CAPTURE, "npx papyrus-app sync && npm run build"], ".next", cachePaths),
75
+ ...frontendSection(preBuild, [
76
+ FRONTEND_ENVIRONMENT_CAPTURE,
77
+ "npx papyrus-app sync && npm run build",
78
+ RUNTIME_OUTPUTS_COPY,
79
+ ], ".next", cachePaths),
67
80
  ].join("\n") + "\n";
68
81
  }
69
82
  export function readerBuildSpec(config) {
@@ -72,11 +85,14 @@ export function readerBuildSpec(config) {
72
85
  "version: 1",
73
86
  ...frontendSection([
74
87
  ...pythonProvisioningCommands(config),
75
- "papyrus content export-published --auth guest --out content-export --clean",
88
+ ...(readerConfig.preBuildCommands ?? []),
89
+ ...(readerConfig.skipContentExport ? [] : ["papyrus ops content export-published --auth guest --out content-export --clean"]),
76
90
  ], [readerConfig.buildCommand], readerConfig.baseDirectory, [".uv-cache/**/*"]),
77
91
  ].join("\n") + "\n";
78
92
  }
79
93
  export function buildSpecFor(config, app) {
94
+ if (app !== "reader" && !hasCmsApp(config))
95
+ throw new Error(`site ${config.siteId} has no cms app`);
80
96
  if (app === "cms-production")
81
97
  return cmsProductionBuildSpec(config);
82
98
  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
 
@@ -22,6 +22,10 @@ const NPM_CLEAN_INSTALL = "npm ci --cache .npm --prefer-offline";
22
22
  const FRONTEND_ENVIRONMENT_CAPTURE =
23
23
  "env | grep -e '^PAPYRUS_' -e '^SITE_ENV=' -e '^NEXT_PUBLIC_' >> .env.production";
24
24
 
25
+ const CHECKOUT_AS_PAPYRUS_ROOT = "PAPYRUS_ROOT=\"$PWD\"";
26
+
27
+ const RUNTIME_OUTPUTS_COPY = "cp amplify_outputs.json .next/amplify_outputs.json";
28
+
25
29
  function pythonProvisioningCommands(config: AmplifyAppShellSiteConfig): string[] {
26
30
  return [
27
31
  "export UV_CACHE_DIR=\"$PWD/.uv-cache\"",
@@ -39,6 +43,10 @@ function reader(config: AmplifyAppShellSiteConfig): NonNullable<AmplifyAppShellS
39
43
  return config.reader;
40
44
  }
41
45
 
46
+ function stagingPreviewFor(config: AmplifyAppShellSiteConfig): { buildCommand: string; baseDirectory: string } | undefined {
47
+ return config.cms?.stagingPreview ?? config.reader;
48
+ }
49
+
42
50
  function frontendSection(preBuildCommands: string[], buildCommands: string[], artifactsDirectory: string, cachePaths: string[]): string[] {
43
51
  const lines = ["frontend:", " phases:"];
44
52
  if (preBuildCommands.length > 0) {
@@ -76,7 +84,11 @@ export function cmsProductionBuildSpec(config: AmplifyAppShellSiteConfig): strin
76
84
  ),
77
85
  ...frontendSection(
78
86
  [],
79
- [FRONTEND_ENVIRONMENT_CAPTURE, "npx papyrus-app sync && npm run build"],
87
+ [
88
+ FRONTEND_ENVIRONMENT_CAPTURE,
89
+ "npx papyrus-app sync && npm run build",
90
+ ...(config.frontend === "pretext" ? [RUNTIME_OUTPUTS_COPY] : []),
91
+ ],
80
92
  ".next",
81
93
  [".next/cache/**/*", ".npm/**/*"],
82
94
  ),
@@ -86,14 +98,13 @@ export function cmsProductionBuildSpec(config: AmplifyAppShellSiteConfig): strin
86
98
  export function cmsStagingBuildSpec(config: AmplifyAppShellSiteConfig): string {
87
99
  const preBuild = [NPM_CLEAN_INSTALL, "npx ampx generate outputs --branch main --app-id $AWS_APP_ID"];
88
100
  const cachePaths = [".next/cache/**/*", ".npm/**/*"];
89
- if (config.frontend === "markus-static") {
90
- const readerConfig = reader(config);
101
+ const preview = config.frontend === "markus-static" ? stagingPreviewFor(config) : undefined;
102
+ if (preview !== undefined) {
91
103
  preBuild.push(
92
104
  ...pythonProvisioningCommands(config),
93
- "papyrus auth refresh-jwt --write-env .env",
94
- "papyrus content export-published --drafts --out content-export --clean",
95
- readerConfig.buildCommand,
96
- `papyrus content upload-preview --dir ${readerConfig.baseDirectory} --prefix ${resolveStoragePreviewPrefix(config)}`,
105
+ `${CHECKOUT_AS_PAPYRUS_ROOT} papyrus ops content export-published --drafts --out content-export --clean`,
106
+ preview.buildCommand,
107
+ `${CHECKOUT_AS_PAPYRUS_ROOT} papyrus ops content upload-preview --dir ${preview.baseDirectory} --prefix ${resolveStoragePreviewPrefix(config)}`,
97
108
  );
98
109
  cachePaths.push(".uv-cache/**/*");
99
110
  }
@@ -101,7 +112,11 @@ export function cmsStagingBuildSpec(config: AmplifyAppShellSiteConfig): string {
101
112
  "version: 1",
102
113
  ...frontendSection(
103
114
  preBuild,
104
- [FRONTEND_ENVIRONMENT_CAPTURE, "npx papyrus-app sync && npm run build"],
115
+ [
116
+ FRONTEND_ENVIRONMENT_CAPTURE,
117
+ "npx papyrus-app sync && npm run build",
118
+ RUNTIME_OUTPUTS_COPY,
119
+ ],
105
120
  ".next",
106
121
  cachePaths,
107
122
  ),
@@ -115,7 +130,8 @@ export function readerBuildSpec(config: AmplifyAppShellSiteConfig): string {
115
130
  ...frontendSection(
116
131
  [
117
132
  ...pythonProvisioningCommands(config),
118
- "papyrus content export-published --auth guest --out content-export --clean",
133
+ ...(readerConfig.preBuildCommands ?? []),
134
+ ...(readerConfig.skipContentExport ? [] : ["papyrus ops content export-published --auth guest --out content-export --clean"]),
119
135
  ],
120
136
  [readerConfig.buildCommand],
121
137
  readerConfig.baseDirectory,
@@ -125,6 +141,7 @@ export function readerBuildSpec(config: AmplifyAppShellSiteConfig): string {
125
141
  }
126
142
 
127
143
  export function buildSpecFor(config: AmplifyAppShellSiteConfig, app: BuildSpecAppName): string {
144
+ if (app !== "reader" && !hasCmsApp(config)) throw new Error(`site ${config.siteId} has no cms app`);
128
145
  if (app === "cms-production") return cmsProductionBuildSpec(config);
129
146
  if (app === "cms-staging") return cmsStagingBuildSpec(config);
130
147
  return readerBuildSpec(config);
@@ -1,14 +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 _.-]*$/;
7
12
  const STACK_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,127}$/;
8
13
  const BRANCH_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9/_.-]*$/;
9
14
  const PAPYRUS_VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+\.\d+)?$/;
10
15
  const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
11
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
+ }
12
48
  const LOCAL_DEVELOPMENT_ORIGIN = "http://localhost:3001/";
13
49
  function fail(field, problem) {
14
50
  throw new Error(`site.json: ${field} ${problem}`);
@@ -47,12 +83,96 @@ function rejectUnknownKeys(record, allowed, field) {
47
83
  fail(`${field}.${key}`, `is not a known field (known: ${allowed.join(", ")})`);
48
84
  }
49
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
+ }
50
162
  export function isStagingEnabled(config) {
51
163
  return config.cms.staging !== false;
52
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
+ }
53
170
  export function resolveStagingDomainName(config) {
54
171
  if (!isStagingEnabled(config))
55
172
  return undefined;
173
+ if (config.cms.domainPrefix) {
174
+ return config.cms.stagingDomainPrefix ? `${config.cms.stagingDomainPrefix}.${config.cms.domainName}` : undefined;
175
+ }
56
176
  if (config.cms.stagingDomainName)
57
177
  return config.cms.stagingDomainName;
58
178
  if (!config.cms.domainName)
@@ -66,22 +186,19 @@ export function resolveStackName(config) {
66
186
  export function resolveStoragePreviewPrefix(config) {
67
187
  return config.storagePreviewPrefix ?? "preview/";
68
188
  }
69
- export function parseSiteConfig(raw) {
70
- const record = requireObject(raw, "site");
71
- rejectUnknownKeys(record, ["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"], "site");
72
- const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
73
- const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
74
- const brand = requireString(record.brand, "brand");
75
- const frontend = requireString(record.frontend, "frontend");
76
- if (frontend !== "pretext" && frontend !== "markus-static") {
77
- fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
78
- }
79
- const hostedZoneId = optionalString(record.hostedZoneId, "hostedZoneId", HOSTED_ZONE_ID_PATTERN, "Route 53 zone id");
80
- const stackName = optionalString(record.stackName, "stackName", STACK_NAME_PATTERN, "CloudFormation stack name");
81
- const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
82
- const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
83
- const cmsRecord = requireObject(record.cms, "cms");
84
- rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "buildComputeType", "environment", "stagingDomainName"], "cms");
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");
85
202
  const buildComputeType = cmsRecord.buildComputeType;
86
203
  if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
87
204
  fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
@@ -89,26 +206,119 @@ export function parseSiteConfig(raw) {
89
206
  if (cmsRecord.staging !== undefined && typeof cmsRecord.staging !== "boolean") {
90
207
  fail("cms.staging", `must be a boolean, got ${JSON.stringify(cmsRecord.staging)}`);
91
208
  }
209
+ if (cmsRecord.applyCognitoDomainPrefix !== undefined && typeof cmsRecord.applyCognitoDomainPrefix !== "boolean") {
210
+ fail("cms.applyCognitoDomainPrefix", `must be a boolean, got ${JSON.stringify(cmsRecord.applyCognitoDomainPrefix)}`);
211
+ }
92
212
  const cms = {
93
213
  appName: optionalString(cmsRecord.appName, "cms.appName", APP_NAME_PATTERN),
94
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"),
95
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,
96
219
  buildComputeType: buildComputeType,
97
220
  environment: requireStringRecord(cmsRecord.environment, "cms.environment"),
98
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"),
99
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
+ }
100
236
  if (cms.staging === false && cms.stagingDomainName) {
101
237
  fail("cms.stagingDomainName", "must not be set when cms.staging is false");
102
238
  }
103
239
  if (cms.stagingDomainName && !cms.domainName) {
104
240
  fail("cms.stagingDomainName", "requires cms.domainName");
105
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
+ }
298
+ export function parseSiteConfig(raw) {
299
+ const record = requireObject(raw, "site");
300
+ rejectUnknownKeys(record, ["siteId", "repository", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"], "site");
301
+ const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
302
+ const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
303
+ const brand = requireString(record.brand, "brand");
304
+ const frontend = requireString(record.frontend, "frontend");
305
+ if (frontend !== "pretext" && frontend !== "markus-static") {
306
+ fail("frontend", `must be "pretext" or "markus-static", got ${JSON.stringify(frontend)}`);
307
+ }
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");
310
+ const papyrusVersion = requireString(record.papyrusVersion, "papyrusVersion", PAPYRUS_VERSION_PATTERN, "exact version such as 1.0.0 or 1.0.0-next.1");
311
+ const storagePreviewPrefix = optionalString(record.storagePreviewPrefix, "storagePreviewPrefix", /^[A-Za-z0-9._-]+\/$/, "must end with /");
312
+ const cms = record.cms === undefined && frontend === "markus-static" ? undefined : parseCms(record.cms);
106
313
  let reader;
107
314
  if (record.reader !== undefined) {
108
315
  if (frontend !== "markus-static")
109
316
  fail("reader", `is only allowed when frontend is "markus-static"`);
110
317
  const readerRecord = requireObject(record.reader, "reader");
111
- 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
+ }
112
322
  reader = {
113
323
  appName: optionalString(readerRecord.appName, "reader.appName", APP_NAME_PATTERN),
114
324
  domainName: optionalString(readerRecord.domainName, "reader.domainName", HOST_NAME_PATTERN, "host name"),
@@ -116,13 +326,28 @@ export function parseSiteConfig(raw) {
116
326
  buildCommand: requireString(readerRecord.buildCommand, "reader.buildCommand"),
117
327
  baseDirectory: requireString(readerRecord.baseDirectory, "reader.baseDirectory"),
118
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,
119
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"`);
120
343
  }
121
- else if (frontend === "markus-static") {
122
- 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");
123
346
  }
124
347
  let github;
125
348
  if (record.github !== undefined) {
349
+ if (cms === undefined)
350
+ fail("github", "requires cms (the CI role is scoped to the CMS app)");
126
351
  const githubRecord = requireObject(record.github, "github");
127
352
  rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
128
353
  const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
@@ -130,7 +355,7 @@ export function parseSiteConfig(raw) {
130
355
  if (repository !== `https://github.com/${owner}/${repo}`) {
131
356
  fail("github", `owner/repo must match repository ${repository}`);
132
357
  }
133
- let branches = cms.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
358
+ let branches = cms?.staging === false ? DEFAULT_CI_BRANCHES.filter((branch) => branch !== "staging") : DEFAULT_CI_BRANCHES;
134
359
  if (githubRecord.branches !== undefined) {
135
360
  if (!Array.isArray(githubRecord.branches) || githubRecord.branches.length === 0) {
136
361
  fail("github.branches", "must be a non-empty array of branch names");
@@ -147,7 +372,7 @@ export function parseSiteConfig(raw) {
147
372
  fail("github.ciCanDeployInfra", "must be a boolean");
148
373
  github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra };
149
374
  }
150
- const hasDomain = cms.domainName !== undefined || reader?.domainName !== undefined;
375
+ const hasDomain = cms?.domainName !== undefined || reader?.domainName !== undefined;
151
376
  if (hasDomain && hostedZoneId === undefined) {
152
377
  fail("hostedZoneId", "is required when cms.domainName or reader.domainName is set");
153
378
  }
@@ -167,20 +392,7 @@ export function parseSiteConfig(raw) {
167
392
  storagePreviewPrefix,
168
393
  github,
169
394
  };
170
- const redirectValue = cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS;
171
- if (redirectValue === undefined)
172
- fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", "is required");
173
- const redirectUrls = redirectValue.split(",").map((url) => url.trim());
174
- const stagingDomainName = resolveStagingDomainName(config);
175
- const requiredOrigins = [
176
- ...(cms.domainName ? [`https://${cms.domainName}/`] : []),
177
- ...(stagingDomainName ? [`https://${stagingDomainName}/`] : []),
178
- LOCAL_DEVELOPMENT_ORIGIN,
179
- ];
180
- for (const origin of requiredOrigins) {
181
- if (!redirectUrls.includes(origin)) {
182
- fail("cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS", `must include ${origin}`);
183
- }
184
- }
395
+ if (hasCmsApp(config))
396
+ validateOauthRedirectUrls(config);
185
397
  return config;
186
398
  }