@anthusai/papyrus 1.0.0-next.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/amplify/auth/prospectus-google-client.js +66 -0
  2. package/amplify/auth/prospectus-google-client.ts +71 -0
  3. package/amplify/auth/redirect-urls.js +54 -0
  4. package/amplify/auth/redirect-urls.ts +66 -0
  5. package/amplify/auth/resource.js +23 -5
  6. package/amplify/auth/resource.ts +31 -5
  7. package/amplify/data/resource.js +42 -80
  8. package/amplify/data/resource.ts +42 -80
  9. package/amplify/data/schema.js +45 -62
  10. package/amplify/data/schema.ts +47 -62
  11. package/amplify/functions/category-action/handler.ts +5 -3
  12. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
  13. package/amplify/functions/console-chat-responder/resource.js +0 -7
  14. package/amplify/functions/console-chat-responder/resource.ts +0 -8
  15. package/amplify/functions/console-chat-responder/src/main.rs +0 -9
  16. package/amplify/functions/content-actions/handler.py +143 -1
  17. package/amplify/functions/content-actions/requirements.txt +2 -1
  18. package/amplify/functions/content-actions/resource.js +6 -1
  19. package/amplify/functions/content-actions/resource.ts +9 -2
  20. package/amplify/functions/email-submission-processor/handler.py +3 -1
  21. package/amplify/functions/renditions/handler.py +120 -0
  22. package/amplify/functions/renditions/requirements.txt +4 -0
  23. package/amplify/functions/ses-inbound-receive/handler.ts +32 -4
  24. package/amplify/functions/shared/email-submission.ts +109 -1
  25. package/amplify/functions/shared/python-bundle.js +42 -2
  26. package/amplify/functions/shared/python-bundle.ts +47 -2
  27. package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
  28. package/amplify/inbound-email/ses-intake.js +86 -0
  29. package/amplify/inbound-email/ses-intake.ts +110 -0
  30. package/amplify/plugins/registry.js +19 -0
  31. package/amplify/plugins/registry.ts +23 -0
  32. package/amplify/plugins/signup/function/handler.py +200 -0
  33. package/amplify/plugins/signup/function/resource.js +27 -0
  34. package/amplify/plugins/signup/function/resource.ts +35 -0
  35. package/amplify/plugins/signup/papyrus.plugin.js +59 -0
  36. package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
  37. package/amplify/plugins/signup/schema.js +40 -0
  38. package/amplify/plugins/signup/schema.ts +41 -0
  39. package/amplify/plugins/signup/waf.js +48 -0
  40. package/amplify/plugins/signup/waf.ts +51 -0
  41. package/amplify/plugins/types.js +1 -0
  42. package/amplify/plugins/types.ts +39 -0
  43. package/amplify/plugins/wire.js +24 -0
  44. package/amplify/plugins/wire.ts +26 -0
  45. package/amplify/publication/default-publication.js +18 -0
  46. package/amplify/publication/default-publication.ts +20 -0
  47. package/amplify/publication/invalidation-alarm.js +26 -0
  48. package/amplify/publication/invalidation-alarm.ts +30 -0
  49. package/amplify/publication/redirect-and-index.js +37 -0
  50. package/amplify/publication/renditions-function.js +74 -0
  51. package/amplify/publication/renditions-function.ts +112 -0
  52. package/amplify/publication/renditions-settings.js +51 -0
  53. package/amplify/publication/renditions-settings.ts +58 -0
  54. package/amplify/publication/reproject-on-deploy.js +73 -0
  55. package/amplify/publication/reproject-on-deploy.ts +78 -0
  56. package/amplify/publication/s3-projection-settings.js +67 -0
  57. package/amplify/publication/s3-projection-settings.ts +101 -0
  58. package/amplify/publication/s3-projection.js +151 -0
  59. package/amplify/publication/s3-projection.ts +201 -0
  60. package/amplify/resolver-waves.js +26 -0
  61. package/amplify/resolver-waves.ts +34 -0
  62. package/amplify/revalidate-invalidation-settings.js +30 -0
  63. package/amplify/revalidate-invalidation-settings.ts +35 -0
  64. package/amplify/revalidate-secret-policy.js +13 -0
  65. package/amplify/revalidate-secret-policy.ts +19 -0
  66. package/amplify/site-backend-config.js +65 -1
  67. package/amplify/site-backend-config.ts +153 -2
  68. package/amplify/site-backend-names.js +103 -0
  69. package/amplify/site-backend-names.ts +166 -0
  70. package/amplify/site-backend.js +150 -176
  71. package/amplify/site-backend.ts +189 -201
  72. package/amplify/storage-backups/construct.js +31 -0
  73. package/amplify/storage-backups/construct.ts +47 -0
  74. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
  75. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  76. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
  77. package/app/[year]/[month]/[day]/page.tsx +1 -1
  78. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
  79. package/app/api/archive/editions/route.ts +1 -1
  80. package/app/api/auth/prospectus/callback/route.ts +9 -0
  81. package/app/api/auth/prospectus/selection/route.ts +9 -0
  82. package/app/api/auth/prospectus/session/route.ts +9 -0
  83. package/app/api/auth/prospectus/sign-in/route.ts +9 -0
  84. package/app/api/auth/prospectus/sign-out/route.ts +9 -0
  85. package/app/api/cyclotron/priming-proof/route.ts +11 -0
  86. package/app/api/media/[...path]/route.ts +5 -16
  87. package/app/api/revalidate/route.ts +15 -7
  88. package/app/archive/page.tsx +1 -1
  89. package/app/articles/[slug]/page.tsx +7 -5
  90. package/app/blog-rhythm.css +278 -0
  91. package/app/cyclotron/page.tsx +13 -0
  92. package/app/globals.css +2 -0
  93. package/app/layout.tsx +16 -0
  94. package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
  95. package/app/newsroom/[section]/page.tsx +6 -3
  96. package/app/newsroom/layout.tsx +4 -9
  97. package/app/newsroom/settings/page.tsx +6 -0
  98. package/app/newsroom/work/page.tsx +10 -0
  99. package/app/page.tsx +22 -8
  100. package/app/robots.ts +2 -5
  101. package/app/tailwind.css +1 -6
  102. package/components/amplify-client-provider.tsx +5 -22
  103. package/components/archive-shell.tsx +2 -22
  104. package/components/article-video.tsx +152 -5
  105. package/components/blog-page-background.tsx +23 -6
  106. package/components/news-desk-page.tsx +4 -15
  107. package/components/news-desk-taxonomy-client.ts +90 -3
  108. package/components/newspaper.tsx +6 -2
  109. package/components/newsroom-access-gate.tsx +2 -1
  110. package/components/newsroom-board-view.tsx +99 -0
  111. package/components/newsroom-client-shell.tsx +12 -0
  112. package/components/newsroom-curation-provider.tsx +17 -0
  113. package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
  114. package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
  115. package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
  116. package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
  117. package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
  118. package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
  119. package/components/newsroom-newsprint-board.css +48 -0
  120. package/components/newsroom-ops-overview.tsx +28 -11
  121. package/components/newsroom-ops-shell.tsx +7 -1
  122. package/components/newsroom-organizer-shell.tsx +130 -0
  123. package/components/newsroom-organizer.css +63 -0
  124. package/components/newsroom-references-view.tsx +92 -8
  125. package/components/papyrus-console-shell.tsx +4 -2
  126. package/components/pictogram-figure.tsx +9 -2
  127. package/components/presentation-header.tsx +135 -0
  128. package/components/presentation-rhythm-hrule.tsx +17 -0
  129. package/components/priming-proof-page.tsx +94 -0
  130. package/components/publication-cyclotron-panel.tsx +57 -0
  131. package/components/reader-auth-control.tsx +8 -5
  132. package/components/reference-curation-controls.tsx +77 -0
  133. package/components/reference-cyclotron-status.tsx +109 -0
  134. package/components/reference-priming.tsx +18 -0
  135. package/components/reference-relevance-review.tsx +75 -0
  136. package/components/settings-page.tsx +2 -1
  137. package/components/signup-entries-panel.tsx +94 -0
  138. package/components/topic-steering-workspace.tsx +78 -8
  139. package/components/use-blog-header-obstacles.ts +81 -0
  140. package/components/use-brand-fonts-ready.ts +28 -0
  141. package/components/use-near-viewport.ts +30 -0
  142. package/components/use-rhythm-overlay.ts +29 -0
  143. package/infra/amplify-app-shell.js +166 -27
  144. package/infra/amplify-app-shell.ts +180 -31
  145. package/infra/build-specs.js +23 -7
  146. package/infra/build-specs.ts +27 -10
  147. package/infra/site-config.js +248 -36
  148. package/infra/site-config.ts +313 -51
  149. package/lib/amplify-outputs-path.ts +5 -1
  150. package/lib/amplify-server-runtime.ts +36 -1
  151. package/lib/approved-priming-proof-reports.ts +6 -0
  152. package/lib/blog-feature-solver.ts +459 -0
  153. package/lib/blog-rhythm.ts +134 -0
  154. package/lib/brand-fonts-watch.ts +43 -0
  155. package/lib/brand-fonts.ts +112 -0
  156. package/lib/cached-content-repository.ts +12 -0
  157. package/lib/category-repository.ts +43 -93
  158. package/lib/content-item-lifecycle.ts +55 -0
  159. package/lib/content-repository.ts +3 -0
  160. package/lib/content-types.ts +4 -0
  161. package/lib/curation-client.ts +61 -0
  162. package/lib/edition-routes.ts +31 -29
  163. package/lib/graphql-content-repository.ts +56 -22
  164. package/lib/media-route-response.ts +45 -0
  165. package/lib/newspaper-layout.ts +3 -2
  166. package/lib/newsroom-base-path.ts +12 -3
  167. package/lib/newsroom-board.ts +99 -0
  168. package/lib/newsroom-category-drilldown.ts +3 -2
  169. package/lib/newsroom-forum-routes.ts +6 -4
  170. package/lib/newsroom-index-filters.ts +14 -10
  171. package/lib/newsroom-nav.ts +3 -1
  172. package/lib/newsroom-organizer-nav.ts +37 -0
  173. package/lib/newsroom-section-routing.ts +20 -0
  174. package/lib/oauth-redirect-priority.ts +40 -0
  175. package/lib/papyrus-web-locations.ts +7 -6
  176. package/lib/plugin-settings.ts +11 -0
  177. package/lib/presentation-footer.ts +3 -2
  178. package/lib/pretext-layout.ts +12 -3
  179. package/lib/priming-proof-report.ts +90 -0
  180. package/lib/prospectus-publication-access.ts +53 -0
  181. package/lib/prospectus-session.ts +196 -0
  182. package/lib/prospectus-sign-in-routes.ts +284 -0
  183. package/lib/prospectus-site-identity.ts +303 -0
  184. package/lib/publication-cyclotron.ts +74 -0
  185. package/lib/rate-limit-retry.ts +55 -0
  186. package/lib/reader-base-path.ts +24 -0
  187. package/lib/reader-cache-lifetime.ts +5 -0
  188. package/lib/reader-static-params.ts +85 -4
  189. package/lib/reader-storage-url.ts +9 -12
  190. package/lib/reference-policy.ts +5 -0
  191. package/lib/relevance-decisions.ts +103 -0
  192. package/lib/relevance-status.ts +63 -0
  193. package/lib/renderer.ts +7 -0
  194. package/lib/revalidate-prewarm.ts +53 -0
  195. package/lib/revalidate-secret.ts +59 -0
  196. package/lib/robots-policy.ts +14 -0
  197. package/lib/root-route-redirect.ts +16 -0
  198. package/lib/semantic-graph.ts +7 -0
  199. package/lib/shared-credentials-provider.ts +61 -0
  200. package/lib/signup-csv.ts +24 -0
  201. package/lib/site-brand.ts +59 -0
  202. package/lib/staging-gated-path.ts +23 -0
  203. package/lib/staging-preview-object.ts +21 -5
  204. package/lib/test-editor-override.ts +4 -0
  205. package/lib/video-loading.ts +22 -0
  206. package/lib/video-script.ts +14 -0
  207. package/middleware.ts +11 -19
  208. package/package.json +73 -8
  209. package/renderers/pretext/article-page.tsx +39 -3
  210. package/renderers/pretext/presentation-shared.tsx +113 -0
  211. package/renderers/pretext/presentation-shell.tsx +15 -102
  212. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  213. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  214. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  215. package/routes.manifest.json +123 -12
  216. package/src/sync.mjs +30 -6
  217. package/src/with-papyrus.d.mts +9 -0
  218. package/src/with-papyrus.mjs +48 -3
  219. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  220. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  221. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
  222. package/components/ai-elements/message.tsx +0 -360
@@ -1,25 +1,31 @@
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, siteAuthRedirectUrls } from "./auth/resource.js";
21
+ import { PROSPECTUS_SIGN_IN_CALLBACK_PATH, addProspectusGoogleOnlyClient } from "./auth/prospectus-google-client.js";
22
+ import { defineSiteData } from "./data/resource.js";
23
+ import { resolvePlugins } from "./plugins/registry.js";
24
+ import { wirePlugins } from "./plugins/wire.js";
18
25
  import { assignmentAction } from "./functions/assignment-action/resource.js";
19
26
  import { categoryAction } from "./functions/category-action/resource.js";
20
27
  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";
28
+ import { contentActions, setSiteFilesForContentActionsBundle } from "./functions/content-actions/resource.js";
23
29
  import { knowledgeQuery } from "./functions/knowledge-query/resource.js";
24
30
  import { manageUserRole } from "./functions/manage-user-role/resource.js";
25
31
  import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource.js";
@@ -30,7 +36,10 @@ import { emailSubmissionProcessor } from "./functions/email-submission-processor
30
36
  import { sesInboundReceive } from "./functions/ses-inbound-receive/resource.js";
31
37
  import { slackDelivery } from "./functions/slack-delivery/resource.js";
32
38
  import { slackEvents } from "./functions/slack-events/resource.js";
39
+ import { addInboundEmailSesIntake } from "./inbound-email/ses-intake.js";
33
40
  import { InboundEmailStack } from "./inbound-email/stack.js";
41
+ import { deriveKnowledgeVectorIndexName, deriveReceiptRuleName, deriveReceiptRuleSetName, deriveStorageBackupVaultName, isLegacyProductionPipeline, planInboundEmailSes, resolveSiteBackendFeatureFlags, } from "./site-backend-names.js";
42
+ import { addStorageBackups } from "./storage-backups/construct.js";
34
43
  import { storage } from "./storage/resource.js";
35
44
  export function defineSiteBackend(papyrusSite) {
36
45
  // `papyrus.config.ts`'s default export. Backend options live under `backend`;
@@ -40,50 +49,46 @@ export function defineSiteBackend(papyrusSite) {
40
49
  const knowledgeEmbeddingModel = "text-embedding-3-small";
41
50
  const amplifyBranch = (process.env.AWS_BRANCH ?? "").trim();
42
51
  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).
52
+ // Amplify app id whose `main` branch is the original p.apyr.us backend; only
53
+ // that app keeps the historical account-global names (see site-backend-names.ts).
45
54
  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);
55
+ const siteBackendIdentity = {
56
+ brandId: (site.brandId
57
+ ?? process.env.PAPYRUS_SITE_BRAND
58
+ ?? process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND
59
+ ?? "papyrus").trim().toLowerCase(),
60
+ amplifyAppId,
61
+ amplifyBranch,
62
+ productionAppId,
63
+ };
64
+ const isAmplifyProductionPipeline = isLegacyProductionPipeline(siteBackendIdentity);
65
+ const knowledgeVectorIndexName = deriveKnowledgeVectorIndexName(siteBackendIdentity);
66
+ const featureFlags = resolveSiteBackendFeatureFlags(site.features, process.env, siteBackendIdentity);
67
+ const enableInboundEmail = featureFlags.inboundEmail;
68
+ const enableSlackAgent = featureFlags.slack;
69
+ const enableStorageBackups = featureFlags.storageBackups;
70
+ const baseAuthConfig = site.auth ?? authConfigFromEnv();
71
+ const activeBrand = (papyrusSite.brands ?? []).find((brand) => brand.id.toLowerCase() === siteBackendIdentity.brandId);
72
+ const authConfig = {
73
+ ...baseAuthConfig,
74
+ newsroomBasePath: baseAuthConfig.newsroomBasePath ?? activeBrand?.newsroomBasePath,
75
+ };
76
+ const publication = resolveSitePublication(site, activeBrand?.renderer?.kind);
77
+ if (publication !== undefined)
78
+ assertValidPublicationConfig(publication);
79
+ const publicationPlan = planS3ProjectionBackend(publication);
80
+ const renditionsPlan = planRenditions(publication, process.cwd());
81
+ setSiteFilesForContentActionsBundle(publicationPlan.enabled ? publicationPlan.siteFiles : undefined);
82
+ const plugins = resolvePlugins(site.plugins);
83
+ const pluginDefinitions = Object.assign({}, ...plugins.map(({ plugin }) => plugin.schemaDefinitions()));
84
+ const pluginFunctions = Object.assign({}, ...plugins.map(({ plugin }) => plugin.functions()));
80
85
  const backend = defineBackend({
86
+ ...pluginFunctions,
81
87
  assignmentAction,
82
- auth: defineSiteAuth(site.auth ?? authConfigFromEnv()),
88
+ auth: defineSiteAuth(authConfig),
83
89
  categoryAction,
84
90
  contentActions,
85
- data,
86
- graphqlJwtAuthorizer,
91
+ data: defineSiteData(pluginDefinitions),
87
92
  knowledgeQuery,
88
93
  manageUserRole,
89
94
  modelAttachmentUpload,
@@ -96,10 +101,20 @@ export function defineSiteBackend(papyrusSite) {
96
101
  slackDelivery,
97
102
  storage,
98
103
  });
104
+ orderModelStacksInWaves(backend.data.resources.graphqlApi.node.scope, Object.keys(backend.data.resources.tables), site.resolverWaveSize ?? DEFAULT_RESOLVER_WAVE_SIZE);
105
+ const requestedCognitoDomainPrefix = (authConfig.cognitoDomainPrefix ?? "").trim();
106
+ if (authConfig.applyCognitoDomainPrefix === true
107
+ && requestedCognitoDomainPrefix !== ""
108
+ && authConfig.disableGoogleOAuth !== true) {
109
+ applyCognitoDomainPrefix(backend.auth.stack, requestedCognitoDomainPrefix);
110
+ }
111
+ const prospectusGoogleOnlyClient = authConfig.disableGoogleOAuth === true
112
+ ? undefined
113
+ : addProspectusGoogleOnlyClient(backend.auth.stack, backend.auth.resources.userPool, siteAuthRedirectUrls(authConfig));
99
114
  const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
100
115
  // Repo root in a Papyrus checkout; package root when installed from npm.
101
116
  const projectRoot = resolve(amplifyBackendDir, "..");
102
- const enableConsoleResponder = site.features?.consoleResponder ?? readEnvFlag("PAPYRUS_ENABLE_CONSOLE_RESPONDER", true);
117
+ const enableConsoleResponder = featureFlags.consoleResponder;
103
118
  if (enableConsoleResponder
104
119
  && !process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI?.trim()
105
120
  && !process.env.PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD?.trim()
@@ -139,36 +154,18 @@ export function defineSiteBackend(papyrusSite) {
139
154
  const messageThreadTable = backend.data.resources.tables.MessageThread;
140
155
  const dataStack = Stack.of(messageTable);
141
156
  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
- || "";
157
+ const amplifySsmEnvConfig = process.env.AMPLIFY_SSM_ENV_CONFIG?.trim() || "";
152
158
  if (enableConsoleResponder) {
153
- const consoleChatResponder = new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
159
+ new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
154
160
  messageTable,
155
161
  messageStreamArn,
156
162
  threadTable: messageThreadTable,
157
163
  projectRoot,
158
164
  graphqlEndpoint,
159
- amplifySsmEnvConfig: jwtSsmEnvConfig || undefined,
160
165
  responseTarget: process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET,
161
166
  model: process.env.PAPYRUS_CONSOLE_MODEL,
162
167
  prebuiltImageUri: process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI,
163
168
  });
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
169
  }
173
170
  if (enableSlackAgent) {
174
171
  const slackEventsLambda = backend.slackEvents.resources.lambda;
@@ -180,9 +177,9 @@ export function defineSiteBackend(papyrusSite) {
180
177
  backend.slackEvents.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
181
178
  backend.slackEvents.addEnvironment("PAPYRUS_CONSOLE_RESPONSE_TARGET", (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud");
182
179
  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);
180
+ if (amplifySsmEnvConfig) {
181
+ backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
182
+ backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
186
183
  }
187
184
  backend.slackDelivery.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
188
185
  backend.slackDelivery.addEnvironment("PAPYRUS_CONSOLE_RESPONSE_TARGET", (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud");
@@ -244,37 +241,19 @@ export function defineSiteBackend(papyrusSite) {
244
241
  let storageBackupPlan;
245
242
  let storageBackupVault;
246
243
  const storageBucketCfn = storageBucket.node.defaultChild;
247
- if ((enableStorageBackups || enableInboundEmail) && storageBucketCfn) {
248
- // Backups and inbound-email intake both rely on S3 → EventBridge notifications.
244
+ if ((enableStorageBackups || enableInboundEmail || renditionsPlan.enabled) && storageBucketCfn) {
245
+ // Backups, inbound-email intake and upload-time renditions all rely on S3 → EventBridge notifications.
249
246
  storageBucketCfn.addPropertyOverride("NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled", true);
250
247
  }
251
248
  if (enableStorageBackups) {
252
249
  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", {
250
+ const storageBackupVaultName = deriveStorageBackupVaultName(siteBackendIdentity, process.env.PAPYRUS_STORAGE_BACKUP_VAULT_NAME);
251
+ const storageBackups = addStorageBackups(storageBackupsStack, {
252
+ storageBucket,
258
253
  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
254
  });
255
+ storageBackupVault = storageBackups.vault;
256
+ storageBackupPlan = storageBackups.plan;
278
257
  }
279
258
  // Grant S3 access on Lambda roles only (not storage bucket policies) to avoid
280
259
  // storage ↔ data nested-stack circular dependencies from allow.resource().
@@ -347,9 +326,58 @@ export function defineSiteBackend(papyrusSite) {
347
326
  if (rebuildSettings.resources.length > 0) {
348
327
  contentActionsLambda.addToRolePolicy(new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }));
349
328
  }
329
+ let s3ProjectionPublication;
330
+ if (publication !== undefined) {
331
+ s3ProjectionPublication = addS3ProjectionPublication(backend.createStack("s3-projection"), publication, {
332
+ mediaBucket: storageBucket,
333
+ renditions: renditionsPlan,
334
+ });
335
+ contentActionsLambda.node.defaultChild.memorySize = publicationPlan.contentActionsMemorySize;
336
+ if (publication.reprojectOnDeploy === true) {
337
+ contentActionsLambda.node.defaultChild.timeout = REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS;
338
+ addReprojectOnDeploy(backend.createStack("reproject-on-deploy"), contentActionsLambda, reprojectDeployToken(process.env));
339
+ }
340
+ }
341
+ if (s3ProjectionPublication !== undefined) {
342
+ addProjectionRetries(contentActionsLambda, s3ProjectionPublication.deadLetterQueue);
343
+ }
344
+ const publicationWiring = contentActionsPublicationWiring(s3ProjectionPublication);
345
+ for (const [name, value] of Object.entries(publicationWiring.environment)) {
346
+ contentActionsLambda.addEnvironment(name, value);
347
+ }
348
+ for (const statement of publicationWiring.policyStatements) {
349
+ contentActionsLambda.addToRolePolicy(statement);
350
+ }
350
351
  if ((site.revalidateBaseUrl ?? "").trim() !== "") {
351
352
  backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
352
- backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET", secret("PAPYRUS_REVALIDATE_SECRET"));
353
+ const revalidateSecretParameter = (process.env.PAPYRUS_REVALIDATE_SECRET_PARAMETER ?? "").trim();
354
+ if (!revalidateSecretParameter.startsWith("/")) {
355
+ 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.");
356
+ }
357
+ backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET_PARAMETER", revalidateSecretParameter);
358
+ const revalidateStack = Stack.of(contentActionsLambda);
359
+ contentActionsLambda.addToRolePolicy(new PolicyStatement(revalidateSecretReadStatement({
360
+ partition: revalidateStack.partition,
361
+ region: revalidateStack.region,
362
+ account: revalidateStack.account,
363
+ parameterName: revalidateSecretParameter,
364
+ })));
365
+ const distributionProblems = validateRevalidateDistributionIds(site.revalidateDistributionIds, site.revalidateBaseUrl);
366
+ if (distributionProblems.length > 0)
367
+ throw new Error(`Invalid backend config: ${distributionProblems.join("; ")}`);
368
+ const distributionIds = site.revalidateDistributionIds ?? [];
369
+ if (distributionIds.length > 0) {
370
+ for (const [name, value] of Object.entries(revalidateDistributionEnvironment(distributionIds))) {
371
+ backend.contentActions.addEnvironment(name, value);
372
+ }
373
+ contentActionsLambda.addToRolePolicy(new PolicyStatement(revalidateInvalidationStatement({ partition: revalidateStack.partition, account: revalidateStack.account, distributionIds })));
374
+ for (const id of distributionIds) {
375
+ addInvalidationPathAlarm(revalidateStack, `ReaderEdgeInvalidationPaths${id}`, id);
376
+ }
377
+ }
378
+ }
379
+ else if (site.revalidateDistributionIds !== undefined) {
380
+ throw new Error("Invalid backend config: revalidateDistributionIds requires revalidateBaseUrl");
353
381
  }
354
382
  grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
355
383
  grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
@@ -416,83 +444,25 @@ export function defineSiteBackend(papyrusSite) {
416
444
  resources: ["*"],
417
445
  }));
418
446
  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", {
447
+ // SES allows one active receipt rule set per region, so a non-legacy site
448
+ // creates its own brand-named rule set and never activates it here.
449
+ const inboundSesPlan = planInboundEmailSes(siteBackendIdentity, enableInboundEmail, site.inboundEmailSes);
450
+ if (inboundSesPlan.createReceiptRules) {
451
+ addInboundEmailSesIntake(storageStack, {
452
+ storageBucket,
457
453
  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),
454
+ ruleSetName: deriveReceiptRuleSetName(siteBackendIdentity, inboundEmailDomain),
455
+ existingRuleSetName: site.inboundEmailSes?.existingReceiptRuleSetName,
456
+ ruleName: deriveReceiptRuleName(siteBackendIdentity),
457
+ activateRuleSet: inboundSesPlan.activateReceiptRuleSet,
458
+ domainIdentity: inboundSesPlan.manageDomainIdentity
459
+ ? {
460
+ domain: inboundEmailDomain,
461
+ hostedZoneId: inboundDnsZoneId,
462
+ hostedZoneName: inboundDnsZoneName,
463
+ recordName: inboundDnsRecordName,
464
+ }
465
+ : undefined,
496
466
  });
497
467
  }
498
468
  new InboundEmailStack(inboundEventStack, "InboundEmail", {
@@ -580,6 +550,9 @@ export function defineSiteBackend(papyrusSite) {
580
550
  }));
581
551
  backend.addOutput({
582
552
  custom: {
553
+ prospectusSignIn: prospectusGoogleOnlyClient
554
+ ? { clientId: prospectusGoogleOnlyClient.ref, callbackPath: PROSPECTUS_SIGN_IN_CALLBACK_PATH }
555
+ : null,
583
556
  inboundEmail: enableInboundEmail
584
557
  ? {
585
558
  domain: inboundEmailDomain,
@@ -611,5 +584,6 @@ export function defineSiteBackend(papyrusSite) {
611
584
  : null,
612
585
  },
613
586
  });
587
+ wirePlugins(backend, plugins, contentActionsLambda);
614
588
  return backend;
615
589
  }