@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,739 @@
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";
5
+ 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
+ import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
11
+ import { CfnEventSourceMapping, CfnFunction, Function as LambdaFunction, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
12
+ import * as s3 from "aws-cdk-lib/aws-s3";
13
+ import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aws-s3vectors";
14
+ import { dirname, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { rebuildTriggerSettings } from "./rebuild-trigger-settings";
17
+ import { authConfigFromEnv, defineSiteAuth } from "./auth/resource";
18
+ import { data } from "./data/resource";
19
+ import { assignmentAction } from "./functions/assignment-action/resource";
20
+ import { categoryAction } from "./functions/category-action/resource";
21
+ import { ConsoleChatResponderStack } from "./functions/console-chat-responder/resource";
22
+ import { graphqlJwtAuthorizer } from "./functions/graphql-jwt-authorizer/resource";
23
+ import { contentActions } from "./functions/content-actions/resource";
24
+ import { knowledgeQuery } from "./functions/knowledge-query/resource";
25
+ import { manageUserRole } from "./functions/manage-user-role/resource";
26
+ import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource";
27
+ import { newsroomSummary } from "./functions/newsroom-summary/resource";
28
+ import { procedureAction } from "./functions/procedure-action/resource";
29
+ import { readerSettings } from "./functions/reader-settings/resource";
30
+ import { emailSubmissionProcessor } from "./functions/email-submission-processor/resource";
31
+ import { sesInboundReceive } from "./functions/ses-inbound-receive/resource";
32
+ import { slackDelivery } from "./functions/slack-delivery/resource";
33
+ import { slackEvents } from "./functions/slack-events/resource";
34
+ import { InboundEmailStack } from "./inbound-email/stack";
35
+ import { storage } from "./storage/resource";
36
+ import type { PapyrusSite } from "../lib/define-site";
37
+
38
+ export function defineSiteBackend(papyrusSite: PapyrusSite) {
39
+ // `papyrus.config.ts`'s default export. Backend options live under `backend`;
40
+ // the default brand id namespaces account-global resources.
41
+ const site = { brandId: papyrusSite.defaultBrand, ...papyrusSite.backend };
42
+ const knowledgeVectorDimension = 1536;
43
+ const knowledgeEmbeddingModel = "text-embedding-3-small";
44
+
45
+ const amplifyBranch = (process.env.AWS_BRANCH ?? "").trim();
46
+ const amplifyAppId = (process.env.AWS_APP_ID ?? "").trim();
47
+ // Amplify app id whose `main` branch is this site's production pipeline (owns
48
+ // account-global resources: SES rule set, backup vault, legacy S3 Vectors name).
49
+ const productionAppId = (site.productionAppId ?? "").trim();
50
+ const isAmplifyProductionPipeline =
51
+ amplifyBranch === "main" && productionAppId !== "" && amplifyAppId === productionAppId;
52
+
53
+ function readEnvFlag(name: string, defaultValue: boolean): boolean {
54
+ const raw = (process.env[name] ?? "").trim().toLowerCase();
55
+ if (["1", "true", "yes", "on"].includes(raw)) return true;
56
+ if (["0", "false", "no", "off"].includes(raw)) return false;
57
+ return defaultValue;
58
+ }
59
+
60
+ function sanitizeAwsName(value: string, maxLength = 50): string {
61
+ return value
62
+ .toLowerCase()
63
+ .replace(/[^a-z0-9-]/g, "-")
64
+ .replace(/-+/g, "-")
65
+ .replace(/^-|-$/g, "")
66
+ .slice(0, maxLength);
67
+ }
68
+
69
+ // S3 Vectors index names are account-global. Keep the original name for the
70
+ // p.apyr.us production app so its existing index + data are not orphaned;
71
+ // namespace every other app's index by site brand so a second CMS app can
72
+ // deploy into the same account without colliding.
73
+ const papyrusSiteBrand = (
74
+ site.brandId
75
+ ?? process.env.PAPYRUS_SITE_BRAND
76
+ ?? process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND
77
+ ?? "papyrus"
78
+ ).trim().toLowerCase();
79
+ const knowledgeVectorIndexName =
80
+ productionAppId !== "" && amplifyAppId === productionAppId
81
+ ? "papyrus-knowledge"
82
+ : `papyrus-knowledge-${sanitizeAwsName(papyrusSiteBrand, 40)}`;
83
+
84
+ // SES active receipt rule sets and shared backup vault names are account-global.
85
+ // Keep them on the production pipeline only unless explicitly opted in.
86
+ // A feature flag in the site config wins over the environment.
87
+ const enableInboundEmail = site.features?.inboundEmail ?? readEnvFlag("PAPYRUS_ENABLE_INBOUND_EMAIL", isAmplifyProductionPipeline);
88
+ const enableSlackAgent = site.features?.slack ?? readEnvFlag("PAPYRUS_ENABLE_SLACK", false);
89
+ const enableStorageBackups = site.features?.storageBackups ?? readEnvFlag("PAPYRUS_ENABLE_STORAGE_BACKUPS", isAmplifyProductionPipeline);
90
+
91
+ const backend = defineBackend({
92
+ assignmentAction,
93
+ auth: defineSiteAuth(site.auth ?? authConfigFromEnv()),
94
+ categoryAction,
95
+ contentActions,
96
+ data,
97
+ graphqlJwtAuthorizer,
98
+ knowledgeQuery,
99
+ manageUserRole,
100
+ modelAttachmentUpload,
101
+ newsroomSummary,
102
+ procedureAction,
103
+ readerSettings,
104
+ emailSubmissionProcessor,
105
+ sesInboundReceive,
106
+ slackEvents,
107
+ slackDelivery,
108
+ storage,
109
+ });
110
+
111
+ const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
112
+ // Repo root in a Papyrus checkout; package root when installed from npm.
113
+ const projectRoot = resolve(amplifyBackendDir, "..");
114
+ const enableConsoleResponder = site.features?.consoleResponder ?? readEnvFlag("PAPYRUS_ENABLE_CONSOLE_RESPONDER", true);
115
+
116
+ if (
117
+ enableConsoleResponder
118
+ && !process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI?.trim()
119
+ && !process.env.PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD?.trim()
120
+ && !isAmplifyProductionPipeline
121
+ ) {
122
+ process.env.PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD = "true";
123
+ }
124
+ const inboundEmailDomain = (process.env.PAPYRUS_INBOUND_EMAIL_DOMAIN ?? "p.apyr.us").trim().toLowerCase();
125
+ const inboundEmailLocalParts = (process.env.PAPYRUS_INBOUND_EMAIL_LOCAL_PARTS ?? "submissions,suggestions")
126
+ .split(",")
127
+ .map((entry) => entry.trim().toLowerCase())
128
+ .filter(Boolean);
129
+ const inboundEmailCorpusKey = (process.env.PAPYRUS_INBOUND_EMAIL_CORPUS_KEY ?? "AI-ML-research").trim();
130
+ const inboundDnsZoneId = (process.env.PAPYRUS_ROUTE53_HOSTED_ZONE_ID ?? "Z10285921B1G7MVRV06W9").trim();
131
+ const inboundDnsZoneName = (process.env.PAPYRUS_ROUTE53_HOSTED_ZONE_NAME ?? "apyr.us").trim().replace(/\.$/, "");
132
+ const inboundDnsRecordName = inboundEmailDomain.endsWith(`.${inboundDnsZoneName}`)
133
+ ? inboundEmailDomain.slice(0, -(inboundDnsZoneName.length + 1))
134
+ : inboundEmailDomain;
135
+
136
+ let slackEventsUrl: string | undefined;
137
+
138
+ if (enableConsoleResponder || enableSlackAgent) {
139
+ const messageTable = backend.data.resources.tables.Message;
140
+ const cfnTables = backend.data.resources.cfnResources.cfnTables;
141
+ const messageCfnTable =
142
+ cfnTables.Message
143
+ ?? cfnTables.MessageTable
144
+ ?? Object.entries(cfnTables).find(([key]) => {
145
+ const normalized = key.toLowerCase();
146
+ return normalized.includes("message") && !normalized.includes("thread");
147
+ })?.[1];
148
+
149
+ if (messageCfnTable) {
150
+ messageCfnTable.streamSpecification = {
151
+ streamViewType: dynamodb.StreamViewType.NEW_IMAGE,
152
+ };
153
+ }
154
+
155
+ const messageStreamArn = messageTable.tableStreamArn ?? messageCfnTable?.attrStreamArn;
156
+ if (!messageStreamArn) {
157
+ throw new Error(`Message agents require Message table stream ARN. cfnTables=${Object.keys(cfnTables).join(",")}`);
158
+ }
159
+
160
+ const messageThreadTable = backend.data.resources.tables.MessageThread;
161
+ const dataStack = Stack.of(messageTable);
162
+ const graphqlEndpoint = backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl;
163
+ const jwtAuthorizerCfn = backend.graphqlJwtAuthorizer.resources.lambda.node.defaultChild as CfnFunction | undefined;
164
+ const jwtAuthorizerEnvironment = jwtAuthorizerCfn?.environment;
165
+ const jwtAuthorizerVariables =
166
+ jwtAuthorizerEnvironment
167
+ && typeof jwtAuthorizerEnvironment === "object"
168
+ && "variables" in jwtAuthorizerEnvironment
169
+ ? (jwtAuthorizerEnvironment as { variables?: Record<string, string> }).variables
170
+ : undefined;
171
+ const jwtSsmEnvConfig =
172
+ process.env.AMPLIFY_SSM_ENV_CONFIG?.trim()
173
+ || jwtAuthorizerVariables?.AMPLIFY_SSM_ENV_CONFIG?.trim()
174
+ || "";
175
+
176
+ if (enableConsoleResponder) {
177
+ const consoleChatResponder = new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
178
+ messageTable,
179
+ messageStreamArn,
180
+ threadTable: messageThreadTable,
181
+ projectRoot,
182
+ graphqlEndpoint,
183
+ amplifySsmEnvConfig: jwtSsmEnvConfig || undefined,
184
+ responseTarget: process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET,
185
+ model: process.env.PAPYRUS_CONSOLE_MODEL,
186
+ prebuiltImageUri: process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI,
187
+ });
188
+ if (jwtSsmEnvConfig) {
189
+ consoleChatResponder.responderFunction.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
190
+ }
191
+ const jwtSecretSsmParam =
192
+ process.env.PAPYRUS_JWT_SECRET_SSM_PARAM?.trim()
193
+ || (productionAppId !== "" && amplifyAppId === productionAppId
194
+ ? `/amplify/${amplifyAppId}/main-branch-cb38ada667/PAPYRUS_JWT_SECRET`
195
+ : "/amplify/papyrus/ryan-sandbox-adcd88a186/PAPYRUS_JWT_SECRET");
196
+ consoleChatResponder.responderFunction.addEnvironment(
197
+ "PAPYRUS_JWT_SECRET_SSM_PARAM",
198
+ jwtSecretSsmParam,
199
+ );
200
+ }
201
+
202
+ if (enableSlackAgent) {
203
+ const slackEventsLambda = backend.slackEvents.resources.lambda as LambdaFunction;
204
+ const slackDeliveryLambda = backend.slackDelivery.resources.lambda as LambdaFunction;
205
+ slackEventsLambda.addToRolePolicy(
206
+ new PolicyStatement({
207
+ actions: ["appsync:GraphQL"],
208
+ resources: ["*"],
209
+ }),
210
+ );
211
+ backend.slackEvents.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
212
+ backend.slackEvents.addEnvironment(
213
+ "PAPYRUS_CONSOLE_RESPONSE_TARGET",
214
+ (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud",
215
+ );
216
+ backend.slackEvents.addEnvironment(
217
+ "PAPYRUS_SLACK_ALLOWED_USER_IDS",
218
+ (process.env.PAPYRUS_SLACK_ALLOWED_USER_IDS ?? "").trim(),
219
+ );
220
+ if (jwtSsmEnvConfig) {
221
+ backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
222
+ backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
223
+ }
224
+ backend.slackDelivery.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
225
+ backend.slackDelivery.addEnvironment(
226
+ "PAPYRUS_CONSOLE_RESPONSE_TARGET",
227
+ (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud",
228
+ );
229
+ const slackBotTokenName = (process.env.PAPYRUS_SLACK_BOT_TOKEN_NAME ?? "PAPYRUS_SLACK_BOT_TOKEN").trim();
230
+ backend.slackDelivery.addEnvironment("PAPYRUS_SLACK_BOT_TOKEN", secret(slackBotTokenName));
231
+
232
+ const slackStack = Stack.of(slackEventsLambda);
233
+ const slackSsmResources = amplifyAppId
234
+ ? [
235
+ `arn:aws:ssm:${slackStack.region}:${slackStack.account}:parameter/amplify/${amplifyAppId}/*`,
236
+ `arn:aws:ssm:${slackStack.region}:${slackStack.account}:parameter/amplify/shared/${amplifyAppId}/*`,
237
+ ]
238
+ : [`arn:aws:ssm:${slackStack.region}:${slackStack.account}:parameter/amplify/*`];
239
+ const slackSsmPolicy = new PolicyStatement({
240
+ effect: Effect.ALLOW,
241
+ actions: ["ssm:GetParameter", "ssm:GetParameters"],
242
+ resources: slackSsmResources,
243
+ });
244
+ slackEventsLambda.addToRolePolicy(slackSsmPolicy);
245
+ slackDeliveryLambda.addToRolePolicy(slackSsmPolicy);
246
+
247
+ // Wire Slack on the data stack (not a nested SlackAgent stack) to avoid CloudFormation
248
+ // circular dependencies between Message and slack-delivery in the data resource group.
249
+ const slackEventsFunctionUrl = slackEventsLambda.addFunctionUrl({
250
+ authType: FunctionUrlAuthType.NONE,
251
+ });
252
+ slackEventsUrl = slackEventsFunctionUrl.url;
253
+
254
+ const slackConsumerStack = backend.createStack("slack-consumer");
255
+ new CfnEventSourceMapping(slackConsumerStack, "SlackDeliveryMessageStreamMapping", {
256
+ batchSize: 1,
257
+ eventSourceArn: messageStreamArn,
258
+ functionName: slackDeliveryLambda.functionArn,
259
+ functionResponseTypes: ["ReportBatchItemFailures"],
260
+ maximumRetryAttempts: 2,
261
+ startingPosition: "LATEST",
262
+ });
263
+
264
+ slackDeliveryLambda.addToRolePolicy(
265
+ new PolicyStatement({
266
+ effect: Effect.ALLOW,
267
+ actions: [
268
+ "dynamodb:DescribeStream",
269
+ "dynamodb:GetRecords",
270
+ "dynamodb:GetShardIterator",
271
+ "dynamodb:ListStreams",
272
+ ],
273
+ resources: [messageStreamArn],
274
+ }),
275
+ );
276
+ slackDeliveryLambda.addToRolePolicy(
277
+ new PolicyStatement({
278
+ effect: Effect.ALLOW,
279
+ actions: ["dynamodb:GetItem", "dynamodb:Query", "dynamodb:BatchGetItem"],
280
+ resources: [messageTable.tableArn, `${messageTable.tableArn}/index/*`],
281
+ }),
282
+ );
283
+ slackDeliveryLambda.addToRolePolicy(
284
+ new PolicyStatement({
285
+ effect: Effect.ALLOW,
286
+ actions: ["appsync:GraphQL"],
287
+ resources: ["*"],
288
+ }),
289
+ );
290
+ }
291
+ }
292
+
293
+ const storageBucket = backend.storage.resources.bucket;
294
+ const storageStack = Stack.of(storageBucket);
295
+
296
+ let storageBackupPlan: backup.BackupPlan | undefined;
297
+ let storageBackupVault: backup.BackupVault | undefined;
298
+
299
+ const storageBucketCfn = storageBucket.node.defaultChild as s3.CfnBucket | undefined;
300
+ if ((enableStorageBackups || enableInboundEmail) && storageBucketCfn) {
301
+ // Backups and inbound-email intake both rely on S3 → EventBridge notifications.
302
+ storageBucketCfn.addPropertyOverride(
303
+ "NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled",
304
+ true,
305
+ );
306
+ }
307
+
308
+ if (enableStorageBackups) {
309
+ const storageBackupsStack = backend.createStack("storage-backups");
310
+ const storageBackupVaultName = sanitizeAwsName(
311
+ process.env.PAPYRUS_STORAGE_BACKUP_VAULT_NAME?.trim()
312
+ || (isAmplifyProductionPipeline
313
+ ? `papyrus-${productionAppId}-main-media-backup-vault`
314
+ : `papyrus-${sanitizeAwsName(storageBackupsStack.stackName, 32)}-media-vault`),
315
+ );
316
+
317
+ storageBackupVault = new backup.BackupVault(storageBackupsStack, "PapyrusStorageBackupVault", {
318
+ backupVaultName: storageBackupVaultName,
319
+ removalPolicy: RemovalPolicy.RETAIN,
320
+ });
321
+ storageBackupPlan = new backup.BackupPlan(storageBackupsStack, "PapyrusStorageBackupPlan", {
322
+ backupVault: storageBackupVault,
323
+ });
324
+
325
+ storageBackupPlan.addRule(
326
+ new backup.BackupPlanRule({
327
+ ruleName: "papyrus-storage-pitr-35d",
328
+ enableContinuousBackup: true,
329
+ deleteAfter: Duration.days(35),
330
+ }),
331
+ );
332
+
333
+ storageBackupPlan.addRule(
334
+ new backup.BackupPlanRule({
335
+ ruleName: "papyrus-storage-daily-365d",
336
+ scheduleExpression: events.Schedule.cron({ minute: "0", hour: "5" }),
337
+ deleteAfter: Duration.days(365),
338
+ }),
339
+ );
340
+
341
+ storageBackupPlan.addSelection("PapyrusStorageBackupSelection", {
342
+ allowRestores: true,
343
+ resources: [backup.BackupResource.fromArn(storageBucket.bucketArn)],
344
+ });
345
+ }
346
+
347
+ // Grant S3 access on Lambda roles only (not storage bucket policies) to avoid
348
+ // storage ↔ data nested-stack circular dependencies from allow.resource().
349
+ const corporaObjectArn = `${storageBucket.bucketArn}/corpora/*`;
350
+ const mediaObjectArn = `${storageBucket.bucketArn}/media/*`;
351
+ const newsroomObjectArn = `${storageBucket.bucketArn}/newsroom/*`;
352
+ const grantCorporaRead = (lambda: LambdaFunction) => {
353
+ lambda.addToRolePolicy(
354
+ new PolicyStatement({
355
+ actions: ["s3:GetObject"],
356
+ resources: [corporaObjectArn],
357
+ }),
358
+ );
359
+ };
360
+ const grantNewsroomReadWrite = (lambda: LambdaFunction) => {
361
+ lambda.addToRolePolicy(
362
+ new PolicyStatement({
363
+ actions: ["s3:GetObject", "s3:PutObject"],
364
+ resources: [newsroomObjectArn],
365
+ }),
366
+ );
367
+ };
368
+ const grantNewsroomReadWriteDelete = (lambda: LambdaFunction) => {
369
+ lambda.addToRolePolicy(
370
+ new PolicyStatement({
371
+ actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
372
+ resources: [newsroomObjectArn],
373
+ }),
374
+ );
375
+ };
376
+ const grantMediaReadWriteDelete = (lambda: LambdaFunction) => {
377
+ lambda.addToRolePolicy(
378
+ new PolicyStatement({
379
+ actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
380
+ resources: [mediaObjectArn],
381
+ }),
382
+ );
383
+ };
384
+ const mediaBucketName = storageBucket.bucketName;
385
+ const withMediaBucketEnv = (resource: { addEnvironment: (key: string, value: string) => void }) => {
386
+ resource.addEnvironment("PAPYRUS_MEDIA_BUCKET_NAME", mediaBucketName);
387
+ resource.addEnvironment("papyrusMedia_BUCKET_NAME", mediaBucketName);
388
+ };
389
+ for (const resource of [
390
+ backend.assignmentAction,
391
+ backend.categoryAction,
392
+ backend.contentActions,
393
+ backend.emailSubmissionProcessor,
394
+ backend.knowledgeQuery,
395
+ backend.modelAttachmentUpload,
396
+ backend.newsroomSummary,
397
+ backend.procedureAction,
398
+ backend.sesInboundReceive,
399
+ ]) {
400
+ withMediaBucketEnv(resource);
401
+ }
402
+ grantCorporaRead(backend.knowledgeQuery.resources.lambda as LambdaFunction);
403
+ grantNewsroomReadWrite(backend.assignmentAction.resources.lambda as LambdaFunction);
404
+ grantNewsroomReadWrite(backend.categoryAction.resources.lambda as LambdaFunction);
405
+ grantNewsroomReadWrite(backend.newsroomSummary.resources.lambda as LambdaFunction);
406
+ const contentActionsLambda = backend.contentActions.resources.lambda as LambdaFunction;
407
+ grantNewsroomReadWriteDelete(contentActionsLambda);
408
+ contentActionsLambda.addEnvironment(
409
+ "PAPYRUS_GRAPHQL_ENDPOINT",
410
+ backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl,
411
+ );
412
+ contentActionsLambda.addToRolePolicy(
413
+ new PolicyStatement({
414
+ actions: ["appsync:GraphQL"],
415
+ resources: ["*"],
416
+ }),
417
+ );
418
+ const rebuildSettings = rebuildTriggerSettings({
419
+ reader: site.reader,
420
+ stagingBuildEnabled: site.stagingBuild?.enabled === true,
421
+ cmsAppId: amplifyAppId,
422
+ region: Stack.of(contentActionsLambda).region,
423
+ account: Stack.of(contentActionsLambda).account,
424
+ });
425
+ for (const [name, value] of Object.entries(rebuildSettings.environment)) {
426
+ contentActionsLambda.addEnvironment(name, value);
427
+ }
428
+ if (rebuildSettings.resources.length > 0) {
429
+ contentActionsLambda.addToRolePolicy(
430
+ new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }),
431
+ );
432
+ }
433
+ if ((site.revalidateBaseUrl ?? "").trim() !== "") {
434
+ backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
435
+ backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET", secret("PAPYRUS_REVALIDATE_SECRET"));
436
+ }
437
+ grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
438
+ grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
439
+ if (backend.sesInboundReceive) {
440
+ grantNewsroomReadWrite(backend.sesInboundReceive.resources.lambda as LambdaFunction);
441
+ }
442
+
443
+ if (enableInboundEmail) {
444
+ if (!backend.sesInboundReceive || !backend.emailSubmissionProcessor) {
445
+ throw new Error("Inbound email is enabled but sesInboundReceive/emailSubmissionProcessor were not registered.");
446
+ }
447
+ const inboundReceive = backend.sesInboundReceive;
448
+ const inboundProcessor = backend.emailSubmissionProcessor;
449
+ const receiveLambda = inboundReceive.resources.lambda as LambdaFunction;
450
+ const processorLambda = inboundProcessor.resources.lambda as LambdaFunction;
451
+ const graphqlEndpoint = backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl;
452
+ const inboundRecipients = inboundEmailLocalParts.map((localPart) => `${localPart}@${inboundEmailDomain}`);
453
+
454
+ inboundReceive.addEnvironment(
455
+ "PAPYRUS_EMAIL_SUBMISSION_PROCESSOR_FUNCTION_NAME",
456
+ processorLambda.functionName,
457
+ );
458
+ inboundReceive.addEnvironment("PAPYRUS_INBOUND_EMAIL_DOMAIN", inboundEmailDomain);
459
+ inboundReceive.addEnvironment("PAPYRUS_INBOUND_EMAIL_LOCAL_PARTS", inboundEmailLocalParts.join(","));
460
+ inboundReceive.addEnvironment("PAPYRUS_INBOUND_EMAIL_CORPUS_KEY", inboundEmailCorpusKey);
461
+ inboundReceive.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
462
+
463
+ inboundProcessor.addEnvironment("PAPYRUS_INBOUND_EMAIL_CORPUS_KEY", inboundEmailCorpusKey);
464
+ inboundProcessor.addEnvironment("PAPYRUS_INBOUND_EMAIL_DOMAIN", inboundEmailDomain);
465
+ inboundProcessor.addEnvironment("PAPYRUS_INBOUND_FEEDBACK_EMAIL_ENABLED", "true");
466
+ inboundProcessor.addEnvironment(
467
+ "PAPYRUS_INBOUND_FEEDBACK_FROM_EMAIL",
468
+ `Papyrus Submissions <submissions@${inboundEmailDomain}>`,
469
+ );
470
+ inboundProcessor.addEnvironment("PAPYRUS_PUBLIC_SITE_BASE_URL", "https://p.apyr.us");
471
+ inboundProcessor.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
472
+
473
+ receiveLambda.addToRolePolicy(
474
+ new PolicyStatement({
475
+ actions: ["lambda:InvokeFunction"],
476
+ resources: [processorLambda.functionArn],
477
+ }),
478
+ );
479
+ receiveLambda.addToRolePolicy(
480
+ new PolicyStatement({
481
+ actions: ["s3:GetObject", "s3:ListBucket"],
482
+ resources: [
483
+ storageBucket.bucketArn,
484
+ `${storageBucket.bucketArn}/inbound-email/*`,
485
+ ],
486
+ }),
487
+ );
488
+ processorLambda.addToRolePolicy(
489
+ new PolicyStatement({
490
+ actions: ["s3:ListBucket"],
491
+ resources: [storageBucket.bucketArn],
492
+ }),
493
+ );
494
+ processorLambda.addToRolePolicy(
495
+ new PolicyStatement({
496
+ actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
497
+ resources: [
498
+ `${storageBucket.bucketArn}/inbound-email/*`,
499
+ `${storageBucket.bucketArn}/inbound-email-archived/*`,
500
+ ],
501
+ }),
502
+ );
503
+ processorLambda.addToRolePolicy(
504
+ new PolicyStatement({
505
+ actions: ["s3:GetObject", "s3:PutObject"],
506
+ resources: [corporaObjectArn],
507
+ }),
508
+ );
509
+ receiveLambda.addToRolePolicy(
510
+ new PolicyStatement({
511
+ actions: ["appsync:GraphQL"],
512
+ resources: ["*"],
513
+ }),
514
+ );
515
+ processorLambda.addToRolePolicy(
516
+ new PolicyStatement({
517
+ actions: ["appsync:GraphQL"],
518
+ resources: ["*"],
519
+ }),
520
+ );
521
+ processorLambda.addToRolePolicy(
522
+ new PolicyStatement({
523
+ actions: ["ses:SendEmail", "ses:SendRawEmail"],
524
+ resources: ["*"],
525
+ }),
526
+ );
527
+
528
+ const inboundEventStack = Stack.of(receiveLambda);
529
+
530
+ // SES receipt rule sets are account-global; only provision on the production pipeline.
531
+ if (isAmplifyProductionPipeline) {
532
+ const inboundDnsZone = route53.HostedZone.fromHostedZoneAttributes(storageStack, "PapyrusInboundDnsZone", {
533
+ hostedZoneId: inboundDnsZoneId,
534
+ zoneName: inboundDnsZoneName,
535
+ });
536
+ const verifyInboundEmailDomain = new AwsCustomResource(storageStack, "VerifyInboundEmailDomain", {
537
+ onCreate: {
538
+ service: "SES",
539
+ action: "verifyDomainIdentity",
540
+ parameters: {
541
+ Domain: inboundEmailDomain,
542
+ },
543
+ physicalResourceId: PhysicalResourceId.of(`ses-domain-verify-${inboundEmailDomain}`),
544
+ },
545
+ onUpdate: {
546
+ service: "SES",
547
+ action: "verifyDomainIdentity",
548
+ parameters: {
549
+ Domain: inboundEmailDomain,
550
+ },
551
+ physicalResourceId: PhysicalResourceId.of(`ses-domain-verify-${inboundEmailDomain}`),
552
+ },
553
+ policy: AwsCustomResourcePolicy.fromSdkCalls({
554
+ resources: AwsCustomResourcePolicy.ANY_RESOURCE,
555
+ }),
556
+ timeout: Duration.minutes(2),
557
+ });
558
+ new route53.TxtRecord(storageStack, "PapyrusInboundEmailDomainVerification", {
559
+ zone: inboundDnsZone,
560
+ recordName: `_amazonses.${inboundDnsRecordName}`,
561
+ ttl: Duration.minutes(5),
562
+ values: [verifyInboundEmailDomain.getResponseField("VerificationToken")],
563
+ });
564
+
565
+ const inboundRuleSet = new ses.ReceiptRuleSet(storageStack, "PapyrusInboundEmailRuleSet", {
566
+ receiptRuleSetName: `papyrus-inbound-${inboundEmailDomain.replace(/\./g, "-")}`,
567
+ });
568
+ inboundRuleSet.addRule("PapyrusInboundSubmissions", {
569
+ recipients: inboundRecipients,
570
+ enabled: true,
571
+ scanEnabled: true,
572
+ tlsPolicy: ses.TlsPolicy.REQUIRE,
573
+ actions: [
574
+ new sesActions.S3({
575
+ bucket: storageBucket,
576
+ objectKeyPrefix: "inbound-email/",
577
+ }),
578
+ ],
579
+ });
580
+
581
+ new AwsCustomResource(storageStack, "ActivateInboundEmailRuleSet", {
582
+ onCreate: {
583
+ service: "SES",
584
+ action: "setActiveReceiptRuleSet",
585
+ parameters: {
586
+ RuleSetName: inboundRuleSet.receiptRuleSetName,
587
+ },
588
+ physicalResourceId: PhysicalResourceId.of(`activate-${inboundRuleSet.receiptRuleSetName}`),
589
+ },
590
+ onUpdate: {
591
+ service: "SES",
592
+ action: "setActiveReceiptRuleSet",
593
+ parameters: {
594
+ RuleSetName: inboundRuleSet.receiptRuleSetName,
595
+ },
596
+ physicalResourceId: PhysicalResourceId.of(`activate-${inboundRuleSet.receiptRuleSetName}`),
597
+ },
598
+ onDelete: {
599
+ service: "SES",
600
+ action: "setActiveReceiptRuleSet",
601
+ parameters: {
602
+ RuleSetName: null,
603
+ },
604
+ },
605
+ policy: AwsCustomResourcePolicy.fromSdkCalls({
606
+ resources: AwsCustomResourcePolicy.ANY_RESOURCE,
607
+ }),
608
+ timeout: Duration.minutes(2),
609
+ });
610
+ }
611
+
612
+ new InboundEmailStack(inboundEventStack, "InboundEmail", {
613
+ storageBucket,
614
+ receiveFunctionArn: receiveLambda.functionArn,
615
+ });
616
+ }
617
+
618
+ const knowledgeVectorsStack = backend.createStack("knowledge-vectors");
619
+ const knowledgeVectorBucket = new CfnVectorBucket(knowledgeVectorsStack, "PapyrusKnowledgeVectorBucket", {
620
+ encryptionConfiguration: {
621
+ sseType: "AES256",
622
+ },
623
+ });
624
+ const knowledgeVectorIndex = new CfnIndex(knowledgeVectorsStack, "PapyrusKnowledgeVectorIndex", {
625
+ dataType: "float32",
626
+ dimension: knowledgeVectorDimension,
627
+ distanceMetric: "cosine",
628
+ indexName: knowledgeVectorIndexName,
629
+ metadataConfiguration: {
630
+ nonFilterableMetadataKeys: ["text", "summary", "sourceUri", "title"],
631
+ },
632
+ vectorBucketArn: knowledgeVectorBucket.attrVectorBucketArn,
633
+ });
634
+ knowledgeVectorIndex.node.addDependency(knowledgeVectorBucket);
635
+ const knowledgeVectorBucketPolicy = new CfnVectorBucketPolicy(knowledgeVectorsStack, "PapyrusKnowledgeVectorBucketPolicy", {
636
+ vectorBucketArn: knowledgeVectorBucket.attrVectorBucketArn,
637
+ policy: {
638
+ Version: "2012-10-17",
639
+ Statement: [
640
+ {
641
+ Sid: "AllowSameAccountPapyrusKnowledgeVectorAccess",
642
+ Effect: "Allow",
643
+ Principal: {
644
+ AWS: `arn:aws:iam::${knowledgeVectorsStack.account}:root`,
645
+ },
646
+ Action: [
647
+ "s3vectors:GetIndex",
648
+ "s3vectors:GetVectors",
649
+ "s3vectors:ListVectors",
650
+ "s3vectors:PutVectors",
651
+ "s3vectors:QueryVectors",
652
+ "s3vectors:DeleteVectors",
653
+ ],
654
+ Resource: [
655
+ knowledgeVectorBucket.attrVectorBucketArn,
656
+ knowledgeVectorIndex.attrIndexArn,
657
+ ],
658
+ },
659
+ ],
660
+ },
661
+ });
662
+ knowledgeVectorBucketPolicy.node.addDependency(knowledgeVectorIndex);
663
+
664
+ const knowledgeQueryLambda = backend.knowledgeQuery.resources.lambda as LambdaFunction;
665
+ backend.knowledgeQuery.addEnvironment("OPENAI_API_KEY", secret("OPENAI_API_KEY"));
666
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_STORAGE_BUCKET_NAME", storageBucket.bucketName);
667
+ knowledgeQueryLambda.addEnvironment(
668
+ "PAPYRUS_GRAPHQL_ENDPOINT",
669
+ backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl,
670
+ );
671
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_S3_VECTOR_INDEX_ARN", knowledgeVectorIndex.attrIndexArn);
672
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_S3_VECTOR_INDEX_NAME", knowledgeVectorIndexName);
673
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_EMBEDDING_MODEL", knowledgeEmbeddingModel);
674
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_EMBEDDING_DIMENSIONS", String(knowledgeVectorDimension));
675
+ knowledgeQueryLambda.addToRolePolicy(
676
+ new PolicyStatement({
677
+ actions: [
678
+ "s3vectors:GetIndex",
679
+ "s3vectors:GetVectors",
680
+ "s3vectors:ListVectors",
681
+ "s3vectors:QueryVectors",
682
+ ],
683
+ resources: [knowledgeVectorIndex.attrIndexArn],
684
+ }),
685
+ );
686
+ knowledgeQueryLambda.addToRolePolicy(
687
+ new PolicyStatement({
688
+ actions: ["ssm:GetParameter"],
689
+ resources: [
690
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/papyrus/*/OPENAI_API_KEY`,
691
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/shared/papyrus/OPENAI_API_KEY`,
692
+ // Pipeline-deployed apps store secrets under amplify/<appId>/... rather
693
+ // than the sandbox's amplify/papyrus/... path. Grant the app's own
694
+ // secret path so a second CMS app (e.g. pilobol-us) can read its key.
695
+ ...(amplifyAppId
696
+ ? [
697
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/${amplifyAppId}/*/OPENAI_API_KEY`,
698
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/shared/${amplifyAppId}/OPENAI_API_KEY`,
699
+ ]
700
+ : []),
701
+ ],
702
+ }),
703
+ );
704
+
705
+ backend.addOutput({
706
+ custom: {
707
+ inboundEmail: enableInboundEmail
708
+ ? {
709
+ domain: inboundEmailDomain,
710
+ localParts: inboundEmailLocalParts,
711
+ addresses: inboundEmailLocalParts.map((localPart) => `${localPart}@${inboundEmailDomain}`),
712
+ corpusKey: inboundEmailCorpusKey,
713
+ }
714
+ : null,
715
+ knowledgeQuery: {
716
+ s3VectorBucketArn: knowledgeVectorBucket.attrVectorBucketArn,
717
+ s3VectorIndexArn: knowledgeVectorIndex.attrIndexArn,
718
+ s3VectorIndexName: knowledgeVectorIndexName,
719
+ embeddingModel: knowledgeEmbeddingModel,
720
+ embeddingDimensions: knowledgeVectorDimension,
721
+ },
722
+ storageBackups: storageBackupPlan && storageBackupVault
723
+ ? {
724
+ backupPlanId: storageBackupPlan.backupPlanId,
725
+ backupVaultArn: storageBackupVault.backupVaultArn,
726
+ backupVaultName: storageBackupVault.backupVaultName,
727
+ protectedBucketArn: storageBucket.bucketArn,
728
+ protectedBucketName: storageBucket.bucketName,
729
+ }
730
+ : null,
731
+ slack: enableSlackAgent && slackEventsUrl
732
+ ? {
733
+ eventsUrl: slackEventsUrl,
734
+ }
735
+ : null,
736
+ },
737
+ });
738
+ return backend;
739
+ }