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