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