@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,20 +1,46 @@
1
- import { ArnFormat, CfnOutput, Duration, Stack } from "aws-cdk-lib";
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
4
  import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider.js";
5
5
  import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs.js";
6
- import { resolveStagingDomainName } from "./site-config.js";
6
+ import { PROSPECTUS_PAPYRUS_SITE_ROUTES, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveProspectusAccountsEndpoint, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
7
7
  function environmentVariables(variables) {
8
8
  return Object.entries(variables).map(([name, value]) => ({ name, value }));
9
9
  }
10
+ function readerCustomHeaders(rules) {
11
+ if (!rules)
12
+ return undefined;
13
+ const lines = ["customHeaders:"];
14
+ for (const rule of rules) {
15
+ lines.push(` - pattern: ${JSON.stringify(rule.pattern)}`, " headers:");
16
+ for (const [key, value] of Object.entries(rule.headers)) {
17
+ lines.push(` - key: ${JSON.stringify(key)}`, ` value: ${JSON.stringify(value)}`);
18
+ }
19
+ }
20
+ return lines.join("\n") + "\n";
21
+ }
10
22
  /**
11
23
  * Provisions the Amplify *app shell* of a Papyrus publication from its
12
24
  * site.json:
13
25
  *
14
26
  * - CMS app (WEB_COMPUTE) with branches `main` (production, owns the backend)
15
27
  * and `staging` (frontend only, reads the production backend)
16
- * - for `markus-static` sites, a reader app (WEB) with branch `main`
17
- * - one domain association per branch/app, IAM service and compute roles
28
+ * - for `markus-static` sites, a reader app (WEB) with branch `main`; a
29
+ * `markus-static` site may omit `cms` to get the reader app alone (no CMS
30
+ * app, no CMS roles, no authoring role)
31
+ * - a domain association per branch/app when a domain is configured (without
32
+ * one, no AWS::Amplify::Domain is created and the default amplifyapp.com
33
+ * URL is used), IAM service and compute roles
34
+ * - `cms.staging: false` omits the staging branch and its domain
35
+ *
36
+ * Google sign-in is always on: the branches never set PAPYRUS_DISABLE_GOOGLE_OAUTH,
37
+ and `cms.cognitoDomainPrefix` becomes the hosted-UI domain
38
+ (PAPYRUS_COGNITO_DOMAIN_PREFIX) only when `cms.applyCognitoDomainPrefix` is true
39
+ (PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX); otherwise Amplify's generated domain is kept. The secrets GOOGLE_CLIENT_ID and
40
+ GOOGLE_CLIENT_SECRET are Amplify backend secrets set in the console, never here.
41
+ The stack also creates the `<siteId>-papyrus-authoring` role. The CLI signs
42
+ * AppSync requests (SigV4) with credentials for that role (SSO, OIDC or any
43
+ * principal in the account that may assume it); no token is stored anywhere.
18
44
  *
19
45
  * No repository and no access token are configured: the apps are manual-deploy
20
46
  * until Amplify's GitHub App is connected once in the console
@@ -26,8 +52,16 @@ export class AmplifyAppShellStack extends Stack {
26
52
  readerAppId;
27
53
  constructor(scope, id, config, props) {
28
54
  super(scope, id, props);
55
+ if (!hasCmsApp(config)) {
56
+ if (!config.reader)
57
+ throw new Error(`site ${config.siteId} has neither a cms section nor a reader`);
58
+ this.readerAppId = this.addReaderApp(config, config.reader);
59
+ return;
60
+ }
29
61
  const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
62
+ const stagingEnabled = isStagingEnabled(config);
30
63
  const stagingDomainName = resolveStagingDomainName(config);
64
+ const cmsHostName = resolveCmsHostName(config);
31
65
  const cmsServiceRole = new iam.Role(this, "AmplifyServiceRole", {
32
66
  assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
33
67
  description: `Backend deploy and staging build service role for Amplify app ${cmsAppName} (AdministratorAccess: documented risk)`,
@@ -41,6 +75,27 @@ export class AmplifyAppShellStack extends Stack {
41
75
  iam.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchLogsFullAccess"),
42
76
  ],
43
77
  });
78
+ const revalidateSecretParameterName = config.frontend === "pretext" ? resolveRevalidateSecretParameterName(config) : undefined;
79
+ if (revalidateSecretParameterName) {
80
+ computeRole.addToPolicy(new iam.PolicyStatement({
81
+ sid: "ReadRevalidateSecret",
82
+ actions: ["ssm:GetParameter"],
83
+ resources: [
84
+ this.formatArn({ service: "ssm", resource: "parameter", resourceName: revalidateSecretParameterName.slice(1), arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
85
+ ],
86
+ }));
87
+ }
88
+ const prospectusAccountsApiStageArn = config.cms.prospectusAccountsApiStageArn;
89
+ const prospectusEnvironment = prospectusAccountsApiStageArn
90
+ ? { PAPYRUS_PROSPECTUS_ACCOUNTS_ENDPOINT: resolveProspectusAccountsEndpoint(prospectusAccountsApiStageArn) }
91
+ : {};
92
+ if (prospectusAccountsApiStageArn) {
93
+ computeRole.addToPolicy(new iam.PolicyStatement({
94
+ sid: "InvokeProspectusAccountsAsRegisteredSite",
95
+ actions: ["execute-api:Invoke"],
96
+ resources: PROSPECTUS_PAPYRUS_SITE_ROUTES.map((route) => `${prospectusAccountsApiStageArn}/${route}`),
97
+ }));
98
+ }
44
99
  const cmsApp = new amplify.CfnApp(this, "App", {
45
100
  name: cmsAppName,
46
101
  description: `Papyrus newsroom CMS for ${config.siteId} (WEB_COMPUTE)`,
@@ -49,11 +104,32 @@ export class AmplifyAppShellStack extends Stack {
49
104
  computeRoleArn: computeRole.roleArn,
50
105
  buildSpec: cmsProductionBuildSpec(config),
51
106
  jobConfig: config.cms.buildComputeType ? { buildComputeType: config.cms.buildComputeType } : undefined,
52
- customRules: [{ source: "/<*>", target: "/index.html", status: "404-200" }],
107
+ customRules: [
108
+ ...(config.cms.redirects ?? []).map((redirect) => ({
109
+ source: `https://${redirect.source}`,
110
+ target: `https://${resolveCmsHostName(config)}`,
111
+ status: String(redirect.status),
112
+ })),
113
+ { source: "/<*>", target: "/index.html", status: "404-200" },
114
+ ],
53
115
  });
54
116
  const stagingMode = config.frontend === "markus-static"
55
117
  ? { PAPYRUS_STAGING_PREVIEW: "static" }
56
118
  : { PAPYRUS_CONTENT_SOURCE: "drafts" };
119
+ const productionOrigin = cmsHostName
120
+ ? `https://${cmsHostName}/`
121
+ : Fn.join("", ["https://main.", cmsApp.attrDefaultDomain, "/"]);
122
+ const stagingOrigin = stagingDomainName
123
+ ? `https://${stagingDomainName}/`
124
+ : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain, "/"]);
125
+ const sharedRootDomain = config.cms.domainPrefix !== undefined;
126
+ const defaultOriginsToAllow = [
127
+ ...(config.cms.domainName ? [] : [productionOrigin]),
128
+ ...(stagingEnabled && !stagingDomainName ? [stagingOrigin] : []),
129
+ ];
130
+ const oauthRedirectUrls = defaultOriginsToAllow.length === 0
131
+ ? config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS
132
+ : Fn.join(",", [config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS, ...defaultOriginsToAllow]);
57
133
  const productionBranch = new amplify.CfnBranch(this, "Branch", {
58
134
  appId: cmsApp.attrAppId,
59
135
  branchName: "main",
@@ -62,50 +138,132 @@ export class AmplifyAppShellStack extends Stack {
62
138
  enablePerformanceMode: false,
63
139
  environmentVariables: environmentVariables({
64
140
  ...config.cms.environment,
141
+ PAPYRUS_COGNITO_DOMAIN_PREFIX: config.cms.cognitoDomainPrefix,
142
+ ...(config.cms.applyCognitoDomainPrefix ? { PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX: "true" } : {}),
143
+ PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
65
144
  PAPYRUS_SITE_BRAND: config.brand,
66
145
  SITE_ENV: "production",
67
146
  PAPYRUS_CONTENT_SOURCE: "published",
147
+ ...(revalidateSecretParameterName ? { PAPYRUS_REVALIDATE_SECRET_PARAMETER: revalidateSecretParameterName } : {}),
148
+ ...prospectusEnvironment,
68
149
  }),
69
150
  });
70
- const stagingBranch = new amplify.CfnBranch(this, "StagingBranch", {
71
- appId: cmsApp.attrAppId,
72
- branchName: "staging",
73
- stage: "BETA",
74
- enableAutoBuild: true,
75
- enablePerformanceMode: false,
76
- buildSpec: cmsStagingBuildSpec(config),
77
- environmentVariables: environmentVariables({
78
- ...config.cms.environment,
79
- PAPYRUS_SITE_BRAND: config.brand,
80
- SITE_ENV: "staging",
81
- ...stagingMode,
82
- NEXT_PUBLIC_PAPYRUS_STAGING_URL: `https://${stagingDomainName}`,
83
- }),
84
- });
85
- const productionDomain = new amplify.CfnDomain(this, "Domain", {
86
- appId: cmsApp.attrAppId,
87
- domainName: config.cms.domainName,
88
- subDomainSettings: [{ branchName: "main", prefix: "" }],
89
- });
90
- productionDomain.addResourceDependency(productionBranch);
91
- const stagingDomain = new amplify.CfnDomain(this, "StagingDomain", {
92
- appId: cmsApp.attrAppId,
93
- domainName: stagingDomainName,
94
- subDomainSettings: [{ branchName: "staging", prefix: "" }],
95
- });
96
- stagingDomain.addResourceDependency(stagingBranch);
151
+ let stagingBranch;
152
+ if (stagingEnabled) {
153
+ stagingBranch = new amplify.CfnBranch(this, "StagingBranch", {
154
+ appId: cmsApp.attrAppId,
155
+ branchName: "staging",
156
+ stage: "BETA",
157
+ enableAutoBuild: true,
158
+ enablePerformanceMode: false,
159
+ buildSpec: cmsStagingBuildSpec(config),
160
+ environmentVariables: environmentVariables({
161
+ ...config.cms.environment,
162
+ PAPYRUS_COGNITO_DOMAIN_PREFIX: config.cms.cognitoDomainPrefix,
163
+ ...(config.cms.applyCognitoDomainPrefix ? { PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX: "true" } : {}),
164
+ PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
165
+ PAPYRUS_SITE_BRAND: config.brand,
166
+ SITE_ENV: "staging",
167
+ ...stagingMode,
168
+ ...prospectusEnvironment,
169
+ NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
170
+ ? `https://${stagingDomainName}`
171
+ : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]),
172
+ }),
173
+ });
174
+ if (stagingDomainName && !sharedRootDomain) {
175
+ const stagingDomain = new amplify.CfnDomain(this, "StagingDomain", {
176
+ appId: cmsApp.attrAppId,
177
+ domainName: stagingDomainName,
178
+ subDomainSettings: [{ branchName: "staging", prefix: "" }],
179
+ });
180
+ stagingDomain.addResourceDependency(stagingBranch);
181
+ }
182
+ }
183
+ if (config.cms.domainName) {
184
+ const subDomainSettings = sharedRootDomain
185
+ ? [
186
+ { branchName: "main", prefix: config.cms.domainPrefix },
187
+ ...(config.cms.redirects ?? []).map((redirect) => ({
188
+ branchName: "main",
189
+ prefix: resolveRedirectSubDomainPrefix(config.cms.domainName, redirect.source),
190
+ })),
191
+ ...(stagingBranch ? [{ branchName: "staging", prefix: config.cms.stagingDomainPrefix }] : []),
192
+ ]
193
+ : [{ branchName: "main", prefix: "" }];
194
+ const productionDomain = new amplify.CfnDomain(this, "Domain", {
195
+ appId: cmsApp.attrAppId,
196
+ domainName: config.cms.domainName,
197
+ subDomainSettings,
198
+ });
199
+ productionDomain.addResourceDependency(productionBranch);
200
+ if (sharedRootDomain && stagingBranch) {
201
+ productionDomain.addResourceDependency(stagingBranch);
202
+ }
203
+ }
97
204
  this.cmsAppId = cmsApp.attrAppId;
98
205
  new CfnOutput(this, "CmsAppId", { value: cmsApp.attrAppId, description: `Amplify app id for ${cmsAppName}` });
99
- new CfnOutput(this, "CmsOrigin", { value: `https://${config.cms.domainName}/` });
100
- new CfnOutput(this, "StagingOrigin", { value: `https://${stagingDomainName}/` });
206
+ new CfnOutput(this, "CmsOrigin", { value: productionOrigin });
207
+ new CfnOutput(this, "CmsComputeRoleArn", {
208
+ value: computeRole.roleArn,
209
+ description: `SSR compute role for ${cmsAppName}; the roleArn of its Prospectus site registration`,
210
+ });
211
+ if (stagingEnabled) {
212
+ new CfnOutput(this, "StagingOrigin", { value: stagingOrigin });
213
+ }
101
214
  if (config.reader) {
102
- this.readerAppId = this.addReaderApp(config, config.reader, cmsApp.attrAppId);
215
+ this.readerAppId = this.addReaderApp(config, config.reader);
103
216
  }
217
+ const amplifyAppIds = [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])];
218
+ this.addAuthoringRole(config, cmsApp.attrAppId, amplifyAppIds);
104
219
  if (config.github) {
105
- this.addGithubCiRole(config, config.github, [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])]);
220
+ this.addGithubCiRole(config, config.github, cmsApp.attrAppId, amplifyAppIds);
106
221
  }
107
222
  }
108
- addGithubCiRole(config, github, amplifyAppIds) {
223
+ authoringStatements(config, cmsAppId, amplifyAppIds) {
224
+ const mediaBucketArn = `arn:${this.partition}:s3:::amplify-${cmsAppId}-*`;
225
+ return [
226
+ new iam.PolicyStatement({
227
+ sid: "SignAppSyncAuthoringRequests",
228
+ actions: ["appsync:GraphQL"],
229
+ resources: ["Query", "Mutation"].map((typeName) => this.formatArn({ service: "appsync", resource: "apis", resourceName: `*/types/${typeName}/fields/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME })),
230
+ }),
231
+ new iam.PolicyStatement({
232
+ sid: "ReadWriteMediaAndPreview",
233
+ actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
234
+ resources: [`${mediaBucketArn}/media/*`, `${mediaBucketArn}/${resolveStoragePreviewPrefix(config)}*`],
235
+ }),
236
+ new iam.PolicyStatement({
237
+ sid: "ListMediaAndPreview",
238
+ actions: ["s3:ListBucket"],
239
+ resources: [mediaBucketArn],
240
+ conditions: { StringLike: { "s3:prefix": ["media/*", `${resolveStoragePreviewPrefix(config)}*`] } },
241
+ }),
242
+ new iam.PolicyStatement({
243
+ sid: "StartAndInspectAmplifyJobs",
244
+ actions: ["amplify:StartJob", "amplify:ListJobs", "amplify:GetJob", "amplify:ListBranches"],
245
+ resources: amplifyAppIds.flatMap((appId) => [
246
+ this.formatArn({ service: "amplify", resource: "apps", resourceName: appId, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
247
+ this.formatArn({ service: "amplify", resource: "apps", resourceName: `${appId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
248
+ ]),
249
+ }),
250
+ ];
251
+ }
252
+ addAuthoringRole(config, cmsAppId, amplifyAppIds) {
253
+ const authoringRole = new iam.Role(this, "PapyrusAuthoringRole", {
254
+ roleName: `${config.siteId}-papyrus-authoring`,
255
+ 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.`,
256
+ assumedBy: new iam.AccountRootPrincipal(),
257
+ });
258
+ for (const statement of this.authoringStatements(config, cmsAppId, amplifyAppIds)) {
259
+ authoringRole.addToPolicy(statement);
260
+ }
261
+ new CfnOutput(this, "PapyrusAuthoringRoleArn", {
262
+ value: authoringRole.roleArn,
263
+ description: `Role the Papyrus CLI assumes for ${config.siteId} (profile role_arn)`,
264
+ });
265
+ }
266
+ addGithubCiRole(config, github, cmsAppId, amplifyAppIds) {
109
267
  const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(this, "GithubOidcProvider", this.formatArn({
110
268
  service: "iam",
111
269
  region: "",
@@ -124,22 +282,9 @@ export class AmplifyAppShellStack extends Stack {
124
282
  },
125
283
  }),
126
284
  });
127
- ciRole.addToPolicy(new iam.PolicyStatement({
128
- sid: "StartAndInspectAmplifyJobs",
129
- actions: ["amplify:StartJob", "amplify:ListJobs", "amplify:GetJob", "amplify:ListBranches"],
130
- resources: amplifyAppIds.flatMap((appId) => [
131
- this.formatArn({ service: "amplify", resource: "apps", resourceName: appId, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
132
- this.formatArn({ service: "amplify", resource: "apps", resourceName: `${appId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
133
- ]),
134
- }));
135
- ciRole.addToPolicy(new iam.PolicyStatement({
136
- sid: "ReadSiteSecretsForJwtMinting",
137
- actions: ["ssm:GetParameter", "ssm:GetParameters"],
138
- resources: [
139
- this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/${this.cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
140
- this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/shared/${this.cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
141
- ],
142
- }));
285
+ for (const statement of this.authoringStatements(config, cmsAppId, amplifyAppIds)) {
286
+ ciRole.addToPolicy(statement);
287
+ }
143
288
  ciRole.addToPolicy(new iam.PolicyStatement({
144
289
  sid: "WhoAmI",
145
290
  actions: ["sts:GetCallerIdentity"],
@@ -150,27 +295,19 @@ export class AmplifyAppShellStack extends Stack {
150
295
  sid: "DeployThisSitesAppShellStack",
151
296
  actions: ["cloudformation:*"],
152
297
  resources: [
153
- this.formatArn({ service: "cloudformation", resource: "stack", resourceName: `amplify-app-shell-${config.siteId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
298
+ this.formatArn({ service: "cloudformation", resource: "stack", resourceName: `${resolveStackName(config)}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
154
299
  ],
155
300
  }));
156
301
  }
157
302
  new CfnOutput(this, "GithubCiRoleArn", { value: ciRole.roleArn, description: `Role for GitHub Actions in ${github.owner}/${github.repo}` });
158
303
  }
159
- addReaderApp(config, reader, cmsAppId) {
304
+ addReaderApp(config, reader) {
160
305
  const readerAppName = reader.appName ?? `${config.siteId}-reader`;
161
306
  const readerBranchName = reader.branchName ?? "main";
162
307
  const readerServiceRole = new iam.Role(this, "ReaderServiceRole", {
163
308
  assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
164
- description: `Static reader build role for Amplify app ${readerAppName} (reads the CMS app's SSM secrets and media only)`,
309
+ description: `Static reader build role for Amplify app ${readerAppName} (reads published media only; content is read as a Cognito guest)`,
165
310
  });
166
- readerServiceRole.addToPolicy(new iam.PolicyStatement({
167
- sid: "ReadSiteSecretsForJwtMinting",
168
- actions: ["ssm:GetParameter", "ssm:GetParameters"],
169
- resources: [
170
- this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/${cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
171
- this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/shared/${cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
172
- ],
173
- }));
174
311
  readerServiceRole.addToPolicy(new iam.PolicyStatement({
175
312
  sid: "ReadMediaForExport",
176
313
  actions: ["s3:GetObject"],
@@ -182,6 +319,11 @@ export class AmplifyAppShellStack extends Stack {
182
319
  platform: "WEB",
183
320
  iamServiceRole: readerServiceRole.roleArn,
184
321
  buildSpec: readerBuildSpec(config),
322
+ customRules: [
323
+ ...(reader.redirects ?? []),
324
+ { source: "/<*>", target: "/404.html", status: "404-200" },
325
+ ],
326
+ customHeaders: readerCustomHeaders(reader.customHeaders),
185
327
  });
186
328
  const readerBranch = new amplify.CfnBranch(this, "ReaderBranch", {
187
329
  appId: readerApp.attrAppId,
@@ -194,14 +336,20 @@ export class AmplifyAppShellStack extends Stack {
194
336
  SITE_ENV: "production",
195
337
  }),
196
338
  });
197
- const readerDomain = new amplify.CfnDomain(this, "ReaderDomain", {
198
- appId: readerApp.attrAppId,
199
- domainName: reader.domainName,
200
- subDomainSettings: [{ branchName: readerBranchName, prefix: "" }],
201
- });
202
- readerDomain.addResourceDependency(readerBranch);
339
+ if (reader.domainName) {
340
+ const readerDomain = new amplify.CfnDomain(this, "ReaderDomain", {
341
+ appId: readerApp.attrAppId,
342
+ domainName: reader.domainName,
343
+ subDomainSettings: (reader.domainPrefixes ?? [""]).map((prefix) => ({ branchName: readerBranchName, prefix })),
344
+ });
345
+ readerDomain.addResourceDependency(readerBranch);
346
+ }
203
347
  new CfnOutput(this, "ReaderAppId", { value: readerApp.attrAppId, description: `Amplify app id for ${readerAppName}` });
204
- new CfnOutput(this, "ReaderOrigin", { value: `https://${reader.domainName}/` });
348
+ new CfnOutput(this, "ReaderOrigin", {
349
+ value: reader.domainName
350
+ ? `https://${reader.domainName}/`
351
+ : Fn.join("", [`https://${readerBranchName}.`, readerApp.attrDefaultDomain, "/"]),
352
+ });
205
353
  return readerApp.attrAppId;
206
354
  }
207
355
  }