@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,615 @@
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, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
12
+ import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aws-s3vectors";
13
+ import { dirname, resolve } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { rebuildTriggerSettings } from "./rebuild-trigger-settings.js";
16
+ import { authConfigFromEnv, defineSiteAuth } from "./auth/resource.js";
17
+ import { data } from "./data/resource.js";
18
+ import { assignmentAction } from "./functions/assignment-action/resource.js";
19
+ import { categoryAction } from "./functions/category-action/resource.js";
20
+ 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";
23
+ import { knowledgeQuery } from "./functions/knowledge-query/resource.js";
24
+ import { manageUserRole } from "./functions/manage-user-role/resource.js";
25
+ import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource.js";
26
+ import { newsroomSummary } from "./functions/newsroom-summary/resource.js";
27
+ import { procedureAction } from "./functions/procedure-action/resource.js";
28
+ import { readerSettings } from "./functions/reader-settings/resource.js";
29
+ import { emailSubmissionProcessor } from "./functions/email-submission-processor/resource.js";
30
+ import { sesInboundReceive } from "./functions/ses-inbound-receive/resource.js";
31
+ import { slackDelivery } from "./functions/slack-delivery/resource.js";
32
+ import { slackEvents } from "./functions/slack-events/resource.js";
33
+ import { InboundEmailStack } from "./inbound-email/stack.js";
34
+ import { storage } from "./storage/resource.js";
35
+ export function defineSiteBackend(papyrusSite) {
36
+ // `papyrus.config.ts`'s default export. Backend options live under `backend`;
37
+ // the default brand id namespaces account-global resources.
38
+ const site = { brandId: papyrusSite.defaultBrand, ...papyrusSite.backend };
39
+ const knowledgeVectorDimension = 1536;
40
+ const knowledgeEmbeddingModel = "text-embedding-3-small";
41
+ const amplifyBranch = (process.env.AWS_BRANCH ?? "").trim();
42
+ 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).
45
+ 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);
80
+ const backend = defineBackend({
81
+ assignmentAction,
82
+ auth: defineSiteAuth(site.auth ?? authConfigFromEnv()),
83
+ categoryAction,
84
+ contentActions,
85
+ data,
86
+ graphqlJwtAuthorizer,
87
+ knowledgeQuery,
88
+ manageUserRole,
89
+ modelAttachmentUpload,
90
+ newsroomSummary,
91
+ procedureAction,
92
+ readerSettings,
93
+ emailSubmissionProcessor,
94
+ sesInboundReceive,
95
+ slackEvents,
96
+ slackDelivery,
97
+ storage,
98
+ });
99
+ const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
100
+ // Repo root in a Papyrus checkout; package root when installed from npm.
101
+ const projectRoot = resolve(amplifyBackendDir, "..");
102
+ const enableConsoleResponder = site.features?.consoleResponder ?? readEnvFlag("PAPYRUS_ENABLE_CONSOLE_RESPONDER", true);
103
+ if (enableConsoleResponder
104
+ && !process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI?.trim()
105
+ && !process.env.PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD?.trim()
106
+ && !isAmplifyProductionPipeline) {
107
+ process.env.PAPYRUS_CONSOLE_RESPONDER_ALLOW_LOCAL_BUILD = "true";
108
+ }
109
+ const inboundEmailDomain = (process.env.PAPYRUS_INBOUND_EMAIL_DOMAIN ?? "p.apyr.us").trim().toLowerCase();
110
+ const inboundEmailLocalParts = (process.env.PAPYRUS_INBOUND_EMAIL_LOCAL_PARTS ?? "submissions,suggestions")
111
+ .split(",")
112
+ .map((entry) => entry.trim().toLowerCase())
113
+ .filter(Boolean);
114
+ const inboundEmailCorpusKey = (process.env.PAPYRUS_INBOUND_EMAIL_CORPUS_KEY ?? "AI-ML-research").trim();
115
+ const inboundDnsZoneId = (process.env.PAPYRUS_ROUTE53_HOSTED_ZONE_ID ?? "Z10285921B1G7MVRV06W9").trim();
116
+ const inboundDnsZoneName = (process.env.PAPYRUS_ROUTE53_HOSTED_ZONE_NAME ?? "apyr.us").trim().replace(/\.$/, "");
117
+ const inboundDnsRecordName = inboundEmailDomain.endsWith(`.${inboundDnsZoneName}`)
118
+ ? inboundEmailDomain.slice(0, -(inboundDnsZoneName.length + 1))
119
+ : inboundEmailDomain;
120
+ let slackEventsUrl;
121
+ if (enableConsoleResponder || enableSlackAgent) {
122
+ const messageTable = backend.data.resources.tables.Message;
123
+ const cfnTables = backend.data.resources.cfnResources.cfnTables;
124
+ const messageCfnTable = cfnTables.Message
125
+ ?? cfnTables.MessageTable
126
+ ?? Object.entries(cfnTables).find(([key]) => {
127
+ const normalized = key.toLowerCase();
128
+ return normalized.includes("message") && !normalized.includes("thread");
129
+ })?.[1];
130
+ if (messageCfnTable) {
131
+ messageCfnTable.streamSpecification = {
132
+ streamViewType: dynamodb.StreamViewType.NEW_IMAGE,
133
+ };
134
+ }
135
+ const messageStreamArn = messageTable.tableStreamArn ?? messageCfnTable?.attrStreamArn;
136
+ if (!messageStreamArn) {
137
+ throw new Error(`Message agents require Message table stream ARN. cfnTables=${Object.keys(cfnTables).join(",")}`);
138
+ }
139
+ const messageThreadTable = backend.data.resources.tables.MessageThread;
140
+ const dataStack = Stack.of(messageTable);
141
+ 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
+ || "";
152
+ if (enableConsoleResponder) {
153
+ const consoleChatResponder = new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
154
+ messageTable,
155
+ messageStreamArn,
156
+ threadTable: messageThreadTable,
157
+ projectRoot,
158
+ graphqlEndpoint,
159
+ amplifySsmEnvConfig: jwtSsmEnvConfig || undefined,
160
+ responseTarget: process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET,
161
+ model: process.env.PAPYRUS_CONSOLE_MODEL,
162
+ prebuiltImageUri: process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI,
163
+ });
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
+ }
173
+ if (enableSlackAgent) {
174
+ const slackEventsLambda = backend.slackEvents.resources.lambda;
175
+ const slackDeliveryLambda = backend.slackDelivery.resources.lambda;
176
+ slackEventsLambda.addToRolePolicy(new PolicyStatement({
177
+ actions: ["appsync:GraphQL"],
178
+ resources: ["*"],
179
+ }));
180
+ backend.slackEvents.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
181
+ backend.slackEvents.addEnvironment("PAPYRUS_CONSOLE_RESPONSE_TARGET", (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud");
182
+ 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);
186
+ }
187
+ backend.slackDelivery.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
188
+ backend.slackDelivery.addEnvironment("PAPYRUS_CONSOLE_RESPONSE_TARGET", (process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET ?? "cloud").trim() || "cloud");
189
+ const slackBotTokenName = (process.env.PAPYRUS_SLACK_BOT_TOKEN_NAME ?? "PAPYRUS_SLACK_BOT_TOKEN").trim();
190
+ backend.slackDelivery.addEnvironment("PAPYRUS_SLACK_BOT_TOKEN", secret(slackBotTokenName));
191
+ const slackStack = Stack.of(slackEventsLambda);
192
+ const slackSsmResources = amplifyAppId
193
+ ? [
194
+ `arn:aws:ssm:${slackStack.region}:${slackStack.account}:parameter/amplify/${amplifyAppId}/*`,
195
+ `arn:aws:ssm:${slackStack.region}:${slackStack.account}:parameter/amplify/shared/${amplifyAppId}/*`,
196
+ ]
197
+ : [`arn:aws:ssm:${slackStack.region}:${slackStack.account}:parameter/amplify/*`];
198
+ const slackSsmPolicy = new PolicyStatement({
199
+ effect: Effect.ALLOW,
200
+ actions: ["ssm:GetParameter", "ssm:GetParameters"],
201
+ resources: slackSsmResources,
202
+ });
203
+ slackEventsLambda.addToRolePolicy(slackSsmPolicy);
204
+ slackDeliveryLambda.addToRolePolicy(slackSsmPolicy);
205
+ // Wire Slack on the data stack (not a nested SlackAgent stack) to avoid CloudFormation
206
+ // circular dependencies between Message and slack-delivery in the data resource group.
207
+ const slackEventsFunctionUrl = slackEventsLambda.addFunctionUrl({
208
+ authType: FunctionUrlAuthType.NONE,
209
+ });
210
+ slackEventsUrl = slackEventsFunctionUrl.url;
211
+ const slackConsumerStack = backend.createStack("slack-consumer");
212
+ new CfnEventSourceMapping(slackConsumerStack, "SlackDeliveryMessageStreamMapping", {
213
+ batchSize: 1,
214
+ eventSourceArn: messageStreamArn,
215
+ functionName: slackDeliveryLambda.functionArn,
216
+ functionResponseTypes: ["ReportBatchItemFailures"],
217
+ maximumRetryAttempts: 2,
218
+ startingPosition: "LATEST",
219
+ });
220
+ slackDeliveryLambda.addToRolePolicy(new PolicyStatement({
221
+ effect: Effect.ALLOW,
222
+ actions: [
223
+ "dynamodb:DescribeStream",
224
+ "dynamodb:GetRecords",
225
+ "dynamodb:GetShardIterator",
226
+ "dynamodb:ListStreams",
227
+ ],
228
+ resources: [messageStreamArn],
229
+ }));
230
+ slackDeliveryLambda.addToRolePolicy(new PolicyStatement({
231
+ effect: Effect.ALLOW,
232
+ actions: ["dynamodb:GetItem", "dynamodb:Query", "dynamodb:BatchGetItem"],
233
+ resources: [messageTable.tableArn, `${messageTable.tableArn}/index/*`],
234
+ }));
235
+ slackDeliveryLambda.addToRolePolicy(new PolicyStatement({
236
+ effect: Effect.ALLOW,
237
+ actions: ["appsync:GraphQL"],
238
+ resources: ["*"],
239
+ }));
240
+ }
241
+ }
242
+ const storageBucket = backend.storage.resources.bucket;
243
+ const storageStack = Stack.of(storageBucket);
244
+ let storageBackupPlan;
245
+ let storageBackupVault;
246
+ const storageBucketCfn = storageBucket.node.defaultChild;
247
+ if ((enableStorageBackups || enableInboundEmail) && storageBucketCfn) {
248
+ // Backups and inbound-email intake both rely on S3 → EventBridge notifications.
249
+ storageBucketCfn.addPropertyOverride("NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled", true);
250
+ }
251
+ if (enableStorageBackups) {
252
+ 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", {
258
+ 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
+ });
278
+ }
279
+ // Grant S3 access on Lambda roles only (not storage bucket policies) to avoid
280
+ // storage ↔ data nested-stack circular dependencies from allow.resource().
281
+ const corporaObjectArn = `${storageBucket.bucketArn}/corpora/*`;
282
+ const mediaObjectArn = `${storageBucket.bucketArn}/media/*`;
283
+ const newsroomObjectArn = `${storageBucket.bucketArn}/newsroom/*`;
284
+ const grantCorporaRead = (lambda) => {
285
+ lambda.addToRolePolicy(new PolicyStatement({
286
+ actions: ["s3:GetObject"],
287
+ resources: [corporaObjectArn],
288
+ }));
289
+ };
290
+ const grantNewsroomReadWrite = (lambda) => {
291
+ lambda.addToRolePolicy(new PolicyStatement({
292
+ actions: ["s3:GetObject", "s3:PutObject"],
293
+ resources: [newsroomObjectArn],
294
+ }));
295
+ };
296
+ const grantNewsroomReadWriteDelete = (lambda) => {
297
+ lambda.addToRolePolicy(new PolicyStatement({
298
+ actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
299
+ resources: [newsroomObjectArn],
300
+ }));
301
+ };
302
+ const grantMediaReadWriteDelete = (lambda) => {
303
+ lambda.addToRolePolicy(new PolicyStatement({
304
+ actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
305
+ resources: [mediaObjectArn],
306
+ }));
307
+ };
308
+ const mediaBucketName = storageBucket.bucketName;
309
+ const withMediaBucketEnv = (resource) => {
310
+ resource.addEnvironment("PAPYRUS_MEDIA_BUCKET_NAME", mediaBucketName);
311
+ resource.addEnvironment("papyrusMedia_BUCKET_NAME", mediaBucketName);
312
+ };
313
+ for (const resource of [
314
+ backend.assignmentAction,
315
+ backend.categoryAction,
316
+ backend.contentActions,
317
+ backend.emailSubmissionProcessor,
318
+ backend.knowledgeQuery,
319
+ backend.modelAttachmentUpload,
320
+ backend.newsroomSummary,
321
+ backend.procedureAction,
322
+ backend.sesInboundReceive,
323
+ ]) {
324
+ withMediaBucketEnv(resource);
325
+ }
326
+ grantCorporaRead(backend.knowledgeQuery.resources.lambda);
327
+ grantNewsroomReadWrite(backend.assignmentAction.resources.lambda);
328
+ grantNewsroomReadWrite(backend.categoryAction.resources.lambda);
329
+ grantNewsroomReadWrite(backend.newsroomSummary.resources.lambda);
330
+ const contentActionsLambda = backend.contentActions.resources.lambda;
331
+ grantNewsroomReadWriteDelete(contentActionsLambda);
332
+ contentActionsLambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl);
333
+ contentActionsLambda.addToRolePolicy(new PolicyStatement({
334
+ actions: ["appsync:GraphQL"],
335
+ resources: ["*"],
336
+ }));
337
+ const rebuildSettings = rebuildTriggerSettings({
338
+ reader: site.reader,
339
+ stagingBuildEnabled: site.stagingBuild?.enabled === true,
340
+ cmsAppId: amplifyAppId,
341
+ region: Stack.of(contentActionsLambda).region,
342
+ account: Stack.of(contentActionsLambda).account,
343
+ });
344
+ for (const [name, value] of Object.entries(rebuildSettings.environment)) {
345
+ contentActionsLambda.addEnvironment(name, value);
346
+ }
347
+ if (rebuildSettings.resources.length > 0) {
348
+ contentActionsLambda.addToRolePolicy(new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }));
349
+ }
350
+ if ((site.revalidateBaseUrl ?? "").trim() !== "") {
351
+ backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
352
+ backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET", secret("PAPYRUS_REVALIDATE_SECRET"));
353
+ }
354
+ grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
355
+ grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
356
+ if (backend.sesInboundReceive) {
357
+ grantNewsroomReadWrite(backend.sesInboundReceive.resources.lambda);
358
+ }
359
+ if (enableInboundEmail) {
360
+ if (!backend.sesInboundReceive || !backend.emailSubmissionProcessor) {
361
+ throw new Error("Inbound email is enabled but sesInboundReceive/emailSubmissionProcessor were not registered.");
362
+ }
363
+ const inboundReceive = backend.sesInboundReceive;
364
+ const inboundProcessor = backend.emailSubmissionProcessor;
365
+ const receiveLambda = inboundReceive.resources.lambda;
366
+ const processorLambda = inboundProcessor.resources.lambda;
367
+ const graphqlEndpoint = backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl;
368
+ const inboundRecipients = inboundEmailLocalParts.map((localPart) => `${localPart}@${inboundEmailDomain}`);
369
+ inboundReceive.addEnvironment("PAPYRUS_EMAIL_SUBMISSION_PROCESSOR_FUNCTION_NAME", processorLambda.functionName);
370
+ inboundReceive.addEnvironment("PAPYRUS_INBOUND_EMAIL_DOMAIN", inboundEmailDomain);
371
+ inboundReceive.addEnvironment("PAPYRUS_INBOUND_EMAIL_LOCAL_PARTS", inboundEmailLocalParts.join(","));
372
+ inboundReceive.addEnvironment("PAPYRUS_INBOUND_EMAIL_CORPUS_KEY", inboundEmailCorpusKey);
373
+ inboundReceive.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
374
+ inboundProcessor.addEnvironment("PAPYRUS_INBOUND_EMAIL_CORPUS_KEY", inboundEmailCorpusKey);
375
+ inboundProcessor.addEnvironment("PAPYRUS_INBOUND_EMAIL_DOMAIN", inboundEmailDomain);
376
+ inboundProcessor.addEnvironment("PAPYRUS_INBOUND_FEEDBACK_EMAIL_ENABLED", "true");
377
+ inboundProcessor.addEnvironment("PAPYRUS_INBOUND_FEEDBACK_FROM_EMAIL", `Papyrus Submissions <submissions@${inboundEmailDomain}>`);
378
+ inboundProcessor.addEnvironment("PAPYRUS_PUBLIC_SITE_BASE_URL", "https://p.apyr.us");
379
+ inboundProcessor.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
380
+ receiveLambda.addToRolePolicy(new PolicyStatement({
381
+ actions: ["lambda:InvokeFunction"],
382
+ resources: [processorLambda.functionArn],
383
+ }));
384
+ receiveLambda.addToRolePolicy(new PolicyStatement({
385
+ actions: ["s3:GetObject", "s3:ListBucket"],
386
+ resources: [
387
+ storageBucket.bucketArn,
388
+ `${storageBucket.bucketArn}/inbound-email/*`,
389
+ ],
390
+ }));
391
+ processorLambda.addToRolePolicy(new PolicyStatement({
392
+ actions: ["s3:ListBucket"],
393
+ resources: [storageBucket.bucketArn],
394
+ }));
395
+ processorLambda.addToRolePolicy(new PolicyStatement({
396
+ actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
397
+ resources: [
398
+ `${storageBucket.bucketArn}/inbound-email/*`,
399
+ `${storageBucket.bucketArn}/inbound-email-archived/*`,
400
+ ],
401
+ }));
402
+ processorLambda.addToRolePolicy(new PolicyStatement({
403
+ actions: ["s3:GetObject", "s3:PutObject"],
404
+ resources: [corporaObjectArn],
405
+ }));
406
+ receiveLambda.addToRolePolicy(new PolicyStatement({
407
+ actions: ["appsync:GraphQL"],
408
+ resources: ["*"],
409
+ }));
410
+ processorLambda.addToRolePolicy(new PolicyStatement({
411
+ actions: ["appsync:GraphQL"],
412
+ resources: ["*"],
413
+ }));
414
+ processorLambda.addToRolePolicy(new PolicyStatement({
415
+ actions: ["ses:SendEmail", "ses:SendRawEmail"],
416
+ resources: ["*"],
417
+ }));
418
+ 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", {
457
+ 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),
496
+ });
497
+ }
498
+ new InboundEmailStack(inboundEventStack, "InboundEmail", {
499
+ storageBucket,
500
+ receiveFunctionArn: receiveLambda.functionArn,
501
+ });
502
+ }
503
+ const knowledgeVectorsStack = backend.createStack("knowledge-vectors");
504
+ const knowledgeVectorBucket = new CfnVectorBucket(knowledgeVectorsStack, "PapyrusKnowledgeVectorBucket", {
505
+ encryptionConfiguration: {
506
+ sseType: "AES256",
507
+ },
508
+ });
509
+ const knowledgeVectorIndex = new CfnIndex(knowledgeVectorsStack, "PapyrusKnowledgeVectorIndex", {
510
+ dataType: "float32",
511
+ dimension: knowledgeVectorDimension,
512
+ distanceMetric: "cosine",
513
+ indexName: knowledgeVectorIndexName,
514
+ metadataConfiguration: {
515
+ nonFilterableMetadataKeys: ["text", "summary", "sourceUri", "title"],
516
+ },
517
+ vectorBucketArn: knowledgeVectorBucket.attrVectorBucketArn,
518
+ });
519
+ knowledgeVectorIndex.node.addDependency(knowledgeVectorBucket);
520
+ const knowledgeVectorBucketPolicy = new CfnVectorBucketPolicy(knowledgeVectorsStack, "PapyrusKnowledgeVectorBucketPolicy", {
521
+ vectorBucketArn: knowledgeVectorBucket.attrVectorBucketArn,
522
+ policy: {
523
+ Version: "2012-10-17",
524
+ Statement: [
525
+ {
526
+ Sid: "AllowSameAccountPapyrusKnowledgeVectorAccess",
527
+ Effect: "Allow",
528
+ Principal: {
529
+ AWS: `arn:aws:iam::${knowledgeVectorsStack.account}:root`,
530
+ },
531
+ Action: [
532
+ "s3vectors:GetIndex",
533
+ "s3vectors:GetVectors",
534
+ "s3vectors:ListVectors",
535
+ "s3vectors:PutVectors",
536
+ "s3vectors:QueryVectors",
537
+ "s3vectors:DeleteVectors",
538
+ ],
539
+ Resource: [
540
+ knowledgeVectorBucket.attrVectorBucketArn,
541
+ knowledgeVectorIndex.attrIndexArn,
542
+ ],
543
+ },
544
+ ],
545
+ },
546
+ });
547
+ knowledgeVectorBucketPolicy.node.addDependency(knowledgeVectorIndex);
548
+ const knowledgeQueryLambda = backend.knowledgeQuery.resources.lambda;
549
+ backend.knowledgeQuery.addEnvironment("OPENAI_API_KEY", secret("OPENAI_API_KEY"));
550
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_STORAGE_BUCKET_NAME", storageBucket.bucketName);
551
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl);
552
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_S3_VECTOR_INDEX_ARN", knowledgeVectorIndex.attrIndexArn);
553
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_S3_VECTOR_INDEX_NAME", knowledgeVectorIndexName);
554
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_EMBEDDING_MODEL", knowledgeEmbeddingModel);
555
+ knowledgeQueryLambda.addEnvironment("PAPYRUS_EMBEDDING_DIMENSIONS", String(knowledgeVectorDimension));
556
+ knowledgeQueryLambda.addToRolePolicy(new PolicyStatement({
557
+ actions: [
558
+ "s3vectors:GetIndex",
559
+ "s3vectors:GetVectors",
560
+ "s3vectors:ListVectors",
561
+ "s3vectors:QueryVectors",
562
+ ],
563
+ resources: [knowledgeVectorIndex.attrIndexArn],
564
+ }));
565
+ knowledgeQueryLambda.addToRolePolicy(new PolicyStatement({
566
+ actions: ["ssm:GetParameter"],
567
+ resources: [
568
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/papyrus/*/OPENAI_API_KEY`,
569
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/shared/papyrus/OPENAI_API_KEY`,
570
+ // Pipeline-deployed apps store secrets under amplify/<appId>/... rather
571
+ // than the sandbox's amplify/papyrus/... path. Grant the app's own
572
+ // secret path so a second CMS app (e.g. pilobol-us) can read its key.
573
+ ...(amplifyAppId
574
+ ? [
575
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/${amplifyAppId}/*/OPENAI_API_KEY`,
576
+ `arn:aws:ssm:${knowledgeVectorsStack.region}:${knowledgeVectorsStack.account}:parameter/amplify/shared/${amplifyAppId}/OPENAI_API_KEY`,
577
+ ]
578
+ : []),
579
+ ],
580
+ }));
581
+ backend.addOutput({
582
+ custom: {
583
+ inboundEmail: enableInboundEmail
584
+ ? {
585
+ domain: inboundEmailDomain,
586
+ localParts: inboundEmailLocalParts,
587
+ addresses: inboundEmailLocalParts.map((localPart) => `${localPart}@${inboundEmailDomain}`),
588
+ corpusKey: inboundEmailCorpusKey,
589
+ }
590
+ : null,
591
+ knowledgeQuery: {
592
+ s3VectorBucketArn: knowledgeVectorBucket.attrVectorBucketArn,
593
+ s3VectorIndexArn: knowledgeVectorIndex.attrIndexArn,
594
+ s3VectorIndexName: knowledgeVectorIndexName,
595
+ embeddingModel: knowledgeEmbeddingModel,
596
+ embeddingDimensions: knowledgeVectorDimension,
597
+ },
598
+ storageBackups: storageBackupPlan && storageBackupVault
599
+ ? {
600
+ backupPlanId: storageBackupPlan.backupPlanId,
601
+ backupVaultArn: storageBackupVault.backupVaultArn,
602
+ backupVaultName: storageBackupVault.backupVaultName,
603
+ protectedBucketArn: storageBucket.bucketArn,
604
+ protectedBucketName: storageBucket.bucketName,
605
+ }
606
+ : null,
607
+ slack: enableSlackAgent && slackEventsUrl
608
+ ? {
609
+ eventsUrl: slackEventsUrl,
610
+ }
611
+ : null,
612
+ },
613
+ });
614
+ return backend;
615
+ }