@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,25 +1,30 @@
1
1
  import { defineBackend, secret } from "@aws-amplify/backend";
2
- import { Duration, RemovalPolicy, Stack } from "aws-cdk-lib";
3
- import * as backup from "aws-cdk-lib/aws-backup";
4
- import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from "aws-cdk-lib/custom-resources";
2
+ import { Stack } from "aws-cdk-lib";
5
3
  import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
6
- import * as events from "aws-cdk-lib/aws-events";
7
- import * as route53 from "aws-cdk-lib/aws-route53";
8
- import * as ses from "aws-cdk-lib/aws-ses";
9
- import * as sesActions from "aws-cdk-lib/aws-ses-actions";
10
4
  import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
11
5
  import { CfnEventSourceMapping, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
12
6
  import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aws-s3vectors";
13
7
  import { dirname, resolve } from "node:path";
14
8
  import { fileURLToPath } from "node:url";
9
+ import { DEFAULT_RESOLVER_WAVE_SIZE, orderModelStacksInWaves } from "./resolver-waves.js";
10
+ import { resolveSitePublication } from "./publication/default-publication.js";
11
+ import { REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS, addReprojectOnDeploy, reprojectDeployToken } from "./publication/reproject-on-deploy.js";
12
+ import { addProjectionRetries, addS3ProjectionPublication } from "./publication/s3-projection.js";
13
+ import { planRenditions } from "./publication/renditions-settings.js";
14
+ import { contentActionsPublicationWiring, planS3ProjectionBackend } from "./publication/s3-projection-settings.js";
15
15
  import { rebuildTriggerSettings } from "./rebuild-trigger-settings.js";
16
- import { authConfigFromEnv, defineSiteAuth } from "./auth/resource.js";
17
- import { data } from "./data/resource.js";
16
+ import { assertValidPublicationConfig } from "./site-backend-config.js";
17
+ import { revalidateSecretReadStatement } from "./revalidate-secret-policy.js";
18
+ import { revalidateDistributionEnvironment, revalidateInvalidationStatement, validateRevalidateDistributionIds, } from "./revalidate-invalidation-settings.js";
19
+ import { addInvalidationPathAlarm } from "./publication/invalidation-alarm.js";
20
+ import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth } from "./auth/resource.js";
21
+ import { defineSiteData } from "./data/resource.js";
22
+ import { resolvePlugins } from "./plugins/registry.js";
23
+ import { wirePlugins } from "./plugins/wire.js";
18
24
  import { assignmentAction } from "./functions/assignment-action/resource.js";
19
25
  import { categoryAction } from "./functions/category-action/resource.js";
20
26
  import { ConsoleChatResponderStack } from "./functions/console-chat-responder/resource.js";
21
- import { graphqlJwtAuthorizer } from "./functions/graphql-jwt-authorizer/resource.js";
22
- import { contentActions } from "./functions/content-actions/resource.js";
27
+ import { contentActions, setSiteFilesForContentActionsBundle } from "./functions/content-actions/resource.js";
23
28
  import { knowledgeQuery } from "./functions/knowledge-query/resource.js";
24
29
  import { manageUserRole } from "./functions/manage-user-role/resource.js";
25
30
  import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource.js";
@@ -30,7 +35,10 @@ import { emailSubmissionProcessor } from "./functions/email-submission-processor
30
35
  import { sesInboundReceive } from "./functions/ses-inbound-receive/resource.js";
31
36
  import { slackDelivery } from "./functions/slack-delivery/resource.js";
32
37
  import { slackEvents } from "./functions/slack-events/resource.js";
38
+ import { addInboundEmailSesIntake } from "./inbound-email/ses-intake.js";
33
39
  import { InboundEmailStack } from "./inbound-email/stack.js";
40
+ import { deriveKnowledgeVectorIndexName, deriveReceiptRuleName, deriveReceiptRuleSetName, deriveStorageBackupVaultName, isLegacyProductionPipeline, planInboundEmailSes, resolveSiteBackendFeatureFlags, } from "./site-backend-names.js";
41
+ import { addStorageBackups } from "./storage-backups/construct.js";
34
42
  import { storage } from "./storage/resource.js";
35
43
  export function defineSiteBackend(papyrusSite) {
36
44
  // `papyrus.config.ts`'s default export. Backend options live under `backend`;
@@ -40,50 +48,46 @@ export function defineSiteBackend(papyrusSite) {
40
48
  const knowledgeEmbeddingModel = "text-embedding-3-small";
41
49
  const amplifyBranch = (process.env.AWS_BRANCH ?? "").trim();
42
50
  const amplifyAppId = (process.env.AWS_APP_ID ?? "").trim();
43
- // Amplify app id whose `main` branch is this site's production pipeline (owns
44
- // account-global resources: SES rule set, backup vault, legacy S3 Vectors name).
51
+ // Amplify app id whose `main` branch is the original p.apyr.us backend; only
52
+ // that app keeps the historical account-global names (see site-backend-names.ts).
45
53
  const productionAppId = (site.productionAppId ?? "").trim();
46
- const isAmplifyProductionPipeline = amplifyBranch === "main" && productionAppId !== "" && amplifyAppId === productionAppId;
47
- function readEnvFlag(name, defaultValue) {
48
- const raw = (process.env[name] ?? "").trim().toLowerCase();
49
- if (["1", "true", "yes", "on"].includes(raw))
50
- return true;
51
- if (["0", "false", "no", "off"].includes(raw))
52
- return false;
53
- return defaultValue;
54
- }
55
- function sanitizeAwsName(value, maxLength = 50) {
56
- return value
57
- .toLowerCase()
58
- .replace(/[^a-z0-9-]/g, "-")
59
- .replace(/-+/g, "-")
60
- .replace(/^-|-$/g, "")
61
- .slice(0, maxLength);
62
- }
63
- // S3 Vectors index names are account-global. Keep the original name for the
64
- // p.apyr.us production app so its existing index + data are not orphaned;
65
- // namespace every other app's index by site brand so a second CMS app can
66
- // deploy into the same account without colliding.
67
- const papyrusSiteBrand = (site.brandId
68
- ?? process.env.PAPYRUS_SITE_BRAND
69
- ?? process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND
70
- ?? "papyrus").trim().toLowerCase();
71
- const knowledgeVectorIndexName = productionAppId !== "" && amplifyAppId === productionAppId
72
- ? "papyrus-knowledge"
73
- : `papyrus-knowledge-${sanitizeAwsName(papyrusSiteBrand, 40)}`;
74
- // SES active receipt rule sets and shared backup vault names are account-global.
75
- // Keep them on the production pipeline only unless explicitly opted in.
76
- // A feature flag in the site config wins over the environment.
77
- const enableInboundEmail = site.features?.inboundEmail ?? readEnvFlag("PAPYRUS_ENABLE_INBOUND_EMAIL", isAmplifyProductionPipeline);
78
- const enableSlackAgent = site.features?.slack ?? readEnvFlag("PAPYRUS_ENABLE_SLACK", false);
79
- const enableStorageBackups = site.features?.storageBackups ?? readEnvFlag("PAPYRUS_ENABLE_STORAGE_BACKUPS", isAmplifyProductionPipeline);
54
+ const siteBackendIdentity = {
55
+ brandId: (site.brandId
56
+ ?? process.env.PAPYRUS_SITE_BRAND
57
+ ?? process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND
58
+ ?? "papyrus").trim().toLowerCase(),
59
+ amplifyAppId,
60
+ amplifyBranch,
61
+ productionAppId,
62
+ };
63
+ const isAmplifyProductionPipeline = isLegacyProductionPipeline(siteBackendIdentity);
64
+ const knowledgeVectorIndexName = deriveKnowledgeVectorIndexName(siteBackendIdentity);
65
+ const featureFlags = resolveSiteBackendFeatureFlags(site.features, process.env, siteBackendIdentity);
66
+ const enableInboundEmail = featureFlags.inboundEmail;
67
+ const enableSlackAgent = featureFlags.slack;
68
+ const enableStorageBackups = featureFlags.storageBackups;
69
+ const baseAuthConfig = site.auth ?? authConfigFromEnv();
70
+ const activeBrand = (papyrusSite.brands ?? []).find((brand) => brand.id.toLowerCase() === siteBackendIdentity.brandId);
71
+ const authConfig = {
72
+ ...baseAuthConfig,
73
+ newsroomBasePath: baseAuthConfig.newsroomBasePath ?? activeBrand?.newsroomBasePath,
74
+ };
75
+ const publication = resolveSitePublication(site, activeBrand?.renderer?.kind);
76
+ if (publication !== undefined)
77
+ assertValidPublicationConfig(publication);
78
+ const publicationPlan = planS3ProjectionBackend(publication);
79
+ const renditionsPlan = planRenditions(publication, process.cwd());
80
+ setSiteFilesForContentActionsBundle(publicationPlan.enabled ? publicationPlan.siteFiles : undefined);
81
+ const plugins = resolvePlugins(site.plugins);
82
+ const pluginDefinitions = Object.assign({}, ...plugins.map(({ plugin }) => plugin.schemaDefinitions()));
83
+ const pluginFunctions = Object.assign({}, ...plugins.map(({ plugin }) => plugin.functions()));
80
84
  const backend = defineBackend({
85
+ ...pluginFunctions,
81
86
  assignmentAction,
82
- auth: defineSiteAuth(site.auth ?? authConfigFromEnv()),
87
+ auth: defineSiteAuth(authConfig),
83
88
  categoryAction,
84
89
  contentActions,
85
- data,
86
- graphqlJwtAuthorizer,
90
+ data: defineSiteData(pluginDefinitions),
87
91
  knowledgeQuery,
88
92
  manageUserRole,
89
93
  modelAttachmentUpload,
@@ -96,10 +100,17 @@ export function defineSiteBackend(papyrusSite) {
96
100
  slackDelivery,
97
101
  storage,
98
102
  });
103
+ orderModelStacksInWaves(backend.data.resources.graphqlApi.node.scope, Object.keys(backend.data.resources.tables), site.resolverWaveSize ?? DEFAULT_RESOLVER_WAVE_SIZE);
104
+ const requestedCognitoDomainPrefix = (authConfig.cognitoDomainPrefix ?? "").trim();
105
+ if (authConfig.applyCognitoDomainPrefix === true
106
+ && requestedCognitoDomainPrefix !== ""
107
+ && authConfig.disableGoogleOAuth !== true) {
108
+ applyCognitoDomainPrefix(backend.auth.stack, requestedCognitoDomainPrefix);
109
+ }
99
110
  const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
100
111
  // Repo root in a Papyrus checkout; package root when installed from npm.
101
112
  const projectRoot = resolve(amplifyBackendDir, "..");
102
- const enableConsoleResponder = site.features?.consoleResponder ?? readEnvFlag("PAPYRUS_ENABLE_CONSOLE_RESPONDER", true);
113
+ const enableConsoleResponder = featureFlags.consoleResponder;
103
114
  if (enableConsoleResponder
104
115
  && !process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI?.trim()
105
116
  && !process.env.PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD?.trim()
@@ -139,36 +150,18 @@ export function defineSiteBackend(papyrusSite) {
139
150
  const messageThreadTable = backend.data.resources.tables.MessageThread;
140
151
  const dataStack = Stack.of(messageTable);
141
152
  const graphqlEndpoint = backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl;
142
- const jwtAuthorizerCfn = backend.graphqlJwtAuthorizer.resources.lambda.node.defaultChild;
143
- const jwtAuthorizerEnvironment = jwtAuthorizerCfn?.environment;
144
- const jwtAuthorizerVariables = jwtAuthorizerEnvironment
145
- && typeof jwtAuthorizerEnvironment === "object"
146
- && "variables" in jwtAuthorizerEnvironment
147
- ? jwtAuthorizerEnvironment.variables
148
- : undefined;
149
- const jwtSsmEnvConfig = process.env.AMPLIFY_SSM_ENV_CONFIG?.trim()
150
- || jwtAuthorizerVariables?.AMPLIFY_SSM_ENV_CONFIG?.trim()
151
- || "";
153
+ const amplifySsmEnvConfig = process.env.AMPLIFY_SSM_ENV_CONFIG?.trim() || "";
152
154
  if (enableConsoleResponder) {
153
- const consoleChatResponder = new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
155
+ new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
154
156
  messageTable,
155
157
  messageStreamArn,
156
158
  threadTable: messageThreadTable,
157
159
  projectRoot,
158
160
  graphqlEndpoint,
159
- amplifySsmEnvConfig: jwtSsmEnvConfig || undefined,
160
161
  responseTarget: process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET,
161
162
  model: process.env.PAPYRUS_CONSOLE_MODEL,
162
163
  prebuiltImageUri: process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI,
163
164
  });
164
- if (jwtSsmEnvConfig) {
165
- consoleChatResponder.responderFunction.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
166
- }
167
- const jwtSecretSsmParam = process.env.PAPYRUS_JWT_SECRET_SSM_PARAM?.trim()
168
- || (productionAppId !== "" && amplifyAppId === productionAppId
169
- ? `/amplify/${amplifyAppId}/main-branch-cb38ada667/PAPYRUS_JWT_SECRET`
170
- : "/amplify/papyrus/ryan-sandbox-adcd88a186/PAPYRUS_JWT_SECRET");
171
- consoleChatResponder.responderFunction.addEnvironment("PAPYRUS_JWT_SECRET_SSM_PARAM", jwtSecretSsmParam);
172
165
  }
173
166
  if (enableSlackAgent) {
174
167
  const slackEventsLambda = backend.slackEvents.resources.lambda;
@@ -180,9 +173,9 @@ export function defineSiteBackend(papyrusSite) {
180
173
  backend.slackEvents.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
181
174
  backend.slackEvents.addEnvironment("PAPYRUS_CONSOLE_RESPONSE_TARGET", (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud");
182
175
  backend.slackEvents.addEnvironment("PAPYRUS_SLACK_ALLOWED_USER_IDS", (process.env.PAPYRUS_SLACK_ALLOWED_USER_IDS ?? "").trim());
183
- if (jwtSsmEnvConfig) {
184
- backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
185
- backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
176
+ if (amplifySsmEnvConfig) {
177
+ backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
178
+ backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
186
179
  }
187
180
  backend.slackDelivery.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
188
181
  backend.slackDelivery.addEnvironment("PAPYRUS_CONSOLE_RESPONSE_TARGET", (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud");
@@ -244,37 +237,19 @@ export function defineSiteBackend(papyrusSite) {
244
237
  let storageBackupPlan;
245
238
  let storageBackupVault;
246
239
  const storageBucketCfn = storageBucket.node.defaultChild;
247
- if ((enableStorageBackups || enableInboundEmail) && storageBucketCfn) {
248
- // Backups and inbound-email intake both rely on S3 → EventBridge notifications.
240
+ if ((enableStorageBackups || enableInboundEmail || renditionsPlan.enabled) && storageBucketCfn) {
241
+ // Backups, inbound-email intake and upload-time renditions all rely on S3 → EventBridge notifications.
249
242
  storageBucketCfn.addPropertyOverride("NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled", true);
250
243
  }
251
244
  if (enableStorageBackups) {
252
245
  const storageBackupsStack = backend.createStack("storage-backups");
253
- const storageBackupVaultName = sanitizeAwsName(process.env.PAPYRUS_STORAGE_BACKUP_VAULT_NAME?.trim()
254
- || (isAmplifyProductionPipeline
255
- ? `papyrus-${productionAppId}-main-media-backup-vault`
256
- : `papyrus-${sanitizeAwsName(storageBackupsStack.stackName, 32)}-media-vault`));
257
- storageBackupVault = new backup.BackupVault(storageBackupsStack, "PapyrusStorageBackupVault", {
246
+ const storageBackupVaultName = deriveStorageBackupVaultName(siteBackendIdentity, process.env.PAPYRUS_STORAGE_BACKUP_VAULT_NAME);
247
+ const storageBackups = addStorageBackups(storageBackupsStack, {
248
+ storageBucket,
258
249
  backupVaultName: storageBackupVaultName,
259
- removalPolicy: RemovalPolicy.RETAIN,
260
- });
261
- storageBackupPlan = new backup.BackupPlan(storageBackupsStack, "PapyrusStorageBackupPlan", {
262
- backupVault: storageBackupVault,
263
- });
264
- storageBackupPlan.addRule(new backup.BackupPlanRule({
265
- ruleName: "papyrus-storage-pitr-35d",
266
- enableContinuousBackup: true,
267
- deleteAfter: Duration.days(35),
268
- }));
269
- storageBackupPlan.addRule(new backup.BackupPlanRule({
270
- ruleName: "papyrus-storage-daily-365d",
271
- scheduleExpression: events.Schedule.cron({ minute: "0", hour: "5" }),
272
- deleteAfter: Duration.days(365),
273
- }));
274
- storageBackupPlan.addSelection("PapyrusStorageBackupSelection", {
275
- allowRestores: true,
276
- resources: [backup.BackupResource.fromArn(storageBucket.bucketArn)],
277
250
  });
251
+ storageBackupVault = storageBackups.vault;
252
+ storageBackupPlan = storageBackups.plan;
278
253
  }
279
254
  // Grant S3 access on Lambda roles only (not storage bucket policies) to avoid
280
255
  // storage ↔ data nested-stack circular dependencies from allow.resource().
@@ -347,9 +322,58 @@ export function defineSiteBackend(papyrusSite) {
347
322
  if (rebuildSettings.resources.length > 0) {
348
323
  contentActionsLambda.addToRolePolicy(new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }));
349
324
  }
325
+ let s3ProjectionPublication;
326
+ if (publication !== undefined) {
327
+ s3ProjectionPublication = addS3ProjectionPublication(backend.createStack("s3-projection"), publication, {
328
+ mediaBucket: storageBucket,
329
+ renditions: renditionsPlan,
330
+ });
331
+ contentActionsLambda.node.defaultChild.memorySize = publicationPlan.contentActionsMemorySize;
332
+ if (publication.reprojectOnDeploy === true) {
333
+ contentActionsLambda.node.defaultChild.timeout = REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS;
334
+ addReprojectOnDeploy(backend.createStack("reproject-on-deploy"), contentActionsLambda, reprojectDeployToken(process.env));
335
+ }
336
+ }
337
+ if (s3ProjectionPublication !== undefined) {
338
+ addProjectionRetries(contentActionsLambda, s3ProjectionPublication.deadLetterQueue);
339
+ }
340
+ const publicationWiring = contentActionsPublicationWiring(s3ProjectionPublication);
341
+ for (const [name, value] of Object.entries(publicationWiring.environment)) {
342
+ contentActionsLambda.addEnvironment(name, value);
343
+ }
344
+ for (const statement of publicationWiring.policyStatements) {
345
+ contentActionsLambda.addToRolePolicy(statement);
346
+ }
350
347
  if ((site.revalidateBaseUrl ?? "").trim() !== "") {
351
348
  backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
352
- backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET", secret("PAPYRUS_REVALIDATE_SECRET"));
349
+ const revalidateSecretParameter = (process.env.PAPYRUS_REVALIDATE_SECRET_PARAMETER ?? "").trim();
350
+ if (!revalidateSecretParameter.startsWith("/")) {
351
+ throw new Error("revalidateBaseUrl is set but PAPYRUS_REVALIDATE_SECRET_PARAMETER is not (an SSM parameter name starting with /). The app-shell template sets it on the production branch for Pretext sites; see docs/site-hosting.md.");
352
+ }
353
+ backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET_PARAMETER", revalidateSecretParameter);
354
+ const revalidateStack = Stack.of(contentActionsLambda);
355
+ contentActionsLambda.addToRolePolicy(new PolicyStatement(revalidateSecretReadStatement({
356
+ partition: revalidateStack.partition,
357
+ region: revalidateStack.region,
358
+ account: revalidateStack.account,
359
+ parameterName: revalidateSecretParameter,
360
+ })));
361
+ const distributionProblems = validateRevalidateDistributionIds(site.revalidateDistributionIds, site.revalidateBaseUrl);
362
+ if (distributionProblems.length > 0)
363
+ throw new Error(`Invalid backend config: ${distributionProblems.join("; ")}`);
364
+ const distributionIds = site.revalidateDistributionIds ?? [];
365
+ if (distributionIds.length > 0) {
366
+ for (const [name, value] of Object.entries(revalidateDistributionEnvironment(distributionIds))) {
367
+ backend.contentActions.addEnvironment(name, value);
368
+ }
369
+ contentActionsLambda.addToRolePolicy(new PolicyStatement(revalidateInvalidationStatement({ partition: revalidateStack.partition, account: revalidateStack.account, distributionIds })));
370
+ for (const id of distributionIds) {
371
+ addInvalidationPathAlarm(revalidateStack, `ReaderEdgeInvalidationPaths${id}`, id);
372
+ }
373
+ }
374
+ }
375
+ else if (site.revalidateDistributionIds !== undefined) {
376
+ throw new Error("Invalid backend config: revalidateDistributionIds requires revalidateBaseUrl");
353
377
  }
354
378
  grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
355
379
  grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
@@ -416,83 +440,25 @@ export function defineSiteBackend(papyrusSite) {
416
440
  resources: ["*"],
417
441
  }));
418
442
  const inboundEventStack = Stack.of(receiveLambda);
419
- // SES receipt rule sets are account-global; only provision on the production pipeline.
420
- if (isAmplifyProductionPipeline) {
421
- const inboundDnsZone = route53.HostedZone.fromHostedZoneAttributes(storageStack, "PapyrusInboundDnsZone", {
422
- hostedZoneId: inboundDnsZoneId,
423
- zoneName: inboundDnsZoneName,
424
- });
425
- const verifyInboundEmailDomain = new AwsCustomResource(storageStack, "VerifyInboundEmailDomain", {
426
- onCreate: {
427
- service: "SES",
428
- action: "verifyDomainIdentity",
429
- parameters: {
430
- Domain: inboundEmailDomain,
431
- },
432
- physicalResourceId: PhysicalResourceId.of(`ses-domain-verify-${inboundEmailDomain}`),
433
- },
434
- onUpdate: {
435
- service: "SES",
436
- action: "verifyDomainIdentity",
437
- parameters: {
438
- Domain: inboundEmailDomain,
439
- },
440
- physicalResourceId: PhysicalResourceId.of(`ses-domain-verify-${inboundEmailDomain}`),
441
- },
442
- policy: AwsCustomResourcePolicy.fromSdkCalls({
443
- resources: AwsCustomResourcePolicy.ANY_RESOURCE,
444
- }),
445
- timeout: Duration.minutes(2),
446
- });
447
- new route53.TxtRecord(storageStack, "PapyrusInboundEmailDomainVerification", {
448
- zone: inboundDnsZone,
449
- recordName: `_amazonses.${inboundDnsRecordName}`,
450
- ttl: Duration.minutes(5),
451
- values: [verifyInboundEmailDomain.getResponseField("VerificationToken")],
452
- });
453
- const inboundRuleSet = new ses.ReceiptRuleSet(storageStack, "PapyrusInboundEmailRuleSet", {
454
- receiptRuleSetName: `papyrus-inbound-${inboundEmailDomain.replace(/\./g, "-")}`,
455
- });
456
- inboundRuleSet.addRule("PapyrusInboundSubmissions", {
443
+ // SES allows one active receipt rule set per region, so a non-legacy site
444
+ // creates its own brand-named rule set and never activates it here.
445
+ const inboundSesPlan = planInboundEmailSes(siteBackendIdentity, enableInboundEmail, site.inboundEmailSes);
446
+ if (inboundSesPlan.createReceiptRules) {
447
+ addInboundEmailSesIntake(storageStack, {
448
+ storageBucket,
457
449
  recipients: inboundRecipients,
458
- enabled: true,
459
- scanEnabled: true,
460
- tlsPolicy: ses.TlsPolicy.REQUIRE,
461
- actions: [
462
- new sesActions.S3({
463
- bucket: storageBucket,
464
- objectKeyPrefix: "inbound-email/",
465
- }),
466
- ],
467
- });
468
- new AwsCustomResource(storageStack, "ActivateInboundEmailRuleSet", {
469
- onCreate: {
470
- service: "SES",
471
- action: "setActiveReceiptRuleSet",
472
- parameters: {
473
- RuleSetName: inboundRuleSet.receiptRuleSetName,
474
- },
475
- physicalResourceId: PhysicalResourceId.of(`activate-${inboundRuleSet.receiptRuleSetName}`),
476
- },
477
- onUpdate: {
478
- service: "SES",
479
- action: "setActiveReceiptRuleSet",
480
- parameters: {
481
- RuleSetName: inboundRuleSet.receiptRuleSetName,
482
- },
483
- physicalResourceId: PhysicalResourceId.of(`activate-${inboundRuleSet.receiptRuleSetName}`),
484
- },
485
- onDelete: {
486
- service: "SES",
487
- action: "setActiveReceiptRuleSet",
488
- parameters: {
489
- RuleSetName: null,
490
- },
491
- },
492
- policy: AwsCustomResourcePolicy.fromSdkCalls({
493
- resources: AwsCustomResourcePolicy.ANY_RESOURCE,
494
- }),
495
- timeout: Duration.minutes(2),
450
+ ruleSetName: deriveReceiptRuleSetName(siteBackendIdentity, inboundEmailDomain),
451
+ existingRuleSetName: site.inboundEmailSes?.existingReceiptRuleSetName,
452
+ ruleName: deriveReceiptRuleName(siteBackendIdentity),
453
+ activateRuleSet: inboundSesPlan.activateReceiptRuleSet,
454
+ domainIdentity: inboundSesPlan.manageDomainIdentity
455
+ ? {
456
+ domain: inboundEmailDomain,
457
+ hostedZoneId: inboundDnsZoneId,
458
+ hostedZoneName: inboundDnsZoneName,
459
+ recordName: inboundDnsRecordName,
460
+ }
461
+ : undefined,
496
462
  });
497
463
  }
498
464
  new InboundEmailStack(inboundEventStack, "InboundEmail", {
@@ -611,5 +577,6 @@ export function defineSiteBackend(papyrusSite) {
611
577
  : null,
612
578
  },
613
579
  });
580
+ wirePlugins(backend, plugins, contentActionsLambda);
614
581
  return backend;
615
582
  }