@anthusai/papyrus 0.0.0 → 1.0.0-next.10

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 (282) hide show
  1. package/LICENSE +21 -0
  2. package/amplify/auth/resource.js +61 -0
  3. package/amplify/auth/resource.ts +65 -0
  4. package/amplify/data/resource.js +2069 -0
  5. package/amplify/data/resource.ts +2185 -0
  6. package/amplify/data/schema.js +1882 -0
  7. package/amplify/data/schema.ts +1990 -0
  8. package/amplify/functions/assignment-action/handler.ts +1416 -0
  9. package/amplify/functions/assignment-action/resource.js +8 -0
  10. package/amplify/functions/assignment-action/resource.ts +9 -0
  11. package/amplify/functions/category-action/handler.ts +2259 -0
  12. package/amplify/functions/category-action/resource.js +8 -0
  13. package/amplify/functions/category-action/resource.ts +9 -0
  14. package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
  15. package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
  16. package/amplify/functions/console-chat-responder/Dockerfile +19 -0
  17. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
  18. package/amplify/functions/console-chat-responder/resource.js +153 -0
  19. package/amplify/functions/console-chat-responder/resource.ts +179 -0
  20. package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
  21. package/amplify/functions/content-actions/handler.py +149 -0
  22. package/amplify/functions/content-actions/requirements.txt +4 -0
  23. package/amplify/functions/content-actions/resource.js +40 -0
  24. package/amplify/functions/content-actions/resource.ts +54 -0
  25. package/amplify/functions/email-submission-processor/handler.py +46 -0
  26. package/amplify/functions/email-submission-processor/requirements.txt +2 -0
  27. package/amplify/functions/email-submission-processor/resource.js +56 -0
  28. package/amplify/functions/email-submission-processor/resource.ts +69 -0
  29. package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
  30. package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
  31. package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
  32. package/amplify/functions/knowledge-query/handler.py +1 -0
  33. package/amplify/functions/knowledge-query/requirements.txt +3 -0
  34. package/amplify/functions/knowledge-query/resource.js +50 -0
  35. package/amplify/functions/knowledge-query/resource.ts +61 -0
  36. package/amplify/functions/manage-user-role/handler.ts +799 -0
  37. package/amplify/functions/manage-user-role/resource.js +8 -0
  38. package/amplify/functions/manage-user-role/resource.ts +9 -0
  39. package/amplify/functions/model-attachment-upload/handler.ts +387 -0
  40. package/amplify/functions/model-attachment-upload/resource.js +8 -0
  41. package/amplify/functions/model-attachment-upload/resource.ts +9 -0
  42. package/amplify/functions/newsroom-summary/handler.ts +321 -0
  43. package/amplify/functions/newsroom-summary/resource.js +8 -0
  44. package/amplify/functions/newsroom-summary/resource.ts +9 -0
  45. package/amplify/functions/procedure-action/handler.ts +621 -0
  46. package/amplify/functions/procedure-action/resource.js +8 -0
  47. package/amplify/functions/procedure-action/resource.ts +9 -0
  48. package/amplify/functions/reader-settings/handler.ts +322 -0
  49. package/amplify/functions/reader-settings/resource.js +8 -0
  50. package/amplify/functions/reader-settings/resource.ts +9 -0
  51. package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
  52. package/amplify/functions/ses-inbound-receive/resource.js +8 -0
  53. package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
  54. package/amplify/functions/shared/amplify-secrets.ts +65 -0
  55. package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
  56. package/amplify/functions/shared/email-mime-intake.ts +196 -0
  57. package/amplify/functions/shared/email-submission.ts +415 -0
  58. package/amplify/functions/shared/inbound-email-intake.ts +37 -0
  59. package/amplify/functions/shared/lambda-data-client.ts +21 -0
  60. package/amplify/functions/shared/model-payloads.ts +186 -0
  61. package/amplify/functions/shared/python-bundle.js +130 -0
  62. package/amplify/functions/shared/python-bundle.ts +162 -0
  63. package/amplify/functions/shared/slack-events.ts +111 -0
  64. package/amplify/functions/slack-delivery/handler.py +112 -0
  65. package/amplify/functions/slack-delivery/requirements.txt +2 -0
  66. package/amplify/functions/slack-delivery/resource.js +38 -0
  67. package/amplify/functions/slack-delivery/resource.ts +51 -0
  68. package/amplify/functions/slack-events/handler.ts +155 -0
  69. package/amplify/functions/slack-events/resource.js +11 -0
  70. package/amplify/functions/slack-events/resource.ts +12 -0
  71. package/amplify/inbound-email/stack.js +46 -0
  72. package/amplify/inbound-email/stack.ts +61 -0
  73. package/amplify/package.json +3 -0
  74. package/amplify/rebuild-trigger-settings.js +22 -0
  75. package/amplify/rebuild-trigger-settings.ts +42 -0
  76. package/amplify/site-backend-config.js +1 -0
  77. package/amplify/site-backend-config.ts +36 -0
  78. package/amplify/site-backend.js +615 -0
  79. package/amplify/site-backend.ts +739 -0
  80. package/amplify/storage/resource.js +21 -0
  81. package/amplify/storage/resource.ts +22 -0
  82. package/app/%5F_preview/[[...path]]/route.ts +8 -0
  83. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
  84. package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
  85. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
  86. package/app/[year]/[month]/[day]/page.tsx +22 -0
  87. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
  88. package/app/api/archive/editions/route.ts +18 -0
  89. package/app/api/media/[...path]/route.ts +35 -0
  90. package/app/api/revalidate/route.ts +94 -0
  91. package/app/archive/page.tsx +24 -0
  92. package/app/articles/[slug]/page.tsx +35 -0
  93. package/app/globals.css +10345 -0
  94. package/app/layout.tsx +142 -0
  95. package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
  96. package/app/newsroom/[section]/page.tsx +47 -0
  97. package/app/newsroom/articles/[id]/page.tsx +15 -0
  98. package/app/newsroom/articles/page.tsx +13 -0
  99. package/app/newsroom/forum/[threadId]/page.tsx +35 -0
  100. package/app/newsroom/insights/[threadId]/page.tsx +22 -0
  101. package/app/newsroom/insights/page.tsx +14 -0
  102. package/app/newsroom/layout.tsx +12 -0
  103. package/app/newsroom/page.tsx +31 -0
  104. package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
  105. package/app/page.tsx +272 -0
  106. package/app/papyrus-theme.css +11 -0
  107. package/app/robots.ts +11 -0
  108. package/app/settings/page.tsx +14 -0
  109. package/app/tailwind.css +65 -0
  110. package/bin/papyrus-app.mjs +20 -0
  111. package/bin/papyrus-infra.mjs +95 -0
  112. package/components/ai-elements/conversation.tsx +168 -0
  113. package/components/ai-elements/message.tsx +360 -0
  114. package/components/ai-elements/model-selector.tsx +30 -0
  115. package/components/ai-elements/prompt-input.tsx +1461 -0
  116. package/components/ai-elements/shimmer.tsx +76 -0
  117. package/components/ai-elements/suggestion.tsx +54 -0
  118. package/components/ai-elements/tool.tsx +173 -0
  119. package/components/amplify-client-provider.tsx +140 -0
  120. package/components/archive-grid.tsx +89 -0
  121. package/components/archive-shell.tsx +41 -0
  122. package/components/article-video.tsx +156 -0
  123. package/components/blog-page-background.tsx +20 -0
  124. package/components/dev-sandbox-editor-sign-in.tsx +60 -0
  125. package/components/markus-ir-preview.tsx +300 -0
  126. package/components/news-desk-client-provider.tsx +211 -0
  127. package/components/news-desk-page.tsx +116 -0
  128. package/components/news-desk-taxonomy-client.ts +3684 -0
  129. package/components/newspaper.tsx +1929 -0
  130. package/components/newsroom-access-gate.tsx +66 -0
  131. package/components/newsroom-article-editor.tsx +741 -0
  132. package/components/newsroom-articles-view.tsx +129 -0
  133. package/components/newsroom-assignments-view.tsx +664 -0
  134. package/components/newsroom-ops-overview.tsx +226 -0
  135. package/components/newsroom-ops-shell.tsx +321 -0
  136. package/components/newsroom-references-view.tsx +598 -0
  137. package/components/newsroom-topics-view.tsx +425 -0
  138. package/components/papyrus-console-shell.tsx +1912 -0
  139. package/components/pictogram-figure.tsx +65 -0
  140. package/components/presentation-footer.tsx +146 -0
  141. package/components/reader-auth-control.tsx +162 -0
  142. package/components/reader-auth-state.ts +122 -0
  143. package/components/reader-settings.ts +332 -0
  144. package/components/reader-theme-control.tsx +85 -0
  145. package/components/reference-source-preview.tsx +163 -0
  146. package/components/settings-page.tsx +239 -0
  147. package/components/staging-banner.tsx +11 -0
  148. package/components/topic-steering-workspace.tsx +18005 -0
  149. package/components/ui/badge.tsx +30 -0
  150. package/components/ui/button-group.tsx +87 -0
  151. package/components/ui/button.tsx +58 -0
  152. package/components/ui/card.tsx +32 -0
  153. package/components/ui/command.tsx +194 -0
  154. package/components/ui/dialog.tsx +158 -0
  155. package/components/ui/dropdown-menu.tsx +266 -0
  156. package/components/ui/hover-card.tsx +51 -0
  157. package/components/ui/input-group.tsx +158 -0
  158. package/components/ui/input.tsx +20 -0
  159. package/components/ui/scroll-area.tsx +16 -0
  160. package/components/ui/select.tsx +199 -0
  161. package/components/ui/separator.tsx +23 -0
  162. package/components/ui/sheet.tsx +92 -0
  163. package/components/ui/spinner.tsx +10 -0
  164. package/components/ui/tabs.tsx +113 -0
  165. package/components/ui/textarea.tsx +18 -0
  166. package/components/ui/tooltip.tsx +66 -0
  167. package/components/use-resolved-papyrus-theme.ts +47 -0
  168. package/infra/amplify-app-shell.js +234 -0
  169. package/infra/amplify-app-shell.ts +284 -0
  170. package/infra/build-specs.js +85 -0
  171. package/infra/build-specs.ts +131 -0
  172. package/infra/github-oidc-provider.js +24 -0
  173. package/infra/github-oidc-provider.ts +30 -0
  174. package/infra/index.js +15 -0
  175. package/infra/site-config.js +186 -0
  176. package/infra/site-config.ts +235 -0
  177. package/lib/amplify-outputs-guard.ts +24 -0
  178. package/lib/amplify-outputs-path.ts +16 -0
  179. package/lib/amplify-server-runtime.ts +63 -0
  180. package/lib/archive-data.ts +30 -0
  181. package/lib/archive-types.ts +15 -0
  182. package/lib/articles.ts +320 -0
  183. package/lib/cached-content-repository.ts +90 -0
  184. package/lib/category-dashboard.ts +37 -0
  185. package/lib/category-repository.ts +1907 -0
  186. package/lib/console-chat-client.ts +1316 -0
  187. package/lib/console-web-context.ts +118 -0
  188. package/lib/content-actions-client.ts +98 -0
  189. package/lib/content-actions-demo.ts +190 -0
  190. package/lib/content-load-timing.ts +23 -0
  191. package/lib/content-repository.ts +43 -0
  192. package/lib/content-source-context.ts +26 -0
  193. package/lib/content-types.ts +104 -0
  194. package/lib/define-site.js +3 -0
  195. package/lib/define-site.ts +21 -0
  196. package/lib/demo-amplify-outputs.ts +15 -0
  197. package/lib/dev-sandbox-editor-auth.ts +18 -0
  198. package/lib/doctrine.ts +128 -0
  199. package/lib/edition-routes.ts +155 -0
  200. package/lib/edition-sections.ts +111 -0
  201. package/lib/empty-edition-layout-plan.ts +109 -0
  202. package/lib/empty-theme.css +1 -0
  203. package/lib/excerpts.ts +8 -0
  204. package/lib/graphql-content-repository.ts +1140 -0
  205. package/lib/image-url.ts +10 -0
  206. package/lib/layout-plan.ts +981 -0
  207. package/lib/layout-scenarios.ts +564 -0
  208. package/lib/markus-body.ts +168 -0
  209. package/lib/markus-ir.ts +235 -0
  210. package/lib/markus-projection.ts +243 -0
  211. package/lib/markus-to-article.ts +93 -0
  212. package/lib/news-desk-session.ts +160 -0
  213. package/lib/newspaper-layout.ts +3459 -0
  214. package/lib/newsroom-articles.ts +325 -0
  215. package/lib/newsroom-assignments.ts +173 -0
  216. package/lib/newsroom-base-path.ts +48 -0
  217. package/lib/newsroom-card-layout.ts +102 -0
  218. package/lib/newsroom-category-drilldown.ts +133 -0
  219. package/lib/newsroom-demo-articles.ts +90 -0
  220. package/lib/newsroom-demo-dashboard.ts +267 -0
  221. package/lib/newsroom-demo-profile.ts +46 -0
  222. package/lib/newsroom-forum-routes.ts +153 -0
  223. package/lib/newsroom-index-filters.ts +202 -0
  224. package/lib/newsroom-knowledge-query-request.ts +51 -0
  225. package/lib/newsroom-list-selection.ts +42 -0
  226. package/lib/newsroom-media-backend.ts +142 -0
  227. package/lib/newsroom-media.ts +182 -0
  228. package/lib/newsroom-nav.ts +44 -0
  229. package/lib/newsroom-references.ts +116 -0
  230. package/lib/newsroom-sections.ts +52 -0
  231. package/lib/newsroom-topics.ts +118 -0
  232. package/lib/papyrus-object-kinds.ts +24 -0
  233. package/lib/papyrus-web-locations.ts +480 -0
  234. package/lib/pilobol-sample.ts +167 -0
  235. package/lib/presentation-footer.ts +92 -0
  236. package/lib/pretext-layout.ts +179 -0
  237. package/lib/public-placeholder-config.ts +102 -0
  238. package/lib/publication-items.ts +181 -0
  239. package/lib/reader-route-config.ts +1 -0
  240. package/lib/reader-static-params.ts +56 -0
  241. package/lib/reader-storage-url.ts +63 -0
  242. package/lib/reference-display.ts +74 -0
  243. package/lib/reference-policy.ts +97 -0
  244. package/lib/reference-source-preview.ts +175 -0
  245. package/lib/renderer-config.ts +50 -0
  246. package/lib/renderer.ts +87 -0
  247. package/lib/reporting-story-budget.ts +638 -0
  248. package/lib/semantic-graph.ts +554 -0
  249. package/lib/site-brand-tenant.ts +22 -0
  250. package/lib/site-brand.ts +194 -0
  251. package/lib/site-env.ts +52 -0
  252. package/lib/site-renderer.ts +17 -0
  253. package/lib/site-stack.ts +195 -0
  254. package/lib/staging-gate.ts +37 -0
  255. package/lib/staging-preview-object.ts +98 -0
  256. package/lib/themed-image.ts +30 -0
  257. package/lib/utils.ts +6 -0
  258. package/lib/video-mode.ts +42 -0
  259. package/lib/video-script.ts +63 -0
  260. package/middleware.ts +103 -0
  261. package/package.json +318 -1
  262. package/renderers/markus/index.ts +25 -0
  263. package/renderers/markus/stubs.tsx +33 -0
  264. package/renderers/pretext/article-page.tsx +169 -0
  265. package/renderers/pretext/index.ts +30 -0
  266. package/renderers/pretext/layouts.ts +16 -0
  267. package/renderers/pretext/presentation-shell.tsx +470 -0
  268. package/routes.manifest.json +208 -0
  269. package/scripts/fixtures/pilobol-sample.ast.json +234 -0
  270. package/scripts/videoml/build-browser-bundle.mjs +22 -0
  271. package/scripts/videoml/build-preview-bundle.mjs +50 -0
  272. package/scripts/videoml/bundle-common.mjs +129 -0
  273. package/scripts/videoml/preview/preview-entry.tsx +381 -0
  274. package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
  275. package/scripts/videoml/preview/preview-timing.ts +42 -0
  276. package/scripts/videoml/preview.html +77 -0
  277. package/scripts/videoml/standard-entry.tsx +46 -0
  278. package/src/backend.js +1 -0
  279. package/src/backend.ts +3 -0
  280. package/src/sync.mjs +87 -0
  281. package/src/with-papyrus.d.mts +2 -0
  282. package/src/with-papyrus.mjs +60 -0
@@ -0,0 +1,234 @@
1
+ import { ArnFormat, CfnOutput, Duration, Fn, Stack } from "aws-cdk-lib";
2
+ import * as iam from "aws-cdk-lib/aws-iam";
3
+ import * as amplify from "aws-cdk-lib/aws-amplify";
4
+ import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider.js";
5
+ import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs.js";
6
+ import { isStagingEnabled, resolveStackName, resolveStagingDomainName } from "./site-config.js";
7
+ function environmentVariables(variables) {
8
+ return Object.entries(variables).map(([name, value]) => ({ name, value }));
9
+ }
10
+ /**
11
+ * Provisions the Amplify *app shell* of a Papyrus publication from its
12
+ * site.json:
13
+ *
14
+ * - CMS app (WEB_COMPUTE) with branches `main` (production, owns the backend)
15
+ * and `staging` (frontend only, reads the production backend)
16
+ * - for `markus-static` sites, a reader app (WEB) with branch `main`
17
+ * - a domain association per branch/app when a domain is configured (without
18
+ * one, no AWS::Amplify::Domain is created and the default amplifyapp.com
19
+ * URL is used), IAM service and compute roles
20
+ * - `cms.staging: false` omits the staging branch and its domain
21
+ *
22
+ * No repository and no access token are configured: the apps are manual-deploy
23
+ * until Amplify's GitHub App is connected once in the console
24
+ * (docs/site-hosting.md). The Papyrus backend itself is created inside the CMS
25
+ * app by `ampx pipeline-deploy` from the production build.
26
+ */
27
+ export class AmplifyAppShellStack extends Stack {
28
+ cmsAppId;
29
+ readerAppId;
30
+ constructor(scope, id, config, props) {
31
+ super(scope, id, props);
32
+ const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
33
+ const stagingEnabled = isStagingEnabled(config);
34
+ const stagingDomainName = resolveStagingDomainName(config);
35
+ const cmsServiceRole = new iam.Role(this, "AmplifyServiceRole", {
36
+ assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
37
+ description: `Backend deploy and staging build service role for Amplify app ${cmsAppName} (AdministratorAccess: documented risk)`,
38
+ managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess")],
39
+ });
40
+ const computeRole = new iam.Role(this, "AmplifyComputeRole", {
41
+ assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
42
+ description: `SSR compute role for Amplify app ${cmsAppName}`,
43
+ managedPolicies: [
44
+ iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"),
45
+ iam.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchLogsFullAccess"),
46
+ ],
47
+ });
48
+ const cmsApp = new amplify.CfnApp(this, "App", {
49
+ name: cmsAppName,
50
+ description: `Papyrus newsroom CMS for ${config.siteId} (WEB_COMPUTE)`,
51
+ platform: "WEB_COMPUTE",
52
+ iamServiceRole: cmsServiceRole.roleArn,
53
+ computeRoleArn: computeRole.roleArn,
54
+ buildSpec: cmsProductionBuildSpec(config),
55
+ jobConfig: config.cms.buildComputeType ? { buildComputeType: config.cms.buildComputeType } : undefined,
56
+ customRules: [{ source: "/<*>", target: "/index.html", status: "404-200" }],
57
+ });
58
+ const stagingMode = config.frontend === "markus-static"
59
+ ? { PAPYRUS_STAGING_PREVIEW: "static" }
60
+ : { PAPYRUS_CONTENT_SOURCE: "drafts" };
61
+ const productionOrigin = config.cms.domainName
62
+ ? `https://${config.cms.domainName}/`
63
+ : Fn.join("", ["https://main.", cmsApp.attrDefaultDomain, "/"]);
64
+ const stagingOrigin = stagingDomainName
65
+ ? `https://${stagingDomainName}/`
66
+ : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain, "/"]);
67
+ const defaultOriginsToAllow = [
68
+ ...(config.cms.domainName ? [] : [productionOrigin]),
69
+ ...(stagingEnabled && !stagingDomainName ? [stagingOrigin] : []),
70
+ ];
71
+ const oauthRedirectUrls = defaultOriginsToAllow.length === 0
72
+ ? config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS
73
+ : Fn.join(",", [config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS, ...defaultOriginsToAllow]);
74
+ const productionBranch = new amplify.CfnBranch(this, "Branch", {
75
+ appId: cmsApp.attrAppId,
76
+ branchName: "main",
77
+ stage: "PRODUCTION",
78
+ enableAutoBuild: true,
79
+ enablePerformanceMode: false,
80
+ environmentVariables: environmentVariables({
81
+ ...config.cms.environment,
82
+ PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
83
+ PAPYRUS_SITE_BRAND: config.brand,
84
+ SITE_ENV: "production",
85
+ PAPYRUS_CONTENT_SOURCE: "published",
86
+ }),
87
+ });
88
+ if (stagingEnabled) {
89
+ const stagingBranch = new amplify.CfnBranch(this, "StagingBranch", {
90
+ appId: cmsApp.attrAppId,
91
+ branchName: "staging",
92
+ stage: "BETA",
93
+ enableAutoBuild: true,
94
+ enablePerformanceMode: false,
95
+ buildSpec: cmsStagingBuildSpec(config),
96
+ environmentVariables: environmentVariables({
97
+ ...config.cms.environment,
98
+ PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
99
+ PAPYRUS_SITE_BRAND: config.brand,
100
+ SITE_ENV: "staging",
101
+ ...stagingMode,
102
+ NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
103
+ ? `https://${stagingDomainName}`
104
+ : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]),
105
+ }),
106
+ });
107
+ if (stagingDomainName) {
108
+ const stagingDomain = new amplify.CfnDomain(this, "StagingDomain", {
109
+ appId: cmsApp.attrAppId,
110
+ domainName: stagingDomainName,
111
+ subDomainSettings: [{ branchName: "staging", prefix: "" }],
112
+ });
113
+ stagingDomain.addResourceDependency(stagingBranch);
114
+ }
115
+ }
116
+ if (config.cms.domainName) {
117
+ const productionDomain = new amplify.CfnDomain(this, "Domain", {
118
+ appId: cmsApp.attrAppId,
119
+ domainName: config.cms.domainName,
120
+ subDomainSettings: [{ branchName: "main", prefix: "" }],
121
+ });
122
+ productionDomain.addResourceDependency(productionBranch);
123
+ }
124
+ this.cmsAppId = cmsApp.attrAppId;
125
+ new CfnOutput(this, "CmsAppId", { value: cmsApp.attrAppId, description: `Amplify app id for ${cmsAppName}` });
126
+ new CfnOutput(this, "CmsOrigin", { value: productionOrigin });
127
+ if (stagingEnabled) {
128
+ new CfnOutput(this, "StagingOrigin", { value: stagingOrigin });
129
+ }
130
+ if (config.reader) {
131
+ this.readerAppId = this.addReaderApp(config, config.reader);
132
+ }
133
+ if (config.github) {
134
+ this.addGithubCiRole(config, config.github, [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])]);
135
+ }
136
+ }
137
+ addGithubCiRole(config, github, amplifyAppIds) {
138
+ const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(this, "GithubOidcProvider", this.formatArn({
139
+ service: "iam",
140
+ region: "",
141
+ resource: "oidc-provider",
142
+ resourceName: GITHUB_OIDC_PROVIDER_HOST,
143
+ arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
144
+ }));
145
+ const ciRole = new iam.Role(this, "GithubCiRole", {
146
+ roleName: `${config.siteId}-github-ci`,
147
+ description: `GitHub Actions role for ${github.owner}/${github.repo} (OIDC, least privilege, no admin policy)`,
148
+ maxSessionDuration: Duration.hours(1),
149
+ assumedBy: new iam.OpenIdConnectPrincipal(provider, {
150
+ StringEquals: { [`${GITHUB_OIDC_PROVIDER_HOST}:aud`]: "sts.amazonaws.com" },
151
+ StringLike: {
152
+ [`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: github.branches.map((branch) => `repo:${github.owner}/${github.repo}:ref:refs/heads/${branch}`),
153
+ },
154
+ }),
155
+ });
156
+ ciRole.addToPolicy(new iam.PolicyStatement({
157
+ sid: "StartAndInspectAmplifyJobs",
158
+ actions: ["amplify:StartJob", "amplify:ListJobs", "amplify:GetJob", "amplify:ListBranches"],
159
+ resources: amplifyAppIds.flatMap((appId) => [
160
+ this.formatArn({ service: "amplify", resource: "apps", resourceName: appId, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
161
+ this.formatArn({ service: "amplify", resource: "apps", resourceName: `${appId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
162
+ ]),
163
+ }));
164
+ ciRole.addToPolicy(new iam.PolicyStatement({
165
+ sid: "ReadSiteSecretsForJwtMinting",
166
+ actions: ["ssm:GetParameter", "ssm:GetParameters"],
167
+ resources: [
168
+ this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/${this.cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
169
+ this.formatArn({ service: "ssm", resource: "parameter", resourceName: `amplify/shared/${this.cmsAppId}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
170
+ ],
171
+ }));
172
+ ciRole.addToPolicy(new iam.PolicyStatement({
173
+ sid: "WhoAmI",
174
+ actions: ["sts:GetCallerIdentity"],
175
+ resources: ["*"],
176
+ }));
177
+ if (github.ciCanDeployInfra) {
178
+ ciRole.addToPolicy(new iam.PolicyStatement({
179
+ sid: "DeployThisSitesAppShellStack",
180
+ actions: ["cloudformation:*"],
181
+ resources: [
182
+ this.formatArn({ service: "cloudformation", resource: "stack", resourceName: `${resolveStackName(config)}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
183
+ ],
184
+ }));
185
+ }
186
+ new CfnOutput(this, "GithubCiRoleArn", { value: ciRole.roleArn, description: `Role for GitHub Actions in ${github.owner}/${github.repo}` });
187
+ }
188
+ addReaderApp(config, reader) {
189
+ const readerAppName = reader.appName ?? `${config.siteId}-reader`;
190
+ const readerBranchName = reader.branchName ?? "main";
191
+ const readerServiceRole = new iam.Role(this, "ReaderServiceRole", {
192
+ assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
193
+ description: `Static reader build role for Amplify app ${readerAppName} (reads published media only; content is read as a Cognito guest)`,
194
+ });
195
+ readerServiceRole.addToPolicy(new iam.PolicyStatement({
196
+ sid: "ReadMediaForExport",
197
+ actions: ["s3:GetObject"],
198
+ resources: ["arn:aws:s3:::amplify-*/media/*"],
199
+ }));
200
+ const readerApp = new amplify.CfnApp(this, "ReaderApp", {
201
+ name: readerAppName,
202
+ description: `Papyrus static reader for ${config.siteId} (WEB)`,
203
+ platform: "WEB",
204
+ iamServiceRole: readerServiceRole.roleArn,
205
+ buildSpec: readerBuildSpec(config),
206
+ });
207
+ const readerBranch = new amplify.CfnBranch(this, "ReaderBranch", {
208
+ appId: readerApp.attrAppId,
209
+ branchName: readerBranchName,
210
+ stage: "PRODUCTION",
211
+ enableAutoBuild: true,
212
+ environmentVariables: environmentVariables({
213
+ ...(reader.environment ?? {}),
214
+ PAPYRUS_SITE_BRAND: config.brand,
215
+ SITE_ENV: "production",
216
+ }),
217
+ });
218
+ if (reader.domainName) {
219
+ const readerDomain = new amplify.CfnDomain(this, "ReaderDomain", {
220
+ appId: readerApp.attrAppId,
221
+ domainName: reader.domainName,
222
+ subDomainSettings: [{ branchName: readerBranchName, prefix: "" }],
223
+ });
224
+ readerDomain.addResourceDependency(readerBranch);
225
+ }
226
+ new CfnOutput(this, "ReaderAppId", { value: readerApp.attrAppId, description: `Amplify app id for ${readerAppName}` });
227
+ new CfnOutput(this, "ReaderOrigin", {
228
+ value: reader.domainName
229
+ ? `https://${reader.domainName}/`
230
+ : Fn.join("", [`https://${readerBranchName}.`, readerApp.attrDefaultDomain, "/"]),
231
+ });
232
+ return readerApp.attrAppId;
233
+ }
234
+ }
@@ -0,0 +1,284 @@
1
+ import { Construct } from "constructs";
2
+ import { ArnFormat, CfnOutput, Duration, Fn, Stack, StackProps } from "aws-cdk-lib";
3
+ import * as iam from "aws-cdk-lib/aws-iam";
4
+ import * as amplify from "aws-cdk-lib/aws-amplify";
5
+ import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider";
6
+ import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs";
7
+ import { AmplifyAppShellSiteConfig, isStagingEnabled, resolveStackName, resolveStagingDomainName } from "./site-config";
8
+
9
+ function environmentVariables(variables: Record<string, string>): amplify.CfnBranch.EnvironmentVariableProperty[] {
10
+ return Object.entries(variables).map(([name, value]) => ({ name, value }));
11
+ }
12
+
13
+ /**
14
+ * Provisions the Amplify *app shell* of a Papyrus publication from its
15
+ * site.json:
16
+ *
17
+ * - CMS app (WEB_COMPUTE) with branches `main` (production, owns the backend)
18
+ * and `staging` (frontend only, reads the production backend)
19
+ * - for `markus-static` sites, a reader app (WEB) with branch `main`
20
+ * - a domain association per branch/app when a domain is configured (without
21
+ * one, no AWS::Amplify::Domain is created and the default amplifyapp.com
22
+ * URL is used), IAM service and compute roles
23
+ * - `cms.staging: false` omits the staging branch and its domain
24
+ *
25
+ * No repository and no access token are configured: the apps are manual-deploy
26
+ * until Amplify's GitHub App is connected once in the console
27
+ * (docs/site-hosting.md). The Papyrus backend itself is created inside the CMS
28
+ * app by `ampx pipeline-deploy` from the production build.
29
+ */
30
+ export class AmplifyAppShellStack extends Stack {
31
+ public readonly cmsAppId: string;
32
+ public readonly readerAppId?: string;
33
+
34
+ constructor(scope: Construct, id: string, config: AmplifyAppShellSiteConfig, props?: StackProps) {
35
+ super(scope, id, props);
36
+
37
+ const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
38
+ const stagingEnabled = isStagingEnabled(config);
39
+ const stagingDomainName = resolveStagingDomainName(config);
40
+
41
+ const cmsServiceRole = new iam.Role(this, "AmplifyServiceRole", {
42
+ assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
43
+ description: `Backend deploy and staging build service role for Amplify app ${cmsAppName} (AdministratorAccess: documented risk)`,
44
+ managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess")],
45
+ });
46
+
47
+ const computeRole = new iam.Role(this, "AmplifyComputeRole", {
48
+ assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
49
+ description: `SSR compute role for Amplify app ${cmsAppName}`,
50
+ managedPolicies: [
51
+ iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"),
52
+ iam.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchLogsFullAccess"),
53
+ ],
54
+ });
55
+
56
+ const cmsApp = new amplify.CfnApp(this, "App", {
57
+ name: cmsAppName,
58
+ description: `Papyrus newsroom CMS for ${config.siteId} (WEB_COMPUTE)`,
59
+ platform: "WEB_COMPUTE",
60
+ iamServiceRole: cmsServiceRole.roleArn,
61
+ computeRoleArn: computeRole.roleArn,
62
+ buildSpec: cmsProductionBuildSpec(config),
63
+ jobConfig: config.cms.buildComputeType ? { buildComputeType: config.cms.buildComputeType } : undefined,
64
+ customRules: [{ source: "/<*>", target: "/index.html", status: "404-200" }],
65
+ });
66
+
67
+ const stagingMode: Record<string, string> = config.frontend === "markus-static"
68
+ ? { PAPYRUS_STAGING_PREVIEW: "static" }
69
+ : { PAPYRUS_CONTENT_SOURCE: "drafts" };
70
+
71
+ const productionOrigin = config.cms.domainName
72
+ ? `https://${config.cms.domainName}/`
73
+ : Fn.join("", ["https://main.", cmsApp.attrDefaultDomain, "/"]);
74
+ const stagingOrigin = stagingDomainName
75
+ ? `https://${stagingDomainName}/`
76
+ : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain, "/"]);
77
+ const defaultOriginsToAllow = [
78
+ ...(config.cms.domainName ? [] : [productionOrigin]),
79
+ ...(stagingEnabled && !stagingDomainName ? [stagingOrigin] : []),
80
+ ];
81
+ const oauthRedirectUrls = defaultOriginsToAllow.length === 0
82
+ ? config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS
83
+ : Fn.join(",", [config.cms.environment.PAPYRUS_OAUTH_REDIRECT_URLS, ...defaultOriginsToAllow]);
84
+
85
+ const productionBranch = new amplify.CfnBranch(this, "Branch", {
86
+ appId: cmsApp.attrAppId,
87
+ branchName: "main",
88
+ stage: "PRODUCTION",
89
+ enableAutoBuild: true,
90
+ enablePerformanceMode: false,
91
+ environmentVariables: environmentVariables({
92
+ ...config.cms.environment,
93
+ PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
94
+ PAPYRUS_SITE_BRAND: config.brand,
95
+ SITE_ENV: "production",
96
+ PAPYRUS_CONTENT_SOURCE: "published",
97
+ }),
98
+ });
99
+
100
+ if (stagingEnabled) {
101
+ const stagingBranch = new amplify.CfnBranch(this, "StagingBranch", {
102
+ appId: cmsApp.attrAppId,
103
+ branchName: "staging",
104
+ stage: "BETA",
105
+ enableAutoBuild: true,
106
+ enablePerformanceMode: false,
107
+ buildSpec: cmsStagingBuildSpec(config),
108
+ environmentVariables: environmentVariables({
109
+ ...config.cms.environment,
110
+ PAPYRUS_OAUTH_REDIRECT_URLS: oauthRedirectUrls,
111
+ PAPYRUS_SITE_BRAND: config.brand,
112
+ SITE_ENV: "staging",
113
+ ...stagingMode,
114
+ NEXT_PUBLIC_PAPYRUS_STAGING_URL: stagingDomainName
115
+ ? `https://${stagingDomainName}`
116
+ : Fn.join("", ["https://staging.", cmsApp.attrDefaultDomain]),
117
+ }),
118
+ });
119
+
120
+ if (stagingDomainName) {
121
+ const stagingDomain = new amplify.CfnDomain(this, "StagingDomain", {
122
+ appId: cmsApp.attrAppId,
123
+ domainName: stagingDomainName,
124
+ subDomainSettings: [{ branchName: "staging", prefix: "" }],
125
+ });
126
+ stagingDomain.addResourceDependency(stagingBranch);
127
+ }
128
+ }
129
+
130
+ if (config.cms.domainName) {
131
+ const productionDomain = new amplify.CfnDomain(this, "Domain", {
132
+ appId: cmsApp.attrAppId,
133
+ domainName: config.cms.domainName,
134
+ subDomainSettings: [{ branchName: "main", prefix: "" }],
135
+ });
136
+ productionDomain.addResourceDependency(productionBranch);
137
+ }
138
+
139
+ this.cmsAppId = cmsApp.attrAppId;
140
+ new CfnOutput(this, "CmsAppId", { value: cmsApp.attrAppId, description: `Amplify app id for ${cmsAppName}` });
141
+ new CfnOutput(this, "CmsOrigin", { value: productionOrigin });
142
+ if (stagingEnabled) {
143
+ new CfnOutput(this, "StagingOrigin", { value: stagingOrigin });
144
+ }
145
+
146
+ if (config.reader) {
147
+ this.readerAppId = this.addReaderApp(config, config.reader);
148
+ }
149
+
150
+ if (config.github) {
151
+ this.addGithubCiRole(config, config.github, [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])]);
152
+ }
153
+ }
154
+
155
+ private addGithubCiRole(
156
+ config: AmplifyAppShellSiteConfig,
157
+ github: NonNullable<AmplifyAppShellSiteConfig["github"]>,
158
+ amplifyAppIds: string[],
159
+ ): void {
160
+ const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(
161
+ this,
162
+ "GithubOidcProvider",
163
+ this.formatArn({
164
+ service: "iam",
165
+ region: "",
166
+ resource: "oidc-provider",
167
+ resourceName: GITHUB_OIDC_PROVIDER_HOST,
168
+ arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
169
+ }),
170
+ );
171
+
172
+ const ciRole = new iam.Role(this, "GithubCiRole", {
173
+ roleName: `${config.siteId}-github-ci`,
174
+ description: `GitHub Actions role for ${github.owner}/${github.repo} (OIDC, least privilege, no admin policy)`,
175
+ maxSessionDuration: Duration.hours(1),
176
+ assumedBy: new iam.OpenIdConnectPrincipal(provider, {
177
+ StringEquals: { [`${GITHUB_OIDC_PROVIDER_HOST}:aud`]: "sts.amazonaws.com" },
178
+ StringLike: {
179
+ [`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: github.branches.map(
180
+ (branch) => `repo:${github.owner}/${github.repo}:ref:refs/heads/${branch}`,
181
+ ),
182
+ },
183
+ }),
184
+ });
185
+
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
+ );
206
+ ciRole.addToPolicy(
207
+ new iam.PolicyStatement({
208
+ sid: "WhoAmI",
209
+ actions: ["sts:GetCallerIdentity"],
210
+ resources: ["*"],
211
+ }),
212
+ );
213
+ if (github.ciCanDeployInfra) {
214
+ ciRole.addToPolicy(
215
+ new iam.PolicyStatement({
216
+ sid: "DeployThisSitesAppShellStack",
217
+ actions: ["cloudformation:*"],
218
+ resources: [
219
+ this.formatArn({ service: "cloudformation", resource: "stack", resourceName: `${resolveStackName(config)}/*`, arnFormat: ArnFormat.SLASH_RESOURCE_NAME }),
220
+ ],
221
+ }),
222
+ );
223
+ }
224
+
225
+ new CfnOutput(this, "GithubCiRoleArn", { value: ciRole.roleArn, description: `Role for GitHub Actions in ${github.owner}/${github.repo}` });
226
+ }
227
+
228
+ private addReaderApp(
229
+ config: AmplifyAppShellSiteConfig,
230
+ reader: NonNullable<AmplifyAppShellSiteConfig["reader"]>,
231
+ ): string {
232
+ const readerAppName = reader.appName ?? `${config.siteId}-reader`;
233
+ const readerBranchName = reader.branchName ?? "main";
234
+
235
+ const readerServiceRole = new iam.Role(this, "ReaderServiceRole", {
236
+ assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
237
+ description: `Static reader build role for Amplify app ${readerAppName} (reads published media only; content is read as a Cognito guest)`,
238
+ });
239
+ readerServiceRole.addToPolicy(
240
+ new iam.PolicyStatement({
241
+ sid: "ReadMediaForExport",
242
+ actions: ["s3:GetObject"],
243
+ resources: ["arn:aws:s3:::amplify-*/media/*"],
244
+ }),
245
+ );
246
+
247
+ const readerApp = new amplify.CfnApp(this, "ReaderApp", {
248
+ name: readerAppName,
249
+ description: `Papyrus static reader for ${config.siteId} (WEB)`,
250
+ platform: "WEB",
251
+ iamServiceRole: readerServiceRole.roleArn,
252
+ buildSpec: readerBuildSpec(config),
253
+ });
254
+
255
+ const readerBranch = new amplify.CfnBranch(this, "ReaderBranch", {
256
+ appId: readerApp.attrAppId,
257
+ branchName: readerBranchName,
258
+ stage: "PRODUCTION",
259
+ enableAutoBuild: true,
260
+ environmentVariables: environmentVariables({
261
+ ...(reader.environment ?? {}),
262
+ PAPYRUS_SITE_BRAND: config.brand,
263
+ SITE_ENV: "production",
264
+ }),
265
+ });
266
+
267
+ if (reader.domainName) {
268
+ const readerDomain = new amplify.CfnDomain(this, "ReaderDomain", {
269
+ appId: readerApp.attrAppId,
270
+ domainName: reader.domainName,
271
+ subDomainSettings: [{ branchName: readerBranchName, prefix: "" }],
272
+ });
273
+ readerDomain.addResourceDependency(readerBranch);
274
+ }
275
+
276
+ new CfnOutput(this, "ReaderAppId", { value: readerApp.attrAppId, description: `Amplify app id for ${readerAppName}` });
277
+ new CfnOutput(this, "ReaderOrigin", {
278
+ value: reader.domainName
279
+ ? `https://${reader.domainName}/`
280
+ : Fn.join("", [`https://${readerBranchName}.`, readerApp.attrDefaultDomain, "/"]),
281
+ });
282
+ return readerApp.attrAppId;
283
+ }
284
+ }
@@ -0,0 +1,85 @@
1
+ import { resolveStoragePreviewPrefix } from "./site-config.js";
2
+ export function papyrusVersionToPep440(version) {
3
+ const match = /^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z-]+\.(\d+))?$/.exec(version);
4
+ if (!match)
5
+ throw new Error(`cannot map ${version} to PEP 440`);
6
+ return match[2] === undefined ? match[1] : `${match[1]}.dev${match[2]}`;
7
+ }
8
+ function yamlScalar(command) {
9
+ const needsQuoting = /: | #|^[-?:,\[\]{}#&*!|>'"%@`]|:$/.test(command);
10
+ return needsQuoting ? `'${command.replace(/'/g, "''")}'` : command;
11
+ }
12
+ function commandLines(commands, indent) {
13
+ return commands.map((command) => `${indent}- ${yamlScalar(command)}`);
14
+ }
15
+ const NPM_CLEAN_INSTALL = "npm ci --cache .npm --prefer-offline";
16
+ const FRONTEND_ENVIRONMENT_CAPTURE = "env | grep -e '^PAPYRUS_' -e '^SITE_ENV=' -e '^NEXT_PUBLIC_' >> .env.production";
17
+ function pythonProvisioningCommands(config) {
18
+ return [
19
+ "export UV_CACHE_DIR=\"$PWD/.uv-cache\"",
20
+ "curl -LsSf https://astral.sh/uv/install.sh | sh",
21
+ "export PATH=\"$HOME/.local/bin:$PATH\"",
22
+ "uv python install 3.12",
23
+ "uv venv --python 3.12 .venv",
24
+ `uv pip install --python .venv "papyrus-newsroom[markus]==${papyrusVersionToPep440(config.papyrusVersion)}"`,
25
+ "export PATH=\"$PWD/.venv/bin:$PATH\"",
26
+ ];
27
+ }
28
+ function reader(config) {
29
+ if (!config.reader)
30
+ throw new Error(`site ${config.siteId} has no reader app (frontend ${config.frontend})`);
31
+ return config.reader;
32
+ }
33
+ function frontendSection(preBuildCommands, buildCommands, artifactsDirectory, cachePaths) {
34
+ const lines = ["frontend:", " phases:"];
35
+ if (preBuildCommands.length > 0) {
36
+ lines.push(" preBuild:", " commands:", ...commandLines(preBuildCommands, " "));
37
+ }
38
+ lines.push(" build:", " commands:", ...commandLines(buildCommands, " "), " artifacts:", ` baseDirectory: ${artifactsDirectory}`, " files:", " - '**/*'", " cache:", " paths:", ...cachePaths.map((cachePath) => ` - ${cachePath}`));
39
+ return lines;
40
+ }
41
+ export function cmsProductionBuildSpec(config) {
42
+ return [
43
+ "version: 1",
44
+ "backend:",
45
+ " phases:",
46
+ " build:",
47
+ " commands:",
48
+ ...commandLines([
49
+ NPM_CLEAN_INSTALL,
50
+ "if [ -z \"${PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI:-}\" ]; then export PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD=true; fi",
51
+ "npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID",
52
+ ], " "),
53
+ ...frontendSection([], [FRONTEND_ENVIRONMENT_CAPTURE, "npx papyrus-app sync && npm run build"], ".next", [".next/cache/**/*", ".npm/**/*"]),
54
+ ].join("\n") + "\n";
55
+ }
56
+ export function cmsStagingBuildSpec(config) {
57
+ const preBuild = [NPM_CLEAN_INSTALL, "npx ampx generate outputs --branch main --app-id $AWS_APP_ID"];
58
+ 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 ops content export-published --drafts --out content-export --clean", readerConfig.buildCommand, `papyrus ops content upload-preview --dir ${readerConfig.baseDirectory} --prefix ${resolveStoragePreviewPrefix(config)}`);
62
+ cachePaths.push(".uv-cache/**/*");
63
+ }
64
+ return [
65
+ "version: 1",
66
+ ...frontendSection(preBuild, [FRONTEND_ENVIRONMENT_CAPTURE, "npx papyrus-app sync && npm run build"], ".next", cachePaths),
67
+ ].join("\n") + "\n";
68
+ }
69
+ export function readerBuildSpec(config) {
70
+ const readerConfig = reader(config);
71
+ return [
72
+ "version: 1",
73
+ ...frontendSection([
74
+ ...pythonProvisioningCommands(config),
75
+ "papyrus ops content export-published --auth guest --out content-export --clean",
76
+ ], [readerConfig.buildCommand], readerConfig.baseDirectory, [".uv-cache/**/*"]),
77
+ ].join("\n") + "\n";
78
+ }
79
+ export function buildSpecFor(config, app) {
80
+ if (app === "cms-production")
81
+ return cmsProductionBuildSpec(config);
82
+ if (app === "cms-staging")
83
+ return cmsStagingBuildSpec(config);
84
+ return readerBuildSpec(config);
85
+ }