@anthusai/papyrus 1.0.0-next.9 → 1.1.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 (222) hide show
  1. package/amplify/auth/prospectus-google-client.js +66 -0
  2. package/amplify/auth/prospectus-google-client.ts +71 -0
  3. package/amplify/auth/redirect-urls.js +54 -0
  4. package/amplify/auth/redirect-urls.ts +66 -0
  5. package/amplify/auth/resource.js +23 -5
  6. package/amplify/auth/resource.ts +31 -5
  7. package/amplify/data/resource.js +42 -80
  8. package/amplify/data/resource.ts +42 -80
  9. package/amplify/data/schema.js +45 -62
  10. package/amplify/data/schema.ts +47 -62
  11. package/amplify/functions/category-action/handler.ts +5 -3
  12. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
  13. package/amplify/functions/console-chat-responder/resource.js +0 -7
  14. package/amplify/functions/console-chat-responder/resource.ts +0 -8
  15. package/amplify/functions/console-chat-responder/src/main.rs +0 -9
  16. package/amplify/functions/content-actions/handler.py +143 -1
  17. package/amplify/functions/content-actions/requirements.txt +2 -1
  18. package/amplify/functions/content-actions/resource.js +6 -1
  19. package/amplify/functions/content-actions/resource.ts +9 -2
  20. package/amplify/functions/email-submission-processor/handler.py +3 -1
  21. package/amplify/functions/renditions/handler.py +120 -0
  22. package/amplify/functions/renditions/requirements.txt +4 -0
  23. package/amplify/functions/ses-inbound-receive/handler.ts +32 -4
  24. package/amplify/functions/shared/email-submission.ts +109 -1
  25. package/amplify/functions/shared/python-bundle.js +42 -2
  26. package/amplify/functions/shared/python-bundle.ts +47 -2
  27. package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
  28. package/amplify/inbound-email/ses-intake.js +86 -0
  29. package/amplify/inbound-email/ses-intake.ts +110 -0
  30. package/amplify/plugins/registry.js +19 -0
  31. package/amplify/plugins/registry.ts +23 -0
  32. package/amplify/plugins/signup/function/handler.py +200 -0
  33. package/amplify/plugins/signup/function/resource.js +27 -0
  34. package/amplify/plugins/signup/function/resource.ts +35 -0
  35. package/amplify/plugins/signup/papyrus.plugin.js +59 -0
  36. package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
  37. package/amplify/plugins/signup/schema.js +40 -0
  38. package/amplify/plugins/signup/schema.ts +41 -0
  39. package/amplify/plugins/signup/waf.js +48 -0
  40. package/amplify/plugins/signup/waf.ts +51 -0
  41. package/amplify/plugins/types.js +1 -0
  42. package/amplify/plugins/types.ts +39 -0
  43. package/amplify/plugins/wire.js +24 -0
  44. package/amplify/plugins/wire.ts +26 -0
  45. package/amplify/publication/default-publication.js +18 -0
  46. package/amplify/publication/default-publication.ts +20 -0
  47. package/amplify/publication/invalidation-alarm.js +26 -0
  48. package/amplify/publication/invalidation-alarm.ts +30 -0
  49. package/amplify/publication/redirect-and-index.js +37 -0
  50. package/amplify/publication/renditions-function.js +74 -0
  51. package/amplify/publication/renditions-function.ts +112 -0
  52. package/amplify/publication/renditions-settings.js +51 -0
  53. package/amplify/publication/renditions-settings.ts +58 -0
  54. package/amplify/publication/reproject-on-deploy.js +73 -0
  55. package/amplify/publication/reproject-on-deploy.ts +78 -0
  56. package/amplify/publication/s3-projection-settings.js +67 -0
  57. package/amplify/publication/s3-projection-settings.ts +101 -0
  58. package/amplify/publication/s3-projection.js +151 -0
  59. package/amplify/publication/s3-projection.ts +201 -0
  60. package/amplify/resolver-waves.js +26 -0
  61. package/amplify/resolver-waves.ts +34 -0
  62. package/amplify/revalidate-invalidation-settings.js +30 -0
  63. package/amplify/revalidate-invalidation-settings.ts +35 -0
  64. package/amplify/revalidate-secret-policy.js +13 -0
  65. package/amplify/revalidate-secret-policy.ts +19 -0
  66. package/amplify/site-backend-config.js +65 -1
  67. package/amplify/site-backend-config.ts +153 -2
  68. package/amplify/site-backend-names.js +103 -0
  69. package/amplify/site-backend-names.ts +166 -0
  70. package/amplify/site-backend.js +150 -176
  71. package/amplify/site-backend.ts +189 -201
  72. package/amplify/storage-backups/construct.js +31 -0
  73. package/amplify/storage-backups/construct.ts +47 -0
  74. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
  75. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  76. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
  77. package/app/[year]/[month]/[day]/page.tsx +1 -1
  78. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
  79. package/app/api/archive/editions/route.ts +1 -1
  80. package/app/api/auth/prospectus/callback/route.ts +9 -0
  81. package/app/api/auth/prospectus/selection/route.ts +9 -0
  82. package/app/api/auth/prospectus/session/route.ts +9 -0
  83. package/app/api/auth/prospectus/sign-in/route.ts +9 -0
  84. package/app/api/auth/prospectus/sign-out/route.ts +9 -0
  85. package/app/api/cyclotron/priming-proof/route.ts +11 -0
  86. package/app/api/media/[...path]/route.ts +5 -16
  87. package/app/api/revalidate/route.ts +15 -7
  88. package/app/archive/page.tsx +1 -1
  89. package/app/articles/[slug]/page.tsx +7 -5
  90. package/app/blog-rhythm.css +278 -0
  91. package/app/cyclotron/page.tsx +13 -0
  92. package/app/globals.css +2 -0
  93. package/app/layout.tsx +16 -0
  94. package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
  95. package/app/newsroom/[section]/page.tsx +6 -3
  96. package/app/newsroom/layout.tsx +4 -9
  97. package/app/newsroom/settings/page.tsx +6 -0
  98. package/app/newsroom/work/page.tsx +10 -0
  99. package/app/page.tsx +22 -8
  100. package/app/robots.ts +2 -5
  101. package/app/tailwind.css +1 -6
  102. package/components/amplify-client-provider.tsx +5 -22
  103. package/components/archive-shell.tsx +2 -22
  104. package/components/article-video.tsx +152 -5
  105. package/components/blog-page-background.tsx +23 -6
  106. package/components/news-desk-page.tsx +4 -15
  107. package/components/news-desk-taxonomy-client.ts +90 -3
  108. package/components/newspaper.tsx +6 -2
  109. package/components/newsroom-access-gate.tsx +2 -1
  110. package/components/newsroom-board-view.tsx +99 -0
  111. package/components/newsroom-client-shell.tsx +12 -0
  112. package/components/newsroom-curation-provider.tsx +17 -0
  113. package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
  114. package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
  115. package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
  116. package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
  117. package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
  118. package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
  119. package/components/newsroom-newsprint-board.css +48 -0
  120. package/components/newsroom-ops-overview.tsx +28 -11
  121. package/components/newsroom-ops-shell.tsx +7 -1
  122. package/components/newsroom-organizer-shell.tsx +130 -0
  123. package/components/newsroom-organizer.css +63 -0
  124. package/components/newsroom-references-view.tsx +92 -8
  125. package/components/papyrus-console-shell.tsx +4 -2
  126. package/components/pictogram-figure.tsx +9 -2
  127. package/components/presentation-header.tsx +135 -0
  128. package/components/presentation-rhythm-hrule.tsx +17 -0
  129. package/components/priming-proof-page.tsx +94 -0
  130. package/components/publication-cyclotron-panel.tsx +57 -0
  131. package/components/reader-auth-control.tsx +8 -5
  132. package/components/reference-curation-controls.tsx +77 -0
  133. package/components/reference-cyclotron-status.tsx +109 -0
  134. package/components/reference-priming.tsx +18 -0
  135. package/components/reference-relevance-review.tsx +75 -0
  136. package/components/settings-page.tsx +2 -1
  137. package/components/signup-entries-panel.tsx +94 -0
  138. package/components/topic-steering-workspace.tsx +78 -8
  139. package/components/use-blog-header-obstacles.ts +81 -0
  140. package/components/use-brand-fonts-ready.ts +28 -0
  141. package/components/use-near-viewport.ts +30 -0
  142. package/components/use-rhythm-overlay.ts +29 -0
  143. package/infra/amplify-app-shell.js +166 -27
  144. package/infra/amplify-app-shell.ts +180 -31
  145. package/infra/build-specs.js +23 -7
  146. package/infra/build-specs.ts +27 -10
  147. package/infra/site-config.js +248 -36
  148. package/infra/site-config.ts +313 -51
  149. package/lib/amplify-outputs-path.ts +5 -1
  150. package/lib/amplify-server-runtime.ts +36 -1
  151. package/lib/approved-priming-proof-reports.ts +6 -0
  152. package/lib/blog-feature-solver.ts +459 -0
  153. package/lib/blog-rhythm.ts +134 -0
  154. package/lib/brand-fonts-watch.ts +43 -0
  155. package/lib/brand-fonts.ts +112 -0
  156. package/lib/cached-content-repository.ts +12 -0
  157. package/lib/category-repository.ts +43 -93
  158. package/lib/content-item-lifecycle.ts +55 -0
  159. package/lib/content-repository.ts +3 -0
  160. package/lib/content-types.ts +4 -0
  161. package/lib/curation-client.ts +61 -0
  162. package/lib/edition-routes.ts +31 -29
  163. package/lib/graphql-content-repository.ts +56 -22
  164. package/lib/media-route-response.ts +45 -0
  165. package/lib/newspaper-layout.ts +3 -2
  166. package/lib/newsroom-base-path.ts +12 -3
  167. package/lib/newsroom-board.ts +99 -0
  168. package/lib/newsroom-category-drilldown.ts +3 -2
  169. package/lib/newsroom-forum-routes.ts +6 -4
  170. package/lib/newsroom-index-filters.ts +14 -10
  171. package/lib/newsroom-nav.ts +3 -1
  172. package/lib/newsroom-organizer-nav.ts +37 -0
  173. package/lib/newsroom-section-routing.ts +20 -0
  174. package/lib/oauth-redirect-priority.ts +40 -0
  175. package/lib/papyrus-web-locations.ts +7 -6
  176. package/lib/plugin-settings.ts +11 -0
  177. package/lib/presentation-footer.ts +3 -2
  178. package/lib/pretext-layout.ts +12 -3
  179. package/lib/priming-proof-report.ts +90 -0
  180. package/lib/prospectus-publication-access.ts +53 -0
  181. package/lib/prospectus-session.ts +196 -0
  182. package/lib/prospectus-sign-in-routes.ts +284 -0
  183. package/lib/prospectus-site-identity.ts +303 -0
  184. package/lib/publication-cyclotron.ts +74 -0
  185. package/lib/rate-limit-retry.ts +55 -0
  186. package/lib/reader-base-path.ts +24 -0
  187. package/lib/reader-cache-lifetime.ts +5 -0
  188. package/lib/reader-static-params.ts +85 -4
  189. package/lib/reader-storage-url.ts +9 -12
  190. package/lib/reference-policy.ts +5 -0
  191. package/lib/relevance-decisions.ts +103 -0
  192. package/lib/relevance-status.ts +63 -0
  193. package/lib/renderer.ts +7 -0
  194. package/lib/revalidate-prewarm.ts +53 -0
  195. package/lib/revalidate-secret.ts +59 -0
  196. package/lib/robots-policy.ts +14 -0
  197. package/lib/root-route-redirect.ts +16 -0
  198. package/lib/semantic-graph.ts +7 -0
  199. package/lib/shared-credentials-provider.ts +61 -0
  200. package/lib/signup-csv.ts +24 -0
  201. package/lib/site-brand.ts +59 -0
  202. package/lib/staging-gated-path.ts +23 -0
  203. package/lib/staging-preview-object.ts +21 -5
  204. package/lib/test-editor-override.ts +4 -0
  205. package/lib/video-loading.ts +22 -0
  206. package/lib/video-script.ts +14 -0
  207. package/middleware.ts +11 -19
  208. package/package.json +73 -8
  209. package/renderers/pretext/article-page.tsx +39 -3
  210. package/renderers/pretext/presentation-shared.tsx +113 -0
  211. package/renderers/pretext/presentation-shell.tsx +15 -102
  212. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  213. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  214. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  215. package/routes.manifest.json +123 -12
  216. package/src/sync.mjs +30 -6
  217. package/src/with-papyrus.d.mts +9 -0
  218. package/src/with-papyrus.mjs +48 -3
  219. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  220. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  221. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
  222. package/components/ai-elements/message.tsx +0 -360
@@ -1,24 +1,48 @@
1
1
  import { ArnFormat, CfnOutput, Duration, Fn, Stack } from "aws-cdk-lib";
2
2
  import * as iam from "aws-cdk-lib/aws-iam";
3
3
  import * as amplify from "aws-cdk-lib/aws-amplify";
4
+ import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
4
5
  import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider.js";
5
6
  import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs.js";
6
- import { isStagingEnabled, resolveStackName, resolveStagingDomainName } from "./site-config.js";
7
+ import { PROSPECTUS_PAPYRUS_SITE_ROUTES, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveProspectusAccountsEndpoint, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
7
8
  function environmentVariables(variables) {
8
9
  return Object.entries(variables).map(([name, value]) => ({ name, value }));
9
10
  }
11
+ function readerCustomHeaders(rules) {
12
+ if (!rules)
13
+ return undefined;
14
+ const lines = ["customHeaders:"];
15
+ for (const rule of rules) {
16
+ lines.push(` - pattern: ${JSON.stringify(rule.pattern)}`, " headers:");
17
+ for (const [key, value] of Object.entries(rule.headers)) {
18
+ lines.push(` - key: ${JSON.stringify(key)}`, ` value: ${JSON.stringify(value)}`);
19
+ }
20
+ }
21
+ return lines.join("\n") + "\n";
22
+ }
10
23
  /**
11
24
  * Provisions the Amplify *app shell* of a Papyrus publication from its
12
25
  * site.json:
13
26
  *
14
27
  * - CMS app (WEB_COMPUTE) with branches `main` (production, owns the backend)
15
28
  * and `staging` (frontend only, reads the production backend)
16
- * - for `markus-static` sites, a reader app (WEB) with branch `main`
29
+ * - for `markus-static` sites, a reader app (WEB) with branch `main`; a
30
+ * `markus-static` site may omit `cms` to get the reader app alone (no CMS
31
+ * app, no CMS roles, no authoring role)
17
32
  * - a domain association per branch/app when a domain is configured (without
18
33
  * one, no AWS::Amplify::Domain is created and the default amplifyapp.com
19
34
  * URL is used), IAM service and compute roles
20
35
  * - `cms.staging: false` omits the staging branch and its domain
21
36
  *
37
+ * Google sign-in is always on: the branches never set PAPYRUS_DISABLE_GOOGLE_OAUTH,
38
+ and `cms.cognitoDomainPrefix` becomes the hosted-UI domain
39
+ (PAPYRUS_COGNITO_DOMAIN_PREFIX) only when `cms.applyCognitoDomainPrefix` is true
40
+ (PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX); otherwise Amplify's generated domain is kept. The secrets GOOGLE_CLIENT_ID and
41
+ GOOGLE_CLIENT_SECRET are Amplify backend secrets set in the console, never here.
42
+ The stack also creates the `<siteId>-papyrus-authoring` role. The CLI signs
43
+ * AppSync requests (SigV4) with credentials for that role (SSO, OIDC or any
44
+ * principal in the account that may assume it); no token is stored anywhere.
45
+ *
22
46
  * No repository and no access token are configured: the apps are manual-deploy
23
47
  * until Amplify's GitHub App is connected once in the console
24
48
  * (docs/site-hosting.md). The Papyrus backend itself is created inside the CMS
@@ -29,9 +53,16 @@ export class AmplifyAppShellStack extends Stack {
29
53
  readerAppId;
30
54
  constructor(scope, id, config, props) {
31
55
  super(scope, id, props);
56
+ if (!hasCmsApp(config)) {
57
+ if (!config.reader)
58
+ throw new Error(`site ${config.siteId} has neither a cms section nor a reader`);
59
+ this.readerAppId = this.addReaderApp(config, config.reader);
60
+ return;
61
+ }
32
62
  const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
33
63
  const stagingEnabled = isStagingEnabled(config);
34
64
  const stagingDomainName = resolveStagingDomainName(config);
65
+ const cmsHostName = resolveCmsHostName(config);
35
66
  const cmsServiceRole = new iam.Role(this, "AmplifyServiceRole", {
36
67
  assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
37
68
  description: `Backend deploy and staging build service role for Amplify app ${cmsAppName} (AdministratorAccess: documented risk)`,
@@ -45,6 +76,44 @@ export class AmplifyAppShellStack extends Stack {
45
76
  iam.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchLogsFullAccess"),
46
77
  ],
47
78
  });
79
+ const revalidateSecretParameterName = config.frontend === "pretext" ? resolveRevalidateSecretParameterName(config) : undefined;
80
+ if (revalidateSecretParameterName) {
81
+ computeRole.addToPolicy(new iam.PolicyStatement({
82
+ sid: "ReadRevalidateSecret",
83
+ actions: ["ssm:GetParameter"],
84
+ resources: [
85
+ this.formatArn({ service: "ssm", resource: "parameter", resourceName: revalidateSecretParameterName.slice(1), arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
86
+ ],
87
+ }));
88
+ }
89
+ const prospectusAccountsApiStageArn = config.cms.prospectusAccountsApiStageArn;
90
+ const prospectusSessionSealingSecret = prospectusAccountsApiStageArn
91
+ ? new secretsmanager.Secret(this, "ProspectusSessionSealingSecret", {
92
+ description: `Seals the Prospectus sign-in cookies of ${cmsAppName}; replacing it signs everyone out`,
93
+ generateSecretString: { passwordLength: 64, excludePunctuation: true },
94
+ })
95
+ : undefined;
96
+ const prospectusEnvironment = prospectusAccountsApiStageArn && prospectusSessionSealingSecret
97
+ ? {
98
+ PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT: resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn),
99
+ PAPYRUS_PROSPECTUS_SESSION_SECRET_ARN: prospectusSessionSealingSecret.secretArn,
100
+ PAPYRUS_PROSPECTUS_SITE_PRODUCT: `papyrus-${config.siteId}`,
101
+ }
102
+ : {};
103
+ if (prospectusSessionSealingSecret) {
104
+ computeRole.addToPolicy(new iam.PolicyStatement({
105
+ sid: "ReadProspectusSessionSealingSecret",
106
+ actions: ["secretsmanager:GetSecretValue"],
107
+ resources: [prospectusSessionSealingSecret.secretArn],
108
+ }));
109
+ }
110
+ if (prospectusAccountsApiStageArn) {
111
+ computeRole.addToPolicy(new iam.PolicyStatement({
112
+ sid: "InvokeProspectusAccountsAsRegisteredSite",
113
+ actions: ["execute-api:Invoke"],
114
+ resources: PROSPECTUS_PAPYRUS_SITE_ROUTES.map((route) => `${prospectusAccountsApiStageArn}/${route}`),
115
+ }));
116
+ }
48
117
  const cmsApp = new amplify.CfnApp(this, "App", {
49
118
  name: cmsAppName,
50
119
  description: `Papyrus newsroom CMS for ${config.siteId} (WEB_COMPUTE)`,
@@ -53,17 +122,25 @@ export class AmplifyAppShellStack extends Stack {
53
122
  computeRoleArn: computeRole.roleArn,
54
123
  buildSpec: cmsProductionBuildSpec(config),
55
124
  jobConfig: config.cms.buildComputeType ? { buildComputeType: config.cms.buildComputeType } : undefined,
56
- customRules: [{ source: "/<*>", target: "/index.html", status: "404-200" }],
125
+ customRules: [
126
+ ...(config.cms.redirects ?? []).map((redirect) => ({
127
+ source: `https://${redirect.source}`,
128
+ target: `https://${resolveCmsHostName(config)}`,
129
+ status: String(redirect.status),
130
+ })),
131
+ { source: "/<*>", target: "/index.html", status: "404-200" },
132
+ ],
57
133
  });
58
134
  const stagingMode = config.frontend === "markus-static"
59
135
  ? { PAPYRUS_STAGING_PREVIEW: "static" }
60
136
  : { PAPYRUS_CONTENT_SOURCE: "drafts" };
61
- const productionOrigin = config.cms.domainName
62
- ? `https://${config.cms.domainName}/`
137
+ const productionOrigin = cmsHostName
138
+ ? `https://${cmsHostName}/`
63
139
  : Fn.join("", ["https://main.", cmsApp.attrDefaultDomain, "/"]);
64
140
  const stagingOrigin = stagingDomainName
65
141
  ? `https://${stagingDomainName}/`
66
142
  : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain, "/"]);
143
+ const sharedRootDomain = config.cms.domainPrefix !== undefined;
67
144
  const defaultOriginsToAllow = [
68
145
  ...(config.cms.domainName ? [] : [productionOrigin]),
69
146
  ...(stagingEnabled && !stagingDomainName ? [stagingOrigin] : []),
@@ -79,14 +156,19 @@ export class AmplifyAppShellStack extends Stack {
79
156
  enablePerformanceMode: false,
80
157
  environmentVariables: environmentVariables({
81
158
  ...config.cms.environment,
159
+ PAPYRUS_COGNITO_DOMAIN_PREFIX: config.cms.cognitoDomainPrefix,
160
+ ...(config.cms.applyCognitoDomainPrefix ? { PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX: "true" } : {}),
82
161
  PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
83
162
  PAPYRUS_SITE_BRAND: config.brand,
84
163
  SITE_ENV: "production",
85
164
  PAPYRUS_CONTENT_SOURCE: "published",
165
+ ...(revalidateSecretParameterName ? { PAPYRUS_REVALIDATE_SECRET_PARAMETER: revalidateSecretParameterName } : {}),
166
+ ...prospectusEnvironment,
86
167
  }),
87
168
  });
169
+ let stagingBranch;
88
170
  if (stagingEnabled) {
89
- const stagingBranch = new amplify.CfnBranch(this, "StagingBranch", {
171
+ stagingBranch = new amplify.CfnBranch(this, "StagingBranch", {
90
172
  appId: cmsApp.attrAppId,
91
173
  branchName: "staging",
92
174
  stage: "BETA",
@@ -95,16 +177,19 @@ export class AmplifyAppShellStack extends Stack {
95
177
  buildSpec: cmsStagingBuildSpec(config),
96
178
  environmentVariables: environmentVariables({
97
179
  ...config.cms.environment,
180
+ PAPYRUS_COGNITO_DOMAIN_PREFIX: config.cms.cognitoDomainPrefix,
181
+ ...(config.cms.applyCognitoDomainPrefix ? { PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX: "true" } : {}),
98
182
  PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
99
183
  PAPYRUS_SITE_BRAND: config.brand,
100
184
  SITE_ENV: "staging",
101
185
  ...stagingMode,
186
+ ...prospectusEnvironment,
102
187
  NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
103
188
  ? `https://${stagingDomainName}`
104
189
  : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]),
105
190
  }),
106
191
  });
107
- if (stagingDomainName) {
192
+ if (stagingDomainName && !sharedRootDomain) {
108
193
  const stagingDomain = new amplify.CfnDomain(this, "StagingDomain", {
109
194
  appId: cmsApp.attrAppId,
110
195
  domainName: stagingDomainName,
@@ -114,27 +199,89 @@ export class AmplifyAppShellStack extends Stack {
114
199
  }
115
200
  }
116
201
  if (config.cms.domainName) {
202
+ const subDomainSettings = sharedRootDomain
203
+ ? [
204
+ { branchName: "main", prefix: config.cms.domainPrefix },
205
+ ...(config.cms.redirects ?? []).map((redirect) => ({
206
+ branchName: "main",
207
+ prefix: resolveRedirectSubDomainPrefix(config.cms.domainName, redirect.source),
208
+ })),
209
+ ...(stagingBranch ? [{ branchName: "staging", prefix: config.cms.stagingDomainPrefix }] : []),
210
+ ]
211
+ : [{ branchName: "main", prefix: "" }];
117
212
  const productionDomain = new amplify.CfnDomain(this, "Domain", {
118
213
  appId: cmsApp.attrAppId,
119
214
  domainName: config.cms.domainName,
120
- subDomainSettings: [{ branchName: "main", prefix: "" }],
215
+ subDomainSettings,
121
216
  });
122
217
  productionDomain.addResourceDependency(productionBranch);
218
+ if (sharedRootDomain && stagingBranch) {
219
+ productionDomain.addResourceDependency(stagingBranch);
220
+ }
123
221
  }
124
222
  this.cmsAppId = cmsApp.attrAppId;
125
223
  new CfnOutput(this, "CmsAppId", { value: cmsApp.attrAppId, description: `Amplify app id for ${cmsAppName}` });
126
224
  new CfnOutput(this, "CmsOrigin", { value: productionOrigin });
225
+ new CfnOutput(this, "CmsComputeRoleArn", {
226
+ value: computeRole.roleArn,
227
+ description: `SSR compute role for ${cmsAppName}; the roleArn of its Prospectus site registration`,
228
+ });
127
229
  if (stagingEnabled) {
128
230
  new CfnOutput(this, "StagingOrigin", { value: stagingOrigin });
129
231
  }
130
232
  if (config.reader) {
131
233
  this.readerAppId = this.addReaderApp(config, config.reader);
132
234
  }
235
+ const amplifyAppIds = [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])];
236
+ this.addAuthoringRole(config, cmsApp.attrAppId, amplifyAppIds);
133
237
  if (config.github) {
134
- this.addGithubCiRole(config, config.github, [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])]);
238
+ this.addGithubCiRole(config, config.github, cmsApp.attrAppId, amplifyAppIds);
135
239
  }
136
240
  }
137
- addGithubCiRole(config, github, amplifyAppIds) {
241
+ authoringStatements(config, cmsAppId, amplifyAppIds) {
242
+ const mediaBucketArn = `arn:${this.partition}:s3:::amplify-${cmsAppId}-*`;
243
+ return [
244
+ new iam.PolicyStatement({
245
+ sid: "SignAppSyncAuthoringRequests",
246
+ actions: ["appsync:GraphQL"],
247
+ resources: ["Query", "Mutation"].map((typeName) => this.formatArn({ service: "appsync", resource: "apis", resourceName: `*/types/${typeName}/fields/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME })),
248
+ }),
249
+ new iam.PolicyStatement({
250
+ sid: "ReadWriteMediaAndPreview",
251
+ actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
252
+ resources: [`${mediaBucketArn}/media/*`, `${mediaBucketArn}/${resolveStoragePreviewPrefix(config)}*`],
253
+ }),
254
+ new iam.PolicyStatement({
255
+ sid: "ListMediaAndPreview",
256
+ actions: ["s3:ListBucket"],
257
+ resources: [mediaBucketArn],
258
+ conditions: { StringLike: { "s3:prefix": ["media/*", `${resolveStoragePreviewPrefix(config)}*`] } },
259
+ }),
260
+ new iam.PolicyStatement({
261
+ sid: "StartAndInspectAmplifyJobs",
262
+ actions: ["amplify:StartJob", "amplify:ListJobs", "amplify:GetJob", "amplify:ListBranches"],
263
+ resources: amplifyAppIds.flatMap((appId) => [
264
+ this.formatArn({ service: "amplify", resource: "apps", resourceName: appId, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
265
+ this.formatArn({ service: "amplify", resource: "apps", resourceName: `${appId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
266
+ ]),
267
+ }),
268
+ ];
269
+ }
270
+ addAuthoringRole(config, cmsAppId, amplifyAppIds) {
271
+ const authoringRole = new iam.Role(this, "PapyrusAuthoringRole", {
272
+ roleName: `${config.siteId}-papyrus-authoring`,
273
+ description: `Papyrus CLI authoring role for ${config.siteId}: AppSync (SigV4), media and preview storage, rebuild jobs. Assumable by principals in this account that are allowed sts:AssumeRole on it.`,
274
+ assumedBy: new iam.AccountRootPrincipal(),
275
+ });
276
+ for (const statement of this.authoringStatements(config, cmsAppId, amplifyAppIds)) {
277
+ authoringRole.addToPolicy(statement);
278
+ }
279
+ new CfnOutput(this, "PapyrusAuthoringRoleArn", {
280
+ value: authoringRole.roleArn,
281
+ description: `Role the Papyrus CLI assumes for ${config.siteId} (profile role_arn)`,
282
+ });
283
+ }
284
+ addGithubCiRole(config, github, cmsAppId, amplifyAppIds) {
138
285
  const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(this, "GithubOidcProvider", this.formatArn({
139
286
  service: "iam",
140
287
  region: "",
@@ -153,22 +300,9 @@ export class AmplifyAppShellStack extends Stack {
153
300
  },
154
301
  }),
155
302
  });
156
- ciRole.addToPolicy(new iam.PolicyStatement({
157
- sid: "StartAndInspectAmplifyJobs",
158
- actions: ["amplify:StartJob", "amplify:ListJobs", "amplify:GetJob", "amplify:ListBranches"],
159
- resources: amplifyAppIds.flatMap((appId) => [
160
- this.formatArn({ service: "amplify", resource: "apps", resourceName: appId, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
161
- this.formatArn({ service: "amplify", resource: "apps", resourceName: `${appId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
162
- ]),
163
- }));
164
- ciRole.addToPolicy(new iam.PolicyStatement({
165
- sid: "ReadSiteSecretsForJwtMinting",
166
- actions: ["ssm:GetParameter", "ssm:GetParameters"],
167
- resources: [
168
- this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/${this.cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
169
- this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/shared/${this.cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
170
- ],
171
- }));
303
+ for (const statement of this.authoringStatements(config, cmsAppId, amplifyAppIds)) {
304
+ ciRole.addToPolicy(statement);
305
+ }
172
306
  ciRole.addToPolicy(new iam.PolicyStatement({
173
307
  sid: "WhoAmI",
174
308
  actions: ["sts:GetCallerIdentity"],
@@ -203,6 +337,11 @@ export class AmplifyAppShellStack extends Stack {
203
337
  platform: "WEB",
204
338
  iamServiceRole: readerServiceRole.roleArn,
205
339
  buildSpec: readerBuildSpec(config),
340
+ customRules: [
341
+ ...(reader.redirects ?? []),
342
+ { source: "/<*>", target: "/404.html", status: "404-200" },
343
+ ],
344
+ customHeaders: readerCustomHeaders(reader.customHeaders),
206
345
  });
207
346
  const readerBranch = new amplify.CfnBranch(this, "ReaderBranch", {
208
347
  appId: readerApp.attrAppId,
@@ -219,7 +358,7 @@ export class AmplifyAppShellStack extends Stack {
219
358
  const readerDomain = new amplify.CfnDomain(this, "ReaderDomain", {
220
359
  appId: readerApp.attrAppId,
221
360
  domainName: reader.domainName,
222
- subDomainSettings: [{ branchName: readerBranchName, prefix: "" }],
361
+ subDomainSettings: (reader.domainPrefixes ?? [""]).map((prefix) => ({ branchName: readerBranchName, prefix })),
223
362
  });
224
363
  readerDomain.addResourceDependency(readerBranch);
225
364
  }